探索生成式 AI

生成式 AI,尤其是 LLM(大型语言模型)已经席卷了公众意识。像许多软件开发人员一样,我对它的可能性感到着迷,但我不确定从长远来看它对我们这个行业意味着什么。我现在在 Thoughtworks 担任了一个职位,负责协调我们关于这项技术将如何影响软件交付实践的工作。我将在这里发布各种备忘录,描述我和我的同事正在学习和思考的内容。

之前的备忘录

工具链(2023 年 7 月 26 日) 

工具链

让我们从工具链开始。每当出现一个新的领域,其模式和技术仍在不断发展时,我都会尝试建立一个关于事物如何组合在一起的心智模型。这有助于处理向我袭来的信息浪潮。该领域正在解决哪些类型的问题?解决这些问题需要哪些常见的拼图块?事物是如何组合在一起的?

如何对工具进行分类

以下是目前我对使用 LLM(大型语言模型)来支持编码的工具的心智模型的维度。

辅助任务

  • 更快地查找信息,并在上下文中查找信息
  • 生成代码
  • 关于代码的“推理”(解释代码或代码中的问题)
  • 将代码转换为其他内容(例如文档文本或图表)

这些是我在编码辅助方面最常遇到的任务类型,尽管如果我将范围扩展到软件交付生命周期中的其他任务,就会有更多任务。

交互模式

我看到了三种主要的交互模式

  • 聊天界面
  • 内联辅助,即在代码编辑器中键入
  • CLI

提示组成

提示的质量显然对工具的实用性有很大影响,这与后端使用的 LLM 的适用性相结合。提示工程并不一定完全留给用户,许多工具在后端为您应用提示技术。

  • 用户从头开始创建提示
  • 工具根据用户输入和附加上下文(例如打开的文件、一组可重复使用的上下文片段或对用户的附加问题)来组成提示

模型的属性

  • 模型的训练内容
    • 它是专门用代码和编码任务训练的吗?哪些语言?
    • 它是在什么时候训练的,即信息有多新
  • 模型的大小(尽管人们仍在争论这方面的重要性,以及对于像编码这样的特定任务来说,什么才是“好的”大小)
  • 模型支持的上下文窗口的大小,基本上是它可以作为提示接受的令牌数量
  • 模型或托管它的后端添加了哪些过滤器

来源和托管

  • 商业产品,使用产品公司托管的 LLM API
  • 开源工具,连接到 LLM API 服务
  • 自建工具,连接到 LLM API 服务
  • 自建工具连接到微调的、自托管的 LLM API

示例

以下是一些该领域常见的工具示例,以及它们如何融入这个模型。(该列表并非对这些工具的认可,也并非对其他工具的否定,它只是为了帮助说明这些维度。)

工具 任务 交互 提示组成 模型 来源/托管
GitHub Copilot 代码生成 内联辅助 由 IDE 扩展组成 用代码训练,漏洞过滤器 商业
GitHub Copilot Chat 所有 聊天 由用户聊天 + 打开的文件组成 用代码训练 商业
ChatGPT 所有 聊天 所有由用户完成 用代码训练 商业
GPT 工程师 代码生成 CLI 根据用户输入组成提示 选择 OpenAI 模型 开源,连接到 OpenAI API
“团队 AI” 所有 Web UI 根据用户输入和用例组成提示 最常使用 OpenAI 的 GPT 模型 由团队维护以满足其用例,连接到 OpenAI API
Meta 的 CodeCompose 代码生成 内联辅助 由编辑器扩展组成 模型在内部用例和代码库上进行了微调 自托管

人们今天在使用什么,接下来会发生什么?

如今,人们最常使用直接聊天交互(例如通过 ChatGPT 或 Copilot Chat)与代码编辑器中的编码辅助(例如通过 GitHub Copilot 或 Tabnine)的组合。与其他方法相比,在编辑器上下文中进行内联辅助可能是当今使用 LLM 进行编码辅助最成熟、最有效的方式。它以小步骤支持开发人员在他们的自然工作流程中。较小的步骤使跟踪和更认真地审查质量变得更容易,并且在它不起作用的情况下,很容易继续前进。

开源世界正在进行大量实验,使用提供提示组合来生成更大代码片段的工具(例如 GPT Engineer、Aider)。我看到了类似的小型提示组合应用程序的使用,这些应用程序由团队针对其特定用例进行了调整,例如,通过将可重复使用的架构和技术堆栈定义与用户故事相结合来生成任务计划或测试代码,类似于我的同事徐浩在 这里 描述的那样。像这样的提示组合应用程序今天最常与 OpenAI 的模型一起使用,因为它们最容易获得并且功能相对强大。然而,随着人们寻求对数据的更多控制,实验正越来越多地转向开源模型和大型超大规模托管模型。

作为下一步,除了高级提示组合之外,人们对模型组件的未来改进寄予厚望。对于编码辅助来说,更大的模型还是更小但经过更专门训练的模型效果更好?具有更大上下文窗口的模型是否能够让我们向它们提供更多代码来推理我们代码库更大部分的质量和架构?在什么规模上,用组织的代码微调模型才会有所回报?开源模型领域会发生什么?这些问题将在未来的备忘录中探讨。

