跳到主要内容

你的编程代理基于落后 Main 分支三周的代码版本重建的代码库索引

· 阅读需 11 分钟
Tian Pan
Software Engineer

你团队中的一个 AI 编程 Agent 提交了一个 PR,在两个文件中调用了四次 parseUserToken()。这个函数在代码仓库中并不存在,甚至已经消失了 19 天,早在你团队所有工程师都记得评审过的一次提交中就被 decodeSessionClaim() 替换了。Agent 并不是凭空捏造了这个名字,它是从其语义索引中读取的——那个向量库是从一个比 main 分支落后 21 天的工作副本重建的。相比之下,Agent 的编辑步骤在会话开始时运行了 git pull,操作的是最新的代码。对同一个代码库的两个视角,相隔三周,而 Agent 却自信地用一段无法针对任何真实环境编译的代码桥接了它们。

这是一种不会自我宣告的失败模式。Agent 运行了。测试看起来通过了。PR 合并了。第一位评审者之所以注意到,仅仅是因为一个被删减的函数与一个无关的辅助函数重名,触发了 linter 报错。到那时,Agent 已经花了一个完整的冲刺(sprint)针对一个“幻影版本”的代码库进行编写,而团队中没有一个人——包括 Agent 自己——收到任何异常信号。

Agent 对代码库的“理解”与代码库的“实际状态”之间的缝隙,是一个没人会在架构图上画出来的“一致性边界”。索引由一个团队按一种节奏更新;工作树(working tree)由另一个团队按另一种节奏获取。Agent 的推理存在于第一个层面,而 Agent 的动作落在了第二个层面。当两者即便只差几次提交——更不用说三周——结果就是一个 Agent 会基于一个已不存在的代码库,提出极其自信的主张。

偏差是如何产生的

索引和工作树是平台回答“这个仓库里有什么代码”的两种方式。大多数 Agent 平台将它们存放在不同的地方,按不同的时间表刷新,并假设结果是一致的。事实往往并非如此。

最常见的偏差来源是镜像缓存(mirror cache)。为了应对 GitHub 的速率限制并加速冷克隆(cold clones),Agent 平台会为每个跟踪的仓库保留一个本地镜像,并定期(每隔几分钟、每小时甚至更久)从 origin 刷新。多年来,这种镜像一直是与 CDN 类似的基础设施;像 gitcachegit-cache-clone 这样的工具对此有明确说明。刷新间隔是可配置的,而问题正出在这里。如果刷新任务在平台迁移期间被暂停、限速、误配置或遗忘,镜像就会在无形中落后于 origin。索引从镜像中提取,而工作树直接从 origin 提取。两者因此产生分歧。

其他诱因则更为微妙。出于性能考虑,语义索引在不同会话之间是持久化的——重新计算嵌入(embeddings)成本高昂,大多数 Agent 平台都会积极复用它们。在大规模分支切换、重写了数千个文件的依赖项升级、生成的代码重新生成或 LFS 拉取之后,索引可能会携带指向已不存在的代码的孤立条目。Cursor 的文档承认了这一点:当自动补全开始引用你三个分支前删除的文件时,说明索引中存在陈旧条目。Cursor 用来增量重新索引的 Merkle 树变化检测器在查找“已修改”文件方面效率很高,但在结构性重写时,它的失效处理并不总是足够激进。

第三个诱因是索引失败但检索未失败。索引器在运行中途崩溃,留下了一个不完整的索引,而 Agent 平台继续根据最后一次索引的内容提供搜索结果。Agent 无法知道它的结果中现在选择性地遗漏了最后六次提交的更改。搜索结果依然自信地返回。这种“自信”正是问题所在。

Agent 在陈旧视图下会做什么

编程 Agent 使用索引做两件事:搜索(“帮我找到处理身份验证的函数”)和 Grounding(知识对齐,“这是我要调用的函数签名”)。两者都以不同的方式受到陈旧性的威胁。

搜索功能的退化是悄无声息的。陈旧的索引会返回“过去”实现某个功能的某个文件,Agent 会将其视为真理。如果该文件已被拆分、重命名或合并到另一个模块中,那么从第一次搜索开始,Agent 关于逻辑所在位置的心智模型就是错误的。后续的搜索无法修复这一点——它们通常会变本加厉,因为 Agent 的后续查询是基于第一次搜索返回的内容。

Grounding 的失败有时会表现得更明显。如果 Agent 调用了一个不再存在的函数,编译器通常会捕获它——故事本该到此结束。但现代编程 Agent 的循环是“迭代直到测试通过”,而该循环的许多实现都在构建失败时提供了一个后备方案:删减调用、模拟(mock)返回值、注释掉报错的代码块。Agent 的提示词告诉它要让测试通过,于是它通过移除报错的部分来让测试通过。这个临时方案就这样被发布了。评审者看到 CI 运行通过且 diff 干净,便予以批准。

最阴险的情况是“部分重叠”。Agent 调用的函数确实存在,但其签名与索引记忆的不同。参数顺序改变了,增加了一个必填参数,或者返回类型被缩小了。代码在某些情况下可以编译,但在运行时会崩溃。Agent 永远不会收到明确的信号表明其 Grounding 是错误的,因为这种错误是间歇性的。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates