## 一、技术选型的纠结与最终决策### 1.1 向量数据库的选择这是第一个选择题。我评估了几个主流选项| 数据库 | 优势 | 劣势 | 我的判断 ||--------|------|------|----------|| Milvus | 功能最全、开源、社区活跃 | 部署运维较重需要K8s | 适合大规模生产 || Qdrant | Rust写的性能好部署简单 | 生态相对年轻 | 中小规模首选 || PGVector | 基于PostgreSQL运维统一 | 索引重建慢大数据量吃力 | 小项目或已有PG栈 || Elasticsearch | 团队熟悉支持全文向量混合检索 | 向量性能不如专用库 | 我们最终选了它 |最终我选了Elasticsearch 8.11以上版本。原因很实际团队对ES太熟了不需要额外学习成本而且我们除了语义检索还需要大量基于关键词的精确匹配比如搜索设备编号MC-2024-0087这种向量检索一塌糊涂。ES天然支持混合检索这个优势在后面会展开说。### 1.2 Embedding模型的选择中文Embedding模型我测试了四款text2vec-large-chinese、m3e-base、bge-large-zh-v1.5、以及OpenAI的text-embedding-3-small。测试方法很简单从客户文档里抽了200个问答对计算Recall10。结果- bge-large-zh-v1.50.87- m3e-base0.84- text2vec0.79- OpenAI0.88但延迟高、有数据出境问题最终选bge-large-zh-v1.5理由中文效果第一梯队可本地部署显存占用约2.5GB我们的单卡A10能跑。## 二、数据处理RAG效果的命门很多教程把RAG的重点放在检索和生成上我在实际项目中得出的结论恰好相反**数据处理占了成功率的70%**。以下是我被现实教训后总结的清洗pipeline。### 2.1 文档解析的噩梦客户的文档格式五花八门PDF、Word、Excel、PPT、扫描图片。PDF里还有大量表格和流程图。踩坑1PyPDF2解析带表格的PDF出来的文字顺序完全错乱。解决方案是换用PDFPlumber它能保留坐标信息按阅读顺序重组文本。踩坑2扫描版PDF是图片需要OCR。试了Tesseract中文识别率惨不忍睹。最终上了PaddleOCR配合版面分析模型表格和正文分开处理。踩坑3Word文档里嵌的Excel对象python-docx读不到。解决方案是先用Aspose或Unstructured库做深度解析。最终的文档解析架构原始文档 → 格式识别 →├─ PDF(文本) → PDFPlumber├─ PDF(扫描) → PaddleOCR 版面分析├─ Word → python-docx 解压内嵌对象├─ Excel → pandas读取每个sheet└─ PPT → python-pptx↓统一输出为Markdown格式保留标题层级和表格为什么统一转Markdown因为Markdown的标题层级可以被切分器利用表格语法能被LLM更好理解。### 2.2 智能切分最被低估的技术点最初的方案很天真按固定长度512字符切分加20字符重叠。结果惨不忍睹——一段技术参数被从中切断上半段问这个设备的功率是多少召回不到对应的下半段。我最终采用的方案是**语义切分 父子文档结构**。**语义切分逻辑**1. 先用标题层级# ## ###作为天然分割边界2. 对于没有标题的正文用句子边界句号、问号、感叹号 语义相似度来判断段落边界3. 每个chunk最小200字、最大800字**父子文档结构**是我认为最实用的技巧- **父文档**完整的段落或章节800-1200字用于后续LLM生成答案的上下文- **子文档**将父文档拆成小块200-300字用于向量检索检索时匹配子文档但返回对应的父文档给LLM。这样既保证了检索精度小粒度匹配又保证了上下文完整性大粒度输入。代码片段pythonclass DocumentChunker:def __init__(self, min_chunk_size200, max_chunk_size800):self.min_size min_chunk_sizeself.max_size max_chunk_sizedef split_by_semantic(self, text: str, headers: List[str]) - List[Dict]:# 先按标题切sections self.split_by_headers(text, headers)chunks []for section in sections:if len(section) self.max_size:chunks.append(self.create_chunk(section, is_parentTrue))else:# 长文本按句子进一步切分sub_chunks self.split_by_sentences(section, self.max_size)# 父chunk保留完整段落parent self.create_chunk(section, is_parentTrue)chunks.append(parent)# 子chunk用于检索关联到父chunk IDfor sub in sub_chunks:child self.create_chunk(sub, is_parentFalse, parent_idparent.id)chunks.append(child)return chunks## 三、检索策略从单路到多路融合初期只用向量检索效果不行。原因是大量用户问的是XX设备的维护流程是什么这种问题里的XX设备是专有名词向量检索无法精确匹配。### 3.1 最终的混合检索架构用户Query →├─ 向量检索bge embedding → 召回top20├─ 关键词检索ES的BM25 → 召回top20├─ 重排序Cross-Encoder → 合并排序取top5└─ 输出给LLM**重排序是关键**。向量检索和BM25各自的top20有重叠但不完全一样。用Cross-Encoder模型对合并后的候选集逐一计算与Query的相关性分数重新排序。这里有个细节Cross-Encoder比Bi-Encoder即双塔向量模型慢得多因为需要把Query和每个Document拼接后过模型。我的优化是只对合并后的最多30个候选做rerank而不是对全库。Cross-Encoder我用的是bge-reranker-large效果明显Hit5从0.78提升到0.89。### 3.2 Query改写解决用户问法不规范的问题一线员工提问极不规范典型例子- 那个机器坏了咋整实际想问某型号数控机床的报警代码处理方法- 上次那个文件指代不明我加了一层Query改写模块用一个小参数模型Qwen2-7B在后台做两件事1. **指代消解**结合会话历史把那个机器替换成具体型号2. **问题补全**把口语化问题改写为规范的检索式问句实测Query改写后检索Hit5提升了约12个百分点。## 四、生成环节让LLM学会我不知道### 4.1 Prompt工程的核心教训一开始用的prompt很简单基于以下参考资料回答用户问题。结果模型在各种编造尤其是当参考资料不包含答案时它会强行凑一个似是而非的回答。最后的prompt结构我迭代了十几版稳定下来的核心框架你是一个{domain}领域的技术专家。你的任务是根据【参考资料】回答用户问题。【参考资料】{retrieved_chunks}【用户问题】{query}【回答要求】1. 如果参考资料足以回答问题请给出准确、简洁的回答并在回答末尾引用来源标注【参考资料X】2. 如果参考资料部分相关但不完整请说明根据现有资料可以确认以下部分...但关于...的信息未找到3. 如果参考资料完全不相关或无有效信息请直接回答抱歉现有知识库中未找到相关信息**绝对不要编造**4. 回答时使用条理化的格式分点说明【回答】关键在第3条明确允许模型说不知道。这大幅降低了幻觉率。### 4.2 对话记忆的取舍产品经理要求支持多轮对话。初期方案是把完整历史对话都塞进上下文结果Token消耗暴涨且随着对话轮次增加检索的query会越来越偏离原始问题。我的最终方案- **对话历史只保留最近3轮**超出部分压缩为摘要- **每轮独立检索**即使用户说继续系统也会把上一轮的上下文拼接到当前query后再去检索而不是复用之前的检索结果- **意图判断**对用户query做轻量级意图分类如果是新话题则清空对话记忆## 五、评估体系没有度量就没有优化我建立了一套三层评估体系### 5.1 离线评估开发阶段构建了500个问答对的测试集每个问答对包含问题、标准答案、参考答案文档ID列表。核心指标- **Hit5**正确文档是否在召回top5中 → 目标 0.9- **MRR**第一个正确文档的排序位置 → 目标 0.85- **AnswerCorrectness**用GPT-4作为裁判比对系统回答和标准答案的语义一致性 → 目标 0.85### 5.2 在线监控生产阶段上线后必须监控- **平均响应延迟**目标 3秒超过则触发告警- **用户反馈**每个回答下面放有用/无用按钮负反馈率 15% 则触发人工review- **空回答率**模型说不知道的比例如果突然下降说明幻觉可能上升### 5.3 持续迭代每月从生产日志中采样负反馈案例人工标注后加入测试集重新评估各模块效果针对性优化。前三个月每月Hit5提升2-3个百分点之后趋缓。## 六、算力成本和部署方案最终生产环境配置| 组件 | 规格 | 成本/月 ||------|------|---------|| ES集群 | 3节点每节点16核32GSSD 2TB | 约4000元 || Embedding服务 | A10 24G单卡 | 约3000元 || Reranker服务 | 共用A10 | - || LLM服务 | 混元API也可用Qwen2-72B本地部署 | 按Token计费约0.02元/次 || 总成本 | | 约10000元/月 |对于3000人企业的内部知识库这个成本在可接受范围。
RAG系统从0到1
发布时间:2026/6/26 2:49:55
## 一、技术选型的纠结与最终决策### 1.1 向量数据库的选择这是第一个选择题。我评估了几个主流选项| 数据库 | 优势 | 劣势 | 我的判断 ||--------|------|------|----------|| Milvus | 功能最全、开源、社区活跃 | 部署运维较重需要K8s | 适合大规模生产 || Qdrant | Rust写的性能好部署简单 | 生态相对年轻 | 中小规模首选 || PGVector | 基于PostgreSQL运维统一 | 索引重建慢大数据量吃力 | 小项目或已有PG栈 || Elasticsearch | 团队熟悉支持全文向量混合检索 | 向量性能不如专用库 | 我们最终选了它 |最终我选了Elasticsearch 8.11以上版本。原因很实际团队对ES太熟了不需要额外学习成本而且我们除了语义检索还需要大量基于关键词的精确匹配比如搜索设备编号MC-2024-0087这种向量检索一塌糊涂。ES天然支持混合检索这个优势在后面会展开说。### 1.2 Embedding模型的选择中文Embedding模型我测试了四款text2vec-large-chinese、m3e-base、bge-large-zh-v1.5、以及OpenAI的text-embedding-3-small。测试方法很简单从客户文档里抽了200个问答对计算Recall10。结果- bge-large-zh-v1.50.87- m3e-base0.84- text2vec0.79- OpenAI0.88但延迟高、有数据出境问题最终选bge-large-zh-v1.5理由中文效果第一梯队可本地部署显存占用约2.5GB我们的单卡A10能跑。## 二、数据处理RAG效果的命门很多教程把RAG的重点放在检索和生成上我在实际项目中得出的结论恰好相反**数据处理占了成功率的70%**。以下是我被现实教训后总结的清洗pipeline。### 2.1 文档解析的噩梦客户的文档格式五花八门PDF、Word、Excel、PPT、扫描图片。PDF里还有大量表格和流程图。踩坑1PyPDF2解析带表格的PDF出来的文字顺序完全错乱。解决方案是换用PDFPlumber它能保留坐标信息按阅读顺序重组文本。踩坑2扫描版PDF是图片需要OCR。试了Tesseract中文识别率惨不忍睹。最终上了PaddleOCR配合版面分析模型表格和正文分开处理。踩坑3Word文档里嵌的Excel对象python-docx读不到。解决方案是先用Aspose或Unstructured库做深度解析。最终的文档解析架构原始文档 → 格式识别 →├─ PDF(文本) → PDFPlumber├─ PDF(扫描) → PaddleOCR 版面分析├─ Word → python-docx 解压内嵌对象├─ Excel → pandas读取每个sheet└─ PPT → python-pptx↓统一输出为Markdown格式保留标题层级和表格为什么统一转Markdown因为Markdown的标题层级可以被切分器利用表格语法能被LLM更好理解。### 2.2 智能切分最被低估的技术点最初的方案很天真按固定长度512字符切分加20字符重叠。结果惨不忍睹——一段技术参数被从中切断上半段问这个设备的功率是多少召回不到对应的下半段。我最终采用的方案是**语义切分 父子文档结构**。**语义切分逻辑**1. 先用标题层级# ## ###作为天然分割边界2. 对于没有标题的正文用句子边界句号、问号、感叹号 语义相似度来判断段落边界3. 每个chunk最小200字、最大800字**父子文档结构**是我认为最实用的技巧- **父文档**完整的段落或章节800-1200字用于后续LLM生成答案的上下文- **子文档**将父文档拆成小块200-300字用于向量检索检索时匹配子文档但返回对应的父文档给LLM。这样既保证了检索精度小粒度匹配又保证了上下文完整性大粒度输入。代码片段pythonclass DocumentChunker:def __init__(self, min_chunk_size200, max_chunk_size800):self.min_size min_chunk_sizeself.max_size max_chunk_sizedef split_by_semantic(self, text: str, headers: List[str]) - List[Dict]:# 先按标题切sections self.split_by_headers(text, headers)chunks []for section in sections:if len(section) self.max_size:chunks.append(self.create_chunk(section, is_parentTrue))else:# 长文本按句子进一步切分sub_chunks self.split_by_sentences(section, self.max_size)# 父chunk保留完整段落parent self.create_chunk(section, is_parentTrue)chunks.append(parent)# 子chunk用于检索关联到父chunk IDfor sub in sub_chunks:child self.create_chunk(sub, is_parentFalse, parent_idparent.id)chunks.append(child)return chunks## 三、检索策略从单路到多路融合初期只用向量检索效果不行。原因是大量用户问的是XX设备的维护流程是什么这种问题里的XX设备是专有名词向量检索无法精确匹配。### 3.1 最终的混合检索架构用户Query →├─ 向量检索bge embedding → 召回top20├─ 关键词检索ES的BM25 → 召回top20├─ 重排序Cross-Encoder → 合并排序取top5└─ 输出给LLM**重排序是关键**。向量检索和BM25各自的top20有重叠但不完全一样。用Cross-Encoder模型对合并后的候选集逐一计算与Query的相关性分数重新排序。这里有个细节Cross-Encoder比Bi-Encoder即双塔向量模型慢得多因为需要把Query和每个Document拼接后过模型。我的优化是只对合并后的最多30个候选做rerank而不是对全库。Cross-Encoder我用的是bge-reranker-large效果明显Hit5从0.78提升到0.89。### 3.2 Query改写解决用户问法不规范的问题一线员工提问极不规范典型例子- 那个机器坏了咋整实际想问某型号数控机床的报警代码处理方法- 上次那个文件指代不明我加了一层Query改写模块用一个小参数模型Qwen2-7B在后台做两件事1. **指代消解**结合会话历史把那个机器替换成具体型号2. **问题补全**把口语化问题改写为规范的检索式问句实测Query改写后检索Hit5提升了约12个百分点。## 四、生成环节让LLM学会我不知道### 4.1 Prompt工程的核心教训一开始用的prompt很简单基于以下参考资料回答用户问题。结果模型在各种编造尤其是当参考资料不包含答案时它会强行凑一个似是而非的回答。最后的prompt结构我迭代了十几版稳定下来的核心框架你是一个{domain}领域的技术专家。你的任务是根据【参考资料】回答用户问题。【参考资料】{retrieved_chunks}【用户问题】{query}【回答要求】1. 如果参考资料足以回答问题请给出准确、简洁的回答并在回答末尾引用来源标注【参考资料X】2. 如果参考资料部分相关但不完整请说明根据现有资料可以确认以下部分...但关于...的信息未找到3. 如果参考资料完全不相关或无有效信息请直接回答抱歉现有知识库中未找到相关信息**绝对不要编造**4. 回答时使用条理化的格式分点说明【回答】关键在第3条明确允许模型说不知道。这大幅降低了幻觉率。### 4.2 对话记忆的取舍产品经理要求支持多轮对话。初期方案是把完整历史对话都塞进上下文结果Token消耗暴涨且随着对话轮次增加检索的query会越来越偏离原始问题。我的最终方案- **对话历史只保留最近3轮**超出部分压缩为摘要- **每轮独立检索**即使用户说继续系统也会把上一轮的上下文拼接到当前query后再去检索而不是复用之前的检索结果- **意图判断**对用户query做轻量级意图分类如果是新话题则清空对话记忆## 五、评估体系没有度量就没有优化我建立了一套三层评估体系### 5.1 离线评估开发阶段构建了500个问答对的测试集每个问答对包含问题、标准答案、参考答案文档ID列表。核心指标- **Hit5**正确文档是否在召回top5中 → 目标 0.9- **MRR**第一个正确文档的排序位置 → 目标 0.85- **AnswerCorrectness**用GPT-4作为裁判比对系统回答和标准答案的语义一致性 → 目标 0.85### 5.2 在线监控生产阶段上线后必须监控- **平均响应延迟**目标 3秒超过则触发告警- **用户反馈**每个回答下面放有用/无用按钮负反馈率 15% 则触发人工review- **空回答率**模型说不知道的比例如果突然下降说明幻觉可能上升### 5.3 持续迭代每月从生产日志中采样负反馈案例人工标注后加入测试集重新评估各模块效果针对性优化。前三个月每月Hit5提升2-3个百分点之后趋缓。## 六、算力成本和部署方案最终生产环境配置| 组件 | 规格 | 成本/月 ||------|------|---------|| ES集群 | 3节点每节点16核32GSSD 2TB | 约4000元 || Embedding服务 | A10 24G单卡 | 约3000元 || Reranker服务 | 共用A10 | - || LLM服务 | 混元API也可用Qwen2-72B本地部署 | 按Token计费约0.02元/次 || 总成本 | | 约10000元/月 |对于3000人企业的内部知识库这个成本在可接受范围。