做AI Agent应用的人,迟早都会撞上同一个墙:token账单。我见过太多项目,Demo阶段跑得欢,一上真实业务,上下文越积越长,一次任务烧掉几十万token,成本直接吃掉利润。更头疼的是,你以为省token就是简单截断历史,结果模型丢了关键决策依据,开始反复问同样的问题、重复执行同样的步骤,整体花费反而翻倍——这是典型的负优化。今天要聊的这套SparseRead思路,我自己的项目里从最初的暴力截断、全量摘要一路迭代过来,最后做到了稳定的token开销下降90%以上,峰值甚至到过92%,同时任务完成质量不降反升。这篇文章会把整个演进过程、核心设计逻辑、以及落地时踩过的坑一次讲清楚,适合正在做Agent、对上下文成本和任务稳定性有要求的朋友参考。
1. 先算一笔账:Agent的token到底烧在哪里
1.1 对话历史随任务进度线性膨胀
大模型Agent本质上是多轮对话驱动的执行器。每执行一个步骤,模型都要把整段对话历史重新读一遍:
- 系统提示词(system prompt),通常包含角色设定、任务规则、输出格式约束;
- 工具定义(tools),JSON Schema或函数签名,一般几千token;
- 过往所有消息(user / assistant / tool消息);
- 最近的工具返回结果(这是大头)。
假设一次真实业务任务需要5轮工具调用,每轮的输入输出加工具返回大约1.5万token,那么光工具历史就积累了7.5万token。加上系统提示和工具定义,单次任务模型的输入token大概在8到10万级别。而实际上,真实Agent任务的工具调用次数往往更多,涉及的信息检索、数据库查询、文件读写都会产生长文本返回,经常一轮就能返回2到3万token。累计下来,一次普通任务突破20万输入token是常事。
更关键的是,对话历史的膨胀不是线性的,而是近似二次增长。因为每轮新增内容都会叠加到后续每一轮的输入中。一个执行10步的任务,总输入token接近每轮token之和乘以轮次的一半。任务越复杂、步骤越多,浪费越严重。很多Agent在长任务跑到后半程时,单次调用的输入token已经非常可观,而真正对当前决策有用的信息可能只占其中一小部分,大量历史细节早已被模型遗忘机制自然忽略,但费用一分不少。
1.2 工具返回结果是真正的隐形杀手
很多开发者优化token时盯着对话轮次和系统提示,却忽略了最大的token黑洞:工具返回结果。
举例来说,一个CRM场景的Agent需要查客户资料。工具返回往往是一份完整JSON,包含客户基本信息、历史订单、工单记录、沟通日志等几十个字段,一个客户的完整数据轻松超过5000 token。如果Agent要分析这个客户的多个维度,会反复调同样的接口拿全量数据,每一轮都要把这几千token塞进上下文。再比如代码库场景,Agent需要读取文件内容,一个中型文件的全文动不动就上万token,再加上其他相关文件,几轮下来上下文就爆了。
我在某公司的一个实际项目中,统计过一次任务各来源token占比:系统提示与工具定义合计约3%,对话历史约12%,工具返回结果接近85%。也就是说,每花100块钱的token费用,大约85块烧在工具返回值上。如果只优化其他部分,效果天花板非常低。这也是为什么很多团队的省token方案做着做着就发现没效果,因为他们砍的是3%的部分,忽略了85%的部分。
1.3 简单截断为什么是负优化
既然工具返回是最大的开销,很多人第一反应自然是“截断”。截断确实能减少即时token消耗,但如果只是简单地把历史消息按长度截掉,会引入一个非常隐蔽的问题:模型失去了前文决策依据。
举个真实例子。某项目的Agent要完成“根据用户需求推荐套餐并生成订单”的任务。流程需要多轮询问用户偏好、查库存、对比价格、确认订单。如果不加处理直接截断历史,模型在第4轮时可能已经不记得用户最初选了A方案,于是重新向用户确认偏好,甚至生成一份与用户需求冲突的新方案。用户被反复追问,Agent反复自我纠错,来回多花了好几轮。最终的token费用不仅没降,反而比不截断时更高,而且用户体验极差。
这就是标题里说的“负优化”阶段。单纯减少单次调用的token,和真正降低整个任务的总token,是两回事。前者看着数字漂亮,后者才真正影响成本和体验。真正的优化,必须在保证模型能够按需获取历史关键信息的前提下,压缩上下文中冗余的部分。
2. SparseRead 的核心思路:用“稀疏表达”替代“全量存储”
2.1 从“把历史全塞进去”到“按需取用”的思维转变
传统的Agent上下文管理,思维上默认了一个前提:为了让模型了解上下文,所有相关信息都必须出现在输入中。这个前提在短任务里成立,但在长任务里会带来巨大浪费。
SparseRead的核心转变是:不再追求“全量存储”,而是把历史信息转成一种稀疏的、高密度的索引表达,只在模型真正需要某段细节时,再把对应内容以额外的工具调用方式加载回上下文。
打个比方。以前的做法是每次开会都让所有人把公司过去一年的所有文档从头到尾读一遍,确保讨论时大家什么都知道。SparseRead的做法是办公室里放一份目录,平时讨论只带摘要页,谁需要看某份文档的细节,再去档案柜取原件,看完归档。从存储和传输的角度看,后者显然高效得多。
在Agent里,这个“目录”就是结构化索引,“摘要页”就是保留的精炼信息,“档案柜取原件”就是按需加载的工具调用。模型在每一轮决策时,只需要看到当前步骤真正需要的最小信息集合;一旦发现缺少必要细节,它可以主动发起一次“回捞”动作,把对应原始内容拉进上下文,用完即弃。
2.2 关键实现:怎么判断哪些内容值得保留
稀疏表达的关键问题之一,是如何判断哪些信息需要保留在上下文中,哪些可以下沉到外部索引。我用下来最稳妥的策略是三层筛选:
- 语义重要性维度:与当前任务目标直接相关的信息必须保留。例如任务目标是“对比三款套餐的性价比”,那么三款套餐的价格、流量、通话时长等关键字段属于高优先级,而一些状态字段、内部ID、时间戳属于低优先级。
- 时效性维度:近期产生的信息通常比早期的信息更重要。但这不是绝对的,如果早期信息是用户明确的约束条件(比如“预算不能超过100元”),那么即便它出现在第1轮,也必须保留到最后一轮。
- 结构关键性维度:某些信息本身是逻辑链条中的关键节点,比如“用户已经确认A方案”“库存检查已通过”“支付接口返回成功”。这类状态信息适合单独抽出来形成“运行时状态快照”,而不是让模型每次都在长历史里自己找。
实际编码时,可以用一个固定字段集加动态扩展字段的方式实现。固定字段包含任务ID、当前已完成步骤、下一步计划、关键用户约束等;动态扩展字段由工具返回结果经过提炼后填入。每轮工具调用后,返回结果先经过一个提炼层(可以是一个便宜的轻量模型,也可以是规则+正则的组合),只保留三层筛选后命中的字段,再写入消息历史。原始结果则存入外部存储(Redis或向量库),与索引ID关联。
2.3 结构化索引加懒加载:如何让模型在需要时“捞回”细节
有了提炼后的精简历史,下一步就是让模型在缺信息时能自助补齐。这里我给Agent定义了一个专用的“上下文加载”工具,本质上就是一个带检索的读取函数。
工具的逻辑不复杂:输入是一个查询描述(自然语言即可),系统会先做两件事。第一,在精简历史的索引表里查这个查询与哪些工具调用、哪些原始结果相关;第二,把命中的原始结果按相关性排序后返回。为了让模型知道“什么东西被精简了、什么东西还能捞回来”,我会在系统提示词里明确告知模型:历史中部分工具返回已经过稀疏化处理,如需要完整数据,请调用上下文加载工具获取。
这个设计带来的一个明显体验变化是:模型开始学会主动判断“我需要更多信息”。在早期版本里,它只会用已有信息硬着头皮推;引入按需加载后,模型会在做关键决策前先捞一次数据,准确率明显上升。这种“模型自己决定什么时候补充上下文”的模式,比任何预设规则都灵活。
3. 实操:把SparseRead落地到Agent项目的全过程
3.1 第一步:先做基线测量,搞清token到底花在哪
任何优化开始前都要先做测量,不然就是凭感觉优化。我给自己的项目先搭建了一个最朴素的埋点方案,记录每次LLM调用的输入token、输出token,以及消息历史中每条消息的来源和原始大小。
采集两到三天的真实任务数据后,按来源统计占比,很快就能定位到主要开销。以我当时的项目为例,真实统计出来一条典型的复杂任务链共28轮LLM调用,累计输入token约63万,其中工具返回结果贡献了约52万token,占比82.5%。这个数字是后续优化的基准线。
这一步建议大家都做,而且要多采集不同复杂度的任务样本。因为简单任务的token开销本来就小,优化空间不明显;如果只盯着简单任务优化,很可能得出“SparseRead没什么用”的错误结论。我见过有人拿一个3步的任务测试,发现token只降了30%,就觉得方案不行,其实长任务里收益是几何级放大的。
3.2 第二步:消息重写与按需加载的组合改造
测量完成后,我开始改造消息历史的写入链路。改造分两路并行进行。
第一路,所有工具返回结果不再直接写入消息历史,而是经过一个“提炼-索引-存储”流水线。以那段JSON为例,核心提炼规则是:只保留与任务目标明确相关的字段,超过阈值的长文本只保留摘要和关键片段,原始内容以压缩格式存到本地缓存服务,并生成一个唯一索引ID。同时,原始内容的字段路径、数据类型、内容摘要形成一个条目追加到索引表中。
第二路,定义了一个轻量工具retrieve_context(query: str, top_k: int),内部实现是:先用向量相似度在索引表里做召回,再用关键词匹配做精排,最后把命中的原始内容截取相关片段返回。返回长度做了硬限制,比如单次最多3000 token,防止模型一次性拉回过多历史导致上下文再次爆炸。
这两路改造在代码层面只需要改动一个工具调用分发器和一个消息格式化函数,不需要动模型本身。技术栈上,提炼层我用过规则和模型两种方案。规则方案快、零额外token开销,适合工具返回结构固定、字段明确的场景;模型方案灵活、能处理非结构化文本,但每次提炼本身会消耗一定token。两者可以结合,高频简单工具走规则,低频复杂工具走轻量模型。
3.3 第三步:参数配置与实际调优表
改造完成后,几个关键参数直接决定了最终效果和任务质量的平衡点。我把自己的配置整理出来,但不是让大家照抄,而是理解每个参数的影响后按自己场景调整。
首先是提炼后保留字段数:字段太少会丢失关键信息,模型频繁回捞数据反而增加调用次数。我的经验是保留与任务目标直接相关的核心字段,加上一个可选扩展字段列表,总体控制在原结果的10%到20%左右。其次是回捞工具的单次返回上限:设太大会让summary失去意义,设太小则需要多次回捞增加额外轮次。一般建议控制在500到3000 token之间,具体看单次决策需要多少信息。第三是索引表的构建深度:不是所有历史信息都需要进索引,只索引工具返回的结果类型和关键字段路径就够了,对话类消息保持原样。
另外建议设置一个“回捞次数上限”,避免模型在缺失信息时反复调用加载工具导致死循环。我在系统提示里加了一条规则:单次任务中调用加载工具超过一定次数后,后续决策必须基于已有信息进行。这个限制能有效兜底。
3.4 效果对比与成本核算过程
改造上线后,我做了三轮效果验证。第一轮是功能回归,跑了之前容易失败的复杂任务,确保任务成功率不降。第二轮是token统计对比,重新用改造前的同一批任务样本跑,改造后28轮调用的累计输入token从约63万降到约5.3万,降幅约91.5%。第三轮是成本换算,按商用模型的输入价格每百万token约2美元计算,单条复杂任务成本从约1.26美元降到约0.1美元,降幅显著。
当然,这只是输入token的对比。SparseRead增加了一次额外的向量检索开销和可能的回捞调用,但这两部分成本极低,相比节省的token费用可以忽略不计。更重要的是,任务完成质量没有下降——改造后基于精简历史加按需回捞机制,关键决策点反而比全量历史时更准确,因为模型不再被冗余信息干扰。
注意:即便降幅接近92%,也要警惕收益被额外机制侵蚀。比如回捞调用如果太频繁,会增加输出token开销;提炼层如果使用LLM,每次提炼也在产生新token。需要用完整链路的总token做对比,而不只是比较输入token。
4. 从“负优化”到“接近92%”:迭代过程中踩过的坑与解决方案
4.1 暴力截断的失败教训
我最早尝试的方案,也是最朴素的方案:对话历史超过一定长度时,直接把最旧的消息丢掉。这个方案上线后很快就出现了问题。在一次多轮需求确认类任务里,用户在第2轮明确说过“不要推荐超过500元的套餐”,但在第6轮时这条消息已被截掉,模型重新推荐了一款700多元的套餐。用户发现推荐不合理,Agent又花了3轮去纠正和重新推荐。最终整条任务的token开销比不截断时还高了40%左右,而且用户体验分数明显下降。
这个案例让我彻底放弃“按长度截断”的思路。长度是最直观的指标,但不是信息重要性的指标。真正决定任务质量的是关键决策信息是否完整,而不是历史有多长。
4.2 全量摘要的语义损失
第二次迭代改成全量摘要方案:每轮结束后,用模型把历史消息总结成一段摘要,替换掉原始对话。这个方案能稳定降低token,但很快暴露出另一个问题——摘要的语义损失。
模型在摘要时会把一些看似不重要的细节省略掉,比如用户随口提的一句“我平时晚上用得多”,可能恰恰是推荐流量套餐时的关键约束。更麻烦的是,摘要层叠效应:第2轮的摘要是对第1轮历史的压缩,第3轮的摘要又基于第2轮的摘要进行压缩,信息经过多轮嵌套后逐渐失真。到任务后期,模型基于的已经不是事实本身,而是对事实的模糊转述。
全量摘要适合短任务或在任务开始时一次性对长文档做摘要,但用在整个多轮对话的每一轮上,效果很不理想。我在这个版本踩了大概两周的坑,才意识到问题本质是“压缩不可逆”,任何有损压缩都会丢失任务必需的细节。
4.3 从压缩到索引:SparseRead的转折点
前两版失败后,我意识到思路需要调整:与其在“压缩”这条路上死磕,不如换一个方向——不把所有信息塞回上下文,而是把信息放到一个模型可以按需访问的外部空间。这就引出SparseRead的核心转变:压缩不是目标,路由才是。
于是我把消息历史拆成了“运行时上下文”和“外部存储”两层。运行时上下文只保留当前步骤的输入输出、关键约束、状态快照;外部存储保存完整工具返回结果,并且用索引表记录每个结果的结构化摘要。模型缺细节时,通过加载工具主动拉取。这个方向落地后,第一版就达到了约84%的降幅,后续再经过参数调优稳定到90%以上。
4.4 长尾问题排查:回捞失效与状态同步异常
方案上线后还遇到几个长尾问题,这里一并列出来供参考。
第一个是回捞失效。某次任务中模型明确调用了加载工具,但返回内容为空。排查后发现缓存服务里的原始内容因为进程重启丢了,索引表还在,源数据却没了。解决方法是给索引表和原始内容加同一把过期时间,查询时先校验内容是否存在,不存在则直接返回“数据已过期”的提示,避免模型拿到空结果后茫然。
第二个是状态同步异常。因为运行时上下文不再包含全部历史,一旦Agent框架内部对“已完成步骤”的维护出现偏差,模型收到的状态快照可能滞后于实际进度。这会导致模型重复执行已经完成的动作。后来我把状态快照的更新时机放在任务定义的每个节点完成处,而不是放在每一轮LLM调用后,问题就消失了。
第三个问题是系统提示词没有同步解释稀疏化规则。早期版本里模型不知道历史被精简过,遇到缺失信息时会自行脑补,而不是调用加载工具。在系统提示里明确说明“工具返回已被稀疏化,完整数据可通过检索工具获取”之后,模型的主动拉取行为才稳定出现。这个看起来不起眼的提示,其实决定了整个机制能否生效。
5. SparseRead的适用边界与后续扩展思路
5.1 哪些场景适合上SparseRead
根据我的实践经验,SparseRead特别适合以下几类场景。
一是工具返回体量大但密度低的任务。比如查询类Agent,工具返回大量JSON但真正影响决策的字段不多。二是需要多轮工具调用的复杂任务。任务轮次越多,历史膨胀越严重,稀疏化的收益就越大。三是任务步骤之间相对独立、模型可以在缺少信息时主动请求补充的场景。四是长文本处理任务,例如读取多个文档后完成对比分析,原始文本不可能全部塞进上下文。
我实测下来效果最明显的案例是客服工单处理类Agent:单条工单附件信息长达数万字,但Agent每次只需要其中几百字就能完成一次决策。SparseRead上线后,这类任务的token成本从每单均值120万降到不到10万,响应时间也同步缩短了约40%。
5.2 哪些场景建议慎用
有收益空间,自然也有不适合SparseRead的场景,这里强调三个。
第一类是每一步决策都依赖全部上下文的任务。比如法律合同审查Agent,任何一处细节遗漏都可能导致错误结论,稀疏化带来的信息损失不可接受。第二类是任务轮次非常少(少于5轮)的短流程。这类任务本身历史很短,稀疏化的收益不明显,反而增加了检索和回捞的复杂度。第三类是对响应延迟极其敏感的实时交互场景。虽然SparseRead整体提速,但偶尔的回捞调用会引入额外的延迟,如果用户等不了往返时间,保守做法是保留全量历史。
如果你要在这类场景里引入稀疏化,我的建议是缩小范围,只精简那些多次调用的低价值工具返回,不对对话历史做任何改动,两者叠加平衡风险和收益。
5.3 与缓存类方案的配合:进一步压降成本
SparseRead是天然适合与上下文缓存配合的。缓存管的是“重复内容不用重复计费”,SparseRead管的是“进入上下文的内容更少、更精炼”。两者方向不同,互不冲突。
在实现上可以做这样的分层:系统提示词、工具定义这类稳定内容走缓存,缓存命中后这部分token几乎免费;每次任务的动态上下文走SparseRead,控制增量token的数量。配合之后,实际账单里可计费的token还能再降一截,整体成本模型更健康。
另外一个可选的扩展方向是动态阈值调整。根据任务复杂度实时决定提炼强度:简单任务少提炼,复杂任务多提炼。这个方向的收益空间也很大,就看你的业务场景是否需要这么细的调控。
我自己在完成这套改造后最大的体会是:优化token成本,本质上是在信息密度和访问成本之间找平衡。全量历史让模型什么都有,但浪费严重;截断让上下文变短,但模型会失忆;摘要看着优雅,但语义失真无法避免。SparseRead通过“稀疏表达+按需回捞”绕开了这三个坑,把信息的存储和访问解耦,让模型在花最小代价的同时还能做到“需要什么取什么”。如果你也正在被Agent的token账单困扰,不妨先花一天时间给上下文埋个点、看看数据分布,说不定优化的机会就藏在那些被忽略的工具返回值里。