机制详解:如何用 tree-sitter 与图排序算法为 LLM 高效构建代码上下文)
aider 仓库地图Repo Map机制详解如何用 tree-sitter 与图排序算法为 LLM 高效构建代码上下文【免费下载链接】aideraider is AI pair programming in your terminal项目地址: https://gitcode.com/GitHub_Trending/ai/aider在大型既有代码库中修改代码难点往往不在改而在理解。本文以 aider 官方技术博文aider/website/_posts/2023-10-22-repomap.md为核心脉络系统讲解 aider 如何基于 tree-sitter 自动抽取全仓库的类、函数与类型签名并以 PageRank 图排序算法挑选最关键、最贴合当前对话的代码片段压缩成可放进上下文窗口的仓库地图作为 LLM 每次改动请求的代码上下文。读完本文你将掌握该机制的完整原理、--map-tokens等核心配置参数的实战用法以及对应源码实现aider/repomap.py的底层调用关系。问题本质大仓库中的代码上下文LLM 非常擅长处理自包含的编码任务——比如写一个无外部依赖的纯函数、把一段循环改写为列表推导式。这类任务的共同点是不需要代码之外的任何背景信息。GitHub CoPilot 等工具在这类简单任务上已经足够好用。但在一个更大的、已经存在的代码库中完成复杂改动无论对人还是对 AI 都困难得多。要成功完成这样的任务需要依次做到找到需要修改的代码Find the code that needs to be changed理解这些代码与代码库其余部分的关联Understand how that code relates to the rest of the codebase正确地修改代码以完成任务Make the correct code change。GPT-4 类的模型在步骤 3生成改动上已经非常擅长——前提是你先告诉它要改哪些文件步骤 1并向它展示这些文件如何融入整个代码库步骤 2。aider 这篇博文的主题正是围绕步骤 2 展开如何高效地把代码上下文喂给模型。目标有三个帮助 LLM 理解整个代码库的大局帮助它理解待改代码所依赖的其他子系统让 LLM 在新增代码、修改既有代码时能够尊重并复用代码库中已有的库、模块与抽象。而所有这些上下文信息都必须以受限于上下文窗口的、尽量精简的方式传达给模型。常见的上下文策略与各自的局限围绕如何给 LLM 提供代码库背景人们尝试过几种朴素方案但各有痛点。方案一把整个代码库都发过去最直接的做法是把整个仓库连同每次改动请求一起发给 LLM——上下文永远充足。但这在仓库稍具规模时立刻失效代码库根本放不进上下文窗口。方案二人工挑选文件发送更有选择性的做法是手工挑选要发送的文件。还是用上面的例子你可以把包含Foo类的文件、以及包含BarLog日志子系统的文件发给模型。这确实可行aider 也天然支持——你可以手动把文件加入与 LLM 的对话aider 称之为 add files to the chat对应/add命令。但发送整个文件是相当笨重的上下文传递方式会白白浪费宝贵的上下文窗口LLM 并不需要看到BarLog的完整实现只需要理解它够用到能正确调用即可。为了传递上下文而把整文件塞进对话很快就会把上下文窗口耗尽。方案三理想目标让工具自动提供上下文aider 同时希望尽量降低使用 AI 编码时的人工操作量。所以在理想状态下应该让 aider 自动识别并提供所需的代码上下文而不是靠用户手动指定。仓库地图Repo Map把最关键的骨架交给模型为了解决上述问题aider 会在用户每次发出改动请求时随请求一起向 LLM 发送一份仓库地图repo map。这份地图包含仓库中每个文件的列表、每个文件中定义的关键符号类、函数、方法、类型等并通过每个定义的关键代码行展示这些符号在源码中的真实定义形态——包括类型和完整调用签名。换句话说它不是文件全文而是代码骨架 定义现场。以下是博文中展示的、aider 自身仓库地图的一个样例片段只列出 base_coder.py 与 commands.py 两个文件的映射aider/coders/base_coder.py: ⋮... │class Coder: │ abs_fnames None ⋮... │ classmethod │ def create( │ self, │ main_model, │ edit_format, │ io, │ skip_model_availabily_checkFalse, │ **kwargs, ⋮... │ def abs_root_path(self, path): ⋮... │ def run(self, with_messageNone): ⋮... aider/commands.py: ⋮... │class Commands: │ voice None │ ⋮... │ def get_commands(self): ⋮... │ def get_command_completions(self, cmd_name, partial): ⋮... │ def run(self, inp): ⋮...从源码实现看这段输出正是RepoMap.to_treeaider/repomap.py 第 748 行起的渲染结果按文件分组每个文件下只列出感兴趣的行lines of interest即被选中的符号定义所在行及其邻近签名行并在渲染时把每行截断到 100 个字符第 782 行防止压缩过的 JS 之类的超长单行撑爆预算。仓库地图的两个关键收益模型能纵观全仓地图让 LLM 可以看到整个仓库各处的类、方法与函数签名。仅仅这些信息往往就足以让它解决大量任务——例如仅凭地图中展示的细节它就能推断出一个模块导出的 API 该如何调用。模型能自主决定还要看哪些文件当需要查看更多代码时LLM 可以依据地图自行判断需要深入查看哪些文件然后主动要求查看这些具体文件aider 会自动把它们加入对话上下文。这就把人肉选择上下文文件的部分工作转移给了模型。优化地图图排序 Token 预算双重裁剪对于大型仓库即便只是一张仓库地图也可能超出上下文窗口。aider 的解法是只发送仓库地图中最相关的部分。从实现看这一裁剪发生在 aider/repomap.py 的RepoMap类中核心链条为get_repo_map()第 103 行起决定本次可用的 token 预算get_ranked_tags_map()/get_ranked_tags_map_uncached()第 576/629 行起负责把排序后的符号装进预算get_ranked_tags()第 365 行起完成图构建与 PageRank 排序。图排序以文件为节点、引用为边aider 会基于整个仓库地图做一次图排序graph ranking把每个源文件当作图中的一个节点若文件之间存在依赖关系A 引用了 B 中定义的符号则在节点之间连边。随后在这张有向图上运行PageRank算法——实际调用的是networkx的nx.pagerankaider/repomap.py 第 525 行。为了让排序结果更贴合当前对话代码在构图时叠加了大量细节化的启发式权重第 470–514 行为每个只有定义、没有引用的标识符添加一条极小权重的自环weight0.1避免其排名意外归零对在对话中被提到的标识符权重乘以 10对被提到过的文件名、路径组件会提升该文件在 PageRank 中的个性化personalization初值对**蛇形snake_case、短横线kebab-case、驼峰camelCase**命名且长度不小于 8 的标识符权重乘以 10——这类往往是真正的 API 名而非局部变量以下划线开头私有的标识符权重乘以 0.1被超过 5 个文件重复定义的同名标识符权重乘以 0.1以抑制命名冲突造成的噪声引用方位于已加入对话的文件中时该引用边的权重额外乘以 50让与当前改动强相关的定义排名更高高频引用按sqrt(num_refs)缩放避免低价值的重复引用喧宾夺主。排序完成后代码会按排名把(文件, 符号)对的定义标签汇总成ranked_tags序列供下一步按 token 预算截取。这段排序逻辑含全部权重启发式是对博文中graph ranking algorithm一段的源码级落地。Token 预算二分搜索最合适的裁剪点预算控制由--map-tokens决定默认是1k tokensRepoMap构造函数默认值即map_tokens1024见 aider/repomap.py 第 49 行。get_ranked_tags_map_uncached会在这个预算约束下做一次二分搜索第 666–706 行从min(max_map_tokens // 25, num_tags)个最高排名的符号开始渲染出候选树统计其 token 数token_count对长文本采用抽样外推估算见第 89–101 行若超出预算就砍掉一半候选若小于预算就加倍直到逼近预算允许 15% 的误差ok_err 0.15在不超过预算的前提下选取包含符号最多、token 数最大的那棵树保证地图尽量有用。博文中特别强调上面展示的样例地图并不包含这些文件的每一个类、方法与函数它只保留最重要的标识符——也就是被代码中其他部分引用最频繁的那些。这些正是 LLM 理解整个代码库所必需的关键上下文。地图的动态伸缩随对话状态变化需要说明的是--map-tokens是一个建议性预算而非硬性上限。aider 官方文档aider/website/docs/repomap.md与源码都印证了这一点在 aider/repomap.py 的get_repo_map中当没有任何文件被加入对话时地图预算会乘以map_mul_no_files得到更大的目标并且不超过max_context_window - 4096以便模型尽可能完整地了解整个仓库第 122–132 行。此外context_coder处理/context命令也会主动放大地图预算见 aider/coders/context_coder.py 第 18 行。也就是说预算会随对话状态动态调整在没加任何文件、需要全局理解的场合显著扩展。仓库中另有一些内置的取舍部分特殊文件如 README 等对理解项目至关重要的文档会通过filter_important_filesaider/special.py被无条件置于地图最前第 657–662 行而排序结果中那些排名很高但没有抽到任何符号标签的文件以及没有标签的其他文件也会按排名/文件名顺序追加进列表第 560–574 行保证地图不遗漏重要文件。用 tree-sitter 构建地图从源码到 AST 再到标签仓库地图的原料——每个文件的符号定义与引用——由tree-sitter负责抽取。aider 使用 Python 绑定py-tree-sitter-languages/grep_ast提供的语言包以 pip 二进制 wheel 形式覆盖绝大多数主流编程语言无需用户手动安装任何外部工具。tree-sitter 会按照编程语言的语法把源码解析成抽象语法树AST。基于 AST我们可以精确地定位函数、类、变量、类型等定义出现在源码的何处definition这些符号在代码的其他位置被谁引用reference。标签查询语言相关的 tags.scm 文件抽取哪些 AST 节点算定义/引用的规则是各语言独立的 tree-sitter 查询文件即*-tags.scm。这些文件存放在仓库的 aider/queries/tree-sitter-language-pack/ 与 aider/queries/tree-sitter-languages/ 两个目录下覆盖 python、go、rust、java、typescript、ruby、elixir、c/cpp 等大量语言。get_scm_fname()aider/repomap.py 第 805 行会优先从tree-sitter-language-pack查找找不到再回退到tree-sitter-languages。核心抽取流程在get_tags_raw()aider/repomap.py 第 279–363 行用filename_to_lang(fname)推断语言取得对应语言的 parser读取该语言的*-tags.scm查询文件把源码 parse 成 AST运行查询把捕获节点归类为name.definition.*kind def或name.reference.*kind ref逐个产出形如Tag(rel_fname, fname, name, kind, line)的标签记录——line即定义所在行号供后续render_tree精确渲染。一个很细的兼容性处理是某些语言如 cpp的 tags.scm 只提供定义不提供引用。为此代码会在只见 def、不见 ref时用pygments对源码重新分词把标识符 token 回填为引用标签第 338–363 行保证每个定义都有对应的引用关系可供构图。抽取结果的两级缓存符号抽取是相对昂贵的操作。RepoMap使用基于diskcache的 SQLite 缓存目录.aider.tags.cache.v{N}缓存版本号随语言包切换而递增见第 35–37、43 行以文件 mtime为失效依据文件未改动则直接复用缓存标签改动后才重新抽取get_tags第 233–264 行。地图渲染层还有一层基于 (文件, 感兴趣行, mtime) 的tree_cache/tree_context_cache内存缓存render_tree第 710–746 行。在较大仓库中首次全量扫描只会发生一次后续会明显加快。为什么弃用 ctags换用 tree-sitter 的四个收益仓库地图并非一开始就基于 tree-sitter——它取代了 aider 最初基于 ctags 构建地图的方案可对照更早的技术文档 aider/website/_posts/2023-05-25-ctags.md。博文列出了迁移的四个理由地图信息更丰富直接来自源码文件的完整函数调用签名与其他细节都能入图而不仅是 ctags 式的简单符号名行号安装即得、开箱即用借助py-tree-sitter-languages对众多编程语言的支持随python -m pip install -U aider-chat一并安装无需额外步骤移除外部依赖不再要求用户通过 brew、apt、choco 等外部包管理器手工安装universal-ctags为未来能力奠基tree-sitter 集成是 aider 后续一系列能力的关键使能组件。如今仓库中 language pack 的 tag 查询文件实际上是各开源 tree-sitter 语言实现里tags.scm的修改版本见博文 Credits 一节与 aider/queries 目录这些上游实现分别以 MIT、Apache-2.0 等开源协议分发仓库内保留了相应归属说明。从理解代码走向自动定位待改代码未来的工作回到开头的三步骤模型找到需要修改的代码理解代码与代码库其余部分的关系正确修改代码。目前 tree-sitter 已帮助 aider 解决了步骤 2代码上下文问题同时它也是步骤 1自动找到所有需要改动的代码的重要基石。当前版本中aider 依然依赖用户指定需要修改哪些源文件用户通过/add命令手动把文件加入对话这些文件才会对 LLM 开放修改权限。这套机制运转良好但博文指出的关键后续工作是借助 LLM 与 tree-sitter 的能力自动识别代码库中哪些部分需要改动从而进一步降低人工指定文件的操作负担。从仓库演进来看这一方向后续延伸出了文件自动发现等能力但识别与定位步骤 1本质上仍然是需要用户参与引导的环节。上手体验配置与关键参数要实际体验本文所述的机制安装并运行 aider 即可。仓库内置了相关配置项文档aider/website/docs/config/options.md与配置示例aider/website/assets/sample.aider.conf.yml。--map-tokens VALUE建议分配给仓库地图的 token 数量设为 0 可关闭地图aider/website/docs/config/options.md 第 261 行命令行定义见 aider/args.py 第 248 行。对应环境变量AIDER_MAP_TOKENS。默认值 1024 tokensRepoMap构造函数默认map_tokens1024aider/repomap.py 第 49 行模型侧默认值同为 1024aider/models.py 第 782 行get_repo_map_tokens设 0 即禁用get_repo_map在max_map_tokens 0时直接返回空第 111–112 行部分编辑模式会主动关闭地图例如 architect 模式默认map_tokens0aider/coders/architect_coder.py 第 28 行上限提醒当用户设置的--map-tokens超过模型建议值 × 2时aider 会在启动信息里给出警告aider/coders/base_coder.py 第 270–273 行因为过大的地图会挤占对话本身的上下文空间。从 aider/models.py 第 786–789 行可以推断对部分上下文窗口更大的模型默认预算会按max_inp_tokens / 8自动上探并被钳制在 1024–4096 之间。典型用法示例# 使用默认 1k token 的仓库地图默认行为 aider # 把地图预算提升到 2k tokens aider --map-tokens 2048 # 强制关闭仓库地图例如纯粹做单文件小改动时 aider --map-tokens 0--map-refresh VALUE控制仓库地图的刷新频率取值auto | always | files | manual默认auto对应环境变量AIDER_MAP_REFRESHoptions.md 第 265 行。从 aider/repomap.py 第 592–613 行的缓存逻辑看manual模式直接复用上次结果files模式仅在文件变化时重建auto则根据上次地图处理耗时是否超过 1 秒来决定是否命中内存缓存。对话中的地图状态加入文件后aider 的对话信息栏会显示类似Repo-map: using 1024 tokens, auto refresh的状态aider/coders/base_coder.py 第 265–277 行在终端使用/tokens之类的命令查看 token 用量时仓库地图会作为独立条目被列出并提示可用--map-tokens调整其大小aider/commands.py 第 478 行。官方问答也建议需要强制为任意模型开启地图时运行aider --map-tokens 1024aider/website/docs/faq.md 第 124 行。小结仓库地图是 aider 在上下文窗口有限与大仓库背景理解必须这对矛盾之间找到的工程解抽取用 tree-sitter 把每个源文件解析为 AST按语言专属的tags.scm查询抽取定义与引用标签aider/repomap.py排序以文件为节点、引用为依赖边构建有向图叠加对话相关性个性化初值与命名启发式权重后运行 PageRank让被引用最多、与当前对话最相关的符号浮出水面裁剪在--map-tokens默认 1024置 0 关闭预算内做二分搜索选择 token 数不超过预算且符号最丰富的渲染结果并对超长行与不必要的实现细节进行压缩服务每次改动请求都将地图随提示词送入对话模型既可凭签名骨架直接理解代码库也可主动请求深入查看某个具体文件。如果你希望进一步研究这份机制的边界行为与验证用例仓库内置的单元测试 tests/basic/test_repomap.py 覆盖了地图生成与排序的核心场景面向更广泛用户的现行说明则以 aider/website/docs/repomap.md 为准——本文所依据的博文aider/website/_posts/2023-10-22-repomap.md记录了该功能诞生之初的设计动机与架构决策是理解 aider 为 AI 建立大仓库认知这条技术主线的最佳起点。【免费下载链接】aideraider is AI pair programming in your terminal项目地址: https://gitcode.com/GitHub_Trending/ai/aider创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考