企业级 RAG 知识库平台架构选型与踩坑全记录
去年接了一个企业内部知识库项目——把公司十几年的文档、wiki、工单系统整合成一个「问一句就能答」的 AI 知识库。听起来简单,做下来踩了一身坑。这篇完整复盘整个架构选型过程和落地中遇到的真实问题。
需求拆解:先搞清楚在做什么
很多 RAG 项目失败,不是技术不行,是需求没搞清。开干之前先明确三件事:
- 知识源是什么:PDF、Word、Confluence、工单系统、代码仓库?格式杂不杂?
- 用户是谁:内部员工还是外部客户?并发量多大?容忍多长延迟?
- 准确率底线:是「大概对就行」还是「必须精确到引用原文段落」?
我们这个项目的情况:
| 维度 | 实际情况 |
|---|---|
| 知识源 | Confluence wiki + PDF 文档 + 工单系统,约 5 万篇 |
| 用户 | 内部员工,峰值并发约 50 路 |
| 延迟要求 | 首 token < 3s,完整回答 < 15s |
| 准确率 | 必须引用原文,不能瞎编 |
需求清楚了,才能做靠谱的架构选型。
全链路架构设计
先看整体架构,再逐层说选型思路:
┌─────────────────────────────────────────────────────────────────────┐
│ 用户交互层 │
│ Web UI / IM 机器人 / API │
└──────────────────────────┬──────────────────────────────────────────┘
│ 用户提问
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 服务编排层 │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Query │→ │ 意图识别 │→ │ Query │→ │ Rerank 重排序 │ │
│ │ 预处理 │ │ & 路由 │ │ 改写扩展 │ │ (Cross-Encoder) │ │
│ └──────────┘ └───────────┘ └──────────┘ └────────┬─────────┘ │
└───────────────────────────────────────────────────────┼─────────────┘
│ 召回上下文
┌───────────────────────────────────┼───────────┐
│ 检索层 │ │
│ ┌──────────┐ ┌──────────────┐ │ │
│ │ 向量检索 │ │ 关键词检索 │ │ │
│ │ (Top-K) │ │ (BM25) │ │ │
│ └────┬─────┘ └──────┬───────┘ │ │
│ └───────┬────────┘ │ │
│ 混合召回融合 │ │
└────────────────┬─────────────────┘ │
│ Top-N 文档块 │
▼ │
┌─────────────────────────────────────────────────────────────────────┐
│ 生成层 │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Prompt │→ │ 上下文 │→ │ LLM 推理 │→ │ 答案后处理 │ │
│ │ 组装 │ │ 截断压缩 │ │ (流式) │ │ + 引用标注 │ │
│ └──────────┘ └───────────┘ └──────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
┌──────────────────┼──────────────────────┐
│ 数据处理层 │
│ ┌────────┐ ┌────────┐ ┌───────────┐ │
│ │ 文档 │→ │ Chunk │→ │ Embedding │ │
│ │ 解析 │ │ 切分 │ │ 向量化 │ │
│ └────────┘ └────────┘ └─────┬─────┘ │
└──────────────────────────────┬──────────┘
│ 向量写入
▼
┌────────────────────────────────┐
│ 向量数据库 + 元数据存储 │
│ (Milvus / Qdrant / pgvector) │
└────────────────────────────────┘这个架构不是一开始就这样的,是踩了好几个坑迭代出来的。下面按层说选型思路。
数据处理层:文档解析是隐形大头
很多人以为 RAG 的难点在检索和生成,实际上文档解析才是最耗精力的环节。
文档解析选型
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| PyMuPDF | 快、轻量 | 表格解析差 | 纯文本 PDF |
| Unstructured | 支持格式多 | 速度慢、依赖重 | 格式杂的场景 |
| Marker | PDF 转 Markdown 质量高 | 仅 PDF | 学术论文、报告 |
| 自研解析器 | 完全可控 | 开发成本高 | 特殊格式需求 |
我们最终用 PyMuPDF + 自研表格提取的组合:常规文本走 PyMuPDF,遇到表格用 OpenCV 做行列检测再结构化。Confluence 和工单系统走 API 直接拿结构化数据,不解析文档。
Chunk 切分策略
这是 RAG 效果的决定性因素之一。切太大检索不准,切太小语义断裂。
实践中的切分策略:
# 递归切分:先按章节,再按段落,最后按长度兜底
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512, # 每块 512 token
chunk_overlap=64, # 块间重叠 64 token,保证语义连贯
separators=["\n## ", "\n### ", "\n\n", "\n", "。", ";", " "],
length_function=lambda x: len(x) # 中文按字符数
)关键经验:
- 重叠窗口必须有:64 token 的重叠能解决 80% 的语义断裂问题
- 按标题层级优先切:不要上来就按长度硬切,先用 Markdown 标题分段
- 表格不要切:整张表作为一个 chunk,切了就废了
- chunk_size 没有银弹:512 是个起点,具体看你的文档特征调
检索层:混合召回是刚需
纯向量检索在专业领域会漏掉关键词精确匹配的场景。比如搜「ISO 27001」,向量可能召回一堆信息安全的内容,但用户要的就是这个标准本身。
混合召回架构
用户 Query
│
├──→ 向量检索 (Embedding 相似度) → Top 20 结果
│ ↓
├──→ 关键词检索 (BM25) → Top 20 结果
│ ↓
└───────────── 混合融合 (RRF 算法) ──→ Top 10 候选
↓
Rerank 重排序
(Cross-Encoder)
↓
Top 3-5 最终上下文RRF(Reciprocal Rank Fusion)融合公式简单但有效:
def rrf_fusion(vector_results, keyword_results, k=60):
"""RRF 融合:排名越靠前得分越高"""
scores = {}
for rank, doc in enumerate(vector_results):
scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank)
for rank, doc in enumerate(keyword_results):
scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank)
return sorted(scores.items(), key=lambda x: -x[1])Rerank:花小钱办大事
向量检索用的是双塔模型(Bi-Encoder),速度快但精度一般。Rerank 用 Cross-Encoder 做精排,把候选集从 10 个精排到 3-5 个,准确率提升非常明显。
推荐 bge-reranker-v2-m3,中英双语,推理速度可接受。这一步是整个 RAG 链路里性价比最高的优化。
生成层:Prompt 工程决定回答质量
检索到了好的上下文,生成阶段如果 Prompt 写得烂,照样翻车。
核心 Prompt 结构
你是一个企业知识库助手。请严格基于以下检索到的上下文回答问题。
规则:
1. 只使用上下文中的信息回答,不要编造
2. 如果上下文没有相关信息,明确回答"根据现有知识库无法回答"
3. 回答时标注引用来源,格式:[来源:文档名]
4. 保持专业、简洁
上下文:
---
{retrieved_context}
---
用户问题:{user_question}几个关键经验:
- 明确告诉模型「不知道就说不知道」:这一句话能干掉 60% 的幻觉
- 要求标注引用来源:既提升可信度,也方便用户验证
- 上下文按相关度排序后拼接:最相关的放最前面,LLM 对开头内容更敏感
- 控制上下文长度:不要一股脑塞 10 万 token,3-5 个 chunk 够了,太多反而干扰
六个真实踩坑记录
坑 1:Embedding 模型选错,召回率惨不忍睹
最初用了 text-embedding-ada-002(OpenAI),中文文档召回率只有 40% 左右。换成 bge-large-zh-v1.5 后直接飙到 78%。
教训:中文场景必须用中文优化的 Embedding 模型。推荐 BGE 系列或 m3e-base。
坑 2:Chunk 切分没考虑表格,表格数据全废
一份产品参数表被切成了 6 个 chunk,检索时只召回了其中 2 个,答案张冠李戴。
教训:表格作为独立 chunk,不参与长度切分。在 chunk 元数据里标记 type: table。
坑 3:只做向量检索,精确术语全漏
用户搜「RFC 7231」,向量检索召回一堆 HTTP 协议介绍,但没有一篇提到 RFC 7231 本身。
教训:混合召回(向量 + BM25)是刚需,不是可选项。
坑 4:上下文太长,LLM 开始「断章取义」
为了追求全面,一次塞了 15 个 chunk(约 8000 token)给 LLM。结果模型只关注了前 3 个和最后 2 个,中间的全被忽略。
教训:上下文不是越多越好。3-5 个高质量 chunk 远胜 15 个平庸 chunk。这就是 Rerank 存在的意义。
坑 5:没有做 Query 改写,多轮对话全靠蒙
用户第一问「公司的报销流程是什么」,第二问「出差标准多少」。系统拿「出差标准多少」去检索,完全找不到上下文。
教训:多轮对话必须做 Query 改写,把指代消解后再检索。
def rewrite_query(chat_history, current_query, llm):
"""用 LLM 做多轮对话的 Query 改写"""
prompt = f"""根据对话历史,将用户最新问题改写为可独立检索的完整问题。
对话历史:{chat_history}
最新问题:{current_query}
改写后的问题:"""
return llm(prompt)坑 6:增量更新没做好,知识库越用越旧
最初全量重建索引,每次更新要跑 4 小时。文档改了但知识库没更新,用户问到的都是旧信息。
教训:必须做增量索引。给每个文档算 hash,变更的才重新切分和 Embedding。
效果评估体系
上线前必须有量化评估,不能靠「感觉还行」就发版。
我们用了两个指标:
- 召回准确率:人工标注 200 个问题的标准答案段落,看检索结果是否命中
- 回答满意度:上线后让用户打分(1-5 星),统计平均分
最终线上数据:
| 指标 | 上线初期 | 优化三个月后 |
|---|---|---|
| 召回准确率 | 62% | 85% |
| 回答满意度 | 3.2 / 5 | 4.3 / 5 |
| 首 token 延迟 | 4.2s | 2.1s |
| 幻觉率(抽检) | 12% | 3% |
复盘总结
RAG 不是「调个 API + 接个向量库」就完事的项目。它的难点在于:工程化链路长,每一环都有坑,任何一环掉链子整体效果就崩。
如果重新做一遍,我会把精力分配调整为:
- 文档解析与 Chunk 策略:40%(最被低估的环节)
- 检索与 Rerank:25%
- Prompt 工程与生成调优:20%
- 架构与工程化:10%
- 评估体系:5%
最后一句:RAG 项目没有银弹,所有选型都要基于你自己的数据特征来定。先跑通最小闭环,再逐环优化。