☰
Agent上下文Token优化实战:Context Engine架构设计与成本压缩
2026/10/17 8:21:26 网站建设 项目流程

先给结论:我一直在维护一套内部叫 Context Engine 的中间层,专门解决 agent 在真实任务里的 token 消耗问题。它并不是一个简单的“把历史消息丢给模型做摘要”的脚本,而是会把每一次 agent 调用的上下文拆成“指令、事实、历史行为、工具返回”四个维度,分别做压缩、裁剪和重建。做完之后,在我们的基准任务里,token 使用量直接降了 71%。这篇内容会把成本账、架构设计、核心实现和踩过的坑完整讲一遍,适合正在做大模型 agent 应用、被账单和上下文长度两头折磨的开发者参考。

1. 先算账:agent 的 token 到底烧在哪

很多团队对 token 成本的感知来自最后的账单,而不是过程。真正把上下文拆开看的时候,大家才发现:真正有用、必须保留的信息可能只占一次调用的 20%~30%,其他全是被反复搬运的历史、冗余工具输出、以及并没有变化的系统指令。

1.1 多轮 agent 的隐形开销:历史、工具结果与提示词膨胀

一个标准的 agent 任务长什么样?用户提一个需求,agent 先理解意图,然后调一个工具查数据,再根据结果反问用户,然后继续查,最后总结。看起来每一步都合理,但每一步都在给自己的下一次调用“增加负重”。

举个例子:某客服工单处理 agent,一个任务平均要执行 30 轮左右。常见的情况是,第 20 轮的时候,agent 的上下文里还躺着前 19 轮的完整对话,包括用户反复修改措辞的过程、中间某次工具返回的冗长 JSON、甚至一次已经被纠正的误操作记录。模型每一次都要重新“读”完这些内容才能生成下一步,而这里面的 token 是被重复计费的。

系统提示词也是重灾区。很多团队会在系统提示词里塞大量角色设定、业务规则、Few-shot 样例、输出格式约束。这些内容每一次调用都原封不动地发过去。如果你的提示词写了 3000 token,30 轮就是 90000 token,这部分几乎没有任何变化,却成了最稳定的消耗源。

工具返回更夸张。一个查询接口可能返回 10 条记录,但 agent 实际只需要其中 2 条。有的工具动辄返回几千 token 的原始 JSON,其中包含大量枚举值、空字段、日志级别信息,模型根本用不上,但成本已经产生了。

1.2 71% 是怎么算出来的:一个可复盘的基准账

不看概念,看具体数字。我的基准任务是某客服工单处理 agent:它需要完成“读取用户工单描述 → 调用订单查询工具 → 确认退款条件 → 给出处理方案”的全流程,平均 30 轮。

优化前,一次完整任务的平均 token 使用量被我拆成了五块,如下表:

上下文组成部分平均 token 数说明
系统提示词3200角色、规则、Few-shot 全部常驻
历史轮次6300015 轮有效历史 × 平均 4200 token
工具返回6850多次查询返回的原始 JSON
当前轮输入7800用户新消息 + 推理链路
其他(分隔符、缓存头等)8682重复出现的格式开销与冗余参数
合计89532一次完整任务的 token 量

优化后,同样任务的用量变成:

上下文组成部分平均 token 数说明
精简系统提示词1800去掉静态重复,只留必要规则
折叠历史摘要6500用事件摘要替代逐轮原文
结构化工具结果1800只保留模型需要的关键字段
当前轮输入9864保留完整推理链与必要上下文
关键事实缓存3000用户身份、订单号等固定事实复用
其他(格式开销)3000统一上下文对象后的残余开销
合计25964一次完整任务的 token 量

算一下省了多少:(89532 − 25964) ÷ 89532 ≈ 0.710,也就是 71%。需要强调的一点是,这个结果不是靠某一招实现的,而是多个策略叠加后的效果。单独做历史摘要大概能省 20% 左右,单独精简系统提示词也能省几个点,真正把数字拉到 71%,靠的是把整个上下文生命周期管理起来。

