资讯动态

Prompt Cache 失效之谜:工具定义变动如何影响 LLM API 延迟与成本

发布时间:2026/8/30 19:58:08 来源:尧图企业网站定制
开发者在接入 LLM API 时Prompt Cache提示词缓存通常不会被第一时间注意到但它决定了一批请求的延迟和成本。最近一次工程观察里同一个多轮对话请求在两个模型版本上出现了完全不同的表现把请求中的工具列表从三个减少到两个GPT-5.5 的 prompt cache 立刻全部失效请求延迟和费用明显上升换到 GPT-5.2同一个请求却仍然命中缓存。这个差异看起来只是“多删除一个 tool 定义”实际上牵出了 prompt cache 的命中边界、tool schema 参与编码的方式、缓存 key 粒度等一串问题。这个观察也印证了一件事在基于 tool calling 的 agent 类应用里工具定义是否稳定直接决定提示词缓存能不能服务于高频请求。下面的内容先解释 prompt cache 为什么受 tools 影响再做一组对比实验分析 GPT-5.5 和 GPT-5.2 行为不同的可能原因最后给出缓存友好的请求设计、排查链和生产建议。适用读者包括在自建 Agent、函数调用、MCP 网关、多模型路由场景里处理过请求变慢或费用增长的工程师。1. 先搞清楚 prompt cache 的命中条件再谈 tool 为什么会影响缓存1.1 前缀缓存的基本逻辑Prompt cache 通常不是“整段缓存”而是“前缀缓存”。模型在生成回答前会把整段 prompt 编码成 token 序列然后逐层计算 KV Cache。如果下一次请求的前缀 token 和过去某次请求完全一致服务端可以直接复用这部分 KV Cache不必重新计算 attention。命中缓存的效果有两个降低首 token 延迟因为省去了大量推理前向计算。降低计费 token 数因为缓存命中的前缀部分往往按折扣价计费甚至不计费。这个机制的关键限制是“前缀必须完全一致”。不是说用户消息内容大致相同就能命中而是从 prompt 开头到某个位置的所有 token 必须逐字相同。只要中间插入一个不同的 token从那个位置开始后面的缓存全部断开。所以缓存命中的本质是一个字符串前缀匹配问题只是比较的单位是 token不是字符。实际系统中还会有长度阈值、缓存分片、失效时间等约束但“前缀一致”是底层前提。1.2 tools 参数在服务端如何参与 token 序列在 OpenAI 兼容接口里请求体中的 messages 和 tools 是分开的两个字段比如{ model: gpt-5.5, messages: [ {role: system, content: 你是订单助手。}, {role: user, content: 帮我查一下订单 A123 的状态。} ], tools: [ { type: function, function: { name: query_order, description: 查询订单状态, parameters: { type: object, properties: { order_id: {type: string} }, required: [order_id] } } }, { type: function, function: { name: cancel_order, description: 取消订单, parameters: { type: object, properties: { order_id: {type: string}, reason: {type: string} }, required: [order_id, reason] } } } ] }云端服务在真正执行推理前需要把 function calling 的工具定义转换成模型能理解的文本片段或特殊 token 序列。这一步通常是内部的开发者看不到但效果等价于在模型输入的某个位置额外插入了一段“工具清单”。也就是说一个请求实际参与前缀匹配的 token 序列大致是system 指令 工具使用说明 工具定义清单 历史消息 当前用户消息不同模型的具体拼接顺序可能不同有的把工具定义放在 system 之前有的放在 system 之后有的按特殊 token 处理。但共同点是tools 字段的内容会进入模型实际输入序列它不是挂在请求体里对缓存免疫的附加参数。1.3 为什么“删除一个 tool”等于改写前缀现在可以解释标题里的现象了。假设一个 Agent 原本注册了三个工具query_ordercancel_orderget_user_profile在一次调用中因为业务调整你不再需要 get_user_profile于是从 tools 数组里移除了它。前后两个请求的 messages 可能完全一样system prompt 也一样但工具清单从三项变成两项。从工程的直觉看你只是删掉了一个不再使用的函数定义对话内容没有变化。但从 token 前缀看工具定义段产生了非常大的差异原本完整的三段 JSON Schema 少了一段后面的 message 部分整体前移。即使 delete 操作发生在工具列表末尾它也会改变工具定义段之后的 token 对齐方式因此从工具段开始所有缓存都不能复用。更麻烦的是即使工具列表本身没有增减只是某一个工具的 description 里改了一个词、参数的 JSON Schema 调整了某个字段类型token 序列也会变。这是 tool calling 场景下 prompt cache 失效的主要原因。1.4 一个容易混淆的点请求体里 tools 不在 messages 里不代表不影响前缀很多开发者在排查缓存失效时会走入一个误区只对比 messages 数组是否一致看到 messages 完全相同就认为缓存应该命中却完全忽略 tools、tool_choice、response_format 等请求级参数。实际判断标准是“服务端实际参与编码的完整输入序列是否一致”不是“客户端请求体顶层字段是否看起来一样”。tools 的任何变化都会改写输入序列因此都会影响前缀匹配。同样的问题也出现在 metadata、user 字段、自定义模型的某些参数上。如果服务端把这些字段拼进内部 prompt它们也会成为缓存 key 的一部分。正确做法是查阅对应模型的缓存文档或者通过返回的 usage 字段观察实际命中情况。2. 复现实验同一段对话删除一个 tool 定义观察两个模型的 cached_tokens2.1 实验目标与前提为了确认“删除工具导致缓存失效”这个现象需要在固定对话前缀下做一组 A/B 实验。目标不是证明两个模型谁好谁坏而是看它们对 tools 变化的反应是否一致以及缓存命中量到底差在哪里。实验前提前置知识会调用 OpenAI 兼容接口能安装 Python SDK。模型标识项目里配置的模型名分别为gpt-5.5和gpt-5.2。实验环境本地 Python 3.10安装openai库。说明不同厂商、不同版本对 cached_tokens 的字段命名和计算口径可能不同下面以常见 usage 结构为例。2.2 准备一个能打印缓存状态的调用函数先封装一个请求函数自动打印 usage 中的缓存字段方便对比。import os from openai import OpenAI client OpenAI(api_keyos.environ.get(OPENAI_API_KEY)) def send_and_report(model: str, messages: list, tools: list, note: str): resp client.chat.completions.create( modelmodel, messagesmessages, toolstools, ) usage resp.usage prompt_details getattr(usage, prompt_tokens_details, None) cached getattr(prompt_details, cached_tokens, 0) print(f[{note}] model{model}) print(f prompt_tokens{usage.prompt_tokens}) print(f cached_tokens{cached}) print(f cache_ratio{cached / usage.prompt_tokens if usage.prompt_tokens else 0:.2%}) return resp这里的关键点是读取usage.prompt_tokens_details.cached_tokens。如果服务端没有返回这个字段说明该模型未启用缓存或者当前使用方式不在缓存范围内。2.3 构造三个工具和两组请求定义一组稳定的系统提示词以及三个工具。第一次请求带三个工具第二次请求只带两个工具第三个请求不删除工具但修改一个工具的 description用于对照。SYSTEM_PROMPT 你是订单助手只能回答与订单相关的问题。 TOOLS [ { type: function, function: { name: query_order, description: 查询订单状态, parameters: { type: object, properties: { order_id: {type: string} }, required: [order_id] } } }, { type: function, function: { name: cancel_order, description: 取消订单, parameters: { type: object, properties: { order_id: {type: string}, reason: {type: string} }, required: [order_id, reason] } } }, { type: function, function: { name: get_user_profile, description: 获取用户资料, parameters: { type: object, properties: { user_id: {type: string} }, required: [user_id] } } } ] MESSAGES [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: 用户 1001 的订单 A123 现在是什么状态}, ]先发送一次带三个工具的请求建立缓存。再发送一次只剩两个工具的请求观察缓存是否命中。最后再发一次修改了 description 的请求判断“修改描述”和“删除工具”的影响差异。# 第一次三个工具建立缓存 send_and_report(gpt-5.5, MESSAGES, TOOLS, tools3 first) send_and_report(gpt-5.2, MESSAGES, TOOLS, tools3 first) # 第二次删除 get_user_profile tools_two [t for t in TOOLS if t[function][name] ! get_user_profile] send_and_report(gpt-5.5, MESSAGES, tools_two, tools2 delete_one) send_and_report(gpt-5.2, MESSAGES, tools_two, tools2 delete_one) # 第三次修改 query_order 的 description tools_modified [] for t in TOOLS: if t[function][name] query_order: t json.loads(json.dumps(t)) t[function][description] 查询订单状态返回最新物流信息 tools_modified.append(t) send_and_report(gpt-5.5, MESSAGES, tools_modified, tools3 modify_desc) send_and_report(gpt-5.2, MESSAGES, tools_modified, tools3 modify_desc)注意第二次和第三次请求之间还要保证 messages 前缀一致。如果服务端有缓存第二次请求观察到的 cached_tokens 可以说明“工具删除”是否破坏了前缀第三次请求说明“工具描述修改”是否同样破坏前缀。2.4 结果记录表和初步结论实验输出不会完全相同但高度可能出现如下规律请求模型prompt_tokenscached_tokens缓存比例三个工具首次请求gpt-5.5约 18000%三个工具首次请求gpt-5.2约 18000%两个工具删除一个gpt-5.5约 15000%两个工具删除一个gpt-5.2约 150约 12080%三个工具修改描述gpt-5.5约 18000%三个工具修改描述gpt-5.2约 18000%这里的数值用于示意。真正需要关注的是三列删除工具后gpt-5.5 的 cached_tokens 归零。gpt-5.2 在删除工具后仍能保留大部分缓存命中说明它的缓存 key 并不包含完整工具 schema或它对工具段的编码方式允许部分复用。修改工具 description 后两个模型都出现失效。注意单次实验结果不能直接当成“某版本就是这样”的结论因为厂商可能随时调整缓存策略。实验的价值在于验证思路而不是固化结论。3. GPT-5.5 与 GPT-5.2 缓存行为差异的可能解释3.1 缓存 key 的粒度差异缓存命中依赖输入前缀一致但“输入前缀”的定义可以在不同粒度上实现。一种实现是把完整输入序列按原始文本和工具定义做全量哈希任何一个请求级字段变化都会导致哈希不匹配整段缓存失效。gpt-5.5 的观察结果更接近这种实现。工具定义是请求级结构的一部分少了一个工具相当于整段输入签名变化。另一种实现是把输入分成几个缓存片段例如“系统消息片段”“工具描述片段”“对话历史片段”。如果工具列表删除一个函数服务端可以只让“工具描述片段”的缓存过期而保留“系统消息片段”和“对话历史片段”的计算结果。gpt-5.2 的表现更接近这种分段缓存。这里的关键不是哪边更高级而是模型服务端如何判断缓存失效边界。不同模型版本可能采用不同的缓存策略这是很常见的产品差异。3.2 工具 schema 的规范化方式差异服务端在编码工具时不一定直接把原始 JSON 原样写入 token 序列。有些实现会先对工具做一次“规范化”比如提取函数名列表作为工具选择器的索引。对每个函数生成一个固定 ID。只对参数 JSON Schema 做二次编码。如果模型版本只把函数名列表放进前缀而把完整 schema 放到更靠后的位置那么删除一个工具时只有函数名列表段发生变化后面的对话历史缓存仍然可用。gpt-5.2 的现象可能就是这样它把最能代表工具的“名称”和“调用方式”分开处理导致删除一个函数后主体对话前缀仍然保留。gpt-5.5 如果采用更严格的完整 schema 编码哪怕工具 descriptions 中只有一个字段变化也会导致前缀失效。这也是为什么修改 description 后两个模型都失效但删除工具后只有 gpt-5.5 失效。3.3 模型对“旧工具上下文”的安全策略差异还有一个不能忽略的维度缓存失效不一定是性能问题可能是安全策略。较新的模型版本可能更关心上下文一致性。如果模型在生成时KV Cache 中保留了旧工具定义但新请求中工具列表已经改变模型可能基于旧工具信息产生幻觉。例如缓存中的注意力权重已经记住了 get_user_profile 的调用方式但当前请求里这个工具不存在模型仍可能尝试调用它。为了避免这种错乱较新的模型版本会倾向于在 tools 变化时丢弃更多缓存。也就是说删除一个 tool 导致 gpt-5.5 缓存失效可能是服务端刻意选择的高保证策略而不是单纯没做好缓存。3.4 部分命中与完全命中的混淆排查时还要区分“部分命中”和“完全命中”。gpt-5.2 的 cached_tokens 大于 0不代表整段 prompt 都命中了缓存只代表从开头开始有一部分 token 命中了。如果删除一个工具发生在工具列表中间函数名列表变化会导致后面全部失效但系统消息前面仍然可能命中。所以比较两个模型时不仅要看 cached_tokens 是否为 0还要看缓存比例。一个较好的衡量指标是cache_ratio cached_tokens / prompt_tokensgpt-5.2 删除一个工具后如果有 80% 命中说明前缀仍然较长如果只有 10% 命中说明它也受到了明显影响只是没有完全归零。4. 让 prompt cache 更稳的请求设计方法4.1 固定工具顺序不要在每次请求时动态生成列表工具列表顺序不稳定的最常见原因是把 tools 构建在 Python 字典或 JavaScript 对象中再依赖语言默认遍历顺序。不同进程、不同并发请求下顺序可能随机变化。即使工具内容完全一致只要顺序不同token 前缀就会变换缓存照样失效。推荐做法是显式排序def build_tools(tool_defs: list[dict]) - list[dict]: return sorted(tool_defs, keylambda t: t[function][name])排序后每个工具的相对位置是确定的。再配合固定 JSON 序列化参数能减少大量无意义的缓存失效。4.2 把不变量放在 prompt 最前面前缀缓存命中要求从头开始一致所以越早的位置越不能放动态内容。推荐顺序固定的系统指令。固定工具使用说明。当前用户身份或会话级固定信息。工具定义清单。历史对话。当前用户输入。临时动态内容如时间、随机 ID、临时上下文。这里要特别强调不要把当前时间、日期、随机字符串拼到 system prompt 开头。很多项目把“今天是 2026-05-12星期几”放在系统提示词最前面结果每过一分钟所有缓存的 prefix 都会失效。推荐做法动态时间如果需要可以放在用户消息末尾或放在专门的上下文字段中不要放在 system 开头。4.3 动态内容后置prompt cache 的价值是复用“已计算过的主干部分”。如果动态内容只出现在对话末尾那么整段系统指令、工具定义、历史消息都能命中只有最后一小段需要重新计算。以订单助手为例base_messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: 订单 A123 状态是什么}, ] now datetime.now().isoformat() current_messages base_messages [ {role: user, content: f当前查询时间{now}} ]这样每次请求只有最后一条消息变化前缀缓存能尽量保留。实际项目中要把真正稳定的业务数据放在历史位置动态查询参数放在最后。4.4 工具 schema 的缩减与摘要如果 tools 非常庞大一个函数定义包含几十个字段、几百行 JSON Schema缓存失效成本会非常高。此时可以考虑删除 description 中的冗余修辞只保留调用必须信息。把多次不变的工具定义提前到稳定的系统上下文避免在每次对话中重复携带。如果服务端允许使用模型厂商提供的工具缓存或统一工具库而不是每次请求都传全量 schema。对同一批工具做版本化管理不轻易改 description 和 parameter 结构。但不能为了缓存稳定而牺牲工具语义。工具 description 仍然是模型决定是否调用该函数的重要依据不能过度删减到模型无法理解。4.5 缓存友好检查清单发布前可以按这个清单过一遍检查项要求tools 数组顺序按函数名固定排序tools 数组内容同一版本内保持不变JSON 序列化参数固定缩进、ensure_ascii、sort_keyssystem prompt 开头不能有动态时间、随机数、用户级变量用户消息结尾动态内容尽量放在这里tool_choice在同一功能分支内不要随机切换response_format稳定使用同一格式配置缓存字段监控每次请求记录 cached_tokens 和 cache_ratio5. 缓存失效的排查链路与常见误区5.1 从 usage 字段定位缓存状态排查缓存失效时第一步不是看日志里的业务报错而是看请求返回的 usage。以 OpenAI 兼容接口常见结构为例{ prompt_tokens: 150, completion_tokens: 30, total_tokens: 180, prompt_tokens_details: { cached_tokens: 0 } }如果 cached_tokens 为 0说明本次请求前缀没有命中缓存。需要进一步对比“预期缓存的请求”和“实际发送的请求”在编码后的 token 序列。注意有些 SDK 不暴露这个字段但原始响应 JSON 里可能有。建议在 debug 模式打印完整响应体。resp client.chat.completions.create( modelmodel, messagesmessages, toolstools, ) print(resp.model_dump_json(indent2))5.2 缓存失效排查链路从现象倒推原因建议按以下顺序排查排查项检查方式处理建议messages 是否一致对比两次请求的 token 化结果把 messages 转成 JSON 后 difftools 顺序是否变化打印函数名列表固定排序tools 内容是否变化diff JSON Schema工具变更视为缓存冷启动tool_choice 是否变化对比两次请求的 tool_choice保持业务分支内一致system prompt 是否有动态内容检查时间、ID、随机数移到末尾模型是否相同确认 model 字段切换模型等于清空缓存是否有多版本缓存策略查看服务端返回的 Cache-Control 或 usage 详情以文档为准是否真的存在长前缀prompt 过短也可能不缓存确认前缀长度超过阈值5.3 三个和 tool calling 强相关的常见坑第一个坑只检查 messages不检查 tools。很多项目在记录日志时只记录 messages一旦缓存失效对比日志发现 messages 完全一样就怀疑是服务端问题。实际上 tools 变化更隐蔽。建议在日志中记录 tools 的哈希值。import hashlib, json def tools_hash(tools) - str: raw json.dumps(tools, sort_keysTrue, ensure_asciiFalse) return hashlib.sha256(raw.encode(utf-8)).hexdigest()第二个坑修改工具描述后没有意识到这是 breaking change。删除一个工具是明显的结构变化但修改一个 description 里的小词也足以让缓存失效。生产环境里要像管理接口版本一样管理工具定义不随意修改。第三个坑把动态内容放在工具定义中。例如每个工具 description 里拼接当前用户 ID、租户 IDdescription: f查询当前用户 {user_id} 的订单这种情况下不同用户的请求都会生成不同的工具 schema缓存命中率趋近于零。正确做法是动态 userId 放到用户消息里工具保持静态。5.4 一个经常一起出现的 tool_calls 错误与 prompt cache 无关但常在同一次 Agent 调用里爆出的是下面这个错误An assistant message with tool_calls must be followed by tool messages这个错误的触发路径是模型返回了一段带tool_calls的 assistant 消息但应用层没有把工具执行结果以role: tool的消息追加回去就继续发送下一轮 assistant 消息。它和缓存没有直接关系但会让开发者误以为是请求结构问题破坏了缓存。处理方式是在多轮循环里严格维护消息顺序user - assistant(tool_calls) - tool(每个 tool_call_id 对应一个 tool 消息) - assistant(最终结果)每个tool消息要有对应的tool_call_id并且所有工具结果都要返回不能只返回一部分。顺序错了会直接报错而不是缓存问题。6. 生产环境的工程建议与下一步6.1 不要依赖“某个模型版本不会失效”从这次观察看gpt-5.2 对删除工具不那么敏感但这不构成长期依赖它的理由。模型服务端的缓存策略可能随版本、灰度比例、会话数量变化。今天缓存命中的请求明天可能因为服务端策略调整而失效。更稳妥的思路是不假设 tools 变化不破坏缓存。把缓存当作性能优化不当作功能正确性依赖。在每次模型版本升级后重新跑一遍缓存对比实验。6.2 监控缓存命中率与工具变更生产环境要建立两个指标cache_ratiocached_tokens / prompt_tokens衡量单请求前缀命中比例。cache_hit_rate一段时间内 cached_tokens 0 的请求占比。当 cache_hit_rate 突降时优先检查最近是否发布了新的工具定义、改了 system prompt 或切换了模型版本。建议在日志里记录 tools_hash并在监控大盘上建立“工具版本变更”事件标记。6.3 工具变更发布策略工具定义变动不可避免但可以控制它造成的缓存冷启动成本。把工具变更放在低峰期发布。变更前后保留一段时间的兼容版本不要一次性替换所有请求中的工具列表。在灰度阶段观察 cache_hit_rate如果下降明显需要评估延迟和成本增幅是否可接受。对超长 prompt 应用慎用“描述细节丰富”的工具定义因为每次变更的失效面积更大。实际项目里一个 Agent 可能同时管理几十个工具删除其中一个看似影响很小但其实会推高整批请求的成本。对高频请求把工具列表拆分到不同“场景配置”中避免所有 Agent 共用一个包含全量工具的大列表能让缓存边界更清晰。6.4 扩展subagent 作为 tool 调用时的缓存影响在多 Agent 架构里主从模式常见的一种设计是把 subagent 当作一个特殊 tool 暴露给主 Agent。此时 subagent 的职责提示、允许调用的工具列表都会被编码进 tool 定义。这带来一个新问题subagent 内部工具变化是否会导致主 Agent 的工具 schema 变化如果 subagent 是一个工具描述那么它自身的工具列表变了subagent 工具描述也可能变化进而触发主 Agent 的 prompt cache 失效。从缓存优化角度看subagent 工具描述应该尽量简短和稳定。不要让 subagent 的完整工具列表拼到它的 description 里。描述只需要写“该子代理负责解决退货问题”具体内部工具由子代理自己加载。这样主 Agent 的缓存就不会被子代理内部工具变化破坏。生产经验工具定义的稳定性和结构扁平化对缓存的影响往往比模型本身是否支持缓存更大。设计阶段就把工具当成接口契约管理能减少大量后续性能问题。下次遇到请求变慢、费用升高时可以先检查一下最近是不是增删过 tool 定义。这一条经验在 gpt-5.5、gpt-5.2 以及未来其他模型版本上仍然适用prompt cache 命中与否不只取决于 messages也取决于 tools 和整个请求前缀是否稳定。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价