
前阵子帮一个团队复盘他们的AI Agent应用账单一个月在模型调用上烧掉的费用比他们整个微服务集群的机器成本还高。刚开始都以为是模型太贵排查下来发现真正的问题在于大量重复的查询、重复的工具调用、重复携带的上下文每一次都在重新付费。这让我重新意识到一个被很多人低估的事实——Agent系统的Token开销往往不是模型价格决定的而是缓存命中率决定的。这篇文章会从我实际踩过的一些坑出发把Agent缓存命中率提升和Token成本控制这两件事掰开揉碎讲清楚覆盖成本结构拆解、缓存架构分层、语义缓存实现、命中率优化方向以及上线后的指标口径和调优方法。适合正在做AI Agent应用、或者已经被LLM调用账单吓到过的同学参考看完可以直接在团队里落地。1. 钱都去哪了一条Agent消息背后的Token账单明细1.1 一次Agent调用的Token消耗拆解很多人对Token成本的认知还停留在我发给模型一句话模型回我一段话这个层面但实际上Agent场景的开销远比这大得多。普通聊天场景一次请求的Token消耗大致是这样系统提示词 历史消息 用户输入 输入Token模型回复 输出Token。这是单轮计费。但Agent不一样。Agent通常走的是任务拆解、循环执行、工具调用的路径一次用户请求模型要来回推理多轮。我举一个真实的工作流例子用户问帮我把上海这周气温超过30度的日子标到日程里并设置提醒。这个请求在执行过程中至少会发生以下对话轮次第一轮系统提示词 工具定义 用户问题 → 模型输出规划决定调用天气查询工具第二轮系统提示词 工具定义 历史上下文 天气API返回结果 → 模型输出下一步决定调用日程写入工具第三轮系统提示词 工具定义 全部历史上下文 日程工具返回结果 → 模型最终生成确认回复。注意每一轮调用系统提示词、工具定义、历史上下文都会被重复发送一遍。我用一个粗算来展示开销假设系统提示词 工具定义固定占了1500 Token历史上下文运行到第三轮时膨胀到3000 Token用户输入300 Token那么每一轮的输入Token分别是2100、3600、4800左右三轮加一起输入Token已经过万再算上每轮几百Token的输出一次完整执行轻松吃掉12000到15000 Token。这个数字意味着什么如果一天有1万个用户发起类似的任务一天的Token消耗就是1.2亿到1.5亿。即便按相对经济的商用模型定价粗估一天的成本也在几千元级别一个月烧出六位数人民币并不夸张。而且这些Token里相当大一部分是重复的、可复用的——这正是我们要下刀的地方。1.2 Token成本吃掉的三个放大器复盘了几套Agent系统之后我总结出Token成本失控的三个主要放大器。第一个放大器是System Prompt与工具定义的重复携带。这是我见过最普遍的浪费源。很多团队为了让Agent稳定工作把系统提示词写到几百行工具定义加了几十个这些内容在每一轮模型调用里都要完整走一遍。可问题是这套固定的提示词和工具定义在短时间内其实是不会变的明明可以被缓存却每次都在按全量计费。第二个放大器是历史上下文的线性膨胀。Agent执行过程中中间结果、工具返回值会被不断塞进上下文里越往后输入Token越多。尤其是有些工具返回的是大段JSON、大段日志Agent为了记住这些信息每一轮都要重新携带但真正用到的可能只是其中一小段。第三个放大器是完全没有复用的重复请求。同一个用户隔五分钟问同一个问题会重新付一次费十个不同的用户问你们的退款政策是什么会付十次费Agent自己在循环里反复查询同一个订单状态也会付N次费。这种浪费是最可惜的因为它不是技术难点而是架构上没有做缓存导致的白白重复。1.3 缓存为什么是成本控制的第一抓手团队在控制Token成本时通常会想到三条路换便宜模型、压缩上下文、加缓存。我的经验是前两条都有明显的天花板和副作用。换便宜模型意味着模型推理能力下降Agent的工具调用准确率和指令遵循能力往往跟着下降团队又要花大量时间去做提示词补偿效果还不一定稳。压缩上下文则更危险Agent本身就需要足够的上下文来维持任务状态强行裁剪很容易丢失关键信息导致管中窥豹式的错误决策。相比之下缓存是收益最直接、副作用最小的一条路。缓存命中一次省掉的不只是回答的那几十个输出Token而是整轮调用里几千甚至上万输入Token加上输出Token的整体开销。而且这是一个线性收益有30%的请求被缓存命中整体成本就大约下降三成。这个账算清楚之后团队共识就很容易达成了。2. 缓存放在哪几层LLM侧缓存与应用侧语义缓存的边界划分2.1 LLM侧缓存KV Cache与Prompt Cache的省钱原理很多人不知道模型厂商在API层面已经做了一层自动缓存最常见的就是KV Cache和Prompt Cache。我先用大白话解释一下原理。LLM在生成回复时需要先把历史Token逐个读一遍生成过程中的中间状态会存放在KV Cache里。同一个前缀如果第二次出现理论上这些中间状态不需要重新计算。现在的商用API大多在后台做了自动的Prompt缓存同一段前缀文本在指定时间窗口内再次出现时这一段的计算开销可以部分甚至全部省去体现在账单上就是输入Token的价格大幅降低。这层缓存看起来是自动的但如果你不主动配合它发挥的作用非常有限。让我用一次实测来说明我把系统提示词保持在完全相同的状态下让它连续处理两个相似请求第二个请求的输入Token计费确实出现了明显折扣但一旦我在系统提示词里插入了一个动态时间戳整段前缀就脏了缓存前缀匹配失败第二个请求立刻恢复全价计费。这个发现非常关键。工程上的应对策略就是前缀静态化把所有动态信息——当前时间、随机数、用户ID、会话编号——从Prompt前缀中剥离出去要么放到消息列表的后面要么放到靠近用户消息的槽位里让系统提示词和工具定义保持字节级稳定。这个动作几乎零成本但能实打实吃到LLM侧缓存的折扣红利。2.2 应用侧语义缓存的职责与适用场景LLM侧缓存解决的是同一段前缀重复计算的问题但它解决不了不同人问同一个问题的问题。这个问题得靠应用侧的语义缓存来解决。语义缓存的核心思路是用向量表示用户问题然后通过相似度匹配判断当前问题是否与之前某个问题语义等价。如果是直接复用之前的回复完全不再调用LLM。在实际落地中语义缓存最适合以下几类场景客服问答Agent大量用户问的是同一批FAQ语义高度重复知识库检索Agent同一份文档被反复询问不同的人用不同的问法但意图完全一致数据报表Agent高频的固定分析模板比如上周的销售额是多少这类查询换个日期就是另一条但模板本身稳定多租户共用场景不同租户问的是同一个规则类问题。不适合语义缓存的场景也很明确强个性化任务必须按用户画像生成差异化内容强时效性任务比如实时股价、天气预警缓存内容很快过期带写操作副作用的任务比如下单、删除、转账这类请求绝对不能从缓存里直接返回结果否则会引发严重的业务一致性问题。2.3 缓存代理层的引入与整体架构把缓存真正落地到Agent系统里我不建议在业务代码里到处埋点而是应该引入一个独立的缓存代理层。这个代理层的位置在用户请求和Agent执行器之间核心职责有三个请求分类、缓存判定、结果写回。完整的调用链路是这样的用户请求进入系统后先经过一个请求预处理器这一步把请求划分为查询类和操作类。操作类请求直接放行到Agent主流程查询类请求则提取缓存Key去语义缓存里做相似度判定。如果命中直接把缓存中的回复返回给用户不走Agent如果未命中请求进入Agent执行器正常完成任务后把回答按一定规则写回缓存再返回给用户。用这个架构有几个明显好处。第一对Agent主流程完全无侵入Agent代码不用知道缓存的存在后续演进不会互相干扰。第二缓存代理层可以独立扩缩容缓存服务扛不住了就多部署几个实例。第三所有命中与未命中的统计数据都集中在这里成本核算和效果评估非常方便。这层代理让我在实践中省下了大量麻烦。有一次业务方要上线新话术直接从管理端更新了缓存版本号几分钟内全部流量切换到新内容完全不需要重启Agent服务。要是没有独立代理层这种操作会变成一场噩梦。3. 语义缓存从零到一向量判定、阈值调参与代码落位3.1 缓存Key与Value的设计思路不止是存问题-答案语义缓存最容易犯的错误是把它当成一个简单的问题-答案键值表。实际工程里Key和Value的设计都要更细。Key的设计需要组合多个维度。我用一个结构化的方式来表示查询指纹用户ID的哈希、Agent类型、会话上下文摘要、问题本身的向量。这样做的原因是不同Agent的回复可能完全不同同一个用户在个性化场景下的两次相似提问未必适合互相复用。如果直接把所有问题的向量丢进一个大池子里做相似度匹配很容易串味。Value的设计分两种形态。一种缓存完整回复适合服务台问答、FAQ这类答案独立且稳定的场景另一种缓存中间过程适合Agent任务链路中的子结果。中间过程缓存的含义是不缓存最终回复而是缓存某个阶段的输出比如工具调用返回的数据、检索命中的文档片段。这两种形态可以混用后面第四章我会展开讲。另外缓存条目必须附加元数据至少包含创建时间、来源Agent版本、缓存内容对应的知识库版本、命中次数、是否曾经被用户纠正过。这些元数据不只是用来排查问题更是后续淘汰坏缓存条目的依据。3.2 向量命中判定与动态阈值调参语义缓存的核心判定逻辑不复杂把用户问题做Embedding在向量库里检索最相似的Top K条计算余弦相似度超过阈值就命中否则未命中。真正的难点在于阈值怎么定。我最早做语义缓存时把阈值设成了0.95结果命中率惨不忍睹只有完全同义改写才能命中效果还不如精确匹配。后来我把阈值降到0.80命中率上去了但新的问题出现了用户问怎么退款和退款要多久被语义缓存误判成了同一个问题返回了完全答非所问的内容。这种坏命中对用户体验的伤害非常大。后来我摸索出一个相对靠谱的调参方法先拿至少500条真实用户日志把其中的query两两组对人工标注语义相同和语义不同两类然后分别计算两组对的余弦相似度画出两条分布曲线取两条曲线谷值之间的位置作为初始阈值。这个方法不一定能一次到位但能给出一个合理的起点后续再根据线上的坏命中率微调。还有一个经验对于包含数字、否定词、时间约束的查询要格外保守。比如退款金额是多少和退款金额不是1000吗字面相似度很高但语义完全相反。我的做法是在缓存判定之前做一个简单的规则检查如果query包含不、没、非、高于、低于这类强约束词就把阈值临时调高或者直接放行不参与缓存。3.3 伪代码实现与工程插入点我提供一个工程上比较实用的语义缓存伪代码这段代码可以直接嵌入到请求处理链路的任意位置。def get_cached_response(query, agent_type, user_id, cache_client, embedding_model, threshold0.9): # 1. 规则预筛选 if has_strong_constraint(query): # 含否定/数值/时间强约束 return None, {decision: skip_by_rule} # 2. 生成查询向量 query_vec embedding_model.embed(query) # 3. 在向量库中检索相似历史请求 similar_items cache_client.vector_search( vectorquery_vec, top_k5, filter{agent_type: agent_type, valid: True}, ) # 4. 阈值判定 for item in similar_items: if item.score threshold and item.user_id_hash hash_user(user_id): return item.response, {decision: hit, score: item.score} elif item.score threshold and item.scope global: return item.response, {decision: hit, score: item.score} return None, {decision: miss} def save_response_to_cache(query, response, agent_type, user_id, cache_client, embedding_model, metadata): # 只在确认是稳定型回复时写缓存 if metadata.get(is_operation, False): return query_vec embedding_model.embed(query) cache_client.vector_upsert( idgenerate_cache_id(query, agent_type), vectorquery_vec, payload{ response: response, agent_type: agent_type, user_id_hash: hash_user(user_id), scope: metadata.get(scope, user), created_at: now(), agent_version: metadata.get(agent_version, ), hit_count: 0, }, )这段代码里有几个细节值得强调。第一规则预筛选一定要在最前面跑一次规则判断比算一次向量便宜得多而且能提前挡住大部分坏命中的风险。第二检索时的filter条件里加上agent_type防止不同Agent的回复互相污染。第三写入缓存的元数据里必须标记作用域是全局共享还是仅限当前用户后续判定时要有不同的信任等级。工程插入点方面我建议放在两个地方。第一在Agent主流程入口处也就是请求分类器之后这是最主要的判定点第二在LLM调用SDK的封装层这样即便Agent内部某些中间调用也能被拦截缓存。如果团队有独立的API网关也可以把缓存逻辑做成一个中间件效果类似。我用一个简单的表格对比这三个插入点的优劣势插入点优点缺点推荐场景Agent主流程入口可完整分类请求、处理写操作限制缓存维度控制最细需要业务代码配合大多数项目建议首选LLM SDK封装层侵入最小所有Agent调用自动生效很难区分查询类和操作类缓存Return值容易出错快速验证阶段、已有系统不方便改API网关中间件对业务完全透明集中管理无法感知Agent内部状态只适合纯查询场景纯知识库问答、多Agent共用网关4. 把命中率从20%拉到60%几个方向的优化路线4.1 缓存Agent的规划结果比缓存最终回复更省钱大多数团队做缓存第一反应是缓存Agent最终返回给用户的那段话。但是我实践下来这个做法只适合最简单的FAQ问答遇到任务型Agent就力不从心了。更值得做的是缓存Agent的规划结果。所谓规划结果就是Agent在拿到用户请求后第一轮推理输出的执行计划也就是先做什么、再做什么、调哪些工具这部分内容。举个例子。用户问我最近一个月的消费明细里有哪些异常项Agent的规划大概率是先查消费明细表再算统计异常然后生成结论。同一个SQL模板、同一个分析逻辑换个用户、换个时间范围规划步骤高度相似。如果你把查询意图 数据范围作为任务指纹命中后直接把规划结果提取出来就可以跳过Agent第一轮从自然语言生成工具调用计划的推理省下这轮的输入输出Token。我实测过这个优化方向在数据报表类Agent上效果尤其明显。一次典型的报表生成任务规划轮往往消耗上千Token缓存规划结果后这部分开销直接归零整体Token消耗下降了15%到20%。4.2 缓存工具调用结果投入产出比最高的一个方向如果说有一个方向让我觉得早知道就早点做那一定是工具结果缓存。Agent执行过程中很大比例的Token消耗发生在工具调用结果的回填上。尤其是那些返回大段数据的工具——查询订单详情、拉取商品信息、查询用户历史记录——一次返回动辄几百上千Token而且这些数据在短时间内是完全相同的。我遇到的最典型例子是汇率查询。用户的对话里提到美元兑人民币汇率Agent去调汇率API拿到一段JSON再塞进上下文。五分钟后另一个用户问同样的问题Agent又调了一次API又拿回同样的数据又塞进上下文。两次之间汇率可能根本没有任何变化。工具结果缓存的实现思路是对工具请求做参数规范化生成工具调用指纹工具名 参数哈希然后用TTL控制有效期。对汇率这种强时效数据TTL设10分钟对订单状态这种相对稳定的数据TTL可以设到30分钟甚至更长。这样做的好处是双份的Agent不需要重复调用外部工具省了外部API费用和延迟同时省掉了这部分工具结果Token的重复写入。4.3 Prompt前缀静态化与动态槽位分离这个方向我在前面LLM侧缓存部分提到过但它在应用侧缓存里同样重要。对一个Agent系统来说每轮请求带来的Token开销主要可以分为三块固定的系统提示词、动态的上下文信息、用户输入。如果你能把固定部分做到字节级稳定那么这部分的Token消耗在LLM侧可以享受Prompt Cache的折扣而在应用侧稳定的前缀也意味着你不必为前缀变了导致缓存Key失效而烦恼。实际操作中我会把系统提示词拆成两个部分静态的角色设定工具规范部分以及动态的当前会话状态部分。静态部分固定不变动态部分通过模板变量在消息序列末尾注入例如当前时间是2026年5月10日用户所在时区为UTC8。这种静态前缀 动态尾部的结构能让两类缓存同时受益。这里有个小技巧值得分享把时间、日期这类动态信息放在用户消息之后、模型回复之前的位置。因为很多缓存策略是按消息序列的前缀来匹配的动态信息越靠后能保持前缀稳定的区域就越大。4.4 坏命中的规避命中率不是越高越好缓存做得越久我越意识到一个反直觉的事实命中率这个单一指标是会骗人的。我见过团队把命中率刷到60%以上但用户满意度反而下降原因就是坏命中太多——缓存返回了一个语义相似但实际不适用甚至错误的答案。坏命中通常发生在三类情况里。第一类用户问法相似但实际意图不同比如预约今天下午的会议和预约明天下午的会议字面上高度相似但结果完全不同。第二类查询中包含隐藏的上下文依赖比如用户说那这个呢指代的是之前聊过的某个具体对象单独看这句话根本无法判断缓存是否相关。第三类缓存内容本身已经过时但TTL还没到期例如价格调整后的商品信息。规避手段我总结为三层防线。第一层规则层包含时间词、否定词、指代词、数量变化的query直接降级为不参与缓存或提高命中阈值。第二层作用域隔离个性化相关的问题缓存只在同一用户ID范围内生效不做跨用户的全局复用。第三层反馈闭环如果用户在命中缓存后发起了我说的是另一个意思、点踩或继续追问系统把这个缓存条目标记为低置信度超过一定次数直接从缓存库中淘汰。这三层防线看上去都不复杂但少了任何一层缓存都会变成一个看起来省了钱、实际伤了用户的隐形炸弹。5. 工程落地复盘指标口径、存储选型与上线调优5.1 命中率指标的正确口径按请求数还是按Token数工程落地时第一个问题就是命中率到底怎么算如果口径错了后续所有优化决策都会被带偏。按请求数计算命中率是最常见的做法但有一个缺陷它会被大量简单请求拉高。比如系统每天有1000个你好级别的问候请求这些请求全部命中命中率被抬到50%但它们的Token消耗占比可能只有2%对成本控制毫无意义。我更推荐按Token当量来计算命中率。也就是每次命中时估算如果走LLM会消耗多少Token把命中带来的Token节省量作为分子把所有请求的总Token消耗量作为分母。这个口径直接反映成本节省效果和团队最关心的账单联动。如果指标基建还没建好还有一个折中方案同时跟踪命中率和坏命中率把坏命中率控制在2%以下作为质量红线。只要质量红线不破命中率提升多少都是收益一旦坏命中率走高就停下来先处理质量而不是继续冲指标。5.2 存储选型Redis、内存还是向量数据库存储选型是工程落地中最容易纠结的部分我给一个实践中的决策依据。精确匹配和前缀匹配用Redis就够这是成本最低的方案。Redis支持哈希和有序集合做精确命中、计数、TTL都很快。如果团队已经有Redis集群几乎零成本接入。语义缓存需要向量检索能力。这里我遇到过两个弯路第一个弯路是试图纯用Redis做暴力向量检索数据量小的时候没问题缓存条目过十万之后延迟开始不可控第二个弯路是一上来就引入分布式向量数据库运维成本很高但初期数据规模根本用不上那么重的组件。我现在的选型原则是缓存条目在十万级以下用轻量级的向量检索方案比如常用的开源向量库直接内嵌在服务里一个文件搞定存储性能完全够用超过百万级或者有分布式查询需求再上独立向量数据库。先用简单的等规模到了再迁移不要一步到位。另外不管是哪种存储都要做双缓存结构先查Redis精确匹配未命中再查向量库。精确匹配的成本是微秒级向量检索至少是毫秒级先用廉价的方式过滤一遍能把向量库的压力降一个量级。5.3 一致性与TTL、冷启动处理缓存一致性是工程上最容易翻车的地方我分享几个具体的处理经验。数据源变更导致缓存内容失效最常见的场景是知识库文档更新了但缓存里的旧答案还在被返回。我的做法是引入版本号机制知识库每发布一个新版本就在缓存元数据里写入对应的版本号语义缓存查询时校验版本号不一致的条目自动失效。这个机制改造成本不高但能有效避免明明更新了知识库用户看到的还是旧回答的尴尬。TTL的设置要有层次。我的经验值是强时效数据汇率、库存TTL 5到10分钟业务数据订单状态TTL 30分钟知识性内容FAQ答案TTL 24到72小时Agent规划结果这类可复用任务的TTL可以放到一周取决于底层依赖变化频率。不能用一套固定TTL打天下否则不是过期太早没法命中就是过期太晚卖过期答案。冷启动是另一个值得提前规划的问题。新系统上线时缓存是空的前面一段时间的命中率会很低。我的做法是上线前把历史的热门问答数据离线跑一遍Embedding批量灌入缓存库冷启动时间从几天缩短到几小时。后续再通过每天的增量日志把新出现的query补充进去缓存基本能保持高水位。5.4 上线后的A/B验证与调优节奏缓存上线不是加个层就完事它需要对用户可见效果做持续验证。我的上线节奏是这样的。第一步小流量灰度。先切1%的流量到带缓存的分组观察响应延迟、坏命中率和用户反馈。这个阶段不要急着看成本节省先确认没有明显质量事故。第二步同query双跑对比。把带缓存和不带缓存的服务部署在并行环境里用同样的query分别打过去人工对比两份回复的差异。这个操作虽然原始但能在早期发现大量阈值不合理、作用域混用的问题。第三步逐步放量。在坏命中率稳定低于2%的前提下流量从5%逐步提到10%、25%、50%每提升一个档位至少观察一天。如果某个档位出现坏命中率反弹立即回滚到上一个档位先调参再放量。第四步阈值微调。调整阈值时每次只动0.01到0.02的范围调整后观察两天再动下一个参数。不要一次调好几个参数否则出了问题根本定位不到是哪个改动引起的。第五步成本效果归因。每周末统计一次Token节省量按月绘制成本曲线并和A/B数据做交叉验证。这一步能让团队清楚地看到缓存的ROI也能在后续申请资源时拿出明确的数据依据。关于这个逐步放量的节奏我个人的体会是它最大的价值不是防止出错而是让团队建立对缓存系统的信心。缓存这个东西上线简单调优复杂但只要指标口径清晰、灰度节奏稳它的收益曲线一定会上扬而且越往后越明显。