从Copilot到全流程AI研发:解密下一代开发模式

文本需要一定 Agent 相关知识、背景了解。 聚焦类似 github copilot workspace 这类工作原理,内容较多,本篇仅仅对难点部分进行详解。

# 源起

LLM 出现以来,最常见的使用场景就是辅助软件编程,通过合适的提示词,先进的大模型能够提高编码的效率,比如代码生成 (opens new window)、测试用例生成。然而,这类工作大多是非常局部的应用,比如生成一个函数,生成一个组件等,生成过程仍然是非常原始复制粘贴,或者 IDE 补全等。我们能否丢掉传统 IDE,基于 LLM 构造一个 Agent 系统,让该系统替代人类工作,完成需求理解分析、任务规划、代码生成、git 提交,最后自动构建部署,以此解决现实世界的问题呢?答案是当然可以。

为了充分评估这类 Agent 系统的能力,社区很早就构建了测试用集,SWE-bench,一个以现实 github 仓库的 issue 为测试集 (opens new window),让模型自动理解整个仓库,并完成代码提交。最初的论文在这:Carlos E.等 Can Language Models Resolve Real-World GitHub Issues? (opens new window)

这类系统有两个方向,第一种是完全无人干扰自主 Agent ,类似 devin(参考之前我的一篇 open-devin 详解),人类只需要给定任务,系统全程无人干扰在后台完成。另一种是 以 LLM 为中心,并让人类和 Agent 交互、协作去解决问题。经过实践对比,这类人机协作更为实用和可靠,并且我们内部验证了其可用性。这类工作的代表是产品有 github copilot workspace 。和 devin 这类纯通用自主系统不一样,人机 copliot 协作是指系统将按一定的流程推进,整个过程由 Agent 驱动,而人类则作为可选的介入者。由于过程是固定、透明的,人类是可介入的,这将极大加强了整个过程的可控性,可用性。

最近几个月我们一直在致力于做这样的探索,并构建了基本的原型,在一些现实世界的需求得到了验证。

本文将重点阐述其思路还有关键逻辑。

# 聚焦关键目标

全研发流程是涉及非常大的工程,它包括提需求、分析需求、编码、提交代码、持续集成 (opens new window)、测试、部署。但实际其中大部分传统自动化可以完成,而难点在于如何通过一个需求单,让模型在整个复杂的仓库中找到相关文件,最后直接生成代码。即使是人类,为了解决一个问题,我们可能需要查阅大量代码文件,并合理的分析出功能模块关系,最终在复杂的依赖中对文件进行增删改查。如何设计这样的 Agent 系统呢?

首先我们需要给需求定义一个标准,它必须是能够被理解和执行的——ASSD (Agent Specific Story Description)。这是我们定义的一种规范,实际上,它和普通的标准需求单没有任何差异,我们所做的是要杜绝“一句话需求”,让 LLM 可以得到标准的任务,避免返工。 人类在解决需求的过程,如果遇到不标准的需求,通常是需要花费大量的沟通成本。 将业务需求转为 ASSD 标准需求的过程可以由专业的产品经理实施,也可以是人类开发。将不精确想法需求转为标准的 ASSD 实际也可以 AI 辅助,不在本文讨论范围。

而 ASSD 需求转为代码的过程, 是整个系统的难点。

按我们以文件改动的广度定义需求复杂度。

  • 低等难度——涉及一个文件的改动

  • 中等——涉及几个文件的改动。

  • 复杂——涉及大量文件的改动。

我们的场景聚焦在低等、部分中等难度的需求上进行 Agent 自动化完成。这些需求可能单一文件改动,或者涉及多个文件的增、删、改。

# 如何从需求到代码

我们不可能把 issue 和代码仓库内容全部给到模型,通常项目代码量是相当巨大的,即使一些模型选词能支持 200K 的上下文,极大的长文本下,模型解决问题的能力也非常弱,在从需求到编程这类任务上几乎不可用、且不可控。

