
在 Agent 开发里摸爬滚打这两年我越来越确信一件事大部分 Agent 跑飞、答非所问、循环调用工具根子都不在模型能力而在于上下文没管好。模型本身是忠实的执行者你喂给它的上下文是什么样它反馈出来的行为就是什么样。所谓上下文工程本质上就是把该让模型知道的和不该让模型知道的在每一轮请求里分清楚让 Agent 在有限的窗口内始终握有最关键的决策信息。这篇文章不聊概念直接拆解上下文工程的构成单元、预算分配、丢失问题和实战落地。适合正在做 Agent 开发、被多轮对话状态搞到头大、或者想从提示词工程进阶到上下文工程的开发者。我尽量把每一步的为什么也讲清楚而不是只给结论。1. 为什么上下文工程成了Agent开发的隐形瓶颈先说个我实际遇到的场景。早期我做过一个客服质检 Agent单看每轮 Prompt 写得挺完善角色设定、回答格式、语气规范都有单独测试效果也不错。一旦放进真实对话流让 Agent 连续处理五轮以上用户提问就开始出幺蛾子前面刚确认过的订单号后面它又问一遍上一步工具返回的库存数是 37 件下一步它居然按 370 件去算更离谱的是偶尔会把上一单客户的信息带到下一单里。这些问题不是模型笨而是每轮请求的内容变了。单轮提示词工程解决的是模型在孤立问题上的表现而真实的 Agent 是一个多轮循环系统每轮都要把之前的状态、工具返回、用户最新意图全部压缩进一次模型调用里。这个压缩过程如果没人精心管理信息就会丢失、错位、过期。我把这个阶段称为 Agent 开发的隐形瓶颈因为排查起来特别隐蔽模型报错不会告诉你上下文缺了一块它只会输出一个看似合理但其实是瞎猜的答案。上下文工程和提示词工程的核心差异就在这里提示词工程关注一句话怎么写模型才听话上下文工程关注多轮信息怎么组织模型才能稳定地听话两者有关联但层级不同。提示词工程是上下文工程里的一个子模块而上下文工程管的是从用户发起请求到模型返回结果之间整条信息链路的组装方式。一个典型的 Agent 单轮循环里上下文至少包含系统提示、历史消息、工具定义、工具返回结果、当前用户输入这几个板块。只要其中一个板块出了问题整体行为立刻变形。这也解释了为什么上下文工程在 Agent 项目里这么吃重。Agent 和普通聊天机器人的根本区别在于它会做事而做事就意味着要调用外部工具、要反馈结果、要基于结果做下一步判断。这一整套交互痕迹都要写进上下文里如果管理不当上下文就会越滚越臃肿、越滚越乱。后面你看到的 Token 超限、费用飙升、响应变慢、Agent 行为漂移全都是这一个根源上的分支症状。2. 上下文工程的构成拆解一份请求里到底放了什么要把上下文工程做好第一步是把上下文拆开看。一份发往大模型的请求不是一个提示词而是多个功能各异的模块拼接体。我建议所有做 Agent 的团队都建立一张上下文资产清单明确每一块内容的来源、用途、变化频率和 Token 占比否则根本无从优化。2.1 系统提示Agent 的宪法系统提示负责定义身份、目标、行为边界和输出格式。这是上下文中最稳定的一块通常整轮对话都不变所以它值得你花最多精力打磨。我在项目里会把它拆成四个子段落角色与职责、工作流程约定、输出格式要求、绝对禁止事项。这四块的优先级不同。角色与职责决定 Agent 的专业倾向工作流程约定决定它遇到多步任务时先做什么后做什么输出格式要求直接影响下游解析绝对禁止事项则是对抗幻觉和越权的最后防线。值得强调的是系统提示不是写得越长越好。每多一条规则模型遵从所有规则的概率就会降低一点我见过把几十条行为规范全塞进系统提示的项目结果 Agent 连最重要的那两条都守不住。合理的做法是分层核心规则用白名单方式列出次要规则放在遇到冲突时按以下优先级处理的说明里。2.2 消息历史多轮对话的显式记忆消息历史是上下文中变化最剧烈的模块。每轮用户说一句话、Agent 回一句话、工具返一个结果都会追加到这里。想象一个没有记忆功能的人和你聊天你只能靠一张不断增长的便签来维持对话这张便签就是消息历史。消息历史的管理难点在于它既不能全量保留也不能随便丢弃。全量保留的结果是 Token 线性膨胀很快撞上窗口上限随便丢弃的结果是 Agent 失忆用户明明两分钟前说过的事它转头就忘。后面我会专门讲截断和摘要策略这里先记住一个原则消息历史里保留的不应该是所有话而是所有影响后续决策的事实。2.3 工具定义与返回结果Agent 的手脚如果说系统提示是宪法那工具定义就是 Agent 可调用的能力清单。这部分通常以 JSON Schema 形式存在描述每个工具的名字、功能、参数和返回值。很多初学者会忽略一个细节工具定义里写的每个字段描述模型都会认真读并且可能误解。字段名是count还是item_count、描述写物品数量还是用户购物车中物品的件数模型的理解力完全不同。工具返回结果则是 Agent 行动后的感知来源。它需要回流到上下文里让模型知道工具执行得怎么样、拿到了什么数据。这块是上下文里最容易被污染的部分工具返回经常是一大坨 JSON里面有大量字段和当前任务无关模型读这些噪音字段不仅浪费 Token还可能被误导。后面我会讲到对工具返回做精简投影的做法。2.4 用户当前输入与临时状态每一次决策的输入信号当前输入是触发新一轮循环的那句话临时状态则是这一轮循环里产生的中间量比如一个变量、一个标志位、一个待确认的清单。这两块内容组合在一起构成了模型在当前这一步看得见的完整世界。我踩过的一个坑是把状态数据散落在对话里而不是结构化地维护。比如用户连续改了三遍收货地址如果每条地址只作为历史消息存在模型确实知道改过三次但当它决定用哪个地址下单时它需要在历史里翻找而且还可能翻错。更稳妥的做法是维护一个独立的current_state把当前生效的收货地址单独拎出来喂给模型历史里保留修改过程只是为了让 Agent 理解来龙去脉。3. Token是硬约束预算分配与窗口管理策略上下文工程绕不开 Token 预算。模型的上下文窗口是有上限的而 Agent 运行需要的上下文永远比想象中多。我见过有人把 200K 上下文的模型用出 2K 的效果原因就是预算分配不合理系统提示和工具定义占了 15K消息历史占了 180K留给当前任务判断的空间只剩 5K模型自然没法好好干活。3.1 给上下文各模块定预算我习惯在项目启动阶段就把上下文拆成预算条目类似做经费规划。一个典型的中型 Agent 可以这样分系统提示 1.5K Token、工具定义 3K Token、消息历史 5K Token、工具返回精简后 2K Token、当前输入 1K Token、临时状态与示例 1K Token。总窗口占用约 13.5K Token给模型推理留足空间。这个比例不是固定的。工具多的 Agent 要压缩工具定义对话轮次长的 Agent 要压缩历史但它们背后的逻辑是一致的先保住系统提示和当前输入再保工具定义最后才轮到历史。因为对绝大多数 Agent 来说历史里的大部分内容是低价值的而系统提示和当前输入直接决定这一轮的行为正确性。3.2 消息历史的三种压缩策略压缩历史有几种主流的打法依据对话特点选一种或组合使用。第一种是滑动窗口截断。保留最近 N 轮消息更早的直接丢弃。简单粗暴适合对话轮次少、每轮信息量大的场景。缺点是如果关键信息出现在第 N 轮之前就会被误杀。第二种是摘要压缩。每经过固定轮数或 Token 达到阈值调用一次模型对旧历史做总结去掉过程保留结论。成本较高但效果好适合长对话。这里有个重要细节摘要本身也会占用 Token如果只做一轮摘要不管控的话摘要会越积越长。需要给摘要设置上限或者做多层摘要先摘要早期内容再对摘要再做摘要。第三种是结构化筛选。不用自然语言而是从历史中抽取关键实体和事实存储成 JSON 结构比如current_order_id、user_consent_status、last_known_issue。重建上下文时直接塞入这些结构化字段比让模型从历史里自己提炼可靠得多。这也是我前面说状态散落在对话里是坑的原因结构化存储才能被稳定地重新注入。3.3 时间衰减信息的价值是递减的我在实际项目里用过一个很有效的策略时间衰减。消息的历史价值会随轮次下降但不同类型的信息衰减速度不一样。用户说过的硬约束比如不要用顺丰衰减很慢几乎全程都要保留闲聊内容衰减极快两轮后就可以丢弃工具返回结果衰减居中保持两三轮即可。具体操作上我会给每条消息打一个持久化等级标签必须级、上下文级、临时级。必须级进摘要或结构化状态上下文级保留最近 N 轮临时级当前轮用完即弃。这个标签机制做出来后历史管理就不再是一门玄学而是一条可执行的规则压缩质量也稳定多了。4. 上下文丢失与信息混乱Agent跑飞的第一大原因Agent 跑飞是开发中必然要面对的梦魇。表现五花八门上一秒还在处理 A 客户的订单下一秒就突然说起了 B 客户的发票刚拿到数据库返回的用户名转手就用了一个完全不存在的人名让 Agent 查了库存再去下单它跳过查询直接报了个默认值。这些现象的根因绝大多数能追溯到上下文的丢失和混乱。4.1 关键信息被截断最常见的情况是消息历史超过窗口上限被截断策略干掉的信息恰好是决策必需的那条。比如用户第 3 轮说过发票抬头要写公司全称Agent 第 10 轮该开票时却问请问发票抬头开什么就是因为第 3 轮的消息被滑动窗口滚掉了。这类问题用独立状态槽来解决最彻底把必须长期记住的信息从消息流里抽离出来单独维护、每次重组上下文时强制注入。我不指望模型能从历史里回忆起关键事实我只确保它每次都能看见关键事实。这个思维的转变很重要——上下文工程的核心不是让模型回忆而是让模型看见。4.2 摘要篡改事实摘要压缩有个隐蔽风险摘要过程本身可能失真。让模型对旧历史做总结时它会把用户说价格超过 500 就不要了概括成用户对价格有要求看似差不多但到了下单决策时差异巨大。我的应对办法有两个。一是对摘要做事实校验凡是涉及数字、时间、名称、否定性表述的内容不允许只出现在摘要里必须同步进入结构化状态字段。二是对关键的不可逆操作支付、删除、提交订单在系统提示里强制要求 Agent 检查关键事实是否在当前上下文中明确存在不存在就必须向用户确认宁可多问一次也不能瞎猜。这个宁可多问的兜底机制帮我在生产环境里拦截了大量事故。4.3 多路上下文互相污染还有一种被忽视的情况多个对话会话、多个任务流共享了同一段上下文。比如一个 Agent 既处理售前咨询又处理售后投诉如果某个全局变量或者系统提示没有按会话隔离A 会话里的信息就可能泄漏到 B 会话里。这个问题在消息历史的编码阶段就埋下了隐患。我在架构上强制做会话隔离“任务流隔离”。会话隔离是指不同用户、不同 session 的上下文对象完全独立底层不能有共享的可变状态任务流隔离是指在同一个会话里如果存在多个并发子任务每个子任务要有独立的上下文快照防止互相挤占。隔离做好了上下文混乱的概率直接下降一个量级。5. 记忆与工具定义最容易偷走上下文的两只贼如果说截断和摘要处理的是历史信息的量那记忆系统和工具定义处理的就是上下文里的质。我观察过很多项目的上下文工程优化效果最明显的永远在这两个方向。5.1 长期记忆到底该不该进上下文很多开发者一听到 Agent 要有记忆就直接把向量数据库的检索结果整段塞进系统提示。这个做法方向没错但细节偏了。给定一个用户问题检索出的 TOP-K 条记忆可能有一大半与当前任务无关全部塞进去等于徒增噪音。我在项目里的做法是分两步先检索候选记忆片段再用一个小模型或规则对候选片段做与当前任务的关联度排序只保留关联度最高的 2-3 条注入上下文。注入的位置也很讲究与当前决策强相关的记忆放临时状态区作为参考背景的记忆放消息历史末尾。位置不同模型对它的重视程度就不同。更有价值的做法是把记忆和状态分开。记忆是用户以前告诉过我们的信息状态是当前正在处理的这笔任务的进行时信息。模型需要状态来执行当下任务需要记忆来理解用户偏好两者不可混为一谈。混在一起的结果往往是状态淹没在记忆里模型记得用户喜欢蓝色却忘了当前这单要收货地址。5.2 工具 Schema 的肥胖病工具定义对上下文的吞噬能力极强。一个工具哪怕只定义一个参数其完整 JSON Schema 也可能占几百 Token。注册十个工具光工具定义就能吃掉几千 Token。更麻烦的是工具描述里的每句话都可能被模型过度解读。我之前维护过一个电商 Agent其中一个查询订单工具的描述里写了支持按订单号、手机号、时间段查询结果模型在处理退货时频繁调用这个查询工具只是为了确认订单是否存在而不是直接走退货流程。原因是描述信息过载模型不知道哪种场景该用哪种参数组合。对工具定义的瘦身我总结出三件事描述只写触发条件核心参数不写实现细节把参数分成必填和可选两栏可选项只保留高频的 2-3 个用枚举和范例代替长文本描述比如对status字段直接列出可枚举值比写一段订单当前所处的状态更省 Token 也更明确下面是我在项目里使用的工具 Schema 精简对照表供参考原始写法精简写法Token 对比status: 订单当前所处的状态包括待付款、已付款、已发货、已完成、已取消、退款中等多种情况status: 枚举pending paid shipped done cancelled refunding约省 60%customer_note: 用户在下单时填写的备注信息可能是对配送时间的特殊要求也可能是给配送员的留言customer_note: 订单备注原样透出约省 40%5.3 工具选择的显式路由上下文工程只管喂信息还不够Agent 在工具众多时会面临选择困难。我在生产项目里验证过一种有效做法显式路由。在系统提示里写清楚你是一个客服 Agent主要处理订单查询、退货退款、发票申请三类任务然后在工具描述里给每个工具标注它属于哪一类。这样模型在面对具体问题时会先判断任务类别再在该类别的工具集合里选择而不是在全部工具里乱找。显式路由还有个额外好处它能把工具定义的顺序也变成一种信号。模型对靠前的工具关注度天然更高我会把高频工具排在前面低频工具排在后面同等条件下模型会优先调用排前面的工具。这算是一种上下文排版学省 Token 又不容易出错。6. 上下文工程实战一个订单处理Agent的完整设计理论说了一堆我用一个真实的订单处理 Agent 来串联全文。这个 Agent 的输入是用户自然语言输出是 API 调用或回复文本涉及查订单、查库存、改地址、发起退款四个工具。目标是让它在连续多轮对话中不丢事实、不重复提问、不误操作。6.1 输入层先格式化再进上下文用户输入不会天然干净。第一步是把它规整成标准结构用户原话、意图类型、关键参数抽取、待澄清问题列表。这一步可以用一个小模型或规则引擎完成输出的结构化字段会进入 Agent 的上下文。关键点是原始用户消息不要直接塞给主 Agent只塞解析后的结构化结果。这么做的理由很简单原始消息里可能有模糊表达、指代词、口语噪音模型处理起来容易误解而结构化结果已经抽离了事实比如{intent: modify_address, field: shipping_address, new_value: 北京市朝阳区...}模型拿到这个 JSON 再决策几乎不可能理解错。6.2 上下文组装层按优先级拼接这是核心的上下文工程环节。我按固定优先级组装系统提示固定不变工具定义精简版按路由分组当前任务的结构化状态current_order_id、target_address、pending_confirmation等与当前任务强相关的历史事实从摘要库或记忆库拉取最近 N 轮原始对话滑动窗口当前输入的结构化结果组装顺序不是随意的。前面的内容是 Agent 的长期记忆和行为准则中间是当前决策的必需事实最后是辅助背景。当窗口紧张时我优先砍第 5 部分的轮次再砍第 4 部分的非关键事实第 1 和第 3 部分坚决不砍。6.3 调度与执行层工具返回的二次加工Agent 决定调用某个工具之后原生的工具返回 JSON 通常又臭又长。我在这一层加了一个返回精简器把原始返回投影成与任务相关的字段再塞回上下文。比如查库存返回的是{product_id: 123, warehouse_a: 10, warehouse_b: 5, reserved: 3, available: 12, eta_days: 3}如果当前任务是告诉用户能不能买那我只保留{available: 12, eta_days: 3}。原返回里的其他字段对决策没帮助塞进上下文只会徒增噪音。工具返回还应附带一点执行元信息比如调用是否成功、耗时是否异常。这些信息放进上下文能帮助模型做出这个数据是否可信的判断。有一次数据库查询超时返回了一个默认值模型看到元信息里的超时标志主动向用户说明了数据可能有延迟观感一下子就不一样了。6.4 收尾与状态持久化让下一轮从正确起点出发每次循环结束前我会把当前对话中的变更事实同步进结构化状态和记忆库。比如用户在这轮改了收货地址状态字段target_address要更新记忆库里的用户常用收货地址列表也要更新。如果这轮完成了退款申请状态里要新增refund_application_id。这个收尾步骤极其重要但很多人会漏掉。漏掉的结果是下一轮的上下文是从旧状态开始的Agent 会拿着过期的信息去决策然后又出现上一轮刚改完地址这一轮又开始问地址的尴尬。我把它命名为轻量状态 CheckPoint逻辑上就是每次循环写一次状态快照确保所有上下文工程里维护的字段永远反映最新事实。下面这段伪代码展示了这个 Agent 上下文组装的核心逻辑class OrderAgentContext: def build(self, user_input, state, history, memories): # 1. 系统提示固定 system_prompt self.get_system_prompt() # 2. 工具定义精简版 tool_schemas self.get_compacted_tools() # 3. 当前结构化状态从状态槽读取 current_state state.to_json() # 4. 与当前任务相关的记忆 relevant_mem memory_retriever.relevant(user_input) # 5. 最近5轮历史 recent_history history.last_n(5) new_input parse_structured(user_input) return [ {role: system, content: system_prompt}, {role: tools, content: tool_schemas}, {role: state, content: current_state}, {role: memory, content: relevant_mem}, *recent_history, {role: user, content: new_input} ]写到这里我想额外说一句上下文组装不是工程上把字符串拼起来的动作它决定了 Agent 每天几百上千次调用的行为基调。这个函数值得你反复打磨把每一块的生成逻辑都做成可观测的出现问题能在 10 分钟内定位是哪一块上下文坏了而不是在模型玄学里浪费时间。7. 现有工具链与后端配套让上下文工程落地上下文工程落到工程代码里离不开基础设施的支持。现在主流做 Agent 的开发框架基本都提供了上下文管理的骨架真正拉开差距的是你在骨架上填充的管理策略。7.1 框架层LangChain / LangGraph 的取舍LangChain 生态里我主要用它的AgentExecutor和ConversationBufferMemory系列但很快发现默认的内存机制是全量保留所有对话或者截断最近 N 轮这两者都不够精细。我建议不要迷信框架默认而是自己重写上下文组装函数只用框架来做工具调用解析和 Agent 循环调度上下文管理单独成为一个可控模块。LangGraph 这种图结构框架更贴合我的需求因为它把 Agent 运行显式建模成节点和边你可以在每个节点之间注入转型函数。比如工具返回节点之后加一个精简投影节点、历史写入节点之后接一个状态抽取节点整个上下文的变化路径是透明的调试起来清晰得多。实际项目中我倾向于用 LangGraph 做骨架配合自定义的上下文层。7.2 记忆库向量检索之外还要有结构化存取向量检索只是记忆库的一半。我在项目里会同时维护两张表一张是向量记忆存自然语言的偏好和反馈一张是结构化记忆存用户属性、历史订单摘要、未完成事项等可量化的信息。注入上下文时向量记忆只作为背景参考结构化记忆直接映射为状态字段后者可编程、可校验对决策的作用更确定。7.3 可观测与评测没有度量就没有优化上下文工程优化不能靠感觉必须有可观测性和评测指标。我在生产环境里对每一次模型调用记录三个指标上下文各模块的 Token 占比、关键事实是否成功注入、模型回答是否引用了注入的事实。前两个指标是工程指标第三个指标是行为指标结合起来能判断上下文管理得好不好。在开发阶段我会建一个回归用例集里面包含二十个左右的多轮对话场景每次修改上下文组装逻辑后跑一遍全量。用例集里的断言不一定要求模型输出一字不差但必须满足关键事实条件比如第二轮回答里必须提到订单号 12345第五轮不允许重复询问发票抬头。有了这个回归机制上下文工程才从拍脑袋调参变成可迭代的工程实践。7.4 两条腿走路缓存与精简对并发的意义关于 AI Agent 扛并发我的经验是上下文工程做得好等于变相降低了每次请求的 Token 消耗从而把模型吞吐能力让给了更多并发。同一个用户的连续对话如果系统提示和工具定义完全一致可以去请求级缓存命中这个优化直接削减 20%-30% 的模型调用量。再进一步把工具定义的公共前缀与每个请求的私有部分拆开很多框架支持前缀复用减少重复计费。还有对历史消息做半请求拼接历史里不变的部分直接复用上轮的编码结果只对增量部分做模型交互。这三层优化叠加起来同样一批机器能扛住的并发会明显提升。我不否认工程上还有其他并发手段但上下文这层省下的 Token 是真金白银对成本和性能都是决定性的。8. 上下文工程的边界没有银弹只有权衡上下文工程做到后面你会越来越清楚地感受到它的边界。模型上下文窗口在持续扩大但窗口大不等于什么都往里面塞。窗口只是容量上限不是质量保证。一个塞满了低质量信息的超大上下文比一个精挑细选的中等上下文表现得差得多这种反直觉现象我验证了无数次。我还想强调一点上下文工程不是越做越复杂越好。很多团队在系统提示里堆了几十条规则在工作流里加了无数个状态字段结果 Agent 反而变迟钝了。成功的上下文工程更像是做减法——找到每个任务里真正驱动决策的那 5% 的信息然后反复优化让它们稳定注入其余内容大胆地丢弃或外置。我个人在实际操作中有几条铁律供你参考每次组装上下文前先问一句这条信息这轮不用会不会出事不会出事就不放每个状态字段都配上来源和更新时间宁可多样式不搞隐形数据工具返回一律精简投影后再入上下文杜绝原封不动地灌输任何新增上下文模块必须以回归用例验收不能只靠肉眼观察上下文工程这个技能需要你在真实项目里反复碰壁才能形成手感。如果你现在做的 Agent 正在频繁上演失忆大戏先别急着怀疑模型能力更别盲目加长窗口把你手里的每份上下文摊开来看一看。把这个问题解决了Agent 的稳定性和并发能力都会上一个台阶这比追逐任何新模型都实在。