使用 ctags 提升 GPT-4 对代码库的理解能力
更新说明
Aider 已不再使用 ctags 来构建代码库映射。 请参阅关于使用 tree-sitter 构建更优代码库映射的最新文章。
GPT-4 在以下场景中极为有用:
- 处理”自包含”的编码任务
- 生成全新代码
- 修改无依赖的纯函数
但在修改或扩展大型复杂现有代码库时,GPT-4 的使用存在困难。 要修改这类代码,GPT 需要理解子系统间相互连接的依赖关系和 API 接口。 我们需要在 GPT 执行编码任务时为其提供这种”代码上下文”,具体需要:
- 帮助 GPT 理解整体代码库结构,使其能够:
- 解析具有复杂依赖关系的代码含义
- 生成尊重并利用现有抽象的新代码
- 以高效方式传递所有这些”代码上下文”,确保其适应 8k token 的上下文窗口限制
为解决这些问题,aider
现在会向 GPT 发送:
- 整个 git 仓库的简明映射
- 包含所有已声明变量和函数调用签名的信息
该代码库映射通过 ctags
自动构建,可从源文件中提取符号定义。
传统上,ctags 由 IDE 和编辑器生成并建立索引,用于帮助开发者搜索和导航大型代码库。
而我们现在使用 ctags 来帮助 GPT 更好地理解、导航和编辑大型仓库中的代码。
要了解这种方法的有效性,可查看这个聊天记录:
- GPT-4 成功创建了黑盒测试用例
- 在未获得被测函数源代码或仓库中任何其他代码的情况下
- 仅利用代码库映射中的元数据,GPT 就能:
- 理解如何调用待测方法
- 实例化多个为测试准备所需的类对象
要使用本文讨论的技术与 GPT-4 协作编程:
- 安装 aider
- 安装 universal ctags
- 在代码库内运行
aider
,应显示 “Repo-map: universal-ctags using 1024 tokens”
问题:代码上下文
GPT-4 擅长处理”自包含”的编码任务,比如编写或修改没有外部依赖的纯函数。GPT 可以轻松应对诸如”写一个斐波那契函数”或”用列表推导式重写循环”这样的请求,因为这些任务只需要讨论中的代码本身,不需要额外的上下文。
但大多数实际代码并非纯粹且自包含的,它们与代码库中许多不同文件的代码相互交织并依赖。如果你要求 GPT “将 Foo 类中的所有 print 语句切换为使用 BarLog 日志系统”,它需要看到包含 print 语句的 Foo 类代码,同时还需要理解项目的 BarLog 子系统。
一个简单的解决方案是将整个代码库与每个变更请求一起发送给 GPT。这样 GPT 就拥有了全部上下文!但对于中等规模的代码库,这种方法就行不通了,因为它们无法放入 8k token 的上下文窗口。
更好的方法是选择性操作,手动挑选要发送的文件。对于上面的例子,你可以发送包含 Foo 类的文件和包含 BarLog 日志子系统的文件。这种方法效果不错,aider
也支持这种方式——你可以手动指定要将哪些文件”添加到聊天”中。
但手动识别正确的文件集合并添加到聊天中并不理想。而且发送整个文件是一种笨拙的传递代码上下文的方式,会浪费宝贵的 8k 上下文窗口。GPT 不需要看到 BarLog 的完整实现,它只需要足够理解如何使用即可。如果只是为了传递上下文就发送大量代码文件,你可能会很快耗尽上下文窗口。
使用仓库地图提供上下文
最新版本的 aider
会在每个变更请求时向 GPT 发送一份仓库地图。该地图包含仓库中所有文件的列表,以及每个文件中定义的符号。可调用对象(如函数和方法)还会包含它们的签名。
以下是 aider 仓库地图的示例片段,仅展示 main.py 和 io.py 的地图:
aider/
...
main.py:
函数
main (args=None, input=None, output=None)
变量
status
...
io.py:
类
FileContentCompleter
InputOutput
FileContentCompleter
成员
__init__ (self, fnames, commands)
get_completions (self, document, complete_event)
InputOutput
成员
__init__ (self, pretty, yes, input_history_file=None, chat_history_file=None, input=None, output=None)
ai_output (self, content)
append_chat_history (self, text, linebreak=False, blockquote=False)
confirm_ask (self, question, default="y")
get_input (self, fnames, commands)
prompt_ask (self, question, default=None)
tool (self, *messages, log_only=False)
tool_error (self, message)
...
这种仓库地图方式具有以下优势:
- GPT 可以查看整个仓库中的变量、类、方法和函数签名。仅凭这些信息,GPT 可能就足以解决许多任务。例如,GPT 可能仅基于地图中显示的细节就能理解如何使用模块导出的 API。
- 如果需要查看更多代码,GPT 可以自行通过地图确定需要查看哪些文件。GPT 随后会请求查看这些特定文件,而
aider
会自动将这些文件添加到聊天上下文中(需经用户批准)。
当然,对于大型仓库,即使是地图本身也可能超出上下文窗口的限制。然而,这种地图方法为与 GPT-4 协作处理大型代码库提供了可能性,这比之前的方法更高效。它还减少了手动筛选需要添加到聊天上下文中的文件的需求,使 GPT 能够自主识别与当前任务相关的文件。
使用 ctags 构建代码地图
在底层,aider
使用 universal ctags 来构建代码地图。Universal ctags 能够扫描多种编程语言的源代码,并提取每个文件中定义的所有符号信息。
从历史上看,ctags 通常由 IDE 或代码编辑器生成并建立索引,目的是方便开发者搜索和导航代码库、查找函数实现等。而我们则利用 ctags 来帮助 GPT 导航和理解代码库。
以下是运行 ctags 扫描源代码时的典型输出。具体来说,这是对上述 main.py
文件执行 ctags --fields=+S --output-format=json
命令的输出:
{
"_type": "tag",
"name": "main",
"path": "aider/main.py",
"pattern": "/^def main(args=None, input=None, output=None):$/",
"kind": "function",
"signature": "(args=None, input=None, output=None)"
}
{
"_type": "tag",
"name": "status",
"path": "aider/main.py",
"pattern": "/^ status = main()$/",
"kind": "variable"
}
代码库地图就是基于这类 ctags 数据构建的,但会格式化为之前展示的空间效率更高的层次树结构。这种格式易于 GPT 理解,并能用最少的 token 数量传达地图数据。
示例聊天记录
这份 聊天记录 展示了 GPT-4 在未被提供被测函数源代码或代码库中任何其他代码的情况下,创建黑盒测试用例的过程。GPT 仅依靠代码库地图中的元数据进行操作。
仅凭地图中的元数据,GPT 就能理解如何调用待测试的方法,以及如何实例化多个为测试做准备所需的类对象。
GPT 在编写测试的第一个版本时犯了一个合理的错误,但在看到 pytest
的错误输出后能够快速修正问题。
未来工作
正如”每次请求都发送整个代码库给GPT”不是解决该问题的高效方案一样,”每次请求都发送整个仓库映射”也可能存在更优的替代方案。发送适当的仓库映射子集将帮助aider
更好地处理那些拥有庞大映射的大型代码仓库。
以下是几种可能减少映射数据量的方法:
- 精炼全局映射,优先处理重要符号并丢弃”内部”或其他全局相关性较低的标识符。可以考虑利用
gpt-3.5-turbo
以灵活且语言无关的方式执行这种精炼。 - 提供一种机制,让GPT从精炼后的全局映射子集开始工作,并允许它请求查看与当前编码任务相关的子树或关键字的更多细节。
- 尝试分析用户给出的自然语言编码任务,预测哪些仓库映射子集是相关的。可以通过分析特定仓库中先前的编码对话记录来实现。处理某些文件或特定类型的功能可能需要仓库中其他部分的一些可预测上下文。对聊天历史、仓库映射或代码库进行向量和关键词搜索可能对此有所帮助。
一个关键目标是优先选择语言无关的解决方案,或者能够轻松部署到大多数流行编程语言的方案。ctags
解决方案具有这一优势,因为它预先内置了对大多数流行语言的支持。我怀疑语言服务器协议(Language Server Protocol)可能是比ctags
更适合解决这个问题的工具。但要为多种语言部署它则更为繁琐,用户需要为他们感兴趣的特定语言搭建LSP服务器。
试用方法
要使用这个实验性的仓库映射功能:
- 安装aider
- 安装ctags
- 在你的代码仓库中运行
aider
,它应该会显示”Repo-map: universal-ctags using 1024 tokens”