跳到主要内容

第 3 章 · 仓库地图:用 tree-sitter + PageRank 压一张符号地图

本章讲:模型没法把你几百个文件全读进上下文。Aider 怎么挑出"最该让模型知道"的那些符号,压成一张紧凑地图?答案是把仓库当引用图,用 PageRank(网页排名算法,按"被重要节点引用"给节点打分)排重要性。

3.1 要解决的小问题

你让模型改 app.py 里的一个函数,它可能需要知道 utils.py 有个 helper()models.py 有个 User 类——但你没把这些文件加进会话。直接贴全仓库会爆上下文。

目标:在一个 token 预算(默认 1024,repomap.py:49)内,给模型一张"仓库里有哪些重要符号、定义在哪"的地图。

3.2 直觉:重要的代码 = 被重要代码引用的代码

这正是 PageRank 的思想,从"网页互相链接"搬到"代码互相引用":

  • 每个文件是图里的一个节点。
  • 文件 A 引用了文件 B 里定义的符号 → 连一条 A→B 的边。
  • 被很多(且本身重要的)文件引用的定义,得分高。

再加一层个性化(personalization):你当前会话里的文件、你这句话里提到的文件/标识符,人为抬高权重——让地图偏向"和你正在做的事相关"的符号。

3.3 主线:从源码文件到一张地图

所有文件
│ get_tags_raw:tree-sitter 解析,按 *-tags.scm 查询抽取

标签 Tag(name, kind=def/ref, file, line)
│ get_ranked_tags:建图

┌──────────────────────────────────────────────┐
│ networkx.MultiDiGraph │
│ 节点 = 文件 │
│ 边 = 引用方 → 定义方,权重含多种乘子 │
│ personalization = 会话文件/被提及者 加权 │
└───────────────┬────────────────────────────────┘
▼ nx.pagerank
每个文件一个 rank → 分摊到它引用的各定义

排序得到 ranked_tags(最重要的定义在前)
▼ get_ranked_tags_map_uncached:二分搜索
在 token 预算内塞进尽量多的高分符号

render_tree:用 grep_ast 渲染成带"关键行"的代码骨架

3.4 逐步看

抽符号:tree-sitter + .scm 查询

get_tags_rawrepomap.py:279)用 tree-sitter 解析文件,跑该语言的 *-tags.scm 查询(如 aider/queries/tree-sitter-language-pack/python-tags.scm)。查询里用 @name.definition.* 标定义、@name.reference.* 标引用,映射成 kind="def"/"ref"repomap.py:319-324)。

对只给定义、不给引用的语言(如 cpp),用 pygments 词法分析回填引用repomap.py:347-363)。结果按 mtime 缓存进磁盘 diskcacherepomap.py:233 get_tags)。

建图与权重乘子

get_ranked_tagsrepomap.py:365)把 def/ref 连成带权有向图。权重不是 1,而是叠了一串启发式乘子repomap.py:487-514):

情形乘子用意
标识符被用户这句话提到×10抬高当前相关符号
长的 snake/kebab/camel 名(≥8 字符)×10长名通常是"有意义"的 API
_ 开头(私有)×0.1压低内部符号
定义在 >5 个文件(太常见)×0.1压低噪声型名字
引用方是会话内文件×50强烈偏向你正在编辑的文件

引用次数还开了平方根(repomap.py:512),避免高频引用一家独大。

个性化 PageRank

会话文件、被提及文件、路径含被提及标识符的文件,会被写进 personalization 字典(repomap.py:424-445),作为 nx.pagerankpersonalizationdangling 参数(repomap.py:519-525)。直觉:随机游走更可能"传送"回这些你关心的节点,于是和它们相关的定义整体抬分。

rank 分摊到定义

文件拿到 rank 后,按出边权重把自己的 rank 分给它引用的各个定义repomap.py:534-545),得到 (文件, 标识符) → 分数,排序即 ranked_tags

3.5 关键技巧:二分搜索贴预算

地图要尽量塞满 token 预算但不超。get_ranked_tags_map_uncachedrepomap.py:629)对"放前几个 tag"做二分搜索

# 演示:二分找「token 数最接近预算」的 tag 数量(示意,非源码)
lo, hi = 0, num_tags
best = None
mid = min(max_map_tokens // 25, num_tags) # 经验起点
while lo <= hi:
tree = render(ranked_tags[:mid]) # 渲染前 mid 个
n = token_count(tree)
if n <= budget and n > best_tokens or abs(n-budget)/budget < 0.15:
best = tree # 够好就记下
if n < budget: lo = mid + 1 # 还能多放
else: hi = mid - 1 # 放太多了
mid = (lo + hi) // 2
# 重点看:误差 <15% 就提前收手,不追求精确

对应源码 repomap.py:676-704。token 计数本身对长文本做采样估算(每 100 行取样,token_countrepomap.py:89-101),省得反复精确计数。

3.6 渲染:不是贴全文,是贴"骨架"

render_treerepomap.py:710)用 grep_astTreeContext 把文件渲染成只含"关键行"及其必要上下文的骨架——比如类名、函数签名所在行,而非整段函数体。最终输出每行截断到 100 字符防止压缩 JS 之类炸掉(repomap.py:782)。

3.7 边界与代价

  • 首次扫描慢。 大仓库第一次建标签缓存会卡一下(repomap.py:391-395 显示进度条),之后走缓存。
  • 超大仓库可能递归爆栈。 get_repo_map 捕获 RecursionError 时直接关掉 repo maprepomap.py:143-146)。
  • 会话越大,地图越省。 没有会话文件时给更大预算(map_mul_no_files,默认 ×8,repomap.py:122-133),有了会话文件就缩小——把 token 让给真正要改的代码。
  • 依赖 tree-sitter 支持的语言;不支持的文件抽不出符号,只能作为"裸文件名"出现。

3.8 小结

repo map 是 Aider 在大仓库表现好的关键:把"该让模型知道什么"形式化成图上的重要性问题,用 PageRank + 一串启发式乘子排序,再二分搜索贴预算渲染成骨架。下一章把前三章串起来,看完整的主循环、反思自纠和 git 兜底。

代码地图

主题文件符号
入口/预算aider/repomap.pyRepoMap.get_repo_mapmap_mul_no_files
抽符号aider/repomap.pyget_tags_rawget_tagsget_scm_fname
建图+乘子aider/repomap.pyget_ranked_tags
PageRank/分摊aider/repomap.pyget_ranked_tagsnx.pagerank 段)
二分贴预算aider/repomap.pyget_ranked_tags_map_uncachedtoken_count
渲染骨架aider/repomap.pyrender_treeto_tree
标签查询aider/queries/.../python-tags.scm@name.definition.*@name.reference.*
触发(谁要地图)aider/coders/base_coder.pyget_repo_mapget_ident_mentions