首页
/ aider 仓库地图(Repo Map)技术解析:从 ctags 初代实现到 tree-sitter 演进

aider 仓库地图(Repo Map)技术解析:从 ctags 初代实现到 tree-sitter 演进

2026-09-08 10:24:58作者:伍霜盼Ellen

在终端里用 AI 结对编程的 aider 之所以能读懂大型既有代码库,核心秘密是一个随每次改动请求一起发送给大模型的仓库地图(repo map)。这份地图最初由 universal-ctags 构建,后来又整体迁移到 tree-sitter 之上。本文以仓库中记载 ctags 初代实现的 ctags.md 为主体脉络,结合 repomap.py 的现行源码,完整讲解“代码上下文(code context)”问题为何存在、repo map 如何被设计出来、ctags 如何从源码中抽取符号,以及它如何演进为今天基于 tree-sitter + 图排序算法的实现。读完你将掌握 repo map 的设计动机、token 预算的调节方法(--map-tokens--map-refresh 等),以及查看与刷新地图的 /map/map-refresh 命令。

原文档的历史定位:先厘清一个重要的时间线

在深入技术细节之前,必须先说明关联文档 ctags.md 在仓库中的历史定位。该文档开篇即用一条 "Updated" 声明指出:

Aider no longer uses ctags to build a repo map.(aider 已不再使用 ctags 构建 repo map。)

它同时指向了仓库内对应的接续文章:repomap.md(站点文档)与博客长文 2023-10-22-repomap.md。也就是说,ctags.md 记录的是 aider repo map 的初代实现方案,属于“为何要做 repo map + 第一版怎么做的”历史技术档案;而现行代码已经用 tree-sitter 重写了整个链路。

因此,正确理解本文内容的方式是:先搞清它要解决的“代码上下文”问题(这一问题的本质至今未变),再看第一代 ctags 方案的长处与短板,最后看它如何在现行实现中被 tree-sitter 取代和增强。 这三层内容在仓库中都有文档与源码相互印证。

问题根源:大模型缺的不是代码能力,而是“代码上下文”

自包含任务 vs 互联代码

GPT-4 这类大模型极其擅长“自包含”的编码任务——生成全新代码,或修改一个没有任何依赖的纯函数。类似“写一个 Fibonacci 函数”“把循环改写为列表推导式”的请求,讨论对象之外不需要任何额外信息,模型可以轻松完成。

但真实世界的大多数代码并不纯粹、也不自包含:它们与其他文件、子系统深度交织。ctags.md 举了一个很形象的例子——如果你要求 GPT“把类 Foo 里所有的 print 语句换成 BarLog 日志系统”,它不仅需要看到带 print 的 Foo 类代码,还需要理解整个 BarLog 子系统的用法。这类任务需要三件事同时成立:

  1. 找到需要被修改的代码;
  2. 理解这些代码与代码库其余部分的关系;
  3. 正确完成代码改动。

GPT 恰好很擅长第 3 步,真正的瓶颈在第 1、2 步——即如何把“代码上下文”高效地喂给它

三种朴素方案的权衡

针对“如何提供上下文”,当时有过三种候选思路:

方案 做法 问题
整库直送 把整个代码库随每次请求发给 GPT 哪怕中规模仓库也放不进当时 8k token 的上下文窗口
手工精选文件 只发送与任务相关的文件 需要人肉判断哪些文件相关;整文件发送浪费窗口;多送几个文件就迅速耗尽 token
手工精选 + 交由模型判断 让 GPT 自己提出要看哪些文件 前提是模型得先“知道仓库里有什么”,否则无从提问

其中“手工指定文件加入对话”正是 aider 支持的能力——用户可以把文件 /add 到聊天中。但这依赖人工判断,且整文件发送非常“笨重”:模型并不需要 BarLog 的完整实现,只需要理解到“能正确调用它”的程度。

ctags.md 的结论是:需要一种自动生成、信息密度高、能塞进有限上下文窗口的仓库级概览,也就是 repo map。

Repo map 是什么:一份全仓库的符号清单

概念与目标

aider 在每个改动请求之外,会额外发送一份整个 git 仓库的简洁地图(concise map)。地图列出仓库中的每个文件,以及每个文件里定义的关键符号;函数、方法等可调用对象还附带其调用签名(call signature)。

