☰
AI Agent长任务跑偏根因与上下文工程四法
2026/10/19 2:03:27 网站建设 项目流程

1. 长任务跑偏的根因不在模型,在上下文管理

做 AI Agent 开发的人大概率都经历过这种场景:一个需要十几步甚至几十步才能完成的任务,Agent 前几步表现正常,到中间某一步突然开始胡言乱语,或者反复执行同一个动作,又或者把前面已经确认过的信息忘得一干二净。你去看日志,发现模型本身没报错,工具调用也正常返回,但整个任务就是走歪了。

这个问题困扰了我很长时间。最初我以为是模型能力不够,换了更强的模型,效果确实有提升,但跑偏依然会发生,只是推迟了几步而已。后来我意识到,问题不在模型本身,而在于上下文工程——Agent 在每一步决策时看到的"信息环境"是否合理。

打个比方:模型是一个能力很强的员工,上下文就是他桌面上的文件。如果桌面上堆了几百页无关材料,关键信息被埋在中间,他再聪明也会看漏。长任务跑偏的本质,就是随着步数增加,上下文里积累了太多噪声、冗余和过期信息,导致模型的注意力被稀释,决策质量断崖式下降。

这篇文章面向正在做 AI Agent 长任务编排的开发者,尤其是那些已经跑通了 Demo、但在真实复杂任务中频繁遇到"跑着跑着就偏了"的人。我会拆解四个经过实测验证的上下文工程方法,每个方法都会讲清楚背后的原理、具体的操作方式,以及我在实践中踩过的坑。这些方法不依赖特定框架,无论你用的是哪种 Agent 架构,思路都可以直接迁移。

2. 方法一:分层上下文窗口,把"桌面"分成几个抽屉

2.1 为什么把所有信息塞进一个窗口必然出问题

大多数人做 Agent 的第一反应是:把所有历史对话、工具返回结果、系统提示全部拼成一个超长 prompt 丢给模型。短任务没问题,但长任务下这个做法会迅速崩溃。

原因有两层。第一层是注意力稀释。Transformer 的注意力机制虽然理论上能处理长序列,但实际效果是:序列越长,每个 token 分到的注意力权重越平均,关键信息的信号强度被大量无关 token 摊薄。有研究表明,当上下文超过某个长度后,模型对中间位置信息的召回率会显著下降,这就是所谓的"中间遗忘"现象。

第二层是信息污染。长任务中,早期步骤产生的中间结果可能已经过期。比如第 3 步查到的某个数据,到第 15 步时已经被后续操作修改了,但旧值还留在上下文里。模型看到两个版本的信息,很容易用错。

我做过一个对比实验:同一个需要 20 步完成的数据处理任务,方案 A 把所有历史塞进一个窗口,方案 B 用分层窗口管理。方案 A 在第 11 步开始出现重复调用,最终失败;方案 B 完整跑完,且 token 消耗只有方案 A 的 40%。差距非常明显。

2.2 三层窗口的具体划分与实现

我的做法是把上下文分成三个逻辑层,每层有不同的生命周期和更新策略。

第一层:系统层(System Layer)。这一层放的是整个任务生命周期内不变或极少变的信息:Agent 的角色定义、任务总目标、全局约束条件、可用工具的清单。这一层的内容一旦设定,在整个任务执行过程中不修改。它的作用是给模型一个稳定的"锚点",让模型始终知道自己在干什么。

第二层:工作层(Working Layer)。这一层放当前阶段正在处理的信息:最近几步的对话、当前步骤相关的工具返回结果、临时的中间变量。这一层是滚动更新的,只保留最近 N 步(我一般设 5 到 8 步),更早的内容会被压缩或移出。

第三层:归档层(Archive Layer)。这一层放历史步骤的摘要。不是原始对话,而是经过压缩的关键信息:已经完成了什么、得到了什么结论、产生了哪些关键数据。归档层的内容按需检索,不是每次都全量注入。

实现上,我用一个简单的数据结构来管理:

