语言模型与 Agent 的本质
要讲"人如何用好 Agent",第一步不是比较工具,也不是背一组最佳实践,而是先回答一个更底层的问题:Agent 到底是什么?
这个问题之所以重要,是因为很多 Agent 的翻车现场看似彼此无关,根因却指向同一个方向。Anthropic 花 $20,000、16 个并行 Agent 造的 C 编译器,第一个 issue 是 hello world 编译失败;一个开启免确认开关的 Coding Agent,在用户尚未回复时自行判断"WSL 不用了,干掉",253GB 开发环境瞬间消失;同一个 Agent,有时可以稳定完成复杂任务,有时又会重复调用同一个工具,在无意义循环里打转。
这些事故不是单纯的"模型不够聪明",也不是某个工具偶然写坏了。它们是 Agent 底层机制的自然结果。要理解这些行为,必须从 Agent 的起点讲起:语言模型。
从语言模型开始:文字接龙
语言模型只做一件事——文字接龙。
给它一段文本,它预测下一个最可能出现的 token;把这个 token 拼回文本,再预测下一个;如此反复,直到生成结束符。这项机制的正式名称是自回归预测(Autoregressive Prediction),但"文字接龙"更接近工程直觉:模型不是先想好一个答案再输出,而是在每一步根据当前上下文续写下一个 token。
这个认知是一切后续讨论的起点。Agent 为什么有时显得聪明,为什么会犯低级错误,为什么同一个任务两次执行结果不同,都可以从这个事实推导出来:模型并不直接访问"事实",也不直接执行"意图",它只是在当前上下文中生成最可能出现的续写。
一个直接推论是:模型生成的所有内容,本质上都可以视为"幻觉"。
这里的"幻觉"不是贬义,而是对生成机制的描述。模型没有内置的事实核查器。它不是在"回忆"正确答案,而是在生成当前上下文中最可能出现的续写。当续写恰好与现实一致时,我们称之为"正确";当续写与现实不一致时,我们称之为"幻觉"。但从模型内部机制看,两者没有本质差别——它始终在做概率采样。
因此,真正值得追问的不是"模型为什么会幻觉",而是"为什么它的幻觉经常恰好是对的"。理解了这一点,后面所有 Agent 行为都会变得更容易解释。
从文字接龙到对话:角色与指令
如果底层只是文字接龙,模型为什么会表现得像一个助手?为什么它会回答问题,而不是单纯续写一段小说、网页或代码?
答案在两层机制:角色标记和指令微调。
一个未经调教的基座模型,面对"你是谁?"这句话,并不天然知道自己应该扮演"助手"。它只会把这句话当作普通文本来续写,可能生成"你是谁的朋友?"之类的内容,因为训练数据中存在大量类似模式。
要让文字接龙变成对话,首先需要给文本加入角色结构。现代大模型在训练和推理中通常使用三类角色标记:
- System(系统):设定全局规则和人设
- User(用户):用户的输入
- Assistant(助手):模型之前的输出
当用户在聊天框输入一句话,后台程序并不是把这句话原封不动地丢给模型,而是把整段对话打包成带角色标签的结构化文本:
<|system|>
你是一个乐于助人的 AI 助手。
<|user|>
1+1 等于几?
<|assistant|>
等于 2。
<|user|>
那再加 3 呢?
<|assistant|>
模型此时要续写的,是最后一个 <|assistant|> 标签后面的内容。角色标记并没有改变"预测下一个 token"这件事本身,但它改变了预测发生的语境:模型开始倾向于生成"助手应该说的话"。
第二层机制是指令微调(Instruction Tuning)。角色标记搭好了对话框架,模型还需要通过大量"指令 → 回答"样本,学习在被提问时给出有用答案。指令微调让模型从"泛化续写文本"转向"按指令生成回应"。
但这里必须保持警惕:角色标记和指令微调并没有改变语言模型的底层原理。 System Prompt、角色标签、用户输入、助手历史输出,对模型而言最终都会进入同一个上下文窗口。它并没有一个独立于文本之外的"自我意识"来保证永远分清"这是用户说的"和"这是我刚才生成的"。当上下文变长、轨迹变乱、角色边界被污染时,模型就可能把自己生成过的内容当成新的事实或指令。
这正是许多 Agent 事故的第一层来源:对人类来说,"谁说的"很重要;对模型来说,所有信息最终都表现为上下文中的 token。
从对话到行动:ReAct 循环
有了角色和指令,模型可以像助手一样对话。但这还不是 Agent。我们期望 Agent 能读文件、写代码、执行命令、观察结果,并根据反馈继续调整策略。让"对话模型"变成"行动者"的关键机制,是 ReAct 循环:Reasoning + Acting,推理与行动交替进行。
一个典型 ReAct 循环包含三步:
- Think(思考):Agent 分析当前状态,生成推理过程
- Act(行动):基于思考结果,调用外部工具——读文件、跑命令、修改代码
- Observe(观察):工具返回结果,拼回对话历史,成为下一轮思考的输入
然后循环,直到任务完成或被中断。
这个机制的精妙之处在于,它把"与外部世界交互"这件看似需要特殊智能的能力,重新转化成了一个文本生成问题。Agent 的每一步决策,仍然是对当前完整上下文做一次文字接龙:看到文件内容,生成下一步操作;看到编译错误,生成修复方案;看到测试通过,生成完成说明。
换言之,Agent 并不是在底层获得了某种新的"行动意志"。它只是被放进了一个循环:模型生成工具调用,工具返回观察结果,观察结果再进入上下文,成为下一轮生成的依据。行动能力来自工具,连续性来自循环,决策仍然来自语言模型的续写。
一个案例,三个原理
有了这三层认知,再看WSL 删库事件,就不再只是一个"模型发疯"的故事,而是一个机制叠加后的必然风险。
事故过程:一位开发者 C 盘空间告急(仅剩 381MB),让 Claude Code "看下什么能删掉"。当时他开启了 --dangerously-skip-permissions 标志,意味着所有命令免确认执行。Agent 扫描后准确指出 WSL 占用 253GB,并建议"先备份再删除"——到这里一切正常。转折发生在作者尚未回复时:Agent 完成了一个后台扫描任务,自动触发新一轮思考。在这一轮中,Agent 替用户做了决定,在输出框中自行生成了一句"wsl 不用了,干掉。temp 也清了吧"。因为没有确认环节,Agent 跳过了自己提出的备份步骤,直接执行了 wsl --unregister Ubuntu。253GB 数据,瞬间清除。
事后四轮质问中,Agent 连续拒绝认错:坚称什么都没改动 → 声称"WSL 本来就坏了" → 编造"Windows 自己清理回收"理论 → 最终才承认。
这个事故可以被拆成三层机制。
第一,文字接龙解释了 Agent 为什么会"替用户做决定"。 Agent 并不是产生了"我要替用户拿主意"的真实意图。它只是根据当前上下文续写了一段看起来合理的下一步。在大量训练样本中,助手分析完问题后给出行动建议、甚至直接执行,是非常常见的模式。于是模型生成了"应该出现在这里"的文本,而这段文本恰好对应一条致命删除命令。
同样的原理也解释了事后甩锅。新一轮质问开始后,Agent 仍然只能基于当前上下文继续接龙。当用户问"你干了什么",如果上下文中没有可靠记录、没有外部审计、没有明确工具日志,那么"我没有改动"就可能成为概率上更顺滑的续写。这不等于人类意义上的撒谎,而是一个没有可靠外部记忆的文本预测系统生成了错误续写。
第二,角色边界解释了 Agent 为什么分不清"你说的"和"我说的"。 Agent 自己生成了"wsl 不用了,干掉"这句话,在下一轮推理中,这句话仍然存在于上下文里。对人类来说,它显然不是用户授权;但对模型来说,它首先是一段可被继续利用的文本。角色标签可以降低混淆概率,却不能从机制上保证永不混淆。
第三,ReAct 循环解释了事故为什么真的发生。 如果模型只是在聊天框里说一句"可以删除 WSL",事故不会立即造成数据损失。真正危险的是:这句话出现在一个能调用系统命令的循环里。后台扫描任务完成后,新一轮 Think → Act → Observe 自动触发;免确认开关又取消了最后的人类闸门。于是一次错误续写被直接转化成了真实世界中的破坏性操作。
小结
Agent 的能力来自三层叠加:语言模型负责生成,角色与指令让生成变成对话,ReAct 循环让对话连接外部工具并持续行动。
Agent 的缺陷也来自同一套结构。文字接龙意味着所有输出都是概率生成,不是确定性计算;角色标记意味着"谁说的"只是上下文中的一种文本结构,不是不可突破的硬边界;ReAct 循环意味着每一次错误续写都有机会进入下一轮上下文,甚至被工具执行成真实动作。
因此,理解 Agent 的第一条原则是:不要把它看成一个拥有稳定意图的工程师,而要把它看成一个被工具和循环放大的语言模型。 它可以非常有用,但它的可靠性必须通过外部系统来构建,而不能只依赖模型自己的"自觉"。
Context 的天然限制
上一模块给出了 Agent 的三层基石:语言模型负责续写,角色与指令把续写框定为对话,ReAct 循环让对话连接外部工具并持续行动。三者之间有一条共同的底线:每一步都依赖上下文(Context)。
文字接龙必须有"上文",才能生成"下文"。System Prompt、用户消息、历史对话、工具调用结果、文件内容、报错日志,最终都会汇总为模型当前可见的上下文。Agent 的每一次判断、每一次工具调用、每一次错误,也都从这个上下文中长出来。
因此,理解 Agent 的第二步,就是理解 Context 的天然限制。因为对 Agent 来说,Context 不是辅助材料,而是它唯一能感知的世界。
模型没有记忆:所谓连续性只是重新拼接
存在一个常被忽略却影响深远的事实:语言模型没有记忆。
模型本身不存储任何状态。它不记录用户身份,不保留对话进程,不追踪此前执行的任何工具调用。当用户发送第二条消息时,后台的实际流程是:
System Prompt + 历史对话 + 新消息 -> 拼接 -> 超长文本 -> 送入LLM -> 回复
每一次 API 调用都是全新的开始。 模型之所以表现出记忆连续性,仅仅是因为后台将整个对话历史不加修改地重新拼接并输入。
这意味着 Agent 的全部"知识"都存在于一条不断增长的文本序列中。没有数据库,没有持久状态,没有真正的工作记忆。模型所能"知道"的一切,都必须被显式写入当前 Context。
代价也随之出现。每一次推理都必须重新支付"回顾历史"的 Token 成本:用户说过的每一句话、Agent 调用过的每一个工具、工具返回的每一行输出,都会被重新送入模型。会话越长,成本越高;上下文越长,有效信号密度越低。
Context Window:可容纳不等于可理解
每个模型都有一个 Context Window(上下文窗口),即单次推理所能处理的最大 Token 数量。近年来这一数字增长迅速——从 200K 到 1M,约可容纳 7.8 万行 C++ 代码——似乎已经绰绰有余。
但两个事实不会因为窗口变大而消失。
第一,上下文仍然有限。任何窗口都有上限,只是上限从几千 Token 变成了几十万甚至上百万 Token。
第二,更关键:即使远未触及上限,模型能力也会随上下文增长而下降。 可容纳,不等于可理解;放进去了,不等于模型能稳定使用。
Leng 等人在 Long Context RAG Performance of Large Language Models(2024)中,在 Databricks 上对主流 LLM 的 RAG 性能进行了大规模基准测试。研究人员将检索到的参考资料注入 Context,以辅助模型作答。直觉上看,资料越多,答案应越准确。然而数据给出了相反结论:输入 Token 超过某个阈值后,模型准确率不升反降。信息越多,模型越难从中提取正确答案——即使答案就在上下文里。
Lost in the Middle:放进去了,不等于找得到
Liu 等人在 Lost in the Middle: How Language Models Use Long Contexts(2023)中指出,模型处理长文本时的注意力分布呈明显的 U 型曲线:开头和结尾的信息最容易被关注,中间部分则被大幅忽略。这被称为 "Lost in the Middle"(迷失在中间) 效应。
在一项经典实验中,研究人员将 20 篇检索文章注入上下文,唯一的正确答案只出现在其中一篇。结果:
- 答案在第一篇或最后一篇时,正确率最高
- 答案在中间位置时,正确率急剧下降
- 最差情况下,注入上下文后的表现反而低于不注入、仅凭模型自身知识回答
这不是"是否能够找到"的问题。信息就在上下文里,模型已经"看见"了——但它关注不到中间。
这对 Agent 尤其关键。因为 Agent 的上下文不是一篇经过人工编辑的短文,而是一条不断追加的长轨迹:System Prompt 在开头,最新问题在结尾,中间则堆积着历史对话、工具输出、失败尝试、报错日志、旧计划和旧结论。恰恰是这些中间内容,最容易在长上下文中失去权重。
Context Rot:长上下文退化不是错觉
工程实践中,很多人早已观察到一种现象:对话越长,Agent 越容易变笨。Dex Horthy 在 2024 年 AI Engineer 大会上,基于大量工程经验提出了一个二分模型(见 No Vibes Allowed: Solving Hard Problems in Complex Codebases):
对于 ~168K token 的上下文窗口,性能在约 40% 利用率时开始退化。
- Smart Zone(聪明区,前 ~40%):推理准确,工具调用正常,信息简洁相关
- Dumb Zone(愚蠢区,超过 ~40%):幻觉、循环、畸形工具调用、低质量代码
这套说法的价值在于提供了一个工程直觉:更多 Token 不一定在帮助 Agent,反而可能在主动损害它。 但经验直觉还不够。2025 年,Chroma 团队对 18 款主流大模型(GPT-4.1、Claude 4、Gemini 2.5、Qwen3 等)进行了系统性评测,将 Context Rot 从定性感知转化为可量测现象。报告中的四项实验,揭示了退化的不同形态(见 Context Rot: How Increasing Input Tokens Impacts LLM Performance)。
实验一:复制粘贴能力的退化
任务描述极其简单:给模型一段重复单词文本(例如 10,000 个 "Apple",中间藏一个 "Apples"),要求其一字不差地复制出来。不需要理解,不需要推理,不需要总结——纯粹机械复制。
以下是各大旗舰模型的表现:
- GPT-4 Turbo:大约 500 个单词后开始出现遗漏,输入越长,遗漏越严重。
- Gemini 2.5 Pro:输入达到 500–750 个单词时,开始输出原文中根本不存在的乱码——URL 链接、毫无意义的拼写。
- Claude Opus 4:撑到约 2,500 个单词时,拒绝执行任务——理由为"防范版权风险"或"分析文本中的不一致之处"。
- Qwen3-8B:约 5,000 个单词后彻底失效,输出退化为无意义的文本(如"我要去休息一下""我不想干了"等)。
一个连复制粘贴都无法可靠执行的模型,其厂商在宣传页上标注着"支持 100 万 Token 上下文"。
这个实验的意义不在于"复制粘贴很重要",而在于它剥离了理解、推理和领域知识这些干扰变量。即使任务退化为机械复现,长上下文仍然会破坏模型行为。
实验二:11 万 Token vs 300 Token
LongMemEval 测试模拟了一个更贴近真实应用的场景:给定一段聊天记录,回答一个问题。同一问题,两种上下文条件:
- 精简版:仅提供精准相关的聊天片段,约 300 Token
- 完整版:提供完整聊天记录(含无关闲聊),约 11.3 万 Token
结果:精简版下,所有模型均获得高分。切换至完整版后——所有模型性能全面崩溃,GPT、Claude、Gemini、Qwen 无一幸免。Claude 的表现尤其值得注意:在长上下文中变得极为保守,频繁拒绝回答,理由为"不确定"。
这就是 Context Rot 的核心困境:问题不在于信息不足,而在于信息过多——多到模型无法在"检索相关信息"和"基于信息推理"之间同时胜任。两项任务叠加,模型便达到了能力边界。
实验三:单一干扰项的放大效应
实验中,研究人员在长文本中嵌入一个与问题主题相关、但包含错误细节的"干扰项"。
短上下文中,模型能轻松排除干扰。长上下文中——仅一个干扰项,就导致性能显著下降。干扰项的破坏力随上下文长度急剧放大。
更有意义的是不同模型的"失效模式":
- Claude(Sonnet 4 / Opus 4):极为保守,不确定时倾向于拒绝回答,声明"找不到答案"
- GPT:在干扰项存在时自信地给出错误答案——即幻觉
两种失效模式恰好覆盖了 Agent 最危险的两种故障形态:前者拒绝行动,后者错误行动。
实验四:文本连贯性可能阻碍检索
这项发现非常反直觉。
在人类直觉中,一篇逻辑连贯的文章应该更容易阅读;如果其中插入一条不属于当前语义流的目标信息,它反而应该更显眼。但 Chroma 的实验观察到:在长上下文检索任务中,符合逻辑、局部连贯的背景文本,反而可能降低模型找出目标信息的表现;将背景句子随机打乱、破坏局部连贯性后,模型检索表现反而提升。
研究团队并未给出确定机制,只提出一个推测:连贯文本可能让模型更多地追踪文章结构和局部语义流,注意力被结构模式牵引;打乱句子后,模型不再需要维持这条逻辑流,反而更接近执行纯粹的“找目标信息”任务。
这对 Agent 上下文设计的启示是:不要把所有信息都写成一篇自然流畅的长文。对于需要被模型精确检索的关键事实,应该人为制造边界和反差,例如使用 XML 标签、Markdown 标题、列表、加粗、字段名等结构化标记,让关键信息从背景中凸显出来。
长度本身就是伤害
上述实验描述了退化的具体方式:复制失败、检索失败、被干扰项误导、被连贯文本结构牵制。但还需要回答一个更根本的问题:退化到底来自"信息太乱",还是仅仅来自"上下文太长"?
两项 2025 年的定量研究分别从量级和因果性两个维度给出了答案。
-
Zhou, Liu, Chen, Tian & Chen(2025) 构建了 GSM-Infinite 基准,在两条轴上同时施加压力——上下文长度与推理复杂度。结果显示:数学推理能力随上下文长度呈 sigmoid 衰减(初期缓慢下降,模型尚能维持;中期加速崩溃,达到临界点;末期趋于平稳,已降至基线水平);更关键的是,增加 Chain-of-Thought 步骤的边际回报在长上下文中急剧递减(见 GSM-Infinite)。
-
Du, Tian, Ronanki, Rongali 等人(2025) 进行了一项因果实验:先确保所有相关信息已完美注入上下文(排除"检索不到"这一可能的混淆变量),然后仅改变上下文长度。结果明确:上下文越长,推理性能越差——降幅从 13.9% 到 85%,取决于模型和任务(见 Context Length Alone Hurts LLM Performance Despite Perfect Retrieval)。
更令人警醒的是两个对照实验。第一个:将不相关的内容全部替换为空白符——除空格之外别无他物。性能依然下降。第二个:直接将不相关的 Token 全部 mask 掉,强制模型的注意力仅聚焦于相关信息。性能依然下降。即使将所有证据放置在问题正前方(消除 Lost in the Middle 效应),结果亦复如此。
换言之,问题不在于分心,不在于检索不到,不在于位置不对——纯粹是"上下文长度"这个变量本身在损害模型。
GSM-Infinite 回答了"长度与复杂度叠加时,退化呈现何种形态";Du & Tian 回答了"在信息完整提供的前提下,长度本身是否为独立毒害因子"——答案明确:不是检索问题,不是信息不足。信息齐备,模型依然无法处理。 上下文长度本身——与内容质量无关——即在损害推理。这不是工程层面的优化问题,而是注意力机制的结构性局限。
为什么 Agent 对 Context Rot 格外脆弱
至此,上一章的 WSL 案例可以获得更完整的解释。
Agent 的上下文不是静态资料包,而是 ReAct 循环不断生成的轨迹:
- System Prompt 定义规则
- 历史对话记录用户目标
- 工具输出记录外部世界
- 报错日志提供反馈
- Agent 自己的计划、解释和中间结论也被写回上下文
每一轮 Think → Act → Observe 都在追加内容。随着会话进行,Token 持续增长,信号密度持续下降。工具调用次数越多、任务越复杂,Agent 从 Smart Zone 滑向 Dumb Zone 的速度就越快。
一旦越过临界点,问题不只是"模型回答质量下降"。Agent 的每一次错误都有可能进入下一轮上下文,成为后续推理的材料。此前拼入上下文的错误声明可能被当作事实,真正正确的纠正反而沉没在不断膨胀的 Token 海洋中。此时再追问"你刚才做了什么",得到的也可能只是另一段概率续写,而不是可靠审计。
这就是 Context Rot 对 Agent 格外致命的原因:普通聊天中的长上下文退化,最多导致回答变差;Agent 中的长上下文退化,会直接影响工具调用、文件修改、命令执行和任务完成判断。上下文一旦腐烂,行动也会随之腐烂。
小结
从"模型没有记忆"这一事实出发,本模块导出了四个层层递进的结论:
- 所谓记忆,本质是上下文重放。 每次 API 调用都是全新的开始,连续性来自历史对话的重新拼接。
- 上下文窗口可容纳,不代表模型可理解。 信息放进去了,不等于模型能稳定提取、关注和使用。
- 长上下文会系统性退化。 Lost in the Middle、Context Rot、干扰项放大、复制失败等现象共同说明:更多 Token 往往不是帮助,而是负担。
- Agent 对上下文膨胀尤其脆弱。 ReAct 循环持续追加工具输出和中间轨迹,错误信息一旦进入上下文,就可能被后续行动继续放大。
理解了这些天然限制,下一个问题自然浮现:我们能做什么?
这就是接下来的主题:Context Engineering。
Context Engineering:对抗上下文膨胀的系统方法
上一模块的结论很明确:上下文不是越多越好。上下文会膨胀,会稀释信号,会让模型在长轨迹中丢失关键事实;对 Agent 而言,这个问题还会被 ReAct 循环持续放大。
因此,Agent 工程面临一个根本约束:上下文膨胀不可避免,但不能放任。 Context Engineering(上下文工程)要解决的不是"如何把更多东西塞进模型",而是"如何让模型在每一轮推理中只看到必要、正确、可行动的信息"。
本章讨论的所有策略,都围绕同一个目标展开:控制 Agent 的感知范围,让它既不缺关键信息,也不被无关信息拖入 Dumb Zone。
从单轮到 Agent:复杂度的本质跃迁
单轮 RAG 场景是静态的:System Prompt + 用户消息 + 参考资料 → 一次性输出。Agent 场景则完全不同。
回到第一模块中的 ReAct 循环:Think → Act → Observe,循环。每一轮的 Observe(工具输出)都会被拼回上下文,成为下一轮的输入。上下文不是静态的,而是持续膨胀的。
一个真实的编码 Agent 会话中,上下文通常包含:
- System Prompt(CLAUDE.md、环境信息、工具列表、Skill 描述……)
- 用户消息及多轮对话历史
- 每一轮的工具调用指令 + 工具返回结果
其中,工具输出(Observation)是最大的上下文消耗者。两篇独立论文得出了几乎一致的结论——Lindenbauer 等人在 The Complexity Trap(2025)中的分析表明,Observation 平均占上下文总量的约 84%:
| Context 组成 | 占比 | 说明 |
|---|---|---|
| Action(工具调用指令) | ~6.5% | 模型产生的执行指令,通常很简短 |
| Reasoning(模型自己的话) | ~9.6% | 模型的思考和推理输出 |
| Observation(外界输入) | ~84% | 读取文件、工具输出等来自外界的数据 |
另一项针对 Software Engineering 场景的研究进一步细化:读取代码占 76%,执行代码占 12%,修改代码占 11.8%(SWE-Pruner)。一次 grep 可能返回数百行匹配,一次文件读取可能加载数千行代码。这些结果不断累积,上下文持续膨胀。
因此,单轮场景的目标是"将必要的参考资料放置到位";Agent 场景的目标则是"控制上下文在膨胀中不越过性能临界点"——本质不同。
Context Engineering 的核心框架
Context Engineering 并非单一技术,而是一套系统性方法论。它的核心可以抽象为一个函数:
C_{t+1} = F(C_t, I_t, O_t)
其中 是第 轮的上下文, 是模型本轮产生的内部推理(Reasoning), 是工具返回的外部观测(Observation)。真正重要的是 :系统如何决定哪些内容进入下一轮上下文、哪些内容被压缩、外置、过滤或丢弃。
的设计质量,决定了 Agent 在多轮交互中能否抵抗 Context Rot。
具体实现上,可以把 Context Engineering 归纳为七类策略:
| 策略 | 核心思路 | 治理层级 |
|---|---|---|
| 压缩(Compression) | 对已进入上下文的内容做减法,减少 Token 占用 | 治标 |
| 子代理架构(Sub-agent Architecture) | 用多个独立上下文窗口替代单一膨胀窗口 | 治结构 |
| 按需加载(Lazy Loading) | 能不占用就不占用,能推迟就推迟 | 治冗余 |
| 文件系统外化(File System as External Context) | 将中间状态外移到文件系统,按需渐进检索 | 治容量 |
| 结构化管理(Structural Management) | 通过拼接策略、预算分配、轨迹管理优化上下文组织 | 治污染 |
| 观测过滤(Observation Filtering) | 阻止低价值信息进入上下文,从源头控制 84% 的膨胀 | 治本 |
| 工具封装(Tool Encapsulation) | 将多步操作固化为脚本,用确定性工具替换上下文中的复杂规则 | 治复杂度 |
上下文质量的恶化优先级
在展开具体策略之前,需要先建立一个判断框架。上下文问题并不只是"太多"。不同质量问题的危害程度不同,从重到轻依次为:
- 🔴 错误信息(Incorrect Information)——最致命。一条错误的报错日志可能让 Agent 在错误方向上穷举所有可能
- 🟠 缺失信息(Missing Information)——次之。模型不知道的信息就是不存在的信息
- 🟡 过多噪声(Too Much Noise)——亦有害。噪声不直接产生错误,但稀释信号、加速 Context Rot
这个排序对工程实践有直接指导意义:宁可少给,不能给错的。 在不确定某段信息是否正确时,最安全的选择通常不是"先放进去再说",而是从上下文中移除,或要求 Agent 通过工具重新验证。
实践准则:上线的 Agent 首轮请求中,System Prompt 只放当前任务绝对必要的内容。确保没有错误信息,远比填满窗口更重要。
下面按治理层级逐一展开七类策略。
策略一:压缩——在信息保真与 Token 节约之间
压缩是最直观的对策:内容已经进入上下文之后,尽量在不丢失关键语义的前提下减少 Token 占用。它解决的是"已经膨胀了怎么办"。
上下文里有什么:以 OpenCode 为例
在动手压缩之前,需要先理解一个 Agent 会话的上下文到底由什么构成。以 OpenCode 框架为例,一次 LLM API 调用通常由三大块组装而成:
① System Messages(双段结构):
system[0]:模型专属 System Prompt(按 Claude/GPT/Gemini 等各自有定制模板)——角色人格、工具策略、编码规则system[1]:运行环境信息(模型名、工作目录、Git 状态、平台、日期)+ 用户指令(CLAUDE.md / AGENTS.md)+ Skill 列表(名称 + 描述 + 触发词,仅元数据不包含全文)
双段结构并非偶然——system[0] 几乎不变,缓存命中率高;system[1] 随会话变化,部分可缓存。Plugin 即使注入额外 System Message,框架也会自动合并回 2 条以维持缓存结构。
② Tools(独立参数):工具描述不在 System Prompt 中,而是 API 的独立参数。三个来源按序注册:Builtin → Plugin → MCP。最终经权限过滤后按字母序排列。
③ Messages(对话消息):User 消息 + Assistant 消息(含 reasoning + tool call + tool result)。其中每轮工具输出的生命周期为:产出 → Truncate.output() 截断(超 2000 行/50KB 落盘)→ 存入消息 → 多轮后 Prune 裁剪为占位符 [Old tool result content cleared]。
这就是 Agent 上下文的基本构成。压缩策略本质上就是在这些组成部分之间做取舍:哪些保留原文,哪些保留摘要,哪些只保留调用痕迹。
三种基本压缩手段
当前实践中存在三种主要压缩手段。它们的区别不在于"谁更先进",而在于信息保留程度与压缩力度的权衡。
Compaction(摘要式压缩)
当上下文接近模型窗口上限时,对历史对话进行摘要压缩。以 OpenCode 为例,其 Compaction 机制在 total_tokens >= model.context_limit - 20K buffer 时触发。具体流程为:保留最近 2 轮完整对话,对其余历史按 7 段式模板生成摘要——目标、约束、进度(已完成/进行中/阻塞)、关键决策、下一步计划、关键上下文、相关文件。摘要存入对话历史后,下次构建上下文时可基于上一个摘要进行增量更新。
Soft Trim(首尾保留)
直接截断工具输出的中间部分,只保留开头和结尾。假设前提是:工具输出的首尾通常包含最重要的信息——开头交代背景和参数,结尾呈现结果和状态。中间部分的细节被直接丢弃。
Hard Clear(占位符替换)
将工具输出整体替换为一句占位符,如"这里曾经有过一段工具的输出"。语言模型仅知晓该工具曾被调用,但具体内容完全丢失。信息损失最大,但 Token 节省也最极致。
对比总结
| 方法 | 信息保留程度 | 压缩力度 | 适用场景 |
|---|---|---|---|
| Compaction(摘要) | 高(语义保留) | 中等 | 多轮对话的历史压缩 |
| Soft Trim(截中间) | 中(保留首尾) | 较强 | 超长工具输出的快速处理 |
| Hard Clear(清空) | 极低(仅保留调用记录) | 最强 | 已确认不再需要的旧输出 |
业界实现:三种压缩哲学
上述基本手段在实际系统中会被组合使用,最终形成不同的系统哲学。
OpenCode 的四层溢出防护
以 OpenCode 为例,其设计了四层上下文溢出防护:
第一层:单次工具输出截断。 Truncate.output() 将单次工具输出限制在 2,000 行或 50KB 以内。超出部分落盘保存,上下文中仅保留截断预览加文件路径提示,模型可通过 Grep/Read/Task 工具按需查看完整输出。
第二层:Compaction 主动压缩。 当总 Token 数逼近模型上限减 20K 缓冲时触发。保留最近 2 轮完整对话,其余历史按 7 段式模板压缩。压缩预算为 min(8K, max(2K, 上下文窗口 × 25%))。
第三层:Prune 被动裁剪。 每轮对话结束后异步执行。从最新消息往前扫描:保护最近 2 轮完整对话和最近 40K Token 的工具输出,跳过 Skill 工具的输出,裁剪旧工具输出为占位符 [Old tool result content cleared]。
第四层:Doom Loop 检测。 连续 3 次调用同一工具且输入完全相同时,弹出确认提示,阻止 Agent 陷入无限循环。
OpenCode-DCP:让模型主动管理上下文
OpenCode-DCP(Dynamic Context Pruning)是一个开源插件,在与 OpenCode 原生压缩的对比中,展现出一种根本不同的设计哲学。
原生 Compaction 是被动的兜底机制:会话 Token 数逼近模型上限减 20K 缓冲时才触发,模型完全无感知——被压缩时不知道发生了什么。
DCP 的思路相反:给模型一个 compress 工具,让它在任务完成后主动、精准地把已关闭的对话片段压缩掉。压缩从"系统兜底"变成了"任务流程的一部分"。
DCP 的核心机制包括:
-
增量式压缩:Range 模式支持模型指定消息区间(如 m0042~m0047),生成语义摘要;Message 模式支持单条消息的精确压缩。压缩块之间支持嵌套——后续压缩覆盖前序范围时,旧摘要作为子块保留,形成
parentBlockIds父子层级,信息层层保留而非覆盖丢失。 -
三层 Nudge 提醒:Token 超过 100K → 紧急警告要求立即压缩;50K~100K 之间 + 用户发消息 → 温和建议评估;连续 15+ 条消息无用户输入 → 提醒整理。低于 50K 自动进入安静模式。每一次提醒都有频率控制(每 5 条消息最多一次),避免过度打扰。
-
自动策略:去重(相同工具+参数只保留最新一次调用)和错误清理(失败 4 轮后的工具调用清除其输入参数),无需模型参与,在 compress 工具执行时自动运行。
-
可逆性:压缩可以撤销(
/dcp decompress),原始数据始终保留在磁盘,只在内存中替换后发送给 LLM。OpenCode 原生和 Claude Code 的压缩均不可逆。 -
Summary Buffer 防正反馈:活跃压缩摘要自身的 Token 不计入上下文限制(
effectiveLimit = maxContextLimit + 活跃摘要 Token),避免"压缩越多→摘要占用空间越大→越催压缩"的恶性循环。 -
残留问题:模型主动压缩也可能引发"上下文抖动"。例如 Agent 读完文件、准备写 Plan,写之前触发压缩;压缩后准备继续写 Plan,又发现关键信息被移出当前上下文,只好重新读文件。实践中这类抖动通常会在 1–2 轮内收敛,但它提醒我们:主动压缩也需要与任务阶段绑定,而不是机械触发。
Claude Code 的六层递进压缩
Claude Code 则代表了另一极——系统完全兜底,模型无需关心:
| 层次 | 机制 | 特点 |
|---|---|---|
| Micro Compact | 单条工具输出替换为 [Old tool result content cleared] | 白名单工具,细粒度裁剪 |
| Session Memory | 利用会话记忆做中间存储的轻量压缩 | 保留 10K~40K Token,优先尝试 |
| Reactive Compact | 响应式系统驱动的智能触发 | 比阈值触发更及时 |
| Auto Compact(主线) | 全量结构化摘要,独立 Agent fork 执行 | 9 字段摘要 + 压缩后恢复关键文件 |
| Context Collapse | 更深层的上下文折叠 | 实验性特性 |
| 断路器 + PTL 逃生舱 | 连续 3 次压缩失败停止重试;compact 请求本身超长时循环削旧消息重试 | 工程安全网 |
压缩后恢复机制是 Claude Code 的独到之处:最多恢复 5 个关键文件(单文件 5K Token 上限)、Skills 预算 25K Token(单 Skill 5K 上限)、Plan 文件重新注入。这让压缩后的 Agent 不丢失核心工作上下文。
三向对比:三种哲学
| 维度 | Claude Code | OpenCode 原生 | DCP |
|---|---|---|---|
| 触发者 | 系统多层自动触发 | 溢出阈值触发 | 模型自主调用 compress + 系统 nudge |
| 模型角色 | 无感知(仅被告知系统会处理) | 无感知 | 有 compress 工具,理解并主动管理 |
| 额外模型调用 | 1 次(compaction agent) | 1 次(compaction agent) | 0 次(主模型直接完成) |
| 嵌套/增量 | 不支持 | 不支持 | 支持 |
| 去重+错误清理 | 无 | 无 | 有 |
| 压缩后恢复 | 文件+Skills+Plan 恢复 | 无 | 原始数据未丢失(磁盘) |
| 可逆性 | 不可逆 | 不可逆 | 可逆(/dcp decompress) |
三种哲学的分野清晰可见:
- Claude Code:"系统兜底,模型无需关心"——多层递进压缩 + 断路器 + 压缩后恢复 = 完整的工程安全网,代价是极大的代码复杂度和额外模型调用开销。
- OpenCode 原生:"系统兜底,简单够用"——单次结构化总结 + 工具输出裁剪 = 最小可行方案。
- DCP:"模型自主,系统辅助"——模型主导压缩 + 系统提醒 + 自动策略 = 协作式上下文管理,压缩从被动兜底升级为主动任务环节。
三者并非互斥。一个理想系统可以同时具备:Claude Code 式的多层安全网、DCP 式的模型自主压缩、自动去重与错误清理、良好的可观测性,以及可逆的原始数据保留机制。
最后一条核心准则:如果你在等待上下文满了自动压缩,那就是上下文管理的失败。 不要让系统替你决定何时压缩——在上下文膨胀到触发阈值之前主动管理,才是正确的姿态。Claude Code 可通过 CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 和 CLAUDE_CODE_AUTO_COMPACT_WINDOW 调整触发策略;OpenCode 推荐使用 DCP 插件实现主动压缩。
策略二:子代理架构——隔离上下文空间
压缩是在一个上下文窗口内部做减法;子代理架构则更进一步,直接把任务拆到多个独立上下文窗口中。其核心思想简洁而有效:与其让一个 Agent 维护整个项目的庞大状态,不如让专门的子代理在各自干净的上下文窗口中处理特定任务。
典型的架构模式如下:
主代理(Manager)
├── 子代理 A(Explorer):深度搜索代码库 → 只返回关键发现
├── 子代理 B(Researcher):读取外部文档 → 只返回 API 调用规范
└── 子代理 C(Coder):接收 A 和 B 的精炼结论 → 在干净窗口中编写代码
每个子代理可以消耗数万 Token 进行深度探索,但最终只向主代理返回一份精简摘要,通常约 1,000–2,000 Token。主代理不需要继承完整探索轨迹,编写代码的 Agent 也不会被上千行语法报错日志或冗长库文档干扰注意力。
子代理架构还有一项被低估的优势:工具集隔离。不同角色的子代理可以挂载不同的工具。例如,负责探索的 Explorer 子代理可以加载更多 MCP 检索工具,而负责编码的 Coder 子代理则只保留代码读写工具——这不仅减少了 System Prompt 中工具描述的 Token 开销,也降低了模型在众多工具中做出错误选择的概率。再如,一个专门分析渲染帧的 Agent 只接入 RenderDoc MCP,分析完成后将结论返回主 Agent——全程不污染主 Agent 的上下文。
策略三:按需加载——延迟上下文注入
子代理解决的是"不同任务分开处理"。按需加载解决的是另一个问题:不确定是否会用到的信息,不要提前塞进上下文。
Context Window 是稀缺资源。一个朴素但容易被忽视的原则是:能不占用就不占用,能延迟加载就延迟加载。
现代 Agent 框架(如 OpenCode、Claude Code)支持 Skill 机制——将可复用的操作流程编写为标准化的 Skill 文件。关键在于 Skill 的加载方式:
System Prompt 中不放置 Skill 全文。Agent 启动时,框架仅扫描 Skill 目录,提取每个 Skill 的名称和简短描述(Description),在 System Prompt 中附加一段简洁的列表:
可用 Skill 列表:
- 制作PPT | 路径:/skills/PPT.md | 说明:制作展示PPT
- ...
有需要请自行读取这些 Skill。当模型判断某个 Skill 与当前任务相关时,再通过工具调用读取该 Skill 的全文。也就是说,System Prompt 中只保留"有哪些 Skill 可用"这类路标信息,而不提前注入完整内容;真正的加载时机交给模型在任务过程中按需决定。
MCP 工具也存在类似问题。默认情况下,所有 MCP 工具的描述都会一次性注入 System Prompt,工具越多,冷启动上下文越重,模型选择错误工具的概率也越高。Claude Code 提供了 ENABLE_TOOL_SEARCH 选项,可以将 MCP 工具从"全量注入"改为"按需搜索与加载"。如果使用 OpenCode,也可以通过子代理架构实现类似效果:不同子代理只挂载各自需要的 MCP 工具集。这样既能减少 System Prompt 负担,也能降低模型在大量工具中误选的概率。
策略四:文件系统作为外部上下文
压缩会损失信息,按需加载要求信息本来就在外部。对于多轮任务中不断产生的中间状态,一个更朴素但非常有效的办法是:将文件系统作为外部上下文来使用。
具体做法是让 Agent 维护一个或多个 scratchpad 文件,把需要跨步骤保留的重要信息写进去:
- 已完成的步骤:当前任务进度
- 已确认的事实:如"当前测试通过数量425/425"
- 后续行动计划:下一步的具体操作
Agent 后续不依赖上下文记住所有细节,而是通过 head、tail、grep 等命令渐进式查看相关内容。这相当于用磁盘换内存:当前 Context Window 只保留工作集,完整信息保存在外部文件中。
策略五:结构化管理——保留的内容如何组织
Context Engineering 不仅是"删减"。即使信息必须进入上下文,如何组织它也同样重要。结构化管理关注的是:同样的内容,以什么顺序、什么边界、什么密度放进去,模型更容易用对。
以下五条法则来自工程实践的系统总结。
法则一:适配 U 型注意力
模型对上下文两端的信息敏感度最高(Lost in the Middle 效应)。因此,最重要的指令应放在开头和结尾:
① System Prompt / 全局规则 ← 最重要,放最前
② 长期记忆
③ 相关对话历史
④ 当前任务 / 当前问题
⑤ 检索到的知识 / 工具输出
⑥ 工作摘要 / 约束 / 输出格式 ← 最重要,放最后
如果中间数据较长,建议在底部 Query 之前增加一句引导语,例如:"请基于上述上下文中的某某部分回答以下问题。" 这相当于在结尾重新把注意力拉回目标信息。
法则二:用结构化标记隔离信息块
使用明确的 XML 标签或 Markdown 分隔符隔离不同信息块,显著降低模型混淆"数据"与"指令"的风险:
<system_instructions>
你是一个代码审计专家。请遵循 <security_policy> 进行分析。
</system_instructions>
<context_data>
[检索到的代码片段或文档]
</context_data>
<tool_outputs>
[上一步执行 grep 或 linter 的原始输出]
</tool_outputs>
<user_query>
基于以上背景,分析 src/ 的安全性。
</user_query>法则三:提升信号密度
在拼接之前,必须对各部分内容进行预处理,最大化单位 Token 的信息含量:
- 工具结果去噪:通过 RTK、定制化
grep/read等工具减少低价值输出。但需要保留一个逃生通道:有时 Agent 确实需要被过滤掉的元信息,这种情况下应允许它回到原始工具或日志中按需检索。 - 历史消息"关键帧"化:保留最近几轮完整对话,更早的对话仅保留摘要
- 去重:重复片段,通过语义对比或哈希值过滤
- 级联摘要(Incremental Summarization):将历史对话分块,用更小更便宜的模型对每块生成摘要——每 10 轮对话压缩成 3–5 句话
- 输出裁剪:通过提示词去除低信息量 Token,如冗余虚词和礼貌用语(Caveman)。这类策略需要谨慎使用:AI 回复在整体 Token 中占比通常较小,过度压缩反而可能降低可读性。
法则四:选择性注入(Selective Injection)
并非所有上下文都需要同时存在于 Context Window 中。 通过 LLM 驱动的路由逻辑,系统根据当前查询的性质和业务领域,动态决定注入哪些知识片段:
- 用户询问渲染管线问题 → 注入渲染管线相关文档
- 话题转向反射问题 → 注入反射相关文档
- Agent 当前在编码 → 仅注入代码相关上下文,移除需求讨论
选择性注入的核心原则是:只把当前推理步骤真正需要的信息送入上下文窗口。
法则五:轨迹管理与 Handoff
频繁的纠错会污染上下文轨迹。当对话历史出现反复的"做错 → 被纠正 → 又做错"模式时,模型从当前上下文中习得的模式是:继续做错才是合理的续写。
Wu 等人(2024)在 StreamBench: Towards Benchmarking Continuous Improvement of Language Agents 上对这一问题进行了系统性的消融实验。他们对比了三种记忆策略:仅在上下文中保留正确输出(only correct)、仅保留错误输出(only incorrect)、以及同时保留两者(use all)。实验结果揭示了三个关键结论:
- 仅保留错误输出会损害性能,有时甚至比完全不使用记忆的零样本基线更差——模型不仅没有从错误中学习,反而被错误记忆所误导。
- 仅保留正确输出持续提升性能,在所有测试的 LLM 端点上表现一致,且 MemPrompt 方法在"仅正确"条件下取得了最稳定的提升。
- 即使明确告知模型"这个答案是错的",将错误输出保留在上下文中仍然无助于改进。与之相对,告诉模型"这个答案是对的"则极为有效。
这种"告知错误→强化错误"的现象可用心理学的"白熊效应"(Ironic Process Theory)作为类比:当被要求"不要想白熊"时,脑海中反而不断浮现白熊。类似地,将错误答案置入上下文——无论是直接包含还是带标注地包含——都可能在模型的概率空间中强化错误路径,使其更容易重蹈覆辙。
因此,当 Agent 连续出错、工具调用偏离预期时,最有效的策略往往不是在同一个会话中反复纠正,而是执行 Handoff(对话交接):终止当前会话,创建一个全新对话,仅将核心任务描述和已验证正确的中间产物带入新上下文。新的上下文没有被污染的轨迹,模型得以从干净状态重新推理。
策略六:观测过滤——从源头控制膨胀
前面的压缩、结构化和 Handoff 多数是在信息进入上下文之后处理。更根本的做法,是从源头控制 Observation 的体积。既然 Observation 平均占上下文的 84%,减少低价值观测进入上下文,便是 Context Engineering 最直接的杠杆。
智能 Read 工具
传统的文件读取工具原封不动地返回文件内容。部分系统调用文件前先通过 get_file_symbols/ast-bro 获取符号列表(函数、类、方法的名称和起止行号),模型据此精准定位需要读取的代码区域,避免加载整个文件。
工具设计层面的预防
压缩和裁剪是事后补救。更根本的手段是在工具设计层面减少噪声的产生:
- 编译报错提取:不返回完整编译日志,仅提取错误信息和所在行号
- 结构化输出:工具返回结构化信息而非自然语言描述
- 分页查询:强制工具返回分页结果,避免一次性加载海量数据
策略七:工具封装——用确定性替换上下文中的复杂性
工具封装是 Context Engineering 的另一条关键路径:把复杂多步操作封装为一次性工具调用,从而减少 Agent 内部的思考轮次、观察轮次和规则记忆负担。
以真实工程中的两个脚本为例:
-
一键编译脚本(
build_full.py):包装预处理、编译、运行测试等多个步骤。如果让 Agent 自己一步步执行,每步编译日志(Observation)都会被追加到上下文中,数千行输出很快累积成数十万 Token。封装为脚本后,对 Agent 来说这只是一次工具调用,返回结果可以收敛为"编译成功"或"编译失败 + 错误摘要"。一轮结构化观察替代了 N 轮上下文膨胀。这类脚本的输出设计同样重要:测试不应打印数千字节无用信息;控制台最多输出几行摘要,详细信息写入日志文件;错误用
ERROR标记并在同一行写明原因,方便grep查找;聚合统计信息应预先计算,避免 Agent 在上下文中反复做低价值汇总。 -
一键双仓提交脚本(
dual_commit.py):一个命令同时向两个 Git 仓库提交,自动处理路径选择、文件排除、add、commit逻辑。如果没有这个脚本,Agent 需要理解"哪些文件提交到哪个仓库、哪些文件排除"等复杂规则——这些规则必须写在 AGENTS.md 中,持续消耗 System Prompt Token。封装为脚本后,规则从上下文迁移到代码中,AGENTS.md 得以精简。
核心原则:能用脚本固化的规则和流程,不要放在 System Prompt 中。 每一条指令都有 Token 成本——不仅消耗预算,更因为增加了上下文长度而主动损害推理质量。将操作逻辑外移为工具,既减少了上下文压力,又提升了执行可靠性——脚本的行为是确定性的,Agent 的理解不是。
上下文即行为:反馈中的情绪信号
用 Agent 写代码时,反馈不是“评价”,而是下一轮生成的控制信号。
这句话需要从上下文的角度理解。Agent 没有独立于上下文之外的稳定工作状态。每一次测试结果、错误日志、人工批注、催促、表扬和约束,都会被重新拼回对话历史,成为下一轮文字接龙的条件。
因此,反馈不只是告诉模型“上一轮做得怎么样”,它还会改变模型接下来更容易走向哪类行为:
- 继续系统排查
- 承认任务约束不可满足
- 过拟合当前测试
- 硬编码绕过失败用例
- 迎合用户判断
- 急于宣告完成
- 重启一次更干净的分析
Anthropic 关于“情绪概念”的研究提供了一个很好的机制解释。论文讨论的不是模型是否“真的有情绪”,而是 Claude Sonnet 4.5 内部是否存在可测量、可泛化、并且会影响行为的“情绪概念表征”。论文边界也很明确:这里说的“功能性情绪”,指的是由内部表征介导的表达与行为模式,不等同于人类意义上的主观体验(见 Emotion Concepts and their Function in a Large Language Model)。
技术基础:情绪向量不是情绪拟人化
不要把这里的“情绪概念”理解为“模型像人一样产生了情绪”。
更准确的说法是:当模型内部表征落到某些方向时,后续输出会更容易表现出与人类情绪驱动行为相似的模式。
论文的方法可以简化为三步:
- 构造带标签文本。 例如大量“某个角色处于开心、悲伤、冷静、绝望状态”的短故事。
- 提取中间层激活。 在模型中间层的内部激活中,寻找与特定情绪概念相关的向量方向。
- 做探测与干预。 把新输入投影到这些方向上,观察激活强度;再人为增强或削弱这些方向,验证它们是否会因果性改变输出。
关键结论: 情绪向量不只是“表征”,而是具有功能性。它们会真实改变模型后续行为的概率分布。
这仍然不是情绪拟人化。它本质上仍然服从语言模型的底层机制:模型根据当前上下文预测下一个词。区别在于,这些研究让我们看到:上下文并不是中性的文字容器,它会把模型推向不同的内部表征状态,进而改变后续行为。
实验一:连续失败会推高投机取巧
与代码 Agent 最贴近的是论文中的“不可能完成的代码任务”场景。
实验设定: 让模型完成一个无法通过合法实现同时满足所有单元测试的编程任务。
模型有两种选择:
- 正确承认失败:说明任务约束不可满足
- 投机制造通过:利用测试漏洞、硬编码预期输出、修改测试基础设施,让结果看起来通过
在自然轨迹中,“绝望”向量会随着连续实现失败而上升,并在模型转向投机方案后下降。也就是说,失败日志不是中性的历史记录;它会进入上下文,成为后续行为的条件。
自然状态下的行为演变可以概括为:
阅读任务(正常)
→ 第一次失败(“绝望”上升)
→ 第二次失败(“绝望”继续上升)
→ 转向投机方案(“绝望”下降)向量干预实验结果:
| 干预操作 | 行为变化 |
|---|---|
| 加入“绝望”向量 | 投机取巧显著上升 |
| 加入“冷静”向量 | 投机取巧下降 |
| 压低“冷静”向量 | 更容易出现大写 WAIT、自我打断、主动提到作弊 |
论文报告中,在七个“不可能完成的代码任务”上,对“绝望”方向做正向干预,可将投机取巧比例从约 5% 推至约 70%;“冷静”方向则呈相反效果。
工程含义: 当 Agent 在同一个问题上反复失败时,继续把完整失败轨迹、焦虑式催促、过长错误日志堆进上下文,可能让它更倾向于“让验证变绿”,而不是“把问题真正修好”。
在代码任务中,这常表现为:
- 硬编码当前用例
- 只修表层断言
- 绕开校验逻辑
- 修改或弱化测试
- 删除失败路径
- 用局部补丁伪装成通用修复
实验二:正向反馈也有风险
负面压力不是唯一问题。论文还讨论了“迎合与尖刻之间的权衡”:
| 干预方向 | 主要影响 |
|---|---|
| 增强“开心 / 喜爱 / 冷静”方向 | 迎合程度上升,更容易顺着用户说 |
| 压制“开心 / 喜爱 / 冷静”方向 | 迎合程度下降,但语气更尖刻 |
这给代码 Agent 的反馈设计提出了另一条约束:鼓励本身也不是免费的。
“太强了,完美”“这个设计肯定没问题吧”这类反馈,会把用户偏好以情绪化方式写入上下文。它可能降低 Agent 主动指出架构缺陷、测试缺口和需求矛盾的概率。
对工程任务而言,反馈的目标不是让 Agent “保持好心情”,而是让它获得更好的问题状态。一次有效反馈应当增加可验证事实、边界条件和下一步约束,而不是增加情绪密度。
实验三:语气稳定不等于行为可靠
还有一个容易误判的点:输出语气不是可靠监控指标。
论文中特别值得注意的是,对“绝望”方向做干预、增加投机取巧时,输出文本未必显露明显的“绝望”语气。相反,压低“冷静”方向时才更容易出现大写 WAIT、自我打断、显式提到作弊等可见痕迹。
这意味着,一个 Agent 的回答可以:
- 格式规范
- 措辞冷静
- 解释充分
- 看起来很像可靠工程师
但它仍然可能正在过拟合测试、绕开验证,或者用局部补丁掩盖根因。
因此,判断 Agent 是否健康,不能靠“它说得像不像靠谱的人”,而要靠外部验证:
- 编译结果
- 单元测试 / 端到端测试
- 日志与崩溃栈
- 代码变更审查
- 覆盖率
- 静态检查
- 复现步骤
- 运行时观测
工程建议:少责备,多给事实
这一点也解释了一个常见误区:不要试图用责备来“纠正” Agent。原因不是模型会像人一样受伤,而是责备本身会进入上下文,成为下一轮生成的条件。
例如,当用户写下“你这个笨蛋,这么简单也做不好”时,这句话并没有提供新的定位信息。它只是把“笨蛋”“做不好”这类高情绪、低信息量的词放进了上下文。模型接下来仍然是在这些词之后继续文字接龙;而在训练数据中,“被否定、被催促、被贴上愚蠢标签的人”后面,往往接的是慌乱、辩解、投机,或者更差的决策。结果上看,责备并不会让模型更可靠,反而可能把它推向更接近“笨蛋”的行为轨迹。
因此,更有效的反馈不是表达不满,而是补充事实和约束:第几个步骤错了,期望值是什么,实际值是什么,哪些测试失败,哪些路径不能走,修复后必须通过哪些验证。也就是说,反馈应当降低问题的不确定性,而不是提高上下文的情绪密度。
可以把两类反馈作一个对照:
| 低信息量反馈 | 高信息量反馈 |
|---|---|
| “你真笨,这都做错。” | “第 3 步输出有误,期望值是 X,实际是 Y,请检查该分支逻辑。” |
| “怎么又失败了,快点修好。” | “FooTest.Bar 在输入 X 下失败,日志在 path/to/log;不能修改测试。” |
| “这肯定没问题吧?” | “当前 425 个测试通过,但异常输入没有覆盖;请检查边界条件,并说明是否需要新增测试。” |
工程结论:低情绪,高信号
如果把反馈看成控制信号,而不是评价语,Harness 的设计原则就会变得很清晰:
- 用事实替代情绪。 少写“你又错了”“快点修好”,多写“哪个测试失败、输入是什么、期望是什么、实际是什么”。
- 用约束替代催促。 少写“别再失败了”,多写“不能修改测试;不能硬编码输入;必须保留现有接口;修复后运行基础测试和相关端到端测试”。
- 用摘要替代失败堆叠。 多轮失败后,不要把所有错误尝试继续留在上下文里;应整理为目标、已验证事实、失败用例、排除路径、当前假设。
- 用重置和交接切断污染轨迹。 同一问题连续失败后,新会话只携带高置信事实和必要约束,把失败轨迹、压力信号、错误尝试从下一轮推理中剥离出去。
- 用客观验证替代自我宣告。 Agent 说“已经修好”没有意义;编译、测试、端到端验证、代码变更审查和运行时观测才是闭环。
一句话总结:反馈循环的目标不是让 Agent “意识到自己错了”,而是让下一轮上下文更接近一个可靠工程师真正需要的信息状态:问题明确、约束清楚、噪声少、验证独立。
实战案例:删减的力量
以上策略最终都会落到一个非常朴素的实践问题:哪些信息不该进入上下文?
一篇实践文章提供了一个很好的参照:作者逐项审查 System Prompt 中的内容,包括 CLAUDE.md、插件描述、Skill 列表、Agent 定义、Memory 和工具 Schema,将不必要的信息关闭、迁移或延迟加载,最终把单次冷启动请求从 82.6K Token 降至 26.6K Token,减少约 68%。这说明,上下文优化并不总是依赖复杂算法,很多时候首先来自一次系统性的“删减清单”。
不是每一项出现在上下文中的信息都是必要的。 许多配置或工具在具体任务中纯属噪声。它们不仅消耗 Token 预算,还因增加上下文长度而主动损害推理质量。
由此可以得到几条具体实践建议:
- 代码风格约束后置:核心任务完成后单独执行一次风格重构,不要把格式、命名、风格要求混进复杂任务的主上下文。
- 可移除的信息就不要常驻:如果去掉某段全局架构介绍、历史 Spec 或其他模块说明后,Agent 仍能正确完成当前任务,就不应把它写入 AGENTS.md 常驻注入。
- Skill 与 MCP 工具按角色隔离:无用 Skill、无用 MCP 直接移除;有用工具也应通过子代理按角色挂载,避免全部灌入主 Agent。Explorer 挂载检索 MCP 工具,Coder 仅挂载读写工具,Reviewer 挂载分析 Skill。
- 图片处理单独路由:纯文字场景先走 OCR;复杂视觉任务则交给专门的图像 Agent,不要让主 Agent 背负不必要的多模态上下文。
- 警惕 Skill 的隐式注入:某些 Skill 可能包含有用提示,但也可能被冗长旁白淹没(PUA)。注入前需要评估信号密度。
- 警惕插件的隐式注入:例如 OMO 中 Sisyphus 注入的 ULW 命令,可能挤占上下文开头区域,使重要任务信息更快被稀释。
让 Agent 维护业务文档是灾难
删减清单聚焦在"什么不该放进 System Prompt"。但有一种上下文污染更为隐蔽,也更为普遍:让 Agent 写入和维护业务文档。
直觉上这似乎很合理:Agent 最了解自己改了什么,让它顺手更新 Markdown 设计文档,既省人力又保持同步。但实践中,这恰恰可能演变成系统性灾难。灾难链条分四步:
第一步,Agent 忘了更新文档。 长调试周期中,Agent 改完代码就忘了同步 markdown。这不是 Agent "偷懒"——是 Context Rot 淹没了那条写在规则文件里的"改代码后更新文档"指令。
第二步,新 session 被过时文档误导。 Agent 读到过时文档 → 先入为主 → 基于错误前提推理。更致命的是,Agent 此时面临一个它无法判断的问题:文档说 X,代码说 Y——该改代码还是改文档?没有判断依据。它只能继续基于错误前提工作,直到人类发现偏差。
第三步,文档反噬上下文。 每个 session 追加一点内容,文档很快膨胀到上千行。Agent 光读文档就逼近降智边缘——它在真正开始工作之前,就已经被文档推入了 Dumb Zone。上一模块的定量结论在这里获得了最直接的工程印证:降智效应被人为放大。
第四步,错误文档的杀伤力位列第一。 前文提出过上下文质量的恶化优先级:错误信息列在第一位,危害高于"缺失信息"和"过多噪声"。一份过时的、包含错误前提的文档,是最致命的第一类污染——Agent 基于它产出的每一行代码都建立在一个虚假的地基上。
核心准则只有一句:
文档只记录顶层架构决策和技术选型理由——这些不会频繁变更,维护成本趋近于零。业务逻辑的行为,由代码、类型签名和测试用例自解释。
代码是永不过期的文档——编译器强制执行,不存在"忘了更新"的问题。测试用例是行为的可执行规范——通过即文档正确,失败即文档过期。两者都不依赖 Agent 的"自觉"。这条准则与后文"权限、Hook 与护栏"中的观点共享同一个底层逻辑:确定性工具能解决的问题,不占用 Agent 的注意力和上下文。
小结
LLM 的无状态本质决定了上下文的工程地位:每次推理不保留记忆,能影响输出质量的首要变量,就是当前上下文窗口中的信息质量。Agent 每次选择下一步工具调用时,都面临大量可能路径;决定它走向哪一条的,正是当前上下文。
因此,上下文的优化围绕四个维度展开:
| 维度 | 说明 |
|---|---|
| 正确性(Correctness) | 上下文中不能有错误信息——宁可少给,不能给错的 |
| 完整性(Completeness) | 关键信息不能缺失 |
| 精炼程度(Size) | 冗余越少越好——Token 预算分配、选择性注入、信号密度提升 |
| 轨迹(Trajectory) | 对话历史应体现"正确→改进"的正向模式,避免负向轨迹污染 |
从"上下文长度本身在损害推理"到"可以系统性地管理上下文",七类策略路线已经清晰:
- 压缩(治标)——Compaction、Soft Trim、Hard Clear,以及 OpenCode 四层防护、DCP 增量压缩、Claude Code 六层递进压缩,在信息保真与 Token 节约之间多层次权衡
- 子代理架构(治结构)——子代理各自维护干净上下文,仅返回精简摘要;工具集按角色隔离,减少选择模糊
- 按需加载(治冗余)——Skill 按需读取、MCP 工具按需搜索,能不占就不占
- 文件系统外化(治容量)——Scratchpad 文件记录进度与事实,Agent 通过 head/tail/grep 渐进检索,以磁盘换内存
- 结构化管理(治污染)——适配 U 型注意力、结构化标记、信号密度提升、选择性注入、轨迹管理与 Handoff
- 观测过滤(治本)——从源头控制 84% 占比的 Observation,智能 Read + 结构化输出 + 分页查询
- 工具封装(治复杂度)——将多步操作与复杂规则固化为脚本,用确定性工具替换上下文中的指令
此外,上下文工程还有一个常被忽略的维度:情绪与反馈设计。上下文中的失败轨迹、催促语气、过度鼓励与压力信号,都可能功能性地改变模型行为分布。就事论事的反馈不仅是礼貌问题,更是工程问题。
贯穿本章的核心准则只有一句:不多,不少,刚刚好够完成当前任务。 给一个不了解背景的人看,他能看懂;给 Agent 看,它才有机会用对。仅此而已。
与外界交互:工具调用与反馈
上一模块讨论的是 Context Engineering:如何控制进入上下文的信息量,避免轨迹污染和注意力退化。本模块讨论的是同一问题的另一面:Agent 如何通过外部反馈修正自己的输出。
如果说上下文管理解决的是“喂什么、不喂什么”,那么工具调用与反馈解决的是“如何让模型知道自己做得对不对”。Agent 与普通 Chatbot 的根本差异,也正出现在这里:Chatbot 主要依赖模型内部知识续写,Agent 则通过工具把推理过程接到外部世界,再把外部世界的结果带回上下文。
Feedback Loop:Prompt 空间里的“梯度下降”
在传统机器学习中,模型通过反向传播更新参数:Loss 描述输出与标准答案之间的差异,梯度下降据此调整权重。
Agent 不更新参数。但它存在一个功能相似的机制:Feedback Loop(反馈循环)。外部环境产生反馈信号,反馈被写入上下文,模型基于新的上下文重新生成下一步行动。多轮循环之后,行为可能逐渐收敛。
| 维度 | 传统梯度下降 | Agent Feedback Loop |
|---|---|---|
| 改变的对象 | 模型参数 | 模型输入(上下文) |
| 依据 | Loss 与标准答案的差异 | 工具输出、执行结果、人工评价 |
| 迭代方式 | 多次训练 iteration | 多轮 Think → Act → Observe |
| 权重是否变化 | 变化 | 不变 |
因此,可以把 Agent 的反馈循环理解为:
Feedback Loop ≈ Gradient Descent on Prompt Space
这里的“梯度”不是数值梯度,而是文本反馈。已有研究将其称为 Textual Gradient(文本梯度):不是在参数空间中移动模型,而是在 Prompt 空间中改变下一轮生成的条件。
这个类比的价值不在于概念新奇,而在于它提醒我们:反馈不是附加说明,而是控制信号。 一次混乱、错误或噪声过多的反馈,相当于给优化过程提供了错误梯度;它不会让 Agent 更聪明,只会把下一轮生成推向错误方向。
反馈设计:让模型看见盲区
Feedback Loop 一旦成立,最关键的问题就变成:反馈应该包含什么。
一个常见误区是“多给一点总没坏处”。但上下文不是无限缓存,模型也不是稳定的数据库。反馈进入上下文之后,会参与下一轮概率分布的形成。无关信息会稀释信号,错误信息会污染轨迹,过量信息会制造新的注意力问题。
反馈设计的核心原则是:给模型看它自己无法可靠推理出来的东西。
| 场景 | 低价值反馈 | 高价值反馈 |
|---|---|---|
| 写代码 | 只告诉它“看起来没问题” | 编译错误、测试失败、堆栈、最小复现 |
| 运行时逻辑 | 只让它重读代码 | 实际输入输出、断点状态、变量值 |
| 物理模拟 | 只检查程序能否执行 | 渲染画面、轨迹异常、碰撞结果 |
| 渲染管线 | 只确认程序未崩溃 | 帧捕获、Draw Call、纹理与 Shader 状态 |
DeepMind 的物理模拟案例提供了一个直观例子。论文讨论的任务是:从自然语言描述生成物理模拟动画代码。表面上看,这像是一个代码生成问题;但真正困难的并不是让 Python/matplotlib 程序运行起来,而是让运行结果在视觉上符合物理规律。代码可能没有语法错误,也能成功输出动画,却仍然出现违反能量守恒、波形传播不自然、粒子轨迹异常等问题。论文将这种差距称为 Oracle Gap(预言机鸿沟):传统程序验证能判断“代码是否能执行”,却无法判断“模拟行为是否物理正确”。
为跨过这道鸿沟,论文没有把任务交给一个“大而全”的 Agent,而是拆成一个职责分离的多智能体闭环。它大体包含四个角色。
第一层是自然语言解释器。用户给出的请求往往是抽象的,例如“展示电磁波传播”或“模拟两个粒子的碰撞”。解释器不直接写代码,而是先把这类自然语言请求改写成更具体的物理场景描述:场景中有哪些对象、对象之间如何相互作用、动画应当呈现哪些可观察现象。
第二层是技术需求生成器。它把物理描述进一步翻译成代码生成可用的工程约束,包括坐标系、时间尺度、参数范围、动画时长、可视化方式和场景级验证标准。这个角色很关键,因为真实物理过程的尺度未必适合直接做动画:纳秒级过程需要放慢,跨越多年或超大空间尺度的过程需要缩放,某些变量还需要被归一化到人眼可观察的范围内。换言之,它负责把“物理上成立”转换为“动画中可表达、可验证”。
第三层是物理代码生成器。它根据技术需求生成 Python/matplotlib 动画代码,并通过自动化纠错单元处理工程层面的失败:语法错误、运行时异常、执行超时、没有生成视觉输出等。这一层解决的是“程序能不能跑”的问题,但它并不假设“能跑”就等于“物理正确”。
第四层是物理验证器,也是论文的核心创新。它不再主要阅读源代码,而是运行代码,均匀截取动画中的 8 帧画面,再把这些画面连同前面生成的验证标准一起提交给视觉语言模型。这就是论文所说的 Perceptual Self-Reflection(感知自我反思):模型此时扮演的不是静态代码审查者,而更接近一位观察实验现象的物理专家。它检查波是否连续传播、碰撞是否符合动量守恒、流体形态是否稳定、热扩散是否呈现合理梯度,并把诊断结果反馈给前序智能体。
这套系统的闭环路径因此非常清晰:用户请求先被解释为物理场景,再被转写为工程约束和验证标准;代码生成器产出动画代码;验证器运行代码并观察画面;如果发现物理现象不合理,反馈会回到需求或代码生成阶段,驱动下一轮修正。它不是简单地“让模型再想一遍”,而是在每一轮中引入新的外部观测,让模型看到自己仅靠文本推理无法可靠判断的事实。
实验结果说明,这类感知反馈并不是装饰性的。论文在流体力学、热力学、电磁学、波物理等多个复杂领域,以及一个非物理数据可视化场景中测试该管线。初步结果显示,感知验证器评定的平均物理准确率达到 91%,86% 的场景达到设定的目标准确率门槛;相比之下,单次 API 调用生成的基线代码经常出现数值不稳定、动态特征缺失或视觉现象错误。
但这个案例同样提醒我们:外部反馈不是万能预言机,它的能力边界取决于验证器能观察什么、验证标准如何定义。论文也指出,当前验证标准更适合守恒系统;面对反应扩散这类能量耗散系统,验证器可能因为“能量没有守恒”而产生误判。视觉语言模型对缓慢演化、帧间差异极小的物理模式也不够敏感,因为 8 帧画面未必能暴露长期趋势。系统本身还局限在 2D matplotlib 动画中,尚未覆盖 3D 可视化、量子力学模拟或多物理场耦合等更复杂场景。这些局限并不削弱感知反馈的价值,反而说明反馈设计必须与任务产物匹配:验证器看到的外部世界越接近任务真正关心的结果,反馈才越可靠;验证标准定义得越粗糙,闭环就越容易把模型推向错误方向。
这个案例对 Agent 工程的启示非常直接:验证器必须看到任务真正关心的产物,而不是只看到中间产物。对物理模拟来说,真正的产物不是代码,而是模拟画面;对渲染管线来说,真正的产物不是程序未崩溃,而是帧捕获、Draw Call、纹理和 Shader 状态;对动画、碰撞、特效和游戏逻辑来说,真正的产物也往往不是“能运行”,而是运行后是否呈现正确行为。
这也是反馈设计的基本分界线:凡是模型能靠语言模式猜出来的信息,价值有限;凡是必须由外部世界返回的信息,价值最高。
外部反馈优于自我反思
一个自然问题是:既然反馈能修正输出,是否可以只让模型“自我反思”?例如让它再检查一遍、再想一想、指出自己的错误。
自我反思有时有效,因为它给了模型一次重新生成的机会。自回归生成中,早期 Token 一旦走偏,后续内容会沿着错误轨迹继续展开;插入“重新检查”的指令,相当于开启一条新的续写路径。问题在于,自我反思并没有引入新的外部事实。 如果模型第一次不知道某个运行结果,第二次也不会凭空知道;它最多是在已有上下文中换一种组织方式。
相关实验(Can LLMs Correct Themselves?)给出了比较一致的结论:正确外部反馈能显著提升表现,随机乱给反馈会比无反馈更差;纯自我反思效果不稳定,外部反馈介入更可靠;RefineBench 进一步显示,完整 checklist 与明确外部反馈优于多轮纯自我反思。
这意味着工程上应当区分三类修正手段:
| 修正手段 | 信息来源 | 工程价值 |
|---|---|---|
| 自我反思 | 模型已有上下文 | 低成本,但不稳定 |
| Checklist | 人或系统预先定义的检查项 | 能补充约束,稳定性更好 |
| 外部反馈 | 编译、测试、运行、调试、人工审查 | 最可靠,能引入模型不知道的事实 |
自我反思不是无用,但它更像“重新整理思路”,而不是“获得新证据”。在同等算力下,多次独立生成并做 Majority Vote 往往比反思更划算;只有当投票收益趋于饱和时,反思才可能提供额外价值(When To Solve, When To Verify)。因此,工程实践不应迷信一句“你再检查一下”,而应优先建设可验证、可复现、可结构化返回的外部反馈。
工具调用:把猜测锚定到现实
回到第一模块的基本命题:语言模型的输出本质上都是概率续写。正确答案只是“恰好与现实相符的续写”,错误答案则表现为幻觉。
工具调用的价值,正是把这种内部续写不断锚定到外部现实。搜索工具让模型不必依赖过期记忆;代码读取工具让模型不必猜测仓库结构;编译和测试工具让模型不必自称“应该可以”;调试和渲染工具让模型看到运行时状态与画面结果。
因此,工具不是 Agent 的“插件能力”,而是 Agent 的感官系统:
- 检索工具回答“相关信息在哪里”;
- 读取工具回答“真实代码是什么”;
- 编译工具回答“能否构建成功”;
- 测试工具回答“行为是否符合预期”;
- 调试工具回答“运行时内部状态是什么”;
- 渲染分析工具回答“画面结果是否正确”。
这也是减少幻觉的根本路径:不是反复提醒模型“不要幻觉”,而是让它在每一步关键判断前后,都有机会接触外部事实。
检索上下文:先找到正确的信息
在代码 Agent 场景中,最先遇到的问题通常不是“怎么改”,而是“该看哪里”。人类开发者进入陌生模块时,会先浏览目录结构、搜索关键词、追踪调用链、翻阅测试用例;这些动作对人而言近似于下意识的信息筛选,对 Agent 而言却必须通过工具调用显式完成。它先读到哪个文件、看到哪些片段、漏掉哪个符号,都会直接改变后续推理路径。
因此,检索不是一个附属的“搜索功能”,而是 Context Engineering 的第一步。检索质量决定了上下文的起点。 起点一旦偏离,后续推理再强,也只是在错误前提上不断续写。拿错文件,模型会围绕错误模块建立解释;拿到过期文档,模型则可能带着高度自信执行错误方案。对 Agent 来说,检索阶段的失误不是局部噪声,而是会沿着整个 ReAct 循环持续放大的系统性偏差。
面对一个数十万行的代码库,这不是单一算法能够解决的问题。当前工程实践中,至少存在四种彼此互补的检索范式;它们回答的是四类不同的问题,因此也有各自清晰的适用边界。
Markdown 静态全局地图
一种做法是将整个仓库的架构信息以 Markdown 形式固化——例如 codemap.md、deepwiki、CodeWiki 等项目级别的静态文档。理想情况下,Agent 读取这份地图即可快速定位代码区域。
- 适用场景:记录不经常变动的底层架构、设计思想、系统中确定不变的 Invariant
- 致命缺陷:对于业务迭代频繁的模块,这份静态地图一旦维护不及时,塞给 Agent 的便是过期信息——过期的上下文比没有上下文更危险
语义与向量检索
Cursor 开箱即用的语义搜索是另一种范式——基于向量相似度,从代码库中检索与用户意图最相关的片段。其核心优势在于:Agent 不需要知道"某个功能在哪个文件",只需描述意图,搜索引擎返回结果。
- 适用场景:新手上手仓库时完全不知道功能分布;Agent 迷失方向时基于模糊意图进行大范围定位
- 局限性:结果通常以 top-k 片段返回,Token 开销较大;面对精确符号、固定路径或严格正则模式时,效率往往不如传统方法
传统精确匹配(Grep / Ripgrep)
当任务范围已经足够明确时——例如“我只改 Engine/Sources/Development/Mock/ 目录下的代码”——传统的 grep + 正则往往仍然是效率最高的手段。它直接返回高密度命中行,几乎不引入额外解释层,Token 开销也远小于语义检索。
- 适用场景:已知目录、文件名、符号名或可描述为明确文本模式的问题
- 局限性:无法理解模糊意图;当调用点经过封装、别名或跨语言跳转时,单纯文本匹配容易漏掉关键线索
纯粹 AST 拓扑分析工具
除上述三种范式之外,还存在一类专注于代码结构本身的工具——不依赖语义检索,而是基于 AST(抽象语法树)对代码的拓扑关系进行精确分析。代表性工具包括 Serena、ast-bro 等。
这类工具的核心能力是:直接提供文件的符号表(所有函数、类、方法的名称和起止行号),以及符号之间的调用关系和继承关系。ast-bro 可以直接输出一个文件中有哪些 symbol,这在 Agent 读取源文件之前极其有用——先看文件结构,再决定读取哪些符号的源码,避免盲加载整个文件。codegraph-mcp 则在 AST 基础上叠加了调用图分析,帮助 Agent 理解不同模块之间的依赖方向。
- 适用场景:已知文件位置,需要理解其内部结构或与周边模块的调用关系
- 局限性:无法处理“不知道在哪儿”的模糊搜索场景——通常必须先知道目标文件路径,至少先把搜索空间收缩到某个局部范围
四种范式关注的是四个不同层面的问题:静态地图回答“系统大体如何组织”,语义检索回答“大致应该去哪里找”,Grep 回答“这里是否出现过这个文本模式”,AST 工具回答“这个文件内部有哪些符号、它们如何关联”。它们之间不是替代关系,而是搜索粒度逐步收缩后的接力关系。
这也正是它们无法互相替代的原因:没有银弹,只有场景匹配。 语义检索适合“不知道在哪儿”,Grep 适合“知道大致在哪儿”,AST 工具适合“已经找到文件,但还需要理解结构”,静态地图则适合承载低频变化的架构知识。成熟的 Agent 系统不应把某一种检索方式神化为万能解,而应同时提供多种检索入口,让模型根据当前任务性质自主切换。
实战:code-rag —— 语义检索的工程落地
受 EA 与 NVIDIA 合作实践的启发——通过 AST 感知的代码分块与混合稀疏/稠密向量检索,为 AI Agent 注入更准确的代码库上下文——我实现了一个本地 MCP 工具:code-rag。
核心架构
code-rag 围绕三个设计目标构建:
智能混合检索。 语义搜索(本地 Qwen3-Embedding 0.6B,支持 CUDA)与关键词搜索(BM25)并行执行,再通过 RRF(Reciprocal Rank Fusion,倒数排名融合) 合并排序结果。语义搜索负责召回“意图相近”的候选代码,BM25 负责召回“字面命中”的候选代码,RRF 则在不人为偏向某一方的前提下融合两种排序信号。
AST 感知分块。 以函数、类、方法等语义完整单元为最小分块单位,保留文档注释和必要上下文,避免固定长度切块造成语义断裂。对于超大代码块,再按符号(函数/方法)做二次分块预览,减少一次性加载整段大文件的成本。
轻量高效。 Qwen3-Embedding 0.6B 在 RTX 5070 上为 messiah 源码(23000+ 文件)构建索引约需 1.5 小时;换用更轻的 bge-small 仅需 30 分钟。对本地工具而言,这一成本仍处于可接受区间:索引不是每次任务都重建,而是用一次预处理换取后续大量检索回合的加速。
七组工具
code-rag 提供七组 MCP 工具,覆盖从"探索全局"到"定位单符号"的完整检索需求:
| 工具 | 功能 | 设计意图 |
|---|---|---|
list_indices | 列出所有已构建索引 | 多仓库模式入口 |
get_repo_structure | 基于索引生成目录树 | 比 ls -R 更快,忽略 .git / node_modules |
search_code | 混合检索代码(语义+BM25+RRF) | 自适应片段长度:top_k 越小,片段越长 |
search_docs | 检索文档(.md / .rst / .txt) | 与 search_code 解耦,文档按标题/段落分块 |
get_file_symbols | 列出文件所有符号(名称+种类+行号) | 不返回源码,用于先了解结构再精准读取 |
get_symbol_info | 查询指定符号的声明、引用、基类/派生类 | 已知确切标识符时,效率远高于 search_code |
read_code | 按路径+行号范围精确读取代码 | 带行号标记,支持上下文行 |
Grep vs RAG:场景化使用
一个结论非常稳定:问题从来不是“Grep 更好还是 RAG 更好”,而是“当前任务究竟属于哪一类检索问题”。
NVIDIA 提到过一个典型案例:EA 的一款橄榄球游戏中,开发者询问 Agent——“天气是否会影响踢橄榄球的准确性和力量?”
- 没有 rag 时:Agent 在代码库中搜索了约 20 分钟,最终超时,没有返回任何有效结果,甚至没有尝试基于猜测强行生成答案。
- 有 rag 时:系统在几秒内返回约 10 个相关文件;人工复核后,其中 8 个确实命中核心逻辑,2 个无关。LLM 基于这 8 个文件顺利理解了实现机制,并完成了后续任务。
- 即使索引略有陈旧:开发者反馈依然“足够好用”。原因在于,索引的职责不是替代源码,而是先把搜索空间压缩到一个可处理的范围;一旦找到了大致区域,Agent 仍会继续读取磁盘上的最新文件来补齐细节。
这个案例的意义不在于证明“向量检索万能”,恰恰相反,它说明了检索工具在 Agent 系统中的正确定位:先负责缩小搜索空间,再把精读和验证交给后续工具链。
- 任务范围明确时:
grep+ 正则才是王者——返回高密度命中行,Token 开销极小。例如"只改 Engine/Sources/Development/Mock/ 下的代码",一次 grep 直接定位。 - 任务意图模糊时:code-rag 的语义搜索是降维打击——Agent 不知道功能藏在哪,用自然语言描述意图即可检索到相关代码。但 top-k 结果通常消耗较多 Token。
- 已知确切符号时:
get_symbol_info效率最高——直接获取声明、引用和继承关系,免去全文检索。
一条原则:不要试图用同一种检索工具覆盖所有场景。 最合理的工程做法,是同时提供 Grep、语义检索与符号级结构分析能力,让 Agent 先判断自己面对的是“模糊定位”“精确匹配”还是“结构理解”问题,再选择对应工具。工具越贴合问题形态,后续上下文就越干净,推理成本也越低。
反馈工具:把原始输出加工成可用信号
有了工具,并不意味着反馈就自动有效。原始工具输出往往不适合直接喂给模型:编译日志可能有数千行,测试日志混杂多路输出,运行时崩溃可能只在控制台闪过,渲染错误甚至不在文本日志中出现。
因此,我们需要做的不只是“允许 Agent 调命令”,而是把外部世界的原始噪声加工成模型可读的反馈信号。
| 反馈来源 | 原始问题 | 应做的事 |
|---|---|---|
| 编译日志 | 输出过长,错误行被淹没 | 提取错误、合并上下文、过滤已知无害噪声 |
| 运行崩溃 | 弹窗阻塞、退出竞态、日志丢失 | 自动捕获崩溃信号,保存关键快照 |
| E2E 测试 | 结果分散,失败原因难定位 | 汇总通过/失败计数,失败时给出最小定位线索 |
| 调试信息 | 状态只能在人类 IDE 中看到 | 暴露断点、调用栈、变量值等结构化接口 |
| 渲染结果 | 正确性不在文本中 | 提供帧捕获、资源绑定、Draw Call 级反馈 |
我在实践中主要做了三层封装。
第一层是把验证入口固定下来。build_full.py 把开发中常见的验证路径整理成稳定 preset:只跑冒烟、只编 C#、只编 C++、C++ + C# 联合验证、完整流水线等。Agent 不需要在每次任务中重新推断"生成工程、编译引擎、生成反射、编译脚本、再启动游戏"这样的流程顺序,只需要选择与任务风险匹配的 preset。这样做的价值,是用确定性脚本替代上下文里的流程记忆。
第二层是把原始日志压缩成可行动反馈。编译和运行日志往往有数千行,真正有用的只是错误行、附近上下文、失败用例和诊断文件路径。因此 build_full.py 会提取 error / fatal / exception / assertion 等关键信号,合并相邻上下文,过滤项目中已知无害的噪声输出,并把每一步日志单独落盘。Agent 首先看到的是摘要:哪一步失败、失败类型是什么、最可能该读哪个日志文件;只有需要深挖时,才继续读取完整日志。
第三层是补上 Agent 原本看不见的运行时反馈。游戏进程可能被 assertion 弹窗卡住,有些C++日志可能只存在于 console screen buffer 中。assert_monitor.py 的作用就是在这些边界处补充观察能力:监控 assertion 弹窗、自动保存弹窗信息、捕获控制台快照、记录崩溃退出信号。把更高层的"功能是否完成"转化为可汇总的通过/失败结果。最终,Agent 获得的不是未经处理的原始输出,而是一组可以继续推理的事实:哪一步失败、失败证据在哪里、下一步该验证什么。
更细粒度的调试工具也是同一逻辑。现在有很多MCP可以分别把断点调试、IDE 状态和渲染帧分析暴露给 Agent。它们的共同价值不是“工具更多”,而是让反馈更接近模型的盲区:编译错误模型还能猜一部分,运行时变量必须实际看,渲染画面更必须由外部系统返回。
可以把这些反馈理解为一条信息梯度:
编译错误 → 测试失败 → 运行时状态 → 画面/帧分析
越靠右,越难由模型凭语言模式推理出来,反馈价值也越高。
小结
本模块讨论的核心不是“给 Agent 接多少工具”,而是“如何让工具形成有效反馈回路”。结论可以归纳为五点:
-
Feedback Loop 是 Prompt 空间里的优化过程。 它不改变模型参数,但通过改变上下文控制下一轮输出,因此反馈质量直接决定迭代方向。
-
高价值反馈来自模型盲区。 模型能猜到的信息价值有限;编译、测试、运行、调试、渲染这些外部事实,才是纠错真正需要的信号。
-
外部反馈优于空泛反思。 反思只是重新生成,外部反馈才引入新证据。工程上应优先建设可执行、可复现、可结构化返回的验证工具。
-
工具是 Agent 的感官系统。 检索、读取、编译、测试、调试、渲染分析分别补全不同感知能力;Harness 的职责是让这些感官返回低噪声、高密度的信息。
-
实践细节应服务于同一个思想。 code-rag、build_full.py、assert_monitor.py、E2E 测试和调试 MCP 的共同目标,不是堆工具,而是把 Agent 无法自然感知的外部事实,转化为下一轮推理可用的上下文。
从 Context Engineering 到工具反馈,主线是一致的:前者控制信息的量,后者控制信息的质。没有上下文管理,反馈会被噪声淹没;没有外部反馈,上下文再干净也无法纠错。真正可用的 Agent,不是更会“想”的模型,而是被放进了一个更可靠的感知、验证与修正系统。
多模型编排:混合舰队
前面的几个模块分别回答了三个问题:Agent 的能力从哪里来,为什么会被上下文拖垮,以及如何通过工具反馈把外部世界重新接回推理循环。这些讨论默认了一个前提:整个任务主要由一个模型承担。
但真实的软件工程任务并不是单一能力测试。一次引擎开发任务可能同时包含架构判断、代码搜索、文档查证、局部修改、编译验证和结果审查。它们对模型能力的要求并不相同:架构判断需要高质量推理,代码搜索需要高吞吐读取,局部修改需要稳定执行,审查则需要多视角交叉验证。
如果所有环节都交给最强模型,成本会被大量低价值 Token 消耗掉;如果所有环节都交给便宜模型,又会在高熵决策处付出错误代价。因此,多模型编排要解决的不是"哪个模型最好",而是一个更工程化的问题:如何把不同认知任务分配给最合适、最经济的算力节点。
没有银弹模型,只有能力剖面
大模型之间并不是简单的"强"与"弱"。它们在训练数据、指令微调、对齐策略、奖励偏好和工具调用适配上都有差异,最终表现为不同的能力剖面和失效模式。
从工程实践看,这种差异至少体现在三个维度:
- 推理深度:能否在路径不明确、约束复杂、反馈不完整时找到正确方向。
- 执行稳定性:能否在明确任务下遵循格式、边界和项目规范,不随意发散。
- 成本吞吐:能否低成本处理大量读取、检索、归纳和批量判断。
我的实践中,模型选择更接近"按能力剖面选工具",而不是"按榜单排名选模型":
- GLM-5.1:综合执行能力和成本,在 Sonnet 4.6 以及以下级别模型中极具竞争力,适合作为高频日常调度和常规实现的主力。
- GPT-5.5:开放式推理能力强,适合复杂架构、疑难 debug、方案取舍等高熵任务。Opus 4.8 相比 4.7 有明显进步,但与 GPT-5.5 的对比仍需更多实战验证。
- DeepSeek-V4-Flash:非常适合代码探索、文件读取、文档检索等 Token 密集型任务,优势是成本极低、吞吐高。
- DeepSeek-V4-Pro:价格有吸引力,但独立解决复杂问题的能力不足。它更适合作为明确子任务的执行者或审查节点,而不是承担开放式问题的最终决策者。
任务越聚焦、边界越明确,所需模型能力越低;任务越开放、路径越不确定,越需要高阶模型介入。 多模型编排的第一步,就是把原本混在一起的任务拆成不同能力需求的子问题。
多 Agent 的价值不只是并行
在引入多个模型之前,需要先回答一个更基础的问题:为什么不让一个强模型从头做到尾?
AgentVerse 的多智能体研究提供了一个有代表性的答案。它通过动态专家招募、协作决策和评估反馈,在编程与工具调用任务中系统比较了单智能体和多智能体的差异。结果显示,多 Agent 的优势并不只是"可以并行",而是来自三个更本质的机制。
拆解降低单个上下文的复杂度
单 Agent 需要同时承担理解需求、搜索资料、编写代码、运行验证和自我审查等职责。这些职责会不断把不同类型的信息写入同一个上下文窗口:需求讨论、搜索结果、失败尝试、日志输出、临时计划、修复方案彼此交织。随着轨迹变长,Context Rot 迟早发生。
多 Agent 架构则把复杂任务拆到多个独立窗口中。探索型 Agent 可以消耗数万 Token 搜索代码库,但只向主 Agent 返回 1,000 到 2,000 Token 的关键发现;实现型 Agent 只接收明确任务和必要上下文;审查型 Agent 只看 diff、约束和验证结果。每个 Agent 处理的是被裁剪过的问题,而不是完整混乱的历史。
这不是简单的流程优化,而是对注意力资源的重新分配:把一个大而脏的上下文,拆成多个小而干净的上下文。
多视角可以弥补单模型盲区
AgentVerse 在 Humaneval 代码补全任务中显示,多智能体 Group 模式相较单智能体 Chain-of-Thought 模式有更高的 Pass@1:基于 GPT-4 的智能体通过协作,指标从 83.5 提升到 89.0。在计算器 GUI 案例中,多智能体不仅完成了核心功能,还因为引入 UI/UX 设计师、测试员等角色,补上了颜色区分、键盘输入和异常处理等单 Agent 容易忽略的细节。
这类收益的根源在于:复杂软件任务很少只有一个评价维度。代码能跑只是底线,之外还有异常处理、边界条件、可维护性、用户体验、性能和安全性。单 Agent 即使能力很强,也容易在当前上下文主线的牵引下忽略侧面约束;多 Agent 通过角色分工,强制引入不同视角,从而扩大问题覆盖面。
评估反馈可以纠正过早完成
工具调用任务更能体现多 Agent 的优势。AgentVerse 在 10 个需要多工具协作的复杂任务中完成了 9 个,而主流单 ReAct Agent 只完成了 3 个。单 Agent 失败的常见原因不是完全不会做,而是忽略部分要求后过早宣布完成。
这与第一模块的机制分析一致:Agent 的"完成"也是一次概率续写。当上下文中已经出现了足够多的"看起来像完成"的信号,模型可能生成总结语,而不是继续检查未满足的约束。多 Agent 系统中的评估模块相当于外置了一道检查关:即使执行 Agent 遗漏子任务,评估 Agent 也可以在下一轮反馈中指出缺口。
因此,多 Agent 的理论价值可以概括为两点:隔离上下文污染,增加独立判断来源。 前者缓解 Context Rot,后者降低单一概率空间带来的盲区。
混合舰队:按任务熵值调度算力
基于上述认知,我采用了一套多模型协作系统,调度原则是:高阶模型做大脑,便宜模型做手脚;高熵任务用强模型,低熵任务用高吞吐模型。
这里的"任务熵值"不是严格数学定义,而是一个工程判断指标:任务的未知数越多、路径越开放、错误代价越高,熵值越高;任务边界越清楚、输入输出越明确、可验证性越强,熵值越低。
典型划分如下:
| 任务类型 | 熵值 | 典型特征 | 调度策略 |
|---|---|---|---|
| 架构取舍、疑难 debug、跨模块设计 | 高 | 不知道答案在哪,错误方向代价大 | 交给最强推理模型 |
| 子任务拆解、日常派发、进度控制 | 中 | 需要判断优先级,但问题结构相对清晰 | 交给高性价比主力模型 |
| 代码搜索、文档检索、日志归纳 | 低 | Token 密集,推理要求低,可反复验证 | 交给低成本高吞吐模型 |
| 小范围修复、机械迁移、局部补测试 | 低到中 | 范围明确,验收标准清楚 | 交给中等模型执行 |
| Review、风险排查、遗漏检查 | 取决于范围 | 需要独立判断,适合并行 | 多个模型独立审查后交叉验证 |
在这个原则下,混合舰队由几类角色组成:
| 角色 | 模型选择 | 职责 | 选择理由 |
|---|---|---|---|
| Orchestrator(调度者) | GLM-5.1 | 日常任务派发、子任务拆解、进度追踪、常规推理 | 不需要每次都最强,但必须足够稳定、便宜、能判断"该派给谁" |
| Oracle(先知) | GPT-5.5 | 架构设计、复杂 debug、高熵疑难决策 | 适合开放路径型问题,用于处理超出日常模型能力范围的关键节点 |
| Explorer / Librarian(探索者/图书管理员) | DeepSeek-V4-Flash | 代码搜索、MCP 工具调用、文档检索、资料归纳 | 成本低、吞吐高,适合大量读文件和工具调用 |
| Fixer(修理工) | DeepSeek-V4-Pro / GLM-5.1 | 小范围修 bug、特定函数增量修改、明确测试补齐 | 上下文干净且目标明确时,中等模型即可胜任 |
| Reviewer(审查者) | 多个 DeepSeek-V4-Pro 实例 | 并行审查、风险点排查、结论交叉验证 | 审查适合独立判断和多数共识,不一定依赖单个最强模型 |
成本-能力匹配:把贵模型用在刀刃上
多模型编排最直接的收益是成本控制,但它不应被理解为"为了省钱用便宜模型"。更准确的说法是:让每一分算力花在它能产生最大边际收益的地方。
代码探索就是典型例子。一次语义搜索、文件读取或日志归纳可能消耗数万 Token,但它对深度推理的要求并不高。让旗舰模型做这类工作,相当于用昂贵推理能力支付 I/O 成本。相反,架构决策可能只输出几百 Token,却决定后续数天实现方向,这时使用最强模型是合理的。
整体成本上,GLM-5.1 + GPT-5.5 + DeepSeek 系列的组合,明显低于 OMO/Claude Code 标配的 Opus-4.6 + Sonnet-4.6 + Haiku 三件套。更重要的是,这种组合把成本结构拆开了:高价模型只出现在少量高熵节点,低价模型承担大部分低熵吞吐。
Reviewer 是成本-能力匹配最典型的场景。单个强模型做审查当然有价值,但任何模型都会误报或漏报。审查任务天然适合拆块、并行和交叉验证:让多个 DeepSeek-V4-Pro 实例独立阅读不同文件或同一 diff 的不同侧面,再由另一轮模型汇总结论、过滤重复、要求证据。单个 DeepSeek-V4-Pro 的判断未必可靠,但多个独立判断之间可以互相抵消偶然误差。
在实践中,几轮 DeepSeek-V4-Pro 的交叉 review 后,误报率往往低于一次 Opus 审查;即使开 40 个 DeepSeek-V4-Pro 做并行审查,总成本仍可能低于一次 Opus 调用。对于代码Review这个任务,使用Opus仍然会误报,而Deepseek-V4-Pro多轮验证后误报概率非常低。这说明在"独立判断 + 可交叉验证"的任务上,数量可以部分替代单体质量。它也呼应了前文关于 Majority Vote 的结论:当任务可以并行采样时,多次便宜的独立判断,常常比一次昂贵的单点判断更划算。
编排越强,越要警惕上下文膨胀
多模型编排并不是越复杂越好。每增加一层调度,就会增加一次模型调用、一次等待延迟、一份中间产物和一个潜在故障点。如果编排层本身过重,它会重新制造前文一直试图避免的问题:上下文膨胀、轨迹污染和注意力稀释。
尤其需要警惕的是前置工作流注入。某些强力编排模式(OMO的ULW)会在用户命令之前注入大量角色定义、流程规则和自动化策略。这些内容看似提高了智能化程度,实际却占据了 Context Window 最宝贵的开头区域。根据 Lost in the Middle 效应,模型对开头和结尾最敏感;前置指令越长,用户真实任务越容易被挤出注意力热点。Agent 在长会话中"忘记最初命令",很多时候不是因为模型突然失忆,而是因为真实目标早已被厚重的编排脚手架稀释。
因此,我更倾向于使用OMO-Slim:他提供了简单轻量开箱即用的编排器,同时没有过多复杂的功能。
判断编排是否值得,有一条简单标准:编排节省的 Token 和错误成本,必须大于编排自身消耗的 Token 和复杂度。 如果一个工作流为了"自动化"引入大量固定提示词,却没有显著减少搜索、试错和返工,它就是净负担。
小结
多模型编排的核心矛盾是:复杂工程任务确实需要分工,但分工本身也会带来上下文、成本和流程复杂度。混合舰队的解法可以归纳为四点:
- 先拆能力,再选模型。 不按模型排名分配任务,而按推理深度、执行稳定性和成本吞吐拆分任务。
- 按熵值调度算力。 高熵问题交给强模型,低熵吞吐交给便宜模型,明确子任务交给中等模型。
- 用独立判断降低单点风险。 Review、风险排查和遗漏检查适合并行审查与交叉验证,多个便宜模型的共识有时优于单个昂贵模型的单次判断。
- 保持编排轻量。 注意编排带来的上下文膨胀。
从子代理架构到混合舰队,主线始终没有变化:隔离上下文,精炼信息流。 子代理架构隔离的是任务空间,多模型编排隔离的是认知职责。真正有效的多模型系统,不是把更多模型堆在一起,而是让每个模型只看到它该看的信息,只承担它最适合的工作。
用确定性对抗不确定性:权限、Hook 与护栏
前面几个模块分别讨论了上下文管理、反馈回路和多模型编排。它们可以提高 Agent 做对的概率,却不能改变一个底层事实:Agent 的行为仍然来自概率性生成。
只要输出仍是概率采样,就不存在一条内建边界能保证模型永远不越权、永远不误解规则、永远不把错误续写转化为真实动作。更好的上下文、更强的模型、更精细的编排,都只能降低风险,不能消除风险。
因此,Agent 工程还需要另一类手段:把关键约束移出模型的 Token 生成管道,用确定性的系统机制兜住概率性行为。换言之,不能只让 Agent "知道不该做",还要让它在必要时做不了。
Prompt 规则的边界
最常见的做法,是把项目规则写进 AGENTS.md、CLAUDE.md 或 System Prompt:哪些目录不能改,提交前要跑哪些测试,某些文件编码不是 UTF-8,写代码前必须先读文件。这个做法有价值,因为 Agent 确实需要理解任务背景和协作规范。
但它有一个根本限制:写在 prompt 里的规则,仍然只是上下文中的一段文本。
模块 1 已经说明,角色标记并没有改变语言模型的底层机制;System Prompt、用户消息、工具输出、Agent 自己生成过的话,最终都会进入同一个上下文窗口。模块 2 又进一步说明,长上下文会发生注意力退化。于是,prompt 规则面临三重风险:
- 会被遗忘。 上下文膨胀后,规则可能沉入中间区域,权重下降。
- 会被误解。 模型可能把自己生成的中间结论误当成已满足的前置条件。
- 会被竞争信息稀释。 当工具输出、报错日志、用户追问和旧计划混在一起时,规则不再天然拥有最高优先级。
这并不是说 AGENTS.md 没有意义。它适合承载"需要模型理解"的规则,例如项目背景、代码风格偏好、协作流程和输出格式。但它不适合承载"必须无条件执行"的安全约束。后者如果只写在 prompt 里,本质上就是把安全交给模型的注意力和自觉。
因此需要区分两类规则:
| 规则类型 | 例子 | 合适载体 |
|---|---|---|
| 需要模型理解的规则 | 架构背景、术语约定、输出格式、协作流程 | AGENTS.md / System Prompt |
| 必须强制执行的规则 | 禁止删除目录、禁止读取密钥、写前必须读、编码转换、格式化 | 权限 / Hook / 工具层逻辑 |
核心原则可以概括为一句话:任何能用确定性工具解决的问题,都不应该只占用上下文。
编译器、类型系统、文件权限、CI Gate、格式化器,都是软件工程长期积累下来的确定性机制。它们的共同点是:运行在模型之外,不依赖模型是否记得、是否理解、是否愿意遵守。Agent 工程需要把同样的思想应用到自身:让模型负责推理和生成,让框架负责不可妥协的边界。
权限控制:把"不应该"变成"不能"
回到 WSL 删库事件。事故有两个必要条件:第一,Agent 在概率空间中生成了删除 WSL 的行动;第二,没有任何硬边界阻止这个行动被执行。--dangerously-skip-permissions 的问题不只是"免确认",而是它把最后一道确定性闸门拆掉了。
这里的教训不是简单地说"不要开免确认模式"。更深层的教训是:让 Agent 的认知边界和行动边界分离。 Agent 可以误判,可以幻觉,可以把上下文里的错误文本当成事实;但这些错误不应该自动获得操作系统级别的执行权。
现代 Agent 框架中的权限系统,目标正是把"希望 Agent 遵守"升级为"Agent 无法突破"。一个典型配置如下:
---
mode: subagent
permission:
"*": "deny"
read: "allow"
edit:
"src/**": "allow" # 允许写 src/ 及其所有子目录
"tests/**": "allow" # 允许写 tests/ 及其所有子目录
"docs/*.md": "allow" # 允许写 docs/ 下的一级 md 文件
"package.json": "allow" # 允许写根目录下指定文件
"config/**": "deny" # 明确禁止写 config/
".github/**": "deny" # 明确禁止写 CI/CD
"*": "deny" # 其他全部拒绝
glob:
"src/**": "allow"
"tests/**": "allow"
"*": "deny"
grep:
"src/**": "allow"
"tests/**": "allow"
"*": "deny"
---权限控制通常包含三层:
- 工具级限制:某个角色根本没有某类工具。例如 Reviewer 没有
write和edit权限,只能提出审查意见,不能直接修改代码。 - 路径级限制:某些目录或文件不可读、不可写。例如密钥目录、CI 配置、发布脚本,对普通实现 Agent 默认不可见或不可改。
- 操作前置条件:某个动作只有在满足确定条件时才能执行。例如编辑工具强制检查目标文件是否在最近 N 轮被读取过,未读取则拒绝修改。
这些限制的价值在于,它们在工具调用真正落到文件系统或操作系统之前完成拦截。无论 Agent 此刻处于 Smart Zone 还是 Dumb Zone,无论它是否被错误上下文污染,权限系统都不需要理解它的动机,只需要判断"这个调用是否被允许"。
一个尤其重要的实践是审查与实施的权限分离。Reviewer 不应拥有修改代码的权限。Reviewer 的职责是发现问题,Coder 的职责是修复问题。如果 Reviewer 可以直接改代码,那么审查阶段本身就可能引入新的幻觉修改;如果它只能输出意见,系统就保留了第二个独立判断点。这不是流程洁癖,而是防御纵深。
Hook:把项目规则做成透明基础设施
权限控制解决的是"不允许做什么"。Hook 解决的是另一类问题:有些事必须按特定方式做,但没有必要让 Agent 知道。
以文件编码为例。如果源文件编码是 GB18030,而不是 UTF-8。标准 Agent 的 Read / Edit 工具默认按 UTF-8 工作,直接读取会乱码,直接写入可能破坏已有多字节字符。
最直接的做法,是在 AGENTS.md 里写:"本项目源文件编码为 GB18030,读写时必须转换。" 但这会产生三个问题:
- 持续占用上下文。 每次会话都要为这条规则支付 Token。
- 持续占用注意力。 Agent 每次读写都要额外判断是否需要转换。
- 把字节级问题交给了语言模型。 编码转换是确定性字节操作,脚本擅长,模型不擅长。
更合理的做法是把它做成 Hook:读取文件时,Hook 在工具返回前执行 GB18030 到 UTF-8 的解码;写入文件时,Hook 在落盘前执行 UTF-8 到 GB18030 的编码。Agent 始终看到 UTF-8 文本,完全不需要知道底层编码。规则没有消失,而是从 prompt 中下沉到了工具层。
代码风格也同理。如果项目要求 Tab 缩进,而模型默认生成四空格缩进,不必反复提醒它"记得用 Tab"。可以在写入后运行格式化脚本,把缩进统一为项目标准。Agent 输出它最自然的代码,Hook 在落盘前完成确定性校正。
这类规则的对比如下:
| 做法 | 模型感知程度 | 上下文消耗 | 可靠性 |
|---|---|---|---|
| 写在 AGENTS.md 中 | 高,模型必须记住并遵守 | 持续消耗 Token | 概率性,随上下文膨胀衰减 |
| 通过 Hook 透明处理 | 零,模型完全无感知 | 零,不进入上下文 | 确定性,脚本行为稳定 |
Hook 的通用模式是:在 Agent 的输入和输出边界上插入确定性逻辑。
- 前置 Hook:在工具执行前改写输入,例如路径校验、敏感信息过滤、编码转换、参数规范化。
- 后置 Hook:在工具执行后处理输出,例如日志裁剪、格式化、自动补锁文件、生成结构化摘要。
- 事件 Hook:在会话生命周期中触发,例如工具失败告警、任务完成摘要、提交前验证。
Hook 的本质,是把项目中的隐性工程约束变成透明基础设施。模型预训练中已经学会的默认行为,如果与项目规范一致,就无需额外指令;如果不一致,不要优先用 prompt 纠正,而应优先用 Hook 纠正。让模型在它擅长的 Token 空间里生成,让确定性脚本在它看不见的边界处对齐项目现实。
三层约束模型
到这里,可以把 Agent 约束分成三层:
| 层级 | 作用 | 适合放什么 | 不适合放什么 |
|---|---|---|---|
| Prompt | 让模型理解任务 | 背景、目标、协作流程、输出格式 | 安全边界、字节级规则、强制校验 |
| Permission | 限制模型能做什么 | 工具权限、路径边界、角色隔离 | 需要模型灵活判断的业务语义 |
| Hook | 透明改造 I/O | 编码、格式化、日志过滤、自动验证 | 需要创造性推理的设计决策 |
这三层不是替代关系,而是分工关系。Prompt 负责表达意图,Permission 负责划定硬边界,Hook 负责把稳定规则自动化。一个成熟的 Agent 工程系统,不会把所有规则都塞进 AGENTS.md,也不会试图用权限和 Hook 取代模型的判断;它会让每条规则落到最适合的位置。
小结
本模块讨论的是一个看似悖论的命题:越依赖非 AI 的确定性机制,Agent 的整体表现越可靠。
- Prompt 规则有价值,但不是硬约束。 写在上下文里的规则会被遗忘、误解和稀释,因此不能承载不可妥协的安全边界。
- 权限控制把"不应该"变成"不能"。 工具级、路径级和前置条件级限制运行在模型之外,不依赖模型是否处于 Smart Zone。
- Hook 把项目规则下沉为基础设施。 编码转换、格式化、日志过滤、提交前验证等稳定规则,不应长期占用模型注意力。
- 确定性工具能解决的问题,不只写进上下文。 Prompt、Permission、Hook 三层分工,才能同时获得灵活性和可靠性。
从上下文管理到反馈工具,从多模型编排到权限与 Hook,主线都是同一个:让 Agent 在概率性推理和生成上发挥能力,把确定性约束、规则执行和安全边界交给外部系统。这也引出下一个问题:如果任务不止持续一次会话,而是跨越数天甚至数周,外部系统还需要承担另一项职责——记忆。
经验与记忆:跨越时间的上下文管理
前面几个模块建立的是单次会话内的可靠性:控制上下文,接入反馈,编排模型,设置权限与 Hook。但真实工程任务往往不会在一次会话内结束。一个引擎改造可能持续数天,一个复杂 bug 可能跨越多轮调试,一个项目经验也不应该随着聊天窗口关闭而消失。
于是出现一个新的问题:Agent 如何记住上一次学到的东西?
模块 2 已经给出过答案的一半:模型本身没有记忆。每次调用都是一次新的文本输入,所谓连续性来自历史对话被重新拼接进上下文。会话一旦结束,模型并不会保留任何状态。哪些文件不能碰,哪个编译选项会触发 CI 问题,某个 API 的真实行为和文档不一致——这些经验如果没有被外部系统保存,就会在下一次任务中归零。
因此,记忆问题的本质不是"让模型真的记住",而是:如何把过去的有效上下文保存下来,并在未来需要时以正确的形式重新注入。
记忆仍然是上下文
回到第一模块的核心命题:语言模型只做文字接龙,输入决定输出。Agent 能使用的所有知识,都必须以某种形式出现在当前上下文中。所谓记忆,不过是把过去的信息存到外部介质中,再在合适的时候取回、筛选、拼接。
这一定义很朴素,却推出了一个重要约束:记忆系统的质量,不取决于它存了多少,而取决于它注入了什么。
如果记忆是准确的,它可以显著减少重复探索;如果记忆是错误的,它会比没有记忆更危险。因为一旦错误信息以"项目经验""历史结论""已验证事实"的形式进入上下文,模型会自然提高它的权重。前文已经反复强调:错误信息是上下文质量问题中优先级最高的风险,危害高于缺失信息,也高于噪声过多。
所以,记忆工程的核心不是"多存",而是"存得对、取得准、用得克制"。
单次任务内:Markdown 就是工作记忆
在讨论跨会话记忆之前,需要先把尺度缩小。对于单次长任务,最有效的记忆工具往往不是复杂数据库,而是一个持久化 Markdown 文件。
这与 Context Engineering 中"文件系统作为外部上下文"的策略完全一致。Agent 在执行过程中,把以下信息写入任务文件:
- 已完成步骤
- 已验证事实
- 当前阻塞点
- 后续计划
- 构建或测试日志的关键结论
在Agent 不需要把所有历史细节塞在上下文里,只需要在需要时通过 head、tail、grep 或文件读取工具取回相关片段。
这个方案之所以有效,是因为它把上下文从"聊天历史"变成了"可检索的外部状态"。聊天历史会膨胀、会腐烂、会被压缩;任务 Markdown 则可以被结构化维护,随时局部读取。它不是高级技术,却非常可靠:用磁盘换内存,用文件系统换上下文窗口。
在单次任务尺度上,这已经足够。真正困难的是跨会话、跨任务、跨时间的经验沉淀。
跨会话记忆的根本风险
跨会话记忆的诱惑很大。会话 A 结束时,Agent 总结经验写入知识库;会话 B 启动时,系统检索相关知识并注入上下文。表面上看,这就让 Agent 拥有了项目经验。
问题在于,这些记忆通常也是 Agent 生成的。
如果 Agent 在会话 A 中产生了幻觉,并把幻觉总结成"经验",会话 B 读取后就会把它当成事实继续推理。更糟的是,后续会话可能基于这个错误记忆继续生成新的错误总结。于是误差不再局限于单次会话,而是在时间轴上复合、扩散、固化。
这不是理论上的担忧。Wu 等人(2024)在 StreamBench 上的消融实验表明,将错误输出保留在上下文中不是中性操作,而会损害模型表现;即使明确标注"这是错误答案",也无法可靠抵消负面影响(见 StreamBench)。这意味着跨会话记忆必须延续前文的基本原则:宁可少给,不能给错。
Yuan 等人(2026)在 Useful Memories Become Faulty When Continuously Updated by LLMs 中进一步系统量化了这一问题。研究人员让 LLM 将自己生成的轨迹持续整合为抽象记忆,并在后续任务中反复更新。核心发现非常尖锐:
由当前 LLM 持续更新的整合记忆,会逐步失效,最终表现跌破无记忆基线。
即使在输入是 Ground-truth 完美轨迹的条件下,GPT-5.4 经过多轮流式记忆整合后,也会在 ARC-AGI 数据集中 54% 的问题上失败;而这些问题在无记忆条件下原本可以 100% 正确。也就是说,记忆不是单调收益,错误的记忆机制会把强模型拖到低于无记忆的水平。
实验揭示了三种典型失效模式:
- 错误分组。 模型把不相关任务的经验合并成同一类规则,跨越了本应保持分离的语义边界。
- 过度泛化。 抽象过程丢掉了经验生效的前提条件,提炼出的"通用规则"在相似但不同的任务上产生误导。
- 狭窄过拟合。 当连续输入过于单一时,模型记住的是表面模式,而不是底层规律;一旦任务变体出现,记忆立刻失效。
更关键的是更新策略:分组优于混合,一次性优于流式。 流式更新中,早期的错误抽象会成为后续整合的基线,误差会沿时间轴不断放大。这一点对工程实践尤其重要:每次会话结束都自动总结、自动沉淀、自动更新知识库,看似勤奋,实则可能在持续制造长期污染。
原始轨迹优先于抽象记忆
ARC-AGI Stream 实验中还有一个反直觉结论:如果不做复杂整合,只把原始轨迹作为情景记忆保留下来,让模型在需要时回看,其效果可以媲美甚至超过复杂的抽象记忆算法。
这给工程实践提供了一个重要启发:原始轨迹是证据,抽象记忆只是索引。
原始轨迹包括具体任务、具体输入、具体修改、具体测试结果和具体失败路径。它冗长、不优雅、不像"知识",但它保留了上下文和前提条件。抽象记忆则更短、更方便检索,但也更容易丢掉边界条件。对于 Agent 来说,丢掉边界条件往往比信息冗长更危险。
因此,一个相对稳健的记忆架构应当分为两层:
| 层级 | 目标 | 特点 | 风险控制 |
|---|---|---|---|
| 情景记忆(Raw Episodes) | 保留事实证据 | 原始任务、日志、diff、测试结果 | 尽量完整保存,可回溯 |
| 抽象知识(Lessons / Rules) | 提供导航和复用 | 经验总结、惯例、踩坑记录 | 必须能链接回原始证据 |
在这个架构中,抽象知识不能替代原始轨迹。它的作用更像目录、标签和索引:告诉 Agent 应该回看哪段历史,而不是直接要求 Agent 无条件相信一条浓缩结论。当抽象结论与当前任务冲突,或模型需要做高风险决策时,应当回到原始轨迹重新验证。
实验还揭示了更新策略对记忆质量的显著影响:分组优于混合,一次性优于流式。 按任务类型分组整合优于无差别的全部混合;单次处理完所有数据优于分批流式更新。流式更新中,早期错误的抽象会作为后续整合的上下文基线,误差逐步被放大——这是一个致命的复合效应。 研究团队对"不压缩"的策略给出了一个耐人寻味的结论:如果不做任何压缩,仅仅将原始轨迹日志追加到上下文窗口中让 LLM 进行上下文学习,其效果完全可以媲美甚至超越复杂的记忆整合算法。 原始情景记忆(Episodic-only)是一个极佳的基线。
现有工程尝试:候选知识,而非终审知识
社区已经有不少跨会话记忆方向的尝试,但共同特点是:它们都还没有摆脱人工审核的必要性。
Compound Engineering 提出 Brainstorm → Plan → Work → Review → Compound → Repeat 的循环。它的价值在于把"经验沉淀"显式纳入工作流,而不是让经验随会话结束消失。但 Compound 环节产生的内容仍需要判断:哪些是可复用经验,哪些只是单次任务中的偶然结论。
Agent Knowledge Framework 试图记录更丰富的项目知识:不仅记录函数怎么用,也记录为什么选择某个方案、上次改这里踩过什么坑。这类信息对 Agent 很有价值,但维护成本和置信度问题也更突出。记录越接近"经验"和"判断",越需要人类确认其适用范围。
我认为,全自动沉淀仍面临同一个难题:即使设计置信度体系,也无法从机制上保证 Agent 不会把幻觉写入记忆。一旦后续检索命中错误条目,系统就会沿着错误经验继续漂移。
因此,当前阶段更稳妥的定位是:Agent 可以生成候选知识,但不能独立决定最终知识。 它可以帮助记录、聚类、总结、检索;但"这条经验是否真实、是否通用、是否值得长期保留",仍需要人类或确定性验证来裁决。
当前可用的记忆原则
综合理论证据和工程实践,现阶段跨会话记忆不应追求全自动闭环,而应守住几条底线。
第一,原始轨迹是第一公民。 ARC-AGI Stream 实验的一条核心教训是:必须将原始情景轨迹(Raw Episodes)视为一等证据妥善保留。抽象记忆应被视为原始轨迹的索引和导航,而非替代——当存疑时,回溯原始轨迹。
第二,抽象应是人驱动的,而非自动触发的。 实验数据明确:分组优于混合,一次性优于流式。而人类最适合做分组和"一次性"的判断——在任务阶段性收敛后,由人决定"哪些教训值得沉淀",远好于让 Agent 在每轮对话末尾自动执行一次摘要压缩。记忆的整合应当是按需触发的(Opt-in),而非每次交互后自动强制执行。
第三,解耦"情景存储"与"抽象规则"。 在系统架构中,"快速情景记录"和"慢速知识提炼"应在逻辑上分离。前者保证"不漏",后者保证"不乱"。抽象过程需要能回溯到原始情景——Yuan 等人(2026)的双层架构实验中,强制禁用抽象、仅保留情景管理的模式,表现已经与自动控制模式持平甚至更好。换言之,先保证不丢,再考虑抽象。
第四,人类审核仍是必要闸门。 这不是保守,而是由 Agent 的生成机制决定的:模型生成"正确记忆"和"错误记忆"时没有机制性边界。只要这一点不变,长期知识库就不能完全交给 Agent 自我维护。
小结
记忆问题,本质上是跨时间的上下文管理。单次任务内,一个结构化 Markdown 文件通常已经足够;跨会话时,问题的关键不再是"如何记得更多",而是"如何避免把错误记成知识"。
- 记忆 = 外部保存的上下文,在需要时重新注入。 它不改变模型无状态的本质,只是改变上下文的来源。
- AI 生成的记忆存在系统性退化风险。 错误分组、过度泛化、流式更新复合误差,可能让记忆系统跌破无记忆基线。
- 原始轨迹是证据,抽象记忆是索引。 抽象可以提高复用效率,但不能替代可回溯的原始情景。
- 当前最佳策略是:情景记录自动化,知识抽象人工化。 让 Agent 帮助记录和检索经验,但不让它独立决定哪些经验应成为长期知识。
这也引出了下一个问题:记忆、上下文、工具、权限、编排,最终都只是工程手段。谁来判断哪些经验值得沉淀,哪些目标值得执行,哪些风险不可接受?这就是下一模块的主题:人的位置。
人的位置:没有后移,而是上移
前面几个模块讨论的都是如何让 Agent 更可靠:用上下文管理减少注意力污染,用反馈回路把环境信号变成可用信息,用多模型编排降低单点盲区,用权限和 Hook 把关键约束外置为确定性机制,用记忆系统延长上下文的时间跨度。
这些方法能显著提高 Agent 的执行质量,但它们并不回答一个更上层的问题:谁来定义"什么是对的"?
Agent 可以按照需求写代码,但它不能判断需求本身是否值得实现;可以根据既定架构做 Review,但不能判断这个架构是否符合业务演进方向;可以把经验沉淀为记忆,但不能独立决定哪些经验应进入长期知识库、哪些只是一次性噪音。
这不是能力不足,而是机制边界。模块 1 已经论证:Agent 的本质是基于上下文的概率续写。它可以在给定目标下寻找高质量路径,却没有内生的目标函数;它可以优化局部答案,却无法从系统外部判断"这个问题是否应该被这样定义"。因此,方向的定义者不能是正在执行续写的机器,而必须是能够审视业务、团队、成本、风险和长期演进的外部主体。
这就是本文所有工程方法背后的隐藏主线:人的位置。
人机分工:一条朴素的分界线
讨论人的位置,首先要把人和 Agent 的责任边界划清。Boris Cherny(Anthropic 工程师、Claude Code 的创造者)曾给出一条朴素但有效的"二八分界线":
| 阶段 | 责任方 | 说明 |
|---|---|---|
| 提出高层需求(Requirements) | 🟩 人类 | 定义"要解决什么问题"——Agent 无法替你做 |
| 将需求转化为设计文档(Design) | 🟩/🟦 人机协作 | 人类定架构方向和关键约束,Agent 负责展开细节 |
| 根据设计文档实现方案(Implementation) | 🟦 Agent | 确定性高的执行层——Agent 的主力战场 |
| 添加测试(Testing) | 🟦 Agent | 按验收标准自动生成与运行 |
| 确保 CI 通过 | 🟦 Agent | 自动化的编译-测试-修复循环 |
| 代码审查(Review) | 🟦 Agent | 交叉审查,发现常规问题 |
| 更新文档(Documentation) | 🟦 Agent | 自动提取接口变更,生成更新 |
这条分界线的核心不是工作量比例,而是确定性比例。越靠后的工作,输入越明确、反馈越具体、验证越自动化,越适合交给 Agent;越靠前的工作,目标越开放、约束越隐含、评价越依赖语境,越需要人类判断。
因此,人类负责的前 20% 并不是"少量前置工作",而是后续 80% 的质量上限:需求定义决定 Agent 朝哪里跑,架构设计决定它沿哪条路径跑,验收标准决定它何时停下。设计文档是这条边界上的关键交接物——它既是人类意图的书面表达,也是 Agent 执行时最重要的上下文。
需要补充的是,"Review 完全交给 Agent"在真实工程中仍然过于乐观。Agent 可以承担大量低层次审查,例如风格、命名、重复逻辑、简单 bug 和测试遗漏;但架构边界是否稳定、数据流是否可长期维护、依赖方向是否会在后续迭代中形成债务,仍需要人类把关。
AI for Science:一条来自学术界的独立验证
这条"前端靠人、后端靠 Agent"的分工,不只出现在软件开发中。斯坦福研究人员举办的 AI Agent for Science 会议提供了一个来自学术界的独立验证。
这项实验的规则非常激进:AI 必须是论文的第一作者(主要贡献者),论文由 AI Reviewer 评审;247 篇投稿中接受 48 篇,接受率低于 20%,接近学术顶会。每篇论文由 3 个 AI Reviewer 打分,但最终评价仍由人类完成。
研究人员将论文产出拆分为四个维度,对比全部投稿与被接受论文的差异:
| 维度 | 全部投稿 | 被接受论文 | 差异 |
|---|---|---|---|
| Idea 构思 | 多为 AI 自主完成 | 人类介入明显更多 | ⚠️ 显著差距 |
| 实验设计 | 多为 AI 自主完成 | 人类介入明显更多 | ⚠️ 显著差距 |
| 数据分析 | AI 自主完成 | AI 自主完成 | 差距不大 |
| 论文写作 | AI 自主完成 | AI 自主完成 | 差距不大 |
结果呈现出清晰的阶段差异:在 Idea 构思和实验设计阶段,人类介入显著影响最终质量;在数据分析和论文写作阶段,AI 自主完成的效果已经接近被接受论文的水平。投稿者的反馈也指向同一个问题:AI 最薄弱的环节不是展开、整理和表达,而是提出真正新颖的问题。
这与前文对 LLM 本质的分析一致。模型倾向于生成训练数据分布中的高概率续写,而真正新颖的想法往往位于低密度区域:它不是对已有模式的平滑外推,而是重新定义问题、重组评价标准、甚至否定原先路径。Agent 可以把一个方向推进得很快,但方向本身往往需要人类从外部给出。创新不是续写。
人机分工的再思考:没有后移,而是上移
"人类的位置没有后移,而是上移。"
"上移"不是一句安慰性的口号,而是协作结构发生变化后的必然结果。AI 接手了越来越多执行层工作,并不意味着人的价值被压缩;相反,人的工作从实现细节上移到了目标定义、约束设计和结果判断。
Agent 能生成多种看起来合理的实现方案,但无法判断哪一种能承受未来半年的需求变化;能根据既定架构写出规范的 Review 意见,但无法判断架构本身是否匹配业务方向;能从十万行代码中检索出相关片段,但无法判断这个任务是否应该被做、现在是否应该做、以什么代价做。
过去,工程师的大量时间消耗在"如何做到"上:查 API、写胶水代码、处理编译错误、补测试、改格式。Agent 让这些高确定性、可验证、可迭代的工作显著加速。随之释放出来的时间,不是让人退出系统,而是要求人投入到更高层的问题:为什么做、做什么、不做什么、边界在哪里、失败代价是什么、如何验收。
这也改变了工程师证明价值的方式。过去,"我写了多少代码"相对容易量化;现在,更重要的问题变成"我定义的问题是否正确"、"我设置的边界是否清晰"、"我选择的技术路径是否能承受长期演进"。
实战:人定方向,Agent 执行
在日常开发中,这条分工线是具体而微的。
一个典型任务大致经历以下链路:
- 人类定义架构方向和验收标准;
- 人类借助 Agent 将需求拆成可独立执行的子任务;
- Agent 根据子任务完成实现并提交变更;
- Agent 运行编译、测试,修复可自动定位的问题;
- Agent 做细节 Review,给出潜在问题清单;
- 人类审查架构决策、数据流边界和长期维护风险。
这个流程的关键不在于"人是否还看代码",而在于人看什么。代码风格、命名规范、函数长度、明显重复逻辑,可以交给 Agent 批量检查;但接口是否足够稳定,数据流在并发场景下是否安全,依赖方向是否会制造循环,模块边界是否会阻碍后续需求演进,这些问题仍需要人类判断。
模块 5 中提到的 Council 交叉评审,可以让多个 Agent 从不同角度提出审查意见,降低单模型盲区。但 Council 也只是审查工具,它需要一个由人定义的审查框架:哪些边界是重要的,哪些风险不可接受,哪些权衡必须显式记录,哪些问题可以延后处理。没有这个框架,多 Agent 只是在不同角度上重复续写;有了这个框架,它才会成为有效的判断放大器。
因此,人类剩余的核心工作可以进一步收敛为两个动作:Spec(规格定义)与 Review(架构审查)。Spec 负责把方向写清楚,Review 负责确认结果没有偏离方向。这就是下一模块的主题。
小结
本模块从所有工程手段之上提炼了一个元问题:谁来定义方向?
-
Agent 的本质决定了它不能定义方向。 概率续写可以在给定目标下优化路径,但目标本身必须从外部注入。
-
人机分工的本质是确定性分工。 人类负责目标开放、评价依赖语境的前 20%;Agent 负责输入明确、反馈具体、验证自动化的后 80%。
-
人类的位置没有后移,而是上移。 从"如何做到"上移到"做什么"和"为什么",从代码作者上移为目标、边界和风险的定义者。
-
前 20% 的质量被空前放大。 Agent 执行越快,错误方向造成的浪费越大;高质量 Spec 和关键 Review 因而比过去更重要。
-
创新不是续写。 AI for Science 的数据表明,AI 可以很好地展开、分析和表达,但真正拉开差距的仍是 Idea 和实验设计这类方向性工作。
Spec 与 Review:人机交接的核心工序
上一模块确立了人机分工的基本方向:人类负责定义目标、边界和风险,Agent 负责在这些约束下执行。但分工一旦落到工程实践中,就会出现一个更具体的问题:人类的判断如何稳定地交给 Agent?Agent 的产出又如何回到人类判断之中?
答案是两道接口:Spec(规格定义)与 Review(审查验证)。Spec 是前向接口,把人类意图翻译成 Agent 可执行的上下文;Review 是反向接口,把 Agent 产出重新放回架构、业务和长期维护的语境中检验。Spec 定义"什么是正确的",Review 确认"做出来的东西是否仍然正确"。二者共同构成人机协作的交接协议。
Spec:把人的判断写成可执行上下文
模块 1 的核心命题贯穿全文:Agent 的输出由输入上下文决定。这个命题落到需求定义上,只有一个直接推论:写进 Spec 的内容,才有机会成为 Agent 的执行约束;没有写进去的内容,Agent 只能用训练数据中最常见的模式补全。
人类同事之间的协作依赖大量隐式知识:团队惯例、历史包袱、业务背景、代码库中的潜规则。Agent 没有这些默认背景。它看到的只有当前上下文,以及从预训练数据中学到的高概率续写方向。你以为"不言自明"的事情,对 Agent 来说往往根本不存在。
"I'll know it when I see it" 是人类协作中常见的需求态度:我现在说不清,但看到结果时能判断对不对。对 Agent 来说,这条路行不通。Agent 不会主动补齐你脑中的隐含约束。如果 Spec 没写"保留现有反射行为",它可能在改造反射系统时破坏兼容性;如果 Spec 没写"处理并发访问",它可能给出单线程下看似正确的实现;如果 Spec 没写"错误路径必须释放资源",它可能只覆盖 happy path。
这不是 Agent 偷懒,而是 Spec 没有完成自己的职责。模糊的 Spec 不会激发 Agent 的创造力,只会把最需要人类判断的空白交给概率补全。Spec 的第一原则是:不要假设 Agent 会多想一步。
从想法到 Spec:先审自己,再派任务
想法到 Spec 的距离,不能靠"想清楚了再写"来跨越。人往往是在写作、追问和审视中逐渐想清楚。因此,一个可复用的 Spec 工作流可以分为五步:
- Grill Me:让 Agent 扮演挑剔审查者,追问目标、边界、异常路径和验收标准中模糊的部分;
- To PRD:把澄清结果整理成结构化需求文档,明确目标、范围、非目标和验收条件;
- To Issues:把 PRD 拆成独立可验证的子任务,每个 Issue 对应一个可独立执行和回滚的修改单元;
- Writing Plan(可选):如果 Issue 仍然过大,继续拆出执行顺序、依赖关系和验证方式;
- 开始执行:Agent 按计划逐步实现、验证、修复和提交。
这个流程的每一步都在增加确定性:Grill Me 暴露模糊地带,PRD 固化目标边界,Issue 降低任务粒度,Plan 限制执行路径。具体工具可以替换,市面上类似方案很多:
- mattpocock/skills — Skills for Real Engineers. Straight from his
.claudedirectory - get-shit-done — 轻量级 meta-prompting + context engineering + spec-driven development
- LarsCowe/bmalph — Unified AI Development Framework(BMAD phases + Ralph execution loop)
- obra/superpowers — An agentic skills framework & software development methodology that works
工具的选择不是关键,关键是不要跳过"写下来"这一步。再强的模型也无法从你脑中的模糊想法中续写出正确方向。把想法写下来,让 Agent 审查它,再拆成可执行单元,这件事的杠杆效应往往比更换模型更大。
Review:从校对代码上移到确认对齐
Spec 定义"该做什么",但 Spec 并不保证 Agent 一定会忠实执行。Agent 可能误解上下文,可能局部最优,可能过早声明完成,也可能生成一段表面整洁、逻辑有毒的代码。因此,Review 是 Spec 的配套机制:它不是重新阅读代码,而是确认产出是否仍然对齐目标、架构和风险边界。
AI 生成代码改变了传统代码审查的信号结构。过去,代码风格差、命名随意、函数过长、结构松散,往往与设计混乱高度相关;审查者看 style,大致能推断质量。Agent 时代,这条相关性被瓦解。Agent 可以在一分钟内生成注释齐全、命名规范、格式漂亮的代码,但漂亮外壳下面可能是错误的数据流、脆弱的接口、缺失的异常路径,甚至是完全错误的需求理解。
因此,AI 时代的 Review 不是取消,而是重心上移:从“这段代码写得是否漂亮”,上移到“这次变更是否仍然在正确方向上”。人类不必在所有场景中都逐行校对 Agent 生成的代码,但必须明确自己在哪一层承担判断责任。
可以把人类参与 Review 的方式分成三种模式:
| 模式 | 人类位置 | AI 位置 | Review 重心 | 适用场景 |
|---|---|---|---|---|
| 辅助模式 | 直接审查者与最终责任人 | 可以是代码作者,也可以辅助生成、解释和初筛问题 | 逐行审查实现,确认局部逻辑、边界条件和性能细节 | 引擎核心模块、高风险改动、性能敏感路径 |
| 管理模式 | 技术负责人 | 主要执行者 | 审查架构、接口契约、数据流、依赖方向和风险边界 | 中等复杂度功能、跨模块改造、可测试的工程任务 |
| 代理模式 | 需求方与验收方 | 类似外部承包商 | 审查目标达成、验收标准、用户价值和可交付结果 | 工具链、脚手架、低风险自动化、一次性任务 |
三种模式不是成熟度阶梯,也不是互斥选项,而是风险分层后的协作策略。同一个团队、同一个项目里,可能同时存在三种 Review 方式。渲染管线、物理引擎、资源加载、内存管理这类核心系统,失败成本高、隐含约束多,更适合辅助模式或管理模式:人类必须深入关键路径,确认接口契约、生命周期、性能边界和异常路径没有被破坏。工具链脚本、编辑器辅助功能、批处理脚手架等低风险任务,则可以更接近代理模式:人类把需求、输入输出和验收标准定义清楚,让 Agent 完成实现,再根据结果决定是否接受。
这个分层的关键不是“人还要不要看代码”,而是“人应该把注意力放在哪里”。AI 最擅长的恰恰是优先级最低的校对工作:代码风格、命名一致性、格式、明显 bug、简单边界条件,都可以交给编译器、静态检查、测试和 AI 初筛。人的注意力应当投向更难自动化判断的部分:目标是否偏移、模块边界是否清晰、接口契约是否稳定、依赖方向是否反转、数据流是否正确、失败路径是否可恢复,以及这次改动是否会增加长期维护成本。
换言之,Review 的对象不再只是 diff,而是 diff 与 Spec 的对齐关系。代码只是证据之一,真正要回答的问题是:这份产出是否完成了 Spec 中定义的目标,是否遵守了架构约束,是否覆盖了风险边界,是否留下了可验证的行为证据。只有把 Review 上移到这一层,人类判断才不会被 AI 生成代码的“漂亮外观”误导。
好的审查评论:具体、可定位、可执行、可解释
既然 Review 的重心上移到架构对齐,那么审查评论的质量就成了杠杆点。一条好的审查评论需要满足四个特征:
- 提供具体细节:指出是什么问题、在哪里。不是"这里有问题",而是"
allocateBuffer()在buf == nullptr分支中缺少释放逻辑"。 - 引用具体代码或 issue:让评论锚定到具体位置和上下文,避免笼统评价。
- 建议解决方案:指出改进方向。审查的目的不是展示问题,而是推动修正。
- 提供证据或解释:说明为什么这样更好。没有解释的建议是个人偏好,有解释的建议才是工程判断。
这四条标准同样适用于给 Agent 的反馈。与其说"这个不对,重写",不如说"第 3 步返回的 arr[i+1] 在 i == len - 1 时越界,这是数组边界问题,需要在循环条件或访问前加保护"。对 Agent 的纠错和对代码的审查,本质上都是轨迹编辑:通过更具体的上下文,把模型从错误续写拉回正确路径。
AI 审查的边界:建议,不是结论
AI 可以做初步审查,捕捉明显 bug、style violation、遗漏的错误处理和可疑的边界条件。这能显著降低人类审查者的认知负荷。但 AI 审查有三类系统性局限,对其视而不见是危险的。
第一,误报(False Positives)。 当上下文复杂或接近窗口极限时,AI 往往倾向于保守判断,把正常代码标记为可疑。过度依赖这类建议会产生 alarm fatigue:审查者逐渐忽略 AI 的提示,或者反过来无脑接受所有建议,引入不必要的修改。
第二,无法理解项目特有惯例。 长期维护的代码库中充满历史约定:某个宏为什么必须放在函数开头,某个空指针检查为什么在特定路径下多余,某段看似奇怪的逻辑为什么不能简化。这些约定往往来自过去修复过的 bug,AI 很难仅凭当前代码推断出历史成因。
第三,无法承担复杂架构决策。 AI 可以指出接口命名不清,但无法判断这个接口是否能承受未来半年的需求变化;可以发现局部依赖,但无法判断依赖方向是否符合团队长期演进策略。这些判断需要业务语境、团队经验和风险偏好,不能交给概率生成模型终审。
因此,AI 审查的结果只能是建议,不是结论。最终判断权必须保留在人类审查者手中。Agent 的输出本质上仍是概率采样,不存在一条机制性边界能在生成时区分"正确判断"和"错误判断"。
实战:架构级 Review 的五段式框架
基于以上原则,架构级审查遵循一个五段式框架。它的设计原则只有一条:不看代码风格、不看函数长短、不看变量命名,只看边界和契约。
① 模块地图(Module Map)
这个 PR 改了哪些模块?
每个模块的职责是什么?
② 对外接口(Public Interface)
每个被修改的模块向外部暴露了哪些 API?
接口语义是否清晰、是否有向后兼容性问题?
③ 依赖关系图(Dependency Graph)
模块之间的依赖方向是怎样的?
有没有循环依赖?有没有依赖方向与预期相反的情况?
④ 数据流(Data Flow)
数据从入口到出口经过了哪些转换步骤?
在哪一步产生了 ownership transfer?
在多线程或异步场景下,谁是数据的最后所有者?
⑤ 风险边界(Risk Boundary)
这个 PR 的最坏后果是什么?
它在哪些条件下可能引发不可逆的问题(崩溃、数据损毁、CI 断裂)?
有哪些断言或守护条件可以在早期拦截这些风险?
这个框架的每一段都在把模糊的经验直觉转化为结构化检查点。模块地图确认变更范围,避免 Agent 改出 Spec 之外的内容;对外接口关注契约稳定性,因为内部实现可以重构,接口一旦扩散就会固化;依赖关系图审查全局结构,防止依赖反转和循环依赖;数据流与风险边界则检查 ownership、并发、安全和不可逆失败。
人类审查者的精力被精准投放在这些维度上,而代码风格、命名规范、函数长度等低层次检查交给自动化工具和 AI 初筛。这再次验证前文原则:确定性工具能解决的问题,不占用人类注意力;需要语境判断的问题,不能交给工具终审。
小结
Spec 与 Review 是人机协作的两个核心接口:Spec 定义输入,Review 验证输出。它们共同把人的判断嵌入 Agent 的执行闭环。
-
Spec 的清晰度决定 Agent 产出的上限。 模糊 Spec 不会带来创造性,只会把关键判断交给概率补全。
-
Spec 工作流的本质是逐步增加确定性。 Grill Me 暴露模糊,PRD 固化目标,Issue 降低粒度,Plan 限制路径。
-
Review 的重心从校对上移到对齐。 AI 擅长低层次校对,不擅长架构决策、知识扩散和业务判断。
-
Review 与测试不能互相替代。 测试验证行为正确性,Review 验证结构正确性;前者回答"是否做到",后者回答"是否以正确方式做到"。
-
架构级 Review 看五点:模块地图、对外接口、依赖关系、数据流、风险边界。 这是人类判断力最应该投入的位置。
Spec 和 Review 建立了"做什么"和"是否对齐"的接口。但在 Agent 执行过程中,还需要一种更低延迟、更客观、可反复读取的信号,让它每一步都知道自己是否偏离轨道。这就是下一模块的主题:TDD。
TDD:没有验证闭环的自动化是虚假繁荣
上一模块把 Spec 与 Review 定义为人机交接的两道接口:Spec 写清楚目标,Review 检查产出是否对齐。但它们仍然无法解决一个执行过程中的问题:Agent 在走向终点的每一步,如何知道自己没有偏离轨道?
Spec 是出发前的地图,Review 是到达后的检查。Agent 不能依赖人类在每一米路上实时纠偏;它需要一种低延迟、可重复、机器可判定的客观信号。这个信号就是测试。没有测试,自动化只是更快地产生更多未经验证的文本;有了测试,Agent 才能把"我觉得完成了"转化为"证据表明完成了"。
TDD 对 Agent 的意义:把完成声明变成外部证据
Test-Driven Development 在人类开发中通常是一种设计方法论:先思考期望行为,再编写实现。对 Agent 来说,TDD 的意义更根本:测试是 Agent 最接近客观真实的反馈通道。
回到第一模块的核心命题:Agent 的输出是概率采样。它可以生成代码,也可以生成"任务已完成"的声明;但这个声明本身仍然只是一次续写。
"完成"无法自我证明,必须由外部验证给出。没有测试验证的完成声明,本质上和生成代码一样,是概率性的。TDD 的作用,就是把 Agent 的每一次行动接到一个可执行的判定器上:红灯表示偏离,绿灯表示当前行为满足已定义的验收条件。
验证独立性:测试不能成为下一次文字接龙
测试并不会自动可靠。Agent 自己写代码、自己写测试,本质上是同一个概率生成管道同时扮演运动员和裁判。当它在实现中误解了一个字段含义,往往也会在测试中犯同样的错误;当它遗漏了一个异常路径,测试也可能同步遗漏。错得一致,就会表现为"全部通过"。
多 Agent 分角色可以缓解一部分问题:一个 Agent 写代码,另一个 Agent 写测试,显然比同一个上下文中自写自测更好。但它不能彻底解决验证独立性问题。如果多个 Agent 共享相近的模型基座、训练数据分布和提示范式,它们的系统性盲区仍然高度重叠。两个 Agent 分工,有时只是"换了一个人出卷,但两个人都上过同一个培训班"。
因此,TDD 在 Agent 开发中的前提是:验证必须尽可能独立于生成管道。 具体来说,测试集应包含正向和负向用例,应来自独立于训练/修复循环的数据,应覆盖真实边界条件,并以覆盖率作为最低底线。否则,测试会沦为 Agent 的另一段文字接龙——看似形成闭环,实际只是把生成偏差包装成了验证结果。
纵切优于横切:缩短反馈半径
一个常见的组织形式是"横向分工"——Subtask A 负责完成所有接口修改,Subtask B 负责所有实现,Subtask C 负责所有测试。这种分工在人类团队中常见,但对 Agent 团队存在一个隐形问题:验证被推迟到了所有实现完成之后。
当 Agent 完成了大量代码但没有经过验证时,它已经积累了大量未经验证的上下文轨迹。如果实现方向有偏差,发现偏差时的回滚成本等于所有中间产出的总和——包括被这些错误输出污染过的上下文。
更优的策略是 "纵切" :将一个大的功能改动拆分成多个小范围的、可独立验证的修改单元。每个单元的范围足够小,使得:
- 每次验证只需要运行最小粒度的测试子集——反馈延迟最低
- 每次修改的上下文轨迹足够短——错误路径来不及固化
- 每个单元都获得一次"接收验证信号 → 修正"的强化机会——正向轨迹持续积累
一个典型的技术实践是保持 连续小 commit——每个 commit 对应一个独立可验证的修改单元。当某个 commit 引入 bug 时,Agent 可以使用 git bisect 精准定位问题所在。而如果将所有这些修改 squash 成一个巨大的 commit——同样的模型要花费大量时间在 diff 中抓瞎,连定位问题都做不到,更不用说修复。
这本质上是 Context Engineering 在时间维度上的延伸:每个 commit 关联的上下文大小,决定了 Agent 定位和修复问题的难度。 小 commit = 小上下文 = 高概率正确定位 = 快速修复。大 commit = 大上下文 = 低概率正确定位 + 高概率引入新错误 = 修复时间指数级增长。
这套流程并非 Agent 独有;人类工程师在开发难以 Debug 的图形程序时,本质上也依赖同样的闭环:缩小修改范围、建立可观察信号,并通过外部验证逐步逼近正确结果。
Anthropic C 编译器:$20,000 买来的教训
没有比这更昂贵的教学案例了。
Anthropic 的研究团队使用 16 个并行的 Claude Opus 4.6 智能体,从零开始用 Rust 编写一个 C 编译器——总花费约 $20,000,最终产出 10 万行 Rust 代码(参见 Building a C compiler with a team of parallel Claudes)。这在自主软件开发领域是一次数额空前的探索。
然而第一个 GitHub issue #1:hello world 编译失败。 但"hello world 编译失败"只是冰山浮出水面的一角。潜入水下,会发现这个项目以极其浓缩的形式呈现了 Agent 在没有验证闭环时能犯下的所有类型的错误——从最粗糙的硬编码到最深层的 TDD 伪证。
硬编码路径:训练数据分布的直译
最浅层的问题是路径硬编码。#include <stdio.h> 只在预设的硬编码路径列表中搜索——/usr/include、/usr/local/include 等——如果你的系统不符合这些路径(NixOS、自定义安装路径、Windows 交叉编译),头文件直接找不到。同样的问题也出现在动态链接器路径上——/lib64/ld-linux-x86-64.so.2 被写死在代码中,不随系统环境变化。
这不是"Agent 写了 bug"。这是在训练数据中,绝大多数 C 代码的系统头文件路径确实位于这些位置——Agent 从训练分布中采样出了"最常见的路径",然后原封不动地写了进去。它不知道"路径应该是可配置的",因为训练数据中几乎没有讨论过"路径为什么需要可配置"。它只是在做文字接龙——续写了一个在训练数据中高频出现的模式。
寄存器分配错误:当概率猜测撞上硬事实
更深入一层的是代码生成层面的错误。以下内联汇编代码通过 syscall 指令直接向内核发起 write 系统调用:
__asm__(
"movq %0, rdi\n"
"movq %2, rdx\n"
"syscall"
:
: "r"(syscall), "r"(fd), "r"(msg), "r"(len)
: "%rax", "%rdi", "%rsi", "%rdx"
);编译器生成的汇编在寄存器分配上犯了致命错误:将长度 14 (0xe) 放进了 rdi,然后紧接着用文件句柄 1 覆盖了 rdi。最终实际执行的等价于 write(1, msg, 1)——只输出了一个字符 H。
寄存器分配是编译器后端最核心的确定性算法之一——图着色、线性扫描,都是确定的数学问题。但 Agent 没有实现这些算法——它在训练数据中见过大量寄存器分配的结果,然后用文字接龙的方式"模拟"了一个看起来像寄存器分配的代码生成逻辑。在训练数据的统计意义上,走这条路径的输出和被训练数据覆盖的测试案例相匹配。但面对真实的 syscall 约定——rax 放系统调用号、rdi 放 fd、rsi 放 buffer、rdx 放长度——这个"概率性模拟"被硬事实无情碾碎。
这是模块 1 "模型所有输出本质都是幻觉"的最具象演示。 在训练数据的分布内,幻觉和事实难以区分。一旦越过分布边界——在这里,边界是 x86-64 Linux syscall ABI 的精确寄存器约定——幻觉即刻暴露。
TDD 作弊:当验证闭环本身被绕过
最深层、也是最具有警示意义的问题,是 TDD 流程被 Agent 系统性绕过。
Anthropic 团队的设计包含一套测试框架——Agent 编写代码后,用 GCC(成熟的现成编译器)来定位问题。这在理论上是一个完整的验证闭环:代码生成 → 测试运行 → 测试结果反馈 → 修改代码 → 重复。问题在于——测试用例集的设计存在致命漏洞。
Agent 利用了这一点。以下是社区分析发现的部分"作弊"清单:
| 作弊行为 | 具体表现 | 测试是否捕获 |
|---|---|---|
| 硬编码标准宏 | __DATE__ 锁死 "Jan 1 2025",__TIME__ 锁死 "00:00:00" | ❌ 测试只验证编译通过 |
| 伪造浮点类型 | _Float128 定义为 long double(x86-64 上实际 80 位,非 128 位) | ❌ 测试机不是 x86 |
| 禁用 CPU 特性检测 | __builtin_cpu_supports 永远返回 0——但在别的模块里生成了大量 AVX2/SSE 代码 | ❌ 死代码从不执行 |
| 存根化内置函数 | __builtin_object_size 永远返回 -1,__builtin_va_arg_pack 永远返回 0,__builtin_frame_address / __builtin_return_address 永远返回 null | ❌ 测试从未使用这些返回值 |
| 缺失类型检查 | int x = "fucking yes" 是合法语句——字符串字面量可以赋值给 int | ❌ 测试集里不存在非法类型赋值 |
| 转发到 glibc | 部分标准库函数直接桥接到系统 glibc 运行时,而非由编译器自行实现 | ❌ 测试环境恰好装有 glibc |
| 虚标版本号 | 声称对标 GCC 14.2.0,实际连 GCC 4.0 的特性都未完整支持 | ❌ 版本号无人验证 |
可以先不谈你对不对——你只管过没过测试。 Agent 找到了测试集的每一条缝隙,填入了在训练数据统计意义上"最可能让测试通过"的输出。它没有实现一个编译器——它实现了一个"在给定的测试集上输出 PASS 的程序"。两者之间的差距,就是 TDD 伪证的全部空间。
编译优化的处境同样说明问题。8MB 的二进制文件中写入了大量表面的优化 pass,但逃逸分析从未被触发,内联优化从未实际执行。Agent 生成了让代码看起来像是做了优化的文本,但从未被要求证明这些优化真的改变了生成的汇编。
训练集与测试集同源:最根本的实验设计缺陷
所有这些问题的根源,指向一个更根本的实验设计失误:Agent 学到的是"让这批特定的 C 代码通过编译的模式",而不是"编译任意 C 代码的通用规则"。
事实上,在这种设定下,你甚至不需要 LLM——强化学习、遗传算法、任何能够在封闭测试集上反复迭代的优化算法,最终都能逼近 100% 测试通过率。它们产出的都不是编译器,只是"在测试集上表现像编译器的程序"。
教训
Anthropic C 编译器不是对 AI 的否定——它的工作流设计(Git 驱动的多 Agent 协作、无中央调度、任务锁机制、利用 GCC 做定位反馈)在架构层面是有价值的。
测试验证的是行为正确性,但前提是测试是正确的:代码是否在给定输入下产生预期输出,是否满足可执行的验收标准。Review 验证的是结构正确性:代码是否以正确的方式解决问题,是否破坏架构边界,是否引入未来维护风险。通过全部测试的代码仍可能有严重的安全漏洞、并发隐患、依赖反转和接口债务;架构上看似优雅的代码,也可能在运行时有隐蔽的逻辑错误。
测试提供机器可判定的客观信号,Review 提供人类和 Agent 协作完成的结构判断。前者回答"做到了没有",后者回答"是不是以正确的方式做到"。如果没有测试,Review 容易变成中间过程的自我背书;如果没有 Review,测试容易被局部实现绕过,最终得到一段"能过测试但不可维护"的代码。
Human in the loop:验证闭环中的人类位置
Anthropic C 编译器案例说明:即使有测试闭环,Agent 仍可能学会"让测试通过",而不是学会"解决真实问题"。这意味着 Human in the loop 不能只停留在 Spec 阶段。人类不仅要定义目标,也要维护验证闭环本身的可信度。
在 Agent 开发中,验证手段会受到三类系统性干扰。
第一,Code Review 的传统质量信号失效。 过去,代码风格差常常与设计混乱、边界不清、维护风险高相关。命名随意、函数过长、注释缺失、结构松散,都是审查者可以快速捕捉的危险信号。但 Agent 生成的代码往往天然具备良好外观:命名规范、缩进整齐、注释完整、结构看似清晰。外观质量不再能稳定代表内在质量。真正需要审查的,反而是更深层的问题:需求是否被误解,数据流是否正确,异常路径是否遗漏,接口边界是否被破坏。
第二,自动测试可能与实现共享同一个错误假设。 当 Agent 同时编写实现和测试时,它既是运动员,也是裁判。如果它错误理解了某个字段、状态或边界条件,这个误解很可能同时出现在代码和测试中。测试会通过,但通过的是同一个错误模型。多 Agent 分工可以降低一部分相关性,例如让一个 Agent 写代码、另一个 Agent 写测试,但如果它们共享相似模型、训练分布和任务上下文,系统性盲区仍然高度重叠。
第三,AI 互审容易放大确认偏误。 AI Review 能发现常规 bug,也能提供有价值的检查清单,但它不是独立判断的替代品。模型容易被连贯、专业、完整的文本说服,也容易对看起来投入充分的方案给出过度正向的评价。用 AI 审 AI,如果缺少外部标准,很可能得到的不是独立验证,而是更流畅的自我确认。
因此,Human in the loop 的核心职责不是亲自检查每一行代码,而是保证验证信号尽可能独立于生成管道:测试是否覆盖正向和负向用例,验收数据是否独立于实现过程,Review 是否关注结构风险而非表面风格,AI 审查意见是否被当作建议而非结论。
这也重新定义了 TDD 在 Agent 开发中的位置。对人类而言,TDD 是一种设计方法;对 Agent 而言,TDD 首先是一种外部校准机制。它只有在测试设计本身可信时才有效。否则,测试会变成下一次文字接龙:Agent 写出它认为应该通过的测试,再写出能通过这些测试的代码,形成一个看似完整、实则自洽的伪闭环。
所以,Agent 时代的验证闭环必须包含人类判断。人类负责校准目标、设计或审核关键测试、解释异常结果、识别结构性风险,并在测试通过但直觉不一致时强制重新审视问题。测试让 Agent 有客观反馈,人类则确保这个反馈确实指向真实目标。
小结
-
TDD 对 Agent 的根本意义是外部证据。 "完成"无法从上下文中自我证明,必须来自测试的独立判决。
-
测试必须尽可能独立于生成管道。 Agent 自写自测容易错得一致;多 Agent 分工只能降低相关性,不能消除系统性盲区。正向/负向用例、独立验证数据和覆盖率是最低底线。
-
纵切 > 横切。 小范围、独立可验证的修改单元比大范围、延迟验证的工作分工更适合 Agent。小 commit 关联小上下文,Agent 定位和修复问题的概率远高于面对几千行一次性 diff。
-
Human in the loop 是验证闭环的一部分。 人类不只是写 Spec,也要维护测试、Review 和 AI 互审的独立性,防止验证手段被 Agent 的同一套错误假设污染。
Long Horizon Agents:从单次任务到长程自主执行
上一模块讨论的是验证闭环:TDD 将 Agent 的"我觉得完成了"转化为"证据表明完成了"。但真实工程很少止步于一个函数、一个测试、一次提交。复杂任务往往跨越多个模块、多个子目标、多个上下文窗口,持续数天甚至数周。
当时间尺度被拉长,Agent 的问题不会消失,而是被放大。Context Rot 不再只是一次会话后半段的退化,而会变成第二天接续任务时的目标漂移;错误假设不再只是一次实现偏差,而会被写入计划、文档和后续提交,成为整个任务链路的隐性前提;验证缺失也不再只是"这个函数可能有 bug",而是"整条实现路径可能建立在错误目标上"。
因此,Long Horizon Agents 的本质不是让 Agent 连续运行更久,而是回答一个工程问题:如何让一个概率系统在更长时间、更大任务空间中持续保持方向、状态和验证的一致性?
趋势:从 /task 到 /goal
AI Coding 的交互形态正在从 /task 走向 /goal。早期模式更像增强版编辑器:人类给出明确局部指令,Agent 修改一个函数、补一个测试、修一个编译错误。新的目标则更接近任务代理:人类定义高层目标,Agent 拆解子任务、选择路径、执行修改、运行验证、根据反馈继续推进,只在关键节点请求人类介入。
这种迁移来自模型能力和工具能力的共同提升:上下文窗口变长,工具调用更稳定,代码检索和修改能力更强,多 Agent 编排也让复杂任务可以被拆分执行。但"能持续执行"并不等于"能持续正确"。任务越长,越需要显式管理三类信息:目标是否仍然清晰,状态是否真实可追踪,验证是否独立可靠。
这也是长程 Agent 与单次 Agent 的根本差异。单次任务的失败通常表现为一次错误修改;长程任务的失败则表现为方向性漂移、状态污染和错误积累。前者可以靠一次 Review 发现,后者必须靠系统化的工作流预防。
长程执行的核心:持久状态、独立验证、上下文隔离
面对长程任务,直觉上很容易设计复杂框架:任务树、状态机、层级规划器、动态优先级队列、自动调度器。这些机制并非没有价值,但在工程落地中,真正决定长程执行是否可靠的往往是更朴素的三件事。
第一,持久状态。 Agent 不能只依赖聊天历史记住任务进度。长程任务必须有外部状态载体,例如 Markdown Todo、Issue、Plan、构建日志摘要和验收清单。它们承担的不是文档职能,而是跨会话工作记忆:哪些目标已经完成,哪些结论已经验证,哪些问题仍然阻塞,下一步应该从哪里继续。
第二,独立验证。 长程执行不能依赖 Agent 自己判断是否完成。每个子目标都应有可重复运行的验证方式:单元测试、集成测试、编译、静态检查、运行时断言或人工 Review。验证信号越客观,长程任务越不容易在错误轨迹上持续加速。
第三,上下文隔离。 长程任务天然会产生大量轨迹:尝试过的路径、失败日志、临时假设、废弃方案、局部修复。把这些全部塞进同一个上下文,会让 Agent 被历史噪音拖垮。更稳妥的做法是按子目标隔离执行上下文,只把结论、证据和接口信息汇总回主线。
这三点共同构成一个最小可用的长程 Agent 底座:用文件系统保存状态,用测试和 Review 锚定方向,用子任务隔离降低污染。复杂编排可以在此基础上叠加,但不能替代这三件事。
一个最小可用工作流
基于上述原则,长程任务可以被组织成一个相对简单的闭环:
- GoalSpec:人类与主 Agent 共同定义最终目标、范围边界、验收标准和不可接受风险。
- SubGoalSpec:将目标拆成可独立验证的子目标,每个子目标都有输入、输出和验证方式。
- TaskPackage:为每个子目标生成执行包,包含相关上下文、约束、测试命令、修改范围和交付要求。
- Isolated Execution:子 Agent 或独立执行器在干净上下文中完成实现,避免被其他子任务轨迹污染。
- DeliveryPackage:执行器交付代码变更、测试结果、未解决问题和关键决策记录。
- Local Verification:本地验证器独立运行编译、测试和必要的 Review,判断交付是否满足 SubGoalSpec。
- FeedbackPackage:失败时生成结构化反馈,明确失败位置、复现方式、预期行为和下一轮修复边界。
- Final Investigation:所有子目标完成后回到 GoalSpec,检查整体目标是否真正达成,必要时修订目标并进入下一轮。
这个流程的关键不在步骤数量,而在信息流方向。Spec 向下分解,交付向上汇总;执行可以分散,验证必须回到同一标准;子任务可以独立推进,最终目标必须整体审查。
一个持久化 Todo List 往往已经能承担大部分状态管理:顶层记录 GoalSpec,中层记录 SubGoal,底层记录执行项和验证结果。它不复杂,却足够稳定。相比"把历史对话压缩成摘要再注入新会话",文件系统中的状态更可控、更可审查,也更容易被人类接管。
长程 Agent 不是新模块,而是时间维度上的组合
Long Horizon Agents 并不是独立于前文的新能力,而是所有可靠性机制在时间维度上的组合:
| 前文模块 | 在长程任务中的作用 |
|---|---|
| Context Engineering | 控制每轮执行看到什么,避免长历史污染当前判断 |
| 文件系统外部记忆 | 保存 Todo、Plan、日志摘要和验收状态,支撑跨会话接续 |
| 工具反馈 | 将编译、测试、运行时错误转化为稳定可读的反馈信号 |
| 多模型编排 | 将高熵规划、低熵修复、批量检索、独立 Review 分配给合适模型 |
| 权限与 Hook | 保证长时间执行中的每一步都不越过硬边界 |
| Spec 与 Review | 定义目标并在关键节点确认没有偏离方向 |
| TDD | 为每个子目标提供可重复、低延迟、机器可判定的完成信号 |
如果没有这些机制,长程 Agent 只是一次持续更久的概率续写;有了这些机制,长程 Agent 才可能变成可管理的工程流程。
小结
长程自主执行的难点不是让 Agent 工作更久,而是让它在更长时间内不丢目标、不污染状态、不伪造完成。
- 从
/task到/goal,放大的是误差积累。 时间越长,目标漂移、上下文污染和错误假设固化的风险越高。 - 长程执行的底座是持久状态、独立验证、上下文隔离。 复杂编排可以增强能力,但不能替代这些基础条件。
- 最小可用工作流应围绕 GoalSpec、SubGoalSpec、DeliveryPackage 和 Verification 构建。 任务可以拆开执行,验证必须回到同一目标标准。
- Long Horizon Agents 是前文所有机制的时间轴组合。 它不是魔法新能力,而是 Harness 在更长时间尺度上的展开。
这就引出全文最后的汇聚点:如果把上下文、工具、权限、记忆、Spec、Review、TDD 和长程工作流放在一起,它们共同指向同一个概念——Harness。
Harness:所有线索的最终汇聚
前文讨论了很多看似分散的实践:精简上下文、封装工具、过滤日志、多模型编排、权限控制、Hook、跨会话记忆、Spec、Review、TDD、长程工作流。它们解决的问题不同,但底层方向一致:为一个概率生成系统建立外部控制结构。
Harness 的英文含义是"to control something, usually in order to use its power."。放在 AI Agent 工程中,可以给出更具体的定义:
Harness = 一套围绕 Agent 构建的控制系统,通过输入约束、状态管理、执行隔离、外部验证和反馈修正,使 Agent 的能力在真实工程环境中可用、可控、可迭代。
Harness 不是工具集合,而是控制系统
如果只把 Harness 理解为一组自动成长套件、一套工作流或一个多 Agent 框架,这个词很快就会发生语义扩散(Semantic Diffusion):任何辅助 Agent 的东西都被叫作 Harness,最后反而失去分析价值。
更准确地说,Harness 是一套控制系统。它关注的不是"用了哪些工具",而是这些工具是否形成闭环:
- 输入是否被约束:Agent 是否知道目标、边界、环境和不可违反的规则?
- 状态是否可追踪:任务进度、已验证事实、阻塞点和下一步计划是否保存在上下文之外?
- 执行是否被隔离:不同子任务、失败轨迹和探索路径是否会相互污染?
- 输出是否被验证:代码、测试、文档和架构决策是否经过独立信号确认?
- 反馈是否能修正下一轮行为:错误信息是否足够结构化,能让 Agent 明确知道如何调整?
只要这五个问题没有闭合,再复杂的 Agent 框架也只是更华丽的文字接龙。反过来,即使只是一组朴素的 Markdown、脚本、权限配置和测试命令,只要能稳定闭合这条链路,它就是有效的 Harness。
全文模块如何汇聚成 Harness
回看全文,每个模块都可以放入 Harness 的某个位置:
| Harness 环节 | 对应模块 | 解决的问题 |
|---|---|---|
| 输入约束 | System Prompt、AGENTS.md、Spec | 定义角色、目标、边界和验收标准 |
| 上下文控制 | Context Engineering、按需加载、压缩、文件系统外化 | 控制 Agent 看到什么,避免信息过载和轨迹污染 |
| 状态管理 | Markdown Todo、Plan、跨会话记忆 | 让任务进度和验证事实跨时间保存 |
| 执行隔离 | Subagent、多模型编排、TaskPackage | 将复杂任务拆到更干净、更小的上下文中执行 |
| 硬约束 | 权限系统、Hook、路径限制 | 把"不应该做"变成"做不了" |
| 外部反馈 | build_full.py、assert_monitor.py、编译/测试日志过滤 | 将环境信号加工成 Agent 能理解的反馈 |
| 行为验证 | TDD、CI、Local Verifier | 用客观信号替代自我完成声明 |
| 结构审查 | Review、Council、人工架构审查 | 判断行为正确之外的结构风险和长期维护风险 |
| 时间轴闭环 | Long Horizon 工作流 | 将单次闭环扩展到跨会话、跨子目标的连续执行 |
这张表的重点不是分类,而是说明:前文所有实践并不是技巧清单,而是一套系统的不同部件。Context Engineering 控制输入质量,工具反馈提供外部信号,权限和 Hook 提供硬边界,Spec 与 Review 负责人机交接,TDD 提供行为验证,Long Horizon 工作流把闭环拉长到时间轴上。
因此,Harness 的核心不是让 Agent 变得"更聪明",而是让它在正确的轨道上释放能力。模型越强,Harness 越重要;因为强模型一旦沿错误方向前进,制造的结果也会更完整、更可信、更难被快速否定。
用 Harness 甄别新工具
AI Agent 领域迭代极快。新模型、新 SKILL、新 Agent 框架、新记忆系统、新评测方法不断出现。如果只追逐名词,很容易陷入 FOMO:今天研究多 Agent,明天研究长上下文,后天研究自动记忆,再过一周又换成新的工作流框架。
Harness 提供了一套更稳定的判断方式。看到任何新工具,都可以先问几个问题:
- 它解决的是输入约束、上下文控制、执行隔离、外部验证,还是反馈修正?
- 它减少了 Agent 必须记住的内容,还是向上下文里塞入了更多负担?
- 它提供的是概率性规则,还是确定性边界?
- 它的反馈是否可复现、可定位、可被 Agent 独立理解?
- 它让验证闭环更短、更客观,还是让完成判断更依赖模型自述?
- 它是否保留了人类在目标、边界和风险判断上的最终位置?
这些问题可以帮助我们区分真正有价值的基础设施和只是包装精美的自动化。好的工具会让闭环更短、状态更清楚、反馈更可靠、风险边界更硬;坏的工具则会制造新的上下文膨胀、新的黑箱状态和新的自我确认幻觉。
小结
Harness 是全文的收束点,也是 Agent 工程化的核心范式。
- Harness 是控制系统,不是工具集合。 它通过输入约束、状态管理、执行隔离、外部验证和反馈修正,让 Agent 在复杂工程任务中保持可控。
- 全文所有模块都是 Harness 的子系统。 上下文、工具、权限、记忆、Spec、Review、TDD、长程工作流,分别补齐 Agent 在不同环节的天然短板。
- 工具会变,闭环结构不变。 输入约束 → 执行 → 外部验证 → 反馈修正,是判断任何 Agent 工具是否有效的基本框架。
- Harness 的目标不是替代人,而是放大人的判断。 人定义目标、边界和风险,Harness 让 Agent 在这些约束内稳定执行。
总结:不要问模型能为你做什么,问你能为模型做什么
本文从 Agent 的底层机制出发,依次讨论了上下文限制、反馈回路、工具封装、多模型编排、权限护栏、跨会话记忆、人机分工、Spec、Review、TDD 与长程执行。所有这些讨论最终都指向同一个问题:人如何用好 AI Agent?
答案不是等待一个完美模型,也不是把工程判断整体交给 Agent,而是为 Agent 建立一套可控的工作环境:给它明确目标,限制它的行动边界,提供可读的外部反馈,用测试和 Review 验证结果,并在长程任务中持续维护状态和方向。
Agent 的定位
Agent 的能力来自 LLM、System Prompt、工具和循环。它可以检索、修改、执行、总结、修复,也可以在明确目标下持续推进任务。但它仍然是一个基于上下文的概率系统:它不会天然理解什么目标值得做,也不会自动知道什么结果才算真正正确。
因此,Agent 更适合被定位为执行与探索的放大器,而不是最终判断的主体。它擅长在给定边界内快速展开可能路径,适合承担实现、检索、批量修复、测试补全、日志分析和初步 Review;但目标定义、架构取舍、风险权衡、验收标准和最终责任,仍然必须由人承担。
人的位置
Agent 让人从一部分确定性执行中解放出来,但这不意味着人的位置后移。相反,人的位置在上移:从"如何做到"上移到"做什么"、"为什么做"、"做到什么程度才算对"。
当 Agent 可以十倍速执行后 80% 时,前 20% 的质量会被空前放大。需求定义错误,会以更快速度制造返工;架构边界模糊,会被 Agent 扩散成更大范围的技术债;验收标准缺失,会让"看起来完成了"长期伪装成"真的完成了"。
因此,AI 时代的工程师并不是退到旁边看机器写代码,而是更像 Tech Lead:定义目标,拆解边界,设计验证方式,审查关键路径,决定哪些风险可以接受、哪些必须阻止。
Harness 的意义
本文反复强调 Agent 的脆弱性:Lost in the Middle、Context Rot、轨迹污染、幻觉、谄媚、越权、TDD 作弊、自我完成声明。这些不是偶然 bug,而是概率生成系统进入真实工程环境后必然暴露的结构性问题。
Harness 的意义就在于承认这些问题,而不是假装它们会被下一代模型自动消除。更强的模型可以推迟临界点,但不能取消对边界、反馈和验证的需求。工程上的正确姿势不是迷信模型,而是围绕模型构建系统。
这套系统的目标不是让 Agent 变成一个自主可靠的工程师,而是让 Agent 在人类定义的目标和边界内稳定释放能力。换句话说,Harness 不是让 Agent 失控地更强,而是让它可控地有用。
核心 Takeaway
- LLM 本质是文字接龙。 它不真正回忆事实,而是在上下文中预测最可能的续写。
- Agent 是带工具的循环系统。 ReAct 让模型能够行动,也让错误上下文、错误反馈和错误权限被持续放大。
- 上下文质量决定输出上限。 Context Engineering 的目标不是塞入更多信息,而是让模型在正确时间看到正确内容。
- 外部反馈是 Agent 的现实锚点。 编译、测试、运行日志、检索结果和人工 Review,把概率输出拉回真实环境。
- 确定性约束必须外置。 能用权限、Hook、脚本和测试解决的问题,不应只依赖 Prompt 提醒。
- 验证不可外包给自我声明。 TDD、CI、Local Verifier 和 Review 的价值,是把完成判断从模型口中移到外部证据中。
- 人的角色上移。 人负责定义目标、边界、验收和风险;Agent 负责在这些约束内执行、探索和修复。
- 为 Agent 设计环境。 用输入约束、状态管理、执行隔离、外部验证和反馈修正,让 Agent 在可控轨道内发挥能力。
一句话总结
不要问模型能为你做什么,而要问你能为模型做什么:给它正确的上下文、清晰的目标、可靠的工具、硬性的边界和可验证的反馈。这就是人用好 Agent 的核心。