这样一份地图带来两个直接收益:

  • 模型能从地图中看到全仓库的变量、类、方法、函数签名,仅凭这些元数据往往就足以解决大量任务——例如仅根据模块导出的 API 形态推断其用法;
  • 当模型需要看更多细节时,它可以自主判断哪些文件与当前任务相关并提出查看请求,aider 再(经用户确认后)自动把这些文件加入对话上下文。

这实际上把“人工策展该看哪些文件”的负担,转移给了模型 + 地图的协作机制。

当时的仓库地图长什么样

ctags.md 给出了一份 aider 自身仓库地图的片段,取自当时的 main.pyio.py。注意这是一份层级化、树形、省 token 的格式,很便于 GPT 阅读:

aider/
   ...
   main.py:
      function
         main (args=None, input=None, output=None)
      variable
         status
   ...
   io.py:
      class
         FileContentCompleter
         InputOutput
      FileContentCompleter
         member
            __init__ (self, fnames, commands)
            get_completions (self, document, complete_event)
      InputOutput
         member
            __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)
   ...

可以看到这份地图按“文件 → 符号(function/class/variable)→ 成员(member)”组织,并用缩进表达嵌套关系。函数名后面直接跟括号括起的完整签名。这种格式既省 token,又保留了对调用方最有价值的信息:知道“有什么、长什么样、怎么调”

关于这份地图的实战价值,ctags.md 引用仓库中的一份对话记录 examples/add-test.md(add-test 示例)作为证据:GPT 在完全没有看到被测函数源码、也没有看到仓库其他任何代码的情况下,仅凭地图中的元数据就完成了“黑盒测试用例编写”任务——它从签名里推断了被测方法的调用方式,并正确实例化了测试准备阶段所需的多个类对象。第一版测试犯了一个合理的错误,但看到 pytest 报错输出后很快自我修正。

第一代实现:用 universal-ctags 抽取符号

ctags 输出的“原材料”

repo map 的底层数据来自 universal-ctags——一款能扫描多种语言源码、抽取每个文件中定义符号的工具。历史上 ctags 由 IDE 与编辑器生成索引,供人类搜索、跳转大代码库;aider 的第一次创新在于把它改用来帮助 GPT 理解和导航代码

ctags.md 给出了当时的抽取命令与输出样例。对前文 main.py 地图片段,运行:

ctags --fields=+S --output-format=json

会得到如下两条 JSON 记录(main.py 中的函数 main 与变量 status):

{
  "_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"
}

关键字段一目了然:name 是符号名,path 是文件路径,kind 标记函数/变量/类等类型,signature 携带调用签名(这由 --fields=+S 追加输出),pattern 则给出定义行的原始代码,便于回溯定位。

从 JSON tag 到树形地图

第一版 repo map 的实现思路是:用 ctags 抽取全仓库每个文件的 tag 数据,再重新格式化为前文那种空间高效的层级树。树形格式相比扁平的 JSON 有两个显著优势——GPT 更容易理解其中的从属结构,且表达同样信息所需 token 数大幅下降。

这一方案天然具备一个重要优点(ctags.md 在 “Future work” 一节中也强调):语言无关、部署简单。universal-ctags 预置了对绝大多数主流编程语言的支持,用户无需为每种语言单独安装解析器,这使得 repo map 能立刻覆盖大多数代码库。

该方案的天花板

ctags.md 同时坦承了它的局限与后续改进方向:

  • 即使只是地图,对超大仓库而言也可能超过上下文窗口;
  • “每次请求都发整份地图”和“每次请求都发整个代码库”一样不是最优解——更合理的做法是只发送与任务相关的地图子集
  • 可能的裁剪思路包括:蒸馏全局地图(优先重要符号、丢弃内部符号)、允许 GPT 从子集出发按需请求子树详情、根据自然语言任务预测相关地图区域。

文档还前瞻性地提到:Language Server Protocol(LSP)或许是比 ctags 更好的工具,但其缺陷是部署繁琐——用户需要为自己使用的每种语言各自拉起一个 LSP server。

现行实现:tree-sitter 取代 ctags

迁移动因(仓库内文档的明确交代)