1.3 Context Engine 的设计目标:不是压缩,而是预算

有了账之后,Context Engine 的定义就清晰了:它是一个介于 agent 和大模型之间的上下文管理中间层,核心职责不是“尽可能把文本变短”,而是“在有限的 token 预算内,让模型拿到完成当前任务所需的最少上下文”。

你可以把它理解成一个“为上下文做预算的人”。就像做饭的时候,食材可以堆满一整个厨房,但这一道菜只需要几样。Context Engine 要做的不是把所有食材都切成小块塞进锅里,而是根据这道菜的需求,决定拿哪几种食材、每样切多少、剩下的放回冰箱。这样既省材料,又不会影响成品质量。

2. Context Engine 的架构:在 agent 和模型之间加一层“上下文预算器”

实现这个目标,不能只在每次调用前写一个“压缩函数”就完事。因为 agent 的上下文是动态的、跨轮的,不同来源的信息有不同的生命周期。Text Engine 的架构必须能区分哪些该留、哪些该扔、哪些该被摘要、哪些该被结构化成可检索的形式。

2.1 整体架构:四层结构各管一件事

我用四层结构来组织 Context Engine:

第一层是采集层。它在 agent 与大模型的每一次请求/响应之间做透明代理,记录请求里的 system prompt、历史消息、工具结果,以及模型返回的推理结果。这一层不修改任何数据,只负责打标记。

第二层是理解层。它会基于当前任务意图,把采集到的上下文拆成几类:固定指令、对话历史、工具输出、事实数据、推理过程。同时估算每一类当前的 token 占比。

第三层是决策层。这里有一套策略引擎,根据意图类型、上下文重要度、token 预算上限来决定:哪些内容原样保留,哪些内容做摘要,哪些内容完全丢弃,哪些内容抽取成结构化事实存入记忆库。

第四层是执行层。执行压缩、截断、重组,把最终结果拼装成一个新的、符合目标模型输入格式的上下文对象,然后交给 agent 去调用模型。

这四层不是每次都全部跑一遍。比如一个只查一次工具、不需要历史记忆的简单请求,决策层会直接判定“保留当前轮全部内容,不加载历史”,整个过程开销很小。

2.2 为什么不用简单截断,也不用“每次全部摘要”

最省事的方案是把历史消息从最老的开始丢,或者每次把所有历史丢给模型让它生成一段摘要。这两种做法我都试过,各自都有明显的坑。

简单截断的问题是:最老的消息不一定最不重要。在一个客服工单里,用户在第三轮说的“商品颜色是蓝色,地址是某某小区”很可能比第 20 轮的寒暄重要得多。按时间顺序截断,会把关键约束丢光,模型后面只能靠蒙。

全部摘要的问题则是:摘要本身也要消耗大量 token,而且摘要会丢失细节。如果每 5 轮做一次全量摘要,摘要里的信息密度会越来越低,模型拿着摘要去做精细操作容易出错。更麻烦的是,摘要可能会把工具返回里的精确数值“概括”成一个模糊表达,比如把“退款金额 128.50 元”变成“金额较小”,这种失真对 agent 任务是致命的。

Context Engine 采取的方式是分层保留:固定指令走精简,对话历史按时间窗口折叠,重要事实进结构化存储,工具结果做字段级过滤。每一层都有专门的处理逻辑,而不是一刀切。

2.3 统一上下文对象:从“消息数组”到“事实 + 行动 + 指令”

过去的 agent 代码里,上下文就是一组 message 对象,用户消息、助手消息、工具消息全塞在一起。Context Engine 引入了一个统一的内部表示,把上下文拆成三个维度:

  • 指令(Instruction):系统提示词、任务目标、输出格式约束、当前用户最新请求。
  • 事实(Fact):用户身份、订单号、商品 ID、时间点、金额等结构化信息。这些内容被抽取后进入记忆库,可以跨轮复用。
  • 行动(Action):agent 已经执行过的工具调用、查询结果、决策路径、中间推理过程。

