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

188 阅读25分钟

本期导读

本文内容包括:

  • RAG的基本概念和工作原理
  • 向量数据库的选择和配置
  • Embedding模型的对比与应用
  • 五种文档分块技术的详细讲解
  • 三种检索策略的实战应用
  • 完整的RAG系统实现代码
  • 生产环境的优化技巧和常见问题

环境准备

Python环境安装

本文所有代码示例都基于LangChain 0.3.79版本。


一、RAG是什么?

1.1 用一个简单的比喻理解RAG

我们可以把RAG想象成考试的两种形式:

场景1:闭卷考试

老师问:"2024年中国GDP是多少?"
学生(只靠记忆):"呃...我只学到2021年的数据..."
问题:知识已经过时

场景2:开卷考试(这就是RAG的工作方式)

老师问:"2024年中国GDP是多少?"
学生的做法:
  1. 翻书找到"2024年经济数据"这一章
  2. 阅读相关内容
  3. 根据书上的准确数字作答
优点:知识可以实时更新,答案有据可查

简单来说,RAG就是给AI加上了"查资料"的能力,让它不再只依赖预先训练的知识。

1.2 RAG的三个核心组成部分

RAG系统由三个核心部分组成,可以理解为:检索 + 生成。

知识库          →      检索器        →      生成器
(存放文档)             (找相关内容)          (生成答案)

组件1:知识库(Knowledge Base)

知识库就像一个数字图书馆,存放着各种文档和数据:

  • 公司规章制度文档
  • 产品使用手册
  • 技术文档
  • 常见问题集
  • 其他业务资料

组件2:检索器(Retriever)

检索器的作用类似于图书馆的目录系统,主要负责:

  • 根据用户问题快速定位相关文档
  • 找出最相关的内容片段
  • 过滤掉不相关的信息

组件3:生成器(Generator)

生成器接收检索到的内容,然后:

  • 理解和分析这些内容
  • 结合用户的问题进行推理
  • 生成连贯、准确的回答

1.3 RAG完整工作流程

RAG系统处理一个用户问题需要经过五个步骤:

步骤1:用户提问

用户输入:"公司的年假政策是什么?"

步骤2:问题向量化

系统将问题转换成数字向量:[0.2, 0.8, 0.1, ...]
这就像给问题打上一个数字"指纹",方便后续的相似度计算

步骤3:检索相关文档

在知识库中搜索,找到最相关的3-5段内容
比如:
"员工享有带薪年假...入职满1年5天..."
"年假可累计使用...最多结转至下年度..."

步骤4:构建提示词

将检索到的内容和用户问题组合:

上下文:【检索到的相关内容】
问题:公司的年假政策是什么?
要求:基于上下文回答问题

步骤5:LLM生成答案

大语言模型根据上下文生成回答:

"根据公司规定,员工年假政策如下:
- 入职满1年:5天年假
- 入职满3年:10天年假
- 入职满5年:15天年假
年假可在当年使用,未使用部分可结转至下一年度。"

步骤6:返回结果

将答案展示给用户,同时附上引用来源,便于核实

1.4 RAG vs 传统AI的区别

维度传统AI(ChatGPT等)RAG增强的AI
知识来源只有训练时的数据训练数据 + 实时文档
更新频率需要重新训练随时更新文档即可
准确性可能产生幻觉基于真实文档,更可靠
可追溯性无法追溯来源可以显示引用来源
私有数据无法访问可以访问企业内部数据

1.5 RAG的典型应用场景

场景1:企业智能客服

用户提问:"如何申请发票?"
RAG系统的处理流程:
  1. 检索公司财务制度文档
  2. 找到"发票申请流程"相关章节
  3. 基于文档内容生成详细的操作步骤
优势:回答准确、实时更新、可追溯来源

场景2:技术文档问答

开发者提问:"如何使用Spring AI创建RAG系统?"
RAG系统的处理流程:
  1. 检索Spring AI官方文档和示例代码
  2. 定位相关的API说明和代码片段
  3. 生成完整的使用教程
优势:始终使用最新API、代码示例准确可靠

场景3:法律合规查询

律师提问:"2024年最新劳动法关于加班的规定是什么?"
RAG系统的处理流程:
  1. 检索最新的法律法规数据库
  2. 定位相关法律条款
  3. 引用原文作答
优势:法条引用准确、有明确法律依据

场景4:医疗辅助诊断

医生输入:"患者出现头痛、发热、颈部僵硬等症状"
RAG系统的处理流程:
  1. 检索医学文献和历史病例
  2. 匹配相似的症状组合
  3. 提供可能的诊断建议和进一步检查建议
