大模型应用技术之RAG检索增强

本期导读

本文内容包括:

  • 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/

使用步骤:

  1. 访问网站
  2. 上传你的向量数据(TSV格式)
  3. 选择降维方法(PCA/t-SNE/UMAP)
  4. 在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 为什么不能简单用"传统检索+大模型"?

有人可能会想:我先用传统检索找到文档,再把文档喂给大模型,不就行了吗?

这个方案的问题:

  1. 检索质量差

    • 关键词匹配经常返回不相关的内容
    • 大模型会基于这些低质量内容生成答案
    • 结果:答非所问或者答案不准确
  2. 无法处理语义理解

    • 传统检索不理解同义词:"请假"="休假"="告假"
    • 传统检索不理解语境:用户问"苹果怎么样",是指水果还是手机?
    • RAG的语义检索能理解这些细微差别
  3. 效率低

    • 传统检索可能返回几十个文档
    • 全部喂给大模型会超出上下文限制
    • 需要手动筛选,失去了自动化的意义

正确的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)

增强检索质量的多种技术:

  1. 预检索优化

    • 查询改写(Query Rewriting)
    • 查询扩展(Query Expansion)
    • HyDE(Hypothetical Document Embeddings)
  2. 检索优化

    • 混合检索(Dense + Sparse)
    • Rerank重排序
    • 上下文压缩
  3. 后检索优化

    • 上下文去重
    • 结果融合
    • 引用提取
🔹 模块化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)

相关推荐
飞哥数智坊7 小时前
Plan Mode 实战:为现有 Vue3 项目接入 CloudBase 持久化能力
ai编程·cursor
腾讯云云开发7 小时前
CloudBase + AI 游戏开发新范式,3小时极速开发
ai编程·游戏开发·小程序·云开发
302wanger7 小时前
Claude平替-iFlow初体验
ai编程
用户4099322502127 小时前
想抓PostgreSQL里的慢SQL?pg_stat_statements基础黑匣子和pg_stat_monitor时间窗,谁能帮你更准揪出性能小偷?
后端·ai编程·trae
xurime10 小时前
Excelize 开源基础库发布 2.10.0 版本更新
golang·开源·github·excel·ai编程·go语言
今天有个Bug10 小时前
claude-sonnet-4-5,IDE中集成,白嫖Claude Code代理,AnyRouter公益站
ai编程·claude code·ide集成·claude 4.5
CoderJia程序员甲20 小时前
GitHub 热榜项目 - 日榜(2025-10-11)
ai·开源·github·ai编程·github热榜
狠活科技20 小时前
免登录!免安装ClI,Claude Code官方插件接入API使用教程
人工智能·vscode·ai编程
下位子1 天前
『AI 编程』用 Codex 开发识字小帮手应用
android·openai·ai编程