这样做的好处是,模型不再需要从一大段对话历史里自己“找信息”。事实被单独提取出来后,可以直接放进上下文最显眼的位置。行动则被折叠成简短的执行轨迹,只保留“做了什么、得到什么、结论是什么”,省掉过程性废料。

3. 核心实现:四个阶段把原始上下文改造成最小可用上下文

架构定好后,实现路径就很清楚了。Context Engine 对每一次模型调用都会执行四个阶段:捕获、分级、压缩、校验。这四个阶段里,前两个是判断,后两个是执行与保障,缺一不可。

3.1 阶段一:捕获与标记,先看清楚手上有什么

我在实现时先写了一个上下文采集函数。它不只是把 messages 数组原样存下来,而是给每一条内容加标签:

def capture_context(raw_messages, system_prompt, tool_results): tagged = [] for msg in raw_messages: tag = classify_message(msg) tagged.append({ "role": msg.role, "content": msg.content, "tag": tag, # system / instruction / user / assistant / tool "tokens": estimate_tokens(msg.content), "timestamp": msg.ts, "importance": 0 }) # 工具结果单独处理,拆成字段级 for result in tool_results: tagged.append({ "role": "tool", "content": extract_essential_fields(result), "tag": "tool_result", "tokens": estimate_tokens(result) }) return tagged

这里的classify_message不是只看 role,而是会结合内容特征判断。比如一条 role 是 user 的消息,如果它正在回答 agent 的追问,并且内容较短,会被标记为“对话推进”;如果它包含了新的订单号、地址、金额等信息,会被标记为“事实”。这个区分很重要,因为后续对两类信息的处理策略完全不同。

实现的时候可以用正则加关键词,也可以用一个小模型做分类。我的经验是,先用规则系统跑通,再慢慢迭代。一开始就用大模型分类,每一轮的分类成本可能会超过省下的 token。

3.2 阶段二:按意图与风险分级,决定保留策略

不是所有任务都需要完整保留推理链。Context Engine 的决策层有一套分级逻辑,我把它简化成三种模式:

  • 重推理模式:比如复杂的多步规划、数学计算、合同审核。这种模式会保留尽量完整的推理链和工具返回值,压缩比例低,甚至不压缩。
  • 标准问答模式:比如客服工单、信息查询。保留关键事实和最近 3~5 轮对话,历史折叠成摘要,工具结果按字段过滤。
  • 轻量模式:比如简单的文本改写、关键词提取。只保留系统指令和当前输入,不加载历史,不加载工具结果。

判断使用哪种模式,依据是“当前任务的复杂度”和“错误代价”。复杂度可以通过工具数量、分支数量来估算;错误代价则需要产品侧预先定义。比如涉及退款金额的操作,错误代价高,必须走重推理模式;只是回答一个产品介绍问题,走轻量模式就够了。

3.3 阶段三:压缩执行与 token 预算分配

这一阶段是核心中的核心。我会给每次调用设定一个 token 预算,然后在预算内按“重要性优先级”分配空间。优先级从高到低是:当前用户请求、必要事实、当前工具输出、最近行为轨迹、历史摘要、精简系统指令、Few-shot 样例。

下面是一个很简化的实现:

class ContextEngine: def __init__(self, token_budget=8000, max_history_rounds=15): self.token_budget = token_budget self.max_history_rounds = max_history_rounds def build_context(self, current_user_msg, tagged_context, facts): priority_order = [ ("current_user", current_user_msg, 0.30), ("facts", compress_facts(facts), 0.20), ("recent_actions", compress_actions(tagged_context), 0.20), ("history_summary", summarize_old_history(tagged_context), 0.15), ("system_instruction", compress_system_prompt(tagged_context), 0.10), ("few_shot", compress_few_shots(tagged_context), 0.05), ] result = [] remaining = self.token_budget for name, content, ratio in priority_order: # 预算比例只作为初始值,实际分配会根据内容动态调整 allocated = min(estimate_tokens(content), int(self.token_budget * ratio)) trimmed = trim_to_budget(content, allocated) result.append(trimmed) remaining -= estimate_tokens(trimmed) # 剩余预算分配给当前用户请求,避免尾段被强切 result[0] = trim_to_budget(result[0], remaining) return result

这个实现里最关键的是预算比例和动态调整。我试过固定预算,比如每条消息最多 1000 token,但效果很差,因为不同内容的信息密度差异太大。后来改成“先按比例分,再把剩余预算回补给当前用户请求”,效果好很多。当前用户请求永远不能因为历史太长而被挤出窗口。

实际代码里还要处理一个问题:模型对上下文的位置敏感。如果把系统指令放在最后,模型可能理解成“新的指令覆盖旧规则”。所以我在拼装上下文时会给每条内容加上明确的前缀标记,比如[系统规则]、[事实缓存]、[历史摘要],确保模型能区分信息来源。

3.4 阶段四:输出校验与回放测试

压缩必然有风险,所以执行完之后必须验证。我加了两层校验:

第一层是完整性校验。对必保字段做检查,比如用户 ID、订单号、金额、日期。如果压缩后的上下文里找不到这些字段,就直接拦截这次调用,回到未压缩版本重新跑。这个校验用规则实现,不消耗模型调用。

第二层是回放测试。在测试环境保存一批真实历史任务,每次修改压缩策略后,用旧策略和新策略各跑一遍,对比最终输出结果。我的内部指标是“关键字段一致性”和“任务成功率”,只要这两个指标不下降,就说明压缩策略可以上线。

后面我会专门讲回放测试里踩过的坑,这里先提一句:不要只看一次输出对不对,要跑至少 50 个典型任务,否则你只会发现压缩在个别场景崩了,但根本定位不了原因。

4. 实测效果与关键参数调优

数字不是一次就跑出来的。上一版架构里,我用的是“每 10 轮做一次对话摘要”,效果只是省了 35% 左右的 token,还丢了不少关键信息。后来把“事实抽取”和“工具结果字段级过滤”加进去,才把数字推到 70% 以上。这中间每一步的调优都离不开测量和回归。

4.1 三个 agent 场景的对比

我在三个不同场景上做了测试:

场景任务特征优化前 token优化后 token节省比例
客服工单处理多轮对话 + 工具查询 + 规则判断895322596471.0%
数据报表助手多表查询 + 数值计算 + 结论生成423601512064.3%
代码修复 agent长文件读取 + 逐步调试1184003910067.0%

可以看到代码修复场景压得也不少,但其实它用的策略完全不同:不是压缩代码本身,而是把调试过程中已经确认无关的部分从上下文里剔除,只保留当前疑似出错的那一段代码和对应的报错信息。

我特别想提醒的是,不要盲目追求“压缩率最高”。在报表助手场景,我一度把 token 压到 12000 以下,省了 72%,但任务成功率从 96% 掉到了 88%。原因是数值计算场景需要保留很多中间量,压缩策略把部分中间结果误判成“冗余”删掉了。后来我把召回阈值调低,把预算调整到 15120,成功率恢复到 95%,节省 64%。

4.2 token 预算怎么定才不损失精度

调参这件事是有规律可循的。我给自己的定法如下:

第一,先跑 20 个未压缩的基线任务,统计“模型真正会用到的 token 量”。怎么统计?我在每次调用前把上下文按内容块拆开,记录哪些内容块在被压缩后会导致错误。这个信息可以通过回放测试反向分析。基线跑完,你大概知道这个场景最少需要多少 token。

第二,把压缩后的 token 预算定在基线用量的 50%~70% 之间,用 20 个任务的回放成功率来判定。如果成功率在 95% 以上,继续往下降;如果跌破 92%,回调到上一个稳定档位。我最后在客服场景选的预算大约是 26000,刚好是基线的 29%,因为整个场景对历史摘要的容忍度比较高。

第三,给关键任务开“预算保险”。如果当前任务的风险等级高,比如涉及支付、权限变更,就允许 Context Engine 自动把预算翻倍,甚至跳过压缩。省 token 的目标永远不能凌驾于任务正确性之上。

4.3 踩过很深的坑:摘要时机、最小保留窗口、知识冲突

第一个坑是摘要时机。我最初在每次调用前都判断“历史长度是否超过阈值”,一旦超过就对全部历史做摘要。这个方案在 20 轮以内的任务上效果还行,但超过 30 轮后,摘要的时效性问题就出来了:模型拿到的摘要可能落后于当前状态,导致 agent 重复询问用户已经回答过的问题。后来我把摘要策略改成“只在关键时间点做摘要”,比如当用户新消息包含新的意图时,或者当工具返回结果需要与历史结合判断时,才触发摘要。这样既避免频繁摘要的浪费,也避免历史信息滞后。

第二个坑是最小保留窗口。我在减少历史轮次时,一开始只保留最近 2 轮,认为“模型只需要记住最近的内容”。结果 agent 经常忘记用户前面给出的约束条件。后来我意识到,很多关键事实不是在最近 2 轮出现的,而是在某个中段轮次以很自然的方式说出来的。于是我把方案改成“保留最近 5 轮完整对话 + 从更早对话中抽取事实存入缓存”。如果缓存里已经有这条事实,就把它放到上下文的事实区;如果没有,才保留那一段原始对话。这样做以后,省下来的 token 和正确率都更稳定。

第三个坑是知识冲突。当历史摘要里说“用户选择了快递配送”,而最新一条消息里用户说“改成到店自提”,这两个信息同时出现在上下文里时,模型偶尔会犯迷糊,不知道以哪个为准。解决方法是给每条事实加一个时间戳,并在拼装时把最新事实放在更靠前的位置,同时显式标注“此事实已更新,旧版本作废”。这种冲突处理在事实抽取模块里必须作为一个独立检查点。

5. 常见问题与排查技巧

Context Engine 用了大半年,被问得最多的问题无非是:压缩后任务质量下降了怎么办?摘要丢东西怎么办?长工具返回怎么处理?这里我把排查思路整理成一套可以照着做的方法。

5.1 压缩后任务完成率下降怎么办

先不要急着回调参。我建议按这个顺序排查:

  1. 对比压缩前后的输出,找到第一个行为差异点。很多时候压缩并没有丢信息,只是改变了信息顺序,模型就把原本做对的步骤做错了。这时候把事实区前移就行。
  2. 查看日志里被裁剪的内容。我每次压缩都会记录丢掉了哪些 text block 的 hash 和长度,有了这个记录,你能精确看到模型“没看到什么”。
  3. 用同一任务跑多次。模型本身有随机性,可能只是某次采样问题。如果连续 5 次都错,再怀疑是压缩导致。
  4. 最后才是调整策略。不要一上来就增加 token 预算,先尝试把同一段内容从摘要还原成原文,看是否修复。如果还原了还不够,说明问题不在压缩,而在 prompt 本身。

5.2 摘要把关键实体丢了怎么办

我遇到过最典型的情况是:摘要里写了“用户表示商品有问题”,但丢了商品型号、购买日期、订单号。模型只能接着问一遍用户,用户当然体验很差。

解决思路是“摘要是用来牺牲细节的,但不是用来牺牲事实的”。在做摘要前,先跑一步实体抽取,把订单号、商品 ID、金额、时间、地址这些结构化字段抽出来,放到事实区。摘要文本只保留流程性的描述,比如“用户已确认到货”、“客服已发起退款申请”。这样模型看到的是“结构化事实 + 流程摘要”,而不是一段模糊的自然语言概述。

另外,摘要的层次也很重要。同一段历史,按“轮”做摘要远不如按“事件”做摘要。比如“用户修改收货地址”是一个事件,哪怕这个事件跨越了 3 轮对话。按事件摘要,模型更容易理解来龙去脉。

5.3 工具长返回、代码块怎么处理

工具返回里的长文本是 token 消耗的大头,也是最不适合直接塞进上下文的内容。我的做法是分成三步:

第一步,把工具返回按格式拆成字段级。如果你是 JSON,就逐字段分析哪些字段会被后续流程用到;如果是自由文本或代码块,就按段落拆分关键信息。

第二步,对“被用到”的字段做二次精简。数值类字段保留原值,避免摘要模糊化;枚举类字段只保留命中项和可选范围;状态类字段保留最终状态即可,不保留全部历史状态迁移。

第三步,给工具结果加一个“可被检索”的指征。如果长代码块或者长日志确实需要在后续操作中查看,我不会把它全部塞进上下文,而是存进一个短时存储,并在上下文里放一个“待引用 ID”。只有当模型判断需要具体内容、发出引用请求时,才把那一段重新加载进上下文。这相当于把“全量读取”变成了“按需读取”,省掉的 token 非常可观。

5.4 排查流程速查表

症状大概率原因处理办法
模型忘记早期约束关键事实被折叠进摘要做实体抽取,将约束放入事实区
模型反复问同样问题摘要不及时或摘要覆盖了最近轮次调整摘要时机,保留最近 3~5 轮原文
同一任务答案时好时坏采样随机性跑 5 次确认,再决定是否改策略
工具结果被错误压缩字段级过滤太激进放宽数值类字段保留规则
模型执行顺序错乱上下文信息顺序变化把当前任务指令移到最前,固定顺序
长代码被截断后调试失败截断破坏了语法结构不要截断代码,用按需加载代码块

这个表是我在真实排查中沉淀下来的。每次有新开发者加入项目,我都会让他们先看这张表,能少走很多弯路。

5.5 回放测试的具体做法

最后补一个回放测试的模板。我把测试集按任务类型分成三类:需要记忆的、需要工具的、需要计算的。每一类至少 20 条真实历史任务。测试脚本会自动执行新旧策略,输出两份结果。

对比时,我不会直接比文本相似度,因为模型生成结果本来就允许表达不同。我更看重的是“结果是否满足验收条件”。比如工单场景的验收条件是:最终给出的处理方案是否包含正确的退款金额、退款方式、处理时效。只要这三个字段都对,就算通过。

这个方法帮我避过一次大坑:有一次我把上下文压缩比例调高,结果文本相似度大幅下降,但关键字段全对。如果当时只看文本相似度,就会误判成“压缩失败”,然后白调一周参数。要拿业务结果当验收标准,不是拿字符串当验收标准。

最后再分享一点个人体会

Context Engine 做下来,我最大的感受是:省 token 不是目的,让每次调用更高效才是目的。有时候多花几十 token 把事实从历史里提出来、放到模型一眼能看到的位置,反而能减少一次因为信息缺失而导致的错误重试,整体更省钱。这个平衡非常微妙,每个场景都不一样,必须靠测量和回访来校准。

另外一个小技巧:在 Context Engine 的日志里记录每一次被丢弃内容的哈希值。这个习惯帮我省了无数时间。当测试发现某个任务出错时,我能直接通过日志还原出模型当时“没看到什么”,而不是对着原始对话反复猜测。有了这个记录,排查时间至少缩短一半。

如果后续要做多 agent 场景,这个 Context Engine 的架构也可以直接共享。多个 agent 可以共用一个记忆库和事实缓存,互相之间不重复携带上下文,里面能省的空间比单个 agent 压缩还要大。这就是另一个话题了,先把单 agent 这 71% 稳稳拿到手,再往下走。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询