第28回 RAG 生产扩展——并行、缓存与隐私保护

纸上跑通三千问,上线一夜满屏红。
不是模型忽变笨,只因账本没算清。

前几回我们把 RAG 的“武功”练得像模像样:
会分块、会检索、会重排、会模块化。

可真要上生产,先打你的往往不是论文难点,而是三座大山:

  1. 并行:用户一多,单线程就会堵成一条长队
  2. 缓存:同样的问题天天问,不缓存就是烧钱
  3. 隐私:资料库里往往是内部文档,泄露一次就不是“评测掉分”,而是事故

这一回不讲高深模型,只讲“能让系统活着跑”的账本功夫。


一、并行:能一起做的事,就别排队做

RAG 的典型流程里,很多步骤天然可以并行:

  • 多查询检索(第25回)
  • 稀疏与稠密两路检索(第26回)
  • 多知识库/多索引检索(多源 RAG)
  • 多段候选证据的重排(对候选集合打分)

并行的目标不是“更快一点点”,而是“撑得住峰值”。
因为用户不会温柔地一个个来问问题。


二、缓存:把“重复劳动”变成“复用资产”

缓存有两层最常用:

  1. 检索缓存:同一个 query 的 top-k 结果缓存起来
  2. 生成缓存:同一个“证据上下文 + 问题”的回答缓存起来

但缓存也有毒:

  • 文档更新后缓存会过期(知识漂移)
  • 个性化/权限不同的人不该共享缓存(隐私与越权)

所以缓存策略必须带“钥匙”:

  • cache key 不仅包含 query,还要包含权限域、版本号
  • 过期策略要和知识库更新联动

三、隐私:RAG 不只是“更安全”,也可能“更好偷”

很多人以为“有检索证据”就更安全。
但 2024 的研究指出:RAG 在引入私有数据库的同时,也会引入新的隐私风险与攻击面。1

比如两类现实担忧:

  • 数据提取:攻击者通过精心构造提问,把库里的敏感段落一点点套出来
  • 权限绕过:检索层或缓存层没做好隔离,A 用户看到 B 用户的证据

也因此出现“隐私保护 RAG”的研究方向:
例如用差分隐私等机制在合理隐私预算下做检索增强。2

这一回我们先不深入数学细节(那要用到更高阶概率论),
只把工程上“最先该做的三件事”立起来:

  1. 权限隔离(谁能检索谁的文档)
  2. 结果最小化(只给必要证据,不把整库倒给模型)
  3. 输出审计(对敏感信息做检测与脱敏)

四、极简可跑代码:并行检索 + 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

  1. 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

  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