上文提到的时间线在这里闭合。迁移发生在 blog 文章 2023-10-22-repomap.md 所记录的版本,其中 "What about ctags?" 一节逐一列出了迁移理由:

  • 地图更丰富:直接从源码给出完整函数调用签名等细节,而不是 ctags 的摘要式字段;
  • 支持语言全、零配置:借助随 aider-chat 正常安装的 Python 包,即可获得绝大多数流行语言的完整支持;
  • 移除手动安装负担:不再要求用户通过 brew、apt、choco 等外部包管理器手工安装 universal-ctags
  • 为后续能力铺路:tree-sitter 集成是未来自动发现待改代码等功能的关键基础设施。

现行抽取链路的源码证据

现行 repo map 的核心实现在 repomap.py,围绕 RepoMap 类展开。其工作链路大体如下:

  1. 解析与抽取(tags 查询):不再调用 ctags 可执行文件,而是先用 tree-sitter 把每个源码文件解析成抽象语法树(AST),再运行按语言编写的查询脚本(tags queries)捕捉定义(def)与引用(ref)节点。查询脚本存放在仓库的两个目录中:tree-sitter-language-pack/.scm 文件,按 get_scm_fnamerepomap.py 中优先加载)以及 tree-sitter-languages/(回退目录)。判断是否启用该语言、加载 parser、运行查询的逻辑位于 get_tags_rawrepomap.py)。
  2. 符号建模:每个被捕获的符号被编码为 Tag namedtuple:rel_fname fname line name kindrepomap.py),其中 kind 区分 def(定义)与 ref(引用)。定义的来源是 tags 查询的 name.definition.* 捕获,引用来自 name.reference.* 捕获。
  3. 缓存:为避免反复解析,标签结果按文件缓存在仓库根目录的 .aider.tags.cache.v4 目录中(TAGS_CACHE_DIRrepomap.py),并校验文件修改时间决定缓存是否失效(见 get_tagsrepomap.py)。

从“ctags 时代只输出 JSON 字段”到“tree-sitter 时代先出 AST 再出 def/ref”,信息维度的变化是本质性的:有了“引用”关系,就可以量化一个符号被多少其他代码使用,这正是下述排序优化的原料。

图排序:只发“最重要”的部分

现行实现针对“地图本身太大”的解法,是发送最相关的那一部分地图。ctags.md 在 “Future work” 中畅想的“地图子集裁剪”,实际上已经以内建能力落地:

  • 构造依赖图:每个源文件是一个节点,文件间的依赖构成边;
  • 运行 PageRank 类图排序算法(实现见 get_ranked_tagsrepomap.py,其中调用了 networkx 的 pagerank);
  • 把“被引用最多”的定义视为代码库中最重要的符号优先呈现,再以二分逼近的方式反复生成树形地图并统计 token 数,直到产出的地图尽量贴近目标 token 预算(见 get_ranked_tags_maprepomap.py 中的 middle 试探与 to_tree 组装逻辑)。

因此,你现在看到的 repo map 样例并不包含文件里每一个类、方法、函数,只包含“被代码库其他部分引用最多”的关键标识符——这些才是模型理解整个代码库最需要的那部分上下文。可视化后的效果可参考站点文档 repomap.md 中展示的当前版本地图片段。

关键配置参数:调好你的 token 预算

repo map 的规模并非固定值,而是由若干 CLI 参数控制的预算与刷新策略共同决定的。这些参数在 args.py 中统一注册(Repomap settings 参数组):

参数 取值/默认 作用
--map-tokens 整数,默认跟随模型配置(RepoMap 内部默认 1024) 建议用于 repo map 的 token 预算;设为 0 可完全禁用 repo map
--map-refresh auto(默认)/ always / files / manual 控制 repo map 的刷新频率:auto 按需自动、always 每次都重建、files 仅当文件变化时、manual 仅在显式请求时刷新
--map-multiplier-no-files 浮点数,默认 2 当对话中尚未加入任何文件时,把地图 token 预算乘以该倍数,以便给模型一个更完整的全局视野

RepoMap 的构造签名为准(repomap.py),map_tokens 的代码层默认值正是 1024,与 ctags.md 中所记载的启动提示 “Repo-map: universal-ctags using 1024 tokens” 遥相呼应。