class LayeredContext: def __init__(self, system_prompt, task_goal): self.system_layer = { "role": system_prompt, "goal": task_goal, "constraints": [], "tools": [] } self.working_layer = [] # 最近N步的完整记录 self.archive_layer = [] # 历史步骤的摘要 self.max_working_steps = 6 def add_step(self, step_record): self.working_layer.append(step_record) if len(self.working_layer) > self.max_working_steps: old_step = self.working_layer.pop(0) summary = self.summarize(old_step) self.archive_layer.append(summary) def build_prompt(self, current_query): # 系统层全量注入 prompt = self.format_system_layer() # 归档层按相关性检索 relevant_archive = self.retrieve_relevant(current_query) prompt += self.format_archive(relevant_archive) # 工作层全量注入 prompt += self.format_working_layer() prompt += current_query return prompt

这里有个关键细节:归档层的检索不是简单的关键词匹配。我试过用向量检索,效果比关键词好很多,因为很多历史信息的关联是语义层面的。比如当前步骤在处理"用户地址",历史里有一条"用户所在城市"的信息,关键词匹配不到,但语义检索能召回。

2.3 分层窗口的实测效果与调参经验

分层之后,最直接的变化是 token 消耗大幅下降。一个原本需要 8000 token 上下文的步骤,分层后通常只需要 2500 到 3500 token。这不只是省钱的问题,更重要的是模型决策质量明显提升。

但分层也带来了新问题:摘要质量决定成败。如果归档层的摘要做得不好,关键信息丢失,模型会做出错误决策。我踩过的坑是:早期用简单的截断式摘要,把历史步骤的前 100 个字保留,结果关键的数据值经常被截掉。后来改成结构化的摘要模板,强制保留"动作、结果、关键数据、状态变更"四个字段,问题才解决。

另一个调参经验是max_working_steps的设置。设太小(比如 3),模型会频繁"失忆",刚做完的操作就忘了;设太大(比如 15),又回到了注意力稀释的老问题。我的经验值是 5 到 8 步,具体取决于单步的信息密度。如果单步工具返回结果很长,就设小一点;如果单步信息很精简,可以设大一点。

注意:分层窗口不是万能的。如果任务本身需要全局视野(比如需要同时对比多个早期步骤的结果),过度压缩工作层反而有害。这种情况要适当放宽工作层,或者在归档层检索时提高召回数量。

3. 方法二:动态上下文压缩,让摘要跟着任务阶段走

3.1 固定摘要策略为什么在长任务中失效

分层窗口解决了"信息太多"的问题,但归档层的摘要如果是一刀切的固定策略,长任务后期依然会出问题。原因是:任务不同阶段关注的信息维度是不一样的。

举个实际例子。一个"从多个数据源收集信息并生成报告"的任务,早期阶段(收集阶段)关注的是"哪些数据源已经查过、返回了什么";中期阶段(整理阶段)关注的是"数据之间的关联和冲突";后期阶段(生成阶段)关注的是"报告的结构和关键结论"。如果归档层用同一套摘要模板,早期摘要里保留的"数据源 URL"在后期毫无用处,而后期需要的"结论性信息"在早期摘要里根本没被提取。

我最初的做法是写一个通用的摘要 prompt,让模型自己决定保留什么。结果发现模型在摘要时倾向于保留"看起来重要"的表面信息,而不是"对后续步骤有用"的信息。这导致归档层积累了大量无用摘要,真正需要的信息反而丢了。

3.2 按任务阶段切换摘要维度的实现

解决方案是让摘要策略跟着任务阶段动态调整。具体做法是:在任务开始时,先让模型把整个任务拆解成几个阶段,为每个阶段定义"关键信息维度",然后在执行过程中根据当前阶段选择对应的摘要模板。