更切实可行的做法是运用 Agent 工程化手段,将复杂的问题拆解成更小的问题,再逐个击破。

这一拆分虽然是从人类解决问题的视角进行的,但我们必须保证这种拆解方案能足够通用,又能确保模型在特定领域发挥长处。

我们的做法和 github colipot workspace 类似。需求到代码整个过程可以拆解成: 方案生成、任务规划、编码。

  • 方案生成 Agent:首先必须有一个 Agent 具备分析需求,并从海量代码中找到需要增、删、改哪些相关的文件,并以自然语言的理解返回。通常会长这个样子:

主题:增加 deepseek (opens new window) 平台及其模型描述和定价信息。
相关代码文件的现状:目前的文件缺少 deepseek 平台的信息。
接下来应该修改的方案:
新增 src/data/deepseek.ts 文件,包含 deepseek 平台的模型描述和定价信息。
修改 src/data/platform.ts 文件,导入 deepseek.ts 并更新 platforms 对象,包含 deepseek 平台的信息。

这一步是最核心,最关键的。我们全篇讲聚焦在这一步。

  • 任务规划 Agent: 任务规划是在需求理解的基础上形成最终的东西步骤。 它指示了对一个项目相关文件的文件增加、修改、删除操作, 是具体的编码前指令。 任务规划的作用就是详细生成文件基本的任务。 这些任务以文件为核心,细化到了对每个文件改动的具体的步骤列表。这一步的作用就是拆解任务到每个文件,为接下来的代码生成做准备。
  1. 增加 src/data/deepseek.ts 。
    目前没有该文件,为了解决这个问题,增加改文件,内容是关于 xxx等。

  2. 修改 src/data/platform.ts 。
    导入 deepseek.ts 并更新 platforms 对象,包含 deepseek 平台的信息

  • 代码生成 Agent:有了规划好的步骤,生成单一的代码文件,是目前大模型的优势,对代码生成 Agent 而言,它只需要关注局部信息。

前面的过程是针对一个针对需求让代码的检索范围逐渐从大到小逐渐缩。 每个 Agent 会基于前面的结果做自己的事情,最终生成每个文件完整的代码,并回显在页面, 让人类 review 。

这种拆解除了更充分利用不同Agent的局部能力,最重要的是提供了过程透明和人类介入的可能。 在每个Agent任务结束后将结果回显在前面界面,产品设计上,能可选的添加一些交互,让人类可以和 Agent 互动,比如,发现生成的方案不对,可以选择重新生成;或者发现方案有缺失,可以新修改方案。 这一切都是自然语言,而不是代码。

图为整个无人研发流程,但下面重点只解答【需求理解】部分的难点

毫无疑问,如何仅仅根据 issue 从代码参考中定位到相关文件是整个项目的关键之处。下文整个部分都是针对方案生成 Agent。(架构图中理解需求部分)。

# 简单方案

# 简单的方案一

一种简单的方案,程序循环加载每个文件的代码,再拼接 issue 任务目标给到 LLM 去理解,请求它该文件是否涉及当前任务。 一定程度上,某些简单的 bug、比如运行时报错、异味等可以被定位到。想象一下,每个需求都需要循环的遍历每个文件的代码,这样的 LLM 请求效率几乎不可用。

# 简单的方案二

既然,不同需求会重复请求 (opens new window)整个代码文件,那我们是否可以提前扫描整个仓库,让 LLM 提前对每个文件进行解读,并生成摘要信息? 这些摘要信息是简短的,在接到不同需求后,可以批次的将文件名列表以及对应的摘要直接给到上下文,让模型返回和任务最相关的文件名。

任务:需要在A 添加 xxx 功能。
[
文件1:该文件有 xxx 功能
文件2: 该文件有 YYY 功能
...
]

返回:

和该任务相关的文件可能是:文件1 、文件N

这样整个查找范围从整个项目的仓库缩小到了可能相关的文件中,然后再去逐一让模型理解这些文件。

