news 2026/10/17 11:55:02

Context Engine:如何将Agent的Token消耗降低71%?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Context Engine:如何将Agent的Token消耗降低71%?

做一个能把 agent 的 token 使用量压下去 71% 的中间层,听起来像是某种黑魔法,但真拆开看,我们只是老老实实回答了三个问题:每一轮调用 token 到底花在了哪、哪些 token 是必须花的、哪些是花了钱但根本没用的。把这三个问题搞清楚,Context Engine 的方案基本就自己长出来了。

写这篇东西的起因,是我们在做 agent 流程优化时发现一个很尴尬的现象——上下文越长,agent 越“笨”,响应越慢,账单还越贵。当时我们内部有个多轮任务型 agent,平均每轮对话要吃掉一万多个 token,其中相当一部分是重复的系统提示、过期的工具返回、以及永远不会再被用到的历史中间结果。单纯靠提示词压缩已经救不回来,必须换一种组织上下文的思路。所以才有了 Context Engine 这个项目。如果你也在被 token 成本和上下文长度折磨,这篇文章会把我们的设计思路、实现细节、参数调优和踩坑记录完整拆给你看,你可以直接拿去做参考。

1. 为什么需要 Context Engine

1.1 token 浪费的真实来源

先别急着设计缓存或者换模型,我们花了两周时间给现有 agent 做了一次 token 审计,把每次调用的 prompt 打出来逐段标价,最后发现浪费点基本集中在这几类。

第一,历史对话无差别堆叠。早期 agent 实现最简单的方式就是把整段 session 历史全量塞进 prompt,这就导致第 10 轮对话时,前 9 轮里的大部分内容依然在上下文里。但真实使用中,用户在第 5 轮说过的那句“帮我看看权限有没有开”,在第 9 轮已经得到解决,之后再也不需要原文,只要留着结论“权限已开启”就够了。

第二,工具返回结果过长且从未清理。我们的 agent 会调用多个内部 API,有些接口返回的是非常完整的 JSON,动辄几百行。但最终任务往往只用到其中三五个字段。之前的设计是“原样返回、原样塞入”,于是每轮都要支付几千 token。更麻烦的是,工具的中间结果在后续轮次里明明已经没用了,却没有任何机制把它们清出上下文。

第三,系统提示词与指令模板反复膨胀。我们最初的 system prompt 只有两百多个英文单词,经过多轮功能叠加,团队里不同的人都在往里面追加约束,最后膨胀到了两千多 token。其中大量是老旧的示例、过时的格式说明、甚至互相矛盾的规则。这个成本是固定支出,每一轮都要付。

第四,检索内容全量注入。知识库召回也好、向量检索也好,早期做法是 top-k 命中什么就全量拼接什么,哪怕命中的段落里只有一句话跟当前问题有关,也要把整个段落塞进去。这种“全量注入”式设计,在召回质量不错时确实省事,但 token 效率非常低。

1.2 我们想要的并不是“省 token”本身

跟很多人想的不一样,我们把目标定为降低 token 使用量,并不是单纯为了省成本。虽然成本确实是直接收益——毕竟 token 是实打实的钱,但我们内部更看重的是另一件事:上下文质量。

当一段上下文里掺了大量无关内容,模型的实际表现会显著下滑。做过长上下文实验的人应该都有体会,你给模型 32k 的窗口,它不一定真的会用满前 20k 的高质量信息。冗余内容会稀释注意力,导致关键指令被忽略、工具输出解析出错、甚至出现没必要的“幻觉”。所以我们真正要做的是:让留在上下文里的每一个 token 都有其存在的理由。

这也是我们坚决不做“硬截断”的原因。最省 token 的方案当然是把历史一刀切掉,只保留最近几轮,这谁都会写。但 agent 的很多任务有长期状态依赖,用户在第 2 轮提到的偏好,可能直接决定第 20 轮的结果。硬截断是拿任务成功率换 token,这种置换我们不接受。

1.3 技术选型:自己造轮子而非用现成方案

调研过 langchain 的记忆模块,也看了一些外部的上下文压缩库,最后我们决定自己写一个 Context Engine。原因有三。

一是我们的 agent 链路高度定制,工具调用格式、状态流转、会话恢复逻辑都有自己的约定,通用组件为了适配所有场景,抽象层很厚,引入之后反而不同步。

