尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

淘天一面:先说下Prefix Caching 的原理然后再说下 Agent 框架怎么不破坏缓存?我说开启prompting caching就自动命中,他摇了摇头...
📅 发布时间:2026/7/16 1:05:04

前段时间有个师妹去面淘天,一面技术面聊到了大模型推理优化。面试官问了一个看起来很基础的问题:"说下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。

然后第二次请求,用户又发了一句话,对话历史变成了 A+B:

[系统提示词 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( model="claude-sonnet-4-6", system=[ { "type": "text", "text": SYSTEM_PROMPT, # 800 tokens 的角色设定 "cache_control": {"type": "ephemeral"} # 缓存断点 } ], tools=[...], # 工具定义也可以单独打断点 messages=conversation_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_keys=True) 这种方式。并且要保证同一份数据在不同代码路径生成的时候字节是完全一致的。数值格式也要固定,比如浮点数保留几位小数,日期格式也要固定。不要出现有的地方是 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%免费】

相关新闻

  • 帝舵中国官方售后服务中心|官方地址及24小时售后电话权威信息通告(2026年7月最新) - 帝舵中国官方服务中心
  • 【华中师范大学、华南师范大学联合主办 | SPIE出版,往届快至会后2.5个月见刊,刊后1.5个月检索 |邀请多位高校专家作主题报告】第五届图像处理、目标检测与跟踪国际学术会议(IPODT 2026)
  • 爱彼中国官方售后服务中心|服务热线及详细地址权威信息声明(2026年7月最新) - 爱彼中国官方服务中心

最新新闻

  • Anaconda 虚拟环境全流程实战:从创建、管理到迁移部署
  • UE5多线程编程实战:AsyncTask避坑指南与性能优化
  • 2026 全栈运维可观测解决方案选型指南:主流产品能力对比与落地分析
  • 2026年七年级数学暑假有理数总复习|高清无水印可打印资料下载指南 - 时时资讯
  • 2026年七年级数学暑假复习|整式加减强化资料(高清无水印、可直接打印) - 时时资讯
  • GodMode9 Lua脚本:3DS文件自动化管理与效率提升实战

日新闻

  • Toon Boom Harmony 高效工作流:从节点视图到镜头标记的实战技巧
  • 真力时售后维修电话,为您提供专业腕表保养与故障修复服务权威公示(2026年7月最新) - 亨得利官方服务中心
  • 【C++】类和对象--构造函数进阶(初始化列表与explicit)

周新闻

  • IX9104 PCIe5.0 高速交换芯片@ACP#完整规格 + 应用场景总结
  • Unity游戏集成Coze智能体:实现NPC智能对话与知识库联动
  • SAP EPIC 建行回单查询:从标准类CL_EPIC_EXAMPLE_CN_CCB_GHTD到Z类的5处关键修改

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号