几个值得注意的行为细节(均有源码依据):

  • 无文件入聊时地图自动放大get_repo_maprepomap.py)在 chat_files 为空时会临时放大预算——目标值取 map_tokens × map_multiplier_no_filesmax_context_window - 4096(留出对话 padding)两者中的较小者(repomap.py)。这解释了为什么刚启动、还没 /add 文件时,aider 会尽量“通读”整个仓库。
  • 动态伸缩而非死板截断:地图大小会随对话状态动态调整,多数时候稳定在 --map-tokens 设定值以内,但在无文件入聊等场景会显著放大。
  • 禁用途径:设 --map-tokens 0 即关闭;RepoMap 也会在解析异常(如 RecursionError)时自动把 max_map_tokens 置 0 并输出 "Disabling repo map, git repo too large?" 提示(repomap.py)。

对话内命令:把地图“看”在眼里

为了在会话中验证与调试 repo map,aider 提供两条对话内命令(实现在 commands.py):

  • /map:打印当前发给模型的 repo map 内容;
  • /map-refresh:强制刷新 repo map(绕过缓存),之后可用 /map 查看刷新结果。

从源码看,cmd_map 直接调用 self.coder.get_repo_map() 并输出到终端;cmd_map_refresh 则以 force_refresh=True 触发一次完整重建。对于想搞清楚“aider 到底往上下文里塞了什么代码知识”的用户,这两条命令是最好的观察窗口。

值得说明的是,ctags.md 中建议的验证方式是“在仓库内运行 aider,看到启动提示 'Repo-map: universal-ctags using 1024 tokens'”。该提示来自当时的 ctags 版本;在现行 tree-sitter 版本中,仓库地图的 token 数统计逻辑依然存在,只是输出改为 "Repo-map: X.X k-tokens" 且由 verbose 开关控制(repomap.py)。

自动化验证:测试如何守护 repo map

仓库中针对 repo map 的自动化测试位于 test_repomap.py,可作为理解实现语义的补充材料:

  • test_get_repo_map 在临时目录中构造 .py.md.json 等空文件,验证 get_repo_map 产出的地图能包含这些文件路径;
  • test_repo_map_refresh_files 则在一个 git 临时仓库中写入多个含函数的 .py 文件,配合 refresh="files" 验证地图随文件变更的刷新行为。

这类测试印证了“repo map 的处理对象是 git 仓库中的源码文件,且刷新策略是可配置、可观察的”这一实现事实,也说明符号缓存、git 目录感知等细节都被纳入工程保障。

今天如何上手验证

按当前仓库的实际安装与使用方式来验证 repo map 的能力:

  1. 参照 install.md 安装 aider(现代版本随包附带 tree-sitter 及各类语言支持,无需再手工安装 universal-ctags——这一点与 ctags.md 记录的初代使用步骤不同);
  2. 在一个 git 仓库内启动 aider 并加入若干源码文件开始对话;
  3. 使用 /map 查看 aider 构建的仓库地图,确认其中包含目标文件的类/函数/签名概览;
  4. /map-refresh 体验强制刷新(地图内容依赖 chat 文件集合与符号重要性排序,会随对话状态变化);
  5. 若你的模型上下文窗口紧张,可用 --map-tokens(例如 --map-tokens 2048)或 --map-tokens 0(禁用)调节。

演进脉络小结

如果把三份文档连起来看,aider 的 repo map 走过了一条清晰的演进线:

  1. 问题不变:如何用有限上下文让模型理解整个代码库(ctags.md 的 Problem 部分);
  2. 第一代答案:universal-ctags 抽取符号 → 格式化树形地图,解决“有上下文可用”的问题,但受地图体积与人工安装负担所限;
  3. 第二代答案:tree-sitter 解析 AST + def/ref 关系 + PageRank 图排序 + token 预算二分逼近,解决“如何只发送最相关的部分”的问题,并以 repomap.py 的形式沉淀为今天的默认实现。

ctags.md 以“Try it out”与“Future work”收尾,其中关于“LSP 也许是更好的工具”的推断,以及“自动识别需要改动的文件”的设想,为后续演进保留了方向感。理解这段历史,有助于你更准确地判断:当你在终端里看到 aider 输出地图统计信息时,背后实际发生的是“AST 解析 → 符号建模 → 依赖图排序 → 预算内截取”这整套流水线,而不是简单地把 ctags 输出塞给大模型。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393