二是我们要的不是“一个记忆组件”,而是一条完整的上下文流水线——压缩、检索、去重、排序、重建 prompt 必须协同工作,这已经超过现成组件单点能力范围。

三是问题本身足够清晰:我们清楚 token 花在哪,清楚每类内容的价值周期,清楚任务对上下文的最低要求。当问题定义足够明确时,自己写一个中间层反而比改造成熟框架更快、更可控。

2. Context Engine 核心设计思路

2.1 对话历史分层:短窗口、长记忆、工作区

Context Engine 的第一个核心思想,是把历史对话拆成三层,分别用不同的策略处理。

短窗口指的是最近 2 到 3 轮的完整对话,这部分不做任何压缩,原样保留。原因是近期对话往往包含用户正在进行的操作路径、刚给出的明确指令,任何压缩都可能损失关键意图。我们用“完整性优先”原则处理短窗口。

长记忆层负责处理更早的历史。这里不是简单给个摘要就完事,而是采用“事件摘要 + 关键信息抽取”双轨记录。事件摘要用来保留流程主线,比如“用户要求生成季度报告,确定使用模板 B”;关键信息抽取则负责保存那些可能在后续被引用的硬数据,比如用户指定的日期、金额、偏好方案。这两条记录是结构化存储,需要时再渲染成文本。

工作区保存的是当前任务的中间状态,包括工具调用结果、临时变量、用户已确认的信息。这一层只保留当前任务相关的上下文,任务一旦结束就清空,不进入长期记忆。这个设计解决了一个非常常见的问题:工具结果太长导致上下文爆炸。我们直接把“可丢弃的工具输出”和“必须保留的任务状态”分离,后者通常只占前者体积的十分之一。

2.2 工具调用的结果压缩

工具结果是上下文里的重灾区,单独拿出来处理。我们的方案是:每个工具调用完成后,结果不再原样进入上下文,而是先经过一个“结果提炼器”。

提炼器做两件事。第一是字段筛选,根据当前任务的 schema 判断哪些字段可能被后续步骤引用,输出的时候只保留这些字段。第二是格式转换,把嵌套 JSON 压成一行紧凑的结构化摘要。比如一个返回 200 行 JSON 的权限查询接口,最终进入上下文的可能是这样:

{"user": "u_1024", "resource": "report:Q3", "permission": "read_write"}

如果任务后续确实需要完整的工具原始结果,Context Engine 会把完整结果放到工作区的“可回溯存储”里,并不塞进 prompt。模型需要时可以通过一个专门的函数去取,但这属于显式请求,不再是默认全量加载。

这里有个经验:提炼器的规则不要写得太死,因为工具输出格式经常变。我们用“schema 模板 + 默认保留最近一次有效结果”的组合,schema 缺失的字段直接丢弃,但保留一份原始输出的引用路径,方便排查。

2.3 按需检索注入

传统 agent 的做法是把所有历史、所有知识一股脑塞进去。Context Engine 改成按需注入:先理解当前轮次的用户输入,找出其中最可能需要的记忆片段,再决定把哪些历史记录和知识段落加载到 prompt 中。

实现上我们做了一个混合检索,取用户输入里的核心实体和动作,通过关键词匹配和向量相似度同时召回长记忆层里的候选片段。召回之后不是直接拼进 prompt,而是先做一个相关度过滤,低于阈值的直接丢弃;再做一个冗余度检测,与当前窗口已有信息重叠度高的也丢弃。

这层设计的核心价值是“用检索替代拼接”。你可能觉得多了一次模型调用或索引查询的开销,但实测下来,这比省掉的几千 token 成本划算得多。

3. 关键实现细节与调参记录

3.1 摘要生成:两代方案演进的教训

第一代摘要,我们直接让模型用自然语言总结全部历史,每次会话结束时生成一段 summary。效果出来后马上就发现了问题:摘要粒度太粗,关键数字经常被吞。用户说“帮我查一下本月 15 号之后的数据”,如果摘要里只写了“用户要求查询本月数据”,后续轮次模型就无法确定具体日期。

第二代改成“结构化解构”,把摘要拆成固定的信息槽位。每个槽位都有明确含义,比如user_requirements、confirmed_facts、pending_tasks、tool_outputs_summary。模型只需按槽位填内容,不得随意发挥。这个改动直接把下游任务的准确率拉高了十几个百分点。