感谢 Kiran Prakash 的意见


中位数 - 三个函数的故事(2023 年 7 月 27 日) 

中位数 - 三个函数的故事

这是一个关于生成中位数函数的小故事,它说明了 LLM 辅助编码的用处和局限性。

我需要在 Typescript 代码库中计算数字列表的中位数。我没有像往常一样开始在互联网上搜索“中位数函数 javascript”,而是尝试让 GitHub Copilot 帮助我。

// calculate the median of a list of numbers

Copilot 首先为我生成了一个合适的函数签名

public static median(values:number[]): number {

然后建议了该函数的三个不同的实现。

建议 1

public static median(values:number[]): number {
    if(values.length === 0) return 0;

    values.sort((a,b) => a-b);

    var half = Math.floor(values.length / 2);

    if (values.length % 2)
    return values[half];

    return (values[half - 1] + values[half]) / 2.0;
}

此实现工作正常。但是,输入参数 values 正在使用 sort 函数进行更改。当向一些同事展示这个例子时,我得到了不同的反应 - 一个人说“这很糟糕,我们通常会忘记写一个关于它的测试,然后它可能会变成一个非常难以调试的错误”。另一个人说,“这并不一定总是坏事,这取决于情况”。我想知道这是否会是 IDE 中的静态代码分析工具会标记为问题的东西,但 ESLint 和 Sonar 都没有标记它。

建议 2

function median2(values: number[]) {
    const sorted = values.slice().sort((a, b) => a - b);
    const middle = Math.floor(sorted.length / 2);
    if (sorted.length % 2 === 0) {
        return (sorted[middle - 1] + sorted[middle]) / 2;
    } else {
        return sorted[middle];
    }
}

这个可以工作,并且没有更改输入参数的问题,因为 slice() 返回输入数组的副本。

建议 3

function median3(values: number[]) {
    const sorted = values.slice().sort((a, b) => a - b);
    const middle = Math.floor(sorted.length / 2);
    return sorted[middle];
}

乍一看,这个看起来比其他两个更好 - 它更短,并且循环复杂度为零,我们倾向于认为这更“优雅”。我为它编写的测试也通过了,太好了!

但我后来意识到,用偶数和奇数个值对其进行测试很重要 - 我编写的测试使用了奇数长度的输入,但此实现对于偶数长度的情况会失败。这个例子很好地说明了我们仍然需要了解我们在做什么,才能为生成的代码编写合理的测试。我们需要知道中位数是如何工作的,才能编写涵盖完整功能的测试。

这和从互联网上复制有什么不同吗?

这样的用例是人们在“自动完成增强版”中看到的好处之一的完美示例:您不必离开您的流程和工具链就能获得此类问题的答案。否则,我们无论如何都会从某个地方复制粘贴它,然后也必须彻底审查该代码,并编写测试。所以风险是一样的 - 对吧?

唯一的区别是,使用 Copilot,我们不知道代码的来源。例如,在 StackOverflow 的情况下,我们有一个关于代码片段质量的额外数据点:点赞数。

顺便说一下,“建议 1”几乎与 关于该主题的 StackOverflow 问题的投票最高的回复 中建议的代码完全相同,尽管存在一个小缺陷。但是,用户在评论中指出了输入参数的变异。

生成测试还是代码?或者两者都生成?

那么反过来呢,如果我先让 Copilot 为我生成测试会怎么样?我用 Copilot Chat 尝试了,它给了我一套非常好的测试,包括一个对于“建议 3”在偶数长度情况下会失败的测试。

it("should return the median of an array of odd length", () => { ... }

it("should return the median of an array of even length", () => { ... }

it("should return the median of an array with negative numbers", () => { ... }

it("should return the median of an array with duplicate values", () => { ... }

在这种特定情况下,对于像中位数这样非常常见且很小的函数,我甚至会考虑使用生成的代码来生成测试和函数。这些测试非常易读,我很容易理解它们的覆盖范围,而且它们会帮助我记住我需要查看偶数和奇数长度的输入。但是,对于其他更复杂的函数,这些函数包含更多自定义代码,我会考虑自己编写测试,作为一种质量控制手段。特别是对于较大的函数,我希望从头开始以结构化的方式思考我的测试用例,而不是从工具中获得部分场景,然后不得不填补缺失的场景。

工具本身可以帮助我修复生成的代码中的缺陷吗?

我让 Copilot Chat 以不更改输入参数的方式重构“建议 1”,它给了我一个合理的修复。但是,这个问题意味着我已经知道我想改进代码中的什么。

我还问 ChatGPT,“建议 3”有什么问题或可以改进的地方,更广泛地说。它确实告诉我它不适用于偶数长度的输入。

结论

  • 您必须知道自己在做什么,才能判断生成的建议。在这种情况下,我需要了解中位数计算的工作原理,才能为生成的代码编写合理的测试。
  • 工具本身可能知道生成的代码中有什么问题或可以改进的地方 - 这是否是一条让它在未来变得更好的道路,还是我们注定要与我们的 AI 工具进行循环对话?
  • 我一直对生成测试和实现持怀疑态度,出于质量控制的原因。但是,生成测试可以让我对遗漏的测试场景有所了解,即使我之后丢弃了代码。而且,根据函数的复杂程度,我可能会考虑使用生成的测试,如果它很容易理解这些场景。

感谢 Aleksei Bekh-Ivanov 和 Erik Doernenburg 的见解


内联辅助 - 什么时候更有用?(2023 年 8 月 1 日) 

内联辅助 - 什么时候更有用?

目前 Thoughtworks 中最常用的编码辅助形式是在 IDE 中进行内联代码生成,其中 IDE 扩展会在开发人员在 IDE 中输入时生成建议。

对于“这有用吗?”这个问题,简短的答案是:“有时有用,有时没用。” ¯_(ツ)_/¯ 您会在互联网上找到各种各样的开发者意见,从“这让我快多了”到“我把它关掉了,它没用”。这是因为这些工具的实用性取决于具体情况。而对实用性的判断取决于您对它们的期望有多高。

我所说的“有用”是什么意思?

在本备忘录中,我将“有用”定义为“生成的建议帮助我比没有使用该工具更快地解决问题,并且质量相当”。这不仅包括代码的编写,还包括对生成的建议的审查和调整,以及在出现质量问题时处理后续的返工。

影响建议实用性的因素

注意:这主要基于使用 GitHub Copilot 的经验。

更流行的技术栈

安全水域:技术栈越流行,模型的训练数据中就包含越多的讨论和代码示例。这意味着对于 Java 或 Javascript 等语言,生成的建议更有可能有用,而对于 Lua 等较新或讨论较少的语言则不然。

然而:我的同事 Erik Doernenburg 写了一篇关于他使用 Rust “将 Copilot 带到困难的地形” 的经验。他的结论是:“总的来说,即使对于像 Rust 这样不太常见的编程语言,以及使用更复杂数据结构的代码库,我也发现 Copilot 很有用。”

更简单和更常见的问题

安全水域:这个有点难定义。什么是“更简单”,什么是“更常见”?我将用一些例子来说明。

  • 常见问题:在之前的 备忘录 中,我讨论了生成中位数函数的例子。我认为这是一个非常常见的问题,因此是生成的好用例。
  • 应用于我们上下文的常见解决方案模式:例如,我成功地使用它来实现需要列表处理的问题,比如对列表进行映射、分组和排序的链。
  • 样板代码:创建样板代码设置,比如 ExpressJS 服务器、React 组件或数据库连接和查询执行。
  • 重复模式:它有助于加快对具有非常常见和重复模式的事物的输入速度,比如创建新的构造函数或数据结构,或者在测试套件中重复测试设置。我通常使用大量的复制粘贴来完成这些事情,而 Copilot 可以加快速度。

一位使用 Copilot 超过 2 个月的同事在与没有许可证的人配对时,“发现不得不手动编写重复代码令人痛苦”。然而,对于已经非常擅长使用 IDE 功能、快捷键和多光标模式等功能的开发人员来说,这种“超级自动完成”的效果可能不太有用。请注意,当编码助手减少了重复代码的痛苦时,我们可能不太愿意进行重构。

然而:当您开始处理更复杂的问题时,即使您随后丢弃了建议,也可以使用编码助手来探索一些想法。

建议的规模更小

安全水域:生成的建议越小,需要的审查工作就越少,开发人员就越容易跟上建议的内容。

建议越大,您需要花在理解它上的时间就越多,您需要更改它以适应您的上下文的机会也越大。较大的代码片段也会诱惑我们以更大的步骤进行操作,这会增加遗漏测试覆盖率或引入不必要事物的风险。

然而:我怀疑这个因素与其他因素之间存在很多相互作用。当您已经知道如何解决问题时,小步骤特别有用。因此,当您还没有计划,因为您经验不足,或者问题更复杂时,更大的代码片段可能有助于您开始制定计划。

更经验丰富的开发人员

安全水域:经验仍然很重要。开发人员的经验越丰富,他们就越有可能判断建议的质量,并能够有效地使用它们。正如 GitHub 自己所说:“它擅长你忘记的东西。” 这项 研究 甚至发现,“在某些情况下,使用这些工具的任务比不使用这些工具的任务要花费初级开发人员多 7% 到 10% 的时间”。

然而:到目前为止,我收集到的观察结果大多来自经验丰富的开发人员。因此,这是我目前对其中所起作用的权衡最不确定的一个方面。我的假设是,从上面提到的其他因素来看,水域越安全,这些工具就越不可能导致经验不足的开发人员走上错误的道路,并且他们获得帮助的机会就越大。结对编程和其他形式的代码审查进一步降低了风险。

更高的错误容忍度

我已经提到了能够判断建议的质量和正确性的重要性。正如广泛报道的那样,大型语言模型可以“幻觉”信息,或者在这种情况下,可以“幻觉”代码。当您正在处理一个问题或用例,而您出错时会产生更大的影响时,您需要特别注意审查建议。例如,当我最近在处理 Web 应用程序中的 cookie 安全问题时,Copilot 建议了一个用于 Content-Security-Policy HTTP 标头的值。由于我在这个领域经验不足,而且这是一个与安全相关的用例,我不想仅仅接受 Copilot 的建议,而是转而到一个值得信赖的在线资源进行研究。

结论

编码辅助有更安全的水域,但正如您从这场讨论中看到的,有许多因素在起作用,并相互影响,决定了它的实用性。有效地使用编码辅助工具是一项技能,它不仅仅是从培训课程或博客文章中学到的。重要的是要使用它们一段时间,在安全水域内外进行实验,并培养一种感觉,即这种工具何时对您有用,以及何时应该继续前进并自己动手。

感谢 James Emmott、Joern Dinkla、Marco Pierobon、Paolo Carrasco、Paul Sobocinski 和 Serj Krasnov 提供的见解和反馈


内联辅助 - 它如何妨碍我们? (2023 年 8 月 3 日) 

内联辅助 - 它如何妨碍我们?

在之前的备忘录中,我谈到了编码辅助在哪些情况下可能有用。本备忘录是一篇二合一文章:这里介绍了我们注意到这些工具可能妨碍我们的两种方式。

放大不良或过时的实践

IDE 中的编码助手的优势之一是,它们可以使用周围代码库的代码片段来增强提示,并提供额外的上下文。我们发现,在编辑器中打开正确的文件以增强提示,对于提高建议的实用性是一个相当大的因素。

然而,这些工具无法区分好的代码和坏的代码。它们会将任何看起来相关的代码注入到上下文中。(根据这项 逆向工程工作,GitHub Copilot 会寻找使用相同编程语言的打开文件,并使用一些启发式方法来找到类似的代码片段以添加到提示中。)因此,编码助手可能会变成团队中那个不断从代码库中的不良示例中复制代码的开发人员。

我们还发现,在重构接口或将新的模式引入代码库之后,助手可能会停留在旧的方式上。例如,团队可能希望引入一种新的模式,比如“开始使用工厂模式进行依赖注入”,但该工具仍然建议当前的依赖注入方式,因为这种方式仍然在整个代码库和打开的文件中普遍存在。我们称之为中毒上下文,目前还没有很好的方法来缓解这个问题。

结论

人工智能渴望用我们的代码库来改进提示上下文,这既是福也是祸。这是开发人员不应该过分信任生成的代码,而应该继续审查并自己思考的众多原因之一。

审查疲劳和自满

使用编码助手意味着要一遍又一遍地进行小的代码审查。通常,当我们编写代码时,我们的流程更多地是关于主动编写代码,以及在脑海中实现解决方案计划。现在,这被阅读和审查代码所打断,这在认知上是不同的,而且大多数人也不像主动编写代码那样喜欢它。这会导致审查疲劳,以及一种感觉,即助手带来的流程中断比增强更多。一些开发人员可能会暂时关闭该工具,以从这种疲劳中解脱出来。或者,如果我们不处理这种疲劳,我们可能会在审查代码时变得马虎和自满。

审查自满也可能是许多认知偏差的结果

  • 自动化偏差 是我们“倾向于偏爱来自自动化系统的建议,并忽略没有自动化而做出的矛盾信息,即使它是正确的”。一旦我们对 GenAI 助手有了良好的体验和成功,我们可能会开始过分信任它们。
  • 当我和人工智能编码助手一起工作时,我也经常感觉到一种扭曲的沉没成本谬误。沉没成本谬误被定义为“一旦在金钱、努力或时间上投入了投资,就更有可能继续进行一项事业”。在这种情况下,我们并没有真正投入时间,相反,我们节省了时间。但是,一旦我们从工具中获得了多行代码建议,花 20 分钟让这个建议起作用,可能比看到这个建议不太正确后花 5 分钟自己编写代码更合理。
  • 一旦我们看到了代码建议,就很难不去看它,我们也更难想到其他解决方案。这是因为锚定效应,当“个人的决定受到特定参考点或‘锚’的影响”时就会发生这种情况。因此,虽然编码助手的建议在我们还不知道如何解决问题时可以很好地用于头脑风暴,但在头脑风暴没有结果,我们需要重新开始思考时,了解锚定效应很重要。

结论

有时,从助手那里休息一下是可以的。我们必须小心,不要成为那种仅仅因为导航系统告诉他们要这样做,就把车开进湖里的人。

感谢 Thoughtworks 加拿大团队的“与 Copilot 集成”小组,他们向我描述了“上下文中毒”效应和审查疲劳:Eren、Geet、Nenad、Om、Rishi、Janice、Vivian、Yada 和 Zack

感谢 Bruno、Chris、Gabriel、Javier 和 Roselma 对本备忘录的审查意见


编码助手不能取代结对编程 (2023 年 8 月 10 日) 

编码助手不能取代结对编程

正如之前的备忘录希望表明的那样,我发现 GenAI 驱动的编码助手是开发人员工具链中非常有用的补充。它们显然可以在某些情况下加快代码编写速度,帮助我们摆脱困境,并更快地记住和查找信息。到目前为止,所有备忘录都主要讨论 IDE 中的内联辅助,但如果我们在此基础上添加聊天机器人接口,那么还有更多有用的辅助潜力。特别强大的是与 IDE 集成的聊天接口,它通过我们不必在提示中详细说明的代码库的额外上下文得到增强。

然而,虽然我看到了潜力,但我坦白地说,当人们谈论编码助手作为结对编程的替代品时,我感到非常沮丧(GitHub 甚至将他们的 Copilot 产品称为“你的 AI 结对程序员”)。在 Thoughtworks,我们长期以来一直是结对编程和一般配对的坚定支持者,以使团队更有效。它是我们“明智的默认实践”的一部分,我们将其用作项目的起点。

将编码助手描述为结对程序员是对这种实践的误解,并强化了人们对配对益处的普遍简化理解和误解。我回到了我用来谈论配对的一组幻灯片,以及发布在本网站上的综合文章,并将我提到的所有好处都压缩到一张幻灯片中。

Title: Benefits and purposes of pair programming, in 5 categories: 1. “one plus one is greater than two”, for things like knowledge exchange or onboarding; 2. Flow, for things like keeping focus and limiting work in process; 3. Avoid waste, referencing the 7 wastes of software development; 4. Continuous Integration, as in integrating multiple times a day, mentioning shorter code review loops; and 5., Practice skills needed on highly effective teams, like task organisation, empathy, communication, feedback

编码助手在这里可以产生最明显影响的领域是第一个,“1 加 1 大于 2”。它们可以帮助我们摆脱困境,可以使入职更好,并且可以帮助战术工作更快,这样我们就可以更多地专注于战略,即整体解决方案的设计。它们还有助于知识共享,例如“这项技术是如何工作的?”。

然而,结对编程也是关于创建集体代码所有权和对代码库历史的共同了解的知识共享类型。它是关于分享任何地方都没有写下来的隐性知识,因此大型语言模型也无法获得。配对也是关于改善团队流程、避免浪费以及使持续集成更容易。它帮助我们练习沟通、同理心以及给予和接受反馈等协作技能。它还为远程优先团队提供了宝贵的相互联系的机会。

结论

编码助手只能涵盖结对编程目标和益处的一小部分。这是因为配对是一种有助于改善整个团队的实践,而不仅仅是一个单独的编码员。如果做得好的话,沟通和协作水平的提高将改善流程和集体代码所有权。我甚至会说,通过在结对中使用这些工具,可以最好地减轻 LLM 辅助编码的风险(参见先前备忘录中的“如何阻碍”)。

使用编码助手使结对更好,而不是取代结对。


使用 GitHub Copilot 进行 TDD(2023 年 8 月 17 日) 

使用 GitHub Copilot 进行 TDD

作者:Paul Sobocinski

GitHub Copilot 等 AI 编码助手的出现是否意味着我们不再需要测试?TDD 会过时吗?为了回答这个问题,让我们考察 TDD 如何帮助软件开发的两种方式:提供良好的反馈,以及解决问题时的“分而治之”方法。

用于良好反馈的 TDD

良好的反馈速度快且准确。在这两方面,没有什么比从编写良好的单元测试开始更好。不是手动测试,不是文档,不是代码审查,是的,甚至不是生成式 AI。事实上,LLM 提供不相关的信息,甚至会产生幻觉。在使用 AI 编码助手时,尤其需要 TDD。出于我们对编写的代码需要快速准确的反馈的原因,我们也需要对 AI 编码助手编写的代码进行快速准确的反馈。

使用 TDD 分而治之解决问题

通过分而治之解决问题意味着较小的问题可以比较大的问题更快地解决。这使得持续集成、基于主干的开发以及最终的持续交付成为可能。但是,如果 AI 助手为我们编写代码,我们真的需要所有这些吗?

是的。LLM 很少在一次提示后提供我们需要的精确功能。因此,迭代开发还没有消失。此外,LLM 似乎在通过思维链提示逐步解决问题时“引发推理”(参见链接研究)。基于 LLM 的 AI 编码助手在分而治之解决问题时表现最佳,而 TDD 是我们用于软件开发的方法。

GitHub Copilot 的 TDD 提示

在 Thoughtworks,我们从今年年初就开始将 GitHub Copilot 与 TDD 结合使用。我们的目标是尝试、评估和改进围绕工具使用的一系列有效实践。

0. 入门

TDD represented as a three-part wheel with 'Getting Started' highlighted in the center

从空白测试文件开始并不意味着从空白上下文开始。我们通常从用户故事开始,并附有一些粗略的笔记。我们还会与我们的配对伙伴一起讨论一个起点。

所有这些都是 Copilot 在我们将其放入打开的文件(例如,我们测试文件的顶部)之前“看不到”的上下文。Copilot 可以处理错别字、要点形式、语法错误——你想到的任何问题。但它无法处理空白文件。

我们已经使用过的起始上下文的一些示例

  • ASCII 艺术模型
  • 验收标准
  • 指导性假设,例如
    • “不需要 GUI”
    • “使用面向对象编程”(与函数式编程相比)

Copilot 使用打开的文件作为上下文,因此同时打开测试文件和实现文件(例如,并排)可以极大地提高 Copilot 的代码补全能力。

1. 红色

TDD represented as a three-part wheel with the 'Red' portion highlighted on the top left third

我们首先编写一个描述性的测试示例名称。名称越描述性,Copilot 的代码补全性能就越好。

我们发现Given-When-Then 结构在三个方面有所帮助。首先,它提醒我们要提供业务上下文。其次,它允许 Copilot 为测试示例提供丰富且富有表现力的命名建议。第三,它揭示了 Copilot 从文件顶部上下文(在上一节中描述)中对问题的“理解”。

例如,如果我们正在处理后端代码,而 Copilot 正在将我们的测试示例名称代码补全为“给定用户……点击购买按钮”,这告诉我们应该更新文件顶部的上下文以指定“假设没有 GUI”或“此测试套件与 Python Flask 应用程序的 API 端点交互”。

更多需要注意的“陷阱”

  • Copilot 可能会一次性代码补全多个测试。这些测试通常是无用的(我们会删除它们)。
  • 随着我们添加更多测试,Copilot 将代码补全多行,而不是一次补全一行。它通常会从测试名称中推断出正确的“安排”和“操作”步骤。
    • 这里有一个陷阱:它不太可能推断出正确的“断言”步骤,因此我们在这里要特别小心,确保新测试在进入“绿色”步骤之前正确失败

2. 绿色

TDD represented as a three-part wheel with the 'Green' portion highlighted on the top right third

现在我们准备让 Copilot 帮助我们进行实现。一个已经存在的、富有表现力和可读的测试套件可以最大限度地发挥 Copilot 在此步骤中的潜力。

话虽如此,Copilot 通常无法采取“小步”。例如,在添加新方法时,“小步”意味着返回一个通过测试的硬编码值。到目前为止,我们还没有能够诱使 Copilot 采取这种方法。

回填测试

Copilot 并没有采取“小步”,而是跳到前面,提供一些功能,这些功能虽然通常是相关的,但还没有经过测试。作为一种变通方法,我们“回填”了缺少的测试。虽然这偏离了标准的 TDD 流程,但我们还没有发现我们的变通方法有任何严重问题。

删除并重新生成

对于需要更新的实现代码,让 Copilot 参与的最有效方法是删除实现,并让它从头开始重新生成代码。如果失败,则可以删除方法内容,并使用代码注释写出逐步方法。如果失败,最好的前进方式可能是暂时关闭 Copilot,并手动编写解决方案。

3. 重构

TDD represented as a three-part wheel with the 'Refactor' portion highlighted on the bottom third

TDD 中的重构意味着进行增量更改,以提高代码库的可维护性和可扩展性,所有这些都在保持行为(以及正常工作的代码库)的同时进行。

为此,我们发现 Copilot 的能力有限。考虑两种情况

  1. “我知道我想尝试的重构操作”:IDE 重构快捷方式和功能(如多光标选择)可以比 Copilot 更快地让我们到达目的地。
  2. “我不知道要采取哪种重构操作”:Copilot 代码补全无法指导我们完成重构。但是,Copilot Chat 可以直接在 IDE 中提出代码改进建议。我们已经开始探索该功能,并看到了它在小范围内提出有用建议的潜力。但对于更大范围的重构建议(即超出单个方法/函数),我们还没有取得太多成功。

有时我们知道重构操作,但不知道执行该操作所需的语法。例如,创建一个测试模拟,允许我们注入依赖项。对于这些情况,Copilot 可以通过代码注释提示提供内联答案。这使我们免于切换到文档或网络搜索。

结论

俗话说,“垃圾进,垃圾出”,这适用于数据工程以及生成式 AI 和 LLM。换句话说:更高质量的输入可以更好地利用 LLM 的能力。在我们的例子中,TDD 保持了高水平的代码质量。这种高质量的输入导致了比其他方式更好的 Copilot 性能。

因此,我们建议将 Copilot 与 TDD 结合使用,我们希望您发现上述提示对这样做有所帮助。

感谢 Thoughtworks 加拿大启动的“与 Copilot 协同”团队;他们是本备忘录中涵盖的发现的主要来源:Om、Vivian、Nenad、Rishi、Zack、Eren、Janice、Yada、Geet 和 Matthew。


GenAI 与其他代码生成器有何不同?(2023 年 9 月 19 日) 

GenAI 与其他代码生成器有何不同?

在我职业生涯的早期,我在模型驱动开发 (MDD) 领域工作了很多。我们会想出一个建模语言来表示我们的领域或应用程序,然后用这种语言描述我们的需求,无论是图形化还是文本化(定制的 UML 或 DSL)。然后,我们会构建代码生成器将这些模型转换为代码,并在代码中留下指定区域,由开发人员进行实现和定制。

然而,这种代码生成方式从未真正流行起来,除了嵌入式开发的一些领域。我认为这是因为它处于一个尴尬的抽象级别,在大多数情况下,它并没有比其他抽象级别(如框架或平台)提供更好的成本效益比。

GenAI 代码生成有什么不同?

我们在软件工程工作中不断做出的一个关键决定是选择合适的抽象级别,以在实现工作量与我们用例所需的定制化和控制级别之间取得良好的平衡。作为一个行业,我们一直在努力提高抽象级别,以减少实现工作量,提高效率。但对于这一点,存在一种无形的力场,它受到我们所需控制级别的限制。以低代码平台为例:它们提高了抽象级别,减少了开发工作量,但结果是最适合某些类型简单直接的应用程序。一旦我们需要做一些更定制和复杂的事情,我们就会撞到力场,不得不再次降低抽象级别。

An illustration of typical abstraction levels in software, starting from 0s and 1s at the bottom, going up via programming languages, frameworks, platforms, and low code applications. A squiggly line at the top marks the "Invisible force field for customizability and extensibility".

GenAI 开辟了全新的潜力领域,因为它不是另一种试图打破这种力场的尝试。相反,它可以使我们人类在所有抽象级别上都更有效率,而无需正式定义结构化语言和翻译器,如编译器或代码生成器。

An illustration of typical abstraction levels in software, starting from 0s and 1s at the bottom, going up via programming languages, frameworks, platforms, and low code applications. A vertical box on the right marked "Natural Language" has arrows going into all of the abstraction levels, indicating that GenAI can help create all of those levels.

我们在抽象级别上应用 GenAI 越高,构建软件的总体工作量就越低。回到低代码示例,在该领域中有一些令人印象深刻的示例,它们展示了如何仅用几个提示就能构建完整的应用程序。但这与低代码抽象级别的相同限制有关,即您可以涵盖的用例。如果您的用例碰到了力场,并且您需要更多控制——您将不得不回到较低的抽象级别,并回到较小的可提示单元。

我们需要重新思考我们的抽象级别吗?

当我思考 GenAI 在软件工程中的潜力时,我会考虑自然语言提示与目标抽象级别之间的抽象距离。 我上面链接的 Google 的 AppSheet 演示 使用了一个非常高级的提示(“我需要创建一个应用程序来帮助我的团队跟踪旅行请求 […] 填写表格 […] 请求应发送给经理 […]”)来创建一个功能性的低代码应用程序。我们可以用类似的提示将目标级别向下推多少层才能获得相同的结果,例如使用 Spring 和 React 框架代码?或者,为了在 Spring 和 React 中获得相同的结果,提示需要详细到什么程度(以及抽象程度降低到什么程度)?

如果我们想更好地利用 GenAI 在软件工程中的潜力,也许我们需要彻底重新思考我们传统的抽象级别,为 GenAI 建立更“可提示”的距离。

感谢 John Hearn、John King、Kevin Bralten、Mike Mason 和 Paul Sobocinski 对这份备忘录的宝贵评论。


最新备忘录:如何解决编码助手的不稳定性

2023 年 11 月 29 日

编码助手的有用性与它的不可靠性之间存在权衡。底层模型非常通用,基于大量训练数据,这些数据与手头的任务相关或无关。此外,大型语言模型会编造东西,它们会“幻觉”,正如人们通常所说的那样。(旁注:关于“幻觉”这个词有很多讨论,关于 它实际上并不是描述这种情况的正确心理学隐喻,但也关于在第一位使用心理学术语,因为它将模型拟人化了。)

这种不可靠性会带来两个主要风险:它会对我的代码质量产生负面影响,也会浪费我的时间。鉴于这些风险,快速有效地评估我对编码助手输入的信心至关重要。

如何确定我对助手输入的信心

以下是一些我在尝试评估使用建议的可靠性和风险时通常会想到的问题。这适用于在键入代码时的“自动完成”建议,也适用于聊天中的答案。

我是否有快速反馈循环?

我越快能发现答案或生成的的信息是否有效,助手浪费我时间的风险就越低。

  • 我的 IDE 能否帮助我进行反馈循环?我是否有语法高亮、编译器或转译器集成、代码风格检查插件?
  • 我是否有测试,或者是否有快速手动运行建议代码的方法?有一次,我使用编码助手聊天来帮助我研究如何在 HTML 页面中最佳地显示可折叠的 JSON 数据结构。聊天告诉我一个我从未听说过的 HTML 元素,所以我不知道它是否存在。但把它放到一个 HTML 文件中并在浏览器中加载它很容易,从而确认了它的存在。举个反例,验证我从未听说过的基础设施代码的反馈循环通常要长得多。

我是否有可靠的反馈循环?

除了 AI 输入的反馈循环速度外,我还反思该反馈循环的可靠性。

  • 如果我有一个测试,我对该测试有多大信心?
  • 是我自己写的测试,还是我也用 AI 助手生成的?
  • 如果 AI 生成了测试,我对评估这些测试有效性的能力有多大信心?如果我正在编写的功能相对简单且常规,并且使用的是我熟悉的语言,那么我的信心当然比处理更复杂或更大的功能要高得多。
  • 我在使用助手时是否与某人结对?他们会对 AI 输入提供额外的输入和审查,并增加我的信心。
  • 如果我不确定我的测试覆盖率,我甚至可以使用助手本身来提高我的信心,并要求它提供更多边缘情况来测试。这就是我如何找到 我在之前备忘录中描述的中位数函数的关键缺失测试场景

误差范围是多少?

我还反思我正在做的事情的误差范围是多少。误差范围越低,我对 AI 输入的批判性就越高。

  • 当我引入一个新模式时,我认为这对代码库的整体设计来说是一个更大的爆炸半径。团队中的其他开发人员会采用这种模式,一旦它出现在代码中,编码助手也会在整个团队中复制这种模式。例如,我注意到在 CSS 中,GitHub Copilot 经常向我推荐 flexbox 布局。但是,选择布局方法是一个重大决定,因此在使用它之前,我想咨询一下前端专家和团队中的其他成员。
  • 与安全相关的任何事情当然都有很低的误差范围。例如,我正在开发一个 Web 应用程序,需要设置一个“Content-Security-Policy”标头。我对这个特定的标头一无所知,所以我首先询问了 Copilot 聊天。但由于主题的敏感性,我不想依赖它的答案,而是转而求助于互联网上可靠的安全信息来源。
  • 这段代码的寿命有多长?如果我正在开发原型或一次性代码,我更有可能在没有太多质疑的情况下使用 AI 输入,而不是在开发生产系统时。

我是否需要非常新的信息?

我需要的答案越新越具体(例如,针对框架的某个版本),它出错的风险就越高,因为我正在寻找的信息可能不可用或 AI 无法区分的概率更高。对于此评估,了解手头的 AI 工具是否可以访问除训练数据之外的更多信息也很重要。如果我使用聊天,我想知道它是否有能力考虑在线搜索,或者它是否仅限于训练数据。

给助手一个时间限制

为了降低浪费我时间的风险,我采取的一种方法是给它一种最后通牒。如果建议没有在付出少量额外努力的情况下为我带来价值,我会继续前进。如果输入不能很快帮助我,我总是假设助手最糟糕的情况,而不是抱有侥幸心理,并在它上面花费 20 分钟以上的时间来使其工作。

我想到的一个例子是,当我使用 AI 聊天来帮助我生成 mermaid.js 类图时。我对 mermaid.js 语法不太熟悉,我一直试图让建议起作用,并认为我可能在 markdown 文件中以错误的方式包含了它。事实证明,语法完全错误,我在大约 10 分钟后终于去查看了在线文档才发现这一点。

为助手创建一个角色

在准备这份备忘录时,我开始思考为助手创建一个角色是否可以帮助我们负责任地使用它,并尽可能减少时间浪费。也许将 AI 拟人化在这种情况下实际上会有所帮助?

考虑到各种不可靠性,我会想象 AI 角色具有以下特征

  • 渴望帮助
  • 固执
  • 博览群书,但缺乏经验(对于龙与地下城爱好者来说:智力很高,智慧很低)
  • 不会承认它“不知道”某件事

我尝试了几个提示,使用图像生成器,要求它生成各种渴望的河狸和固执的驴子。这是我最喜欢的那个(“渴望固执的驴子快乐书籍电脑;卡通,基于矢量,颜色平坦区域”在 Midjourney 中)

a cartoonish picture of a very excited donkey among a stack of books

你甚至可以为你的角色想出一个有趣的名字,并在团队中谈论它。“Dusty 在那次会议上是一个令人讨厌的自作聪明的人,我们不得不关闭它一段时间”,或者“我很高兴 Dusty 在那里,我在午饭前完成了那项任务”。但你永远不应该说“Dusty 导致了那起事件!”,因为 Dusty 基本上是未成年人,他们没有犯错的许可。我们就像父母,最终对提交负责,而“父母对其子女的行为负责”。

结论

情况评估清单可能看起来很多,需要在每次使用编码助手时都应用。但我相信,我们使用这些工具越多,就会越擅长使用它们。我们一直在编码时根据经验进行多维度的快速评估。我发现,我遇到的情况越多,例如上面提到的情况,我就越擅长决定何时使用和信任助手——就像我越是被烫伤,就越会小心地触摸热炉一样。

你可能还会想,“如果 AI 助手不可靠,那我为什么要使用它们呢?”。我们在使用生成式 AI 工具时必须进行思维转变。 我们不能用对“常规”软件的相同期望来使用它们。GitHub Copilot 不是一个传统的代码生成器,它不会给你 100% 你需要的东西。但在 40-60% 的情况下,它可以让你完成 40-80% 的工作,这仍然很有用。当你调整这些期望,并给自己一些时间来了解渴望的驴子的行为和怪癖时,你将从 AI 编码助手那里获得更多收益。

感谢 Brandon Cook、Jörn Dinkla、Paul Sobocinski 和 Ryder Dain 的反馈和输入。

这份备忘录是在 GitHub Copilot 处于活动状态的情况下,在 markdown 文件中编写的。它有助于提供想法和措辞,有时当我卡住时,但建议很少以最终文本中的形式出现。我使用 ChatGPT 作为同义词词典,并为驴子寻找一个好名字。