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 - 20000 | RAG 召回内容,取决于切片策略 |
| 工具调用结果 | 500 - 10000 | API 返回的原始数据,往往很长 |
| 当前用户输入 | 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. 实操:从零搭一个带上下文管理的 Agent
5.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_margin=0.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_summary=False): self.session_layer.append({"content": content, "is_summary": is_summary}) def add_task(self, content, ttl=1): 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 占比。这个日志在排查问题时价值极高,能让你一眼看出是系统层太胖、会话层太长,还是任务层有脏数据。养成这个习惯,上下文问题的排查效率会提升一个档次。