上一篇学习了 RAG 的基本原理,以及 HelloAgents 中 RAG 系统的整体架构。本篇继续结合 HelloAgents 的源码,看看一份文档是如何一步步变成可以被大模型检索的知识。
整个过程可以概括为一句话:
任意格式文档 → Markdown → 智能分块 → Embedding → 向量数据库。
虽然流程并不复杂,但每一步都承担着不同的职责。
一、HelloAgents 的 RAG Pipeline
HelloAgents 并没有把 RAG 写成一个庞大的类,而是将整个流程拆分成多个独立模块,再统一封装到RAGTool中。
文档中给出的整体流程如下:
任意格式文档 │ ▼ MarkItDown转换 │ ▼ Markdown文本 │ ▼ 智能分块(Chunk) │ ▼ Embedding │ ▼ Qdrant向量数据库这样的设计有两个优点:
- 每个模块职责明确,方便维护和扩展。
- 如果以后更换 Embedding 模型或向量数据库,上层业务几乎不用修改。
整个 RAG 系统对外统一由RAGTool提供接口,开发者只需要调用一个工具即可完成知识导入、检索和管理。
例如:
rag_tool = RAGTool( knowledge_base_path="./knowledge_base", collection_name="rag_collection" )可以看到,真正复杂的流程都被封装到了工具内部。
二、第一步:MarkItDown 统一文档格式
现实项目中的知识来源往往非常复杂。
可能有:
- Word
- Excel
- PowerPoint
- TXT
- 图片
- HTML
- Markdown
如果每种格式都单独处理,代码会非常复杂。
HelloAgents 的解决方案就是引入MarkItDown。
它负责把不同类型的文档统一转换成 Markdown。
源码中的核心逻辑如下:
def _convert_to_markdown(path: str) -> str: md_instance = _get_markitdown_instance() result = md_instance.convert(path) return result.text_content无论输入是什么格式,最终都会得到统一的 Markdown 文本。
这样做最大的好处就是:
后面的文本切分、Embedding 和检索流程都只需要处理 Markdown,不需要关心原始文件类型。
三、为什么要转换成 Markdown?
很多人第一次看到这里都会有一个疑问:
为什么不用原始文本,而要先转换成 Markdown?
主要原因有两个。
第一,Markdown 本身具有良好的结构。
例如:
# RAG ## 什么是RAG RAG 是一种…… ## 工作流程 ……标题层级、段落、列表等信息都会被保留下来。
第二,Markdown 更方便后续进行语义分块。
相比一整篇连续文本,根据标题切分能够更好地保持上下文关系,提高后续检索质量。
因此,HelloAgents 才会先完成格式统一,再进入下一步处理。
四、第二步:智能分块(Chunk)
完成格式转换之后,并不会直接进行向量化。
原因很简单:
一篇文档往往太长,Embedding 模型无法一次处理。
因此,需要把文档切成多个 Chunk。
HelloAgents 首先根据 Markdown 的标题进行分割。
源码中可以看到:
def _split_paragraphs_with_headings(text): if raw.strip().startswith("#"): # 根据标题划分层级例如:
# Java Java简介 ## JVM JVM介绍 ## JVM内存 ……最终会形成多个语义完整的小段落,而不是随意按照字符数量截断。
这样既保留了标题信息,也避免一个知识点被拆得支离破碎。
五、第三步:Token 分块
完成标题分割之后,HelloAgents 还会继续按照 Token 数量进行控制。
对应源码:
def _chunk_paragraphs( paragraphs, chunk_tokens, overlap_tokens ):为什么还需要第二次切分?
因为有些章节仍然很长。
例如一本技术文档,一个章节可能有几千字。
Embedding 模型一般都会限制输入长度,因此还需要进一步控制每个 Chunk 的大小。
HelloAgents 根据 Token 数量生成最终的 Chunk。
整个流程如下:
Markdown │ 标题切分 │ Token统计 │ Chunk这样生成的 Chunk 更适合后续进行向量化。
六、Overlap 为什么存在?
源码中还有一个参数:
overlap_tokens它表示:
Chunk 之间保留一定重叠内容。
例如:
Chunk1: A B C D E Chunk2: D E F G H其中 D、E 就属于重叠部分。
为什么需要这样做?
因为很多知识并不是一句话能够表达完整。
如果完全切断:
Chunk1 Java内存包括…… Chunk2 ……年轻代、老年代……那么第二块可能失去了前面的上下文。
因此,适当保留部分内容,可以保证语义连续性,提高检索质量。
七、第四步:Embedding 向量化
完成 Chunk 之后,就进入了 RAG 最重要的一步——Embedding。
Embedding 的作用就是:
将文本转换成向量。
例如:
"Python是一门语言" ↓ [0.12,0.53,0.81,...]以后用户的问题也会转换成向量。
然后通过计算两个向量之间的距离,找到最相似的知识。
HelloAgents 中统一使用了 Embedding 接口。
源码中可以看到:
embedder = get_text_embedder() part_vecs = embedder.encode(part)这样做最大的优点就是:
Embedding 模型可以自由替换。
文档中提到,HelloAgents 默认支持:
- 百炼 API
- sentence-transformers
- TF-IDF(兜底方案)
如果想更换其他 Embedding 模型,只需要修改这一层即可。
八、第五步:存入 Qdrant
完成向量化之后,就需要存入向量数据库。
HelloAgents 默认使用的是Qdrant。
整个流程可以理解为:
文本 ↓ Embedding ↓ 向量 ↓ Qdrant以后用户提出问题时:
同样会进行 Embedding。
然后直接到 Qdrant 中寻找最相近的几个向量。
这样就完成了一次语义检索。
相比传统关键词搜索,向量数据库更加关注的是:
语义是否相似,而不是文字是否完全一致。
因此,即使用户换一种表达方式,也有机会检索到正确内容。
九、总结
这一篇主要学习了 HelloAgents 中 RAG 的实现流程。
整个 Pipeline 可以概括为五个步骤:
| 步骤 | 作用 |
|---|---|
| MarkItDown | 将 PDF、Word 等各种文档统一转换为 Markdown |
| Markdown 分块 | 根据标题结构划分语义段落,保持文档层次 |
| Token 分块 | 控制 Chunk 大小,并通过 Overlap 保持上下文连续性 |
| Embedding | 将文本转换为向量,便于进行语义检索 |
| Qdrant | 存储向量并完成相似度搜索 |
可以看到,真正决定 RAG 检索效果的,并不仅仅是大语言模型,而是前面的整个知识处理流程。从文档格式统一、智能分块,到向量化和存储,每一步都会影响最终的检索质量。
下一篇将继续学习 HelloAgents 中的高级检索策略,包括MQE(多查询扩展)、HyDE(假设文档嵌入)等内容,看看 RAG 是如何进一步提升知识召回率和回答准确性的。