本期导读
本文内容包括:
- RAG的基本概念和工作原理
- 向量数据库的选择和配置
- Embedding模型的对比与应用
- 五种文档分块技术的详细讲解
- 三种检索策略的实战应用
- 完整的RAG系统实现代码
- 生产环境的优化技巧和常见问题
环境准备
Python环境安装
本文所有代码示例都基于LangChain 0.3.79版本。
一、RAG是什么?
1.1 用一个简单的比喻理解RAG
我们可以把RAG想象成考试的两种形式:
场景1:闭卷考试
arduino
老师问:"2024年中国GDP是多少?"
学生(只靠记忆):"呃...我只学到2021年的数据..."
问题:知识已经过时
场景2:开卷考试(这就是RAG的工作方式)
markdown
老师问:"2024年中国GDP是多少?"
学生的做法:
1. 翻书找到"2024年经济数据"这一章
2. 阅读相关内容
3. 根据书上的准确数字作答
优点:知识可以实时更新,答案有据可查
简单来说,RAG就是给AI加上了"查资料"的能力,让它不再只依赖预先训练的知识。
1.2 RAG的三个核心组成部分
RAG系统由三个核心部分组成,可以理解为:检索 + 生成。
scss
知识库 → 检索器 → 生成器
(存放文档) (找相关内容) (生成答案)
组件1:知识库(Knowledge Base)
知识库就像一个数字图书馆,存放着各种文档和数据:
- 公司规章制度文档
- 产品使用手册
- 技术文档
- 常见问题集
- 其他业务资料
组件2:检索器(Retriever)
检索器的作用类似于图书馆的目录系统,主要负责:
- 根据用户问题快速定位相关文档
- 找出最相关的内容片段
- 过滤掉不相关的信息
组件3:生成器(Generator)
生成器接收检索到的内容,然后:
- 理解和分析这些内容
- 结合用户的问题进行推理
- 生成连贯、准确的回答
1.3 RAG完整工作流程
RAG系统处理一个用户问题需要经过五个步骤:
步骤1:用户提问
arduino
用户输入:"公司的年假政策是什么?"
步骤2:问题向量化
arduino
系统将问题转换成数字向量:[0.2, 0.8, 0.1, ...]
这就像给问题打上一个数字"指纹",方便后续的相似度计算
步骤3:检索相关文档
arduino
在知识库中搜索,找到最相关的3-5段内容
比如:
"员工享有带薪年假...入职满1年5天..."
"年假可累计使用...最多结转至下年度..."
步骤4:构建提示词
将检索到的内容和用户问题组合:
上下文:【检索到的相关内容】
问题:公司的年假政策是什么?
要求:基于上下文回答问题
步骤5:LLM生成答案
diff
大语言模型根据上下文生成回答:
"根据公司规定,员工年假政策如下:
- 入职满1年:5天年假
- 入职满3年:10天年假
- 入职满5年:15天年假
年假可在当年使用,未使用部分可结转至下一年度。"
步骤6:返回结果
将答案展示给用户,同时附上引用来源,便于核实
1.4 RAG vs 传统AI的区别
维度 | 传统AI(ChatGPT等) | RAG增强的AI |
---|---|---|
知识来源 | 只有训练时的数据 | 训练数据 + 实时文档 |
更新频率 | 需要重新训练 | 随时更新文档即可 |
准确性 | 可能产生幻觉 | 基于真实文档,更可靠 |
可追溯性 | 无法追溯来源 | 可以显示引用来源 |
私有数据 | 无法访问 | 可以访问企业内部数据 |
1.5 RAG的典型应用场景
场景1:企业智能客服
markdown
用户提问:"如何申请发票?"
RAG系统的处理流程:
1. 检索公司财务制度文档
2. 找到"发票申请流程"相关章节
3. 基于文档内容生成详细的操作步骤
优势:回答准确、实时更新、可追溯来源
场景2:技术文档问答
markdown
开发者提问:"如何使用Spring AI创建RAG系统?"
RAG系统的处理流程:
1. 检索Spring AI官方文档和示例代码
2. 定位相关的API说明和代码片段
3. 生成完整的使用教程
优势:始终使用最新API、代码示例准确可靠
场景3:法律合规查询
markdown
律师提问:"2024年最新劳动法关于加班的规定是什么?"
RAG系统的处理流程:
1. 检索最新的法律法规数据库
2. 定位相关法律条款
3. 引用原文作答
优势:法条引用准确、有明确法律依据
场景4:医疗辅助诊断
markdown
医生输入:"患者出现头痛、发热、颈部僵硬等症状"
RAG系统的处理流程:
1. 检索医学文献和历史病例
2. 匹配相似的症状组合
3. 提供可能的诊断建议和进一步检查建议
优势:基于真实医学知识和临床经验
1.6 理解向量和Embedding(核心概念)
什么是向量?
arduino
文本 → 向量(一串数字)
例子:
"苹果" → [0.8, 0.1, 0.2, 0.9, ...]
"香蕉" → [0.7, 0.2, 0.1, 0.8, ...]
"汽车" → [0.1, 0.9, 0.8, 0.2, ...]
为什么需要向量?
- 计算机不懂文字,但懂数字
- 相似的词,向量也相似
- 可以快速计算相似度
向量相似度计算
csharp
"苹果"和"香蕉"的向量:
[0.8, 0.1, 0.2] 和 [0.7, 0.2, 0.1]
相似度:0.92(很相似,都是水果)
"苹果"和"汽车"的向量:
[0.8, 0.1, 0.2] 和 [0.1, 0.9, 0.8]
相似度:0.15(不相似)
可视化向量空间
为了更直观地理解向量和它们之间的关系,可以使用TensorFlow Embedding Projector这个在线工具。
在线地址:projector.tensorflow.org/
使用步骤:
- 访问网站
- 上传你的向量数据(TSV格式)
- 选择降维方法(PCA/t-SNE/UMAP)
- 在3D空间中查看向量的分布
通过这个工具,你可以看到:
- 语义相似的词在空间中会聚集在一起
- 词与词之间的关系和距离
- 帮助你直观理解Embedding的工作原理
下面是导出向量数据的代码示例:
python
# 导出向量用于Projector可视化
import numpy as np
# 假设你有embeddings和对应的文本
embeddings = np.array([...]) # shape: (n_samples, embedding_dim)
texts = ["文本1", "文本2", "文本3"]
# 导出为TSV格式
# 1. 向量文件
np.savetxt('vectors.tsv', embeddings, delimiter='\t')
# 2. 元数据文件
with open('metadata.tsv', 'w', encoding='utf-8') as f:
f.write('Text\n')
for text in texts:
f.write(f'{text}\n')
# 上传到 https://projector.tensorflow.org/ 查看
print("文件已生成,可以上传到TensorFlow Projector查看!")
1.7 小结
简单来说,RAG让AI具备了"查资料"的能力,不再只依赖训练时学到的知识。
对比:
- 没有RAG:AI只能依靠训练时学到的知识,这些知识可能已经过时或不够准确
- 有了RAG:AI会先检索最新的相关资料,再基于这些资料来回答问题,答案更准确也更及时
二、为什么需要RAG?
2.1 大模型的三大局限
局限1:知识截止日期问题
大语言模型的训练数据都有一个截止日期。比如GPT-3.5的知识截止在2021年9月,当用户询问2024年的信息时,模型就无法给出准确答案。
举例:
arduino
用户:"2024年巴黎奥运会金牌榜前三名是哪些国家?"
GPT-3.5:"抱歉,我的知识截止到2021年9月,无法回答关于2024年的问题。"
RAG的解决方案:实时检索最新的体育新闻数据库,获取最新的金牌榜信息,然后给出准确的回答。
局限2:无法访问私有数据
通用大模型只能使用公开的训练数据,无法访问企业内部的私有数据。
举例:
arduino
员工:"公司2024年Q3的销售额是多少?"
通用LLM:"我无法访问您公司的内部数据,建议您咨询财务部门。"
RAG的解决方案:检索公司内部的财报系统,提取Q3销售数据,基于真实数据给出回答。
局限3:容易产生幻觉
大模型有时会"一本正经地胡说八道",生成看似合理但实际错误的内容。
举例:
arduino
用户:"请列出《三体》中的主要人物关系。"
LLM可能编造:"王明是叶文洁的学生,后来成为了..."
(实际上《三体》中没有"王明"这个角色)
RAG的解决方案:先检索《三体》的原文和官方设定,基于真实内容生成答案,大大减少了产生幻觉的可能性。
2.2 有RAG vs 没有RAG的对比
为了更直观地理解RAG的价值,我们来看一个实际的对比案例。
场景:用户咨询公司的请假流程
没有RAG的传统大模型:
diff
用户:"公司请假需要提前几天申请?"
大模型的回答(基于通用知识):
"一般公司规定请假需要提前1-3天申请,具体以您所在公司的规章制度为准。
建议您查阅公司员工手册或咨询人力资源部门。"
问题:
- 回答很模糊,只能给出通用建议
- 无法提供公司具体的政策细节
- 用户还是不知道具体要提前几天
- 可能给出与公司实际规定不符的信息
使用RAG的增强系统:
markdown
用户:"公司请假需要提前几天申请?"
RAG系统的处理过程:
1. 检索公司《员工手册》
2. 找到相关章节:"第三章 考勤管理 - 请假制度"
3. 提取关键信息:
- 事假:提前3个工作日申请
- 病假:当日电话通知,返岗后补假条
- 年假:提前7个工作日申请
RAG系统的回答:
"根据公司《员工手册》第三章考勤管理规定:
- 事假需要提前3个工作日申请
- 病假需要当日电话通知直属主管,返岗后补交假条
- 年假需要提前7个工作日申请
具体流程是:在OA系统提交申请 → 主管审批 → 人力资源备案
参考来源:《员工手册》第3.2节"
优势:
- 回答准确具体,直接给出公司的实际规定
- 有明确的引用来源,便于核实
- 涵盖了不同类型假期的不同规定
- 用户可以直接按照指引操作
关键差异总结:
对比维度 | 没有RAG | 有RAG |
---|---|---|
信息来源 | 模型训练时的通用知识 | 企业实时的私有文档 |
回答准确性 | 模糊、通用 | 精确、具体 |
可信度 | 需要人工核实 | 有引用来源支撑 |
实用性 | 仅供参考 | 可直接使用 |
更新性 | 知识可能过时 | 随文档更新 |
2.3 RAG vs 传统检索系统
很多人会问:我们以前也有文档检索系统,RAG和传统检索有什么区别?
传统检索系统的工作方式:
markdown
1. 用户输入关键词:
"请假 提前 天数"
2. 系统进行关键词匹配:
在文档中查找包含"请假"、"提前"、"天数"的段落
3. 返回匹配的文档列表:
- 《员工手册》第3章
- 《考勤管理制度》第2节
- 《假期申请流程说明》
...(可能有十几个结果)
4. 用户的困境:
- 需要自己点开每个文档查看
- 需要自己判断哪个是最相关的
- 需要自己总结和理解内容
- 可能花费10-20分钟才能找到答案
RAG系统的工作方式:
markdown
1. 用户输入自然语言问题:
"公司请假需要提前几天申请?"
2. 系统智能处理:
- 理解用户真正想问的是什么
- 在知识库中进行语义检索(不只是关键词匹配)
- 找到最相关的3-5段内容
3. 系统直接给出答案:
"根据公司规定,不同类型的假期申请时间不同:
- 事假:提前3个工作日
- 病假:当日通知
- 年假:提前7个工作日
来源:《员工手册》第3.2节"
4. 用户体验:
- 5秒内得到精准答案
- 不需要自己翻阅文档
- 信息已经整理好,直接可用
核心区别对比:
维度 | 传统检索 | RAG检索增强生成 |
---|---|---|
输入方式 | 关键词 | 自然语言问题 |
匹配方式 | 精确关键词匹配 | 语义相似度匹配 |
返回结果 | 文档列表 | 直接的答案 |
结果形式 | 原始文档片段 | 重新组织的连贯回答 |
理解能力 | 无理解能力 | 理解问题意图 |
用户负担 | 需要自己阅读和理解 | 直接获得答案 |
处理复杂问题 | 困难 | 可以综合多个来源 |
时间成本 | 5-20分钟 | 5-10秒 |
举例说明传统检索的局限性:
场景:用户问"我入职2年了,能休几天年假?"
传统检索系统:
markdown
1. 提取关键词:"入职"、"2年"、"年假"
2. 返回所有包含这些词的文档
3. 可能返回:
- 《新员工入职指南》(不相关,只是有"入职"二字)
- 《公司2年发展规划》(不相关,只是有"2年")
- 《年假管理办法》(相关,但需要自己查找对应年限)
问题:
- 无法理解"2年"是指"工作年限"
- 无法自动计算对应的年假天数
- 返回太多噪音文档
RAG系统:
markdown
1. 理解用户意图:查询工作年限2年对应的年假天数
2. 检索相关政策:"入职满1年5天,满3年10天"
3. 智能推理:2年介于1-3年之间
4. 给出答案:"您入职2年,根据公司规定可享受5天年假。
满3年后将升至10天。来源:《员工手册》第3.2节"
优势:
- 理解了"2年"指的是工作年限
- 自动找到对应的政策条款
- 给出了明确的答案
- 还提供了额外有用的信息(3年后的变化)
2.4 为什么不能简单用"传统检索+大模型"?
有人可能会想:我先用传统检索找到文档,再把文档喂给大模型,不就行了吗?
这个方案的问题:
-
检索质量差
- 关键词匹配经常返回不相关的内容
- 大模型会基于这些低质量内容生成答案
- 结果:答非所问或者答案不准确
-
无法处理语义理解
- 传统检索不理解同义词:"请假"="休假"="告假"
- 传统检索不理解语境:用户问"苹果怎么样",是指水果还是手机?
- RAG的语义检索能理解这些细微差别
-
效率低
- 传统检索可能返回几十个文档
- 全部喂给大模型会超出上下文限制
- 需要手动筛选,失去了自动化的意义
正确的RAG方案:
- 使用向量数据库进行语义检索
- 精准找到最相关的3-5段内容
- 大模型基于高质量内容生成答案
- 结果准确、高效、可靠
2.5 RAG vs 微调 vs Prompt Engineering
除了传统检索,我们还需要了解RAG与其他大模型增强技术的对比。
维度 | RAG | 微调(Fine-tuning) | Prompt Engineering |
---|---|---|---|
数据更新 | 实时更新,只需更新知识库 | 需要重新训练模型 | 无法持续更新 |
成本 | 低(只需存储和检索) | 高(GPU训练成本) | 极低 |
准确性 | 高(基于真实数据) | 高(学习到模式) | 中等 |
可解释性 | 强(可追溯来源) | 弱(黑盒) | 强 |
适用场景 | 知识密集型任务 | 特定领域任务 | 通用任务 |
技术门槛 | 中等 | 高 | 低 |
响应速度 | 中等(需检索) | 快 | 快 |
结论:对于需要频繁更新的知识型应用,RAG是最佳选择。
二、RAG核心原理
2.1 RAG的完整工作流程
scss
┌─────────────────────────────────────────────────────────────┐
│ RAG完整工作流程 │
└─────────────────────────────────────────────────────────────┘
【离线阶段:知识库构建】
┌──────────────┐
│ 原始文档 │ (PDF, Word, HTML, Markdown...)
└──────┬───────┘
↓
┌──────────────┐
│ 文档加载 │ (Document Loader)
└──────┬───────┘
↓
┌──────────────┐
│ 文本分块 │ (Text Splitter: 按句子/段落/Token切分)
└──────┬───────┘
↓
┌──────────────┐
│ 向量化 │ (Embedding Model: text → vector)
└──────┬───────┘
↓
┌──────────────┐
│ 存储到向量库 │ (Vector Database: Milvus/Qdrant/Pinecone)
└──────────────┘
【在线阶段:检索生成】
┌──────────────┐
│ 用户问题 │ "公司Q3销售额是多少?"
└──────┬───────┘
↓
┌──────────────┐
│ 问题向量化 │ (同样的Embedding Model)
└──────┬───────┘
↓
┌──────────────┐
│ 向量检索 │ (相似度计算,Top-K召回)
└──────┬───────┘
↓
┌──────────────┐
│ 重排序 │ (可选:Rerank提高精度)
└──────┬───────┘
↓
┌──────────────┐
│ 构建Prompt │ "上下文:{检索内容}\n问题:{用户问题}"
└──────┬───────┘
↓
┌──────────────┐
│ LLM生成答案 │ (GPT-4/Claude/通义千问)
└──────┬───────┘
↓
┌──────────────┐
│ 返回结果 │ (答案 + 引用来源)
└──────────────┘
2.2 RAG的三种进化形态
🔹 基础RAG(Naive RAG)
ini
# 最简单的RAG流程
def naive_rag(question, vector_db, llm):
# 1. 检索
docs = vector_db.similarity_search(question, k=3)
# 2. 拼接上下文
context = "\n".join([doc.content for doc in docs])
# 3. 生成
prompt = f"根据以下内容回答问题:\n{context}\n\n问题:{question}"
answer = llm.generate(prompt)
return answer
优点 :简单直接,易于实现 缺点:检索质量不稳定,可能包含无关内容
🔹 高级RAG(Advanced RAG)
增强检索质量的多种技术:
-
预检索优化
- 查询改写(Query Rewriting)
- 查询扩展(Query Expansion)
- HyDE(Hypothetical Document Embeddings)
-
检索优化
- 混合检索(Dense + Sparse)
- Rerank重排序
- 上下文压缩
-
后检索优化
- 上下文去重
- 结果融合
- 引用提取
🔹 模块化RAG(Modular RAG)
┌─────────────────────────────────────────┐
│ 模块化RAG架构 │
├─────────────────────────────────────────┤
│ 路由模块:选择合适的检索策略 │
│ 检索模块:多路召回(向量/关键词/图谱) │
│ 排序模块:多阶段Rerank │
│ 生成模块:结构化输出 │
│ 记忆模块:多轮对话上下文 │
│ 评估模块:自动评分和反馈 │
└─────────────────────────────────────────┘
三、向量数据库选型
3.1 主流向量数据库对比
数据库 | 类型 | 语言 | 性能 | 易用性 | 推荐场景 |
---|---|---|---|---|---|
Milvus | 开源 | Go/C++ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 大规模生产环境 |
Qdrant | 开源 | Rust | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 中小规模应用 |
Pinecone | 商业 | - | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 快速上线 |
Chroma | 开源 | Python | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 开发测试 |
Weaviate | 开源 | Go | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 多模态应用 |
PGVector | 插件 | C | ⭐⭐⭐ | ⭐⭐⭐⭐ | 已有PostgreSQL |
Elasticsearch | 开源 | Java | ⭐⭐⭐ | ⭐⭐⭐⭐ | 混合检索 |
3.2 选型决策树
markdown
开始选型
↓
数据量是否超过1000万?
├─ 是 → Milvus(分布式架构)
└─ 否 ↓
需要混合检索(关键词+向量)?
├─ 是 → Elasticsearch 或 Weaviate
└─ 否 ↓
预算充足,追求简单?
├─ 是 → Pinecone(托管服务)
└─ 否 ↓
已有PostgreSQL数据库?
├─ 是 → PGVector(直接扩展)
└─ 否 ↓
Python生态,快速原型?
├─ 是 → Chroma(嵌入式)
└─ 否 → Qdrant(性能+易用平衡)
3.3 实战:快速搭建向量数据库
方案1:Chroma(最快5分钟上手)
ini
# 安装
pip install chromadb
# 使用
import chromadb
# 创建客户端(持久化)
client = chromadb.PersistentClient(path="./chroma_db")
# 创建集合
collection = client.create_collection(
name="my_documents",
metadata={"hnsw:space": "cosine"} # 余弦相似度
)
# 添加文档
collection.add(
documents=[
"Spring AI是Java生态的AI框架",
"LangChain是Python的AI应用框架",
"RAG检索增强生成技术"
],
ids=["doc1", "doc2", "doc3"],
metadatas=[
{"source": "docs", "category": "framework"},
{"source": "docs", "category": "framework"},
{"source": "docs", "category": "technology"}
]
)
# 查询
results = collection.query(
query_texts=["Java AI框架"],
n_results=2
)
print(results['documents'])
# 输出: [['Spring AI是Java生态的AI框架', 'LangChain是Python的AI应用框架']]
方案2:Qdrant(生产级部署)
ini
# 安装
pip install qdrant-client
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# 连接(本地或云端)
client = QdrantClient(url="http://localhost:6333")
# 或使用云服务
# client = QdrantClient(url="https://xxx.qdrant.io", api_key="your_key")
# 创建集合
client.create_collection(
collection_name="documents",
vectors_config=VectorParams(
size=1536, # OpenAI embedding维度
distance=Distance.COSINE
)
)
# 插入数据
points = [
PointStruct(
id=1,
vector=[0.1] * 1536, # 实际应该是embedding向量
payload={
"text": "Spring AI是Java生态的AI框架",
"category": "framework"
}
)
]
client.upsert(collection_name="documents", points=points)
# 搜索
search_result = client.search(
collection_name="documents",
query_vector=[0.1] * 1536,
limit=3
)
方案3:Milvus(企业级方案)
ini
# 安装
pip install pymilvus
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType
# 连接
connections.connect(host="localhost", port="19530")
# 定义Schema
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1536),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535)
]
schema = CollectionSchema(fields=fields)
# 创建集合
collection = Collection(name="documents", schema=schema)
# 创建索引(提升检索性能)
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "L2",
"params": {"nlist": 128}
}
collection.create_index(field_name="embedding", index_params=index_params)
# 插入数据
entities = [
[[0.1] * 1536], # embeddings
["Spring AI是Java生态的AI框架"] # texts
]
collection.insert(entities)
# 加载集合到内存
collection.load()
# 搜索
search_params = {"metric_type": "L2", "params": {"nprobe": 10}}
results = collection.search(
data=[[0.1] * 1536],
anns_field="embedding",
param=search_params,
limit=3,
output_fields=["text"]
)
四、Embedding模型选择
4.1 主流Embedding模型对比
模型 | 提供方 | 维度 | 语言支持 | 价格 | 适用场景 |
---|---|---|---|---|---|
text-embedding-3-large | OpenAI | 3072 | 多语言 | $0.13/1M tokens | 高质量通用 |
text-embedding-3-small | OpenAI | 1536 | 多语言 | $0.02/1M tokens | 成本敏感 |
BGE-large-zh | 智源 | 1024 | 中文优化 | 免费(自部署) | 中文场景 |
BGE-M3 | 智源 | 1024 | 多语言 | 免费(自部署) | 多语言混合 |
M3E | Moka | 768 | 中文 | 免费 | 中文小样本 |
text2vec | - | 多种 | 中文 | 免费 | 中文轻量级 |
Jina Embeddings | Jina AI | 768 | 多语言 | $0.02/1M tokens | 长文本 |
4.2 Embedding质量测试
中文Embedding基准测试(MTEB Chinese) :
模型 | 平均得分 | 分类 | 聚类 | 检索 |
---|---|---|---|---|
BGE-large-zh-v1.5 | 64.53 | 72.1 | 62.4 | 69.1 |
M3E-large | 63.12 | 70.8 | 60.9 | 67.5 |
text2vec-large-chinese | 60.45 | 68.3 | 58.2 | 64.8 |
4.3 实战:使用不同Embedding模型
方案1:OpenAI Embeddings
ini
from openai import OpenAI
client = OpenAI(api_key="your-api-key")
def get_embedding(text, model="text-embedding-3-small"):
text = text.replace("\n", " ")
response = client.embeddings.create(
input=[text],
model=model
)
return response.data[0].embedding
# 使用
embedding = get_embedding("RAG检索增强生成技术")
print(f"维度: {len(embedding)}") # 1536
方案2:本地部署BGE模型
python
# 安装
pip install sentence-transformers
from sentence_transformers import SentenceTransformer
# 加载模型(首次会下载)
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
# 生成embedding
texts = [
"RAG检索增强生成技术",
"向量数据库是RAG的核心组件"
]
embeddings = model.encode(texts, normalize_embeddings=True)
print(f"维度: {embeddings.shape}") # (2, 1024)
# 计算相似度
from sklearn.metrics.pairwise import cosine_similarity
similarity = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
print(f"相似度: {similarity:.4f}")
方案3:Jina Embeddings API
python
import requests
def jina_embedding(text):
url = "https://api.jina.ai/v1/embeddings"
headers = {
"Authorization": "Bearer your-api-key",
"Content-Type": "application/json"
}
data = {
"input": [text],
"model": "jina-embeddings-v2-base-zh"
}
response = requests.post(url, headers=headers, json=data)
return response.json()['data'][0]['embedding']
embedding = jina_embedding("RAG技术")
print(f"维度: {len(embedding)}")
4.4 Embedding优化技巧
技巧1:添加指令(Instruction)
ini
# BGE模型建议
instruction = "为这个句子生成表示以用于检索相关文章:"
query = instruction + "RAG是什么?"
# 文档侧不需要instruction
doc = "RAG是检索增强生成技术"
query_embedding = model.encode(query)
doc_embedding = model.encode(doc)
技巧2:分段处理长文本
python
def embed_long_text(text, model, max_length=512):
"""处理超长文本"""
# 分段
chunks = split_text(text, max_length)
# 每段embedding
chunk_embeddings = model.encode(chunks)
# 平均池化或加权平均
avg_embedding = chunk_embeddings.mean(axis=0)
return avg_embedding
技巧3:领域自适应微调
ini
from sentence_transformers import SentenceTransformer, InputExample, losses
from torch.utils.data import DataLoader
# 准备训练数据(文本对 + 相似度标签)
train_examples = [
InputExample(texts=["查询1", "文档1"], label=0.9),
InputExample(texts=["查询2", "文档2"], label=0.3),
]
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
# 加载预训练模型
model = SentenceTransformer('BAAI/bge-base-zh-v1.5')
# 定义损失函数
train_loss = losses.CosineSimilarityLoss(model)
# 微调
model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=1,
warmup_steps=100
)
# 保存
model.save('./fine-tuned-bge')
五、文本分块策略
5.1 为什么需要分块?
当我们面对一篇10000字的长文档时,如果直接将整篇文档向量化并存入数据库,会遇到几个问题:
不分块的问题:
- 整篇文档的向量信息密度低,检索时很难精准匹配
- 如果文档超过模型的上下文窗口长度,根本无法处理
- 相关性打分不准确,影响检索效果
分块的好处:
- 按段落或章节切分,每个块都保持语义的完整性
- 控制块的大小,既能精准检索,又能高效生成答案
- 提升检索准确度,更容易找到最相关的内容段落
5.2 五种分块技术概览
接下来我们会介绍五种分块技术,从简单到复杂,各有适用场景:
技术名称 | 实现难度 | 分块质量 | 适用场景 | 推荐程度 |
---|---|---|---|---|
固定大小分块 | 简单 | 一般 | 简单文本 | 中等 |
递归字符分块 | 较简单 | 良好 | 通用场景 | 强烈推荐 |
文档结构分块 | 中等 | 良好 | 结构化文档 | 推荐 |
语义分块 | 较复杂 | 优秀 | 高质量要求 | 推荐 |
Agent分块 | 复杂 | 优秀 | 复杂文档 | 特定场景 |
5.3 技术1:固定大小分块
这是最简单的分块方法,就像切西瓜一样,按照固定的大小将文档切分。
基本原理
例如设定每块500个字符:
arduino
"春天来了,花开了..." [第1块:0-500字符]
"树叶绿了,小鸟..." [第2块:500-1000字符]
优缺点分析
优点:实现简单,处理速度快 缺点:可能在句子中间切断,破坏语义的完整性
LangChain实战代码
ini
from langchain_text_splitters import CharacterTextSplitter
from langchain_core.documents import Document
# 示例文本
text = """
RAG(检索增强生成)是一种将检索系统与大型语言模型相结合的技术。
它通过在生成回答之前先检索相关文档,来提高回答的准确性和可靠性。
RAG系统包含三个核心组件:文档存储、检索器和生成器。
文档存储负责保存知识库,检索器负责找到相关内容,生成器负责生成最终答案。
"""
# 1. 固定字符分块
char_splitter = CharacterTextSplitter(
separator="\n\n", # 按段落分隔
chunk_size=100, # 每块100字符
chunk_overlap=20, # 重叠20字符(保持上下文连贯)
length_function=len
)
chunks = char_splitter.split_text(text)
print("📌 固定字符分块结果:")
for i, chunk in enumerate(chunks, 1):
print(f"\n块{i} ({len(chunk)}字符):")
print(f" {chunk[:50]}...")
# 转换为Document对象(带元数据)
documents = [
Document(
page_content=chunk,
metadata={"chunk_id": i, "method": "fixed_size"}
)
for i, chunk in enumerate(chunks)
]
输出示例:
erlang
块1 (97字符):
RAG(检索增强生成)是一种将检索系统与大型语言模型相结合的技术...
块2 (93字符):
它通过在生成回答之前先检索相关文档,来提高回答的准确性...
5.4 技术2:递归字符分块(最常用⭐推荐)
原理
markdown
智能分块:先尝试大分隔符,不行再用小的
分隔符优先级:
1. \n\n(段落)
2. \n(换行)
3. 。!?(句子)
4. 空格
5. 字符
就像切菜,先切大块,再切小块
为什么推荐?
- ✅ 保持语义完整性
- ✅ 适应各种文档格式
- ✅ LangChain内置,开箱即用
LangChain实战代码
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 递归分块器(推荐配置)
recursive_splitter = RecursiveCharacterTextSplitter(
# 分隔符优先级(从大到小)
separators=[
"\n\n", # 段落
"\n", # 换行
"。", # 中文句号
"!", # 感叹号
"?", # 问号
";", # 分号
",", # 逗号
" ", # 空格
"" # 字符
],
chunk_size=500, # 目标块大小
chunk_overlap=100, # 重叠大小
length_function=len,
is_separator_regex=False
)
# 分块
chunks = recursive_splitter.split_text(text)
print("📌 递归字符分块结果:")
for i, chunk in enumerate(chunks, 1):
print(f"\n块{i}:")
print(f" 长度: {len(chunk)}")
print(f" 内容: {chunk}")
print(f" 完整性: {'✅句子完整' if chunk[-1] in '。!?' else '⚠️可能被切断'}")
针对不同文档类型的配置
ini
# 配置1:中文文档(推荐)
chinese_splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""],
chunk_size=500,
chunk_overlap=100
)
# 配置2:英文文档
english_splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", ". ", "! ", "? ", "; ", ", ", " ", ""],
chunk_size=1000,
chunk_overlap=200
)
# 配置3:代码文档
code_splitter = RecursiveCharacterTextSplitter(
separators=["\n\nclass ", "\n\ndef ", "\n\n", "\n", " ", ""],
chunk_size=800,
chunk_overlap=100
)
# 配置4:Markdown文档
markdown_splitter = RecursiveCharacterTextSplitter(
separators=["## ", "### ", "\n\n", "\n", " ", ""],
chunk_size=600,
chunk_overlap=100
)
5.5 技术3:文档结构分块(结构化文档)
原理
diff
根据文档的自然结构分块:
- Markdown: 按标题层级
- HTML: 按标签
- Code: 按函数/类
- PDF: 按章节
保留文档结构信息
LangChain实战代码
python
from langchain_text_splitters import (
MarkdownHeaderTextSplitter,
HTMLHeaderTextSplitter,
RecursiveCharacterTextSplitter
)
# 示例:Markdown文档分块
markdown_text = """
# RAG技术指南
## 1. 什么是RAG
RAG是检索增强生成技术,结合了检索和生成两个过程。
### 1.1 核心组件
包括文档存储、检索器和生成器三个部分。
### 1.2 工作流程
用户提问 → 检索相关文档 → 生成答案
## 2. RAG的应用
RAG广泛应用于问答系统、智能客服等场景。
"""
# Markdown按标题分块
headers_to_split_on = [
("#", "H1"), # 一级标题
("##", "H2"), # 二级标题
("###", "H3"), # 三级标题
]
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False # 保留标题
)
# 分块
md_chunks = markdown_splitter.split_text(markdown_text)
print("📌 Markdown结构化分块结果:")
for i, chunk in enumerate(md_chunks, 1):
print(f"\n块{i}:")
print(f" 内容: {chunk.page_content[:100]}...")
print(f" 元数据: {chunk.metadata}")
# 输出示例:
# 块1:
# 内容: RAG是检索增强生成技术...
# 元数据: {'H1': 'RAG技术指南', 'H2': '1. 什么是RAG'}
HTML文档分块
ini
# HTML按标签分块
html_text = """
<html>
<body>
<h1>RAG技术</h1>
<p>RAG是一种强大的技术...</p>
<h2>核心组件</h2>
<p>包括检索器和生成器...</p>
<div class="code">
<pre>示例代码...</pre>
</div>
</body>
</html>
"""
headers_to_split_on = [
("h1", "Header 1"),
("h2", "Header 2"),
("div", "Division"),
]
html_splitter = HTMLHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
html_chunks = html_splitter.split_text(html_text)
组合使用:结构分块 + 大小控制
ini
# 第一步:按结构分块
md_chunks = markdown_splitter.split_text(markdown_text)
# 第二步:对大块再细分
final_chunks = []
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100
)
for chunk in md_chunks:
# 如果块太大,进一步分割
if len(chunk.page_content) > 500:
sub_chunks = text_splitter.split_text(chunk.page_content)
for sub_chunk in sub_chunks:
final_chunks.append(
Document(
page_content=sub_chunk,
metadata={**chunk.metadata, "is_split": True}
)
)
else:
final_chunks.append(chunk)
print(f"✅ 最终生成 {len(final_chunks)} 个块")
5.6 技术4:语义分块(高质量)
原理
diff
基于语义相似度分块:
- 计算每个句子的向量
- 相似的句子归为一块
- 不相似的句子开始新块
优点:语义连贯性最强
缺点:计算成本较高
完整实战代码
ini
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
def semantic_chunking(
text: str,
model_name: str = "BAAI/bge-small-zh-v1.5",
similarity_threshold: float = 0.5,
min_chunk_size: int = 50
):
"""
基于语义相似度的分块
Args:
text: 输入文本
model_name: Embedding模型
similarity_threshold: 相似度阈值(0-1)
min_chunk_size: 最小块大小
"""
# 1. 加载模型
model = SentenceTransformer(model_name)
# 2. 按句子分割
sentences = [s.strip() for s in text.split('。') if s.strip()]
if not sentences:
return []
# 3. 计算句子向量
print("🔄 计算句子向量...")
embeddings = model.encode(sentences, show_progress_bar=True)
# 4. 基于相似度分块
chunks = []
current_chunk = [sentences[0]]
for i in range(1, len(sentences)):
# 计算当前句子与前一句的相似度
similarity = cosine_similarity(
[embeddings[i-1]],
[embeddings[i]]
)[0][0]
if similarity > similarity_threshold:
# 相似度高,归入同一块
current_chunk.append(sentences[i])
else:
# 相似度低,开始新块
chunk_text = '。'.join(current_chunk) + '。'
if len(chunk_text) >= min_chunk_size:
chunks.append(chunk_text)
current_chunk = [sentences[i]]
# 添加最后一块
if current_chunk:
chunk_text = '。'.join(current_chunk) + '。'
if len(chunk_text) >= min_chunk_size:
chunks.append(chunk_text)
return chunks
# 使用示例
long_text = """
RAG技术是当前AI领域的热点。它结合了检索和生成两大技术。
传统的大模型只依赖预训练知识。这导致知识更新困难。
RAG通过外部检索解决了这个问题。系统可以实时获取最新信息。
在企业应用中,RAG非常实用。它能够访问私有数据库。
客服系统是典型的应用场景。用户问题可以得到准确回答。
"""
chunks = semantic_chunking(
text=long_text,
similarity_threshold=0.6, # 调整这个值控制块的大小
min_chunk_size=30
)
print("\n📌 语义分块结果:")
for i, chunk in enumerate(chunks, 1):
print(f"\n块{i}:")
print(f" {chunk}")
输出示例:
bash
块1: # 语义相关的句子聚在一起
RAG技术是当前AI领域的热点。它结合了检索和生成两大技术。
块2: # 话题转换,新的块
传统的大模型只依赖预训练知识。这导致知识更新困难。
RAG通过外部检索解决了这个问题。系统可以实时获取最新信息。
块3: # 应用场景相关
在企业应用中,RAG非常实用。它能够访问私有数据库。
客服系统是典型的应用场景。用户问题可以得到准确回答。
5.7 技术5:Agent分块(最智能)
原理
markdown
使用LLM作为Agent来智能分块:
1. Agent阅读文档
2. 理解文档结构和主题
3. 智能决定分块位置
4. 生成带摘要的块
优点:最智能,适应性强
缺点:成本高,速度慢
完整实战代码
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
from typing import List
# 定义输出结构
class DocumentChunk(BaseModel):
content: str = Field(description="块的内容")
summary: str = Field(description="块的摘要")
keywords: List[str] = Field(description="关键词列表")
topic: str = Field(description="主题")
class ChunkingResult(BaseModel):
chunks: List[DocumentChunk] = Field(description="分块结果列表")
def agent_chunking(text: str, max_chunks: int = 5):
"""
使用LLM Agent智能分块
Args:
text: 输入文本
max_chunks: 最大块数
"""
# 初始化LLM
llm = ChatOpenAI(
model="gpt-4o-mini", # 使用更便宜的模型
temperature=0
)
# 创建Prompt
prompt = PromptTemplate(
template="""
你是一个文档分块专家。请将以下文档智能地分成{max_chunks}个主题连贯的块。
要求:
1. 每个块应该包含一个完整的主题或概念
2. 保持段落完整性,不要在句子中间切断
3. 为每个块生成简洁的摘要
4. 提取3-5个关键词
5. 确定主题标签
文档内容:
{text}
请以JSON格式输出,包含chunks数组,每个chunk包含:
- content: 块内容
- summary: 摘要(不超过50字)
- keywords: 关键词列表
- topic: 主题标签
{format_instructions}
""",
input_variables=["text", "max_chunks"],
partial_variables={
"format_instructions": JsonOutputParser(
pydantic_object=ChunkingResult
).get_format_instructions()
}
)
# 构建链
chain = prompt | llm | JsonOutputParser(pydantic_object=ChunkingResult)
# 执行分块
print("🤖 Agent正在智能分块...")
result = chain.invoke({"text": text, "max_chunks": max_chunks})
return result["chunks"]
# 使用示例
document_text = """
RAG技术的核心原理是将检索系统与大语言模型相结合。
传统的大模型只能基于训练时的知识回答问题,而RAG可以实时检索外部知识库。
RAG系统包含三个主要组件:文档存储、检索器和生成器。
文档存储负责保存和索引知识库内容,通常使用向量数据库。
检索器负责根据用户查询找到最相关的文档片段。
生成器则基于检索到的内容生成最终答案。
在实际应用中,RAG的效果取决于多个因素。
文档分块策略会影响检索精度,太大或太小都不合适。
Embedding模型的选择也很重要,要根据具体场景选择。
检索策略可以是向量检索、关键词检索或混合检索。
RAG的优化是一个持续的过程。
需要通过评估指标来衡量效果,比如准确率和召回率。
还要进行A/B测试来对比不同配置的效果。
最终目标是在准确性、速度和成本之间找到平衡。
"""
chunks = agent_chunking(document_text, max_chunks=4)
print("\n📌 Agent智能分块结果:")
for i, chunk in enumerate(chunks, 1):
print(f"\n{'='*60}")
print(f"块{i}: {chunk['topic']}")
print(f"{'='*60}")
print(f"📝 摘要: {chunk['summary']}")
print(f"🔑 关键词: {', '.join(chunk['keywords'])}")
print(f"📄 内容预览: {chunk['content'][:100]}...")
输出示例:
markdown
============================================================
块1: RAG技术原理
============================================================
📝 摘要: RAG结合检索和生成,突破传统模型知识限制
🔑 关键词: RAG, 检索, 大语言模型, 知识库, 实时
📄 内容预览: RAG技术的核心原理是将检索系统与大语言模型相结合...
============================================================
块2: RAG系统架构
============================================================
📝 摘要: RAG系统三大组件:文档存储、检索器、生成器
🔑 关键词: 文档存储, 检索器, 生成器, 向量数据库
📄 内容预览: RAG系统包含三个主要组件:文档存储、检索器和生成器...
5.8 分块技术对比总结
技术 | 实现难度 | 计算成本 | 分块质量 | 推荐场景 |
---|---|---|---|---|
固定大小 | ⭐ | 💰 | ⭐⭐ | 快速原型 |
递归字符 | ⭐⭐ | 💰 | ⭐⭐⭐⭐ | 通用推荐 |
文档结构 | ⭐⭐⭐ | 💰💰 | ⭐⭐⭐⭐ | Markdown/HTML |
语义分块 | ⭐⭐⭐⭐ | 💰💰💰 | ⭐⭐⭐⭐⭐ | 高质量要求 |
Agent分块 | ⭐⭐⭐⭐⭐ | 💰💰💰💰 | ⭐⭐⭐⭐⭐ | 复杂文档 |
5.9 分块最佳实践
1. 选择合适的chunk_size
makefile
# 不同场景的推荐配置
CHUNK_CONFIGS = {
"short_qa": { # 短问答
"chunk_size": 256,
"chunk_overlap": 50,
"splitter": "recursive"
},
"long_documents": { # 长文档
"chunk_size": 1024,
"chunk_overlap": 200,
"splitter": "recursive"
},
"code": { # 代码文档
"chunk_size": 800,
"chunk_overlap": 100,
"splitter": "code_specific"
},
"conversation": { # 对话历史
"chunk_size": 2000,
"chunk_overlap": 400,
"splitter": "recursive"
}
}
2. 添加上下文信息
python
def add_context_to_chunks(chunks, document_title, document_type):
"""为每个块添加上下文信息"""
enriched_chunks = []
for i, chunk in enumerate(chunks):
enriched_chunk = Document(
page_content=chunk,
metadata={
"chunk_id": i,
"total_chunks": len(chunks),
"document_title": document_title,
"document_type": document_type,
"position": f"{i+1}/{len(chunks)}",
# 添加前后文预览
"prev_text": chunks[i-1][-50:] if i > 0 else "",
"next_text": chunks[i+1][:50] if i < len(chunks)-1 else ""
}
)
enriched_chunks.append(enriched_chunk)
return enriched_chunks
3. 实时监控分块质量
python
def evaluate_chunks(chunks):
"""评估分块质量"""
stats = {
"total_chunks": len(chunks),
"avg_chunk_size": np.mean([len(c) for c in chunks]),
"min_chunk_size": min([len(c) for c in chunks]),
"max_chunk_size": max([len(c) for c in chunks]),
"std_chunk_size": np.std([len(c) for c in chunks])
}
print("📊 分块质量评估:")
print(f" 总块数: {stats['total_chunks']}")
print(f" 平均大小: {stats['avg_chunk_size']:.0f} 字符")
print(f" 大小范围: {stats['min_chunk_size']} - {stats['max_chunk_size']}")
print(f" 标准差: {stats['std_chunk_size']:.2f}")
# 质量建议
if stats['std_chunk_size'] > stats['avg_chunk_size'] * 0.5:
print("⚠️ 建议:块大小差异较大,考虑调整参数")
else:
print("✅ 块大小分布合理")
return stats
5.3 实战:多种分块实现
方法1:LangChain 0.3 Text Splitter
ini
# LangChain 0.3 新版导入方式
from langchain_text_splitters import (
CharacterTextSplitter,
RecursiveCharacterTextSplitter,
TokenTextSplitter
)
text = """
RAG(检索增强生成)是一种结合检索和生成的技术。
它通过检索外部知识库来增强大模型的能力。
RAG主要包括两个阶段:
1. 离线阶段:构建知识库
2. 在线阶段:检索和生成
"""
# 1. 按字符分块
char_splitter = CharacterTextSplitter(
separator="\n\n",
chunk_size=100,
chunk_overlap=20,
length_function=len,
is_separator_regex=False
)
char_chunks = char_splitter.split_text(text)
# 2. 递归分块(推荐)
recursive_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20,
length_function=len,
separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""]
)
recursive_chunks = recursive_splitter.split_text(text)
# 3. 按Token分块(使用tiktoken)
token_splitter = TokenTextSplitter(
encoding_name="cl100k_base", # GPT-4 tokenizer
chunk_size=50,
chunk_overlap=10
)
token_chunks = token_splitter.split_text(text)
print("递归分块结果:")
for i, chunk in enumerate(recursive_chunks):
print(f"块{i+1}: {chunk}\n")
方法2:语义分块(基于Embedding)
ini
import numpy as np
from sentence_transformers import SentenceTransformer
def semantic_chunking(sentences, model, threshold=0.5):
"""基于语义相似度的分块"""
if not sentences:
return []
# 计算每个句子的embedding
embeddings = model.encode(sentences)
chunks = []
current_chunk = [sentences[0]]
for i in range(1, len(sentences)):
# 计算当前句子与前一句的相似度
similarity = cosine_similarity(
[embeddings[i-1]],
[embeddings[i]]
)[0][0]
if similarity > threshold:
# 相似度高,归入同一块
current_chunk.append(sentences[i])
else:
# 相似度低,开始新块
chunks.append(" ".join(current_chunk))
current_chunk = [sentences[i]]
# 添加最后一块
chunks.append(" ".join(current_chunk))
return chunks
# 使用
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
sentences = text.split("。")
semantic_chunks = semantic_chunking(sentences, model, threshold=0.6)
方法3:滑动窗口分块(保留上下文)
arduino
def sliding_window_chunking(text, window_size=500, step=400):
"""滑动窗口分块,有重叠"""
chunks = []
start = 0
while start < len(text):
end = start + window_size
chunk = text[start:end]
# 如果不是最后一块,尝试在句子边界结束
if end < len(text):
last_period = chunk.rfind('。')
if last_period > window_size * 0.5: # 至少保留一半
end = start + last_period + 1
chunk = text[start:end]
chunks.append(chunk)
start += step
return chunks
chunks = sliding_window_chunking(text, window_size=100, step=80)
5.4 分块最佳实践
推荐分块大小
makefile
# 根据不同场景选择
CHUNK_SIZES = {
"qa_short": { # 短问答
"chunk_size": 256,
"chunk_overlap": 50
},
"qa_long": { # 长文档问答
"chunk_size": 512,
"chunk_overlap": 100
},
"summarization": { # 摘要任务
"chunk_size": 1024,
"chunk_overlap": 200
},
"code": { # 代码文档
"chunk_size": 400,
"chunk_overlap": 80
}
}
元数据增强
python
# LangChain 0.3 新版导入
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
def create_enriched_chunks(text, metadata):
"""创建带丰富元数据的分块(LangChain 0.3)"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
length_function=len
)
chunks = splitter.split_text(text)
documents = []
for i, chunk in enumerate(chunks):
doc = Document(
page_content=chunk,
metadata={
**metadata, # 原始元数据
"chunk_id": i,
"chunk_total": len(chunks),
"char_count": len(chunk),
# 添加标题作为上下文
"context": f"文档标题:{metadata.get('title', '')}\n",
# 添加位置信息
"position": f"{i+1}/{len(chunks)}"
}
)
documents.append(doc)
return documents
# 使用
docs = create_enriched_chunks(
text=article_text,
metadata={
"title": "RAG技术详解",
"author": "张三",
"date": "2024-01-01",
"source": "技术博客",
"category": "AI技术"
}
)
六、检索策略详解
6.1 三种检索方式对比
🔵 Dense Retrieval(密集检索)
ini
# 基于向量相似度
query_embedding = model.encode("什么是RAG?")
results = vector_db.similarity_search(query_embedding, k=5)
✅ 优点:语义理解强,能捕捉隐含意图
❌ 缺点:对关键词精确匹配不敏感
🟢 Sparse Retrieval(稀疏检索)
ini
# 基于关键词匹配(BM25)
from rank_bm25 import BM25Okapi
corpus = [doc1, doc2, doc3, ...]
tokenized_corpus = [doc.split() for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)
query = "RAG 检索"
scores = bm25.get_scores(query.split())
✅ 优点:精确关键词匹配,解释性强
❌ 缺点:无法理解语义,召回率可能较低
🟣 Hybrid Retrieval(混合检索)
ini
# 结合两者优势
dense_results = vector_search(query) # 语义检索
sparse_results = bm25_search(query) # 关键词检索
final_results = reciprocal_rank_fusion(dense_results, sparse_results)
✅ 优点:结合两者优势,召回率和准确率都高
❌ 缺点:实现复杂,计算量大
6.2 实战:实现混合检索
完整混合检索系统
python
from typing import List, Dict
import numpy as np
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
import jieba
class HybridRetriever:
def __init__(self, documents: List[Dict], embedding_model="BAAI/bge-small-zh-v1.5"):
"""
混合检索器
Args:
documents: [{"id": "1", "text": "文档内容", ...}, ...]
"""
self.documents = documents
self.texts = [doc['text'] for doc in documents]
# 初始化向量模型
self.embedding_model = SentenceTransformer(embedding_model)
# 预计算文档向量
self.doc_embeddings = self.embedding_model.encode(
self.texts,
normalize_embeddings=True
)
# 初始化BM25
tokenized_corpus = [list(jieba.cut(text)) for text in self.texts]
self.bm25 = BM25Okapi(tokenized_corpus)
print(f"✅ 混合检索器初始化完成,文档数: {len(documents)}")
def dense_search(self, query: str, top_k: int = 10) -> List[Dict]:
"""向量检索"""
query_embedding = self.embedding_model.encode([query], normalize_embeddings=True)[0]
# 计算余弦相似度
similarities = np.dot(self.doc_embeddings, query_embedding)
# 获取Top-K
top_indices = np.argsort(similarities)[::-1][:top_k]
results = []
for idx in top_indices:
results.append({
"document": self.documents[idx],
"score": float(similarities[idx]),
"method": "dense"
})
return results
def sparse_search(self, query: str, top_k: int = 10) -> List[Dict]:
"""BM25检索"""
tokenized_query = list(jieba.cut(query))
scores = self.bm25.get_scores(tokenized_query)
# 获取Top-K
top_indices = np.argsort(scores)[::-1][:top_k]
results = []
for idx in top_indices:
if scores[idx] > 0: # 只返回有分数的
results.append({
"document": self.documents[idx],
"score": float(scores[idx]),
"method": "sparse"
})
return results
def hybrid_search(
self,
query: str,
top_k: int = 5,
dense_weight: float = 0.6,
sparse_weight: float = 0.4
) -> List[Dict]:
"""
混合检索(加权融合)
Args:
dense_weight: 向量检索权重
sparse_weight: BM25权重(两者之和应为1.0)
"""
# 分别检索
dense_results = self.dense_search(query, top_k=top_k*2)
sparse_results = self.sparse_search(query, top_k=top_k*2)
# 归一化分数
dense_scores = normalize_scores([r['score'] for r in dense_results])
sparse_scores = normalize_scores([r['score'] for r in sparse_results])
# 合并分数
score_dict = {}
for i, result in enumerate(dense_results):
doc_id = result['document']['id']
score_dict[doc_id] = dense_scores[i] * dense_weight
for i, result in enumerate(sparse_results):
doc_id = result['document']['id']
if doc_id in score_dict:
score_dict[doc_id] += sparse_scores[i] * sparse_weight
else:
score_dict[doc_id] = sparse_scores[i] * sparse_weight
# 排序
sorted_ids = sorted(score_dict.items(), key=lambda x: x[1], reverse=True)
# 构建结果
doc_map = {doc['id']: doc for doc in self.documents}
results = []
for doc_id, score in sorted_ids[:top_k]:
results.append({
"document": doc_map[doc_id],
"score": score,
"method": "hybrid"
})
return results
def normalize_scores(scores: List[float]) -> List[float]:
"""Min-Max归一化"""
if not scores:
return []
min_score = min(scores)
max_score = max(scores)
if max_score == min_score:
return [1.0] * len(scores)
return [(s - min_score) / (max_score - min_score) for s in scores]
# 使用示例
if __name__ == "__main__":
# 准备文档
documents = [
{"id": "1", "text": "RAG是检索增强生成技术,结合了检索和生成两个过程"},
{"id": "2", "text": "向量数据库用于存储文档的向量表示,支持相似度检索"},
{"id": "3", "text": "Embedding模型将文本转换为向量,是RAG的核心组件"},
{"id": "4", "text": "混合检索结合了向量检索和关键词检索的优势"},
{"id": "5", "text": "BM25是经典的关键词检索算法,在信息检索中广泛使用"}
]
# 初始化检索器
retriever = HybridRetriever(documents)
# 测试查询
query = "向量检索和关键词检索的区别"
print("\n📌 向量检索结果:")
dense_results = retriever.dense_search(query, top_k=3)
for r in dense_results:
print(f" - {r['document']['text'][:50]}... (分数: {r['score']:.4f})")
print("\n📌 BM25检索结果:")
sparse_results = retriever.sparse_search(query, top_k=3)
for r in sparse_results:
print(f" - {r['document']['text'][:50]}... (分数: {r['score']:.4f})")
print("\n📌 混合检索结果:")
hybrid_results = retriever.hybrid_search(query, top_k=3)
for r in hybrid_results:
print(f" - {r['document']['text'][:50]}... (分数: {r['score']:.4f})")
6.3 高级检索技术
技术1:查询改写(Query Rewriting)
ini
def query_rewrite(query: str, llm) -> List[str]:
"""用LLM改写查询,生成多个变体"""
prompt = f"""
将以下查询改写成3个不同但语义相似的问题:
原查询:{query}
要求:
1. 保持原意
2. 使用不同表达方式
3. 每行一个问题
"""
response = llm.generate(prompt)
queries = [query] + response.strip().split('\n')
return queries
# 使用
original_query = "RAG如何提升大模型效果?"
expanded_queries = query_rewrite(original_query, llm)
# 对每个查询进行检索,然后合并结果
all_results = []
for q in expanded_queries:
results = retriever.search(q)
all_results.extend(results)
# 去重和重排
final_results = deduplicate_and_rerank(all_results)
技术2:HyDE (Hypothetical Document Embeddings)
ini
def hyde_search(query: str, retriever, llm) -> List[Dict]:
"""
HyDE: 先让LLM生成假想的答案文档,
然后用这个文档去检索,通常效果更好
"""
# 1. 生成假想文档
prompt = f"""
请针对以下问题,生成一个详细的答案(即使你不确定):
问题:{query}
答案:
"""
hypothetical_doc = llm.generate(prompt)
# 2. 用假想文档检索
results = retriever.dense_search(hypothetical_doc, top_k=5)
return results
技术3:MMR(最大边际相关性)
ini
def mmr_rerank(
query_embedding: np.ndarray,
doc_embeddings: np.ndarray,
lambda_param: float = 0.5,
top_k: int = 5
) -> List[int]:
"""
MMR重排序:在相关性和多样性之间平衡
Args:
query_embedding: 查询向量
doc_embeddings: 文档向量
lambda_param: 相关性权重(1=只看相关性,0=只看多样性)
"""
selected = []
remaining = list(range(len(doc_embeddings)))
# 计算所有文档与查询的相似度
query_sim = np.dot(doc_embeddings, query_embedding)
# 选择第一个最相关的
first_idx = np.argmax(query_sim)
selected.append(first_idx)
remaining.remove(first_idx)
# 迭代选择剩余文档
while len(selected) < top_k and remaining:
mmr_scores = []
for idx in remaining:
# 相关性得分
relevance = query_sim[idx]
# 多样性得分(与已选文档的最大相似度)
selected_embeddings = doc_embeddings[selected]
diversity = np.max(np.dot(selected_embeddings, doc_embeddings[idx]))
# MMR得分
mmr_score = lambda_param * relevance - (1 - lambda_param) * diversity
mmr_scores.append((idx, mmr_score))
# 选择MMR得分最高的
best_idx = max(mmr_scores, key=lambda x: x[1])[0]
selected.append(best_idx)
remaining.remove(best_idx)
return selected
七、完整RAG系统实现
7.1 基于LangChain 0.3的RAG系统(Python)
python
"""
完整的RAG系统实现(生产级)- LangChain 0.3版本
包括:文档加载、分块、向量化、检索、生成、评估
"""
# LangChain 0.3 新版导入方式
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain_core.prompts import PromptTemplate
from langchain_core.callbacks import StreamingStdOutCallbackHandler
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
import os
class ProductionRAGSystem:
def __init__(
self,
docs_path: str,
embedding_model: str = "BAAI/bge-small-zh-v1.5",
llm_model: str = "gpt-3.5-turbo",
persist_directory: str = "./chroma_db"
):
self.docs_path = docs_path
self.persist_directory = persist_directory
# 初始化Embedding模型
print("📚 加载Embedding模型...")
self.embeddings = HuggingFaceEmbeddings(
model_name=embedding_model,
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
# 初始化LLM
print("🤖 初始化大模型...")
self.llm = ChatOpenAI(
model_name=llm_model,
temperature=0,
streaming=True,
callbacks=[StreamingStdOutCallbackHandler()]
)
# 向量数据库
self.vectorstore = None
self.qa_chain = None
def load_documents(self):
"""加载文档"""
print(f"📄 从 {self.docs_path} 加载文档...")
loader = DirectoryLoader(
self.docs_path,
glob="**/*.txt",
loader_cls=TextLoader,
loader_kwargs={'encoding': 'utf-8'}
)
documents = loader.load()
print(f"✅ 成功加载 {len(documents)} 个文档")
return documents
def split_documents(self, documents):
"""文档分块"""
print("✂️ 分块处理...")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
length_function=len,
separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""]
)
chunks = text_splitter.split_documents(documents)
print(f"✅ 生成 {len(chunks)} 个文本块")
return chunks
def build_vectorstore(self, chunks):
"""构建向量库"""
print("🗄️ 构建向量数据库...")
self.vectorstore = Chroma.from_documents(
documents=chunks,
embedding=self.embeddings,
persist_directory=self.persist_directory
)
self.vectorstore.persist()
print(f"✅ 向量库已保存到 {self.persist_directory}")
def load_vectorstore(self):
"""加载已有向量库"""
print("📂 加载已有向量库...")
self.vectorstore = Chroma(
persist_directory=self.persist_directory,
embedding_function=self.embeddings
)
print("✅ 向量库加载完成")
def setup_qa_chain(self, k=3, use_lcel=True):
"""
设置问答链(LangChain 0.3版本)
k: 检索Top-K文档
use_lcel: 是否使用LCEL(推荐,更灵活)
"""
print(f"⚙️ 设置QA链(检索Top-{k})...")
# 自定义Prompt模板
template = """
请基于以下上下文信息回答用户的问题。如果上下文中没有相关信息,请诚实地说"根据提供的信息无法回答"。
上下文信息:
{context}
用户问题:{question}
回答要求:
1. 准确、简洁
2. 基于上下文,不要编造
3. 如果有多个要点,请分点列出
4. 引用来源(如果有)
回答:
"""
prompt = PromptTemplate(
template=template,
input_variables=["context", "question"]
)
# 创建检索器
retriever = self.vectorstore.as_retriever(
search_type="similarity", # 或 "mmr" 增加多样性
search_kwargs={"k": k}
)
if use_lcel:
# 方式1:使用LCEL(LangChain Expression Language)- 推荐
# 更灵活,易于调试和自定义
def format_docs(docs):
return "\n\n".join([doc.page_content for doc in docs])
self.qa_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
| prompt
| self.llm
| StrOutputParser()
)
# 保存retriever用于返回source documents
self.retriever = retriever
else:
# 方式2:使用传统的RetrievalQA链
self.qa_chain = RetrievalQA.from_chain_type(
llm=self.llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": prompt}
)
self.use_lcel = use_lcel
print("✅ QA链设置完成")
def query(self, question: str) -> dict:
"""查询(兼容LCEL和传统方式)"""
if not self.qa_chain:
raise ValueError("请先调用 setup_qa_chain()")
print(f"\n💬 问题:{question}\n")
print("🔍 检索中...")
if self.use_lcel:
# LCEL方式
answer = self.qa_chain.invoke(question)
# 手动获取source documents
source_documents = self.retriever.invoke(question)
result = {
"query": question,
"result": answer,
"source_documents": source_documents
}
else:
# 传统方式
result = self.qa_chain.invoke({"query": question})
return result
def initialize(self, rebuild=False):
"""初始化完整系统"""
if rebuild or not os.path.exists(self.persist_directory):
# 重新构建
docs = self.load_documents()
chunks = self.split_documents(docs)
self.build_vectorstore(chunks)
else:
# 加载已有
self.load_vectorstore()
# 设置QA链
self.setup_qa_chain(k=3)
print("\n🎉 RAG系统初始化完成!\n")
# 使用示例(LangChain 0.3)
if __name__ == "__main__":
# 安装依赖
"""
pip install langchain==0.3.0
pip install langchain-community
pip install langchain-openai
pip install langchain-huggingface
pip install langchain-text-splitters
pip install chromadb
pip install sentence-transformers
pip install tiktoken
"""
# 初始化系统(使用LCEL)
rag = ProductionRAGSystem(
docs_path="./knowledge_base", # 你的文档目录
embedding_model="BAAI/bge-small-zh-v1.5",
llm_model="gpt-3.5-turbo"
)
# 第一次运行需要构建向量库
rag.initialize(rebuild=False)
# 查询
questions = [
"什么是RAG技术?",
"RAG有哪些应用场景?",
"如何选择向量数据库?"
]
for question in questions:
result = rag.query(question)
print(f"📝 答案:{result['result']}\n")
print("📚 参考来源:")
for i, doc in enumerate(result['source_documents']):
print(f" [{i+1}] {doc.page_content[:100]}...")
print(f" 来源:{doc.metadata.get('source', '未知')}\n")
print("-" * 80 + "\n")
7.2 基于Spring AI的RAG系统(Java)
java
package com.example.rag;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 生产级RAG服务实现
*/
@Service
public class RAGService {
@Autowired
private EmbeddingClient embeddingClient;
@Autowired
private VectorStore vectorStore;
@Autowired
private ChatClient chatClient;
/**
* 加载并处理文档
*/
public void loadDocuments(String filePath) {
// 1. 读取文档
TextReader reader = new TextReader(filePath);
List<Document> documents = reader.get();
// 2. 文本分块
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> chunks = splitter.apply(documents);
// 3. 向量化并存储
vectorStore.add(chunks);
System.out.println("✅ 成功加载 " + chunks.size() + " 个文档块");
}
/**
* RAG查询
*/
public RAGResponse query(String question) {
// 1. 检索相关文档
List<Document> relevantDocs = vectorStore.similaritySearch(question);
// 2. 构建上下文
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
// 3. 构建Prompt
String promptTemplate = """
请基于以下上下文信息回答用户的问题。
上下文:
{context}
问题:{question}
回答:
""";
PromptTemplate template = new PromptTemplate(promptTemplate);
Prompt prompt = template.create(Map.of(
"context", context,
"question", question
));
// 4. 调用LLM生成答案
String answer = chatClient.call(prompt).getResult().getOutput().getContent();
// 5. 返回结果
return RAGResponse.builder()
.question(question)
.answer(answer)
.sources(relevantDocs)
.build();
}
/**
* 批量查询
*/
public List<RAGResponse> batchQuery(List<String> questions) {
return questions.parallelStream()
.map(this::query)
.collect(Collectors.toList());
}
}
/**
* RAG响应对象
*/
@Data
@Builder
public class RAGResponse {
private String question;
private String answer;
private List<Document> sources;
private Double confidence;
}
/**
* Controller示例
*/
@RestController
@RequestMapping("/api/rag")
public class RAGController {
@Autowired
private RAGService ragService;
@PostMapping("/query")
public RAGResponse query(@RequestBody QueryRequest request) {
return ragService.query(request.getQuestion());
}
@PostMapping("/load")
public ResponseEntity<String> loadDocuments(@RequestParam String filePath) {
ragService.loadDocuments(filePath);
return ResponseEntity.ok("文档加载成功");
}
}
7.3 配置文件(application.yml)
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-3.5-turbo
temperature: 0.0
embedding:
options:
model: text-embedding-3-small
vectorstore:
qdrant:
host: localhost
port: 6333
collection-name: knowledge_base
# 自定义配置
rag:
chunk-size: 500
chunk-overlap: 100
retrieval-top-k: 3
embedding-batch-size: 100
八、RAG评估与优化
8.1 RAG评估指标
🎯 检索质量指标
指标 | 说明 | 计算方法 | 目标 |
---|---|---|---|
Precision@K | 检索结果的精确度 | 相关文档数 / K | 越高越好 |
Recall@K | 召回率 | 检索到的相关文档 / 总相关文档 | 越高越好 |
MRR | 平均倒数排名 | 1/第一个相关文档的排名 | 越高越好 |
NDCG@K | 归一化折损累积增益 | 考虑排序的相关性指标 | 越高越好 |
🎯 生成质量指标
指标 | 说明 | 评估方式 |
---|---|---|
Faithfulness | 答案是否忠实于上下文 | LLM评估或人工标注 |
Answer Relevancy | 答案与问题的相关性 | Embedding相似度 |
Context Relevancy | 检索上下文的相关性 | 人工标注或LLM评估 |
Hallucination Rate | 幻觉率 | 检测答案中的虚假信息 |
8.2 使用RAGAS进行自动化评估
python
"""
使用RAGAS框架进行RAG系统评估
"""
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_recall,
context_precision,
)
from datasets import Dataset
# 准备评估数据集
eval_data = {
"question": [
"什么是RAG技术?",
"RAG有哪些应用场景?"
],
"answer": [
"RAG是检索增强生成技术...",
"RAG主要应用于问答系统..."
],
"contexts": [
["RAG(检索增强生成)结合了检索和生成...", "RAG是一种..."],
["RAG应用场景包括:1. 企业知识库..."]
],
"ground_truths": [ # 标准答案(可选)
"RAG是Retrieval-Augmented Generation的缩写...",
"RAG可用于问答系统、文档摘要等场景"
]
}
dataset = Dataset.from_dict(eval_data)
# 执行评估
result = evaluate(
dataset,
metrics=[
faithfulness,
answer_relevancy,
context_recall,
context_precision,
],
)
print("📊 评估结果:")
print(f"Faithfulness: {result['faithfulness']:.4f}")
print(f"Answer Relevancy: {result['answer_relevancy']:.4f}")
print(f"Context Recall: {result['context_recall']:.4f}")
print(f"Context Precision: {result['context_precision']:.4f}")
# 保存结果
result.to_pandas().to_csv("rag_evaluation_results.csv")
8.3 优化技巧清单
✅ 数据质量优化
python
# 1. 文档清洗
def clean_document(text):
# 去除多余空白
text = re.sub(r'\s+', ' ', text)
# 去除特殊字符
text = re.sub(r'[^\w\s\u4e00-\u9fff]', '', text)
# 去除重复内容
lines = text.split('\n')
unique_lines = list(dict.fromkeys(lines))
return '\n'.join(unique_lines)
# 2. 元数据增强
def add_metadata(doc, source_type):
return {
"content": doc,
"source": source_type,
"timestamp": datetime.now(),
"keywords": extract_keywords(doc),
"summary": generate_summary(doc)
}
✅ 检索优化
ini
# 1. 多路召回
def multi_recall(query):
# 向量召回
vector_results = vector_search(query, top_k=20)
# 关键词召回
keyword_results = bm25_search(query, top_k=20)
# 图谱召回(如果有)
graph_results = knowledge_graph_search(query, top_k=10)
# 融合
return reciprocal_rank_fusion([
vector_results,
keyword_results,
graph_results
])
# 2. 查询扩展
def expand_query(query, llm):
prompt = f"生成5个与'{query}'语义相近但表达不同的查询:"
expanded = llm.generate(prompt)
return [query] + expanded.split('\n')
# 3. 相关性过滤
def filter_by_relevance(results, threshold=0.7):
return [r for r in results if r['score'] > threshold]
✅ 生成优化
python
# 1. 上下文压缩(LangChain 0.3)
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever
)
# 使用压缩检索器
compressed_docs = compression_retriever.invoke("你的查询")
# 2. 引用溯源
def add_citations(answer, sources):
"""在答案中添加引用标记"""
prompt = f"""
在以下答案中添加引用标记[1][2]等:
答案:{answer}
来源:
{sources}
带引用的答案:
"""
return llm.generate(prompt)
# 3. 答案验证
def verify_answer(question, answer, context):
"""验证答案是否基于上下文"""
prompt = f"""
问题:{question}
答案:{answer}
上下文:{context}
判断答案是否基于上下文,是否存在幻觉?
回答:是/否,理由:
"""
verification = llm.generate(prompt)
return "是" in verification
✅ 性能优化
python
# 1. 批量处理
def batch_embed(texts, batch_size=32):
embeddings = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
batch_embeddings = embedding_model.encode(batch)
embeddings.extend(batch_embeddings)
return embeddings
# 2. 缓存机制
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_embed(text):
return embedding_model.encode(text)
# 3. 异步检索
import asyncio
async def async_retrieve(query):
vector_task = asyncio.create_task(vector_search(query))
bm25_task = asyncio.create_task(bm25_search(query))
vector_results, bm25_results = await asyncio.gather(
vector_task,
bm25_task
)
return merge_results(vector_results, bm25_results)
九、实战案例
案例1:企业文档问答系统
python
"""
场景:为公司内部文档构建智能问答系统(LangChain 0.3)
文档类型:规章制度、技术文档、FAQ等
"""
import os
from pathlib import Path
class EnterpriseRAG:
def __init__(self):
self.rag_system = ProductionRAGSystem(
docs_path="./company_docs",
embedding_model="BAAI/bge-large-zh-v1.5", # 中文优化
llm_model="gpt-4" # 高质量生成
)
def load_company_docs(self):
"""加载公司文档,支持多种格式(LangChain 0.3)"""
# LangChain 0.3 导入方式
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
UnstructuredMarkdownLoader
)
all_docs = []
docs_dir = Path("./company_docs")
for file_path in docs_dir.rglob("*"):
if file_path.suffix == ".pdf":
loader = PyPDFLoader(str(file_path))
elif file_path.suffix in [".doc", ".docx"]:
loader = Docx2txtLoader(str(file_path))
elif file_path.suffix == ".md":
loader = UnstructuredMarkdownLoader(str(file_path))
else:
continue
docs = loader.load()
# 添加元数据
for doc in docs:
doc.metadata.update({
"department": self.extract_department(file_path),
"doc_type": self.classify_doc_type(doc.page_content),
"last_updated": file_path.stat().st_mtime
})
all_docs.extend(docs)
return all_docs
def extract_department(self, file_path):
"""从路径提取部门信息"""
# 假设路径格式: ./company_docs/技术部/xxx.pdf
parts = file_path.parts
if len(parts) > 2:
return parts[-2]
return "未分类"
def classify_doc_type(self, content):
"""文档类型分类"""
if "规章" in content or "制度" in content:
return "规章制度"
elif "技术" in content or "开发" in content:
return "技术文档"
elif "FAQ" in content or "常见问题" in content:
return "FAQ"
else:
return "其他"
def query_with_filters(self, question, department=None, doc_type=None):
"""带过滤条件的查询"""
# 构建过滤条件
filters = {}
if department:
filters['department'] = department
if doc_type:
filters['doc_type'] = doc_type
# 检索
retriever = self.rag_system.vectorstore.as_retriever(
search_kwargs={
"k": 5,
"filter": filters
}
)
# 查询
result = self.rag_system.qa_chain({"query": question})
return result
# 使用
enterprise_rag = EnterpriseRAG()
enterprise_rag.rag_system.initialize(rebuild=True)
# 查询示例
result = enterprise_rag.query_with_filters(
question="员工请假流程是什么?",
department="人力资源部",
doc_type="规章制度"
)
print(result['result'])
案例2:技术文档助手
python
"""
场景:开发者技术文档智能助手(LangChain 0.3)
支持:代码示例提取、API文档查询、最佳实践推荐
"""
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
class TechDocAssistant:
def __init__(self):
self.rag_system = ProductionRAGSystem(
docs_path="./tech_docs",
embedding_model="BAAI/bge-base-zh-v1.5"
)
def extract_code_examples(self, question):
"""提取代码示例(使用LCEL)"""
# 定制Prompt
code_prompt = PromptTemplate(
template="""
基于以下文档,提取与问题相关的代码示例:
文档:
{context}
问题:{question}
请按以下格式输出:
1. 代码示例(用```包裹)
2. 代码说明
3. 注意事项
""",
input_variables=["context", "question"]
)
# 使用LCEL构建链
def format_docs(docs):
return "\n".join([doc.page_content for doc in docs])
retriever = self.rag_system.vectorstore.as_retriever(
search_kwargs={"k": 3}
)
chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
| code_prompt
| self.rag_system.llm
| StrOutputParser()
)
# 执行
answer = chain.invoke(question)
return answer
def recommend_best_practices(self, topic):
"""推荐最佳实践"""
question = f"{topic}的最佳实践是什么?"
# 使用更大的k值获取更多上下文
docs = self.rag_system.vectorstore.similarity_search(question, k=10)
# 提取包含"最佳实践"、"建议"等关键词的文档
relevant_docs = [
doc for doc in docs
if any(keyword in doc.page_content for keyword in ["最佳实践", "建议", "推荐", "注意"])
]
if not relevant_docs:
return "未找到相关最佳实践"
context = "\n\n".join([doc.page_content for doc in relevant_docs[:5]])
prompt = f"""
总结以下关于{topic}的最佳实践:
{context}
请以要点形式列出:
1. ...
2. ...
"""
return self.rag_system.llm.generate(prompt)
# 使用
assistant = TechDocAssistant()
assistant.rag_system.initialize()
# 提取代码示例
code = assistant.extract_code_examples("如何使用Spring AI创建RAG系统?")
print(code)
# 推荐最佳实践
practices = assistant.recommend_best_practices("RAG系统优化")
print(practices)
十、常见问题与解决方案
Q1: 检索结果不相关
症状:返回的文档与查询无关
原因:
- Embedding模型不适配
- 分块粒度不合适
- 查询表达不清晰
解决方案:
ini
# 1. 使用领域适配的Embedding模型
model = SentenceTransformer('你的领域模型')
# 2. 调整分块大小
splitter = RecursiveCharacterTextSplitter(
chunk_size=300, # 减小块大小
chunk_overlap=50
)
# 3. 查询改写
def improve_query(query):
return f"关于{query}的详细信息"
Q2: 答案包含幻觉
症状:生成的答案不基于检索内容
解决方案:
python
# 1. 更严格的Prompt
strict_prompt = """
严格基于以下上下文回答问题。如果上下文中没有信息,回答"无法从提供的信息中找到答案"。
不要使用上下文之外的知识。
上下文:{context}
问题:{question}
答案:
"""
# 2. 答案验证
def verify_and_regenerate(answer, context):
if not is_grounded(answer, context):
return "根据提供的信息无法回答该问题"
return answer
# 3. 使用Faithfulness评估
from ragas.metrics import faithfulness
score = faithfulness.score(answer, context)
if score < 0.7:
answer = "答案可信度较低,请核实"
Q3: 响应速度慢
症状:查询响应时间超过5秒
优化方案:
python
# 1. 向量数据库索引优化
# Milvus: 使用IVF索引
index_params = {
"index_type": "IVF_SQ8", # 量化索引
"metric_type": "L2",
"params": {"nlist": 1024}
}
# 2. 减少检索数量
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 从5降到3
# 3. 使用缓存
import redis
cache = redis.Redis()
def cached_query(question):
cached_answer = cache.get(question)
if cached_answer:
return cached_answer
answer = rag_system.query(question)
cache.setex(question, 3600, answer) # 缓存1小时
return answer
# 4. 异步处理
async def async_rag_query(question):
retrieval_task = asyncio.create_task(retrieve(question))
docs = await retrieval_task
answer = await generate(docs, question)
return answer
Q4: 中文分词问题
症状:中文关键词检索效果差
解决方案:
ini
import jieba
jieba.load_userdict("custom_dict.txt") # 加载自定义词典
# BM25使用jieba分词
tokenized_corpus = [list(jieba.cut(doc)) for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)
# 查询也要分词
query_tokens = list(jieba.cut(query))
scores = bm25.get_scores(query_tokens)
Q5: 成本控制
症状:Embedding和LLM调用成本高
优化方案:
ini
# 1. 使用本地Embedding模型
model = SentenceTransformer('BAAI/bge-small-zh-v1.5') # 免费
# 2. 批量Embedding
texts = [doc1, doc2, ..., doc100]
embeddings = model.encode(texts) # 一次处理100个
# 3. 使用更小的LLM
# gpt-4-turbo → gpt-3.5-turbo → 本地模型
# 4. Prompt压缩
from langchain.retrievers.document_compressors import LLMChainExtractor
compressor = LLMChainExtractor.from_llm(llm)
# 5. 流式输出(提升体验)
for chunk in llm.stream(prompt):
print(chunk, end='', flush=True)