从数据管道到推理服务链路中每个环节的缓存都值得审视基础设施不需要漂亮话。缓存不是加在某个地方就能收工的——它应该出现在延迟最大、成本最高、重复计算最多的每一个节点上。一、从端到端延迟看缓存的机会地图一个典型的 RAG检索增强生成问答系统端到端的请求链路拆解如下环节操作耗时是否可缓存1请求解析与校验1ms否2用户问题 Embedding40ms (GPU)否每次问题不同3向量检索 Top-K 文档8ms是相似问题检索结果相同4文档片段重排序15ms是相同文档不变5Prompt 组装1ms否6LLM 推理生成2800ms (GPU)是结果缓存收益最大7后处理与格式化2ms否在这 7 个环节中3 个环节有缓存机会。如果不加任何缓存端到端延迟约 2866ms。如果在环节 6 命中缓存延迟直接降至 50ms 以内——这就是缓存 98% 的延迟削减效果。但如果不审视链路通常只会看到结果缓存这一个机会。而忽视了环节 3 的检索结果缓存不同用户的相似问题检索 Top-K 结果高度重叠。环节 4 的 Rerank 结果缓存相同文档集合的排序结果不变。数据管道的中间结果缓存文档处理、分块、Embedding 等可以增量复用。flowchart LR subgraph 数据管道 A1[原始文档] -- A2[文档解析] -- A3[智能分块] -- A4[Embedding] -- A5[向量索引] A2 -.-|缓存| C1[(解析结果)] A3 -.-|缓存| C2[(分块结果)] A4 -.-|缓存| C3[(Embedding向量)] end subgraph 推理服务 B1[用户查询] -- B2[Query Embedding] -- B3[向量检索] -- B4[Rerank] -- B5[Prompt组装] -- B6[LLM推理] B3 -.-|缓存| C4[(检索结果)] B4 -.-|缓存| C5[(Rerank结果)] B6 -.-|缓存| C6[(推理结果)] end style C1 fill:#e8f5e9 style C2 fill:#e8f5e9 style C3 fill:#e8f5e9 style C4 fill:#fff3e0 style C5 fill:#fff3e0 style C6 fill:#ffcdd2二、数据管道中的中间结果缓存数据管道Ingestion Pipeline的中间结果缓存是最容易被忽视的。典型流程原始文档 → 格式解析 → 智能分块 → Embedding 向量化 → 写入索引。解析结果缓存对于 PDF/Word/HTML 等富文本文档解析是 CPU 密集操作。一个 50 页的 PDF使用 PyMuPDF 解析需要 2-5 秒。如果文档内容不变解析结果应该被缓存。def parse_document(doc_id: str, raw_data: bytes) - ParsedDocument: cache_key fparse:{doc_id}:{sha256(raw_data).hexdigest()} cached redis.get(cache_key) if cached: return msgpack.unpackb(cached) parsed pymupdf.open(raw_data) result extract_text_and_structure(parsed) redis.setex(cache_key, 86400 * 7, msgpack.packb(result)) # 7天 return result分块结果缓存文档经过分块策略固定长度、语义分块、递归分块等后的结果。分块算法变更时需要全局失效可以通过在 cache key 中加入分块策略版本号实现chunk:{doc_id}:{doc_hash}:{chunk_strategy_version}Embedding 结果缓存这是数据管道中 ROI 最高的缓存。单个文档的 Embedding 计算消耗 GPU 时间且可能被多次使用同一文档被不同的索引引用。详细内容已在第 6 篇文章中讨论。三、检索环节的缓存设计向量检索结果在以下场景高度可缓存高频热点问题知识库类产品中如何入职请假流程等常见问题每天被搜索数百次检索 Top-K 结果完全相同。批量相似查询用户可能对同一个主题连续追问 3-5 个问题这些问题共享相同的向量检索空间。会话内缓存同一会话中用户问题的检索上下文有局部性。检索缓存的实现type SearchCache struct { client *redis.ClusterClient index *VectorIndex ttl time.Duration } func (c *SearchCache) Search(query string, topK int) ([]*Document, error) { key : fmt.Sprintf(search:%x:%d, sha256.Sum256([]byte(query)), topK) cached, err : c.client.Get(ctx, key).Bytes() if err nil { var docs []*Document msgpack.Unmarshal(cached, docs) return docs, nil } // 未命中执行真实检索 docs, err : c.index.Search(query, topK) if err ! nil { return nil, err } // 写入缓存异步 go func() { data, _ : msgpack.Marshal(docs) c.client.Set(ctx, key, data, c.ttl) }() return docs, nil }检索缓存的失效需要与知识库版本绑定。当知识库更新时检索缓存应选择性失效——只清理涉及被修改文档的缓存。但实际上向量检索的 Top-K 结果可能因为任意文档的修改而改变这种选择性失效的维护成本高于收益。实践中更简单的做法是将知识库版本嵌入缓存 Keysearch:{kb_version}:{query_hash}:{topK}当知识库版本变更时旧版本的缓存自然不再被使用。Rerank 环节的缓存设计与此类似。Rerank 模型如 bge-reranker-v2-m3对一组文档-查询对计算相关性分数。如果相同的 query 相同的文档集合Rerank 结果完全可缓存。Key 设计rerank:{reranker_model_version}:{query_hash}:{doc_ids_hash}四、缓存的全链路协同多层缓存在同一条链路中同时存在需要协同管理1命中率传递效应上游缓存命中后下游的检索和推理步骤全部跳过。这意味着下游缓存的命中率统计会夸大——很多请求在上游就返回了根本没有走到下游的缓存层。在构建命中率看板时需要以总请求数为分母而非到达该层的请求数。2一致性窗口不同层的缓存有不同的 TTL 和失效时机推理结果缓存跟随模型版本、知识库版本失效。检索结果缓存跟随知识库版本失效。中间结果缓存跟随原始文档内容和算法版本失效。存在一个时间窗口其中检索缓存已刷新反映了新的知识库但推理结果缓存尚未失。此时会命中旧的推理结果导致回答与当前检索结果不一致。这个窗口的大小取决于主动失效的延迟。通过统一的知识库版本号作为所有缓存层的一级前缀可以避免这个不一致{kb_version}:{layer}:{params}当kb_version变更时所有层的缓存同时全局失效。3缓存穿透的协同防护多层缓存意味着多层穿透风险。如果在推理结果缓存层使用了 singleflight 模式但在检索缓存层没有使用大量并发请求会在检索层产生重复计算。每一层独立做穿透防护是必要的。五、总结缓存审视的 checklist从数据管道到推理服务的全链路缓存审视可以按这个 checklist 逐项排查数据管道文档解析结果是否有缓存分块结果是否有缓存Embedding 向量是否有缓存分块策略和 Embedding 模型版本是否已纳入缓存 Key推理请求入口请求解析是否需要缓存HTTP 请求体是否有缓存空间如 GET 请求的 ETag检索环节高频问题是否命中检索缓存检索缓存是否按知识库版本隔离相同 question 不同 topK 是否分开缓存Rerank 环节query document set 的组合是否有缓存Rerank 模型版本是否纳入 Key推理生成推理结果缓存是否支持精确匹配 语义匹配是否有 singleflight 防护失效策略是否组合了 TTL 版本 主动失效监控每层的命中率、延迟分布、内存占用是否都有 Prometheus 指标是否有端到端的缓存延迟对比命中 vs 未命中缓存在 AI 系统中不是锦上添花的优化而是架构层面的设计决策。一个没有缓存的 AI 推理系统等于每一次用户请求都在烧 GPU 的钱。审视全链路的缓存机会本质上是在审视哪些计算是重复的哪些算力是可以省下来的。
从数据管道到推理服务:链路中每个环节的缓存都值得审视
从数据管道到推理服务链路中每个环节的缓存都值得审视基础设施不需要漂亮话。缓存不是加在某个地方就能收工的——它应该出现在延迟最大、成本最高、重复计算最多的每一个节点上。一、从端到端延迟看缓存的机会地图一个典型的 RAG检索增强生成问答系统端到端的请求链路拆解如下环节操作耗时是否可缓存1请求解析与校验1ms否2用户问题 Embedding40ms (GPU)否每次问题不同3向量检索 Top-K 文档8ms是相似问题检索结果相同4文档片段重排序15ms是相同文档不变5Prompt 组装1ms否6LLM 推理生成2800ms (GPU)是结果缓存收益最大7后处理与格式化2ms否在这 7 个环节中3 个环节有缓存机会。如果不加任何缓存端到端延迟约 2866ms。如果在环节 6 命中缓存延迟直接降至 50ms 以内——这就是缓存 98% 的延迟削减效果。但如果不审视链路通常只会看到结果缓存这一个机会。而忽视了环节 3 的检索结果缓存不同用户的相似问题检索 Top-K 结果高度重叠。环节 4 的 Rerank 结果缓存相同文档集合的排序结果不变。数据管道的中间结果缓存文档处理、分块、Embedding 等可以增量复用。flowchart LR subgraph 数据管道 A1[原始文档] -- A2[文档解析] -- A3[智能分块] -- A4[Embedding] -- A5[向量索引] A2 -.-|缓存| C1[(解析结果)] A3 -.-|缓存| C2[(分块结果)] A4 -.-|缓存| C3[(Embedding向量)] end subgraph 推理服务 B1[用户查询] -- B2[Query Embedding] -- B3[向量检索] -- B4[Rerank] -- B5[Prompt组装] -- B6[LLM推理] B3 -.-|缓存| C4[(检索结果)] B4 -.-|缓存| C5[(Rerank结果)] B6 -.-|缓存| C6[(推理结果)] end style C1 fill:#e8f5e9 style C2 fill:#e8f5e9 style C3 fill:#e8f5e9 style C4 fill:#fff3e0 style C5 fill:#fff3e0 style C6 fill:#ffcdd2二、数据管道中的中间结果缓存数据管道Ingestion Pipeline的中间结果缓存是最容易被忽视的。典型流程原始文档 → 格式解析 → 智能分块 → Embedding 向量化 → 写入索引。解析结果缓存对于 PDF/Word/HTML 等富文本文档解析是 CPU 密集操作。一个 50 页的 PDF使用 PyMuPDF 解析需要 2-5 秒。如果文档内容不变解析结果应该被缓存。def parse_document(doc_id: str, raw_data: bytes) - ParsedDocument: cache_key fparse:{doc_id}:{sha256(raw_data).hexdigest()} cached redis.get(cache_key) if cached: return msgpack.unpackb(cached) parsed pymupdf.open(raw_data) result extract_text_and_structure(parsed) redis.setex(cache_key, 86400 * 7, msgpack.packb(result)) # 7天 return result分块结果缓存文档经过分块策略固定长度、语义分块、递归分块等后的结果。分块算法变更时需要全局失效可以通过在 cache key 中加入分块策略版本号实现chunk:{doc_id}:{doc_hash}:{chunk_strategy_version}Embedding 结果缓存这是数据管道中 ROI 最高的缓存。单个文档的 Embedding 计算消耗 GPU 时间且可能被多次使用同一文档被不同的索引引用。详细内容已在第 6 篇文章中讨论。三、检索环节的缓存设计向量检索结果在以下场景高度可缓存高频热点问题知识库类产品中如何入职请假流程等常见问题每天被搜索数百次检索 Top-K 结果完全相同。批量相似查询用户可能对同一个主题连续追问 3-5 个问题这些问题共享相同的向量检索空间。会话内缓存同一会话中用户问题的检索上下文有局部性。检索缓存的实现type SearchCache struct { client *redis.ClusterClient index *VectorIndex ttl time.Duration } func (c *SearchCache) Search(query string, topK int) ([]*Document, error) { key : fmt.Sprintf(search:%x:%d, sha256.Sum256([]byte(query)), topK) cached, err : c.client.Get(ctx, key).Bytes() if err nil { var docs []*Document msgpack.Unmarshal(cached, docs) return docs, nil } // 未命中执行真实检索 docs, err : c.index.Search(query, topK) if err ! nil { return nil, err } // 写入缓存异步 go func() { data, _ : msgpack.Marshal(docs) c.client.Set(ctx, key, data, c.ttl) }() return docs, nil }检索缓存的失效需要与知识库版本绑定。当知识库更新时检索缓存应选择性失效——只清理涉及被修改文档的缓存。但实际上向量检索的 Top-K 结果可能因为任意文档的修改而改变这种选择性失效的维护成本高于收益。实践中更简单的做法是将知识库版本嵌入缓存 Keysearch:{kb_version}:{query_hash}:{topK}当知识库版本变更时旧版本的缓存自然不再被使用。Rerank 环节的缓存设计与此类似。Rerank 模型如 bge-reranker-v2-m3对一组文档-查询对计算相关性分数。如果相同的 query 相同的文档集合Rerank 结果完全可缓存。Key 设计rerank:{reranker_model_version}:{query_hash}:{doc_ids_hash}四、缓存的全链路协同多层缓存在同一条链路中同时存在需要协同管理1命中率传递效应上游缓存命中后下游的检索和推理步骤全部跳过。这意味着下游缓存的命中率统计会夸大——很多请求在上游就返回了根本没有走到下游的缓存层。在构建命中率看板时需要以总请求数为分母而非到达该层的请求数。2一致性窗口不同层的缓存有不同的 TTL 和失效时机推理结果缓存跟随模型版本、知识库版本失效。检索结果缓存跟随知识库版本失效。中间结果缓存跟随原始文档内容和算法版本失效。存在一个时间窗口其中检索缓存已刷新反映了新的知识库但推理结果缓存尚未失。此时会命中旧的推理结果导致回答与当前检索结果不一致。这个窗口的大小取决于主动失效的延迟。通过统一的知识库版本号作为所有缓存层的一级前缀可以避免这个不一致{kb_version}:{layer}:{params}当kb_version变更时所有层的缓存同时全局失效。3缓存穿透的协同防护多层缓存意味着多层穿透风险。如果在推理结果缓存层使用了 singleflight 模式但在检索缓存层没有使用大量并发请求会在检索层产生重复计算。每一层独立做穿透防护是必要的。五、总结缓存审视的 checklist从数据管道到推理服务的全链路缓存审视可以按这个 checklist 逐项排查数据管道文档解析结果是否有缓存分块结果是否有缓存Embedding 向量是否有缓存分块策略和 Embedding 模型版本是否已纳入缓存 Key推理请求入口请求解析是否需要缓存HTTP 请求体是否有缓存空间如 GET 请求的 ETag检索环节高频问题是否命中检索缓存检索缓存是否按知识库版本隔离相同 question 不同 topK 是否分开缓存Rerank 环节query document set 的组合是否有缓存Rerank 模型版本是否纳入 Key推理生成推理结果缓存是否支持精确匹配 语义匹配是否有 singleflight 防护失效策略是否组合了 TTL 版本 主动失效监控每层的命中率、延迟分布、内存占用是否都有 Prometheus 指标是否有端到端的缓存延迟对比命中 vs 未命中缓存在 AI 系统中不是锦上添花的优化而是架构层面的设计决策。一个没有缓存的 AI 推理系统等于每一次用户请求都在烧 GPU 的钱。审视全链路的缓存机会本质上是在审视哪些计算是重复的哪些算力是可以省下来的。