然而,这种方案依然只能解决简单的单文件修改任务,并且对摘要的质量要求非常高。事实上,现实的任务不可能仅仅改动单一文件,我们可能需要为任务:新增一个文件 A 模块中,并在B 文件中引用文件 A 。有时候为了解决一个疑难杂症,我们可能需要了解多个文件的相互关系,并做出判断。采用简单的方案是仅仅不够的。

# 更智能的自我决策

从程序执行的直觉告诉我们,可以利用传统算法,比如AST语法树 (opens new window)提前解析出代码模块的依赖关系,并通过适当的优化剪裁,作为摘要信息的一部分存起来,以供后续加强模型对项目的理解。相当于我们对每个文件的理解不仅仅是摘要,还包含哪些文件和它有相关行。但这依然是加强上下文信息的部分优化。

我们参考人类资深工程师,首次接到一个新任务、新项目的时候会做哪些事情? 通常打开仓库会看到项目的根目录全部文件(1)。

资深工程师可能会有这些经验:

Node.js 项目可能是这样:
package.json
node_modules/
README.me

React 项目可能是这样:
package.json
public/
src/
Java Maven 项目可能这样:
pom.xml
src/main/java/
src/test/java/

相信上面对大多数人类工程师也没有疑问。LLM 接受了大量这样的知识训练,实际上通用 LLM 比人类可能还更清楚这些细节。

也就是说,给定一个任务1,和给定一个项目结构。请你分析,并返回接下来应该做什么, 我们会从下面的几个动作中选定一个。

open=>file // 打开文件路径
open=>folder
preview=>file
search().

通常我会打开 README.md (2),以详细了解基本的信息,并总结。

接下来,我的经验会告诉我,接下来的动作应该是 open=>src (3),我想看看基本的项目路径。

src 下面的路径可能是这样的:

src
├── app
├── components
├── data
└── lib

接下来,我们会根据当前已知信息,不断更新我们接下来的动作,这一切都是基于通用的软件经验,以及当前获取到的信息。最糟糕的情况是,一个新项目:无任何代码注释,且文件结构几乎混乱,命名不规范。这类项目对任何人类都是噩梦,我们可能需要几乎查询每个文件才能定位到要改的代码。

对于 Agent 系统也是,它不断自我反思,给出下个可能性最大的动作,并看到动作执行的结果,再次反思,继续更新动作,直到最终完成任务。

核心逻辑

# Agent组成

方案生成 Agent 用于理解项目和生产方案,我们暂定它为 preAgent。preAgent 只接受一个纯自然语言的 issue 需求和项目仓库地址。返回的内容就是对需求的理解,以及定位到需要改动的相关文件以及方案。它由 3 个 Agent 组成。

  • CurrentThinkingAgent

当前思维区是整个逻辑的核心,代表模型对当前情况和最新信息不断自我反思、推理,得出接下来的动作。 实际单一的自我循环反思决策Agent,即使没有外部信息额外信息(比如摘要)可以使用也能做很多事情。而我们的代码摘要等相关细节信息会进一步提高决策的准确性。

以当前思维区为核心的提示词对话示意。

[
user: 从现在开始你充当一名专业的软件架构师 (opens new window),你将会面对一个从来没有见过的项目,然后收到任务,被要求找到需要任务的相关文件。 你有权利浏览该项目的任何相关文件,并且在方案上有权利增加、删除、和修改。 但是一个项目的的文件数非常多,你不太可能想要逐一阅读(如果需要也可以)。
你的任务是:
....
你的策略是通过当前看到的文件结构,决定接下来应该打开哪个文件(可直接定位到目标文件),目录查看。我将会先把根目录下的文件发给你,接下来你可以选择的 commond 命令列表是:
open=>filename // 打开一个文件路径,系统将展示文件内容
open=>folder // 打开文件夹,系统将会展示文件夹内容
系统将会执行你的要求返回对应内容,你每步骤只能执行一个动作,请返回接下来你需要的动作,直到你认为已经找到相关文件,并提供了方案。
assist: 返回 open=>xxx
user: 返回内容代码 xxx ,(可选:请返回接来的指令)
assist: open=>xxx
user: 返回内容xxx
assist: 现在我知道了 xxxx
]

