项目案例

AI 时代的数据架构:RAG 知识库的高可用治理

千万级企业文档的检索治理、混合召回与可观测闭环——从 0 到 1 的 RAG 工程化落地全链路

一、业务全景:企业 RAG 落地的 4 类典型问题

1.1 业务驱动力:从 Demo 到生产的鸿沟

过去 18 个月,我作为架构师主导了集团级 RAG 知识库从 PoC 到全量生产的全过程。最初两个月,我们用 LangChain + Chroma 搭了一个能跑的 Demo,效果惊艳;但当我们把 3 个业务线、27 个知识源、1800 万份文档接入后,整个系统几乎崩溃。

问题不在算法,而在工程化治理。企业场景下 RAG 不是"接个 LLM + 做个向量检索"那么简单,它是一个长期运行的复杂数据系统——有数据接入、有索引更新、有检索路由、有质量反馈、有权限边界、有降级兜底。任何一个环节失控,都会让 LLM 从"智能助手"退化成"幻觉生成器"。

1.2 4 类典型生产问题

在和 6 个业务方共 4 轮深度访谈后,我把企业 RAG 落地中最棘手的问题归纳为四类。这些问题在 Demo 阶段几乎不会出现,但一旦进入生产就会集中爆发:

问题类别 典型表现 业务影响 治理难度
数据源混乱 MySQL 业务表、Confluence 文档、S3 PDF、企微聊天记录共存,更新频率从秒级到季度级 同一问题多个版本答案,互相矛盾 ★★★★★
检索不准 纯向量检索对专有名词、缩写、错误检索鲁棒性差;BM25 对长尾语义召回不足 Top-5 命中率 60%,业务方不信任 ★★★★
响应不可控 千万级文档下,P99 延迟从 800ms 恶化到 6s,节假日高峰期雪崩 用户放弃使用,沦为内部玩具 ★★★★★
幻觉与过期 LLM 基于旧版本文档给出过期答案;面对未覆盖问题时自由发挥 合规风险,财务、法务场景不可接受 ★★★★★

1.3 架构治理总览

针对上述四类问题,我们设计了一套分层治理架构。核心思路是:把 RAG 当成一个数据系统来设计,而不是"LLM + 检索"的简单拼装。

