AI Agent上下文工程实战:从Token管理到三层缓冲架构
1. 为什么上下文工程成了 AI Agent 的分水岭做 AI Agent 开发的人十有八九都经历过这样的场景模型本身能力不差工具链也搭好了但 Agent 跑起来就是不稳定——要么答非所问要么在多轮对话里把前面聊过的关键信息丢得一干二净要么在调用 API 时把参数拼错。很多人第一反应是“模型不行”于是换更大的模型、换更贵的 API结果问题依旧。我踩过这个坑之后才真正意识到大部分 Agent 的失败不是模型能力问题而是上下文管理问题。这就是“上下文工程”Context Engineering这个概念最近被反复提起的原因。它和早几年流行的“提示词工程”Prompt Engineering不是一回事。提示词工程关注的是“怎么把一句话写好”而上下文工程关注的是“在 Agent 运行的每一个时刻模型的上下文窗口里到底应该放什么”。前者是静态的、一次性的后者是动态的、贯穿整个 Agent 生命周期的。打个比方提示词工程像是给员工写一份岗位说明书上下文工程则像是给员工配一个随时更新的工作台——该放什么文件、该收走什么废纸、什么时候递上新资料全都要管。一个 Agent 要稳定工作光有一份好的说明书远远不够工作台的整理才是日常。这篇文章我想把上下文工程这件事拆开讲透。核心会覆盖几个问题上下文窗口到底是怎么被消耗的、Agent 里有哪些上下文来源、怎么设计一套可复用的上下文管理策略、以及在实际搭建 AI Agent 时那些文档里不会写的坑。适合已经动手写过 Agent、或者正准备用大语言模型 API 搭一套自动化流程的人看。如果你还在纠结“AI Agent 是什么”这个层面也能看懂因为我会尽量用生活化的例子把原理讲清楚。2. 上下文窗口到底是怎么被吃掉的2.1 从 Token 说起上下文窗口不是“字数”很多人第一次接触大语言模型 API 时会看到类似“maximum context length is 1048576 tokens”这样的报错。这里的 token 不是字数而是模型处理文本的最小单位。英文里一个 token 大约对应 0.75 个单词中文里一个汉字通常占 1 到 2 个 token具体取决于分词器。所以一个 128K 的上下文窗口实际能装的中文内容可能只有六七万字而不是十二万字。这个数字听起来很大但在 Agent 场景里消耗得极快。我做过一个粗略的统计一个中等复杂度的 Agent 单次任务上下文消耗大致是这样的上下文来源典型 Token 消耗说明系统提示词500 - 2000角色设定、行为规范、输出格式要求工具定义1000 - 5000每个工具的 JSON Schema工具越多消耗越大对话历史2000 - 50000随轮次线性增长是最大的变量检索到的知识1000 - 20000RAG 召回内容取决于切片策略工具调用结果500 - 10000API 返回的原始数据往往很长当前用户输入100 - 2000相对稳定把这些加起来一个跑了十几轮的 Agent 很容易就逼近窗口上限。一旦超了要么报错要么模型开始“遗忘”早期内容——这就是为什么很多 Agent 聊到后面就变傻。2.2 上下文不是越多越好注意力稀释效应新手常有的一个误区是“既然窗口大那就把所有信息都塞进去”。我早期也这么干过把整个知识库、全部历史对话、所有工具定义一股脑塞给模型结果发现效果反而更差。原因在于注意力稀释模型在处理长上下文时对每个 token 的关注度是有限的信息越多关键信息被“淹没”的概率越大。有个很直观的实验把一份 20 页的文档和一份 2 页的摘要分别喂给模型问同一个细节问题。在 2 页摘要上模型的准确率往往更高因为干扰信息少。这不是模型“读不完”而是它“抓不住重点”。所以上下文工程的核心目标不是“塞满”而是“精准”——在正确的时刻把正确的信息以正确的形式放进窗口。2.3 上下文的三层结构系统层、会话层、任务层我在实际项目里习惯把 Agent 的上下文分成三层来管理这个划分方式帮我理清了很多混乱系统层几乎不变的部分包括角色设定、全局规则、工具定义。这部分应该尽量精简能压缩就压缩能懒加载就懒加载。会话层随对话推进而变化的部分包括历史消息、用户偏好、当前状态。这部分需要做摘要和裁剪。任务层针对当前具体任务临时注入的部分包括检索结果、工具返回、中间推理。这部分用完即弃不该长期驻留。把这三层分开管理之后你会发现很多“Agent 变傻”的问题其实是因为任务层的临时数据污染了会话层或者系统层的冗余定义挤占了任务层的空间。分层之后每一层可以独立优化互不干扰。3. AI Agent 里上下文的六大来源与处理策略3.1 系统提示词越短越稳越具体越好系统提示词是 Agent 的“宪法”但很多人的宪法写得像小说。我见过一个系统提示词写了三千多字里面塞满了各种边界情况的处理说明结果模型反而抓不住主线。实测下来系统提示词控制在 800 字以内效果通常最好。写系统提示词有几个实操要点。第一把“必须做什么”和“禁止做什么”分开写模型对否定指令的遵循度普遍偏低所以禁止项要写得非常具体比如不要写“不要编造信息”而要写“如果知识库中没有相关内容直接回复‘未找到相关信息’不要推测”。第二输出格式要求放在最后因为模型对末尾内容的记忆更牢。第三能用结构化格式如 JSON Schema描述的部分就不要用自然语言描述省 token 也更准确。提示系统提示词里的工具定义如果很多可以考虑按需加载。比如一个 Agent 有 20 个工具但当前任务只可能用到其中 5 个那就只注入这 5 个的定义。这个技巧在工具数量多的时候能省下大量 token。3.2 对话历史摘要 滑窗的组合拳对话历史是上下文消耗的大头也是最难处理的部分。常见的策略有三种全量保留、滑窗截断、摘要压缩。全量保留只适合短对话滑窗截断会丢失早期关键信息摘要压缩则可能丢失细节。我的经验是组合使用最近 3 到 5 轮对话保留原文保证短期连贯性。更早的对话做摘要摘要里重点保留实体人名、地名、数字、决策结论、未完成事项。摘要本身也要控制长度超过一定轮次后把旧摘要再合并成更高层的摘要。这里有个细节很多人忽略摘要的生成时机。不要等到上下文快满了才摘要而应该在每轮对话结束后就增量更新摘要。这样摘要质量更稳定也不会在关键时刻触发一次昂贵的摘要调用。我一般会在对话轮次达到 8 到 10 轮时开始触发摘要之后每 3 轮更新一次。3.3 工具定义与调用结果Schema 精简与结果裁剪工具定义消耗的 token 往往被低估。一个稍微复杂的工具它的 JSON Schema 可能就有几百个 token十个工具就是几千。优化方法有几个一是合并相似工具比如“查询用户”和“查询订单”如果参数高度重合可以考虑合并成一个带 type 参数的工具二是精简描述字段把冗长的说明压缩成一句话三是按需注入前面提过的懒加载。工具调用结果的裁剪更关键。很多 API 返回的原始数据又长又杂直接塞进上下文是灾难。我的做法是在工具层就做一次预处理只提取模型真正需要的字段把嵌套结构拍平把长文本截断并标注“已截断”。比如一个返回 50 条记录的 API如果模型只需要知道总数和前 3 条那就只传这些。这个预处理逻辑写在工具代码里而不是指望模型自己去筛。3.4 检索增强内容切片策略决定成败RAG检索增强生成是 Agent 获取外部知识的主要方式但检索回来的内容怎么放进上下文学问很大。我见过最常见的错误是把整个文档切片直接塞进去结果一个切片 2000 字召回 5 个就是一万字还没开始推理上下文就满了。更合理的做法是两级切片 重排序。第一级切片做小比如 200 到 300 字保证召回精度召回后再做重排序只取最相关的 3 到 5 个切片如果切片内容确实需要更多上下文再按需扩展相邻切片。这样既保证了相关性又控制了体积。另外检索结果里应该带上来源标识方便模型在回答时引用也方便你排查问题。3.5 中间推理与草稿该丢就丢Agent 在做复杂任务时往往会产生大量中间推理内容比如思维链、草稿、临时计算。这些内容对当前步骤有用但对后续步骤往往是噪音。我的原则是中间推理只在当前步骤的上下文里保留步骤完成后立即清理。具体实现上可以把中间推理放在一个独立的“草稿区”每完成一个子任务就清空。如果某个中间结论需要保留就把它提炼成一句话写进会话层的摘要里。这样既保留了关键信息又避免了草稿堆积。这个技巧在多步推理的 Agent 里效果特别明显能显著降低后期步骤的上下文压力。3.6 外部状态别把状态机塞进上下文有些 Agent 需要维护复杂的状态比如订单流程、表单填写。新手容易犯的错是把整个状态对象序列化成 JSON 塞进上下文每轮都传一遍。状态对象一旦复杂token 消耗就很可观而且模型每次都要重新解析容易出错。更好的做法是把状态存在外部比如数据库或内存上下文里只放一个状态 ID 和当前步骤的简要描述。模型需要状态时通过工具调用来查询。这样上下文里永远只有轻量级的引用而不是沉重的数据。这个思路和编程里的“指针 vs 值”很像用引用代替拷贝既省空间又避免不一致。4. 一套可复用的上下文管理架构怎么搭4.1 整体架构三层缓冲 一个调度器把前面讲的策略串起来我实际用的架构大致是这样的三层缓冲 一个调度器。三层缓冲对应前面说的系统层、会话层、任务层每层有自己的容量预算和淘汰策略。调度器负责在每次调用模型前把三层缓冲里的内容按优先级组装成最终的上下文。容量预算的分配我一般按这个比例系统层不超过 20%会话层不超过 40%任务层不超过 40%。这个比例不是死的任务型 Agent 可以给任务层更多对话型 Agent 可以给会话层更多。关键是要有预算意识不能任由某一层无限膨胀。调度器的组装逻辑也有讲究。我习惯按“系统层 → 会话层摘要 → 任务层 → 会话层近期对话 → 当前输入”的顺序排列。为什么把近期对话放在任务层后面因为模型对末尾内容更敏感把最相关的近期对话和当前输入放在最后能提高遵循度。这个顺序是我反复调整后定下来的你可以根据自己的场景微调。4.2 关键组件摘要器、裁剪器、注入器架构落地需要三个核心组件。摘要器负责把长对话压缩成短摘要我一般用一个小模型来做这件事成本低且够用。摘要的 prompt 要固定保证格式一致方便后续解析。裁剪器负责按预算淘汰内容淘汰顺序是先淘汰任务层的过期数据再淘汰会话层的旧摘要最后才动系统层。注入器负责在正确的时机把检索结果、工具返回等动态内容放进上下文它需要知道当前任务处于哪个阶段。这三个组件我建议都做成独立的模块通过接口调用而不是耦合在 Agent 主逻辑里。这样方便单独测试和替换。比如摘要器效果不好你可以换一个 prompt 或换一个模型不影响其他部分。这种模块化设计在 Agent 迭代频繁的阶段特别有价值。4.3 预算计算给上下文留出安全边际很多人算上下文预算时直接用窗口上限减去当前用量这是危险的。因为模型在生成回复时也要消耗 token而且工具调用、格式修正都可能产生额外消耗。我的经验是留出 20% 的安全边际。比如窗口是 128K那实际可用预算按 100K 来规划。另外不同模型的 token 计算方式不同同一个文本在不同模型下的 token 数可能差 10% 到 20%。所以预算计算要用目标模型的分词器来算不能拍脑袋。如果用的是第三方 API很多平台提供了 token 计算接口调用一下就能得到准确数字。这个步骤看起来麻烦但能避免很多“莫名其妙超限”的问题。5. 实操从零搭一个带上下文管理的 Agent5.1 环境准备与依赖选择我以一个 Python 项目为例演示怎么把这套思路落地。依赖方面核心是大语言模型的 SDK我一般用官方 SDK 而不是第三方封装因为官方 SDK 对 token 计算和流式输出的支持更完整。另外需要几个辅助库用于 token 计算的 tiktoken 或对应模型的分词器用于向量检索的向量库以及用于结构化存储的轻量数据库。如果你用的是国产大模型 API比如智谱、DeepSeek 这些它们的 SDK 用法大同小异核心接口都是 chat completions。选型时重点看两点一是是否支持流式输出二是是否提供 token 计算接口。这两点直接影响上下文管理的精度。免费额度和价格也要考虑但不要为了省钱选一个 token 计算不准的 API后期排查问题会很痛苦。5.2 核心代码上下文管理器的实现下面是一个简化版的上下文管理器核心逻辑用 Python 写重点展示分层和预算控制class ContextManager: def __init__(self, max_tokens, safety_margin0.2): self.max_tokens max_tokens self.budget int(max_tokens * (1 - safety_margin)) self.system_layer [] self.session_layer [] self.task_layer [] def add_system(self, content): self.system_layer.append(content) def add_session(self, content, is_summaryFalse): self.session_layer.append({content: content, is_summary: is_summary}) def add_task(self, content, ttl1): self.task_layer.append({content: content, ttl: ttl}) def _count(self, items): return sum(count_tokens(i[content] if isinstance(i, dict) else i) for i in items) def assemble(self, current_input): # 按优先级组装超预算时从低优先级淘汰 system_tokens self._count(self.system_layer) session_tokens self._count(self.session_layer) task_tokens self._count(self.task_layer) input_tokens count_tokens(current_input) # 淘汰过期任务层数据 self.task_layer [t for t in self.task_layer if t[ttl] 0] for t in self.task_layer: t[ttl] - 1 # 如果还超淘汰旧摘要 while system_tokens session_tokens task_tokens input_tokens self.budget: if self.session_layer and self.session_layer[0][is_summary]: removed self.session_layer.pop(0) session_tokens - count_tokens(removed[content]) else: break return self._build_messages(current_input)这段代码的关键点是淘汰顺序和ttl 机制。任务层数据带一个存活轮次用完自动过期不需要手动清理。会话层的摘要按先进先出淘汰保证最新的摘要优先保留。实际项目里你还需要加上摘要生成、token 计数缓存等逻辑但骨架就是这样。5.3 参数选择预算比例与摘要触发点参数选择没有标准答案但有几个经验值可以参考。预算比例方面对话型 Agent 我用系统 15%、会话 50%、任务 35%任务型 Agent 用系统 20%、会话 25%、任务 55%。摘要触发点方面对话轮次达到 8 轮开始第一次摘要之后每 3 轮更新。摘要长度控制在原文的 20% 到 30%太短丢信息太长没意义。这些数字不是拍脑袋来的是我在几个项目里反复调整后稳定下来的。你可以从这些值出发根据自己的场景微调。调整时建议记录每次变更后的效果指标比如任务完成率、平均轮次、超限次数用数据说话而不是凭感觉。5.4 实测记录优化前后的对比我在一个客服 Agent 项目里做过对比测试。优化前上下文不做管理全量塞入结果在第 12 轮左右开始出现明显的信息遗忘任务完成率约 68%平均每 5 次对话就有 1 次超限报错。优化后采用三层缓冲加摘要策略任务完成率提升到 89%超限报错基本消失平均 token 消耗下降了约 40%。这个提升主要来自两个方面一是摘要让早期关键信息得以保留模型不再“失忆”二是任务层的 ttl 机制让临时数据及时清理减少了噪音干扰。值得一提的是优化后我并没有换模型用的还是同一个 API所以提升完全来自上下文管理。这也印证了开头那个判断很多 Agent 问题不是模型问题是上下文问题。6. 常见问题与排查技巧实录6.1 模型“失忆”先查摘要再查顺序Agent 聊到后面忘记前面说过的内容是最常见的问题。排查顺序我一般是这样的先看摘要是否覆盖了被遗忘的信息如果摘要里没有说明摘要生成有问题如果摘要里有但模型还是忘说明摘要的位置太靠前被后续内容稀释了可以尝试把关键摘要复制一份放到上下文末尾。还有一个隐蔽的原因是消息顺序。有些框架会把系统提示词放在最前面然后是一大堆历史最后才是当前输入。如果历史太长系统提示词的影响力就被稀释了。解决办法是在历史之后、当前输入之前再插入一段简短的系统提醒重申关键规则。这个技巧我叫它“末尾锚定”实测对遵循度提升明显。6.2 工具调用参数错误检查 Schema 和示例Agent 调用 API 时参数拼错通常有两个原因一是工具定义的 Schema 不够明确比如字段类型没写清楚、枚举值没列全二是缺少示例。模型对示例的遵循度远高于对描述的理解。所以每个工具定义里最好带一个完整的调用示例包括参数和预期返回。另外如果工具参数里有嵌套结构Schema 要写得非常详细每一层都要有描述。我见过一个工具的参数是一个对象数组Schema 里只写了 type: array没写 items 的结构结果模型每次生成的参数格式都不一样。补上 items 的详细定义后问题就解决了。这个坑很典型值得单独记一笔。6.3 超限报错预留边际与动态降级超限报错虽然烦但排查起来相对直接。首先要确认 token 计算是否准确用目标模型的分词器重新算一遍。如果计算没问题那就是预算留得不够把安全边际从 20% 提到 25% 或 30%。如果还是频繁超限说明内容本身太多需要做动态降级比如检索结果从 5 条降到 3 条历史对话从 5 轮降到 3 轮。动态降级的逻辑可以做成一个配置根据当前上下文用量自动调整。比如用量超过预算的 80% 时自动减少检索条数和历史轮数。这个机制在高峰期特别有用能避免因为个别长对话导致整个服务不可用。我一般会把这个降级逻辑和监控告警结合起来降级触发时记录日志方便后续分析。6.4 常见问题速查表问题现象可能原因排查方向解决技巧模型失忆摘要缺失或位置靠前检查摘要内容和位置末尾锚定关键信息参数拼错Schema 不明确或缺示例检查工具定义补充示例和嵌套描述超限报错预算不足或计算不准用分词器重算提高边际动态降级回答跑偏上下文噪音过多检查任务层数据启用 ttl 及时清理响应变慢上下文过长统计各层 token 占比压缩系统层和任务层工具不调用工具定义被稀释检查工具定义位置工具定义前置或末尾重申这张表是我从实际排查记录里整理出来的覆盖了八成以上的常见问题。遇到新问题时我会先对照这张表快速定位实在找不到再深入分析。这个习惯帮我省了很多时间。6.5 几个文档里不会写的坑第一个坑是摘要的累积误差。摘要本身是模型生成的会有信息损失如果摘要再被摘要误差会累积。我的做法是摘要只做一层不做多层摘要旧摘要直接淘汰而不是再压缩。这样虽然会丢一些早期信息但保证了摘要的准确性。第二个坑是工具返回的隐藏 token。有些 API 返回的 JSON 里有很多空字段和嵌套结构序列化后 token 数远超预期。我一般会在工具层做一次字段过滤把空值和不需要的嵌套删掉再返回。这个优化在一个项目里省了将近 30% 的 token。第三个坑是流式输出下的上下文统计。流式输出时模型的回复是逐 token 返回的如果你在流式过程中统计上下文会统计到不完整的回复。正确做法是等流式结束后再统计或者用累计的方式统计。这个细节不注意会导致预算计算偏差。7. 上下文工程的边界与后续演进聊到这里上下文工程的核心思路基本讲完了。我想补充一点个人体会上下文工程不是万能的它解决的是“信息怎么放”的问题解决不了“信息本身对不对”的问题。如果检索的知识是错的或者工具返回的数据有问题再好的上下文管理也救不回来。所以上下文工程要和数据质量、工具可靠性一起抓不能偏废。另外随着模型窗口越来越大有人觉得上下文工程会变得不重要。我的看法恰恰相反窗口越大信息越多筛选和组织的价值就越高。就像仓库越大库存管理越重要而不是越不重要。未来的 Agent 竞争很大程度上是上下文管理能力的竞争。如果你正在搭 Agent我的建议是先把上下文管理的基础设施搭好再往上堆功能。这个顺序反了后期会非常痛苦。我见过太多项目功能做了一堆最后卡在上下文超限和模型失忆上回头重构成本极高。先把地基打牢后面加功能就是水到渠成的事。最后分享一个我常用的小技巧在开发阶段把每次模型调用的完整上下文 dump 到日志里包括各层的 token 占比。这个日志在排查问题时价值极高能让你一眼看出是系统层太胖、会话层太长还是任务层有脏数据。养成这个习惯上下文问题的排查效率会提升一个档次。