模型将不断返回返回需要探索的路径,并且最新探索的信息会不断更新在上下文中,一直循环直到找到方案为止。

  • CompressionAgent

自动压缩总结已经探索过的路径。由于指令执行越来越多,上下文会变得太长,当超出一定的阈值,会把这些探索过的文件记录进行压缩。 每次 CurrentThinkingAgent 执行后,都需要调用。如何设置最大值? qwen72b 虽然支持 128k 巨大上下文,但需要验证当前场景海量上下文下模型的效果。从优化上, open=>file 打开一定的次数,就应该被压缩,多余代码文件会影响模型后续理解。

最终变成的历史记录类似如下:

[
assist: 我已经看过的相关文件是:
aaa.py 文件: 一句话介绍,结论:不太涉及该需求。
bbb.py 文件: 一句话介绍,结论: 涉及该需求,里面的 xx 功能需要改造。
……
]

压缩的内容会被放到思维区 Agent 作为历史上下文一部分。

  • ObserverAgent

观察当前工作区的进展,并给出方向调整,是否已经完成。为何不让当前思维区Agent 自己决定是否完成? 因为当前思维区往往关注当下,添加额外的全局判断,会增加模型理解的难度,我们可以把这个功能区分来,用另一个观察者进行。代价是每次当前工作区都需要过一下观察者,额外资源消耗。 实际,目前测试,即时不添加 ObserverAgent 仍然能解决多个文件管理改动的代码(上面的案例就是一个Agent独立自我反思完成)

提示词示意

user: 从现在你充当一名架构师,面对一个从未见过的项目,将收到任务,你不需要自己实际采取行动,将会有团队成员进行解决。 你将会负责对别人工作过程从全局进行观察,并实时此判断任务是否已经找到最终解决方案 (opens new window)
当前工作过程实时进展如下:
[
assist: 我是一名软件工程师,正在解决一个问题,被要求找到相关文档和解决方案。下面是我的思路,接下来是(其实这里就是把思维区第一段提示词,切换成第一人称而已)
user: xxx
assist: open=>xx
user: xxx
]
上面是当前工作区,请进行分析并给出指令如下。
{
analise: 分析
next: 1: 继续、 2: 完成(代表最终完成)。
suggestion: 可选的建议,这个建议会直接插入到当前实时工作区,打断成员当前工作。 如果你观察到团队成员正在按合理的方向进行,请勿返回。如果观看到偏离方向,或者陷入迷惑阶段,你才给出建议,进行纠正。
}

# 工具方法

模型返回的动作,不能简单的基于文件系统命令包装后返回,而是需要做大量提示友好型工程,以更好地适配 LLM 。

open => file // 打开文件,由于文件可能会非常大,需要针对 llm 有好性优化。
open => folder
preview=>file ,文件数非常大的情况下,让模型预览文件函数级别的摘要。
search=>(xx),让模型决策搜索,返回被搜索文件的相关摘要和文件路径。

最终生成的方案可能如下:

主要代码模块

通过以上方案,验证了一些加入copilot workspace 公开的 github 仓库,实践表明能做到同等的效果。

到此为止,我们讲述了核心机制,但仍然由大量的工程细节,诸如大型文件需要分批次打开和预览、提示词优化、任务规划、编码生成、界面 UI 交互、性能评估等工作。

# 其他部分

其他部分都是工程细节,不是难点,大家可以各自探索。

相关论文:

SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering (opens new window)

SWE-bench: Can Language Models Resolve Real-World GitHub Issues? (opens new window)

相关产品:

GitHub Next | Copilot Workspace (opens new window)

最后:感谢 SWE-bench 、open-devin、github Copilot Workspace 这类最早探索技术边界的项目和团队;特别是 Copilot Workspace 给与了我们很大的设计灵感。

更新时间: