恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Context Engine:如何将Agent的Token消耗降低71%?
首页
资讯中心
/
Context Engine:如何将Agent的Token消耗降低71%?
Context Engine:如何将Agent的Token消耗降低71%?
发布时间:2026/10/17 11:55:05
做一个能把 agent 的 token 使用量压下去 71% 的中间层听起来像是某种黑魔法但真拆开看我们只是老老实实回答了三个问题每一轮调用 token 到底花在了哪、哪些 token 是必须花的、哪些是花了钱但根本没用的。把这三个问题搞清楚Context Engine 的方案基本就自己长出来了。写这篇东西的起因是我们在做 agent 流程优化时发现一个很尴尬的现象——上下文越长agent 越“笨”响应越慢账单还越贵。当时我们内部有个多轮任务型 agent平均每轮对话要吃掉一万多个 token其中相当一部分是重复的系统提示、过期的工具返回、以及永远不会再被用到的历史中间结果。单纯靠提示词压缩已经救不回来必须换一种组织上下文的思路。所以才有了 Context Engine 这个项目。如果你也在被 token 成本和上下文长度折磨这篇文章会把我们的设计思路、实现细节、参数调优和踩坑记录完整拆给你看你可以直接拿去做参考。1. 为什么需要 Context Engine1.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_tokens800)这里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 权重自动上调当输入是模糊描述、更多依赖语义理解时向量检索权重上调。这里的核心认知是检索不是“找到最像的”而是“找到当前决策真正需要的”。所以我们的评估指标不是 recallk而是“注入后的下游任务成功率”。按这个指标调出来的参数比单纯看相似度分数靠谱得多。3.4 上下文重建的完整流程Context Engine 在每次 agent 发起模型调用前会执行一次完整的上下文重建。流程是解析当前用户输入提取意图、实体、涉及的资源类型。从工作区加载当前任务状态仅加载必要字段。从短窗口取出最近 3 轮完整对话。调用检索层从长记忆里返回候选片段过滤后注入。汇总系统提示、工具描述、历史摘要、检索片段合成最终 prompt。最后做一次 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 的用途标上类别统计一遍。看完那两张统计表你自然就知道该从哪里切第一刀了。