PHASE_SUMMARY_TEMPLATES = { "collection": { "fields": ["source", "status", "key_findings", "data_id"], "instruction": "保留数据源标识、查询状态、关键发现和数据引用ID" }, "analysis": { "fields": ["data_refs", "relationships", "conflicts", "conclusions"], "instruction": "保留涉及的数据引用、发现的关联关系、冲突点和初步结论" }, "generation": { "fields": ["structure", "key_points", "evidence_refs"], "instruction": "保留报告结构、核心论点和支撑证据的引用" } } def summarize_with_phase(step_record, current_phase): template = PHASE_SUMMARY_TEMPLATES[current_phase] prompt = f""" 当前任务阶段:{current_phase} 摘要要求:{template['instruction']} 必须保留的字段:{', '.join(template['fields'])} 待摘要内容: {step_record} 请输出结构化摘要,严格包含上述字段。 """ return call_model(prompt)

这个做法的关键在于阶段划分要提前做,且要允许动态调整。我试过让模型在执行过程中自己判断阶段切换,效果不稳定,模型经常在该切换时没切换。后来改成:任务开始时先做一次阶段规划,执行过程中用一个简单的规则判断(比如"收集到 N 个数据源后进入分析阶段"),阶段切换时重新生成后续步骤的摘要模板。

3.3 压缩比控制与信息保真的平衡

动态压缩的另一个难点是压缩比的控制。压得太狠,信息丢失;压得太松,等于没压。我的经验是:归档层摘要的总长度控制在原始内容的 15% 到 25% 之间比较合适。

但这个比例不是固定的。对于信息密度高的步骤(比如一次返回了大量结构化数据的工具调用),压缩比可以到 10%;对于信息密度低的步骤(比如一次简单的确认操作),压缩比可以到 40%。判断依据是"这一步产生了多少对后续有用的信息"。

我还用了一个小技巧:给摘要加置信度标记。让模型在生成摘要时,对每个保留的字段标注一个"重要性"分数(1 到 5)。后续检索时,优先召回高重要性字段。这个做法在信息冲突时特别有用——当两个历史摘要对同一件事有不同描述时,优先信任高重要性的那个。

实测下来,动态压缩相比固定摘要,在 20 步以上的长任务中,任务完成率提升了大约 30%。最明显的改善是:模型在后期步骤中引用早期信息的准确率大幅提高,不再出现"张冠李戴"的情况。

4. 方法三:上下文隔离,把不同子任务的"工作台"分开

4.1 子任务之间的信息串扰是怎么发生的

长任务往往包含多个子任务。比如一个"竞品分析"任务,可能包含"收集竞品 A 信息""收集竞品 B 信息""对比分析""生成报告"四个子任务。如果所有子任务共享同一个上下文窗口,会出现严重的信息串扰。

我遇到过最典型的情况:Agent 在分析竞品 A 时,把竞品 B 的某个特性误安到了 A 头上。原因是两个竞品的信息都在同一个上下文里,模型在生成时发生了"注意力泄漏"。这种错误在人工检查时很难发现,因为表面上逻辑是通顺的,只是事实错了。

信息串扰的根源是:模型没有天然的"隔离"概念。在它看来,上下文里的所有 token 都是同等可用的信息,它不会主动区分"这是竞品 A 的信息,那是竞品 B 的信息"。当两个子任务的信息在语义上相似时,混淆的概率非常高。

4.2 用独立上下文空间隔离子任务的实操方案

我的解决方案是为每个子任务创建独立的上下文空间,子任务之间通过显式的"接口"传递信息,而不是共享上下文。

具体实现上,我设计了一个SubTaskContext结构:

class SubTaskContext: def __init__(self, task_id, task_goal, input_data): self.task_id = task_id self.goal = task_goal self.input_data = input_data # 从父任务或前序子任务传入 self.local_history = [] # 本子任务内部的执行历史 self.output_schema = None # 输出格式定义 def execute(self): # 子任务在自己的上下文里独立执行 # 不感知其他子任务的存在 result = run_agent_loop( goal=self.goal, context=self.local_history, input_data=self.input_data ) # 按 schema 格式化输出 return self.format_output(result, self.output_schema)