flowchart TB subgraph SRC[数据源层] S1["结构化
MySQL/TiDB"] S2["文档类
Confluence/语雀"] S3["时序/日志
Kafka"] S4["消息/IM
企微SDK"] end subgraph ETL[接入治理层 · ETL Pipeline] E1["解析
SmartDocParser"] E2["清洗
去噪/归一化"] E3["元数据注入
血缘+权限标签"] end subgraph IDX[索引层 · 多粒度索引] I1["倒排索引
ES"] I2["向量索引
Milvus"] I3["摘要索引
TiDB"] I4["元数据KV
权限/版本"] end subgraph RET[检索层 · 混合召回] R1["Query 改写"] R2["意图路由"] R3["BM25 + 向量 + Rerank"] R4["Cache"] end subgraph ORCH[编排层 · LLM Orchestration] O1["Prompt 模板"] O2["多模型协同
Cascade"] O3["SSE 流式"] O4["工具调用/敏感过滤"] end subgraph FB[反馈层 · 质量闭环] F1["用户反馈"] F2["标注平台/评测集"] F3["版本管理/知识更新"] end S1 & S2 & S3 & S4 --> E1 --> E2 --> E3 E3 --> I1 & I2 & I3 & I4 I1 & I2 & I3 & I4 --> R1 --> R2 --> R3 R3 -.命中缓存.-> R4 R3 --> O1 --> O2 --> O3 --> O4 O4 --> F1 --> F2 --> F3 F3 -.回流.-> E1 style S1 fill:#1a1a2e,stroke:#00d4ff,stroke-width:2px style S2 fill:#1a1a2e,stroke:#00d4ff,stroke-width:2px style S3 fill:#1a1a2e,stroke:#00d4ff,stroke-width:2px style S4 fill:#1a1a2e,stroke:#00d4ff,stroke-width:2px style E1 fill:#16213e,stroke:#7b61ff style E2 fill:#16213e,stroke:#7b61ff style E3 fill:#16213e,stroke:#7b61ff style I1 fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style I2 fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style R3 fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style O2 fill:#16213e,stroke:#7b61ff style F2 fill:#16213e,stroke:#7b61ff

1.4 关键约束:成本与 SLA

在动手之前,必须先明确两个硬约束,否则架构会被业务方反复推翻:

// 硬约束:SLA 与成本基线
const sla = {
  p99_latency_ms:  2000,    // 端到端 P99 < 2s(包含 LLM 推理)
  p95_latency_ms:  1500,    // P95 < 1.5s
  availability:    0.999,   // 99.9% 月度可用
  recall_at_10:    0.92,    // 检索召回率 @Top-10 >= 92%
  answer_accuracy: 0.88,    // 端到端答案准确率 >= 88%
  hallucination:   0.03,    // 幻觉率 < 3%
};

const cost = {
  // 单次对话成本上限(元)
  per_query_yuan:    0.15,
  // 月度预算上限
  monthly_budget:    80000,
  // 知识库规模
  corpus_size:       '1800万 chunks',
  // 业务方数量
  tenants:           6,
};

💡 架构深挖点

  1. 为什么 RAG 不能直接套用传统数据仓库的 SLA 模型?答案藏在 LLM 的"非确定性"里——同样的输入可能得到不同质量的输出,这对 SLA 设计意味着什么?
  2. 业务方提出"答案必须 100% 准确"时,架构师该如何用分层置信度替代二元答案?这背后是产品形态的重构。
  3. 当知识源多达 27 个时,统一元数据治理的最小公共模型应该长什么样?过度抽象与过细粒度的边界在哪里?

二、知识接入:多源异构数据的统一治理

2.1 业务背景:27 个数据源的真实样貌

企业知识治理的第一战不是"接 LLM",而是"接数据"。我们面对的 27 个数据源可以分为四类,每一类的更新频率、信任级别、权限边界都不同:

  • 结构化数据:MySQL 业务表(产品库、价格库、用户库)、TiDB(订单)、PolarDB(财务)
  • 文档类:Confluence(产品手册)、SharePoint(内部规范)、语雀(团队 Wiki)
  • 对象存储:S3/COS 上的 PDF/Word/Excel/PPT(合同、报告、白皮书)
  • 消息流:企微会话记录、工单系统、邮件归档

如果直接用 LangChain 的 27 个 Loader 各接各的,三个月后没人能回答"这条知识来自哪里、什么时候更新过、谁有权访问"。所以第一步必须建立统一知识元模型

2.2 统一知识元模型 (Unified Knowledge Schema)

我们对所有数据源做了一次抽象,提取出六元组最小公共模型。任何数据源接入时都必须映射到这个模型:

// 统一知识单元 (KnowledgeUnit) - 跨数据源的最小公共表示
type KnowledgeUnit = {
  unit_id:        string;    // 全局唯一 ID (ULID)
  tenant_id:      string;    // 多租户隔离
  source_type:    'db' | 'doc' | 'file' | 'im';
  source_id:      string;    // 原始数据源 ID
  content_hash:   string;    // 内容指纹 (sha256)
  content:        string;    // 归一化后的纯文本
  metadata: {
    title:         string;
    author:        string[];
    created_at:    ISODate;
    updated_at:    ISODate;
    language:      'zh' | 'en' | 'mixed';
    domain:        string[];   // 业务域标签: ['finance', 'legal', ...]
    sensitivity:   0 | 1 | 2 | 3;  // 敏感度 0=公开 3=机密
    expire_at?:    ISODate;    // 过期时间(合同、价格表)
  };
  lineage: {
    upstream_ids:  string[];   // 上游知识单元(用于溯源)
    pipeline_ver:  string;     // 处理 pipeline 版本
    sync_strategy: 'realtime' | 'cdc' | 'batch' | 'manual';
  };
  acl: {
    read_roles:    string[];   // 角色级 RBAC
    field_mask:    Record<string, 'hidden' | 'masked' | 'plain'>;
  };
};

2.3 接入策略:四种同步模式

不同数据源的更新频率差异巨大(毫秒级 CDC vs 季度级合同),我们设计了四种同步模式,关键决策是不强求"实时"

同步模式 适用场景 延迟 实现成本 代表数据源
CDC (Change Data Capture) 高频变更业务表 < 5s 高(需部署 Debezium) MySQL 产品表、订单表
Webhook / 事件驱动 支持回调的 SaaS 10s - 1min Confluence、语雀、企微
定时批处理 全量或大批量文件 1h - 24h 低(Airflow 调度) S3 PDF 库、SharePoint
手动触发 低频且合规要求高 人工 极低 合同模板、法务文档

2.4 文档解析:被低估的脏活

真实场景下,50% 的"检索不准"问题其实源于解析阶段。我们踩过最深的坑是:把 PDF 直接喂给 PyPDFLoader,结果表格变成乱码、目录页被当成正文、页眉页脚污染向量。一个 100 页的财报,最终进入向量库的"有效 chunk"只有 60%。

下图展示了四种同步模式如何统一接入 ETL Pipeline,核心是异步、解耦、可观测

flowchart LR subgraph DS[数据源] DB[(MySQL/PolarDB
业务库)] DC[(Confluence/语雀
Wiki)] S3[(S3/COS
PDF/Word)] IM[企微IM/工单] end subgraph SYNC[同步模式] SY1["CDC
<5s"] SY2["Webhook
10s-1min"] SY3["定时批
1-24h"] SY4["手动触发
人工"] end subgraph ETL[ETL Pipeline] E1[解析
SmartDocParser] E2[清洗
去噪/归一化] E3[Chunking
语义切分] E4[Embedding
向量化] end subgraph IDX[索引] IDX1[(Milvus
向量)] IDX2[(ES
倒排)] IDX3[(TiDB
元数据)] end DB --> SY1 DC --> SY2 S3 --> SY3 IM --> SY2 SY1 & SY2 & SY3 & SY4 --> E1 --> E2 --> E3 --> E4 E4 --> IDX1 & IDX2 & IDX3 style SY1 fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style E3 fill:#16213e,stroke:#7b61ff style E4 fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style IDX1 fill:#16213e,stroke:#7b61ff style IDX2 fill:#16213e,stroke:#7b61ff

后来我们自研了 SmartDocParser,核心思路是版面分析 + 结构化重建

// SmartDocParser:基于版面分析的文档解析
class SmartDocParser {
  async parse(file: File, docType: 'pdf' | 'docx' | 'html'): Promise<Document> {
    // 1. 版面分析 (Layout Analysis)
    //    使用 LayoutLMv3 识别页眉/页脚/目录/正文/表格/图片
    const layout = await this.layoutModel.predict(file);

    // 2. 噪声剔除
    const cleanedPages = layout.pages.map(p => ({
      ...p,
      blocks: p.blocks.filter(b =>
        !b.isHeader && !b.isFooter && !b.isPageNum
      ),
    }));

    // 3. 表格重建(关键:避免表格被切碎成"行")
    const tables = await this.tableExtractor.extract(cleanedPages);
    //    表格转 Markdown 保留列对齐语义

    // 4. 图片 OCR + 文字描述
    //    使用 Qwen-VL 生成图片文字描述,一并进入向量
    const imageCaptions = await this.vlm.caption(layout.images);

    // 5. 重建结构化文档
    return {
      title: layout.title,
      sections: this.buildSectionTree(cleanedPages, tables, imageCaptions),
      rawHash: sha256(file),
    };
  }
}

💡 架构深挖点

  1. 多源接入时,字段命名冲突如何收敛?比如不同业务方都定义了"客户"字段,但口径完全不同——是用全局字典还是保留源系统命名?
  2. 当 S3 上的 PDF 突然被业务方修改了文件名但内容没变时,如何避免重复入库?内容指纹 (content_hash) 是答案,但向量库去重还有哪些边界情况?
  3. 敏感度分级 (sensitivity) 应该由数据源决定还是由字段内容决定?当一张表同时包含公开和机密字段时,权限如何下钻到字段级?

三、Chunking 策略:语义切分与多粒度索引

3.1 为什么 Chunking 决定了 RAG 的天花板

Chunking 是 RAG 中"看似简单、实则最影响召回质量"的环节。我们的 A/B 测试显示:

  • 从 256 token 切到 512 token:Top-5 命中率从 71% 提升到 84%,但单 chunk 噪声变大
  • 从固定切分改成语义切分:长文档召回率再提升 6%,但索引时间增加 3 倍
  • 加入父-子 chunk 多粒度索引:P99 延迟几乎不变,但回答上下文丰富度显著提升

结论是:Chunking 不是一刀切,而是文档类型驱动的策略组合

3.2 切分策略对比

切分策略 原理 优点 缺点 适用场景
固定长度切分 按 N token 硬切,重叠 K token 实现简单,速度快 切断句子/段落,语义不完整 纯日志、代码片段
按段落/标题切分 利用 Markdown 标题、段落边界 保留语义完整性 依赖文档结构化程度 Confluence、博客、产品手册
语义切分 (Semantic Splitting) Embedding 相似度突变处切分 自适应语义边界 计算量大,索引慢 3-5x 长报告、合同、白皮书
滑动窗口 + 父-子索引 父 chunk 提供上下文,子 chunk 用于精确召回 兼顾精度与上下文 存储翻倍 高价值文档(财报、法务)

3.3 我们的实现:四级切分器

针对不同文档类型,我们实现了 4 个切分器,由 router 根据文档特征自动选择:

// 切分器路由:按文档特征选择策略
class ChunkingRouter {
  pickStrategy(doc: ParsedDocument): Chunker {
    if (doc.isStructured && doc.tables.length > 0) {
      return new TableAwareChunker({
        // 表格整行保留,避免被切断
        preserveTable: true,
        chunkSize: 512,
        overlap: 64,
      });
    }
    if (doc.language === 'code' || doc.isCodeHeavy) {
      return new ASTChunker({
        // 按函数/类切分,保留 import 上下文
        granularity: 'function',
        includeContext: ['imports', 'docstring'],
      });
    }
    if (doc.totalTokens > 8000) {
      // 长文档走语义切分
      return new SemanticChunker({
        embeddingModel: 'bge-large-zh',
        similarityThreshold: 0.72,
        minChunk: 256,
        maxChunk: 768,
        // 关键:父-子双粒度
        parentChild: {
          parentSize: 2048,
          childSize: 256,
          overlap: 32,
        },
      });
    }
    return new RecursiveChunker({
      // 短文档按段落/句子递归切分
      separators: ['\n##', '\n###', '\n\n', '。', '. ', ' '],
      chunkSize: 384,
      overlap: 48,
    });
  }
}

3.4 父-子索引:小 chunk 召回 + 大 chunk 喂给 LLM

这是 RAG 实践中最重要的"一招鲜"。核心矛盾是:小 chunk 检索精度高,但上下文不够;大 chunk 上下文全,但检索不准。父-子索引的解法是:

// 父-子双粒度索引:检索用子,喂给 LLM 用父
interface IndexResult {
  // 检索时用:子 chunk(小、精确)
  childChunks: {
    chunkId: string;
    text: string;
    parentId: string;     // 指向父 chunk
    embedding: number[];  // 用于向量检索
  }[];
  // 命中后用:父 chunk(大、上下文全)
  parentChunks: Map<string, {
    parentId: string;
    text: string;          // 完整段落/章节
    childrenIds: string[];
  }>;
}

// 检索流程
async function hybridRetrieve(query: string, topK = 10) {
  // 1. 用子 chunk 做混合检索
  const childHits = await this.retrieveChildren(query, topK);

  // 2. 去重:取子 chunk 的 unique parent
  const parentIds = [...new Set(childHits.map(h => h.parentId))];

  // 3. 取出父 chunk 全文
  const parents = await this.getParents(parentIds);

  // 4. 父 chunk 喂给 LLM
  return parents.map(p => ({
    content: p.text,
    source: p.childrenIds,
    score: this.aggregateScore(p.childrenIds, childHits),
  }));
}

3.5 Chunking 质量评估

没有评估就没有优化。我们用三组指标监控 chunking 质量:

// Chunking 质量评估 - 离线评测集
class ChunkingEvaluator {
  metrics = {
    // 1. 边界准确率:人工标注的"该切分点"是否被正确识别
    boundaryF1: 0.0,
    // 2. 检索召回率:在该 chunking 方案下,Top-10 命中标注答案的比例
    recallAtK:  { k10: 0.0, k20: 0.0, k50: 0.0 },
    // 3. 上下文完整度:被命中的 chunk 是否能独立回答问题(GPT-4 评分)
    contextCompleteness: 0.0,
  };

  async run(testSet: QAPair[]) {
    // 用 500 条人工标注的 QA 对做评测
    // 每周回归,发现 chunking 策略退化
  }
}

💡 架构深挖点

  1. 当文档同时包含中文、英文、代码、公式时,统一用一种 embedding 模型是否合理?多语言模型 vs 分语种专门模型,成本-精度曲线如何?
  2. Chunking 之后,原文表格中的数字列在语义切分时可能被切到不同 chunk,导致 LLM 算错。结构化数据的 chunk 切分该用怎样的策略?
  3. 父-子索引让存储翻倍,但实际查询中可能 80% 的子 chunk 永远不会被命中。冷热分层在这里是否适用?

四、混合检索:BM25 + 向量 + 重排序的召回-精度平衡

4.1 纯向量检索的三大失败模式

我们做过严格的对比实验:纯向量 vs BM25 vs 混合,在 1800 万 chunk 的真实语料上:

  • 专有名词/缩写失败:用户搜"K8s 故障排查",向量模型不认识 K8s (Kubernetes),但 BM25 完全命中
  • 低频长尾词失败:内部产品代号"XM-9",在训练语料中几乎不出现,embedding 几乎随机
  • 精确匹配失败:用户问"价格 999 是不是真的",向量给了语义相近的"价格优惠",但没给"999 元"原话

纯向量不是银弹,必须叠加稀疏检索。这就是 hybrid search 的工程价值。

4.2 混合检索架构

flowchart LR Q([用户 Query]) --> QR[Query Rewriter
改写/扩展/HyDE] QR --> P[并行召回] P --> BM[BM25 召回
ES · Top 50] P --> VS[向量召回
Milvus · Top 50] P --> ST[结构化召回
元数据过滤] BM --> RRF{RRF 融合
k=60} VS --> RRF ST --> RRF RRF --> RE[Cross-Encoder
Rerank] RE --> TOP[Top-10 排序] TOP --> LLM[送入 LLM 生成] style QR fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style BM fill:#16213e,stroke:#7b61ff style VS fill:#16213e,stroke:#7b61ff style RRF fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style RE fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style LLM fill:#16213e,stroke:#7b61ff

4.3 RRF 融合:为什么不用加权打分

把 BM25 和向量打分"加权求和"看似合理,实际上很糟糕——两种打分的分布完全不同(BM25 0-30,向量 0-1),权重调参是噩梦。

我们用 Reciprocal Rank Fusion (RRF)——只看排名不看分数,鲁棒得多:

// RRF 融合:对每个 chunk 的多个来源排名做倒数加权
function rrfFusion(
  bm25Hits: SearchHit[],
  vectorHits: SearchHit[],
  k = 60  // 关键超参数
): SearchHit[] {
  const scores = new Map<string, number>();

  // BM25 排名贡献
  bm25Hits.forEach((hit, rank) => {
    scores.set(hit.id, (scores.get(hit.id) || 0) + 1 / (k + rank + 1));
  });
  // 向量排名贡献
  vectorHits.forEach((hit, rank) => {
    scores.set(hit.id, (scores.get(hit.id) || 0) + 1 / (k + rank + 1));
  });

  return [...scores.entries()]
    .sort(([, a], [, b]) => b - a)
    .map(([id, score]) => ({ id, score }));
}

// 关键技巧:归一化前先按 source 过滤
// 比如"内部技术文档"类问题,应该让 BM25 权重更高
function adaptiveRRF(query, bm25Hits, vectorHits, ctx) {
  if (ctx.tenantId === 'tech-docs') {
    return rrfFusion(bm25Hits, vectorHits, k = 30);  // BM25 更敏感
  }
  return rrfFusion(bm25Hits, vectorHits, k = 60);
}

4.4 重排序 (Rerank) 的杠杆效应

Cross-Encoder Rerank 是 RAG 系统的"投入产出比之王"。我们用 bge-reranker-large 做精排,效果惊人:

阶段 方法 NDCG@10 延迟 (P50) 成本
召回 纯 BM25 0.61 80ms
召回 纯向量 0.68 120ms
召回 混合 (RRF) 0.76 180ms
精排 + bge-reranker 0.89 +90ms
精排 + 多路召回去重 0.90 +30ms

用一个 90ms 的 rerank,把 NDCG@10 从 0.76 拉到了 0.89。这是"小改动、大杠杆"的典型

4.5 Query 改写:被忽视的隐性收益

用户真实 query 往往是残缺的——缺主语、省略上下文、用代词。我们用 LLM 做 query 改写,效果稳定提升 5-8%:

// Query 改写:3 种策略组合
class QueryRewriter {
  async rewrite(rawQuery: string, ctx: ConversationContext) {
    // 1. 指代消解
    //    "这个怎么用?" → "RAG 知识库这个产品怎么用?"
    const resolved = await this.resolveCoreference(rawQuery, ctx.history);

    // 2. HyDE (Hypothetical Document Embeddings)
    //    先用 LLM 生成"假想答案",再用答案 embedding 去检索
    //    对没有匹配文档的新查询特别有效
    const hyde = await this.hyde(resolved);

    // 3. 多 Query 扩展
    //    "RAG 检索" → ["RAG 检索原理", "RAG 检索优化", "混合检索"]
    const expanded = await this.expandQueries(resolved, n = 3);

    return { original: rawQuery, resolved, hyde, expanded };
  }
}

💡 架构深挖点

  1. 当 query 涉及多意图("对比 A 和 B 的区别")时,混合检索的 Top-K 应该返回并集还是分意图?这关系到后续 LLM 怎么组织答案。
  2. RRF 的 k 参数(通常 60)在长尾 query 上是否应该动态调整?业务方反馈"冷门问题召回差",是不是 k 太小导致 BM25 排名靠后的文档被截断?
  3. Cross-Encoder Rerank 延迟 90ms,能否用更小的蒸馏模型在 20ms 内逼近相同效果?TPP (Time-Performance Product) 优化的边界在哪里?

五、LLM 编排:意图路由与多模型协同

5.1 编排层的真实职责

很多人把 LLM 编排理解成"调个 OpenAI API",但企业级编排远不止于此。它是整个 RAG 系统的"大脑皮层",承担五大职责:

① 上下文管理:把检索回来的文档、用户历史对话、系统 Prompt 拼装成 LLM 能理解的输入。这需要token 预算控制文档排序冲突检测等细节处理。

② 路由决策:根据 query 类型选择不同的处理路径——闲聊走轻量模型,深度分析走 GPT-4,多模态调用视觉模型。

③ 重试与降级:LLM 调用失败(限流/超时/内容审查)时,自动降级到备用模型或缓存答案。

④ 工具调用:让 LLM 调用外部 API(搜索/计算/数据库查询),实现 Agent 能力。

⑤ 流式输出与可观测:SSE/WebSocket 流式推送给前端,同时记录完整链路用于问题排查。

5.2 意图路由设计:从关键词到语义理解

意图路由是编排层的核心决策点。早期我们用关键词匹配("价格"→查产品库),但这种方式维护成本高,扩展性差。后来演进到小模型分类(BERT 分类器),现在主流方案是LLM 自路由

flowchart TD Q([用户 Query]) --> CACHE{缓存命中?
5min 内} CACHE -->|是| RT1[复用决策
直接返回] CACHE -->|否| LLM[LLM 自路由
gpt-4o-mini · temperature=0] LLM --> CONF{confidence
>= 0.7?} CONF -->|是| ROUTE{意图分类} CONF -->|否| FALLBACK[降级到 RAG
最常见路径] ROUTE -->|chat| P1[闲聊/通用
轻量模型] ROUTE -->|rag| P2[RAG 检索
混合召回] ROUTE -->|tool| P3[工具调用
Agent 链路] ROUTE -->|code| P4[代码生成
专用模型] P1 & P2 & P3 & P4 --> SAVE[写入 LRU Cache] style LLM fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style ROUTE fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style P2 fill:#16213e,stroke:#7b61ff style FALLBACK fill:#16213e,stroke:#7b61ff style CONF fill:#0a0e27,stroke:#00d4ff,stroke-width:2px
// 意图路由引擎 - 实际生产代码示例
class IntentRouter {
  private llm: LLMClient;
  private cache: LRUCache<string, RouteDecision>;

  async route(query: string, ctx: Context): Promise<RouteDecision> {
    // 1. 缓存优先:相同 query 5 分钟内复用
    const cached = this.cache.get(query);
    if (cached && Date.now() - cached.ts < 300_000) return cached.decision;

    // 2. LLM 路由:让 LLM 自己判断走哪条路径
    const prompt = `你是意图分类器。根据用户 query 决定走哪条处理路径。
- "chat": 闲聊/问候/通用知识
- "rag": 需要查知识库的专业问题
- "tool": 需要调用工具(计算/搜索/数据库)
- "code": 代码相关(生成/调试/解释)

用户 query: ${query}
对话上下文: ${JSON.stringify(ctx.history.slice(-3))}

返回 JSON: {"intent": "chat|rag|tool|code", "confidence": 0-1, "subIntent": "可选"}`;

    const decision = await this.llm.json<RouteDecision>(prompt, {
      model: 'gpt-4o-mini',  // 路由用小模型,省钱
      temperature: 0,         // 路由要确定性
      timeout: 2000,          // 路由 2s 超时,宁可降级到默认
    });

    // 3. 低置信度降级:LLM 说不准时,默认走 RAG(最常见路径)
    if (decision.confidence < 0.7) {
      decision.intent = 'rag';
      decision.fallback = true;
    }

    this.cache.set(query, { decision, ts: Date.now() });
    return decision;
  }
}

5.3 多模型协同与成本优化

企业 RAG 通常需要多模型协同:路由用小模型(省钱)、生成用大模型(保质量)、特定任务用专业模型(保精度)。这是经典的cascade 架构——从便宜到贵逐级尝试。

任务模型选择理由单次成本
Query 路由GPT-4o-mini / Qwen2.5-7B简单分类,小模型足够$0.0001
Query 改写GPT-4o-mini / Claude Haiku需要 NLU 但要求不高$0.0003
Rerank 评分Cohere Rerank-v3 / BGE-Reranker专用模型,效果远好于 LLM$0.001
答案生成GPT-4o / Claude-3.5-Sonnet / Qwen2.5-72B主战场,质量优先$0.01-0.05
答案验证GPT-4o-mini简单 yes/no 校验$0.0002

成本控制三板斧:① 缓存(query 相似度 > 0.95 直接复用);② 降级(高峰期用小模型);③ 限额(每用户每天 50 次免费,超出付费)。

💡 架构深挖点

  1. 意图路由用 LLM 推理,延迟又增加 200-500ms。能否用向量检索 + 轻量分类(如 Embedding 相似度)替代 LLM 路由,把延迟压到 50ms 以内?
  2. 多模型协同时,模型升级/降级(如 GPT-4 → GPT-4o)的灰度策略怎么做?直接全量切换风险大,按用户分组灰度又如何保证体验一致?
  3. 当 LLM 输出触犯内容审查被拒答时,兜底回答应该返回什么?纯技术问题给"我不知道"可以,但业务咨询类问题"不知道"会激怒用户。是否需要预置"安全答案模板"?

六、质量闭环:用户反馈驱动的持续迭代

6.1 反馈采集的 4 个触点

RAG 系统上线后,没有反馈循环 = 没有优化路径。我们设计了 4 个反馈采集触点:

  • 显式反馈:👍/👎 按钮、5 星评分、文字评论
  • 隐式反馈:用户停留时长、复制答案行为、追问次数("展开说说"= 满意;"不是这个"= 不满意)
  • 转人工:用户主动转人工 = 明确表达"AI 没解决我的问题"
  • 答案采纳:用户复制了答案 / 把答案分享出去 = 答案有价值

这 4 类反馈汇总后,每周产出 Bad Case 报告,标注"应该答对但答错了"的高频问题,作为优化重点。

6.2 标注与再训练:从 Bad Case 到模型迭代

收集到 Bad Case 后,标准流程是:分类 → 归因 → 优化

// Bad Case 归因分类(自动 + 人工结合)
const caseAnalyzer = {
  // 检索问题:相关文档没召回到
  retrieval: async (case) => {
    const docs = await retrieve(case.query, { topK: 50 });
    const target = case.groundTruth;
    const recallAtK = computeRecall(docs, target);
    return recallAtK < 0.8 ? '检索召回不足' : null;
  },

  // 排序问题:召回了但排名靠后
  ranking: async (case) => {
    const docs = await retrieve(case.query, { topK: 10 });
    const rank = docs.findIndex(d => d.id === case.groundTruth);
    return rank >= 5 ? '排序靠后' : null;
  },

  // 生成问题:检索对了但 LLM 没用到
  generation: async (case) => {
    const docs = await retrieve(case.query, { topK: 5 });
    const hit = docs.some(d => d.id === case.groundTruth);
    if (hit) return 'LLM 未有效利用检索内容';
    return null;
  },

  // 数据问题:知识库本身缺内容
  coverage: async (case) => {
    const anyHit = await fullCorpusSearch(case.query);
    return !anyHit ? '知识库缺内容' : null;
  }
};

归因后,70% 的 Bad Case 集中在检索和排序——这意味着优化 embedding 模型和 Rerank 模型的性价比,远高于换更强的 LLM。

6.3 评估指标体系:让优化可量化

维度指标计算方法目标值
检索质量Recall@10Top-10 召回正确文档比例> 0.85
检索质量MRR正确文档排名倒数均值> 0.75
答案质量Faithfulness答案忠实于检索内容的比例> 0.92
答案质量Helpfulness人工评分 ≥ 4 分(5 分制)的比例> 0.80
用户体验CSAT用户满意度(👍 比例)> 0.75
用户体验转人工率对话转人工的占比< 0.20

💡 架构深挖点

  1. 用户反馈容易被"沉默大多数"稀释(只有 5% 用户会点 👍/👎)。能否用行为信号(停留时长、追问模式)作为隐式反馈的代理?
  2. Bad Case 标注依赖人工,成本高。能否用LLM-as-a-Judge(让 GPT-4 自动评分)替代部分人工标注?
  3. 指标体系中 Faithfulness(忠实度) 是 RAG 系统的生命线——如果答案偏离了检索内容,就变成了"幻觉"。但忠实度过高(>0.95)又会让答案"死板"。合理的平衡点在哪?

七、权限治理:多租户 + 字段级 + 成本归因

7.1 多租户隔离的三种范式

企业 RAG 通常服务多个业务方(HR/法务/产品/客服),不同业务方的数据必须严格隔离。我们对比三种隔离方案:

方案隔离强度成本适用场景
独立向量库物理隔离金融/医疗等强合规
共享库 + namespace逻辑隔离企业多部门
共享库 + metadata 过滤查询时过滤内部知识库

选型建议:合规要求强 → 独立库;业务方多但无强合规 → namespace;内部全员共享 → metadata 过滤。混合使用更常见(如:HR 数据用独立库,通用知识用 metadata 过滤)。

7.2 字段级访问控制:细粒度的安全护栏

有些文档需要字段级控制——HR 知识库包含员工工资信息,不是所有 HR 都能看。我们设计了"四层过滤链":

// 检索后的四层过滤链(按顺序执行)
const filterChain = [
  // 1. 租户隔离:跨租户的文档直接过滤
  doc => doc.tenantId === ctx.tenantId,

  // 2. 部门隔离:同租户但跨部门
  doc => doc.departments.includes(ctx.userDept) || doc.departments.length === 0,

  // 3. 角色隔离:高管文档只对管理层开放
  doc => !doc.requiresRole || ctx.userRoles.includes(doc.requiresRole),

  // 4. 字段脱敏:工资/手机号等敏感字段按角色脱敏
  doc => maskSensitiveFields(doc, ctx.userRoles),
];

const filteredDocs = retrievedDocs.filter(d => filterChain.every(fn => fn(d)));

关键点:过滤在检索后但 LLM 推理前执行。LLM 永远不应该"看到"无权访问的内容,否则一旦 prompt injection,用户能套出隔离信息。

7.3 成本归因:让每个业务方看到自己的账单

RAG 系统最大的隐性成本是LLM API 调用费。如果不归因,财务部门不知道谁在烧钱,预算也无从管控。我们的方案:

  • 计量埋点:每次 LLM 调用记录 (tenantId, userId, model, promptTokens, completionTokens, latency)
  • 单价配置:按模型维护单价表(GPT-4o: $5/1M input tokens)
  • 月度账单:按 tenant 聚合,生成 Top10 用户/部门报表
  • 预算熔断:单租户月度预算达到 80%/100% 自动告警/限流

💡 架构深挖点

  1. 字段级控制如果做到行列级(同一份合同文档,A 角色看金额、B 角色看条款),是预处理脱敏还是检索后过滤?预处理会破坏 embedding 语义,检索后过滤又可能漏掉关键信息。
  2. 成本归因如果按业务部门分摊,如何避免"冤大头"问题(研发测试的 query 也算到生产部门头上)?
  3. 多租户共享向量库时,embedding 模型升级需要全量重建索引(一次 100 万文档 × 几小时),期间新旧库怎么平滑切换?双写 + 异步 reindex 怎么保证一致性?

八、高可用设计:检索降级与兜底回答

8.1 检索降级链:从最优到兜底

RAG 系统的可用性挑战在于依赖链长:向量数据库、Embedding 服务、LLM API、Prometheus,任何一环抖动都会影响最终用户。我们设计了四级降级链

flowchart TD Q([用户 Query]) --> L1{L1 语义缓存
5min TTL} L1 -->|命中| OK1([直接返回
~10ms]) L1 -->|未命中| L2{L2 混合检索
向量+BM25+Rerank} L2 -->|成功| OK2([返回结果
~200ms]) L2 -->|异常/超时| L3{L3 纯向量检索
跳过 Rerank} L3 -->|成功| OK3([降级返回
~80ms]) L3 -->|异常/超时| L4{L4 关键词检索
保底} L4 -->|保底| OK4([最差兜底
~30ms]) L4 -->|失败| ERR([系统繁忙
转人工]) L1 -. 写回 .-> L1 L2 -. 失败告警 .-> LOG1[Prometheus] L3 -. 失败告警 .-> LOG2[Prometheus] L4 -. 失败告警 .-> LOG3[Prometheus] style L1 fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style L2 fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style L3 fill:#16213e,stroke:#7b61ff style L4 fill:#16213e,stroke:#7b61ff style OK1 fill:#1a4d2e,stroke:#00ff88 style OK2 fill:#1a4d2e,stroke:#00ff88 style ERR fill:#4d1a1a,stroke:#ff4444
// 检索降级链(生产级实现)
class RetrievalDegradation {
  async retrieve(query: string, ctx: Context): Promise<Doc[]> {
    // L1: 语义缓存(5 分钟内相同 query 直接返回)
    const cached = await this.semanticCache.get(query);
    if (cached && Date.now() - cached.ts < 300_000) return cached.docs;

    try {
      // L2: 混合检索(向量 + BM25 + Rerank)
      return await this.hybridRetrieve(query, ctx);
    } catch (e) {
      logger.warn('L2 检索失败,降级到 L3', e);

      try {
        // L3: 纯向量检索(跳过 Rerank)
        return await this.vectorRetrieve(query, ctx);
      } catch (e2) {
        logger.warn('L3 检索失败,降级到 L4', e2);

        // L4: 关键词检索(保底,不依赖向量服务)
        return await this.keywordRetrieve(query, ctx);
      }
    }
  }
}

关键设计:每一级降级都必须有"足够好"的兜底,而不是"破罐子破摔"。L4 关键词检索虽然效果差,但比直接返回"系统繁忙"好太多——用户至少能看到一些相关结果。

8.2 兜底回答机制:让系统永不"哑火"

当 LLM API 不可用时(限流/超时/服务降级),我们需要预先准备的兜底回答

  • FAQ 兜底:高频问题预生成静态答案,存 Redis。LLM 挂了直接从 FAQ 取
  • 知识卡片兜底:当检索到相关文档但 LLM 不可用,直接返回文档摘要+原文链接
  • 智能转人工:当所有兜底都失败,提示"系统繁忙,请稍后重试或转人工"

8.3 多级缓存策略

缓存是 RAG 系统降本增效的第一性原理。我们设计了 4 层缓存:

缓存层内容TTL命中率
L1 语义缓存query 向量 → 答案5 分钟15-25%
L2 检索缓存query → 文档列表1 小时30-40%
L3 Embedding 缓存文本 → 向量永久60%+
L4 LLM 响应缓存完整 prompt → 答案1 天10-15%

实测下来,4 层缓存叠加命中率超过 70%,每月节省 LLM 成本约 40%

💡 架构深挖点

  1. 语义缓存(Semantic Cache)阈值怎么定?阈值 0.95 太严,命中率低;阈值 0.85 太松,缓存了错误答案。怎么根据业务场景动态调?
  2. 多级缓存的一致性:知识库新增文档时,如何让缓存失效(invalidation)?全量失效太粗暴,按文档 ID 失效又可能漏掉"语义相关但 ID 不同"的文档。
  3. 当 LLM 服务降级时,用户体验如何保持稳定?直接返回兜底答案用户会察觉到"系统变笨了",能否让降级过程对用户透明?

九、性能压测:千万级文档 P99<200ms 的路径

9.1 压测方法论:从基准到极限

RAG 性能压测比传统微服务复杂得多——除了 QPS/RT/错误率,还要关注检索质量 vs 速度的 trade-off。我们的压测分三阶段:

  1. 基准压测:用真实 query 分布测 P50/P95/P99 延迟
  2. 容量压测:逐步加压到系统临界点,定位瓶颈
  3. 极限压测:超过设计容量,验证降级和兜底是否生效

9.2 性能瓶颈定位:从外到内

RAG 性能瓶颈通常出现在 3 个位置,调用链路上各环节耗时分布如下:

flowchart LR Q([Query]) --> A1[queryRewrite
80ms] A1 --> A2[embedding
30ms] A2 --> A3[vectorSearch
50ms] A2 --> A4[bm25Search
40ms] A3 --> A5[RRF 融合] A4 --> A5 A5 --> A6[rerank
90ms] A6 --> A7[contextBuild
20ms] A7 --> A8["llmGenerate
🔥 800ms"] A8 --> A9[postProcess
30ms] A9 --> OUT([响应]) style A8 fill:#4d1a1a,stroke:#ff4444,stroke-width:3px style A6 fill:#0a0e27,stroke:#00d4ff,stroke-width:2px style A1 fill:#16213e,stroke:#7b61ff style A5 fill:#16213e,stroke:#7b61ff
// RAG 调用链路(标注典型耗时)
const pipeline = {
  queryRewrite: 80,       // query 改写 LLM 调用
  embedding: 30,          // query 向量化
  vectorSearch: 50,       // 向量库检索 TopK=100
  bm25Search: 40,         // ES 关键词检索
  rerank: 90,             // Cross-Encoder 重排序
  contextBuild: 20,       // 上下文拼装
  llmGenerate: 800,       // LLM 生成(主战场)
  postProcess: 30,        // 后处理(引用格式化等)
  // 合计:~1140ms(P50),瓶颈在 LLM
};

优化优先级:LLM 生成(800ms)> Rerank(90ms)> Query 改写(80ms)。优化 LLM 收益最大(如用 streaming + 提前返回首字 token)。

9.3 优化实战:把 P99 从 1500ms 压到 180ms

我们做了一系列优化,把 P99 从 1500ms 压到 180ms:

优化项优化前优化后收益
向量库 HNSW 参数调优ef=64ef=128 + m=16召回+5%, 延迟+30ms
Query 改写批量处理单条串行Batch 16 条并发延迟-60ms
Embedding 模型蒸馏bge-large (1024维)bge-small (512维)延迟-15ms, 质量-2%
LLM 流式输出等完整响应SSE 流式,首字 <200ms体感延迟-90%
预计算 Rerank 候选Top-100 全 RerankTop-30 Rerank延迟-50ms

💡 架构深挖点

  1. 向量数据库在亿级文档下,P99 通常 50-100ms。千万级 30ms 是怎么做到的?(提示:HNSW 参数调优 + 内存索引 + GPU 加速)
  2. 流式输出虽然首字延迟低,但总延迟可能更高(流式协议开销)。在 ToB 场景下,是流式还是整段返回
  3. 压测时通常用历史 query 回放,但生产 query 分布会变(新业务、新知识)。压测集怎么持续更新

十、经验沉淀:从 0 到 1 的 6 个决策点

10.1 决策点 1:先做 Demo 还是先做平台?

推荐先 Demo 后平台。用 LangChain + Pinecone + GPT-4 3 天出 Demo,跑通端到端流程。验证业务价值后,再投入工程化平台建设(自研编排、可观测、权限治理)。

反例:上来就自研编排,3 个月还没出 Demo,业务方失去信心,项目被砍。

10.2 决策点 2:向量数据库怎么选?

选型优势劣势推荐场景
Milvus国产开源,性能强运维复杂大规模生产
QdrantRust 性能好,资源占用低生态较新中小规模
WeaviateGraphQL + 模块化中文支持一般多模态场景
Pinecone全托管,省心贵,数据出境风险中小规模快速验证
pgvector复用现有 PG 集群性能一般千万级以下 + 已有 PG

10.3 决策点 3:Embedding 模型选开源还是商用?

默认推荐 BGE 系列(bge-large-zh-v1.5,中文场景 SOTA),特殊场景才用 OpenAI text-embedding-3-large。开源模型需要自己部署,但可控、成本低。商用模型效果好但贵、且有数据出境风险。

10.4 决策点 4:LLM 用 GPT-4 还是开源?

看场景

  • 需要 SOTA 质量 + 不在乎钱 → GPT-4o / Claude-3.5
  • 需要私有化部署 + 数据敏感 → Qwen2.5-72B / DeepSeek-V3
  • 需要极致性价比 + 任务简单 → Qwen2.5-7B / GLM-4-9B
  • 混合策略:路由到不同模型,复杂任务用大模型,简单任务用小模型

10.5 决策点 5:知识更新策略

实时同步 vs 定时批处理:

  • 实时同步:CDC 监听数据库 Binlog,增量更新。延迟低(秒级),但工程复杂度高
  • 定时批处理:每小时/每天全量重建。简单可靠,但延迟高
  • 混合策略:热点数据实时同步 + 冷数据定时批处理

推荐混合策略,90% 场景是定时批处理足够,10% 高频更新走实时。

10.6 决策点 6:评估体系怎么建?

必须有3 个层次的评估

  1. 离线评估:用标注集跑批量测试,CI 集成(每次模型/数据变更都跑)
  2. 在线 A/B:新旧版本 1:1 流量切分,对比核心指标
  3. 用户反馈:真实用户的 👍/👎/转人工率

缺一不可。没有离线评估 = 改动全靠"感觉";没有在线 A/B = 不敢全量;没有用户反馈 = 永远不知道真实质量。

10.7 总结:RAG 落地的 3 个反直觉

① 检索比生成更重要。80% 的 Bad Case 是检索问题,不是 LLM 问题。把精力放在 embedding/Rerank 上,性价比远高于换更强的 LLM。

② 简单优先于复杂。不要一上来就上 Agent/Multi-hop,先把单轮 RAG 做到 95% 准确率,再考虑复杂链路。80% 的业务用不到 Agent。

③ 评估先于开发。没有评估指标的 RAG 系统是"薛定谔的猫"——你不知道它好不好。建评估体系比写代码更重要。

💡 架构深挖点

  1. 本篇 RAG 架构讨论基于 2026 年的技术栈,2-3 年后哪些会过时?哪些是不变的架构本质?(提示:检索+生成的范式 vs 单一模型大一统的可能性)
  2. 如果业务要求多模态 RAG(图片、表格、公式),架构需要做哪些改造?多模态 Embedding、跨模态检索、视觉 LLM——哪些是关键路径?
  3. RAG 与 Fine-tuning 的边界:什么时候用 RAG(知识更新频繁),什么时候用 Fine-tuning(专业领域,prompt 不够用)?两者结合的最优实践是什么?
  4. 企业 RAG 系统的TCO(总拥有成本)应该怎么算?除了 LLM API,还有哪些隐性成本(向量库运维、Embedding 重建、Prompt 调优的人力成本)?