在本文中,我们将简要讨论 Agent 系统 input token 失控的问题与治理方法。对于一个 Agent 系统,缺乏有效的 token 治理会导致成本飙升、模型注意力稀释等问题,因此,合理的治理可以给系统带来极大的增益。
问题背景
Anthropic 在 Building Effective Agents 中把 Agent 定义为模型自主驱动的决策过程,即执行过程中每一步都要从环境拿到 ground truth(工具调用结果或代码执行输出)来评估进展,并需要停止条件(最大迭代次数之类)维持控制,这就是当前主流Agent Loop 的本质。而更加具体一些,譬如 Lilian Weng 总结的 ReAct 范式,给出了 推理(thought)→ 行动(action)→ 观察(observation) 的循环结构。
因此,Agent 系统与单次 LLM 调用不同,它在一个循环里反复向模型发起请求:识别意图、调用工具、观察结果、再决定下一步。每一次请求都不是孤立的,系统提示词、工具定义、历史对话、记忆检索结果和工具返回的 observation 都会随请求一并发送,作为模型决策的依据。任务越复杂,循环轮次越多,请求承载的内容就越重。以一个涉及文件读取、代码执行和多轮修正的任务为例,可能在多轮以后,历史与 observation 已经占据请求的主体,而真正影响当前决策的信息只占一小部分。
这种膨胀并非某一条 prompt 设计不当所致,而是多类上下文来源叠加累积的结果。工具规模随注册数量增长,历史随对话轮次增长,工具返回随数据规模增长,每一类来源都有独立的增长动力,共同推高输入规模。若各模块均可向请求追加内容而缺乏统一约束,input token 便随任务推进持续膨胀,成本、延迟和模型注意力随之恶化。
在前段时间测试 OpenGIS 项目(指路文章《Agent驱动的GIS客户端OpenGIS:harness的架构升级》)时,发现 OpenGIS 每次任务耗费 token 惊人的高而 cache 命中率惊人的低,譬如一个简单的地图操控任务,其输入 token 可达约50k,但 cache 命中率仅约 10%-20%,这回导致使用成本相当昂贵!分析下来,我总结出下面三个问题:
- 架构差;context构成不稳定,没有实现尽量大范围的稳定前缀
- tool 过多;OpenGIS 由于包含了基础的 tool 与大量地图操作 tool,以及一些面向特殊内置功能的tool,一次性暴露所有tool以及相关prompt会导致使用大量token
- 任务面广;相对于Code、OpenCode等面向code的agent,OpenGIS多出包含大量地图相关操作,这导致任务面更广更难管理。
其中,第三点显然是设计意图导致的必有缺陷,因此,我们需要考虑的是升级架构、优化暴露面以降低使用成本,最终的结果是,OpenGIS的简单地图操控任务可以压缩到10-20k,cache 命中率不同的任务场景浮动很大,但是在30%-70%的范围内浮动,整体使用成本降低至了原先的 1/5 甚至更多,但是仍然和主流 Agent 存在差异,还在继续优化。
本文是对优化工作一些内容的总结凝练,以及对主流相关工作的整理。闲言少叙,我们先来了解一些前置知识。在前文中,我们提到了请求承载的内容膨胀的问题,那么要优化这个问题,必须了解其构成,通常意义上一次 provider request 里的输入可以拆成:
对应到工程对象,就是:
我们必须首先明确一个事实:模型看到的上下文是系统组装出来的产物。如果每个模块都可以往 request 里追加内容,input token 会随着任务轮次、工具数量、文件大小和日志长度一起增长。因此,我们的优化对象不应该只是 prompt 文字,而是更加总体的对象: provider request 编排。
从 Prompt Engineering 转向 Context Engineering
我们需要知道,Prompt Engineering 与 Context Engineering 是两个层次的问题,其中 Prompt 是发送给模型的一段静态文本,Prompt Engineering 处理的是这段文本本身,包括措辞、示例、指令结构等。Context 则是模型在每一轮实际看到的所有内容,包括系统指令、工具定义、历史对话、检索结果和工具返回。
Anthropic 在 2025 年的工程实践中给出了更完整的定义:Context Engineering 是从不断演化的可能信息集合中,策展出进入有限上下文窗口的内容。与写 prompt 的一次性任务不同,策展发生在每一次决定传给模型什么的时候,是循环中反复执行的动作。
传统 Agent 实现倾向于 Full Context,即每轮都把系统指令、全部工具、完整历史、所有检索文档和完整工具结果塞进请求,这种方式在任务稍长的背景下就会暴露三个问题:
- 成本。请求规模随轮次线性增长,一个多轮任务(譬如10轮以上),历史与工具返回的重复传输很有可能占掉大部分 token,成本随任务长度持续攀升。
- 注意力稀释。模型的注意力预算有限,每个新 token 都会消耗预算,上下文越长边际收益越低。Liu 等人2023年的研究认为,模型对长上下文中间位置信息的利用显著弱于开头和结尾。把低价值内容堆进上下文不仅会浪费 token,还可能会主动恶化模型对关键信息的提取。
- 缓存失效。Prompt caching 依赖稳定前缀,任何每轮变化的内容插进前缀都会让缓存整体失效,可以说,动态内容越多,命中率越低,成本优化空间越小。
Context Engineering 的目标是把 Agent 从 Full Context 变成 Context-on-Demand,对于后者来说,每次请求的流程可以依次加入:
+ 证据指针
从工程实现看,上下文不是静态 prompt,而是每轮根据任务目标、工具状态、历史依赖、检索结果和预算约束生成的 request。这意味着上下文治理不能只靠写好一段 prompt,而要把组装动作本身做成系统的一部分,在介绍完总体的内容后,我们将在下面的章节中介绍具体方法。
构成一:Token Budget Manager
要让 Context-on-Demand 真正落地,系统需要一个显式的 Token Budget Manager。它负责为每一类上下文分配预算上限(譬如分配系统提示词、工具 schema、历史对话、检索内容这些部分各自给多少预算),并在请求组装过程中执行这些约束,请求组装过程中的每一步取舍都由这个管理器来裁决。
一个常见的请求组装流程是:
在组装之前,每一步都受预算约束,譬如说下表展示的预算比例(当然,这只是一个例子,具体的构成与比例还需要按照需求定夺):
| 上下文部分 | 建议预算 |
|---|---|
| System Prompt | 5% 到 10% |
| Tool Schema | 5% 到 15% |
| 当前用户任务 | 5% |
| 结构化任务状态 | 10% 到 20% |
| RAG 检索内容 | 30% 到 50% |
| 工具 observation | 10% 到 20% |
| 输出空间 | 单独预留 |
这些比例不是固定配置,而是 admission control(准入控制)的输入。
以一个 RAG 任务为例:系统按照用户提问召回 50 篇文档,压缩后准备注入 top-5,但此时预算只够满足 top-3,同时这一轮的历史对话和工具返回也超了。这时候压缩就要按优先级进行:
- 将工具返回替换为摘要与证据指针
- 检索内容从 top-5 减少 top-3
- 历史只保留最近几轮和任务状态摘要
构成二:Agent 上下文压缩
在长任务中,历史对话、工具返回、中间轨迹都会持续累积,上下文不可能无限增长,所以压缩是绕不开的环节。但我们需要明确一点:压缩不能只按时间截断,不能简单粗暴地"只保留最近 N 轮"。原因在于 Agent context 的信息价值不由时间顺序决定,而由任务依赖关系决定。
基于这个原则,需要保留的信息通常包括:
- 当前目标
- 用户明确约束
- 已确认事实
- 最新计划
- 当前阻塞点
- 关键证据
- 高风险决策依据
可以压缩或删除的信息通常包括:
- 失败的无关尝试
- 重复 observation
- 长篇工具日志
- 过时计划
- 已经写入结构化状态的历史对话
确定了保留什么、删除什么之后,接下来是怎么压缩的问题。常用的手段有四种,各自适合不同的内容:
- 摘要。把一段历史交给 LLM 压成几句话。需要注意的是摘要最好带上上一轮的摘要一起合并(锚定合并),否则多轮压缩之后摘要会越滚越大,或者丢失早期信息。
- 截断。直接删除超龄消息。实现简单,但风险是可能删掉后面还在引用的内容,比如一个工具结果被删了,模型后续却还在讨论它。
- 结构化投影。把自然语言对话转成结构化 task state,比如当前目标、已确认事实、约束、阻塞点。这个手段信息密度最高,后面章节会专门展开。
- 占位符替换。旧工具结果只保留元信息(哪个工具、调用 id、时间戳),正文删除,模型需要时再通过 pointer 读取原始内容。
实际系统中这几种手段通常组合使用,譬如对历史对话采用摘要加结构化,对工具结果采用占位符,将日志直接截断。
压缩的时机也需要考虑。比较好的做法是触发式压缩,即当 token 估算超过某个阈值,或者工具结果在上下文中占比过高时,才执行压缩。压缩时还需要保护最近的一小段消息,它们是当前决策的直接上下文,不应该被压掉。
构成三:工具按需注入
工具 schema 是 Agent 系统里最容易被低估的 token 来源。工具数量上来以后,每轮注入几十个甚至上百个完整 schema,会让简单任务也背上很重的上下文。
但"按需注入"不能理解成每轮都去检索一次工具,更常见的形态是把工具分成三层:
- 核心工具:永远注入。文件读写、代码执行这类基础能力几乎所有任务都用得上,不值得为它们做检索。
- 领域工具包:按任务类型成组注入。地图操作、栅格处理、样式调整这类能力,一个任务通常只涉及其中一组。
- 扩展工具:按需读取。低频、重型或特殊能力,schema 不常驻,模型需要时通过 tool docs 或 skill 读取详细说明。
按需注入有两种实现路线,适用场景不同:
- 确定性路由:先做任务识别,再按规则或关键词选中对应的工具包。好处是可预测、稳定,工具集合的构成每轮都是确定的;代价是依赖任务分类的质量,识别错了工具就带错。
- 语义检索:把工具描述向量化,用 embedding 检索当前任务相关的工具。好处是灵活,能覆盖规则想不到的组合;代价是每次请求多一次检索开销,而且检索结果有随机性,工具集合不稳定。
一个常见的流程是:
这里有一个容易被忽略的权衡:按需注入的目的是减少 schema 数量,但工具集合如果每轮都在变,prompt 前缀的缓存就会频繁失效。所以注入的粒度很关键。按工具包粒度路由,集合粗、变化少,缓存稳定;按单个工具粒度检索,集合细、每轮都可能不同,缓存命中率会受影响。因此,还需要在实践中进行调整。
工具 schema 本身也需要压缩。schema 只描述接口,不承载业务文档。推荐形态:
1 | { |
当然,在 schema 里放大量背景、示例、异常分支和业务说明是不合适的。这些内容应该移到工具内部、工具文档、skill 或 operation contract 中。模型需要时再读取,不需要时不进入 request。
构成四:历史对话转结构化状态
在多轮对话中,如果每轮都把全部 messages 发给模型,token 会线性增长。因此,替代方案是把历史对话投影成结构化 task state,比如下面这样:
1 | { |
一段多轮自然语言对话压缩成这样的结构通常只要几百 token,却也保留了决策所需的事实和约束。为了做到这一点,我们需要一个投影机制,常用的做法有两种:
- 增量更新:保留上一轮的 task state,让 LLM 只读本轮新增的对话,更新 goal、confirmed_facts、constraints 这些字段。这样做会大大降低成本,且历史不会重复处理,代价是状态可能会导致缓慢漂移,比如某个早期事实被错误覆盖。
- 全量重建:每轮把全部历史重新过一遍,重新生成 task state。准确,但每次都付出重读历史的成本,省下的 token 又花回去了。
在实践中,我们更倾向增量更新,这和前面讲的锚定合并(摘要带上上一轮摘要)是同一个思路,即旧状态是基底,新对话只做增量修补。并且在结构化之后,最近几轮应该保留原文,因为模型经常需要看细节,比如用户的原话、具体的代码片段;更早的轮次才转成 task state。
不可避免的,task state 必然会丢信息。用户原话的精确措辞("不要用 X 方法"和"尽量避免 X"语义不同)、长代码片段、日志这类内容都不适合塞进结构化状态。所以当状态里出现矛盾或者模型对某个事实不确定时,应该需要回退到原始对话。
一个较完整的记忆分层可以是:
| 层 | 内容 | 是否每轮注入 |
|---|---|---|
| Working Memory | 当前任务目标、约束、阻塞点、最近动作 | 是 |
| Episodic Memory | 历史任务经验、失败模式、修复记录 | 按需检索 |
| Semantic Memory | 长期知识、项目事实、数据集说明 | 按需检索 |
| Procedural Memory | 可复用流程、工具用法、操作规范 | 按需读取 |
| Artifact Store | 文件、日志、图片、代码输出、大数据 | 只传引用 |
这样,「完整历史」可以被拆成多个可检索、可替换、可裁剪的对象。
构成五:Observation Compression
工具返回结果是另一个 token 黑洞。例如代码搜索返回多个文件,而每个文件可能存在几百行,如果全部进入上下文无疑会大大增加模型的压力与噪声。因此工具结果也需要压缩,但压缩发生在哪一层,决定了整个系统的形态。
相比工具侧压缩,现阶段在实践中更推荐的做法是运行时统一处理,即工具照常返回完整结果,由统一的 observation normalizer 转成标准结构,比如:
1 | { |
这个结构中的字段可以包括:
- summary:给模型看的结论
- evidence:是支撑结论的关键证据(文件、行号、片段)
- next_step:提示下一步动作。
- 完整内容落盘,模型需要时再通过 artifact id、文件路径、行号读取。
压缩的时机有两层。第一层是工具结果进入上下文之前,先压成 summary 加 evidence;第二层是进入上下文之后,按预算触发式再次压缩,旧的 observation 退化成占位符,只保留工具名和调用 id,对应前文所说的触发式压缩。
构成六:如果你需要 RAG 检索,那请多召回少注入
RAG Agent 容易把 top-n chunk 直接送进模型,但检索是廉价的,注入是昂贵的。召回阶段多取几篇文档几乎不花什么代价,但多注入多注入的 chunk 会占用模型的注意力,因此,正确的关系应该是召回阶段尽量多,注入阶段尽量少。
一个典型的流程是:
Decomposition
Top-20 / Top-50
Top-3 / Top-5
Compression
中间的每一步都有它的作用:
- Query Rewrite 把模糊的用户问题改写成适合检索的形式
- Rerank 把召回结果按相关性重排
- Chunk Compression 把选中的 chunk 压成摘要
最终进入 request 的不是文档堆,而是压缩后的证据对象:
1 | { |
每个证据对象都带 source 引用,模型引用时有据可查。这样既降低了 token,也提高了可追溯性。
构成七:Prompt Compression 与 Context Compression 模型
Prompt Compression 可以用于历史对话、长文档、工具 observation、日志、搜索结果和 Agent 轨迹。它的目标不是把句子统一缩短,而是删除低信息量 token,保留对决策有意义的实体和约束。
适合保留的内容:
- 文件路径
- 函数名
- 类名
- 字段名
- 错误码
- 数值
- 配置 ID
- 用户硬性约束
- 安全限制
适合压缩的内容:
- 解释性长句
- 重复日志
- 已经验证失败的路径
- 中间推理过程
- 非关键输出样本
压缩前:
1 | 这个函数主要用于在玩家点击攻击按钮之后,根据当前角色状态、技能冷却状态、资源消耗情况以及目标选择结果,最终决定是否可以释放技能。 |
压缩后:
1 | 函数:判断玩家点击后是否可释放技能。 |
可以注意到,压缩后保留的全是实体(函数、依赖项),删掉的都是修饰和解释。执行压缩的方式有多种,譬如规则提取实体,或者使用专门的压缩模型,抑或直接让主 LLM 完成,三者成本和质量不同,需要开发者按场景选择。
构成八:Agent 轨迹压缩
ReAct 类 Agent 每一步都会产生一组:
- Thought
- Action
- Observation
- Thought
- Action
- Observation
长任务里,这样的三元组会累积几十上百组,是上下文里膨胀最快的部分之一。轨迹的保留策略需要分级:
- 最近 N 步保留原始轨迹
- 更早轨迹压缩为任务状态
- 失败尝试只保留结论
- 关键证据保留引用
例如,一段十几步的排查过程可以压缩成:
1 | { |
discarded_paths 是这段压缩的关键,它把模型"查过但无关"的路径也记录下来,模型后续不会重复排查。这类压缩比删除旧消息更可靠,因为它保留了当前决策仍然依赖的证据,也保留了已经排除的可能。
构成九:大文件使用 Artifact Pointer
代码、日志、PDF、表格、图片和地理数据不应该直接进入 prompt,它们应该作为 artifact 存储,上下文里只放必要摘要和引用:
1 | { |
指针里应当包含两个内容:
- 摘要(模型判断是否需要看细节的依据)
- 定位信息(文件、时间范围、相关行号)
模型需要细节时再发起读取:
1 | read_artifact_lines(log_2026_06_08_001, 1200, 1230) |
大部分轮次模型只需要摘要,只有少数时候需要精读,这样可以按需支付精读的代价。当然,指针同样存在生命周期问题,artifact 落盘后需要约定保留策略,任务结束或不再被引用时清理,否则存储会无限增长。
构成十:多 Agent 只传结构化摘要
多 Agent 系统容易发生 token 乘法级别的增长,试想我们激活了 N 个子 Agent,如果每个都拿到完整上下文,再把完整过程传回主 Agent,上下文不是线性增长,而是乘数增长。实际上,每个子 Agent 的工作过程对主 Agent 并没有价值,主 Agent 只需要结论和关键依据。
更合适的通信格式是:
1 | { |
也就是每种角色只传自己的产出物:
构成十一:模型分层
减少 input token 还可以通过模型分层实现,把工作按难度拆给不同规模的模型:
1 | 小模型 / 规则系统: |
思路是大模型不需要看到所有原始输入,只接收筛选和压缩后的高价值上下文,前置的小模型承担 routing、summarization、filtering、guard 这类低风险任务。
需要说明的是,模型分层不是免费的,即多一层调用就多一次延迟,小模型的选择质量可能影响下游,两个模型的输出一致性也需要维护。它的适用场景是高频、低风险、判定规则明确的任务,比如意图识别和文档初筛;复杂推理和最终决策仍然留给大模型。
构成十二:Prompt Caching 的边界
Prompt Caching 通常降低的是重复前缀的计算成本和延迟,不一定减少发送给 provider 的 input token。它的收益来自前缀复用,所以只对稳定内容有效:
- 稳定 system prompt
- 稳定工具 schema 顺序
- 固定公共规则前缀
- 动态内容后置
也就是说,缓存的前提是前缀稳定。如果每轮动态内容插在前缀里,缓存频繁失效,收益就很小。但缓存不能替代上下文治理:即使缓存命中率很高,每轮仍然发送长 system prompt、长工具 schema、长历史和长 observation,系统结构依然臃肿,只是重复部分便宜了一些。
两者的分工可以这样区分:
1 | Prompt Caching 解决重复前缀的计算成本。 |
构成十三:可观测性与评测
Input token 治理必须有观测面,建议至少记录这些指标:
- 每轮 input tokens、output tokens
- system / tools / history / memory / retrieval / observation 分项 token
- 工具 schema 数量与 schema token
- 最大的若干消息
- cache hit tokens、miss tokens、provider 是否上报 cache 字段
- tool observation 被压缩前后的大小
- artifact pointer 数量
- auto-final 命中次数
- request pressure:ok / warm / hot / overflow
更进一步,可以建立固定的 regression case,把典型任务变成可比较的基准:
| Case | 目标 |
|---|---|
| 简单地图缩放 | 验证不因全工具和长历史膨胀 |
| 图层样式修改 | 验证 style 工具是否按需暴露 |
| CSV 加载 | 验证数据样本和字段摘要是否有界 |
| Operation 修复 | 验证脚本复用和失败轨迹压缩 |
| Worker 启停 | 验证后台日志不会污染 chat context |
| RAG 文档问答 | 验证 retrieve many, feed few |
每个 case 记录 total tokens、schema tokens、history tokens、observation tokens、cache hit、LLM call count 等指标,以帮助我们进行量化观测。
参考文章
[1] Anthropic. Building Effective Agents[EB/OL]. (2024-12-19). https://www.anthropic.com/research/building-effective-agents.
[2] Weng L. LLM Powered Autonomous Agents[EB/OL]. (2023-06-23). https://lilianweng.github.io/posts/2023-06-23-agent/.
[3] Anthropic. Effective Context Engineering for AI Agents[EB/OL]. (2025-09-29). https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents.
[4] Liu N F, Lin K, Hewitt J, et al. Lost in the Middle: How Language Models Use Long Contexts[J]. Transactions of the Association for Computational Linguistics, 2024, 12: 157-173. https://arxiv.org/abs/2307.03172.
[5] Yao S, Zhao J, Yu D, et al. ReAct: Synergizing Reasoning and Acting in Language Models[C]. ICLR 2023. https://arxiv.org/abs/2210.03629.
[6] Anthropic. Contextual Retrieval[EB/OL]. (2024-09-19). https://www.anthropic.com/engineering/contextual-retrieval.
[7] Anthropic. Prompt Caching[EB/OL]. (2024-08-07). https://www.anthropic.com/news/prompt-caching.
[8] Jiang H, Wu Q, Lin C, et al. LLMLingua: Compressing Prompts for Accelerated Inference of Large Language Models[C]. EMNLP 2023. https://arxiv.org/abs/2310.05736.
[9] Packer C, Wooders S, Lin K, et al. MemGPT: Towards LLMs as Operating Systems[EB/OL]. (2023-10). https://arxiv.org/abs/2310.08560.
[10] OpenAI. A Practical Guide to Building Agents[EB/OL]. (2024-12). https://openai.com/index/a-practical-guide-to-building-agents/.
[11] Anthropic. Claude Code Best Practices[EB/OL]. (2025-02-21). https://www.anthropic.com/engineering/claude-code-best-practices.