父任务负责编排子任务的执行顺序,并在子任务之间传递数据。关键是:子任务的输出必须是结构化的,不能是一段自由文本。我一般要求子任务输出 JSON 格式,包含明确的字段。这样父任务在传递数据时,可以精确控制哪些信息进入下一个子任务。

举个例子,竞品分析任务中,"收集竞品 A 信息"子任务的输出可能是:

{ "competitor": "A", "features": ["功能1", "功能2"], "pricing": "价格信息", "strengths": ["优势1"], "weaknesses": ["劣势1"], "evidence": ["来源1", "来源2"] }

父任务拿到这个 JSON 后,只把features和pricing传给"对比分析"子任务,evidence留在父任务层面备用。这样对比分析子任务的上下文里只有干净的结构化数据,没有原始网页内容、没有其他竞品的干扰信息。

4.3 隔离带来的额外开销与应对

上下文隔离不是没有代价的。最大的代价是信息传递的损耗。子任务之间通过结构化接口传递信息,意味着一些"隐性知识"会丢失。比如子任务 A 在执行过程中发现"某个数据源不可靠",这个判断如果没写进输出 schema,子任务 B 就不知道,可能会重复踩坑。

我的应对方式是:在输出 schema 里加一个meta字段,专门放这类"执行过程中的观察和警告"。子任务在输出时,除了业务数据,还要输出执行元信息。父任务在编排时,可以选择性地把 meta 信息传递给后续子任务。

另一个代价是编排复杂度上升。子任务隔离后,父任务需要管理子任务的依赖关系、执行顺序、数据传递。我早期手写编排逻辑,代码很快变得难以维护。后来改用 DAG(有向无环图)来描述子任务依赖,每个节点是一个子任务,边是数据传递关系。这样编排逻辑清晰很多,也方便做并行执行优化。

实测数据:在一个包含 6 个子任务的长任务中,隔离方案相比共享上下文方案,事实性错误率从 12% 降到了 2% 以下。代价是总 token 消耗增加了约 15%(因为子任务之间有重复的系统提示),但任务成功率从 65% 提升到了 90% 以上,非常值得。

5. 方法四:上下文校验,在关键节点做一致性检查

5.1 为什么模型自己发现不了上下文矛盾

即使做了分层、压缩、隔离,长任务中依然可能出现上下文矛盾。比如:第 5 步确认了"用户预算是 5000",第 12 步却基于"预算 8000"做决策。这种矛盾模型自己很难发现,因为它在每一步决策时,看到的是当前上下文,不会主动去比对历史信息。

我分析过模型"发现不了矛盾"的原因:模型的注意力是局部的。在生成第 12 步的决策时,模型主要关注的是最近几步的上下文和当前查询,早期信息虽然也在窗口里,但权重很低。除非矛盾非常明显(比如两个数字紧挨着出现),否则模型不会主动做一致性检查。

这就像一个人在多线程工作时,手头正在处理的任务会占据主要注意力,之前确认过的细节如果不在眼前,很容易被忽略。模型没有"回头检查"的习惯,除非你显式地要求它这么做。

5.2 在哪些节点插入校验最有效

校验不是越多越好。每一步都校验,token 消耗会爆炸,而且大部分校验是冗余的。我的经验是:在四个关键节点插入校验。

节点一:阶段切换时。当一个子任务完成、进入下一个子任务时,校验前序子任务的输出是否满足后续子任务的输入要求。比如后续子任务需要"竞品 A 的价格",前序输出里有没有这个字段。

节点二:关键决策前。当 Agent 即将做出一个不可逆的决策(比如提交订单、发送邮件、删除数据)时,校验决策依据是否与历史信息一致。

节点三:检测到异常时。当 Agent 连续两步执行了相似的操作,或者工具返回了错误,触发校验,检查是不是上下文出了问题。

节点四:任务收尾时。任务完成前,做一次全局一致性检查,确保最终输出与所有历史约束不矛盾。

校验的实现方式是一个独立的"校验 Agent",它拿到当前上下文和待校验的决策,输出"通过"或"发现问题 + 具体问题描述"。

