淘天一面:先说下Prefix Caching 的原理然后再说下 Agent 框架怎么不破坏缓存?我说开启prompting caching就自动命中,他摇了摇头...

发布时间:2026/7/16 0:59:54
淘天一面:先说下Prefix Caching 的原理然后再说下 Agent 框架怎么不破坏缓存?我说开启prompting caching就自动命中,他摇了摇头... 前段时间有个师妹去面淘天一面技术面聊到了大模型推理优化。面试官问了一个看起来很基础的问题说下Prefix Caching的原理然后再说下Agent框架怎么保证不破坏缓存他当时挺自信的因为之前确实用过Anthropic的Prompt Caching接口觉得这个东西不就是开个开关的事嘛。他和面试官说只要开启prompt caching这个API就自动会命中了。面试官听完摇了摇头追问他那你说说为什么很多人开了这个功能缓存命中率还是接近零他愣了一下说可能是网络延迟的问题。面试官又问那Agent框架里system prompt每次请求一模一样就一定能命中吗“他想了想说应该能吧——面试官没再追问只是说你回去再想想”。今天就把Prefix Caching的原理和Agent框架的缓存设计这两件事彻底说清楚。◆ ◆ ◆一、先讲个场景假设说你现在在做一个客服 Agent对吧。你的 system prompt 里写了大概 800 字的角色设定、工具说明、还有 few-shot 示例然后每次用户发一句话过来你都得把这 800 字连同工具定义、历史对话一起塞给大模型。而且一次对话下来可能要调用大模型 5 到 10 次。就是那种流程——用户提问模型决定调工具拿到工具结果模型再思考一下再调工具循环好几轮最后才给出回复。你想啊如果每次调用都得让模型把这 800 字从头读一遍、重新算一遍那你的账单和延迟都会变得非常难看。Prefix Caching也就是前缀缓存就是解决这个问题的关键技术。但是呢很多人只知道开了能省钱至于它的工作原理是什么、为什么自己写的 Agent 用了半天就是不命中缓存其实并不清楚。这篇文章就是要把这两件事给你讲透。◆ ◆ ◆二、Prefix Caching 到底缓存了什么‣ 2.1 从 Attention 说起Transformer 每生成一个 token 的时候都需要让这个 token 去看一下前面所有的 token然后计算注意力。这里有个问题——如果每生成一个新 token 就把前面所有 token 重新算一遍那开销就太大了。所以推理引擎会做一件事就是把每个 token 算出来的 Key 和 Value 向量存起来。这个存起来的东西就叫做 KV Cache。一次大模型请求的推理过程呢其实分两个阶段◆Prefill也就是预填充把输入的 prompt 一次性喂给模型并行计算出所有输入 token 的 KV 向量然后存入 KV Cache。这个阶段的计算量很大跟输入长度基本成正比。◆Decode也就是解码模型一个个吐出新 token每吐一个的时候只需要用这个新 token 去查询前面已经缓存好的 KV 就行了不需要重新去计算历史部分。如果两次请求的前面一段 token 完全相同注意是逐字节相同那么这段的 KV Cache 就可以直接复用不需要重新计算了只对新增的那一部分做 Prefill 就行。这也是面试官想听到的第一层答案——Prefix Caching不是什么魔法开关它的本质是利用KV Cache的可复用性前提是前缀必须逐字节一致。光说开了就行等于没说。‣ 2.2 举个具体的例子假设你的请求结构长成这个样子[系统提示词 800 tokens][工具定义 400 tokens][对话历史 A][本轮新消息]第一次请求的时候呢模型要对全部大概 1200 多个 tokens 做 Prefill。然后第二次请求用户又发了一句话对话历史变成了 AB[系统提示词 800 tokens][工具定义 400 tokens][对话历史 A][对话历史 B][本轮新消息]只要前面系统提示词、工具定义、对话历史 A 这一段跟上次逐字节完全相同推理引擎就能识别出来这是命中的前缀直接复用已经算好的 KV Cache。你只需要对新增的对话历史 B 和本轮新消息这部分做 Prefill 就够了。这意味着什么呢前缀越长、复用率越高你需要新算的部分就越少。首 token 延迟会大幅下降同时计算成本也会大幅下降。像 Anthropic、OpenAI 这些厂商对缓存命中部分通常只收正常价格的 10% 到 25% 左右。‣ 2.3 引擎那一侧是怎么实现识别相同前缀的主流的实现思路呢是把 KV Cache 按固定大小的块来组织然后用前缀树或者叫 Radix Tree 的结构去管理这些块。具体来说就是把 token 序列按块切分每个块算一个哈希值。这个哈希不光是基于本块内容的还要把前面所有块的哈希串联上去保证路径是唯一的。请求进来的时候从根节点开始沿着 token 序列去前缀树里查找看看有没有相同路径的块。命中的块就直接复用 KV Cache从第一个不匹配的块开始往后都得重新计算。目前比较有代表性的实现包括vLLM 的 Automatic Prefix Caching、SGLang 的 RadixAttention还有 Anthropic API 提供的显式 Prompt Caching。Anthropic 这个是通过在消息里加 cache_control 断点来告诉服务端说这里往前的内容我希望被缓存请优先复用。拿 Anthropic API 来说显式声明缓存断点大概就是这么写的response client.messages.create( modelclaude-sonnet-4-6, system[ { type: text, text: SYSTEM_PROMPT, # 800 tokens 的角色设定 cache_control: {type: ephemeral} # 缓存断点 } ], tools[...], # 工具定义也可以单独打断点 messagesconversation_history)cache_control 这个东西呢就相当于在告诉引擎“从开头到这个断点为止的这些内容请求你把 KV Cache 存下来默认大概是存 5 分钟左右下次只要这段内容一字不差就直接复用。”‣ 2.4 一个关键结论前缀必须逐字节相同这句话是整篇文章里最重要的一句。哪怕你只改动了一个字符、一个空格、一个标点符号或者 JSON 序列化的时候字段顺序变了这段前缀在引擎眼里就是全新的内容。缓存直接失效从这个位置往后的所有内容都得重新计算。这也是为什么很多人自己写的 Agent看起来复用了 90% 的历史对话但实际命中率却很低。原因就在于破坏缓存往往发生在你意识不到的那些细节里。下面这部分才是这篇文章真正想要讲清楚的东西。◆ ◆ ◆三、Agent 框架为什么天生就容易破坏缓存一个典型的 Agent 循环是这样子的用户提问 → 模型输出可能带 tool_use → 执行工具拿到结果 → 把工具结果塞回消息列表 → 再次请求模型 → ... 循环直到模型给出最终回复每一轮循环呢都是在前一轮的消息列表基础上追加内容然后再发一次请求。如果这个追加的过程是干净利落的——只在末尾加东西不改动前面的内容——那么前缀天然是稳定的缓存命中率会很高。但是现实中呢很多 Agent 实现会在这几个地方悄悄改动了前缀‣ 3.1 时间戳、请求 ID 这些动态字段被塞进了前面# 反例system_prompt f你是客服助手。当前时间{datetime.now()}请求 ID{uuid.uuid4()}...(800字角色设定)看起来好像只是加了两行元信息嘛但因为写在了 system prompt 的最前面每次请求这两行都是不同的。这就导致整个 system prompt 从第一个 token 起就不再匹配了800 字的设定全部失效缓存命中率直接归零。回过头看那个面试场景——面试官问system prompt每次请求一模一样就一定能命中吗答案是不一定。哪怕system prompt的内容本身没变如果你把时间戳、请求ID这类动态字段写在了最前面整个前缀从第一个字节起就不同了。这就是开了但不命中最常见的原因之一。‣ 3.2 JSON 序列化的字段顺序不稳定工具定义、工具返回结果这些东西如果你用字典拼装再序列化成字符串塞进 prompt那就有个问题。一旦语言的字典或者序列化库不保证顺序或者说你在不同的地方构造了内容相同但字段顺序不同的字典生成的 JSON 字符串字节就不一样了。比如这样{city: Beijing, temp: 23}跟这样{temp: 23, city: Beijing}这两个字符串在语义上完全一样但在前缀匹配看来它们就是两个完全不同的东西。‣ 3.3 历史消息被事后加工了有一些 Agent 框架呢为了省 token会对历史对话做压缩、做摘要、做删减。比如说把第 3 轮的工具调用结果精简一下什么的。这种回过头去改历史的操作哪怕你只改动了中间某一轮也会导致这一轮之后的所有内容全部失去缓存。因为前缀匹配是从头开始逐块比较的嘛中间断了的话后面即使没变也没有用了。‣ 3.4 工具定义的顺序或者内容随请求变化举个例子有些做法是根据用户意图动态地只传相关的几个工具给模型意图是省 token。但结果就是每次工具列表都不一样。而工具定义在 prompt 里通常排在很靠前的位置一变就是全灭。这在淘天这种电商场景里尤其容易踩坑——比如一个客服Agent用户问订单问题时传订单工具问退款问题时传退款工具看似省了token实际上每次工具列表一变前面的缓存全废了。‣ 3.5 一个真实的对比例子改动之前缓存不友好的版本messages [ {role: system, content: f现在是 {datetime.now()}你是...}, ...]tools pick_relevant_tools(user_intent) # 每次不同改动之后缓存友好的版本messages [ {role: system, content: 你是..., cache_control: {type: ephemeral}}, {role: user, content: f当前时间{datetime.now()}\n{user_question}} # 动态内容挪到最后]tools ALL_TOOLS # 固定全量工具列表不做动态裁剪你看只是把时间戳从系统提示词开头挪到了用户消息末尾把工具列表固定住缓存命中率就能从接近 0% 提升到 90% 以上。◆ ◆ ◆四、Agent 框架该怎么设计才能保住缓存把上面那些反例反过来其实就是设计原则了。可以总结成几条硬规矩‣ 原则一Append-only只加不改历史消息一旦发出去了就不要再动它。如果你有压缩历史的需求尽量做成新增一条摘要消息而不是回头去改写、删除已有的消息。如果一定要做历史压缩的话你得清楚代价——压缩点之后的缓存会重新建立压缩点之前的可以保留。‣ 原则二稳定内容在前易变内容在后请求结构应该设计成一个稳定度递减的顺序[System Prompt几乎不变] → [工具定义较少变化] → [历史对话逐步累加只加不改] → [本轮新消息 / 动态变量时间戳、随机数等放这里]任何每次必变的东西比如时间、随机 ID、用户当前地理位置之类的都应该放在整个 prompt 的最后面让它影响的范围最小。‣ 原则三显式声明缓存断点不要指望引擎去猜如果你的 API 支持显式 cache_control像 Anthropic 那样那你就要主动在合适的位置打断点而不是依赖引擎的自动前缀检测。断点一般打在这些地方◆system prompt 结尾◆工具定义结尾◆每一轮对话历史的末尾用多断点策略方便不同轮次都能命中各自的最长前缀比如说一个多轮 Agent 对话你可以每隔几轮在最新的一条消息上打断点。这样上一次请求缓存的内容就会被这次请求继续复用messages [ {role: user, content: ...}, {role: assistant, content: [...]}, {role: user, content: [ {type: tool_result, content: tool_output}, ]}, { role: user, content: [{type: text, text: latest_message, cache_control: {type: ephemeral}}] # 断点滚动前移 }]‣ 原则四序列化要确定性工具定义、工具返回结果这些凡是需要拼进 prompt 的结构化数据序列化的时候要固定字段顺序。比如 Python 里用 json.dumps(data, sort_keysTrue) 这种方式。并且要保证同一份数据在不同代码路径生成的时候字节是完全一致的。数值格式也要固定比如浮点数保留几位小数日期格式也要固定。不要出现有的地方是 2026-07-12有的地方是 July 12, 2026 这种同义不同形的情况。‣ 原则五工具列表尽量固定不要做动态裁剪出于成本考虑想只传相关工具这个直觉是可以理解的。但是呢工具定义一旦放在 prompt 靠前的位置动态增减带来的缓存损失往往比少传几个工具定义省下的 token 更贵。更好的做法是工具全量固定传真正要控制的是模型该不该调用某个工具。这个可以通过 prompt 里的策略说明去引导而不是从物理上把工具定义抽掉。‣ 原则六多 Agent、多会话之间做前缀级别的路由如果你的系统有多个 Agent 角色比如客服 Agent、订单查询 Agent、退款 Agent 各有一套 system prompt那在做负载均衡的时候就要尽量把同一个前缀的请求路由到同一台或者同一组推理节点上。这也是 SGLang RadixAttention、vLLM 这类引擎在多机部署时会做前缀感知调度的原因。如果你的请求在多台机器之间随机漂移的话即使前缀完全没变KV Cache 不在那台机器上也是一样白搭。◆ ◆ ◆五、一个更完整的例子从高频失效到高命中率的改造改造之前的 Agent 请求构造代码def build_request(user_msg, history, user_intent): system ( f当前时间: {datetime.now()}\n f会话ID: {uuid.uuid4()}\n 你是一名专业客服助手负责... 800字角色设定省略 ) tools select_tools_by_intent(user_intent) # 每次意图不同工具列表不同 messages compress_if_too_long(history) [{role: user, content: user_msg}] return {system: system, tools: tools, messages: messages}这段代码有三个缓存杀手时间戳和会话 ID 放在了 system 最前面、工具列表随意图变化、历史对话会被压缩然后回头改写。改造之后的版本STATIC_SYSTEM_PROMPT 你是一名专业客服助手负责...(800字角色设定固定不变)ALL_TOOLS [...] # 固定全量工具不做动态裁剪def build_request(user_msg, history): system [{ type: text, text: STATIC_SYSTEM_PROMPT, cache_control: {type: ephemeral} # 断点1系统提示词 }] tools ALL_TOOLS # 固定配合断点缓存 # 历史消息只追加不回头修改如需摘要作为新增消息插入而非改写旧消息 messages history [{ role: user, content: [{ type: text, text: f当前时间{datetime.now()}\n{user_msg}, # 动态内容放最后 cache_control: {type: ephemeral} # 断点2滚动到最新一轮 }] }] return {system: system, tools: tools, messages: messages}这个版本里呢系统提示词永远逐字节相同稳定命中。工具定义固定也是稳定命中。历史消息只增不改每一轮新请求都能复用上一轮已经缓存好的全部内容。动态的时间戳被挪到了每轮消息的末尾影响范围被压缩到了最小。在实际压测当中这种改造往往能把一个 5 到 8 轮的 Agent 会话里Prefill 阶段需要重新计算的 token 占比从 60% 到 80% 直接降到 5% 到 15%。这个效果会直接体现在成本和延迟这两个指标上。◆ ◆ ◆六、小结一下Prefix Caching 的原理其实并不复杂。Transformer 推理靠 KV Cache 来避免重复计算只要前缀逐字节相同就能复用引擎用前缀树或者 Radix Tree 这类结构来快速识别可复用的部分。但真正决定你能不能吃到这份红利的从来就不是要不要开缓存这个问题。关键在于你的 Agent 框架有没有把稳定内容和易变内容分开有没有做到历史只增不改序列化是不是确定性的。这几乎是一个纯粹的工程纪律问题——改一个字符的代价可能就是几百上千 token 的重新计算。回到开头那个面试场景面试官问的其实是两个问题Prefix Caching的原理是什么Agent框架怎么保证不破坏缓存。第一个问题答案是KV Cache复用加前缀树匹配。第二个问题的答案就是上面这六条原则。如果当时能把这两层都说出来面试官大概就不会摇头了。下次如果你发现自己的 Agent 缓存命中率上不去不妨从这几个角度回头检查一遍时间戳放哪了工具列表是不是又聪明地动态裁剪了历史消息是不是被悄悄改写过JSON 序列化的顺序稳不稳定往往答案就藏在这些不起眼的细节里。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】