在大型仓库上,智能体的难题不是生成——而是找出真正要紧的那十二行。
真实代码库动辄数百万 token;上下文窗口没有那么大。因此每个编码智能体首先是检索系统,其次才是生成器:它必须从一个含糊的 issue 导航到精确的调用点,把恰好够用的仓库内容保留在工作记忆里以保证正确,并且不让其余 99.99% 毒化自己的上下文。本文涵盖代码搜索 vs 向量检索、符号级索引、在大型目录树上做上下文预算,以及为何代码检索自成一门学问。
代码不是散文;这里词法搜索通常胜过向量。
对代码做朴素 RAG——切块、嵌入、余弦检索——在智能体真正需要的操作上表现不佳:"这个符号定义在哪""谁调用了这个函数""这个 import 解析到什么"。这些是精确查询,而 grep/ripgrep 加一个符号索引能精确且可验证地回答它们,向量则只能模糊地回答。最强的导航栈是混合式:对标识符与调用图用词法与结构化搜索,把向量留给智能体尚不知道名字、确实语义化的查询("处理重试退避的那段代码")。
索引符号与图,而不只是文本。
扁平的文本索引无法回答"查找引用"。一份由解析器或 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 行模块会把窗口花在噪声上,之后每轮还得重新略读。
上下文预算是一个带硬上限的分配问题。
把窗口当作一份固定预算,在四个相互竞争的索取者之间切分:任务(issue + 复现)、待编辑代码(你将改动的片段)、证据(佐证此改动的调用者、测试、跟踪),以及草稿区(智能体自身的推理与历史)。花在无关文件上的每一个 token,都是从证据那里偷来的。这门纪律要求无情驱逐:为检验某个被反驳假设而读入的文件,应当压成一行并丢弃,而不是永远背着。
代码检索有个精度问题,必须由智能体来管控。
像 handler 或 config 这样的符号会命中数百个无关位置;语义检索会返回貌似合理实则错误的近邻。不加过滤,这就是上下文中毒——模型锚定在一个被自信检索出来的错误文件上并去编辑它。缓解办法:按与失败栈帧的结构邻近度排序,而非仅按命中数;优先取 traceback 点名的文件;并让智能体在编辑前说出其定位假设并通过复现缺陷加以确认,使糟糕检索由裁决者而非 diff 来抓住。
U2 中代价最高的失败是自信的错误定位:检索出的文件看着对、补丁干净、连目标测试都通过——而别处某条未被测试的调用路径现在崩了。没有结构性打地基的高检索召回率,正好制造这种情况。
仓库地图:一份廉价、持久的骨架胜过反复探索。
每个任务都重新发现项目布局纯属浪费。一份仓库地图——对顶层包、关键模块及其公共符号做的紧凑、排序后的提纲(Aider 推广的技术)——用几百 token 给智能体一个持久的心智模型,让导航从"我大致知道鉴权代码在哪"而非从零开始。它在仓库变化时重建,跨压缩钉在工作记忆里,是非任务上下文中杠杆率最高的单一部件。
当廉价路径走不通时。
对结构良好、可静态分析、命名有意义的代码,词法加符号搜索占绝对优势。它在动态分派、元编程、代码生成、标识符复用的 monorepo,以及名字毫无信息量的无文档遗留代码上退化——在那里,向量检索和把测试当文档来读才值回成本。默认采用精确的结构化导航,仅在代码结构不再说真话之处加入语义检索;代码 RAG 的失败模式不是找不到答案,而是自信地检索出错误的那个。