优势:基于真实医学知识和临床经验

1.6 理解向量和Embedding(核心概念)

什么是向量?
文本 → 向量(一串数字)

例子:
"苹果" → [0.8, 0.1, 0.2, 0.9, ...]
"香蕉" → [0.7, 0.2, 0.1, 0.8, ...]
"汽车" → [0.1, 0.9, 0.8, 0.2, ...]

为什么需要向量?
- 计算机不懂文字,但懂数字
- 相似的词,向量也相似
- 可以快速计算相似度
向量相似度计算
"苹果""香蕉"的向量:
[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的工作原理

下面是导出向量数据的代码示例:

# 导出向量用于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年的信息时,模型就无法给出准确答案。

举例:

用户:"2024年巴黎奥运会金牌榜前三名是哪些国家?"
GPT-3.5"抱歉,我的知识截止到2021年9月,无法回答关于2024年的问题。"

RAG的解决方案:实时检索最新的体育新闻数据库,获取最新的金牌榜信息,然后给出准确的回答。

局限2:无法访问私有数据

通用大模型只能使用公开的训练数据,无法访问企业内部的私有数据。

举例:

员工:"公司2024年Q3的销售额是多少?"
通用LLM:"我无法访问您公司的内部数据,建议您咨询财务部门。"

RAG的解决方案:检索公司内部的财报系统,提取Q3销售数据,基于真实数据给出回答。

局限3:容易产生幻觉

大模型有时会"一本正经地胡说八道",生成看似合理但实际错误的内容。

举例:

用户:"请列出《三体》中的主要人物关系。"
LLM可能编造:"王明是叶文洁的学生,后来成为了..." 
(实际上《三体》中没有"王明"这个角色)

RAG的解决方案:先检索《三体》的原文和官方设定,基于真实内容生成答案,大大减少了产生幻觉的可能性。

2.2 有RAG vs 没有RAG的对比

为了更直观地理解RAG的价值,我们来看一个实际的对比案例。

场景:用户咨询公司的请假流程

没有RAG的传统大模型

用户:"公司请假需要提前几天申请?"

大模型的回答(基于通用知识):
"一般公司规定请假需要提前1-3天申请,具体以您所在公司的规章制度为准。
建议您查阅公司员工手册或咨询人力资源部门。"

问题:
- 回答很模糊,只能给出通用建议
- 无法提供公司具体的政策细节
- 用户还是不知道具体要提前几天
- 可能给出与公司实际规定不符的信息

使用RAG的增强系统

用户:"公司请假需要提前几天申请?"

RAG系统的处理过程:
1. 检索公司《员工手册》
2. 找到相关章节:"第三章 考勤管理 - 请假制度"
3. 提取关键信息:
   - 事假:提前3个工作日申请
   - 病假:当日电话通知,返岗后补假条
   - 年假:提前7个工作日申请
   
RAG系统的回答:
"根据公司《员工手册》第三章考勤管理规定:
- 事假需要提前3个工作日申请
- 病假需要当日电话通知直属主管,返岗后补交假条
- 年假需要提前7个工作日申请

具体流程是:在OA系统提交申请 → 主管审批 → 人力资源备案

参考来源:《员工手册》第3.2节"

优势:
- 回答准确具体,直接给出公司的实际规定
- 有明确的引用来源,便于核实
- 涵盖了不同类型假期的不同规定
- 用户可以直接按照指引操作

关键差异总结:

对比维度没有RAG有RAG
信息来源模型训练时的通用知识企业实时的私有文档
回答准确性模糊、通用精确、具体
可信度需要人工核实有引用来源支撑
实用性仅供参考可直接使用
更新性知识可能过时随文档更新

2.3 RAG vs 传统检索系统

很多人会问:我们以前也有文档检索系统,RAG和传统检索有什么区别?

传统检索系统的工作方式

1. 用户输入关键词:
   "请假 提前 天数"

2. 系统进行关键词匹配:
   在文档中查找包含"请假"、"提前"、"天数"的段落

3. 返回匹配的文档列表:
   - 《员工手册》第3章
   - 《考勤管理制度》第2节
   - 《假期申请流程说明》
   ...(可能有十几个结果)

4. 用户的困境:
   - 需要自己点开每个文档查看
   - 需要自己判断哪个是最相关的
   - 需要自己总结和理解内容
   - 可能花费10-20分钟才能找到答案

RAG系统的工作方式

1. 用户输入自然语言问题:
   "公司请假需要提前几天申请?"

2. 系统智能处理:
   - 理解用户真正想问的是什么
   - 在知识库中进行语义检索(不只是关键词匹配)
   - 找到最相关的3-5段内容

3. 系统直接给出答案:
   "根据公司规定,不同类型的假期申请时间不同:
   - 事假:提前3个工作日
   - 病假:当日通知
   - 年假:提前7个工作日
   来源:《员工手册》第3.2节"

4. 用户体验:
   - 5秒内得到精准答案
   - 不需要自己翻阅文档
   - 信息已经整理好,直接可用

核心区别对比

维度传统检索RAG检索增强生成
输入方式关键词自然语言问题
匹配方式精确关键词匹配语义相似度匹配
返回结果文档列表直接的答案
结果形式原始文档片段重新组织的连贯回答
理解能力无理解能力理解问题意图
用户负担需要自己阅读和理解直接获得答案
处理复杂问题困难可以综合多个来源
时间成本5-20分钟5-10秒

举例说明传统检索的局限性

场景:用户问"我入职2年了,能休几天年假?"

传统检索系统

1. 提取关键词:"入职"、"2年"、"年假"
2. 返回所有包含这些词的文档
3. 可能返回:
   - 《新员工入职指南》(不相关,只是有"入职"二字)
   - 《公司2年发展规划》(不相关,只是有"2年")
   - 《年假管理办法》(相关,但需要自己查找对应年限)

问题:
- 无法理解"2年"是指"工作年限"
- 无法自动计算对应的年假天数
- 返回太多噪音文档

RAG系统

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的完整工作流程

┌─────────────────────────────────────────────────────────────┐
│                   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)
# 最简单的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 选型决策树

