第28回 RAG 生产扩展——并行、缓存与隐私保护
纸上跑通三千问,上线一夜满屏红。
不是模型忽变笨,只因账本没算清。
前几回我们把 RAG 的“武功”练得像模像样:
会分块、会检索、会重排、会模块化。
可真要上生产,先打你的往往不是论文难点,而是三座大山:
- 并行:用户一多,单线程就会堵成一条长队
- 缓存:同样的问题天天问,不缓存就是烧钱
- 隐私:资料库里往往是内部文档,泄露一次就不是“评测掉分”,而是事故
这一回不讲高深模型,只讲“能让系统活着跑”的账本功夫。
一、并行:能一起做的事,就别排队做
RAG 的典型流程里,很多步骤天然可以并行:
- 多查询检索(第25回)
- 稀疏与稠密两路检索(第26回)
- 多知识库/多索引检索(多源 RAG)
- 多段候选证据的重排(对候选集合打分)
并行的目标不是“更快一点点”,而是“撑得住峰值”。
因为用户不会温柔地一个个来问问题。
二、缓存:把“重复劳动”变成“复用资产”
缓存有两层最常用:
- 检索缓存:同一个 query 的 top-k 结果缓存起来
- 生成缓存:同一个“证据上下文 + 问题”的回答缓存起来
但缓存也有毒:
- 文档更新后缓存会过期(知识漂移)
- 个性化/权限不同的人不该共享缓存(隐私与越权)
所以缓存策略必须带“钥匙”:
- cache key 不仅包含 query,还要包含权限域、版本号
- 过期策略要和知识库更新联动
三、隐私:RAG 不只是“更安全”,也可能“更好偷”
很多人以为“有检索证据”就更安全。
但 2024 的研究指出:RAG 在引入私有数据库的同时,也会引入新的隐私风险与攻击面。1
比如两类现实担忧:
- 数据提取:攻击者通过精心构造提问,把库里的敏感段落一点点套出来
- 权限绕过:检索层或缓存层没做好隔离,A 用户看到 B 用户的证据
也因此出现“隐私保护 RAG”的研究方向:
例如用差分隐私等机制在合理隐私预算下做检索增强。2
这一回我们先不深入数学细节(那要用到更高阶概率论),
只把工程上“最先该做的三件事”立起来:
- 权限隔离(谁能检索谁的文档)
- 结果最小化(只给必要证据,不把整库倒给模型)
- 输出审计(对敏感信息做检测与脱敏)
四、极简可跑代码:并行检索 + LRU 缓存 + 脱敏
下面代码是一个小演示:
- 两个“索引”(模拟两个知识库)并行检索
- 用 LRU 缓存保存检索结果
- 对返回证据做一个非常朴素的脱敏(手机号/邮箱)
import re
from collections import OrderedDict
from concurrent.futures import ThreadPoolExecutor
INDEX_A = [
"产品A 部署文档:内网地址 https://intranet.example ,联系人邮箱 ops@example.com 。",
"故障手册:如果延迟升高,先查缓存命中率与并发队列长度。",
"权限规则:不同部门只能检索自己命名空间下的文档。",
]
INDEX_B = [
"客户支持:客服电话 13800138000 ,工单系统需要二次确认。",
"合规提示:敏感数据不得进入提示词,必要时先脱敏。",
"RAG 上线要做:并行、缓存、监控、回滚。",
]
def redact(text):
text = re.sub(r"\b1[3-9]\d{9}\b", "[PHONE]", text)
text = re.sub(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", "[EMAIL]", text)
return text
class LRU:
def __init__(self, cap=32):
self.cap = cap
self.od = OrderedDict()
def get(self, k):
if k not in self.od:
return None
v = self.od.pop(k)
self.od[k] = v
return v
def put(self, k, v):
if k in self.od:
self.od.pop(k)
self.od[k] = v
while len(self.od) > self.cap:
self.od.popitem(last=False)
def search(index, q):
q = q.strip()
out = []
for s in index:
if q and q in s:
out.append(s)
return out
cache = LRU(cap=8)
def parallel_retrieve(q):
cached = cache.get(q)
if cached is not None:
return {"cached": True, "hits": cached}
with ThreadPoolExecutor(max_workers=2) as ex:
fa = ex.submit(search, INDEX_A, q)
fb = ex.submit(search, INDEX_B, q)
hits = fa.result() + fb.result()
hits = [redact(x) for x in hits]
cache.put(q, hits)
return {"cached": False, "hits": hits}
if __name__ == "__main__":
for q in ["缓存", "邮箱", "电话", "权限"]:
r1 = parallel_retrieve(q)
r2 = parallel_retrieve(q)
print(q, "->", r1)
print(q, "->", r2)
print()
你会看到:
- 第一次检索
cached=False,第二次立刻cached=True - 命中证据会被简单脱敏,避免直接回传敏感字段
真实系统当然比这复杂得多,但核心思想不变:
并行让你扛得住,缓存让你省得起,脱敏让你不出事。
五、上线清单:不讲情怀,讲必做项
给看官一张“最短上线清单”,不做就别上线:
- 监控:检索命中率、延迟、错误率、缓存命中率
- 回滚:检索器/重排器/提示词改动要能一键回退
- 权限:索引分域、查询鉴权、缓存隔离
- 日志:记录证据来源与引用,便于追责与复盘
- 脱敏:输入输出的敏感信息检测与处理
等你把这张清单做实了,再谈“更聪明的检索策略”。
六、小结:生产不是把代码跑起来,是把风险关起来
这一回你要记住:
- 并行让系统扛峰值
- 缓存让系统降成本
- 隐私让系统别翻车
下一回(第29回)我们要面对一个新对手:长上下文模型。
有人说“上下文够长,RAG 该退场”。
到底该不该?
我们拿研究与账本一起算一算。
欲知后事如何,且听下回分解。
幻觉核查
- RAG 的隐私风险与攻击面:可核对 2024 工作对“隐私问题与风险”的讨论范围。1
- 差分隐私 RAG 的方向:可核对相关论文标题与摘要对“privacy-preserving RAG with differential privacy”的描述。2
- 本回代码的脱敏规则非常简化,只用于展示“输出审计”的最小形态,不代表工业级敏感信息识别。
逻辑审计
- 与第27回衔接:第27回讲模块化,本回把“记账与可控”推进到生产维度:并行、缓存、权限、审计。
- 与导读一致:导读强调“可核查与可验证执行”,本回强调“可追溯证据来源与日志”,以应对真实世界风险。
- 为第29回铺路:生产的核心是成本与风险;长上下文 vs RAG 的讨论,本质也是成本、可靠性与可控性的权衡。
引用与溯源
Footnotes
-
Zeng, S., et al. The Good and The Bad: Exploring Privacy Issues in Retrieval-Augmented Generation (RAG) arXiv:2402.16893 (2024-02) https://arxiv.org/abs/2402.16893 ↩ ↩2
-
Koga, T., et al. Privacy-Preserving Retrieval-Augmented Generation with Differential Privacy arXiv:2412.04697 (2024-12) https://arxiv.org/abs/2412.04697 ↩ ↩2