生成式人工智能的一个自然‘前沿’应用是在软件开发生命周期(SDLC)里。由于LLM已经变得非常擅长生成、调试和分析代码,这得益于从诸如Github和Stack Overflow等公开来源获取了大量的有价值的训练数据。
像 Github Copilot 和 Cursor 这样的工具已经被开发者广泛采用,他们用这些工具来自动化软件开发和维护的许多环节。
对于LLM来说,诸如在大规模代码库诊断并解决bug之类的实际软件工程任务仍然面临挑战。
- 虽然上下文窗口正在变大,你通常不能在一个输入提示中包含整个代码库的所有内容。 我们需要高效的方法来搜索和浏览代码。
- LLM处理代码的方式与人类不同——在设计既安全又高效的AI代理工具时,我们必须考虑这种细微差别。关于“代理-计算机接口”(ACI)的需求在SWE-Agent论文中进行了详细讨论。
- 生成模型本质上是概率性的,可能产生意想不到的结果。重要的是要利用这种创意解决方案的倾向,同时让代理保持在正确的轨道上。
这说明了现实中的复杂情况,体现了像SWE-Bench这样的基准测试的价值,它让大模型和工作流程解决GitHub上的问题,比如matplotlib、pytest和django等流行开源仓库中的问题。
SWE-Bench 不仅提供了一个数据集,还提供了一个用于自动化测试评估的稳健框架。提交的内容将根据其是否解决了用于验证修复有效性的单元测试来进行评估。
我认为这是一个相对容易上手的机会,可以亲自接触和操作这些领域的前沿概念,比如代理编排和运行时计算。SWE-Bench [排行榜(排行榜页面见:https://www.swebench.com/)] 不断地在演变,不仅是因为更强大的基础模型的推出,也是因为各个团队在实验更有效的流程和工作方式。
本文中,我将讨论我的解决方案“Aegis“,截至撰写本文时,该解决方案在SWE-Bench Lite上的通过率为33.67%,超过了亚马逊、IBM等大型组织的提交。
为什么是埃吉斯盾?
埃吉斯盾(/ˈiːdʒɪs/ [EE-jis];[1] 古希腊语:αἰγίς aigís),如《伊利亚特》中所述,是雅典娜和宙斯所持的一种装置,通常被解释为兽皮或盾牌,并且有时会带有蛇发女妖的头。
现代人说的‘在某人的埃吉斯盾下做事’,意味着在一位强大、有知识或仁慈的保护者庇护下做事。
- 维基百科
aegis
也是《帝国时代II》中的一个秘籍,能够瞬间建造建筑并采集资源。
只想看代码吗? 可以看看 Aegis 的 Github 仓库(并在喜欢的话给它点个 ⭐️ 吧)。
人机界面及环境配置在我看来,环境设置是解决方案中最复杂的部分。因为每个仓库(和问题单)都有自己文件和依赖的状态,代理程序会在一个Docker容器内交互式地运行命令。这样的沙箱环境也确保了不受控制的代理程序不会对我的整体系统造成太大损害。
幸运的是,SWE-Bench团队为每个问题实例提供了Docker镜像。但我需要在这些环境中添加一层来安全地执行预定义的命令。[Environment](https://github.com/evandiewald/aegis/blob/main/issue_reviewer/environment.py)
这个类提供了这种抽象(例如 execute_command
、ls
和 get_patch
这些有用的方法)并构建在Docker客户端之上。
接下来,[Editor](https://github.com/evandiewald/aegis/blob/main/issue_reviewer/editor.py)
类提供了一个更适用于模型的抽象层,用于处理 Environment
,即我们的代理-环境交互。这是解决方案的核心部分,定义了用于文件查看、编辑、搜索和测试执行的工具。
让我们拿文件查看工具做个例子。
当LLM使用open_file
工具时,它必须提供file_path
和line_number
。如果路径和行号都有效的话,我们会返回一个互动的“界面”给LLM,例如open_file(django/db/models/fields/related.py, 860)
,该工具会返回:
打开文件:django/db/models/fields/related.py
...854行以上...
855: ] if self.unique else []
856:
857: def deconstruct(self):
858: name, path, args, kwargs = super().deconstruct()
859: del kwargs['to_fields']
860: del kwargs['from_fields']
861: # 处理更简单的参数,比如
862: if self.db_index:
863: del kwargs['db_index']
864: else:
865: kwargs['db_index'] = False
866: if self.db_constraint is not True:
867: kwargs['db_constraint'] = self.db_constraint
868: # 关系处理还需要更多的工作。
869: to_meta = getattr(self.remote_field.model, "_meta", None)
870: if self.remote_field.field_name and (
871: not to_meta or (to_meta.pk and self.remote_field.field_name != to_meta.pk.name)):
872: kwargs['to_field'] = self.remote_field.field_name
873: return name, path, args, kwargs
874:
875: def to_python(self, value):
876: return self.target_field.to_python(value)
877:
878: @property
879: def target_field(self):
880: return self.foreign_related_fields[0]
881:
882: def get_reverse_path_info(self, filtered_relation=None):
883: """获取从相关模型到此字段模型的路径信息。"""
884: opts = self.model._meta
885: from_opts = self.remote_field.model._meta
886: return [PathInfo(
887: from_opts=from_opts,
888: to_opts=opts,
889: target_fields=(opts.pk,),
890: join_field=self.remote_field,
891: m2m=self.unique 为 False,
892: direct=直接关联为 False,
893: filtered_relation=filtered_relation,
894: )]
895: # to_meta 的主键不为空且 self.remote_field 的字段名不等于 to_meta 的主键名。
...
窗口会从提供的行号前面的几行开始显示。你可以看到视图包含了实际文件内容之上的注释,比如行号和Opened file:
标题。虽然这些组件不显眼,但它们有助于为语言模型提供进一步的搜索和编辑的有用上下文。
从这里,这里还有scroll_up
和scroll_down
工具,这些工具不需要任何参数,因为它们只是简单地翻页窗口,而我们跟踪着当前打开的文件。这些工具只是用来翻页的,向上或向下滚动窗口。
当我们深入探讨实际工作流程时,我们将进一步介绍并演示其他工具。
工作流程概述总的来说,Aegis工作流程设计的目标是模仿人类开发者通常会采用的方式。
- 任务理解和规划
- 故障定位与诊断
- 修改代码
- 代码验证
- 如有需要,则进行迭代
大多数解决方案,包括我自己的,都集中在代理化工作流上——这意味着大型语言模型(LLM)“代理”在执行的动作中具有一定程度的灵活性。然而,有一个非常出色且高效的方案叫做“无代理”,它采用了线性行动路径。在我设计Aegis的过程中,我从无代理论文和代码库中学到了很多。
为了实现代理式编排,我使用了LangGraph和LangChain来简化解析和工具配置等未细分的繁重任务,同时保持足够的定制性。这帮助我相对快速地搭建了该工作流。
让我们来拆解这些步骤,并探讨其中一些最常见的概念。
任务理解和规划一个典型的程序错误 (bug) 报告可能是这样的:
这是来自一个天文学相关库仓库的一个SWE-Bench测试示例。
通常会有一个问题的描述、期望的行为以及重现步骤。这为维护人员提供了一个相对清晰的开始,以了解如何重现 bug 并评估可能的修复方案。
基于代理的解决方案大多数情况下始于一个“规划”阶段。随着最近对“深度思考”模型如OpenAI的o1和DeepSeek的R1的兴奋,这种长时间的思考过程已经在模型训练后的行为中体现出来。
不过,值得注意的是,大多数大型语言模型(LLM)可以通过少量样本提示或只是简单地说“让我们一步步来思考”表现出一定程度的链式思考。
无论如何,这个理解/规划的步骤对于分解复杂任务来说至关重要。这里是一个Aegis系统为Django项目仓库中的一个问题生成的计划的例子。
让我理解一下所报告的问题:
1. 在 Django 的 `SQLCompiler.get_order_by()` 方法中存在一个 bug,它错误地移除了由多行 `RawSQL` 语句创建的 "order by" 子句。
2. 问题发生的原因是 `ordering_parts.search(sql)` 在多行 SQL 中搜索,但只返回包含 "ASC" 或 "DESC" 单词的最后一行。
3. 如果有多个原始查询的最后一行相同,这些最后一行被添加到一个 `seen` 集合中,只有第一个子句会被 `SQLCompiler.get_order_by()` 返回。
4. 报告者建议通过先用空格连接 SQL 字符串使其成为单行,然后进行正则搜索来修复此问题:`sql_oneline = ' '.join(sql.split('\\n'))`。
让我首先在代码库中找到相关的代码,以便更好地理解问题。
我应该在 Django ORM 代码库中查找 `SQLCompiler.get_order_by()` 方法。
故障的定位
这可能是最重要的阶段,具有最广泛的方法和最复杂的特性。想象一下,一个开发人员会如何浏览一个新的代码库。他们会浏览文件树,扫描文件内容,甚至可能搜索特定函数或类的使用情况。
GitHub的界面设计得非常巧妙,方便用户高效浏览大型仓库。
人类在处理大量信息背景并选择性地忘记其中95%不相关的部分时非常高效。相比之下,基于变压器的大型语言模型(LLM)几乎完美地记住了这些相对少量的数据。
对于我的实现,我希望给代理在搜索方式上更多的灵活性,但也要在重要方面加以限制。Aegis(Aegis)中有主要的两个搜索工具:
grep
风格的关键字搜索- 基于代码片段向量表示的语义搜索
在进行语义搜索时,我根据语法树来拆分代码块,以便更好地区分类和函数,而不是简单地按照字符数量来划分。我还根据类型将这个“代码索引”分为源代码和测试代码,以提供更有意义的过滤选项。
作为预处理步骤,代码片段(snippets)使用Cohere Embed v3编码,并存储在本地的FAISS向量数据库中。这种方法很大程度上借鉴了令人惊叹的Moatless Tools开源项目。
当调用 semantic_search
工具并提供一个自然语言 query
和可选的过滤器时,最相关的10个代码块会首先被发送到一个轻量级的语言模型(Haiku 3.5)中重新排序,以找到最相关的代码片段。这些被选出的片段会被返回到主线程,减少了核心对话交互中使用的令牌数量,同时确保仍然捕获有用的上下文信息。
截取的示例如下,其中包含与请求相关的代码段。
如果代理确切知道它要找的关键词,它可以使用更直接的explicit_search
工具。此工具将返回最多100个匹配文件和行。
这个工具可以直接搜索特定的关键词。
另一种有效的方法是先让代理尝试复现问题,因为这样产生的错误追踪通常可以作为很好的调试起点。
我尝试了这种方法,虽然它通常效果很好,但我发现,在某些环境中,代理在配置测试环境上浪费了太多时间,而不是直接解决问题。正如代理工作流中常见的那样,结构和灵活性之间需要找到一个平衡。
编码一旦代理确定了哪些文件和函数是引发问题的文件和函数,它就进入编辑环节。
创建有效的文件编辑工具以供LLM使用本身就是一个挑战。最初,我的edit
工具是受到SWE-Agent论文的启发。该工具的参数有文件路径、起始行和结束行,以及用于替换该范围内的文件内容的替换文本。
基于行号的文件编辑功能被发现对于最初的代理程序来说太复杂了。
结果证明非常容易出错。模型经常会截断过多或过少的行,有时甚至完全忽略缩进。即使作为一个用户,我也觉得这个工具有点不太方便。
正如在 SWE-Agent 论文中所述,通过在尝试编辑后立即增加一个自动代码检查步骤(例如 flake8
),如果代码检查失败,会让大模型再试一次,从而稍微提升了性能。
然而,即使有了代码格式检查这一步,修改仍然失败得过于频繁。此外,它还违背了我一开始就设定的设计原则之一——我希望避免需要对每种编程语言单独进行配置的功能。Java 的代码格式检查器与 Python 的会有所不同,各自有不同的配置细节和安装流程。
我最终决定使用的文件编辑工具来自Anthropic。他们发现对于大规模语言模型来说,最有效的编辑工具是一个简单的str_replace
,该工具将old_str
替换为new_str
。这避免了指定行范围的繁琐,使模型能够更直接地表达其意图。我发现str_replace
工具很少失败,因此我能够简化并移除代码检查工具的复杂性。
在需要新增一行(比如说添加导入语句或新增测试用例)的情况下,insert
工具接受一个行号和要插入的新文本 new_text
。
一旦做出修改,代理程序需要确认1),这些修改不会破坏现有的测试,2),并且确实解决了原先的bug。
再次,我从Moatless Tools中得到了灵感。当文件被修改时,Aegis 会自动运行相关的测试用例,并将结果反馈在编辑响应中。我们怎么找到相关的测试文件呢?我们使用与当前文件语义最接近的测试文件路径。
例如,如果有人编辑了 django/utils/decorators.py
,我们会从 tests/utils_tests/test_decorators.py
运行测试。下面是一个来自 str_replace
工具的示例响应。下面是测试结果摘要,任何失败都会在这里显示。
代码编辑,带有自动测试功能。
通过在编辑后自动运行测试用例,我们消除了模型决定运行哪个测试文件的决策点。这显示了代理是否破坏了现有代码,并指导它下一步该怎么做。
模型也被鼓励(但不要求)为当前的问题创建针对问题的测试用例。代理最确定的预测是那些已经通过适当验证的。
如有需要,继续迭代代理工作流的强大在于它们能够采取非线性的解题路径。修改后,Aegis可能会发现需要更多上下文,比如查找原始问题中未涉及的其他实现方式。毕竟,错误报告往往源自一些意外的特殊情况,我们不希望修复过程中影响到功能的正常使用。
关于测试时计算的一点说明很多在SWE-Bench上表现最好的实例都利用了某种形式的推理时长调整。总的来说,这是一个非常有趣的研究领域,从高层次来看,这个想法是,在提高LLM性能的努力过程中,虽然很多努力都集中在扩展训练能力上(例如,更大的模型,更复杂的微调策略),但也可以考虑扩展推理过程中的能力。
一种简单的扩展推理的方式是让模型并行生成N个解决方案,然后提交最可能的结果,甚至可以让另一组AI实体“投票”选出最佳方案。更复杂的方法则围绕着蒙特卡洛树搜索(Monte Carlo Tree Search)展开,这种方法通过使用独立的奖励模型来平衡不同决策路径的深度和广度。
在我的资源有限的环境中,我还没有在Aegis中尝试过这些策略。不过,我会密切留意这一领域,并思考如何实现高效的方法,例如使用一个基于强大推理模型的中央“监督者”代理来指导一组更经济的任务导向型模型。OpenAI最近在ARC-AGI上的突破性成果展示,每项任务的花费超过3000美元,引起了广泛关注!
示例轨迹图你可以通过观察示例 路径 ,或者当代理在解决问题时的思考过程来了解模型的行为和限制。
比如,让我们看看实例 ID 为 scikit-learn__scikit-learn-15512
的 Aegis 运行情况,这与 Claude 3.7 Sonnet 的一个 bug 报告有关。
理解任务并进行规划
代理程序首先分解问题和预期的行为。接着,它一步一步地制定计划来确认并解决这个问题。
Aegis 输出:
scikit-learn 的 Affinity Propagation 算法文档说明,当算法未能收敛时:
cluster_center_indices_
应该返回一个空数组labels_
应该为每个样本返回 -1然而,用户报告说当算法未能收敛(通过设置一个非常低的
max_iter
值)时,他们得到:
cluster_center_indices_
返回[2]
(不是一个空数组)labels_
返回[0, 0, 0]
(不是为每个样本返回 -1)用户提到他们使用的是 scikit-learn 版本 0.20.3。
所以任务是:
- 确认这是否是一个 bug
- 修复行为以符合文档或更新文档以反映预期结果
让我在 scikit-learn 中查找 Affinity Propagation 的实现。
故障查找
接下来,Aegis 利用提供的搜索工具开始查找收敛问题的根源,并搜集相关资料。
它使用了 semantic_search
工具,查询关键词为 affinity_propagation 函数实现
。这成功地找到了相关的代码段,如下(注:输出已被截断)。
爱吉斯决定重点关注 affinity_propagation
中的相关收敛性检查——如下所示为工具 open_file
的输出:
太棒了!Aegis 已经找到了这个漏洞,并且已经制定了具体的行动计划:
Aegis 输出:
现在我明白了问题所在。情况如下:
1. 查看第196行代码:
if (not unconverged and (K > 0)) or (it == max_iter):
,当满足以下条件之一时,算法会跳出循环:— 算法已收敛且K值大于0 (
not unconverged and (K > 0)
)— 或者达到了最大迭代次数 (
it == max_iter
)2. 当达到最大迭代次数
max_iter
时,它仍会在第204行识别当前状态下的原型,并继续聚类。3. 没有设置标志来判断算法是否真正收敛。
4. 第222行至第226行仅在没有原型(
K == 0
)时设置“未收敛”结果 (labels = np.array([-1] * n_samples)
和cluster_centers_indices = []
)。这就是为什么当算法未收敛(因为
max_iter
太小),但仍然识别出一些原型时,它会返回这些原型而非“未收敛”的结果。我会通过修改代码来正确追踪收敛状态来解决这个问题。
代码编辑和测试
Aegis 现在使用 str_replace
工具进行适当的修改,包含一个新的 converged
标志以及更新了返回逻辑以确保符合预期。所有相关测试会自动运行且均通过了!这意味着更改很可能没有以任何意外方式破坏任何内容。
虽然这在技术上解决了预期的行为,代理还注意到报告者请求了一个新的属性 converged_
以便追踪这个标志。这里有一个有趣的例子,展示Aegis如何处理自己的错误——它首先尝试盲目地更新 AffinityPropagation
类,但由于现有代码不完全正确,str_replace
工具也因此返回了一个错误。
Aegis 使用 open_file
命令重新打开相同的代码片段,然后修改了编辑。这次它成功了,。于是,它决定我们应该将这个新属性添加到文档字符串 (docstring) 中:
代理人接下来计划向 test_affinity_propagation.py
添加一个新的测试用例来进一步验证这些更改,但在这种情况下,执行由于递归限制而停止。我设置了这个限制是为了控制成本和延迟,使其保持在可管理的范围内。
此提供的补丁成功解决了问题,基于SWE-Bench测试套件。
结果和观察现在,我测试了Claude 3.5 Sonnet v2、OpenAI o3-mini,以及全新具备“混合推理”能力的Claude 3.7 Sonnet。我用SWE-Bench Lite(由SWE-Bench数据集中的300个实例构成的较小且更经济的子集)进行了验证。
我的3.5版本Sonnet在SWE-Bench Lite上的精度不到30%,在我停止执行时,Aegis使用o3-mini的解决率为30.67%。使用3.7版本的Sonnet表现最佳,解决率为33.67%(即300个实例中有101个被解决)。虽然距离排行榜的顶端还有距离,但我对此结果感到鼓舞,因为它超过了像亚马逊和IBM这样的大型组织的提交结果。
一些观察如下:
- 正如提供商自己所指出的,像o1这样的纯深度推理模型与其他通用(类似GPT)的大型语言模型相比,与通用模型相比,提示策略有所不同。推理模型是专门用于如SWE-Bench这样的复杂任务,因此我期待跟进相关研究(并提出自己的想法),以找到利用这种新型模型的最佳方式。
- 从3.5 Sonnet到3.7的性能提升非常明显。3.7版本似乎更擅长提前收集相关背景信息,避免陷入“局部最小值”,即死胡同决策路径。然而,我发现它有时开始编辑前准备时间过长,并且有时会遇到25步的自我限制。
- 我在令牌效率方面绝对有改进的空间。我的对话线程很长,这显著增加了成本和延迟。我计划探索分解任务和/或裁剪或总结先前步骤的方法,同时确保不会丢失关键信息。
最后说一点——,
构建具有代理性质的工作流程涉及解决上百个微小决策,无论是从传统的软件开发角度,还是在处理这些概率性和常常难以理解的大型语言模型(LLM)的世界中,这些模型的能力在不断演进。
这一领域因为开源贡献、出版物以及例如SWE-Bench这样的竞争性基准,发展得如此之快。 跟踪研究的增长和实际演示真的非常有趣!
你可以在这里找到该项目: https://github.com/evandiewald/aegis
参考文献共同学习,写下你的评论
评论加载中...
作者其他优质文章