def validate_decision(context, decision, check_type): prompt = f""" 你是一个上下文一致性校验器。请检查以下决策是否与历史上下文矛盾。 校验类型:{check_type} 历史上下文摘要:{context.get_archive_summary()} 当前工作上下文:{context.get_working_summary()} 待校验决策:{decision} 检查要点: 1. 决策依据的数据是否与历史信息一致 2. 决策是否违反了任何已确认的约束 3. 决策是否与之前的结论矛盾 输出格式: {{"passed": true/false, "issues": ["问题1", "问题2"], "suggestion": "修正建议"}} """ return call_model(prompt)

5.3 校验失败后的回滚与修复策略

校验发现问题后,不能简单地让 Agent "重新做"。因为 Agent 可能不知道具体哪里错了。我的做法是分级修复。

一级修复:局部修正。如果问题只是某个数据值用错了,直接把正确的值注入上下文,让 Agent 基于修正后的信息重新决策。这种修复成本最低。

二级修复:步骤回滚。如果问题涉及多个步骤的连锁错误,需要回滚到错误发生前的状态,重新执行。这要求上下文管理支持快照和回滚。我在每个关键节点都会保存上下文快照,回滚时恢复到最近的正确快照。

三级修复:任务重启。如果错误太严重,或者回滚成本太高,直接重启任务,但把失败原因作为约束注入新任务的系统层,避免重蹈覆辙。

实测中,一级修复能解决约 70% 的校验问题,二级修复解决 25%,只有 5% 需要三级修复。校验本身会增加约 20% 的 token 消耗,但考虑到它避免的失败重试成本,整体是划算的。

提示:校验 Agent 的 prompt 要尽量精简,只给它必要的历史摘要,不要给它完整上下文。否则校验本身也会遇到注意力稀释问题,漏报率会上升。

6. 四个方法的组合使用与优先级排序

6.1 不同任务规模下的方法选择

这四个方法不是必须全部用上。根据任务规模,我总结了一个选择策略:

任务步数推荐方法组合预期效果
5 步以内不需要特殊处理直接跑,问题不大
5-15 步分层窗口 + 关键节点校验成功率 85%+
15-30 步分层窗口 + 动态压缩 + 校验成功率 80%+
30 步以上四个方法全用成功率 75%+

注意,即使全用上,30 步以上的任务成功率也很难到 90%。这是因为长任务的失败因素不只是上下文,还有工具可靠性、外部环境变化等。上下文工程能解决的是"模型决策质量"这一块,大概能贡献 60% 到 70% 的成功率提升。

6.2 实施顺序:先做哪个,后做哪个

如果要从零开始搭建,我建议的实施顺序是:

第一步:分层窗口。这是基础,其他方法都依赖它。先把上下文分成系统层、工作层、归档层,跑通基本流程。

第二步:关键节点校验。这个投入产出比最高。不需要改太多架构,加一个校验函数就行,但能立刻减少明显的错误。

第三步:上下文隔离。当任务包含多个子任务时,隔离能显著降低串扰。但如果任务本身是线性的,隔离的收益不大,可以跳过。

第四步:动态压缩。这是最复杂的,需要设计阶段划分和摘要模板。建议在前三步稳定后再做。

我见过一些团队一上来就做动态压缩,结果因为基础的分层没做好,压缩后的信息无处安放,反而更乱。顺序很重要。

6.3 监控与迭代:怎么知道上下文工程生效了

上下文工程的效果需要量化监控。我一般跟踪四个指标:

  • 任务完成率:最直接的指标,但反馈周期长。
  • 单步决策准确率:抽样检查每一步的决策是否合理,能更早发现问题。
  • 上下文 token 效率:任务总 token 消耗除以任务步数,衡量上下文管理的效率。
  • 校验触发率:校验发现问题的频率。太高说明上下文管理有问题,太低可能校验太松。

我每周会跑一批回归测试任务,对比调整前后的指标。有一次我发现校验触发率突然下降,排查后发现是校验 Agent 的 prompt 被改动了,导致它变得过于宽松。如果没有监控,这个问题可能要等到任务失败率上升才会被发现。

