RAG Demo 3天跑通,上线又磨了2周——这10个工程坑,中文场景几乎全踩一遍

0 阅读14分钟

RAG 从入门到工程落地

RAG 从 Demo 到生产的距离,是一句话的距离——"Transformer 多头注意力机制"被中文切分切成三段的那一刻,你才意识到 Demo 和产品的差距有多大。做 PrismAI 的 RAG 模块,MVP 只用了 3 天——切分、Embedding、检索、生成四步跑通。然后用户开始用,问题来了:中文切分把概念切断、Embedding 维度不匹配、检索结果和问题不相关、跨用户向量空间不一致……从 Demo 到生产,中间隔着 10 个你以为不是问题的问题。

阅读约 20 分钟 | 系列第 2/14 篇

一、RAG 解决了什么问题

LLM 有两大先天缺陷:① 知识截止日期(训练数据有终点)② 幻觉(对训练数据外的问题可能编造答案)。RAG 通过"检索+生成"的方式,让 LLM 能使用训练数据之外的知识。

有 RAG 和没有 RAG 的区别

没有 RAG(纯 LLM):
  用户:"OceanBase 迁移后性能为什么断崖下跌?"
  LLM:编造一段听起来合理但可能完全错误的分析
​
有 RAG:
  用户:"OceanBase 迁移后性能为什么断崖下跌?"
  → 向量检索找到相关技术文档中的"PL/SQL 函数逐行求值"段落
  → 作为上下文传给 LLM
  → LLM:"根据文档记录,根因是 OceanBase 对 WHERE 条件中的 PL/SQL 函数逐行求值...
          解决方案有 4 种:参数绑定、标量子查询包装..."
  → 回答正确,且有据可查

RAG 数据流

文档上传 → 解析(PDF/Word/Markdown)→ 文本切分 → 向量化 → 入库(向量数据库)
                                                                      ↓
用户提问 → 问题向量化 → 相似度检索 Top-K → 拼接 Prompt → LLM 生成 → 带引用来源的回答

二、文档切分策略

这是 RAG 最容易出问题的环节。切分不对,后面全白费。

三种策略

策略方式适合不适合
固定大小每 N 个 token 一刀切,相邻块重叠 10-20%FAQ、规章制度(条目独立性强)连贯性强的技术文档
语义切分按段落/章节边界切,保持语义完整技术文档、业务手册无结构文本
递归切分先按一级标题→二级标题→段落逐级切合同协议、法规(层级清晰)扁平结构文档

关键参数

  • Chunk Size:256-512 token(中文约 400-800 字,1 个汉字 ≈ 1.2-1.5 token)。太小丢失上下文,太大检索精度下降
  • Overlap:10-20%。防止关键信息恰好落在分块边界上
  • 核心原则:每个 Chunk 自包含——单独拿出来看,不需要翻前一段也知道在说什么

Chunk Enrichment(块增强)——一个成本极低但效果显著的优化:

原文:"根据第3条,订单取消需提前通知..."
增强后:"[《某交易系统接口规范》/ 第三章 订单接口 / 2025-06 更新]
         根据第3条,订单取消需提前通知..."

检索时 AI 就知道这段内容的来源和时效性,输出的引用也更准确。


三、Embedding 与向量数据库

Embedding 模型选择

模型维度语言费用适合
BGE-large-zh-v1.51024中文最优免费(本地)中文知识库首选
text-embedding-3-small1536多语言OpenAI 计费多语言、快速验证
Jina embeddings-v31024多语言有免费额度长文本(支持 8192 token 输入)

附:Embedding 模型的训练原理——对比学习

"为什么 BGE 在中文检索上比 OpenAI Embedding 好?"——回答这个问题需要理解 Embedding 模型的训练方法。

当前主流 Embedding 模型(BGE、text-embedding-3、Jina)均使用对比学习(Contrastive Learning) 范式训练:

训练数据构造:
  正样本对(语义相近)                    负样本对(语义无关)
  ──────────────────                    ──────────────────
  查询:"Java 多线程知识点"               查询:"Java 多线程知识点"
  文档:"synchronized 关键字的底层实现"  文档:"今天天气不错适合出去玩"
  → 这两个向量的距离应该很近              → 这两个向量的距离应该很远
​
  查询:"MySQL 索引优化"                 查询:"MySQL 索引优化"
  文档:"B+ 树的结构和页分裂机制"        文档:"Spring Boot 自动装配原理"
  → 向量余弦相似度应 > 0.8               → 向量余弦相似度应 < 0.3

训练目标(InfoNCE Loss)

