如果你正在用 LLM 做线上服务大概率已经被两样东西折磨过每个月看账单时的心跳加速和用户截图里那个转圈好几秒的 loading。这篇文章聊的是用 LangChain 做生产落地时LLM 缓存的三档对比——无缓存、普通缓存、语义缓存分别解决什么问题、各自有什么坑以及我在实际项目里跑出来的一组数据。内容面向正在做 LLM 应用工程化、被成本和延迟困扰的开发者读完可以直接拿去参考设计。先说一个容易被带偏的概念很多人一听“LLM 缓存”先想到的是推理框架里的 KV Cache / Prefix Cache。那类缓存优化的是 GPU 显存里的中间状态复用属于推理引擎层的事情。这篇文章聊的是应用层缓存把 LLM 的输入输出结果存下来后续请求遇到相同或相似的输入直接返回历史答案省掉一次模型调用。两者可以同时存在但千万别混淆否则你排查缓存命中率的时候会被完全不同的指标带偏。1. 为什么要在生产环境做 LLM 缓存1.1 先算一笔账单假设你负责一个企业内部知识库问答机器人每天真实调用 1 万次每次问答平均消耗输入 500 tokens、输出 300 tokens。按市面上常见商用模型的 API 价格粗算一次请求的模型成本大概在 0.52 分钱之间一天就是 100200 元一个月下来 30006000 元。听起来不多等业务跑到每天 10 万次调用月账单就是几万块起步。更扎心的是这些请求里有相当大一部分是重复的。我做过一个内部员工服务助手上线两个月后拉日志发现用户反复问“年假怎么休”“报销流程是什么”“Wi-Fi 密码怎么改”这类问题换个说法继续问。模型每次都老老实实重新推理一遍花了钱还让用户等了好几秒。延迟那边同样有账。非流式场景下一个复杂问题的首 token 往往要等 1.5 秒以上完整回答 35 秒很正常。对用户来说这已经不是“快慢”的问题而是“到底卡没卡死”的体验问题了。如果缓存命中答案直接从内存或者向量库里捞出来几十毫秒返回体验完全是两个水平。1.2 缓存真正要解决的是“重复问题”做缓存之前必须先搞清楚一个指标重复率。即一段时间内相同或相似的请求占全部请求的比例。如果业务本身几乎没有重复请求比如每次都是让模型写一篇完全不同的文案那缓存收益就很有限如果 30% 以上的请求其实是重复的那缓存绝对值回票价。LangChain 在缓存这件事上给了一套很轻量的官方方案set_llm_cache、SQLiteCache、RedisCache等等。但它默认不开启而且官方内置的是基于精确匹配的普通缓存没有语义匹配能力。所以在生产环境里“要不要开缓存”和“开哪一档缓存”完全取决于你的业务重复形态是“一模一样的问题”占多数还是“意思差不多但说法五花八门”占多数。2. 三档方案全景无缓存、普通缓存、语义缓存2.1 先厘清概念应用层缓存和 KV Cache很多同学上手就查“LLM 缓存”搜到的全是 KV Cache 的资料然后越看越迷糊。这里我把边界划清楚KV Cache 是大模型推理时把已经计算过的 token 的键值对缓存下来避免每生成一个新 token 都重新算一遍前文注意力它的目标是在单次生成中提速应用层缓存则是把“问题答案”整个存下来目标是在多次请求之间省钱提速。本文下面所有讨论都基于应用层缓存。如果你用 LangChain 做业务业务侧的缓存命中率、缓存淘汰、TTL、分布式共享这些才是你能掌控和优化的东西。KV Cache 一般是模型服务侧比如 vLLM、SGLang自动处理的不需要你在业务代码里碰。2.2 三档方案核心差异三档方案的本质区别在于“判定两次请求是否相同”的粒度。维度无缓存普通缓存语义缓存复用判定不判定字符串精确相等向量语义相似实现复杂度无低中高命中率我的项目实测0%5%15%20%40%命中时平均延迟无命中概念50ms 以内100300ms未命中时额外开销无极小一次 Embedding 计算适用场景低频、强随机生成FAQ、固定模板问法多样但意图固定的场景主要风险无缓存大量 miss收益有限误命中返回不相关内容普通缓存和语义缓存并不是替代关系更多是递进关系。我见过不少团队直接上语义缓存结果因为阈值没调好返回了一堆驴唇不对马嘴的答案最后被迫全部关掉。建议按顺序跑先无缓存拿基线再上普通缓存看基础命中率最后再决定要不要加语义层。3. 第一档无缓存先别急着省3.1 无缓存的价值是拿到干净基线很多团队上来就想做缓存我觉得这是本末倒置。缓存本质上是在“用历史答案替换实时计算”如果你连“不缓存时的真实表现”都不清楚后面所有优化都说不清收益。无缓存状态跑一到两周重点采集几个数据请求总量与日增长率去重后的 prompt 比例用于估算重复率上限P50、P95 延迟P95 决定用户体验下限每日 token 消耗总量和估算费用错误率与重试率排除因为异常导致的虚假重复请求采集方式很简单在 LangChain 链路入口加一个日志中间件把请求的 prompt、模型参数、响应耗时、token 用量打到日志系统即可。这一步不需要引入任何缓存组件但它是后面做缓存效果评估的标尺。3.2 什么业务场景可以继续用无缓存并不是所有场景都需要缓存。如果你做的是一次性数据分析、内部调试工具、低频的内容改写每天调用量几百次以内加缓存反而增加维护成本和出 bug 的暴露面。还有一些场景是必须谨慎缓存的强随机生成类任务比如让模型每天生成不同的营销文案、不同的标题语义上的“相似问题”往往需要不同的答案以及强调最新性的任务比如实时政策解读、股票分析旧答案可能已经过期。这类业务我一般建议维持无缓存或者只在极严格的条件下做精确缓存。先想清楚业务是否需要“每次都不一样”再决定要不要往下走。4. 第二档普通缓存LangChain 一把梭4.1 LangChain 自带缓存的正确接法LangChain 官方缓存的使用门槛很低核心就是全局设置一个 cache 实例。老版本里常见的是llm.cache True新版本已经改成了基于 cache 实例的写法。不同版本 API 差异比较大我这里给一个当前社区普遍使用的示意from langchain.globals import set_llm_cache from langchain_community.cache import SQLiteCache set_llm_cache(SQLiteCache(database_path./llm_cache.db)) llm ChatOpenAI(modelgpt-4o-mini, temperature0) answer llm.invoke(报销流程是什么)SQLiteCache会把缓存持久化到本地文件进程重启后仍然有效。除了它LangChain 还提供了InMemoryCache和RedisCache。我的习惯是单机调试用InMemoryCache线下小规模部署用SQLiteCache生产多实例必须上RedisCache或者自己包一层共享存储。一个容易踩的版本坑是如果你同时用了 LCEL 构建 Runnable 链并且调的是链对象缓存是否生效取决于底层 LLM 实例是否走统一缓存入口。建议在接缓存后先手动调用同一个 prompt 两次确认第二次没有真实模型调用再继续往下做。4.2 生产环境精确缓存要改的三个地方默认缓存能用但生产环境直接裸用是不行的至少要处理三件事。第一缓存 Key 设计。LangChain 自带的缓存实际上是用“prompt 原文 模型名 参数”构成的 key 做精确匹配。问题在于很多人用 LangChain 时会把 System Prompt 拼进 messages而不同业务版本可能共用同一个 model 实例导致 System Prompt 更新后旧缓存还在继续命中。在我自己的项目里缓存 key 从来都是手工构造的把所有参与生成的因素——模型名、temperature、max_tokens、system prompt 版本、用户 prompt——按固定顺序序列化后做哈希。import hashlib import json def make_cache_key(messages, model, temperature, prompt_version): payload json.dumps( { messages: messages, model: model, temperature: temperature, prompt_version: prompt_version, }, ensure_asciiFalse, sort_keysTrue, ) return hashlib.sha256(payload.encode(utf-8)).hexdigest()第二存储介质和容量。进程内 cache 会随着请求数量无限膨胀必须设置容量上限和淘汰策略。内存里我一般用 LRU落 Redis 则直接设置 TTL比如 24 小时过期避免缓存空间无限增长。第三命中率统计。没有命中率的缓存就是玄学。在查缓存之前埋点记录 key 是否存在定期算精确命中率。我见过不少团队接了缓存但从未看命中率直到账单没降才来排查结果发现缓存 key 里拼了时间戳永远 miss。4.3 普通缓存的上限命中率不高是常态普通缓存是字符串字典的精确匹配它的天花板很低。用户把“报销流程是什么”改成“报销流程怎么走”在它眼里就是完全不同的两个请求。我实测过知识库问答场景纯精确缓存命中率普遍只有 5%15%空间和 Redis 开销倒是不大但整体省钱效果非常有限。这也解释了为什么需要第三档语义缓存用户的语言表达是五花八门的但问的意图高度集中。只有把“意思相近”也识别出来命中率才能真正上去。5. 第三档语义缓存把“像”变成“是”5.1 语义缓存的工作原理语义缓存的核心思路是把文本变成向量再算相似度。用户问题 A 和用户问题 B 字面完全不一样但向量距离很近说明它们大概率在问同一件事这时候就可以复用 A 的答案。一条语义缓存链路里通常有三个角色编码器负责把问题转成向量常用 bge-m3、text-embedding-3-small 这类模型相似度计算一般用余弦相似度区间在 -1 到 1 之间越接近 1 表示越相似存储保存“问题向量 原始问题 答案”的索引可以是专门的向量数据库也可以是一个简单的内存向量索引当新请求进来先对请求做向量化然后在存储里检索最相似的历史问题如果相似度超过阈值直接把历史答案返回。5.2 工程实现路径框架与自研怎么选语义缓存实现大致分两条路。一条是用现成框架比如 GPTCache。它对 LangChain 有适配包装起来比较快适合团队规模小、不想自己写底层检索的场景。大致思路是把 LLM 包一层from gptcache import Cache from gptcache.adapter.langchain_llms import LangChainLLMs from gptcache.processor.pre import get_prompt from gptcache.similarity_evaluation.np import NumpyNormEvaluation cache Cache() cache.init( pre_embedding_funcget_prompt, embedding_funclambda texts: ..., similarity_evaluationNumpyNormEvaluation(), ) llm LangChainLLMs(llmChatOpenAI(modelgpt-4o-mini, temperature0)) answer llm(prompt报销流程是什么)但说实话如果你项目已经重度使用 LangChain而且要精细控制阈值、连接池、缓存版本我建议自己包一层。自研不复杂核心就是实现一个带缓存判断的 LLM 包装器放在链的最底层。# 示意代码语义缓存包装器 import hashlib import numpy as np from langchain_core.language_models import LLM from langchain_core.outputs import Generation, LLMResult class SemanticCachedLLM(LLM): def __init__(self, llm, embeddings, vector_store, threshold0.92): super().__init__() self.llm llm self.embeddings embeddings self.vector_store vector_store self.threshold threshold property def _llm_type(self): return semantic_cached_llm def _generate(self, prompts, stopNone, run_managerNone, **kwargs): generations [] for prompt in prompts: query_vec self.embeddings.embed_query(prompt) hits self.vector_store.search(query_vec, k1) if hits and hits[0].distance self.threshold: generations.append(Generation(texthits[0].metadata[answer])) continue model_answer self.llm.invoke(prompt) generations.append(Generation(textmodel_answer)) self.vector_store.add( embeddings[query_vec], metadatas[ { prompt: prompt, answer: model_answer, } ], ids[hashlib.md5(prompt.encode(utf-8)).hexdigest()], ) return LLMResult(generations[generations])这里的vector_store.search返回的距离到底是不是余弦相似度取决于你用的向量库 API。比如有些库返回的是欧氏距离那阈值判断逻辑就要反过来距离越小越相似。做的时候一定要先验证检索接口的返回语义别想当然。包装器放进 LangChain 链时也很简单把原来的llm替换成SemanticCachedLLM实例即可上游的 Prompt 模板、输出解析器完全不用动。这是 LangChain 抽象能力带来的最大好处缓存逻辑对业务透明。5.3 阈值、向量模型和存储选型阈值是语义缓存的关键也是玄学。阈值设高了命中保守大量该命中的请求漏掉阈值设低了经常把“像但不是同一问题”的也命中返回牛头不对马嘴的答案。我的习惯是先从一个中间值起步比如 0.92然后在离线验证集上统计两个指标命中召回率该命中的被命中多少和误判率不该命中的被命中多少。误判太多就上调到 0.95漏得太多就下调到 0.88。不同领域的差异非常大专业术语多的场景需要更高阈值口语化对话可以松一点。向量模型选择上中文场景我推荐本地可部署的 bge-m3 或同类模型离线可控、没有外网调用延迟和费用如果业务已经用了商用 embedding 服务也可以直接复用。需要特别提醒的是embedding 模型换版本之后历史缓存的向量分布会变新旧向量之间算相似度会整体漂移。所以 embedding 模型一旦升级要么清空重建缓存要么把 embedding 模型版本加进缓存 key确保新旧数据不混用。存储这块请求量小可以用 Redis 的向量检索能力一套基础设施同时解决普通缓存和语义缓存规模大了再上专门的向量数据库。向量数据同样要设置生命周期否则数据量会越堆越大检索延迟和成本都不可控。6. 三档实测一组可以复现的数据与选型建议6.1 我在项目里跑出来的对比数据以下数据来自我做过的一个内部知识库问答项目场景是员工自助问答平均每天 2000 次请求。模型用了一个商用小模型temperature0请求平均输入 400 tokens、输出 250 tokens。连续跑了一周取均值。指标无缓存普通缓存语义缓存平均命中率0%9%31%命中响应延迟无约 65ms约 180ms未命中响应延迟约 3.8s约 3.8s约 3.9s每周模型费用100%约 91%约 69%是否额外引入向量依赖否否是普通缓存 9% 的命中率其实只是“喝了点汤”语义缓存把命中率拉到 31%费用下降明显但多出来的 180ms 命中响应不算快主要是 embedding 计算和向量检索的链路开销。另外语义缓存未命中时比普通缓存多了一次 embedding 调用这部分成本也要算进总账里。这套数据本身不普适但它揭示了一个规律语义缓存的实际收益上限取决于你业务中“同义不同形”的请求占比。占比高收益大如果你的用户提问本来就高度统一普通缓存可能就够了没必要引入向量链路。6.2 不同业务的选型建议客服问答、知识库检索优先做缓存而且直接上语义缓存。用户问法五花八门但意图高度集中语义缓存命中率提升最明显。代码生成、SQL 生成谨慎做语义缓存。同样的语义描述可能对应完全不同的实现细节缓存很容易把旧方案返回给你造成“看起来对、实际上已过时”的问题。如果一定要做至少只对固定模板类请求做精确缓存。文案创作、头脑风暴缓存价值很低。用户要的就是“每次不一样”语义缓存反而会把结果锁死。医疗、金融等强合规场景安全大于省钱。建议只做带版本号的精确缓存或者干脆无缓存避免语义缓存返回已经过时的结论。7. 生产落地避坑实录7.1 我在生产中踩过的五个坑第一个坑temperature 不是 0 还开缓存。模型有随机性同一个 prompt 两次结果本来就可能不同缓存一旦命中用户会感觉到“同一个问题答案今天和昨天不一样隔几分钟又突然变回去了”。排查起来非常像 bug其实是缓存把随机性冻住了。做缓存前必须把 temperature 设成 0 或开启确定性采样。第二个坑缓存 key 漏了 System Prompt 版本。我见过线上事故运营改了一版 Prompt 想让回答风格更专业结果大部分请求命中旧缓存用户看到的仍然是旧风格答案。后来我把 prompt_version 手工加进缓存 key每次 Prompt 变更必须手动 bump 版本号才算根治。第三个坑语义缓存把“同一意图、不同输入细节”的问题误判为相同。比如“北京明天天气”和“上海明天天气”向量相似度很高如果缓存了北京的答案上海用户会直接拿到北京的天气。解决办法是在语义缓存判断之前先把关键槽位地点、日期、数字等抽出来槽位不同直接跳过缓存。这是很多人压根没注意到的细节。第四个坑多实例部署时本地缓存不一致。用SQLiteCache或InMemoryCache部署多个副本时用户第一次请求落在 A 节点命中第二次请求被负载均衡到 B 节点又 miss缓存效果大打折扣而且要复现问题非常困难。生产环境请用 Redis 或共享存储做集中式缓存。第五个坑embedding 模型升级后向量不可比。我升级过一版 embedding 模型结果线上语义缓存突然大面积 miss。后来查明白是向量空间整体变了历史缓存向量和新向量算相似度已经不在同一个尺度上。处理方式是上线前做缓存预热或者直接把 embedding 模型版本写进缓存 key。7.2 排查清单与监控指标语义缓存接入后光看“命中率”一个指标是不够的我建议至少盯五个hit_rate整体命中率反映缓存收益cost_saved估算的省钱量用命中次数乘以单次调用成本p95_latency命中链路和未命中链路分开统计cache_error命中后答案被用户反馈为错误的比例这个指标能发现阈值过松问题miss_reason区分“从来没缓存过”和“相似度没超过阈值”用于调整阈值方向日志埋点我一般在缓存包装器里做命中、未命中、阈值差分别打日志。排查问题时你会发现有数据的系统和靠感觉的系统完全是两个效率水平。8. 关于缓存的一些大实话做 LLM 缓存这一年多我最大的体会是缓存是放大器不是救世主。如果 prompt 本身写得又长又冗余一次请求塞了 3000 tokens不如先做检索裁剪把真正需要的上下文喂给模型如果模型选型本身就贵得离谱不如先换个小模型跑跑看。缓存解决的是“重复问题”的成本它不会帮你降低首次请求的延迟也不会让首次请求变便宜。从落地节奏上我强烈建议按“无缓存 → 普通缓存 → 语义缓存”逐级推进每一级都拉真实日志看数据不要跳级。直接上语义缓存不是不行而是你缺少基准出了问题根本判断不了是阈值问题、embedding 问题还是链路问题。最后分享一个小技巧缓存冷启动阶段命中率很低可以把历史日志里高频重复的问题离线批量灌进缓存相当于预热业务一上线就能立刻享受到一部分缓存收益。这一步成本极低效果却非常明显。
企业数字化 ERP 产品动态
相关推荐
QT框架下CRC64校验码实现与优化指南 1. CRC64校验码基础概念解析在数据传输和存储领域,校验机制是确保数据完整性的重要手段。CRC64作为循环冗余校验家族中的一员,相比常见的CRC32提供了更强大的错误检测能力。ECMA-182标准定义的CRC64算法采用64位多项式计算,其碰撞概率低至1/2… · 2026/11/4 21:11:14
DeepSeek V4.1 Flash 技术解读:轻量化 AI 模型的高效推理之道 1. 先说清楚:这个 V4.1 Flash 到底是什么来头 最近打开技术社区和聊天群,到处都是 DeepSeek V4.1 Flash、Flash Attention、Flash 这个 Flash 那个的讨论。我本来以为又是哪家厂商整了个营销新词,结果蹲了一圈消息源,发现这事比我… · 2026/11/4 21:10:13
MongoDB分片集群性能优化:Zone划分与读写分离实践 1. MongoDB分片集群性能优化概述作为一名长期奋战在数据库运维一线的工程师,我见过太多团队在MongoDB分片集群使用初期表现良好,但随着数据量增长却陷入性能泥潭的案例。分片集群虽然提供了理论上无限的水平扩展能力,但如果不掌握正确的调优方… · 2026/11/4 21:10:13
Electron+Vue3+TypeScript构建金融桌面应用实践 1. 项目背景与核心价值 十年前我刚入行时,前端开发还停留在jQuery操作DOM的阶段。如今随着工程化体系成熟,我们不仅能构建复杂SPA应用,还能用前端技术栈开发跨平台桌面应用。这个转变背后,是前端工程化从CLI工具链到GUI应用架构的… · 2026/11/4 22:00:52
离散数学核心概念:从命题逻辑到图论应用 1. 离散数学学习笔记:从命题逻辑到图论 作为一名计算机专业的学生,我深刻体会到离散数学在计算机科学中的重要性。这门课程不仅是数据结构、数据库等后续课程的基础,更是培养计算思维和抽象建模能力的核心课程。下面我将分享我在学习《离散数… · 2026/11/4 22:00:52
直流无刷电机AX/BY/CZ相序详解:别再误解为垂直轴线 前阵子在维修群里看到一组照片:一台直流无刷电机的接线盒拆开,六根引出线分别标着 A、X、B、Y、C、Z。有人问了一句:“AX、BY、CZ是按垂直轴线划分的吗?”我第一反应是哭笑不得,但转念一想,这问题其实非常典… · 2026/11/4 21:59:50
LeNet-5从零手撕:理解CNN底层逻辑的母语级入门 1. 为什么今天还要重读LeNet:它不是古董,而是神经网络的“母语词典”你打开任何一本深度学习入门教材,翻到卷积神经网络那一章,十有八九第一张图就是那个由7层结构、不到6万个参数组成的简陋模型——LeNet-5。很多人扫一眼就跳过去… · 2026/11/4 21:59:50
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38