Agent开发(二) — 谈谈RAG
背景
在我们平常使用 LLMs 时有许多挑战,例如领域知识差距、事实性问题以及幻觉。通常会通过使用外部知识(如数据库)来增强 LLMs,比如说DeepSeek上的网络搜索功能,但是这些功能仅限于外部数据,如果我们想要检索本地庞大、错综复杂的数据的时候又该怎么办呢?
为了解决这一矛盾,研究者提出了检索增强生成(Retrieval-Augmented Generation, RAG)这一范式。RAG 的核心思想是:将语言模型的生成能力与外部知识检索机制相结合,在生成回答前,先从指定的知识源(如本地文档集合、向量数据库或结构化数据系统)中动态检索与用户查询最相关的信息片段,并将这些“证据”作为上下文输入给 LLM。这样,模型的回答不仅基于其内部参数,更建立在真实、可追溯、可更新的外部知识之上。
RAG 范式
截止在2025年,我们已经从RAG1.0 — 单纯的切块,向量,双路召回转向了RAG2.0 时代
- GraphRAG:将知识图谱嵌入检索流程,利用实体关系提升上下文理解能力,特别适用于复杂推理任务。
- Agentic RAG:引入智能体(Agent)机制,使RAG系统具备自主规划、多轮检索与反思能力,实现“主动检索”(Active Retrieval)。
- Multi-modal RAG:支持文本、图像、视频、结构化数据等多模态内容的联合检索与生成,为跨模态问答和内容创作提供支持
RAG系统通常分为三个模块,而现在2025年先进的RAG系统在这三个模块分别有了足够的创新和增长:
- 索引(Indexing):采用分层索引、语义分块(semantic chunking)、元数据增强和向量-关键词混合索引,提升召回率。
- 检索(Retrieval):引入重排序(re-ranking)、查询扩展(query expansion)、自适应检索(adaptive retrieval)以及基于用户意图的动态检索策略。
- 生成(Generation):结合指令微调、上下文压缩、证据融合(evidence fusion)和冲突检测机制,确保生成内容忠实于检索结果。
分块/索引
在进行索引之前,需要对庞大的数据量进行合理的分块。如何选择合适的分块策略,不仅取决于原始内容的结构特性(如是否为技术文档、法律条文、代码或会议记录),也与下游生成任务对上下文完整性的要求密切相关。不当的分块可能导致关键信息被割裂、检索召回率下降,甚至引发生成幻觉。
此外,不同嵌入模型对输入长度的支持存在显著差异,这也直接影响分块上限的设计。例如,BAAI/bge-m3 支持最长 8,192 个 token 的上下文 ,而 Qwen/Qwen3-Embedding-0.6B 则可处理高达 32,768 个 token 的长文本 。
以下是六种被广泛采用且经过实践验证的分块方式:
1. 递归字符切分(Recursive Character Splitting)
该方法按优先级顺序(如:段落 → 句号 → 换行符 → 空格)尝试使用分隔符切分文本。若某一级分隔符切分后仍超出最大长度,则回退至更细粒度的分隔符继续尝试,直至满足长度与重叠约束
-
原文片段
“
RAG 把检索与生成捆在一起,靠外部知识回答用户。流程分三阶段:索引、检索、生成。若分块不合理,上下文就会断气。” -
示例输出(max=40 字,overlap=10 字)
- Chunk 1 RAG 把检索与生成捆在一起,靠外部知识回答用户。
- Chunk 2 靠外部知识回答用户。流程分三阶段:索引、检索、生成。
- Chunk 3 段:索引、检索、生成。若分块不合理,上下文就会断气。
→ 句尾对齐,但“段”字落单,显示“短语级回退”痕迹。
2. 固定长度切分(Fixed-Size Chunking)
以字符数或 token 数为单位,采用滑动窗口对文本进行等长切分,并可设置固定重叠区域以缓解边界信息丢失 。
-
原文片段
“RAG 把检索与生成捆在一起,靠外部知识回答用户。流程分三阶段:索引、检索、生成。若分块不合理,上下文就会断气。” -
同一原文,max=20 字,overlap=5 字
- Chunk 1 RAG 把检索与生成捆在一
- Chunk 2 与生成捆在一起,靠外部
- Chunk 3 靠外部知识回答用户。流程
- Chunk 4 流程分三阶段:索引、检
- Chunk 5 、检索、生成。若分块不合
→ 每块 20 字整,句读被横切,重叠区用下划线高亮“粘胶”。
3. 句子/语义驱动切分(Semantic Chunking)
先利用句子分割模型(如 spaCy、NLTK)或语义聚类算法(如基于嵌入的相似度聚类)识别自然语言单元,再将这些单元按顺序组合,直到接近长度上限;若加入下一个单元会超限,则回退至上一个完整单元
-
原文片段
“RAG 把检索与生成捆在一起,靠外部知识回答用户。流程分三阶段:索引、检索、生成。若分块不合理,上下文就会断气。” -
同一原文,max≈45 字
- Chunk 1 RAG 把检索与生成捆在一起,靠外部知识回答用户。
- Chunk 2 流程分三阶段:索引、检索、生成。
- Chunk 3 若分块不合理,上下文就会断气。
→ 每块都是完整句子,长度不均但语义闭合。
4. 结构化切分(Structure-Aware Chunking)
该策略适用于具有显式逻辑结构的文档(如技术手册、法律条文、API 文档、Markdown 报告等)。系统首先解析文档的结构标签(如标题、列表、代码块、表格),再以结构单元为边界进行切分,并将结构路径作为元数据保留,用于后续检索排序或生成格式还原。
-
示例源文档(Markdown 格式的用户手册节选)
# 用户权限管理指南 ## 2. 角色定义 ### 2.1 管理员(Admin) 拥有系统全部操作权限,包括用户增删、配置修改和日志审计。 ```python role = { "name": "admin", "permissions": ["read", "write", "delete", "audit"] }2.2 编辑者(Editor)
可创建和修改内容,但无法管理用户或查看审计日志。 role: editor permissions: - read - write
-
示例输出(每块对应一个结构单元,保留路径与类型)
-
Chunk A
{ "section_path": "用户权限管理指南 / 2. 角色定义 / 2.1 管理员(Admin)", "content": "拥有系统全部操作权限,包括用户增删、配置修改和日志审计。\\n```python\\nrole = {\\n \\"name\\": \\"admin\\",\\n \\"permissions\\": [\\"read\\", \\"write\\", \\"delete\\", \\"audit\\"]\\n}\\n```", "metadata": { "doc_type": "user_manual", "has_code": true, "code_language": "python" } } -
Chunk B
{ "section_path": "用户权限管理指南 / 2. 角色定义 / 2.2 编辑者(Editor)", "content": "可创建和修改内容,但无法管理用户或查看审计日志。\\n```yaml\\nrole: editor\\npermissions:\\n - read\\n - write\\n```", "metadata": { "doc_type": "user_manual", "has_code": true, "code_language": "yaml" } }
-
→ 当用户提问“管理员有哪些权限?”时,检索系统不仅匹配到关键词“权限”和“admin”,还能通过 section_path 精确定位到“2.1 管理员”章节,并在生成答案时保留原始代码块格式,提升专业性与可信度。
5. 语言特化切分(Language-Specific Chunking)
针对中文、阿拉伯语、德语等具有特殊书写规则的语言,采用符合其词法或标点习惯的切分逻辑(如中文按句号/分号,而非空格),避免在词语中间截断 。
-
原文(混排示例,max≈30 字,overlap=5 字)
“中文切忌把‘知识图谱’劈成‘知识’/‘图谱’;德语‘Arbeitsunterbrechung’不可从‘Arbeit’腰斩;阿拉伯语‘الذكاءالاصطناعي’也得整体离场。”
-
示例输出
- Chunk 1 中文切忌把‘知识图谱’劈成‘知识’/‘图谱’;
- Chunk 2 ‘图谱’;德语‘Arbeitsunterbrechung’不可从
- Chunk 3 从‘Arbeit’腰斩;阿拉伯语‘الذكاءالاصطناعي’也得整体离场。
6. 分层(Parent-Child)切分(Hierarchical Chunking)
该策略通过“粗粒度父块 + 细粒度子块”的双层结构,兼顾检索精度与上下文完整性。子块用于向量检索(短、精准),父块用于生成上下文(长、完整)。两者通过 ID 关联,实现“命中即召回全景”。
-
示例源文档(一段关于 RAG 优化的技术说明)
3.3 重排序(Re-ranking) 在初步检索得到 top-k 候选文档后,使用交叉编码器(cross-encoder)对结果进行重排序,可显著提升最终排序质量。例如,BGE-Reranker-v2 能在保持低延迟的同时,将 MRR@10 提升 15% 以上。实际部署中,建议将重排序模块置于检索与生成之间,作为质量过滤关卡。 -
示例输出
-
子块 Child-3.3.1(用于向量索引)
{ "id": "Child-3.3.1", "parent_id": "Parent-3.3", "content": "在初步检索得到 top-k 候选文档后,使用交叉编码器(cross-encoder)对结果进行重排序,可显著提升最终排序质量。" } -
子块 Child-3.3.2(用于向量索引)
{ "id": "Child-3.3.2", "parent_id": "Parent-3.3", "content": "例如,BGE-Reranker-v2 能在保持低延迟的同时,将 MRR@10 提升 15% 以上。" } -
子块 Child-3.3.3(用于向量索引)
{ "id": "Child-3.3.3", "parent_id": "Parent-3.3", "content": "实际部署中,建议将重排序模块置于检索与生成之间,作为质量过滤关卡。" } -
父块 Parent-3.3(不参与向量索引,仅存储完整上下文)
{ "id": "Parent-3.3", "title": "3.3 重排序(Re-ranking)", "full_content": "在初步检索得到 top-k 候选文档后,使用交叉编码器(cross-encoder)对结果进行重排序,可显著提升最终排序质量。例如,BGE-Reranker-v2 能在保持低延迟的同时,将 MRR@10 提升 15% 以上。实际部署中,建议将重排序模块置于检索与生成之间,作为质量过滤关卡。" }
-
→ 当用户查询“BGE-Reranker-v2 效果如何?”时,系统通过向量检索命中 Child-3.3.2,随即加载其父块 Parent-3.3 作为 LLM 的完整上下文。这样,生成的答案不仅能准确引用性能数据,还能解释其在 RAG 流程中的位置与作用,避免“断章取义”。
实战举例
为直观理解不同分块策略的效果,我们以 LangChain 项目自身的 README.md 作为示例文档,使用 LangChain 提供的文本切分工具进行对比实验。
环境配置
首先是拉取项目
git clone https://www.github.com/langchain-ai/langchain
然后是安装依赖
pip install -e .
这里以项目的中的 README.md 作为文档的来源
递归字符切分
from langchain_text_splitters import RecursiveCharacterTextSplitter
def load_readme():
with open("/private/tmp/langchain/README.md", "r", encoding="utf-8") as f:
return f.read()
def main():
text = load_readme()
# 使用 LangChain 的递归字符切分器
splitter = RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=50
)
chunks = splitter.split_text(text)
print(f"递归字符切分: {len(chunks)} 块")
for i, chunk in enumerate(chunks[:3]):
print(f"\n块 {i+1} (长度: {len(chunk)} 字符):")
print("-" * 40)
preview = chunk[:100] + "..." if len(chunk) > 100 else chunk
print(preview)
if __name__ == "__main__":
main()
运行起来输出的结果应该如下
递归字符切分: 17 块
块 1 (长度: 367 字符):
----------------------------------------
<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset=".github/ima...
块 2 (长度: 356 字符):
----------------------------------------
<p align="center">
<a href="https://opensource.org/licenses/MIT" target="_blank">
<img src="...
块 3 (长度: 343 字符):
----------------------------------------
</a>
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInV...
此策略在无明确语义边界时自动降级到字符级切分,适合通用文本,但可能割裂 HTML 标签或代码片段。
语义切块
from langchain_text_splitters import RecursiveCharacterTextSplitter
def load_readme():
with open("/private/tmp/langchain/README.md", "r", encoding="utf-8") as f:
return f.read()
def main():
text = load_readme()
# 使用 LangChain 的递归字符切分器,基于语义分隔符
splitter = RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", "! ", "? ", " ", ""]
)
chunks = splitter.split_text(text)
print(f"语义驱动切分: {len(chunks)} 块")
for i, chunk in enumerate(chunks[:3]):
print(f"\n块 {i+1} (长度: {len(chunk)} 字符):")
print("-" * 40)
preview = chunk[:100] + "..." if len(chunk) > 100 else chunk
print(preview)
if __name__ == "__main__":
main(
运行起来结果应该如下
语义驱动切分: 17 块
块 1 (长度: 367 字符):
----------------------------------------
<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset=".github/ima...
块 2 (长度: 356 字符):
----------------------------------------
<p align="center">
<a href="https://opensource.org/licenses/MIT" target="_blank">
<img src="...
块 3 (长度: 343 字符):
----------------------------------------
</a>
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInV...
尽管输出与递归切分相似(因 README.md 前半部分以 HTML 为主,缺乏自然语言句子边界),但在纯文本段落中,该策略能有效避免在句中截断。
分层切分
from langchain_text_splitters import MarkdownHeaderTextSplitter
def load_readme():
with open("/private/tmp/langchain/README.md", "r", encoding="utf-8") as f:
return f.read()
def main():
text = load_readme()
# 使用 LangChain 的 Markdown 头部切分器实现分层切分
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False
)
chunks = splitter.split_text(text)
print(f"分层切分: {len(chunks)} 块")
for i, chunk in enumerate(chunks[:3]):
print(f"\n块 {i+1} (长度: {len(chunk.page_content)} 字符):")
print("-" * 40)
preview = chunk.page_content[:100] + "..." if len(chunk.page_content) > 100 else chunk.page_content
print(preview)
if chunk.metadata:
print(f"元数据: {chunk.metadata}")
if __name__ == "__main__":
main()
运行后输出结果大概如下
分层切分: 4 块
块 1 (长度: 2388 字符):
----------------------------------------
<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset=".github/images/lo...
块 2 (长度: 676 字符):
----------------------------------------
## Why use LangChain?
LangChain helps developers build applications powered by LLMs through a stan...
元数据: {'Header 2': 'Why use LangChain?'}
块 3 (长度: 1256 字符):
----------------------------------------
## LangChain’s ecosystem
While the LangChain framework can be used standalone, it also integrates ...
元数据: {'Header 2': 'LangChain’s ecosystem'}
该策略将文档按标题结构切分为逻辑完整的章节,每块附带路径元数据,非常适合技术文档、API 手册等结构化内容。
检索(Retrieval)
上面聊完分块后,就应该到检索了,检索的目标是从海量知识库中精准召回与用户查询最相关的上下文片段。有常见的四种方法
1. 重排序(Re-ranking)
初始检索(如使用 BGE-M3 或 Qwen3-Embedding)通常采用双编码器(bi-encoder)架构,虽高效但精度有限。重排序引入交叉编码器(cross-encoder)对 top-k 候选文档进行精细化打分,显著提升排序质量。
典型流程:Embedding 检索(召回 50–100 文档) → Reranker 重排(精选 top-5) → 送入 LLM 生成
2. 后退提示(StepBack-prompt)
StepBack 提示:一种提示技术,使 LLMs 能够进行抽象,产生指导推理的概念和原则;当采用 RAG 框架时,这会导致更可靠的响应,因为 LLM 远离具体实例,必要时可以更广泛地进行推理。
3.递归检索和查询引擎(Recursive Retrieval and Query Engine)
递归检索与查询引擎:涉及一个递归的检索过程,可能从较小的语义块开始,随后检索更大的块以丰富上下文;这对于在效率和上下文丰富信息之间取得平衡非常有用。
4. 假设的文档嵌入(Hypothetical Document Embeddings)
假设文档嵌入:HyDE 会生成一个假设的答案,将其进行嵌入,并利用该嵌入来检索与假设答案相似的文档,而不是直接使用原始查询。
实战举例
为直观理解不同策略的效果,拉取另外一个Demo项目下来,该Demo项目也是基于 langchain 开发
git clone https://github.com/charSLee013/langchain-demo
然后安装依赖之类的不再啰嗦
本次需要真实的Reank以及Embeding模型还有LLM调用,这里采用 SiliconFlow
将下面内容复制到 .env 后并替换成您的 api-key (可以从 cloud.siliconflow.cn/me/account/… 复制)
# SiliconFlow API Configuration
SILICONFLOW_API_KEY=<这里填写你的api-key>
SILICONFLOW_BASE_URL=https://api.siliconflow.cn
# Embedding Model
EMBEDDING_MODEL=BAAI/bge-m3
# Reranking Model
RERANKING_MODEL=Qwen/Qwen3-Reranker-0.6B
# LLM Model
LLM_MODEL=Qwen/Qwen3-VL-235B-A22B-Instruct
该项目准备了三个不同的检索方案:
hyde_demo.py– 简单相似性查询query_expansion_demo.py– LLM 扩展查询的相似性搜索reranking_demo.py– 相似性搜索+外部重排序
如果你配置好了 .env 后可以直接运行
(.venv) (base) charslee@iPad-Pro langchain % .venv/bin/python hyde_demo.py "What is LangChain Text Splitters?"
query: What is LangChain Text Splitters?
hyde: LangChain, text splitter, document chunking, tokenization, NLP preprocessing
chunks: 268, added: 268, skipped: 0
initial (top5):
1. /private/tmp/langchain/libs/text-splitters/README.md | ```bash pip install langchain-text-splitters ```
2. /private/tmp/langchain/libs/text-splitters/README.md | LangChain Text Splitters contains utilities for splitting into chunks a wide variety of text documents. For full docum
3. /private/tmp/langchain/libs/text-splitters/README.md | [](https://opensource.org/lic
4. /private/tmp/langchain/README.md | HyDE (hypothetical terms → retrieve): ```bash .venv/bin/python hyde_demo.py "What is LangChain Text Splitters?" ```
5. /private/tmp/langchain/libs/README.md | > [!IMPORTANT] > [**View all LangChain integrations packages**](https://docs.langchain.com/oss/python/integrations/provi
hyde (top5):
1. /private/tmp/langchain/libs/text-splitters/README.md | LangChain Text Splitters contains utilities for splitting into chunks a wide variety of text documents. For full docum
2. /private/tmp/langchain/libs/text-splitters/README.md | [](https://opensource.org/lic
3. /private/tmp/langchain/libs/text-splitters/README.md | ```bash pip install langchain-text-splitters ```
4. /private/tmp/langchain/README.md | HyDE (hypothetical terms → retrieve): ```bash .venv/bin/python hyde_demo.py "What is LangChain Text Splitters?" ```
5. /private/tmp/langchain/libs/langchain_v1/README.md | ⚡ Building applications with LLMs through composability ⚡ [已从早期的“切块 + 向量检索 + 生成”简单流水线,演进为高度模块化、可定制的智能知识交互系统。要构建一个高效、准确、可靠的 RAG 应用,需在 分块(索引) 与 检索 两大环节协同优化。
1. 分块策略决定知识表达的粒度与完整性
- 没有“万能”的分块方法,需根据文档结构(如 Markdown、代码、法律文本)和任务需求(问答、摘要、推理)选择合适策略。
- 结构化切分(如基于 Markdown 标题)适合技术文档,保留逻辑层次与元数据;
- 分层切分(Parent-Child)兼顾检索精度与上下文完整性,是复杂问答的理想选择;
- 语义/语言特化切分可避免关键短语断裂,提升中文等语言的召回质量。
2. 检索策略决定知识调用的准确性与深度
- 基础向量检索(如 BGE-M3)虽快,但易受查询表述偏差影响;
- HyDE 通过生成“假设答案”引导检索,更贴近理想语义;
- StepBack Prompting 提升 LLM 的抽象推理能力,避免陷入细节幻觉;
- 递归检索 实现“由点及面”的上下文扩展;
- 重排序(Reranking)作为质量关卡,显著提升 top-k 结果的相关性。
3. 实践验证:工具链与真实模型缺一不可
- 利用 LangChain 等框架可快速实验不同切分与检索策略;
- 本地知识库的 RAG 系统,其价值不仅在于“能回答”,更在于“答得准、有依据、可追溯”。
最终目标:让 LLM 不再“凭空编造”,而是成为一位基于你私有知识库的专家助手——既懂上下文,又守事实边界。
通过合理设计分块与检索流程,我们不仅能缓解幻觉问题,更能释放 LLM 在企业、科研、工程等专业场景中的真正潜力。