7. 我踩过的三个坑和对应的解法

7.1 摘要过度压缩导致关键信息丢失

早期我做动态压缩时,为了省 token,把压缩比设得很激进,归档层摘要只有原始内容的 5%。结果模型在后期步骤中频繁"失忆",明明前面已经确认过的信息,后面又去重新查询。

排查过程:我对比了压缩前后的任务日志,发现失败的任务都有一个共同点——在某个步骤需要引用早期信息时,归档层里找不到。进一步分析发现,被压掉的信息不是"不重要",而是"当时看起来不重要,但后续需要"。

解法:把压缩比从 5% 放宽到 15%-25%,并且在摘要模板里强制保留"数据引用 ID"字段。即使具体数据被压缩了,通过 ID 也能追溯到原始记录。这个改动后,信息丢失导致的问题减少了 80%。

7.2 校验 Agent 本身被上下文污染

校验 Agent 的职责是检查一致性,但它自己也需要上下文。我最初把完整上下文喂给校验 Agent,结果它也被长上下文影响了,漏报率很高。

具体表现是:当上下文超过 5000 token 时,校验 Agent 开始"偷懒",对明显的问题也返回"通过"。我一开始以为是校验 prompt 写得不好,反复改 prompt 都没用。后来才意识到是上下文长度的问题。

解法:校验 Agent 只给摘要,不给完整上下文。摘要由归档层生成,长度控制在 1000 token 以内。同时,校验 Agent 的 prompt 里明确要求"如果信息不足以判断,返回'无法校验'而不是'通过'"。这个改动后,校验的准确率从 60% 提升到了 85% 以上。

7.3 子任务隔离后父任务成为瓶颈

上下文隔离解决了子任务串扰,但父任务需要管理所有子任务的输入输出,很快变成了瓶颈。特别是当子任务数量多、依赖关系复杂时,父任务的编排逻辑变得极其复杂,而且父任务自己的上下文也在膨胀。

我遇到的具体问题是:父任务需要记住每个子任务的输出,以便传递给后续子任务。当子任务达到 10 个以上时,父任务的上下文里堆满了各种 JSON 输出,又开始出现注意力稀释。

解法:父任务不保存子任务的完整输出,只保存"输出摘要 + 存储引用"。子任务的完整输出存到外部存储(比如文件或数据库),父任务需要时通过引用去取。这样父任务的上下文始终保持精简。同时,用 DAG 编排替代手写逻辑,把依赖关系显式化,减少父任务的"记忆负担"。

这三个坑有一个共同点:都是因为"省"而导致的。省 token、省事、省代码,最后都付出了更大的代价。上下文工程的核心不是"省",而是"在正确的地方花正确的成本"。

8. 写在最后:上下文工程是 Agent 的"内功"

做了这么多 Agent 项目,我越来越觉得:模型能力是"外功",上下文工程是"内功"。外功决定上限,内功决定下限。一个上下文管理做得好的 Agent,用中等模型就能跑出稳定效果;上下文管理做得差的,用最强模型也照样跑偏。

这四个方法——分层窗口、动态压缩、上下文隔离、关键节点校验——不是孤立的技巧,而是一套完整的上下文管理思路。它们的共同目标是:让模型在每一步决策时,看到的都是最相关、最干净、最一致的信息。

如果你现在正在被长任务跑偏困扰,我的建议是先从分层窗口和关键节点校验入手,这两个改动成本最低、见效最快。等基础稳定了,再逐步引入动态压缩和隔离。不要试图一次性全上,那样调试起来会很痛苦。

最后分享一个我常用的调试技巧:当任务跑偏时,不要只看失败的那一步,把失败步骤前 5 步的上下文完整打印出来,人工检查信息是否一致、是否有冗余、是否有过期数据。十有八九,问题就藏在那 5 步的上下文里。这个习惯帮我定位了大部分上下文相关的问题,比盲目改 prompt 有效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询