Loss = -log( exp(正样本相似度 / τ) / (exp(正样本相似度 / τ) + Σ exp(负样本相似度 / τ)) )
​
其中 τ(温度系数)控制模型对"难区分样本"的敏感度。
简化为直觉:让正样本对的距离尽可能近,同时让所有负样本对的距离尽可能远。

BGE 在中文上优于 OpenAI Embedding 的原因

因素BGEOpenAI text-embedding-3
中文训练数据大量中文问答对 + 检索对以英文为主,中文为辅
训练任务设计专门针对检索任务设计负样本(同一领域内的难区分负样本)通用任务(分类+检索+聚类混合训练)
指令感知支持 Instruction-aware:不同任务加不同指令前缀不支持指令感知,一个模型一种输出
开源可调试✅ 可查看训练数据和配置❌ 闭源,无法验证中文覆盖度

关键认知:Embedding 模型的"好坏"不是绝对的——它取决于你的语言、领域和任务类型。中文技术文档检索场景中,BGE 的领域匹配度高于通用模型。如果换到英文法律文档场景,可能 HuggingFace 上的专用 Embedding 模型表现更好。

PrismAI 的 Embedding 设计决策

PrismAI 平台设计中推荐 BGE 作为本地 Embedding 方案——而不是用用户自己的 API Key 调 Embedding API。为什么?公共知识库需要跨用户检索一致性——如果用户 A 用 GLM 的 Embedding 模型写向量,用户 B 用通义千问的模型去检索,两个向量在不同的语义空间中,检索结果完全不可用。平台统一 Embedding 保证了跨用户的向量空间兼容。当前一期实现中,EmbeddingService 实际对接的是 GLM(embedding-2)和通义千问(text-embedding-v3)两种远程 API(见 §八 代码走读)——选择远程 API 是为了快速验证 RAG 管线,BGE 本地部署规划在二期降本优化。

向量数据库选型

方案规模部署延迟PrismAI 状态
Chroma< 10 万条pip install 零部署< 10ms本地开发/原型
内存暴力搜索< 1 万条无依赖O(n) 遍历✅ V1 使用
Milvus百万~十亿Docker/K8s 部署< 5ms(HNSW)V2 升级方向

PrismAI V1 选择了最轻量的内存暴力搜索方案——启动时全量加载向量到 ConcurrentHashMap,查询时遍历计算余弦相似度,用小顶堆维护 Top-K。设计注释写得很诚实:

"当单方向文档量 >5000 或搜索 P99 >200ms 时,可替换为 Lucene HNSW 实现。"

这就是工程上"先跑通再优化"的典型决策——V1 阶段文档量少,暴力搜索完全够用,不引入 Milvus 的运维复杂度。

余弦相似度计算VectorIndexService.java:196-206):