开始选型
    ↓
数据量是否超过1000万?
    ├─ 是 → Milvus(分布式架构)
    └─ 否 ↓
需要混合检索(关键词+向量)?
    ├─ 是 → Elasticsearch 或 Weaviate
    └─ 否 ↓
预算充足,追求简单?
    ├─ 是 → Pinecone(托管服务)
    └─ 否 ↓
已有PostgreSQL数据库?
    ├─ 是 → PGVector(直接扩展)
    └─ 否 ↓
Python生态,快速原型?
    ├─ 是 → Chroma(嵌入式)
    └─ 否 → Qdrant(性能+易用平衡)

3.3 实战:快速搭建向量数据库

方案1:Chroma(最快5分钟上手)
# 安装
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(生产级部署)
# 安装
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(企业级方案)
# 安装
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-largeOpenAI3072多语言$0.13/1M tokens高质量通用
text-embedding-3-smallOpenAI1536多语言$0.02/1M tokens成本敏感
BGE-large-zh智源1024中文优化免费(自部署)中文场景
BGE-M3智源1024多语言免费(自部署)多语言混合
M3EMoka768中文免费中文小样本
text2vec-多种中文免费中文轻量级
Jina EmbeddingsJina AI768多语言$0.02/1M tokens长文本

4.2 Embedding质量测试

中文Embedding基准测试(MTEB Chinese)

模型平均得分分类聚类检索
BGE-large-zh-v1.564.5372.162.469.1
M3E-large63.1270.860.967.5
text2vec-large-chinese60.4568.358.264.8

4.3 实战:使用不同Embedding模型

方案1:OpenAI Embeddings
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模型
# 安装
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
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)
# BGE模型建议
instruction = "为这个句子生成表示以用于检索相关文章:"
query = instruction + "RAG是什么?"

# 文档侧不需要instruction
doc = "RAG是检索增强生成技术"

query_embedding = model.encode(query)
doc_embedding = model.encode(doc)
技巧2:分段处理长文本
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:领域自适应微调
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个字符:

"春天来了,花开了..." [第1块:0-500字符]
"树叶绿了,小鸟..." [第2块:500-1000字符]
优缺点分析

优点:实现简单,处理速度快 缺点:可能在句子中间切断,破坏语义的完整性

LangChain实战代码
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)
]

输出示例

1 (97字符):
  RAG(检索增强生成)是一种将检索系统与大型语言模型相结合的技术...

块2 (93字符):
  它通过在生成回答之前先检索相关文档,来提高回答的准确性...

5.4 技术2:递归字符分块(最常用⭐推荐)

原理
智能分块:先尝试大分隔符,不行再用小的

分隔符优先级:
1. \n\n(段落)
2. \n(换行)
3. 。!?(句子)
4. 空格
5. 字符

就像切菜,先切大块,再切小块
为什么推荐?
  • ✅ 保持语义完整性
  • ✅ 适应各种文档格式
  • ✅ LangChain内置,开箱即用
LangChain实战代码
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 '⚠️可能被切断'}")
针对不同文档类型的配置
# 配置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:文档结构分块(结构化文档)

