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( goalself.goal, contextself.local_history, input_dataself.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 有效得多。