// PrismAI 实际代码
private double cosineSimilarity(float[] a, float[] b) {
    if (a.length != b.length) return 0.0;
    double dot = 0.0, normA = 0.0, normB = 0.0;
    for (int i = 0; i < a.length; i++) {
        dot += (double) a[i] * b[i];
        normA += (double) a[i] * a[i];
        normB += (double) b[i] * b[i];
    }
    if (normA == 0.0 || normB == 0.0) return 0.0;
    return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

四、混合检索

纯向量检索的盲区:擅长语义匹配("数据库"↔"DB"),但可能漏掉精确的关键词("PGET_SYSCURRDATE")。混合检索 = 向量语义相似度 + BM25 关键词精确匹配,加权融合后重排序。

混合检索流程:
用户问题
  ├── 向量检索(语义匹配)   → Top-K₁ 结果 ──┐
  └── BM25 检索(关键词匹配) → Top-K₂ 结果 ──┼── RRF 融合(k=60)→ 最终排序 → 传给 LLM

PrismAI 设计文档中采用了 RRF(Reciprocal Rank Fusion, k=60)进行融合。BM25 权重 0.3 + 向量权重 0.7——向量为主、关键词为辅,适合中文技术文档场景。


五、RAG 评估:你怎么知道你的 RAG 好不好用

三层评估体系:

第一层:检索评估 → 检索回来的文档是否相关?排序是否正确?
  指标:Hit Rate(Top-K 命中率)、MRR(首个相关文档的排名倒数)、NDCG(排序加权)
​
第二层:生成评估 → 答案是否忠实于检索文档?有没有编造?
  框架:RAGAS(业界最常用)
  指标:Faithfulness(忠实度)、Answer Relevancy(答案相关性)、
        Context Precision(上下文精度)、Context Recall(上下文召回)
​
第三层:端到端评估 → 用户满意度
  指标:点赞率、点踩率、回答可用率、考试通过率

LLM-as-Judge(当前性价比最高的自动评估方式): 用强模型评估弱模型输出——输入问题+检索文档+待评估答案,输出 1-5 分的 faithfulness/correctness/completeness + 解释。每 100 条对话抽样 5 条自动打分。

PrismAI 的评估策略

  • 离线:建 50 条标准问答对,每次改 prompt 或检索策略跑一遍,对比 MRR 和 Faithfulness
  • 在线:用户点赞/点踩 + LLM-as-Judge 自动抽样
  • 监控:检索命中率、平均相似度、AI 搜索触发频率(反映知识库覆盖不足)
  • 门禁:prompt 模板升级前先跑离线评估集,低于阈值不发布

六、高级 RAG 技术

基础 RAG 有三个常见问题,各有对应方案:

问题现象方案原理
检索不相关问"AQS 原理"返回"美国质量标准 AQS"Self-RAG / CRAG详见下方展开
跨文档多跳推理"synchronized 锁升级后对 GC 有什么影响"GraphRAG用知识图谱替代向量检索,沿图谱路径做多跳遍历
短问题精度低"怎么优化慢查询"口语化太短HyDE先让 LLM 生成"假设性答案",用答案的向量去检索——答案和文档在向量空间更接近

CRAG(Corrective Retrieval-Augmented Generation)工作机制

CRAG 在传统 RAG 的"检索→生成"之间插入一个检索评估器(Retrieval Evaluator)

用户提问 → 向量检索 → 返回 Top-K 文档
                            ↓
                   ┌─ 检索评估器(轻量级 LLM 调用)─┐
                   │  对每个检索结果打分:            │
                   │  "这份文档与问题的相关度是?"     │
                   └─────────┬──────────────────────┘
                             ↓
            ┌────────────────┼────────────────┐
            ↓                ↓                ↓
      高相关度           中相关度          低相关度
      (>0.8)           (0.5-0.8)         (<0.5)
            ↓                ↓                ↓
      直接用于生成      融合后使用      丢弃 + 触发外部搜索
                                      (Web Search / 备用知识库)
            ↓                ↓                ↓
            └────────────────┴────────────────┘
                             ↓
                      LLM 基于筛选后的文档生成回答

与普通 RAG 的关键区别

  • 普通 RAG:检索 → 无条件信任 → 生成(可能基于不相关文档编造)
  • CRAG:检索 → 先质疑、打分数 → 剔除不相关 → 补充外部知识 → 生成

工程落地要点:检索评估器本身也是一个 LLM 调用,会增加一次推理的延迟和成本。实践中通常用 7B 级小模型做评估(如 Qwen2.5-7B),速度远快于生成阶段的大模型。评估器可以复用一个已有的本地部署小模型,无需额外成本。

Self-RAG 与 CRAG 的关系:Self-RAG 在 CRAG 的基础上更进一步——它在生成过程中还增加了"自我反思"环节,即每生成一段内容后自评"这段内容的 factual 程度如何",发现编造则回退重写。CRAG 关注检索质量,Self-RAG 关注生成质量,两者可以叠加使用。

PrismAI 的 RAG 升级路线

阶段技术解决的问题
V1(已实现)基础 RAG + 内存向量索引基本问答、来源标注
V1.5+ Self-RAG(检索结果自评)减少不相关文档干扰
V2+ Milvus HNSW + HyDE检索性能 + 短问题精度
V3(按需)+ GraphRAG跨文档多跳推理

PrismAI 三级出题源降级(本质是简化版 CRAG):

出题时获取知识:
  Layer 1:知识库 RAG 检索  → 有结果 → 使用
                              → 无结果/相关性低 ↓
  Layer 2:LLM 训练数据      → 模型自身知识兜底
                              → 知识不可靠 ↓
  Layer 3:AI 搜索(可选)    → DuckDuckGo 等外部搜索补充

七、RAG vs 微调:选型决策框架

RAG vs 微调的本质区别:

维度RAG微调(Fine-tuning)
改什么不改模型参数,只优化检索+prompt改模型权重参数
知识更新实时(更新向量库即可)需重新训练
成本低(Embedding + 检索开销)高(GPU 训练 + 数据标注)
幻觉控制有来源可追溯来源不可追溯
适合知识密集型、频繁变动风格/格式固定、领域术语注入

决策框架

知识是否频繁变动?
  ├── 是 → RAG(考纲每年更新、法规变化)
  └── 否 → 需要特定输出格式/风格?
            ├── 是 → 微调(客服话术、代码风格)
            └── 否 → RAG 或 Prompt 工程即可

资源有多少?
  ├── 有 GPU + 标注数据 → 可考虑微调
  └── 只有 API Key → RAG

PrismAI 为什么选 RAG

  1. 考纲每年可能变化,微调跟不上,RAG 更新文档即可
  2. Java/会计/架构师三套完全不同的知识域,微调一个模型全覆盖极难
  3. 考试答案必须标注来源,RAG 天然支持,微调做不到
  4. 个人项目无 GPU,API 调用是唯一可行方案
  5. 公共知识库跨用户共享,RAG 天然支持

进阶——不是二选一:RAFT(训练时注入检索文档)、GraphRAG(知识图谱替代向量检索)、RAG + LoRA 微调混合。业界趋势是组合使用。


八、实战:PrismAI RAG 全链路代码走读

PrismAI 的 RAG 全链路涉及三个核心组件:

代码状态说明:截至 PrismAI 一期,RAG 管线的摄取侧(DocumentParser → TextSplitter → EmbeddingService → 磁盘持久化)已完整实现,但检索侧(VectorIndexService.search() → RagSearchTool → 混合检索融合)属于二期待开发。当前对话实际走的是 KnowledgeServiceImpl.search() 的 SQL LIKE 关键词匹配,而非语义检索。以下代码走读中标注了 ✅(已实现)和 🟡(二期设计)。

链路全景

用户上传文档(PDF/Word/Markdown)
  → ✅ DocumentParser(解析为纯文本)
  → ✅ TextSplitter(语义切分,512 token/chunk,64 token overlap)
  → ✅ EmbeddingService.embedBatchAndStore()(BGE 向量化 + 磁盘持久化)
  → ✅ DocumentChunkRepository(MySQL 存元数据:chunkId/documentId/content/vectorId)
  → 🟡 VectorIndexService.loadAll()(启动时 ConcurrentHashMap 全量加载——代码骨架已有)
  → 🟡 RagSearchTool(Agent 工具——LLM 自主决定何时调用——二期设计)
  → 🟡 VectorIndexService.search()(余弦相似度 + 小顶堆 Top-K——二期设计)
  → 🟡 混合检索融合(0.7×向量分 + 0.3×关键词分——二期设计)
  → 结果拼入 System Prompt → LLM 生成 → SSE 流式返回

EmbeddingService ✅(mindforge-service/.../EmbeddingService.java):

  • 支持 GLM(embedding-2)和通义千问(text-embedding-v3)两种 Embedding API
  • 向量以 JSON 数组格式存到 data/vectors/{uuid}.json
  • 30 秒超时,调用失败抛 RuntimeException(由 Agent Tool 层 catch)

VectorIndexService 🟡(mindforge-service/.../VectorIndexService.java,二期待实现):

  • @PostConstruct loadAll() 启动时全量加载到 ConcurrentHashMap
  • directionIndex(Map<Long, List>)实现按备考方向隔离检索
  • 查询用 PriorityQueue 小顶堆维护 Top-K,时间复杂度 O(n log k)
  • 设计注释标注了升级路径:">5000 条或 P99>200ms 时换 Lucene HNSW"

RagSearchTool 🟡(mindforge-agent/.../tools/RagSearchTool.java,二期待实现):

  • 实现 AgentTool 接口,注册到 AgentToolRegistry
  • 参数 query + topK(上限 10),由 LLM 自主决定何时调用
  • 返回 JSON 格式的检索结果列表(含 chunkId/documentId/content/score)
  • 通过 _provider_apiKey_directionId 等系统注入参数获取运行时上下文

核心要点回顾

  1. 文档切分是 RAG 的生命线——Chunk Size 256-512 token(中文约 400-800 字)+ Overlap 10-20% + 每个 Chunk 自包含
  2. Embedding 的好坏不是绝对的——对比学习(InfoNCE Loss)是理解 Embedding 质量的关键。BGE 在中文上优于 OpenAI Embedding 是因为中文训练数据量 + 针对检索任务的负样本设计 + 指令感知
  3. 混合检索(向量 + BM25) 是中文技术文档场景的标配,RRF 融合优于简单加权
  4. 三层评估体系(检索 MRR → 生成 RAGAS → 端到端点赞率)让"改 Prompt 是变好了还是变坏了"有了数据答案
  5. CRAG 的检索评估器 是当前性价比最高的 RAG 增强手段——用 7B 小模型在"检索→生成"之间插一层质量门禁
  6. RAG vs 微调不是二选一——先用 RAG 验证价值,确有需要再考虑微调

RAG 四步管线看起来简单——切分、Embedding、检索、生成。但每一步从 Demo 到生产都藏着一堆工程决策,中文场景尤甚。收藏这篇文章,下次 RAG 效果不好时逐环节对照排查。 点赞+收藏,下一章拆 Agent 的 ReAct 循环——核心代码不到 50 行,却是 AI 应用开发的"操作系统"。 上一篇:《AI应用开发全景:6组件全景地图》 | 下一篇:《Agent的本质:ReAct循环不到50行,却是AI开发的"操作系统"》 系列合集掘金AI合集