做过长任务 Agent 的人,大概率都遇到过同一个场景:对话开头的那几步还像模像样,订单信息、用户需求、流程约束都记得清清楚楚,但任务执行到中后段,它突然开始“自由发挥”——要么把已经处理完的步骤又做一遍,要么对着一个中间分支无限深挖,要么把最初的用户诉求忘得一干二净,最后交回来一份看起来完整、实际上目标早偏了的结果。
我排查过很多这类“跑偏”案例,结论几乎一致:模型本身没太大问题,问题主要出在上下文工程上。上下文工程并不是把“很多信息塞给模型”这么简单,它要解决的是长任务中“哪些信息进窗口、进多少、以什么顺序进、过期信息怎么退出、关键信息怎么不被冲淡”这一整套闭环。这篇就结合我自己在多个项目里的调优经历,拆解 4 个被反复验证有效的方法,以及它们背后的原理和可以直接照抄的实操细节。正在做多步骤 Agent、流程自动化、长文档处理,或者被 Agent 对话漂移搞到头秃的人,应该都能从中找到思路。
1. 先搞清楚“跑偏”到底是怎么发生的:上下文窗口里的信息衰减
上下文工程的第一个原则,是别急着堆信息。你只有先明白为什么 Agent 会在几十步之后突然“轴”起来,才知道该往哪个方向修。
1.1 上下文窗口不是内存,更像一块不断被擦写的白板
很多刚上手的人会把上下文窗口理解成一个“无所不包的大背包”,觉得只要把历史对话、中间结果、角色设定全塞进去,模型就能随时调用。但实际上,上下文窗口是一块有一定容量的白板,而长任务里每一步产生的中间信息都会往上写。写的东西一多,最早的、最边缘的内容就会被挤到非常靠后的“注意力盲区”里。
模型在做下一步预测时,理论上能看到窗口里的全部内容,但注意力分布天然不均匀。窗口中间靠后的内容权重通常最高,而窗口非常前端或非常末尾的信息,在长序列里经常是“看到了但没真正参与决策”。我自己做过一个粗略测试:把一个 10 万 token 的长对话压缩到窗口中部,和同样内容放在窗口前部,后续任务完成度差了接近三成。这不是模型蠢,而是长序列注意力天然有这种倾斜。
所以“跑偏”的第一层含义,是信息还在窗口里,但它在注意力层面已经“隐形”了。你以为是模型失忆,其实是上下文排布让重要信息落在了注意力盲区。
1.2 三种最常见的跑偏模式:记错、盯偏、绕圈
根据我看到的现场表现,长任务跑偏基本可以归成三类,排查的时候非常有用:
- 记错目标:任务初始要求被后续大量中间内容覆盖,Agent 后续生成的每一步都“沾一点”原始目标,但整体已经偏离。典型表现是最后交付物的结构和最初需求对不上,比如要的是分析报告,最后做成了数据清洗日志。
- 盯偏细节:Agent 被某个中间发现吸引,无限深挖某个分支任务,把主流程晾在一边。典型表现是某一步骤的执行轮数远超预期,子步骤数量膨胀,产出却和主目标无关。
- 绕圈重复:Agent 回到已完成的步骤,重新生成一遍类似内容,或者反复修正同一个中间结果,始终进不到下一步。典型表现是日志里出现连续的重复动作,函数调用记录高度雷同,执行时间明显拉长。
这三类问题,通常不是单一原因,而是多个上下文管理点同时失守。但只要把上下文工程的几根柱子立起来,它们会同时被压下去一大半。
1.3 哪些任务最容易踩雷
也不是所有任务都容易跑偏。我总结下来,低风险任务通常具备“步骤短、状态少、结果可验”三个特征,比如单轮问答、简单文书改写、一次性代码生成。高风险任务则普遍长这样:
- 任务步骤超过 15 到 20 步,且步骤之间没有强依赖关系,Agent 可走的路太多;
- 任务过程中需要不断吸收外部数据、文件内容或用户新输入,上下文处于持续增长状态;
- 最终结果没有一个绝对客观的对错标准,比如“写一份有洞察的分析”“整理一份尽量全面的材料”,这类任务跑偏了也不容易被立刻发现;
- 多个子目标并行推进,彼此之间还要共享某些中间结论。
如果你的任务命中了两条以上,先别急着换更大的上下文窗口。窗口再大,注意力倾斜的问题依然存在,只是延后爆发。真正该做的,是从上下文工程层面把信息流管起来。
2. 方法一:用显式任务状态替代模糊记忆
第一个方法,也是我认为最核心的一个:不要让 Agent 去“回忆”自己做到哪了,而是把当前进度和后续计划作为一种强制结构,直接写进上下文。
2.1 为什么“状态”比记忆可靠
模型没有真正意义上的“状态”。你在对话里问它“你刚才进行到哪一步了”,它能答出来,是因为它在重复解析历史上下文,而不是在读取一个内部寄存器。这意味着一旦历史上下文被压缩、被截断、被大量新信息冲淡,它对自己“进行到哪一步”的判断就会失真。
解决办法很简单:把隐式记忆变成显式状态。所谓显式状态,就是一段结构化的文本或数据,例如“当前步骤:5 / 总步骤:9;已完成:A、B、C;待完成:D、E、F;当前依赖:D 需要 C 的结果”。这段状态在每一轮执行结束后被更新,并在下一轮作为“粘在窗口最前端”的强制输入。
我习惯把它类比成协作办公场景里贴在最上方的项目看板。人之所以不会做一半忘了目标,不是记忆力好,而是任务拆解、进度标注和下一步提醒这些外部脚手架在持续起作用。Agent 也需要同样的人工脚手架。
2.2 落地方案:状态块 + 检查点 + 进度断言
具体落地我分三块来做:
- 定义状态块 在系统指令里预留一个固定字段,比如
CURRENT_STATE,格式固定为:
[任务目标] 一句话还原最终要交付的结果。 [已完成步骤] 按顺序列出已完成的关键动作,每条不超过一行。 [当前步骤] 现在正在执行的步骤,以及该步骤的输入依赖。 [下一步骤] 紧接着要执行的步骤,以及预计输出。 [风险提示] 当前上下文里值得注意的矛盾信息或缺失信息。这个状态块在每一轮 Agent 执行之后由主调度代码重写,而不是依赖模型自己改。模型只负责读它、在它的约束下推进。
设置检查点 在任务的“大里程碑”处,比如完成数据收集、完成初稿、完成第一轮校验,把当前中间产物写成一个独立的持久化记录。检查点的作用不只是防崩溃,更是给后续步骤提供“重新锚定”的坐标。一旦发现 Agent 开始绕圈,调度层可以直接把状态块回滚到最近的检查点,重新注入,而不是从头再来。
加进度断言 在每一轮执行前,让模型根据状态块生成一个简短断言:“我确认当前正在执行第 X 步,理由是……”这个动作看起来很笨,但在界面上阶段,效果异常显著。它强迫模型在做下一步之前先对自身位置做一次显式确认,能明显降低绕圈和跳步概率。
2.3 这里最容易翻车的三个坑
状态块不是加了就完事,实际用起来有几个细节需要特别注意。
第一个坑是状态块被“淹没”。如果状态块放在系统指令末尾,而后面的每一步历史对话都很长,状态块照样会落进注意力盲区。所以我建议把状态块放在上下文的最前端,紧跟在系统指令的核心约束后面,并且每一轮都重新重复注入一次,而不是让它随着对话自然滚动到后面。
第二个坑是状态描述写得太啰嗦。状态块只应该包含“当前这一个时间点”的决策信息,不要把过去十步的详细日志都贴进去。写得太细,和上下文过长是同一个问题,注意力照样被摊薄。已完成的步骤列个标题就行,细节放在检查点记录里。
第三个坑是让模型自己维护状态。我见过很多方案,让 Agent 输出“new_state”字段,再把这段字段拼回去当下一轮状态。短期对话没问题,长任务里模型一旦误判,错误状态就会被不断放大,最终完全丢失真实进度。正确做法是主流程代码里有一份状态数据,模型的输出只是建议,是否更新、怎么更新由代码判断。
3. 方法二:对长历史做分层压缩,保留决策链而不是流水账
第二个方法是处理“上下文过长”的常规手段——压缩。但我观察到,很多人压缩的姿势不对,把压缩做成了单纯删减,结果重要决策依据被连坐删除,Agent 照样跑偏。
3.1 上下文压缩要分清“流水账”和“决策链”
我把上下文里的信息分成两类:流水账和决策链。
流水账指的是过程性记录,比如“第 8 步读取了文件 A,第 9 步调用了接口 B,第 10 步返回了标题列表……”这类信息对理解当前任务已经不重要了,留着只会占窗口。
决策链则是“为什么做出当前选择”的关键依据,比如“因为用户说要优先控制成本,所以模型在第 6 步选择了低成本的方案 B”“用户在第 12 步否掉了旧逻辑,原因是兼容性要求”。这些信息一旦丢失,后续步骤就会失去判断方向,等同于失忆。
所以压缩的原则只有一条:把流水账扔掉,把决策链留下来。我见过不少失败案例,压缩时按时间窗口简单截断,把 20 轮前的对话直接砍掉。用户最初提的关键约束往往就埋在那堆早期对话里,砍完之后 Agent 立刻变了一个人设,拼命追求它自己以为的目标。
3.2 分层压缩的具体操作
我会在系统里维护三个层级的上下文:
- 原始层:完整保留全部原始对话和执行记录,只用于回溯,不参与模型推理;
- 摘要层:把决策链提炼成一段 300 到 800 字的动态摘要,随着任务推进不断更新;
- 状态层:也就是上一节说的状态块,只放当前这一步需要的直接信息。
模型每轮真正读取的是“系统指令 + 状态块 + 摘要层 + 最近 2 到 3 轮原始交互”。原始层是给调试用的,不让它进推理上下文。这样即使任务执行了 50 步,模型看到的上下文长度也保持在一个稳定范围,注意力不会因为窗口膨胀而失控。
摘要层的更新节奏,我建议采用“关键转折必更新,日常步骤累积更新”。用户提出新要求、切换了开关、确认了一个重要结论、推翻了一个旧结论,这些事件一发生就立刻写进摘要。常规步骤,比如“读取文件”“处理了其中一段”,先攒着,等摘要本身快超出目标长度时再压缩一轮。
3.3 摘要到底怎么写,效果差异巨大
同样一段历史,让模型“总结一下”,和让模型“提取后续决策必须保留的约束与依据”,得到的效果完全不同。前者会输出一堆正确的废话,比如“用户提出了一些需求”“模型完成了一些操作”;后者才会产出真正可用的决策链。
我平时用一段固定模板来做摘要:
请基于以下历史记录,提取对后续任务仍然重要的信息,严格按照以下格式输出,不要补充评论: 1. [持久目标]:用户最终要达成的结果,以及不可违背的硬性约束; 2. [关键决策]:已作出的、影响后续步骤的选择及原因; 3. [未决问题]:尚未解决、需要后续继续跟进的争议或缺口; 4. [关键事实]:后续任何一步都可能用到的实体信息,例如账号、日期、文件路径、数值口径。注意一个细节:摘要里不要出现“用户说”“模型说”这类过程描述,只保留命题本身。越接近“决策数据”的摘要,后续轮次引用起来越不容易因转述损耗而失真。
压缩之后还应该做一次“关键约束回填校验”。压缩前先圈出几个绝对不能丢的关键点,比如原始需求、硬性约束、否决项,压缩后人工或脚本检查这些点是否还在摘要里。这一步看似繁琐,但能在早期拦截大部分“压着压着就跑偏”的问题。
4. 方法三:选择性注入,控制每一轮的上下文“配比”
第三个方法,解决的是上下文里“不加区分地全塞”的问题。很多时候 Agent 跑偏不是信息不够,而是无关信息太多,把决策信号压低了。
4.1 上下文多不等于好:注意力的稀释效应
模型在做每一步判断时,其实是在窗口所有信息上做加权。如果窗口里塞了 30% 相关信息、70% 背景噪音,那么即使相关信息的绝对量很大,它的决策权重也会被噪音分摊。最典型的场景是长文档处理:把整份一百页的文档全部塞进上下文,再让 Agent“找出与当前问题相关的内容并处理”,它非常容易抓住文档前几章的高频词,而忽略真正需要关注的后半部分。
这就像开会时,桌上堆了一百份资料,其中只有三份和本次议题相关。多数人反而会翻到最上面的那份就开始照着做,而不是花半小时把一百份全看完再决定重点。Agent 也这样,而且它更吃这个亏。
4.2 按需检索 + 局部上下文的注入流程
我的做法分四步:
- 把外部长内容先做切块和向量化,也就是先建一个“外部资料库”,不让原始文档进主上下文;
- 每执行到一个步骤,先根据该步骤的输入需求生成检索词,去资料库里取回最相关的 3 到 5 个片段;
- 把取回的片段和当前步骤的指令、状态块拼成一个临时上下文块,供这一步单独使用;
- 这一步结束后,临时上下文块就丢掉了,不需要跟着主上下文长期保留。
这套流程在长文档总结、多文件分析、知识库问答类任务里效果非常明显。它的本质是把“让模型从大海捞针”改成“先把针捞出来,再让模型看针”。
检索词本身的生成也很重要。我见过直接用“上一步输出全文”当检索词的做法,结果检索回来一堆同义反复的片段。更有效的做法是让模型先生成“当前这一步需要什么样的信息”,比如实体词、时间范围、信息类型,再用这套结构化条件去做检索。条件越具体,取回的片段越贴题,后续 Agent 也就越不容易被无关内容带偏。
4.3 注入顺序和边界条件
取回片段之后,放在上下文什么位置也有讲究。我的习惯是放在状态块之后、最近历史之前。因为窗口最前端的注意力权重通常较高,状态块锚定“我在哪”,紧接着放“这一步需要的外部信息”,再往后放最近对话,执行链路就比较顺。
还要特别注意一个边界条件:当检索结果与状态块里的“未决问题”冲突时,以哪个为准。我在方案里规定,检索结果只能作为参考信息,不覆盖状态块里的已确认决策。如果二者冲突,Agent 必须先停下来向调度层或用户确认,而不是擅自按新检索结果推翻旧结论。这个规则能避免一种很隐蔽的跑偏——“被一个错误的检索片段带歪”。
另外一个常见误区是检索只做一次。长任务推进过程中,Agent 需要的知识会持续变化。我建议每个大步骤都重新跑一次检索,而不是一开始把一堆资料塞进窗口让它“备用”。备用信息越少,注意力越集中,跑偏概率越低。
5. 方法四:设计系统级元上下文,让 Agent 在高处“校准”
前三个方法处理的是“具体信息怎么放”,第四个方法处理的是一个更抽象层面:Agent 对自己身份、边界和评判标准的定义。我把它叫做系统级元上下文。
5.1 元上下文:让 Agent 有一个“不会过期的自我定位”
长任务跑偏到后期,常见的一种表现是 Agent 开始把自己“当成了另一个人”。比如本来是做数据分析的 Agent,到第 30 轮的时候突然开始输出营销文案;本来是帮用户整理资料的 Agent,突然开始给建议、做评价。这种“身份漂移”,往往就是你最开始定义角色时的那段描述,被几百轮后续内容彻底盖住了。
解决办法是把角色、边界、评价标准单独抽出来,作为一份“元上下文”,在每一轮都重新注入。这份元上下文不需要很长,但必须包含三个要素:
- 我是谁:当前这个 Agent 承担什么角色,服务对象是谁;
- 我绝不做什么:哪些边界行为一旦出现就必须停止,比如“不要修改用户原始数据”“不要执行超出需求范围的步骤”;
- 我如何判断完成:什么状态代表当前步骤真正做完、可以进入下一步,这个判断标准最好能对应到具体的可检查条件。
我习惯把这份元上下文放在所有输入的最前端,并且比状态块还要靠前。无论后面发生什么,模型每一轮开口之前,都会先“看见”自己是谁、边界在哪。这个动作对身份漂移的抑制效果比任何现场纠偏都稳定。
5.2 规划与自我检查:把“校准”变成流程动作
元上下文不能只是静态文本,还要配合动态的规划与自检动作。
在主任务启动时,我会让 Agent 先输出一份完整规划,包括预计步骤、每步输入、每步输出、每步完成标志。这份规划本身也是一段重要上下文,并且要放在状态块和摘要之间。后续步骤如果溢出了规划范围,调度层就能及时发现并干预。
在每个里程碑检查点,我会额外让模型回答三个问题:
- 当前产出距离最终目标还有多远?
- 最近三步中,有没有哪一步偏离了规划?偏差原因是什么?
- 最初的需求约束是否仍全部有效?有没有被新信息覆盖?
这三个问题不需要每次都注入完整历史,只要给模型看状态块、摘要和最近几轮记录,它就能做出判断。实际跑下来,这三个问题能提前暴露大部分跑偏风险,尤其是那种“已经偏了但所有人没发现”的风险。
更激进一些,还可以设置双通道校验:一个通道是执行 Agent,负责生成;另一个通道是校验 Agent,负责看执行 Agent 的产出和目标之间是否对齐。校验 Agent 不修改内容,只输出“继续 / 修正 / 终止”三类信号。两个通道共享同一份元上下文和状态块,但使用不同的指令措辞,能降低“自己看自己”的盲区。
5.3 四个方法如何拼成一套完整方案
单独强调某一个方法,都有点“单腿走路”。我实际跑下来的组合方式是:
- 用显式状态块锚定当前进度;
- 用摘要层和检索把有效信息压到最少但最关键;
- 用元上下文固定角色和边界;
- 用规划和自检查找偏差头。
这一套下来,Agent 的上下文结构基本固定为:
系统级元上下文(角色 / 边界 / 完成标准) → 初始任务驱动(终极目标 / 核心需求) → 状态块(当前步骤 / 已完成 / 下一步) → 选择性注入(当前步骤相关的外部资料) → 动态摘要(决策链) → 最近 2 到 3 轮原始历史 → 当前用户输入每次执行一个新步骤时,调度层重新拼装这份上下文,而不是让模型在一条持续变长的历史里自己翻滚。这样既控制了窗口长度,也保证了每个环节的信息数量和质量都处于可控状态。
6. 真实调优记录:一个长流程任务从连续跑偏到稳定收敛
理论讲完,分享一次比较完整的调优过程。这个案例我改了任务背景和细节,但结构和踩点都是真实的,参考价值不打折。
6.1 任务场景与初始痛点
这是一个多阶段的业务材料整理任务。输入是一批格式混杂的素材文件,最终要交付一份结构完整、结论可靠的整理结果。任务步骤包括素材读取、分类、去重、主干抽取、辅助信息补全、结构重组、结论生成,以及最终一致性检查,拆完有接近 30 个执行节点。
最初版本里,我只做了一件事:把初始需求写进系统提示,然后把素材全部塞进上下文,让 Agent 一步一步往下做。结果是每跑到第 12 步左右就开始出岔子。总结后的主要异常有三类:
- 明明已经完成了素材分类,后面整理的时候又把已分好类的素材重新按新标准分了一遍;
- 对某个辅助信息分支过度查阅,把主干分析晾了二十多分钟;
- 交付文本开头还在按最初需求口径组织,到了后半部分突然按另一个口径写,而且没有任何说明。
日志里最典型的一幕是,Agent 在第 15 轮重新“发现”了第 4 轮就已经确认过的分类口径,并且把它当成新信息写进了后续逻辑。这本质上是早期决策链没有进入长期可见区,等于白做了。
6.2 调优过程:一套方案渐次加进去
我没有一次性把所有方法都堆上去,而是按“状态 → 压缩 → 检索 → 元上下文”的顺序逐个加,便于观察每个措施独立带来的改善。
第一轮只加了状态块和检查点。改动之后,Agent 已经很少出现“重新分类”这类进度回跳,因为状态块里明确写着“分类已完成,当前处于主干抽取阶段”。但新的问题浮出来:摘要层还没建,早期用户口径信息仍然被大量过程性内容稀释,Agent 在后期仍会偶尔偏离初始口径。
第二轮加了动态摘要,专门把“消费口径、排除项、结构偏好”这些关键约束提取到摘要层。这个改动稳住了一大半偏口径的问题。代价是摘要本身需要持续维护,如果某一次摘要生成质量不高,后续几轮都会受影响。
第三轮加了选择性检索。素材不再整体塞进上下文,而是按需取片段。上下文长度肉眼可见地降下来了,单轮延迟明显改善,也因为噪音减少,Agent 围绕主干推进的连贯性变好了。
第四轮加了元上下文和规划自检。这次改变的是“支柱”,跑偏率被压到了很低的水平。最直观的表现是,哪怕中间有人为插入一次“无关联的闲聊”,Agent 也能回到主线继续执行,不会因为这段插入而被带走。
6.3 数据与经验
调优前后的对比数据大致是这样的:初始版本在测试集上,跑偏率约为四成,也就是每 5 次长任务运行就有约 2 次需要人工干预;加入完整方案后,跑偏率降到不足一成。平均执行轮数也从原来的 28 轮降到 19 轮,因为绕圈的消耗被大幅压缩。
但这套方案也不是零成本。摘要有维护成本,状态块和检索需要写调度代码,双通道校验更是接近双倍模型调用。所以在实际项目里,我会建议“看症状下药”:如果只是偶尔绕圈,先加状态块;如果早期口径会被冲淡,加摘要;如果窗口过长,加检索;如果身份漂移明显,再加元上下文。全上固然效果好,但至少有一半项目用不到那么重的手段。
7. 常见问题与排查技巧实录
最后整理一份我在交流群里、项目评审里被问得最多的问题,以及对应的排查思路。这些问题几乎每个做 Agent 的人都会遇到。
7.1 问题现象与根因速查表
| 表面现象 | 常见根因 | 优先排查方向 |
|---|---|---|
| 任务后段忘记初始需求 | 早期决策链被后续内容稀释 | 摘要层有没有保住“持久目标”和“硬性约束” |
| 反复重做已完成步骤 | 缺少显式进度状态 | 状态块是否存在、是否每轮刷新、是否放在窗口前端 |
| 被无关资料带偏 | 上下文里噪音太多 | 是否做了按需检索,是否把全部资料塞进窗口 |
| 角色身份漂移,行为越来越不像原本的 Agent | 元上下文位置靠后或被覆盖 | 系统指令里的角色边界是否每轮重新注入 |
| 单个步骤执行轮数异常膨胀 | 子目标缺少完成标准 | 检查点里有没有明确的“本步完成标志” |
| 结果格式符合要求但内在逻辑错位 | 中间结论没有回填校验 | 关键步骤后是否做“产出 vs 目标”一致性检查 |
7.2 排查顺序和三个易被忽略的细节
遇到“跑偏”,我建议按“状态 → 摘要 → 检索 → 元上下文”的顺序排查,而不是先怀疑模型能力。因为前四项在工程上都可控,调整成本也低。
有三个细节特别容易被忽略。
第一个是状态块该放的位置。很多人加了状态块,却放在历史对话末尾,那等于没加。上下文窗口的注意力重心通常在开头附近,状态块一定要放在开头那一小段区域,并且每轮固定出现,不要移动位置。
第二个是摘要更新时机。摘要不能只在“上下文太长”时才更新,而要在关键事件发生时立刻更新。很多漏掉的口径约束,往往是延迟更新期间被新对话覆写掉的。
第三个是检索词的生成。直接用原问题去检索,经常捞回一堆表面上相关、实际不解决当前子问题的片段。先让模型生成“当前步骤需要哪些信息”,再基于这个生成结果去检索,效果会好一个档次。
7.3 排查过程中的备注技巧
我在排查长任务问题时,一定会给每轮日志导出三个字段:当前输入、当前状态块、当前摘要。这三份数据拼在一起,基本就能还原出模型在这一轮“看到的决策基础”。如果能看到模型跑偏前状态块没变、摘要也没变,那就不是上下文问题,可能要重新审视模型能力或工具调用逻辑。
另一个好用的调试技巧,是在任务中途人为插入一条与主题无关的输入,比如突然问一句“顺便帮我查一下另一个东西”。一个上下文结构够稳的 Agent 会明确区分“这是新增任务”还是“当前主任务的依赖信息”,并给出下一步处理建议;而一个上下文结构不稳的 Agent 会直接把这条输入当成主线的一部分,开始痛苦纠结。这条技巧我用来当压力测试,比单纯看正常流程更快暴露上下文管理的薄弱点。
做长任务 Agent,上下文工程不需要一次上多重的方案,但“信息怎么放”这件事,值得反复打磨。每个要素最终都会反映在任务的稳定性上,早一点把白板管清楚,后面就能少当很多次救火队员。