大型 Monorepo 的索引新鲜度:为编程 Agent 提供增量上下文
搜索有结果不代表结果属于当前提交。本文用 commit 水位、变更日志、删除与重命名处理、脏工作树覆盖层和原子索引发布保证 Agent 不读取过期符号。
本文要点
- 主题聚焦:AI Agent、TypeScript、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
大型 monorepo 的符号、引用和摘要索引可能需要数十分钟构建。编程 Agent 发起查询时看到函数定义和调用方,并不代表这些结果属于当前 HEAD:文件可能已经重命名,导出已删除,工作树还有未提交修改。过期索引比没有索引更危险,因为它会提供看似精确的错误证据。
编程 Agent 的仓库地图描述仓库事实层,本文关注索引如何证明“我覆盖到哪个提交、哪些变更尚未合入,以及查询结果怎样降级”。
本文命令固定在 Linux/GNU CI 容器(Bash、GNU coreutils、Git)执行;macOS/Windows runner 必须使用同一容器。
新鲜度是版本关系,不是更新时间
索引清单记录源 commit、索引器与语言服务版本、配置 digest、包含和排除规则、分片、构建状态及内容摘要。updatedAt=刚刚 没有意义:它可能刚刚完成的是旧分支。只有发布状态为 complete 且清单摘要通过校验的版本才能对外查询。
{
"sourceCommit": "7a50f4d",
"indexer": "code-indexer@sha256:31c9...",
"configDigest": "sha256:90ab...",
"shards": 128,
"completedShards": 128,
"state": "complete"
}
查询服务比较 sourceCommit、当前 HEAD 和脏工作树。索引提交等于 HEAD 且工作树干净时可直接使用;索引是 HEAD 的祖先且差分在预算内时,加载增量覆盖层;分支已经分叉、配置变化或事件丢失时,索引不可证明新鲜,必须重建或直接读源码。
HEAD_SHA=$(git rev-parse HEAD)
INDEX_SHA=$(jq -r .sourceCommit .agent-index/manifest.json)
test "$(git merge-base "$INDEX_SHA" "$HEAD_SHA")" = "$INDEX_SHA"
git diff --name-status --find-renames "$INDEX_SHA..$HEAD_SHA"
TMP_ROOT=$(mktemp -d)
TMP_INDEX="$TMP_ROOT/index"
cleanup() {
test ! -e "$TMP_INDEX" || unlink "$TMP_INDEX"
test ! -e "$TMP_INDEX.lock" || unlink "$TMP_INDEX.lock"
rmdir -- "$TMP_ROOT" 2>/dev/null || true
}
trap cleanup EXIT
GIT_INDEX_FILE="$TMP_INDEX" git read-tree HEAD
GIT_INDEX_FILE="$TMP_INDEX" git -c core.filemode=true add -A -- .
WORKTREE_TREE=$(GIT_INDEX_FILE="$TMP_INDEX" git write-tree)
printf 'head=%s\nworktreeTree=%s\n' "$HEAD_SHA" "$WORKTREE_TREE"
cleanup
trap - EXIT
临时 index 从 HEAD 纳入已跟踪改动、删除及未忽略的未跟踪文件。tree OID 覆盖真实 blob、完整路径与 Git mode,且无需解析文件名。查询返回 indexCommit、headCommit、worktreeTree 和源码范围。子模块脏状态不进入父 tree,需另记子模块 HEAD 与 tree。
增量日志必须表达删除和重命名
文件监听器适合低延迟提示,但不能作为唯一事实。进程停机、队列溢出或 Watchman recrawl 都可能漏事件。增量器以 Git 提交差分作为可重建主线,工作树事件作为临时覆盖,并为每批分配单调序号和校验点。
新增文件解析后写入符号与依赖边;修改文件先按旧内容删除旧边,再写新边;删除文件产生 tombstone,阻止旧定义继续被返回;重命名同时保留身份迁移和路径变化。只处理“当前存在文件”会留下幽灵符号,尤其会误导调用方搜索。
批次在暂存索引完成,校验文档数、符号数、反向边和 tombstone 后,原子切换 current 指针。读请求要么看到旧的完整版本,要么看到新的完整版本,不能读到 128 个分片中只完成 90 个的混合状态。该一致性思路与RAG 增量索引一致性相似,但代码索引还必须处理符号绑定、编译配置和生成文件边界。
上下文包由基础索引和覆盖层合成
任务从目标符号或文件出发,在基础图上展开定义、调用方、测试和配置,再用提交差分与脏工作树覆盖。修改过的文件优先由磁盘直接读取,删除节点从候选中剔除,新文件进行即时解析。摘要只有在源内容 hash 相同时才能复用。
上下文选择受明确预算控制:先保留任务目标、公共契约和高风险调用方,再按依赖距离加入实现与测试,最后才加入历史说明。编程 Agent 的上下文预算负责压缩策略;索引层必须保证交给它的候选不是旧版本。
若增量文件超过 500 个、工作区配置或 lockfile 改变、语言服务重启后无法恢复同一项目图,直接触发分片重建。阈值来自压测,不应照抄示例。超预算期间可以对少量目标执行 LSP 与源码直读,明确标记“partial”,不能返回空调用方并声称无影响。
用已知变更校准新鲜度门
验收集至少包含跨包改名、删除公共导出、移动文件、修改路径别名、生成文件更新、未提交编辑、强制切分支和监听器事件丢失。每个场景查询定义与引用,要求结果与从当前源码全量构建的黄金索引一致。测试影响范围可借鉴编程 Agent 的测试影响分析,但索引验收还要验证返回内容和行号确实属于当前版本。
生产指标追踪索引落后提交数、增量队列年龄、recrawl、tombstone、查询回退比例、脏覆盖解析耗时和新鲜度拒绝数。只监控“索引任务成功”会漏掉 current 指针未切换、分片缺失和查询服务仍读旧版本。
回滚索引,不回滚源码
新索引出现错误时,将 current 指针原子切回上一份完整且与其 sourceCommit 匹配的版本,清空不兼容覆盖层,再从当前 HEAD 重建。旧索引若落后当前代码,只能用于诊断,查询必须继续直接读源码或暂停自动改写。绝不能为了让索引重新“匹配”而回滚开发者代码。
本地版本目录可用一次原子指针替换恢复,LAST_GOOD 必须来自已签名发布记录:
test "$(jq -r .state ".agent-index/versions/$LAST_GOOD/manifest.json")" \
= "complete"
ln -sfn "versions/$LAST_GOOD" .agent-index/current.next
mv -Tf .agent-index/current.next .agent-index/current
对象存储则使用带旧版本条件的 compare-and-swap,避免两个恢复任务互相覆盖。指针切换后仍要比较该版本水位与当前 HEAD;只要落后,就强制源码直读并排队重建。
大型仓库可由Monorepo 工具链提供项目边界和任务图,但 Agent 能否信任查询,最终取决于 commit 水位、事件完整性、覆盖层和失败降级是否都可验证。新鲜度不是一个时间戳,而是一条从当前代码到每段上下文都能复算的版本证明。
Related