具体实现片段大致是这样:

def build_history_summary(history_events, task_state): sections = { "confirmed_facts": [], # 用户已确认的信息 "pending_tasks": [], # 未完成的工作 "decisions": [], # 关键决策与理由 "preferences": [], # 用户偏好 } for event in history_events: slot = slot_classifier(event) sections[slot].append(compact(event)) return render_sections(sections, max_tokens=800)

这里compact是一个抽取函数,负责过滤掉寒暄、重复确认、无信息量的状态汇报。它不需要调用大模型,直接用规则从事件对象里抽取结构化字段即可。和纯自然语言摘要相比,这种方式的优势是:信息密度高、可以按需部分加载、也不会出现模型自己“脑补”出不存在事实的风险。

3.2 短窗口里的去重与差分加载

短窗口虽然完整保留,但也会积累重复内容。最常见的情况是:用户连续几轮都在调整同一个条件,而每轮 prompt 都包含之前所有轮次的完整上下文,导致同一份状态信息被重复计费。

我们做了一个简单的差分检测:进入短窗口的每条消息会先算一个指纹,指纹相同或相似度极高的消息会被折叠。比如用户在第 3 轮说“改成红色主题”,第 4 轮说“还是改成蓝色吧”,短窗口里不需要同时保留这两个中间状态,只需要最终确定的“蓝色主题”。折叠基于启发式规则,不做语义判断,避免过度合并导致误伤。

实测中这部分的收益率相当高,因为我们 agent 的典型场景就是“用户反复调整配置项”,折叠之后短窗口的平均体积直接下降了 40%。

3.3 检索层的具体参数与效果

检索是 Context Engine 最依赖模型能力的地方,参数也调得最久。最终我们固定下来的参数供参考:

参数项设置值说明
召回方式BM25 + 向量检索混合兼顾关键词精确匹配与语义近似
召回数量top 8太多会引入噪声,太少会漏关键信息
向量模型维度768内部 embedding 模型的默认维度
相关度阈值0.62低于该值的片段直接丢弃
冗余检测方法与当前窗口重叠 token 比例超过 55% 的片段判定冗余
长记忆单次注入上限1300 token超出部分宁可放弃,保证窗口质量
短窗口完整保留轮次3 轮第 4 轮起进入摘要评估

混合检索的权重不是固定的,我们做了个自适应调节:当用户输入中包含明确的编号、日期、项目名等强标识信息时,BM25 权重自动上调;当输入是模糊描述、更多依赖语义理解时,向量检索权重上调。

这里的核心认知是:检索不是“找到最像的”,而是“找到当前决策真正需要的”。所以我们的评估指标不是 recall@k,而是“注入后的下游任务成功率”。按这个指标调出来的参数,比单纯看相似度分数靠谱得多。

3.4 上下文重建的完整流程

Context Engine 在每次 agent 发起模型调用前,会执行一次完整的上下文重建。流程是:

  1. 解析当前用户输入,提取意图、实体、涉及的资源类型。
  2. 从工作区加载当前任务状态,仅加载必要字段。
  3. 从短窗口取出最近 3 轮完整对话。
  4. 调用检索层,从长记忆里返回候选片段,过滤后注入。
  5. 汇总系统提示、工具描述、历史摘要、检索片段,合成最终 prompt。
  6. 最后做一次 token 预算检查,超出上限则按优先级丢弃“可回溯内容”,而不是截断历史。

第六步很关键。常规做法是超出长度就从头截断,但这会优先牺牲掉系统提示和工具描述——这类内容恰恰是不能丢的。我们的做法是维护一个“可丢弃优先级”列表,从低到高依次是:历史工具原始输出、长记忆检索片段、历史摘要、近期对话、系统提示与工具描述。永远先丢最不重要的内容。

4. 效果数据与问题排查

4.1 压测结果:71% 是怎么算出来的

我们以内网一个真实的运维工单处理 agent 为基准,选了 300 条真实会话回放作为压测集。对比接入 Context Engine 前后的 token 消耗,结果如下:

指标接入前接入后变化
单次调用平均 token 数127603698-71.0%
单轮对话平均 token 数4150011200-73.0%
平均响应延迟4.2 秒2.1 秒-50.0%
任务完成率91.2%93.8%+2.6%
单会话成本基础值约 29%-71.0%

需要说明的是,token 数准确计算用了我们自研的计费 tokenizer,和模型供应商的计费口径基本一致,误差在 1% 以内。

任务完成率不仅没有下降,反而涨了 2.6 个百分点。这印证了我们最初的观点:减少冗余 token 并不会损伤模型表现,反而因为上下文更聚焦,模型理解指令的准确度更高。这个结果对我们团队来说是最有说服力的。

4.2 上线过程中踩到的三个大坑

坑一:摘要压缩导致“隐性失忆”。最早期版本里模型会把“用户之前提过但尚未解决”的细节误当成已解决项,在摘要里顺手省略。结果在多轮任务中出现用户反复追问同一件事的情况。后来我们在摘要槽位里加了pending_tasks独立槽位,并明确要求该槽位宁可多列,不可漏掉,情况才好转。

坑二:指纹去重误伤重要变化。有一版去重阈值调得太激进,把“从百度搜索切换到必应搜索”识别成与“从百度搜索切换到谷歌搜索”高度相似,于是折叠掉了。但用户其实是在对比不同搜索引擎的结果差异,这个变化恰恰是任务核心。后来我们改为只对配置类消息做折叠,对涉及操作对象切换的消息强制保留,才解决问题。

坑三:长记忆检索延迟被低估。我们最初把检索服务部署在独立容器里,每次召回平均要 80 到 150 毫秒。这个延迟在单轮调用里看着不高,但 agent 经常连续调用多次模型,累积起来体感明显。后来我们把检索索引直接放进 Context Engine 的内存缓存,并且加了 RRD 级缓存策略,同一 session 的重复查询直接走缓存,检索延迟降到 15 毫秒以内。

4.3 常见问题排查速查表

按惯例,把上线后运维最常碰到的问题整理成速查表,供你排查时对照。

现象可能原因处理办法
摘要摘要后用户关键信息丢失槽位分类器误判检查 slot_classifier 对“数字 + 日期 + 资源名”的识别正则
模型总忽略检索片段检索片段注入位置太靠后把检索片段放在历史摘要前,紧跟系统提示后
短窗口折叠后上下文漂移折叠阈值过严断开对操作类消息的折叠,仅允许配置类折叠
工具结果字段缺失schema 模板更新滞后建立工具结果字段的自动差异检测,及时更新模板
长任务执行到一半状态丢失工作区被误清理增加状态持久化标记,任务未结束不得清理工作区
检索召回的片段与当前问题无关向量模型对领域术语不敏感扩充领域专用词典,同时提高 BM25 权重

排查还有一条经验:不要只看 prompt 最终长度,要看模型实际“真正用到”的内容比例。我们给自己加了一个审计工具,随机抽取 5% 的请求,把 prompt 里被模型最终产出的逻辑链路有引用关系的片段标记出来,算一个“有效 token 占比”。这个指标一开始只有 35%,优化后提升到 72% 左右。它比总 token 数更能反映上下文组织做得好不好。

4.4 针对长会话的分支历史处理

很多 agent 会话不是线性的,用户可能中间突然切换了目标,然后又切回来。线性摘要很难处理这种分支状态。

我们的方法是在长记忆层保存“事件节点图”,每个节点是一个完整步骤,节点之间保留父指针。当新一轮用户输入命中某个节点的关键信息时,检索层会把该节点及其父路径一起召回。这个设计牺牲了一点存储成本,但极大增强了多任务交替场景的上下文恢复能力。

分支历史里还有一类特殊情况:用户主动撤回某个操作。这类撤回信息在摘要里几乎无法自然表达,所以我们单独立了一个revocation_queue,记录“哪些操作被撤销、撤销前状态是什么”。在重建上下文时,这个队列会以“已知撤回信息”的形式单独注入 prompt,避免模型在后续轮次里把已撤销的操作当成有效状态。

5. 工具选型与基础环境参考

整个 Context Engine 我们只用一个轻量的内存数据库存摘要和索引,没有引入额外的重量级存储组件。消息队列、任务调度这些也都是复用现有平台能力。

如果你打算在类似场景里复刻这套方案,工具选型上给一个参考:检索部分直接用内部自带的向量库就行,不需要单独搭建 Elasticsearch;摘要生成直接调用大模型,一个对话摘要任务用中低档模型就能完成,不必烧更贵的模型;状态存储用 Redis 这类带过期机制的 KV 存储就够了,注意给不同 session 设置合理的过期时间。

需要留意的是,这套系统并不依赖某个特定模型品牌,核心是可插拔设计。我们把模型调用封装成一个接口,底层换成哪个模型都行,不影响 Context Engine 的工作。好处是将来有更便宜或更智能的模型时,无缝切换。

6. 后续可以继续扩展的三个方向

Context Engine 上线之后,我们已经在规划几条更进一步的优化。

一条是动态预算分配。现在每个上下文部分都有固定预算,但实际任务难度差异很大。简单的查询类任务可以压缩到几百 token,复杂任务需要几万 token。我们打算做一个基于任务复杂度的动态预算分配器,让简单任务更快、复杂任务不丢信息。

另一条是跨 session 的条件记忆复用。目前长记忆只服务单条会话,但同一用户的多条会话之间往往有可复用的偏好和结论。未来计划把用户的长期偏好抽离出来,做成独立于会话的用户级记忆层,这样新会话一开始就能继承已有的关键信息,不需要从零累积。

第三条是把摘要生成进一步做成增量式。当前每轮 token 压缩会触发一次摘要刷新,频繁时也是不小的开销。计划改成只在事件流出现明显转折时触发刷新,其他轮次直接更新结构化槽位,省掉一部分模型调用。

做这个项目最深的体会是:token 优化本质上是信息管理问题,不是算术题。很多人一上来就问“用什么缓存、用什么压缩算法”,但真正值得先想清楚的,是你的上下文里哪些信息有长期价值、哪些信息只是临时中间态,以及模型在每一轮决策时到底需要看到什么。把这些理清楚,压掉 70% 的 token 是水到渠成的事。

如果你正在被 agent 的内存问题困扰,建议先别急着抄任何代码,花两天时间把自己的 prompt 历史打出来,把每一个 token 的用途标上类别统计一遍。看完那两张统计表,你自然就知道该从哪里切第一刀了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 11:55:00

网络安全设计实战:从风险分析到落地验证的完整指南

"网安设计"这四个字拆开看,网安是网络安全,设计是方案设计。但把两个词拼在一起,很多刚入行的朋友反而懵了:这到底是要写一份文档,还是搭一套架构?是画拓扑图,还是填合规检查表&#…

作者头像 李华
网站建设 2026/10/17 11:54:51

Spring事务传播行为详解:7种策略与回滚机制实战避坑

先说个真实事故。某天晚上,线上支付回调突然大量重试,原因是订单状态被更新成了“已支付”,但账户余额却一分没扣。查到最后,业务代码里一个Service方法成功提交了订单状态,另一个Service的扣款方法抛了异常&#xff0…

作者头像 李华
网站建设 2026/10/17 11:54:47

GIS大赛上午场空间分析综合题全流程实操指南

全国大学生GIS应用技能大赛办到第八届,矩阵题型的核心其实越来越清楚了:上午场的基本功好不好,几乎直接决定全天心态。我常跟学弟学妹讲,上午那三小时不是拼谁会的高级功能多,而是拼谁能在压力状态下,把Arc…

作者头像 李华
网站建设 2026/10/17 11:54:43

单卡5090跑125B大模型:MoE显存优化与Strata实战

这段时间在硬件圈和大模型圈子里,"单卡 5090 跑 125B 大模型"这件事被反复刷屏。第一次看到 Strata 这个名字的时候,我也跟大多数人一样先算了一笔账:125B 参数,就算用 4-bit 量化,光权重也得 60 多 GB&…

作者头像 李华
网站建设 2026/10/17 11:53:23

AgentGym-RL实战:从零搭建LLM智能体多轮强化学习训练流水线

AgentGym-RL 这个名字听起来像个“健身房”,实际上它就是把 LLM 智能体的训练过程搬进了一个可交互、可反馈、可无限重来的环境里。我过去大半年一直在折腾这类事:把一个能写代码、能查资料、能操作工具的 LLM 智能体,从“会回答问题”练到“…

作者头像 李华