AI Agent落地瓶颈:上下文工程实战指南
1. 为什么上下文工程成了AI Agent落地的真正瓶颈过去一年我参与过好几个AI Agent项目从客服自动应答到代码仓库巡检几乎每个项目在Demo阶段都跑得挺漂亮一旦进入真实业务场景就开始掉链子。团队一开始总怀疑是模型能力不够换更大的模型、调更高的temperature、加更多的few-shot示例折腾一圈下来发现提升有限。后来复盘才意识到真正卡住我们的不是模型本身而是上下文工程没做扎实。所谓上下文工程说白了就是在每一次调用大语言模型时你往它的上下文窗口里塞什么、按什么顺序塞、塞多少、什么时候该丢、什么时候该压缩。这件事听起来像是写Prompt的升级版但实际复杂度完全不在一个量级。Prompt工程关注的是单次交互怎么问得清楚而上下文工程关注的是一个Agent在几十轮甚至上百轮循环中如何持续维护一份让模型能做出正确决策的信息状态。为什么它现在变得这么关键因为Agent和普通对话机器人的本质区别在于Agent要自主决策、调用工具、观察结果、再决策这个循环里每一步都会产生新的上下文。工具返回的JSON、历史对话、系统指令、检索到的文档片段、中间推理过程全都要塞进有限的上下文窗口。窗口是有限的信息是无限增长的这个矛盾就是上下文工程要解决的核心问题。我见过太多项目把上下文当成一个随便拼字符串的环节结果就是模型在第3轮还能正确调用工具到第15轮就开始胡言乱语或者反复调用同一个工具陷入死循环。这些现象背后几乎都能追溯到上下文管理的问题——要么是历史信息把窗口撑爆导致关键指令被截断要么是工具返回的冗余数据淹没了真正有用的信号。这篇文章我想把上下文工程这件事拆开讲透。从它的核心构成、ReAct循环里的上下文流转、压缩与检索策略、到实际落地时的踩坑经验尽量给出一套可以直接参考复现的思路。不管你是刚接触Agent开发还是已经踩过一些坑希望都能从中拿到点能用的东西。2. 拆解Agent上下文的五个组成部分要管好上下文先得知道上下文里到底有什么。很多人以为上下文就是用户说的话加系统提示词这在简单对话里没错但在Agent场景下远远不够。一个成熟Agent的单次上下文通常由五个部分构成每一部分的管理策略都不一样。2.1 系统指令与角色约束这是上下文的最顶层定义了Agent是谁、能做什么、不能做什么、输出格式是什么。它通常在整个会话中保持不变但恰恰因为它不变很多人在做上下文压缩时会不小心把它裁掉导致Agent行为突然失控。系统指令里我一般会放三类内容角色定义你是一个负责XX的助手、能力边界你只能调用以下工具、输出规范必须返回JSON格式字段包括thought、action、action_input。这三类里输出规范最关键因为Agent循环要靠解析模型输出来决定下一步动作格式一旦错乱整个循环就崩了。提示系统指令建议放在上下文的最前面并且在压缩历史时永远保留。我习惯给它单独打一个不可压缩的标记压缩逻辑里直接跳过。2.2 对话历史与推理轨迹这是增长最快、也最需要管理的部分。在ReAct模式里每一轮循环都会产生思考-行动-观察三元组这些三元组累积起来就是推理轨迹。它既是Agent保持连贯性的依据也是上下文膨胀的主要来源。我做过一个统计一个中等复杂度的任务比如帮我查一下最近三天的订单异常并生成报告Agent平均要跑12到18轮循环。每轮三元组平均消耗300到500个token光推理轨迹就能吃掉5000到9000个token。如果再加上工具返回的原始数据很容易就逼近甚至超过上下文窗口。2.3 工具定义与调用结果工具定义是静态的通常放在系统指令附近描述每个工具的名称、用途、参数格式。这部分相对稳定但工具数量一多也会占不少空间。我见过一个项目接了20多个工具光工具描述就占了3000多token。工具调用结果则是动态的而且往往是信息密度最低的部分。一个数据库查询可能返回几十行JSON但Agent真正需要的可能只是其中两三个字段。如果不做处理直接塞进上下文就是在浪费宝贵的窗口空间。2.4 外部检索的知识片段很多Agent需要接入知识库比如RAG检索。检索回来的文档片段会作为上下文的一部分提供给模型。这部分的管理难点在于检索质量不稳定有时候召回一堆不相关的内容反而干扰模型判断。我的做法是给检索片段加一个相关性阈值低于阈值的直接不塞进上下文宁可让模型说我没找到相关信息也不要让它被噪声带偏。2.5 临时状态与中间变量这是最容易被忽略的一部分。Agent在执行任务时往往需要记住一些中间状态比如已经查过订单A了当前处理到第3步。这些状态如果不显式写进上下文模型就会遗忘导致重复劳动。我通常会在上下文里维护一个结构化的任务状态区块用简洁的键值对记录进度。它占不了多少token但能显著减少Agent的重复行为。把这五部分理清楚之后你会发现上下文工程的核心任务就变成了在有限的窗口里动态地决定每一部分保留多少、以什么形式保留。这就是接下来要讲的ReAct循环里的具体策略。3. ReAct循环中上下文的动态流转ReActReasoning Acting是目前Agent最主流的运行范式它的核心是让模型交替进行推理和行动。理解上下文在这个循环里怎么流转是做好上下文工程的前提。3.1 一轮完整循环里上下文发生了什么我用一个具体例子来说明。假设任务是查询北京今天的天气如果下雨就提醒我带伞。第一轮上下文是系统指令 用户问题。模型输出thought我需要查天气和action调用天气工具参数是北京。这时候上下文追加了模型的输出。第二轮工具返回结果北京今天小雨。这个结果作为observation追加进上下文。此时上下文变成了系统指令 用户问题 第一轮thought/action observation。第三轮模型看到observation后输出thought下雨了需要提醒带伞和最终答案。上下文再追加这部分。可以看到每一轮循环上下文都在增长。如果任务复杂循环十几轮上下文就会变得很长。这里的关键问题是模型在每一轮都需要看到全部历史吗答案是不一定。模型真正需要的是当前决策所需的最小充分信息。比如到了第三轮第一轮里我需要查天气这个thought其实已经不重要了重要的是observation里的天气结果。但完全丢掉又可能丢失上下文连贯性。这就引出了压缩策略。3.2 观察结果的截断与结构化工具返回的observation是上下文膨胀的重灾区。我的处理原则是能结构化就不放原文能截断就不放全量。举个例子一个查询订单的工具返回了这样的JSON{ order_id: 12345, status: 异常, create_time: 2024-01-01, items: [...50条商品...], logistics: {...大量物流轨迹...}, customer: {...客户详细信息...} }Agent真正关心的可能只是status和order_id。如果直接把整个JSON塞进去可能消耗上千token其中90%是噪声。我的做法是在工具层就做一次预处理只把关键字段提取出来格式化成简洁文本再放进上下文订单12345状态异常这一下就把token消耗降了一个数量级。而且信息更聚焦模型反而不容易分心。注意截断要谨慎不能把模型决策必需的信息截掉。我一般会针对每个工具单独定义关键字段白名单而不是用通用的截断规则。3.3 推理轨迹的滑动窗口与摘要对于历史推理轨迹我常用两种策略结合滑动窗口 阶段性摘要。滑动窗口就是只保留最近N轮的完整三元组更早的直接丢弃。N一般取5到8具体看任务复杂度。这个策略简单有效但缺点是可能丢掉早期的关键信息。所以我会配合阶段性摘要当累积的轨迹超过一定长度时触发一次摘要把前面的轨迹压缩成一段简短描述比如已完成查询订单状态、确认异常原因当前进度准备生成报告。这段摘要替代原始轨迹放进上下文既保留了进度信息又大幅压缩了体积。这两种策略的组合我在实际项目里验证过能把长任务的上下文体积控制在原始的三分之一左右而任务成功率基本不受影响。3.4 循环终止条件的上下文信号还有一个容易被忽略的点Agent什么时候该停止循环这个判断也依赖上下文。如果上下文里没有明确的任务已完成信号模型可能会一直循环下去。我会在系统指令里明确告诉模型当你认为任务已经完成或者无法继续推进时必须输出特定的终止标记。同时在上下文里维护一个已尝试动作列表避免模型反复尝试同一个失败的动作。这个列表不需要很长记录最近几次失败的动作即可但它能有效防止死循环。4. 上下文压缩的四种实战策略前面反复提到压缩这一节我把实际用过的压缩策略系统梳理一下。每种策略都有适用场景没有银弹关键是组合使用。4.1 基于token预算的动态裁剪最基础也最必要的策略是给上下文设一个token预算然后按优先级分配。我的分配逻辑大致是这样的上下文部分优先级预算占比是否可压缩系统指令最高15%否当前任务状态高10%部分最近N轮轨迹高30%是工具定义中15%是检索片段中20%是早期历史摘要低10%是这个比例不是固定的要根据任务类型调整。比如工具密集型任务工具定义占比要调高知识问答型任务检索片段占比要调高。动态裁剪的关键是在每次调用模型前先算一遍当前上下文的token数超预算就按优先级从低到高裁剪。这个计算本身有开销但相比模型调用可以忽略不计。4.2 语义压缩而非机械截断机械截断的问题是可能把一句话截成半句导致语义破碎。更好的做法是语义压缩用一个小模型或者规则把长文本改写成短文本保留核心语义。比如一段工具返回的日志2024-01-01 10:00:00 系统开始处理订单 2024-01-01 10:00:05 订单校验通过 2024-01-01 10:00:10 库存检查失败商品A缺货 2024-01-01 10:00:15 处理终止语义压缩后订单处理失败商品A缺货信息量几乎没损失但token数从上百降到十几个。这种压缩可以用规则实现提取关键行也可以用模型实现让模型总结。规则实现快且稳定模型实现灵活但慢我一般优先用规则。4.3 检索增强的按需加载与其把所有可能用到的信息都塞进上下文不如按需加载。具体做法是上下文里只放一个信息索引告诉模型有哪些信息可用当模型需要时再通过工具调用去取。比如知识库场景上下文里先放文档标题列表模型判断需要哪篇再调用工具读取全文。这样上下文体积可控而且模型获取的是它真正需要的内容。这个策略的代价是多了一次工具调用往返延迟会增加。所以适合信息量大、但单次只需要一小部分的场景。4.4 压缩带来的信息损失如何兜底任何压缩都有信息损失风险。我的兜底方案是保留原始上下文的完整副本压缩版只用于模型调用一旦发现模型因为信息缺失做出错误决策可以回溯原始上下文排查。另外我会在压缩后的上下文里加一句提示以上为压缩摘要如需详细信息可调用XX工具获取。这样模型知道自己看到的是摘要必要时会主动去取详情而不是基于不完整信息硬猜。5. 工具返回结果的处理最容易被低估的环节如果说上下文工程有一个性价比最高的优化点那一定是工具返回结果的处理。我见过太多项目在这里偷懒直接把工具原始输出塞进上下文结果就是token哗哗地烧效果还不好。5.1 为什么原始工具输出是上下文毒药工具返回的数据设计初衷是给程序消费的不是给模型消费的。它往往包含大量模型不需要的字段、嵌套结构、冗余信息。直接塞给模型有三个问题第一浪费token。一个查询接口返回的完整JSON可能有几千token模型真正需要的可能就几十个。第二干扰判断。冗余信息会稀释关键信号模型可能被无关字段带偏。我遇到过模型把日志里的时间戳当成业务数据的情况。第三格式不友好。模型对结构化JSON的理解能力不如对自然语言的尤其是深层嵌套的结构容易解析出错。5.2 在工具层做预处理的三条原则我的做法是在工具层就做预处理让工具返回模型友好的结果。三条原则原则一只返回决策必需字段。每个工具明确定义模型需要哪些字段其余一律不返回。这个定义要结合Agent的实际任务来定不能拍脑袋。原则二扁平化结构。深层嵌套改成扁平键值对或简洁文本。比如{a: {b: {c: 1}}}改成a.b.c: 1。原则三控制单次返回量。如果结果可能很多加limit参数或者只返回摘要加一个获取详情的入口。5.3 错误信息的处理方式工具调用失败是常态错误信息怎么进上下文也有讲究。直接把异常堆栈塞进去模型看不懂还可能被吓到。我的做法是把错误归类成几种模型能理解的类型参数错误告诉模型哪个参数不对应该怎么改权限错误告诉模型这个操作不被允许换别的方式超时错误告诉模型可以重试业务错误把业务错误码翻译成自然语言这样模型拿到错误信息后能做出有针对性的调整而不是盲目重试。5.4 一个真实的优化案例之前有个项目Agent要查询数据库生成报表。最初工具直接返回查询结果的全部行一个报表任务上下文能到15000token经常超限。后来我做了三件事查询加limit、只返回聚合结果而非明细、错误信息结构化。上下文直接降到3000token以内任务成功率反而从70%提升到90%以上。这个案例说明工具返回结果的处理不只是省token更是提升Agent决策质量的关键。6. 长任务场景下的上下文持久化与恢复短任务上下文管理相对简单跑完就丢。但长任务比如需要几小时甚至跨天完成的流程上下文管理就复杂了因为会话可能中断需要恢复。6.1 上下文快照的存储设计我的做法是定期给上下文做快照存到外部存储。快照不是存原始上下文全文而是存重建上下文所需的最小信息任务状态、已完成步骤摘要、关键中间结果、当前进度指针。这样恢复时用快照加系统指令就能重建一个可继续执行的上下文而不需要保存全部历史。存储成本低恢复也快。6.2 跨会话的状态一致性长任务可能跨多个会话状态一致性是个难点。比如用户在会话A里让Agent查订单会话B里问刚才那个订单怎么样了Agent得知道刚才那个指什么。我的方案是给每个任务分配一个task_id状态和task_id绑定。新会话开始时先根据用户身份或显式引用找到相关task_id加载对应状态。这样跨会话的连贯性就有了保障。6.3 恢复时的上下文重建顺序恢复上下文时顺序很重要。我一般按这个顺序重建系统指令 → 任务状态 → 历史摘要 → 最近几轮完整轨迹 → 当前待处理输入。这个顺序保证了模型先知道我是谁、在干什么再看到干到哪了最后处理现在要做什么。顺序错了会怎样我试过把历史摘要放在系统指令前面结果模型经常忽略系统指令里的格式要求输出格式错乱。这个坑踩过一次就记住了。7. 上下文工程里那些文档不会告诉你的坑前面讲的都是方法论这一节我想聊聊实际踩过的坑。这些东西在官方文档里基本找不到但每一个都让我debug了大半天。7.1 上下文顺序对模型注意力的影响同样一组信息放在上下文不同位置模型的使用效果差别很大。我的实测经验是关键指令放开头和结尾模型遵循度最高放中间最容易被忽略。这和大语言模型的注意力机制有关中间位置的信息容易被淹没。所以我在组织上下文时会把最重要的约束输出格式、终止条件放在系统指令末尾再强调一遍形成首尾呼应。7.2 工具描述写得越详细反而越糟直觉上工具描述越详细模型越会用。但实测发现描述过长反而会让模型抓不住重点甚至调用错误的工具。我现在的做法是每个工具描述控制在两三句话第一句说用途第二句说关键参数第三句给一个调用示例。简洁明确比面面俱到更有效。7.3 历史信息不是越多越好新手常犯的错误是把所有历史都留着万一有用呢。但历史越多模型越容易被早期信息干扰做出不符合当前情况的决策。我现在的原则是历史只保留对当前决策有影响的其余果断丢。宁可让模型重新查一次也不要让它被过时信息误导。7.4 压缩触发时机比压缩算法更重要很多人纠结用什么算法压缩其实触发时机更关键。压缩太早信息还没充分利用就丢了压缩太晚上下文已经超限了。我的经验是在上下文达到预算的70%时触发压缩留出缓冲空间。这个阈值可以根据任务调整但不要等到90%才压那时候往往已经来不及了。7.5 不同模型对上下文的敏感度差异同一个上下文换个模型效果可能天差地别。有的模型对长上下文处理得好有的模型对格式要求特别严格。所以上下文工程不能脱离具体模型来谈。我的做法是针对主力模型调优上下文策略同时保留一套通用兜底策略方便切换模型时不至于全盘重来。8. 一套可复用的上下文管理框架思路讲了这么多策略和坑最后我想给一个可以落地的框架思路。它不是具体代码而是一种组织方式你可以根据自己的技术栈去实现。8.1 分层设计把上下文当成数据结构而非字符串最核心的思路转变是不要把上下文当成一个不断拼接的字符串而要当成一个有结构的数据对象。这个对象里系统指令、任务状态、历史轨迹、工具结果各占一个字段每个字段有自己的管理策略。这样做的好处是压缩、裁剪、持久化都可以针对具体字段操作而不是对一坨字符串做正则替换。可维护性和可扩展性完全不是一个级别。8.2 每个字段配一个管理策略有了分层结构就可以给每个字段配策略。比如系统指令永不压缩永远置顶任务状态结构化存储每次更新覆盖历史轨迹滑动窗口 超限摘要工具结果工具层预处理 关键字段提取检索片段相关性阈值过滤 按需加载这些策略可以配置化不同任务用不同配置。这样一套框架就能适配多种Agent场景。8.3 监控与迭代上下文工程是持续调优的过程最后一点也是最重要的上下文工程不是一次设计好就完事的它需要持续监控和迭代。我会记录每次模型调用的上下文token数、压缩触发次数、任务成功率定期分析哪些环节是瓶颈。我自己的体会是一个Agent项目上线后上下文策略的调优往往能带来比换模型更大的效果提升。而且这个调优是渐进的每次改一点观察效果再改一点。急不得但也停不得。这套框架思路我在几个项目里用过虽然具体实现不同但核心逻辑是通用的。如果你正在做Agent开发建议从把上下文当数据结构这一步开始先把结构理清楚再逐步加策略。别一上来就追求完美的压缩算法那往往是本末倒置。我在实际使用中发现上下文工程最考验的不是技术能力而是对业务的理解——你得清楚Agent在每个环节真正需要什么信息才能做出正确的取舍。这个判断力只能靠一个个项目喂出来。