给编码 Agent 加上可验证的上下文边界
文章提出在模型与代码库之间增加上下文管理层,显式记录 Agent 的知识边界,减少测试通过却引入回归的问题。内容涵盖分层架构、核心组件实现及生产验证模式。
AI 深度解读
AI 编程代理修完代码、跑绿测试,却在几天后引发线上回归,问题可能出在它根本没读到关键配置和架构约束。原文提出在模型与代码库之间增加一层 Context Harness:先检查上下文,再允许模型生成和应用补丁。
这个思路值得借鉴,但原文更接近一份 TypeScript 架构草案,没有提供生产实测或故障率对照。读它最有价值的地方,是学会把「模型应该知道什么」变成可检查的规则,同时看清这些规则能保证到哪一步。
测试通过,为什么还会修出线上问题
作者用一个支付服务的假设场景解释「静默失败」(Silent Failure)。
任务是修复 paymentService.spec.ts 中不稳定的测试。代理读了测试和部分源码,在 paymentService.ts 中加入重试机制,测试通过,于是报告成功。但它漏掉了三件事:
paymentConfig.ts已经定义重试次数,代理却硬编码了3。- 限流中间件会与重试逻辑冲突,在负载下触发
429。 - README 要求通过环境变量配置重试,补丁没有遵守。
这些问题未必产生类型错误,也未必落在现有测试的覆盖范围内。代码局部成立,放回系统却不成立。
作者把原因归结为上下文不足:代理拿到的信息足以写出一个看似合理的补丁,却不足以判断它是否符合整个项目。
测试通过只能证明已有检查通过,不能证明代理看过所有相关约束。
原文认为,linters、类型检查、测试和 CI 主要验证产出,Context Harness 则验证输入。不过这条界线没有那么绝对:架构规则也可以进入 CI,Harness 自身也需要检查生成后的补丁。真正有用的区别,是把检查提前到模型调用之前,并保留应用补丁前的第二道检查。
Context Harness 要把哪些事管起来
Context Harness 可以理解成上下文的管理与准入层。它记录代理读过什么、内容来自哪里,以及缺少哪些材料时必须停止。
作者把它拆成四层。
第一层负责获取。除了检索目标文件,还要补齐导入模块、配置和类型定义。生产方案可以使用 embeddings 或代码图遍历,并按函数、类型、常量截取内容,减少整文件带来的噪声。这里的「依赖闭包」,就是沿依赖关系继续查找,而不是找到目标文件就停下。
第二层负责来源追踪。文件路径、Git 提交、获取时间和检索置信度都要记录。否则代理运行很久后,即使文件被其他进程修改,也可能继续依据旧内容操作。
第三层负责校验不变量(Invariant),也就是任务执行前必须成立的条件。例如:目标文件的导入能够解析,重试配置已经出现,相关中间件已进入上下文。
原文通过下面的注册代码,为支付服务任务声明必要材料:
// setup-harness.ts
import { ContextHarness } from './harness';
import { ImportResolutionInvariant, RequiredSymbolInvariant } from './invariants';
export function createHarness(store: ContextStore): ContextHarness {
const invariants = [
// 目标文件的所有导入都必须能够解析
new ImportResolutionInvariant(['src/services/paymentService.ts']),
// 上下文中必须包含重试配置
new RequiredSymbolInvariant('retryCount', 'paymentConfig'),
// 代理必须能够看到限流中间件
new RequiredSymbolInvariant('rateLimit', 'middleware'),
];
return new ContextHarness(store, invariants);
}
重点看两个 RequiredSymbolInvariant:系统能发现遗漏,是因为有人提前写明必须存在 retryCount 和 rateLimit。它没有自动推导出所有业务约束。代码片段还缺少 ContextStore 的导入,复制时需要补齐。
第四层负责执行拦截。补丁只能触及已提供上下文的文件,不能引用禁止访问的符号;不满足条件就拒绝应用,并返回缺失项和修复提示。
这样,「大概读够了」才有机会变成一个明确的执行条件。
真正难的是补齐、保鲜和验证补丁
作者设计的运行顺序是:获取上下文,调用 validate(),通过后执行 callLLM(),再用 enforcePatch() 检查补丁,最后调用 repo.applyPatch()。
校验失败会抛出 ContextValidationError,携带失败结果与 contextHash。生产流程可以从 failures 中收集 missing,解析对应路径,补读文件再检查。示例设置 maxRetries = 3,避免无限补充;错误提示还可以给出 --include src/config/paymentConfig.ts 这样的补救参数。
来源记录则用于检查内容是否过期。下面的代码通过提交标识变化,阻止代理继续使用旧文件:
export function checkStaleness(store: ContextStore, repo: any): void {
for (const entry of store.getAll()) {
if (entry.sourceType !== 'file') continue;
const currentCommit = repo.getCommit(entry.sourcePath);
if (currentCommit !== entry.gitCommit) {
throw new Error(
`Context for '${entry.sourcePath}' is stale. ` +
`Expected commit ${entry.gitCommit}, found ${currentCommit}. ` +
`Re-fetch before proceeding.`
);
}
}
}
关键是 currentCommit !== entry.gitCommit。它能检查提交标识变化,但若 getCommit() 只返回 Git 提交,就发现不了工作区尚未提交的修改。原文没有补足这部分。
作者还建议记录 store.hash(),使用 sha256 生成上下文摘要,并把校验规则与时间写入日志,方便事后追查。不过摘要只能帮助核对内容是否一致,不能单独还原代理当时读到的材料。
置信度也被纳入设计:示例给检索结果 0.95、依赖文件 0.9、补充文件 1.0,assertConfidence() 的默认阈值为 0.8。这些都是示例赋值,没有校准依据。更具体的问题是,该函数只执行 console.warn(),没有真正把结果标成 needs-review,也没有阻止应用补丁。
语义检查同样需要接入正确位置。作者提出检查 @pure 函数是否引入 console.log、fs.writeFile,配置是否符合 JSON schema,以及 services/ 是否导入 ui/。但示例的架构检查遍历的是上下文存储中的代码,没有展示如何对应用补丁后的内容重新检查。
校验旧代码通过,不等于生成的新代码也满足规则。
我的判断
我认同把已知约束前移。对维护 Node 服务、CLI 或 VSCode 扩展的团队来说,把配置、依赖和允许修改的范围显式化,比反复提醒代理「仔细检查」更容易执行和审计。
但原文的核心结论需要收窄。「静默失败很少是模型问题」没有对照数据支持;作者也承认,即使上下文充分,模型仍可能推理错误。文章末尾还被截断,不能把收益列表当成完整的生产验证报告。
实现层面也有明显距离。extractImports() 用正则提取导入字符串,再直接交给 getByPath() 查找,没有展示相对路径、扩展名、别名和包依赖的解析。RequiredSymbolInvariant 使用 includes(),只能证明字符串出现过,不能证明符号定义和语义正确。
获取阶段只对名称含 paymentService 的目标补一轮依赖,也没有实现完整的递归闭包。README 里的环境变量要求,在注册规则中同样没有对应检查。
更根本的局限是:Harness 能拦住已经编码的遗漏,不能证明未知约束不存在。相比只扩大上下文窗口,它的优势是条件明确;相比现有测试和架构检查,它的新增价值主要在执行时机、来源记录和定向补齐。三者适合配合使用,原文没有证明其中任何一种可以替代其他方案。
能拿来做什么
- 给 AI 修改 Node 服务的流程增加一张任务上下文清单,明确目标源码、测试、配置和相关中间件。先覆盖团队已经踩过的坑,再逐步增加规则。
- 把生成补丁与应用补丁分开。先检查修改路径和依赖,再对补丁应用后的代码执行类型检查、测试及架构约束检查。
- 为长时间运行的代理记录文件版本、上下文摘要和校验结果;应用补丁前核对工作区内容,发现变化就重新读取并验证。
- 对补齐流程设置次数上限,并明确低置信度结果的处理动作。若只是打印警告,就不要把输出标记为已经验证。