原理
根据文档的自然结构分块:
- Markdown: 按标题层级
- HTML: 按标签
- Code: 按函数/类
- PDF: 按章节

保留文档结构信息
LangChain实战代码
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文档分块
# 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)
组合使用:结构分块 + 大小控制
# 第一步:按结构分块
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:语义分块(高质量)

原理
基于语义相似度分块:
- 计算每个句子的向量
- 相似的句子归为一块
- 不相似的句子开始新块

优点:语义连贯性最强
缺点:计算成本较高
完整实战代码
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}")

输出示例

块1:  # 语义相关的句子聚在一起
  RAG技术是当前AI领域的热点。它结合了检索和生成两大技术。

块2:  # 话题转换,新的块
  传统的大模型只依赖预训练知识。这导致知识更新困难。
  RAG通过外部检索解决了这个问题。系统可以实时获取最新信息。

块3:  # 应用场景相关
  在企业应用中,RAG非常实用。它能够访问私有数据库。
  客服系统是典型的应用场景。用户问题可以得到准确回答。

5.7 技术5:Agent分块(最智能)

原理
使用LLM作为Agent来智能分块:
1. Agent阅读文档
2. 理解文档结构和主题
3. 智能决定分块位置
4. 生成带摘要的块

优点:最智能,适应性强
缺点:成本高,速度慢
完整实战代码
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]}...")

输出示例

============================================================
块1: RAG技术原理
============================================================
📝 摘要: RAG结合检索和生成,突破传统模型知识限制
🔑 关键词: RAG, 检索, 大语言模型, 知识库, 实时
📄 内容预览: RAG技术的核心原理是将检索系统与大语言模型相结合...

============================================================
块2: RAG系统架构
============================================================
📝 摘要: RAG系统三大组件:文档存储、检索器、生成器
🔑 关键词: 文档存储, 检索器, 生成器, 向量数据库
📄 内容预览: RAG系统包含三个主要组件:文档存储、检索器和生成器...

5.8 分块技术对比总结

技术实现难度计算成本分块质量推荐场景
固定大小💰⭐⭐快速原型
递归字符⭐⭐💰⭐⭐⭐⭐通用推荐
文档结构⭐⭐⭐💰💰⭐⭐⭐⭐Markdown/HTML
语义分块⭐⭐⭐⭐💰💰💰⭐⭐⭐⭐⭐高质量要求
Agent分块⭐⭐⭐⭐⭐💰💰💰💰⭐⭐⭐⭐⭐复杂文档

5.9 分块最佳实践

1. 选择合适的chunk_size
# 不同场景的推荐配置
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. 添加上下文信息
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. 实时监控分块质量
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
# 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)
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:滑动窗口分块(保留上下文)
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 分块最佳实践

推荐分块大小
# 根据不同场景选择
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
    }
}
元数据增强
# 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(密集检索)
# 基于向量相似度
query_embedding = model.encode("什么是RAG?")
results = vector_db.similarity_search(query_embedding, k=5)

✅ 优点:语义理解强,能捕捉隐含意图
❌ 缺点:对关键词精确匹配不敏感
🟢 Sparse Retrieval(稀疏检索)
# 基于关键词匹配(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(混合检索)
# 结合两者优势
dense_results = vector_search(query)      # 语义检索
sparse_results = bm25_search(query)       # 关键词检索
final_results = reciprocal_rank_fusion(dense_results, sparse_results)

✅ 优点:结合两者优势,召回率和准确率都高
❌ 缺点:实现复杂,计算量大

6.2 实战:实现混合检索

完整混合检索系统
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)
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)
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(最大边际相关性)
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)

"""
完整的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)

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)

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进行自动化评估

"""
使用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 优化技巧清单

✅ 数据质量优化
# 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)
    }
✅ 检索优化
# 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]
✅ 生成优化
# 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
✅ 性能优化
# 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:企业文档问答系统

"""
场景:为公司内部文档构建智能问答系统(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:技术文档助手

"""
场景:开发者技术文档智能助手(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模型不适配
  • 分块粒度不合适
  • 查询表达不清晰

解决方案

# 1. 使用领域适配的Embedding模型
model = SentenceTransformer('你的领域模型')

# 2. 调整分块大小
splitter = RecursiveCharacterTextSplitter(
    chunk_size=300,  # 减小块大小
    chunk_overlap=50
)

# 3. 查询改写
def improve_query(query):
    return f"关于{query}的详细信息"

Q2: 答案包含幻觉

症状:生成的答案不基于检索内容

解决方案

# 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秒

优化方案

# 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: 中文分词问题

症状:中文关键词检索效果差

解决方案

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调用成本高

优化方案

# 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)