仓库导航与代码上下文

U2
实战手册 · 编码与计算机操作智能体

在大型仓库上,智能体的难题不是生成——而是找出真正要紧的那十二行。

真实代码库动辄数百万 token;上下文窗口没有那么大。因此每个编码智能体首先是检索系统,其次才是生成器:它必须从一个含糊的 issue 导航到精确的调用点,把恰好够用的仓库内容保留在工作记忆里以保证正确,并且不让其余 99.99% 毒化自己的上下文。本文涵盖代码搜索 vs 向量检索、符号级索引、在大型目录树上做上下文预算,以及为何代码检索自成一门学问。

STEP 1

代码不是散文;这里词法搜索通常胜过向量。

对代码做朴素 RAG——切块、嵌入、余弦检索——在智能体真正需要的操作上表现不佳:"这个符号定义在哪""谁调用了这个函数""这个 import 解析到什么"。这些是精确查询,而 grep/ripgrep 加一个符号索引能精确且可验证地回答它们,向量则只能模糊地回答。最强的导航栈是混合式:对标识符与调用图用词法与结构化搜索,把向量留给智能体尚不知道名字、确实语义化的查询("处理重试退避的那段代码")。

STEP 2

索引符号与图,而不只是文本。

扁平的文本索引无法回答"查找引用"。一份由解析器或 LSP/ctags/tree-sitter 构建的符号索引,把跳转到定义查找引用作为原语工具交给智能体——这正是人类工程师在代码里穿梭所用的能力。这把导航从"读文件碰运气"变成图遍历:从 issue 的表层符号跳到其定义,再跳到其调用者,每跳一步收窄候选编辑集,而不是用整文件淹没上下文。

# navigation as graph traversal, not file dumping
hits  = repo.grep(symbol)                 # lexical: exact, cheap, verifiable
defn  = repo.goto_def(hits[0])           # structural: one true site
calls = repo.refs(defn)                  # who depends on this?
ctx   = budget.select(defn, calls, k=12)  # keep ~12 spans, not 12 files

用视窗化视图而非整文件。一个 open(path, line, ±40) 视口让智能体盯住相关片段,并为调用图留出预算;转储 1500 行模块会把窗口花在噪声上,之后每轮还得重新略读。

STEP 3

上下文预算是一个带硬上限的分配问题。

把窗口当作一份固定预算,在四个相互竞争的索取者之间切分:任务(issue + 复现)、待编辑代码(你将改动的片段)、证据(佐证此改动的调用者、测试、跟踪),以及草稿区(智能体自身的推理与历史)。花在无关文件上的每一个 token,都是从证据那里偷来的。这门纪律要求无情驱逐:为检验某个被反驳假设而读入的文件,应当压成一行并丢弃,而不是永远背着。

STEP 4

代码检索有个精度问题,必须由智能体来管控。

handlerconfig 这样的符号会命中数百个无关位置;语义检索会返回貌似合理实则错误的近邻。不加过滤,这就是上下文中毒——模型锚定在一个被自信检索出来的错误文件上并去编辑它。缓解办法:按与失败栈帧的结构邻近度排序,而非仅按命中数;优先取 traceback 点名的文件;并让智能体在编辑前说出其定位假设并通过复现缺陷加以确认,使糟糕检索由裁决者而非 diff 来抓住。

U2 中代价最高的失败是自信的错误定位:检索出的文件看着对、补丁干净、连目标测试都通过——而别处某条未被测试的调用路径现在崩了。没有结构性打地基的高检索召回率,正好制造这种情况。

STEP 5

仓库地图:一份廉价、持久的骨架胜过反复探索。

每个任务都重新发现项目布局纯属浪费。一份仓库地图——对顶层包、关键模块及其公共符号做的紧凑、排序后的提纲(Aider 推广的技术)——用几百 token 给智能体一个持久的心智模型,让导航从"我大致知道鉴权代码在哪"而非从零开始。它在仓库变化时重建,跨压缩钉在工作记忆里,是非任务上下文中杠杆率最高的单一部件。

STEP 6

当廉价路径走不通时。

对结构良好、可静态分析、命名有意义的代码,词法加符号搜索占绝对优势。它在动态分派、元编程、代码生成、标识符复用的 monorepo,以及名字毫无信息量的无文档遗留代码上退化——在那里,向量检索和把测试当文档来读才值回成本。默认采用精确的结构化导航,仅在代码结构不再说真话之处加入语义检索;代码 RAG 的失败模式不是找不到答案,而是自信地检索出错误的那个。