【手搓 Agent 第2.1关】搭建 Agent 进阶能力:RAG(下)
上篇手写极简 RAG 让我们吃透了核心流程,但极简关键词检索存在语义缺失、匹配精度低、无法处理海量文档、不可持久化等问题,完全无法用于真实业务场景。想要落地企业级私有知识库问答,必须依托标准化框架、向量模型与向量数据库,搭建完整工业级 RAG 架构。
承接中篇沙盒实操,本篇进入正式工程落地阶段。我们将从零配置全套工业级依赖,对比业界主流技术选型,实现文档智能分块、本地私有化 Embedding、向量库持久化存储,最终改造适配 Stage1 基础 Agent,让智能体具备专业、精准、可溯源的私有知识库问答能力,完成整套 RAG 模块闭环落地。
一、RAG 工业级调包流程
理解底层逻辑后,在实际业务中使用框架和向量库来处理百万字级别的文档。
我们先使用本地文档练手,以确定 RAG 流程没有问题。当该流程可以运转后,再把“读取本地文件”的动作替换为“接收网页抓取工具传来的文本”。这样我们可以更好判断大模型返回文字不当时,是由于 RAG 流程问题,还是网页环境抓取问题(如反爬虫 403 报错、满是乱码的广告弹窗、以及排版极其不规则的 HTML等)。
在一开始,需要先安装以下几个核心库:
# 安装 LangChain 核心与扩展包 (骨架与大脑接口)
pip install langchain langchain-openai langchain-community langchain-text-splitters pypdf langchain-huggingface langchain-chroma
# 安装本地极其轻量级的向量数据库 Chroma (海马体 / 向量数据库)
pip install chromadb
# 安装本地中文 Embedding 所需的库
pip install sentence-transformers
补充:这 3 个库适合我们目前搭建一个完整的工业级 RAG 最小可行性产品(MVP),实际工业界落地时会有其他替代方案。
| RAG 核心模块 | 本次演练选型 | 业界主流替代方案(及适用场景) |
|---|---|---|
| 编排框架 (Framework) | LangChain | - LlamaIndex: 目前做纯 RAG 体验最好的框架,专门为数据摄取和检索优化。 - Semantic Kernel: 微软主推,适合 C#/.NET 企业的技术栈。 - 纯原生手写 (Custom): 很多大厂为了极致的性能和可控性,后期都会剥离 LangChain,自己造轮子。 |
| 向量数据库 (Vector DB) | ChromaDB | - FAISS: Facebook 开源,老牌且轻量、快速,适合本地开发 。 - Milvus / Qdrant: 支持亿级数据的分布式数据库,企业级大规模部署的首选 。 - Pinecone / Weaviate: 托管型云服务,不想自己运维服务器的商业化应用最爱 。 |
| 向量化模型 (Embedding) | sentence-transformers | - BGE (BAAI): 智源开源的中文最强向量模型,免费且支持本地部署。 - Nomic / Ollama: 适合隐私敏感场景的本地轻量级部署 。 |
1. 本地知识库构建
在这一步,我们将告别之前手工写的 split(";"),使用工业界最常用的滑动窗口切分法。
我们现在需要新建一个 Python 脚本(比如 step1_split.py)作为离线数据准备阶段。
- 文档加载、分块(Chunking)、向量化(Embedding)和存入 ChromaDB,这是一个“后台预处理”动作。通常是由一个独立的 Python 脚本,在系统启动前,或者每天半夜定时跑一次,把知识库建好,以避免每次询问大模型时大模型都要跑一遍,既费时又费钱。
- 这样,我们 Stage 1 写的主循环代码执行时,就可以直接查阅
step1_split.py这个独立脚本。
文档加载与摄取
第一步是将原始资料导入系统,同脚本放在相同目录下。常见的文档来源包括:
- PDF、Word、Markdown、TXT 等本地文件
- 网页内容(通过爬虫或 API 抓取)
- 数据库或企业知识库导出内容
from langchain_community.document_loaders import PyMuPDFLoader
print("正在加载本地文档...")
loader = PyPDFLoader("粮仓建设标准.pdf")
docs = loader.load()
print(f"成功加载文档,当前文档总数:{len(docs)}") # PDF总页数
注:目前使用这个库会报警,因为 LangChain 现在正在从
langchain_community拆分工具库,以避免原有库极其臃肿。但是目前 LangChain 主推的独立 PDF 解析集成安装包太大了。对于我们这个项目,用目前的PyPDFLoader就足够了。如果想用最新的包,可以下载pip install langchain-unstructured unstructured[pdf]。
文档分块策略
大语言模型的输入长度有限(如 4K、8K、32K tokens),所以我们需要将长文档切分成更小的“语义单元”,每个单元称为一个 chunk。
常见分块策略:
- 按段落/句子/章节切分:每段/句/章为一个 chunk,适合结构清晰的文档。
- 固定长度滑窗:每 N 个 token 为一个 chunk,支持重叠(overlap)以保留上下文。Overlap 是滑动窗口重叠的字符数(防止一句话被从中间硬生生劈开)。
- 按语义切分:使用句法分析或语义边界进行智能切分(如 NLTK、spaCy)。
示例代码(固定长度切分):
from langchain.text_splitter import RecursiveCharacterTextSplitter
print("\n正在启动字符切分器...")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每个 Chunk 的最大字符数,可适当调整
chunk_overlap=100, # 滑动窗口重叠的字符数,可适当调整
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " "] # 按照这个优先级去寻找切分点
)
chunks = text_splitter.split_documents(docs)
RecursiveCharacterTextSplitter 叫递归字符分割器,核心逻辑:优先用靠左的分隔符切文本,保证尽量完整语义断开,不把一句话拦腰截断,执行顺序:
- 先拿
\n\n(两段换行,段落分隔)切割文本;- 切出来的片段 ≤ chunk_size(500 字):直接保留,不用再切;
- 片段超 500 字:递归,用下一级分隔符
\n(单行换行)再切;
- 换行切完还是超长 → 用中文句号
。分句; - 句号不行再用感叹号、问号、分号、逗号;
- 最后兜底用空格强制切割(最差情况,会拆碎句子)。
简单说:越靠左,分割粒度越大、语义越完整;越靠右,拆分越碎。
结果可视化
可在脚本末尾加上以下代码观察输出结果:
print(f"文档已被切分为 {len(chunks)} 个 Chunk。具体内容如下:\n")
for i, chunk in enumerate(chunks):
print(f"--- Chunk {i+1} ---")
print(chunk.page_content)
print("-" * 20)
如果只想打印前几个 chunk 看结果,可以更换第二行代码,如以下代码表示打印前 3 个 chunk:for i, chunk in enumerate(chunks[:3]):。
我们观察打印结果是为了加深对 chunk 的理解,观察后就可以把这几行Ctrl + /一键注释掉。在实际工程中,不需要一直打印结果,因为把几十万字的碎片全部打印在终端不仅毫无意义,还会导致内存溢出或日志爆炸。
2. 本地向量化与存储
Embedding向量化
📌 什么是向量化?
向量化(Embedding)是将文本转化为多维空间中的向量表示,使得语义相近的文本在向量空间中距离更近。这样我们就可以用“找最近的向量”来实现语义检索。
🔧 向量化实践
- 使用云端 API 向量化(如OpenAI)
核心特点:需要联网,文档文本会上传至 OpenAI 服务器;按 Token 计费,无需占用本地磁盘 / 算力,开箱即用;不适合涉密、隐私敏感文档。
依赖安装:
pip install langchain-openai openai
示例代码:
from langchain_openai import OpenAIEmbeddings
# 自动读取环境变量OPENAI_API_KEY
embedding_model = OpenAIEmbeddings()
# 批量对所有文档块生成向量
vectors = embedding_model.embed_documents([chunk.page_content for chunk in chunks])
- 本地部署向量模型(推荐)
无需外网、无调用费用,文档隐私性强;分为两种落地方式:
方式 A:HuggingFace 直接加载 BGE 中文模型(本文采用):
适用场景:纯 Python 脚本运行,不需要额外启动软件,轻量化中文向量首选智源人工智能研究院(BAAI)开源的 bge-small-zh-v1.5,它是目前公认最轻量且强悍的中文开源模型。
示例代码:
from langchain_huggingface import HuggingFaceEmbeddings
# 唤醒本地纯正中文 Embedding 模型
print("正在加载本地 BGE 中文向量模型 (首次运行自动下载数百MB的模型权重,耐心等待)...")
model_name = "BAAI/bge-small-zh-v1.5"
model_kwargs = {'device': 'cpu'} # 有N卡改为 cuda 加速
encode_kwargs = {'normalize_embeddings': True} # 向量归一化,适配余弦相似度计算
embeddings = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
方案 B:Ollama 托管向量模型(nomic-embed-text,独立本地服务):
适用场景:统一管理大模型 + 向量模型,提供 HTTP 接口,多程序共享向量服务;模型由 Ollama 进程常驻。
- 终端拉取向量模型
ollama pull nomic-embed-text
- LangChain 调用代码
pip install langchain-ollama
from langchain_ollama import OllamaEmbeddings
# 调用Ollama本地运行的向量模型
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectors = embeddings.embed_documents([chunk.page_content for chunk in chunks])
三种方案对比:
| 方案 | 是否联网 | 数据是否外传 | 额外软件 | 成本 | 适合场景 |
|---|---|---|---|---|---|
| OpenAI 云端 API | 必须联网 | 文本上传第三方 | 无 | 按 token 收费 | 无隐私要求、快速调试 |
| 本地 BGE (HuggingFace) | 仅首次下载模型需网 | 完全本地 | 无 | 一次性占用磁盘 | 单机脚本、中文文档、涉密文件 |
| Ollama nomic-embed-text | 仅拉模型需网 | 完全本地 | 需安装 Ollama 客户端 | 免费 | 多程序复用向量服务、统一本地大模型栈 |
向量间的相似度计算
当用户提问时,我们会将问题也向量化,然后与知识库中的所有向量进行相似度计算,找出最相关的几个 chunk。
常见相似度指标
| 指标 | 公式 | 特点 |
|---|---|---|
| 余弦相似度 | $$\cos(\theta) = \frac{\vec{A} \cdot \vec{B}}{|\vec{A}| |\vec{B}|}$$ | 仅衡量向量方向相似度,不受文本长短影响;前文 BGE 模型开启normalize_embeddings归一化,就是为了适配余弦计算,文本检索通用首选 |
| 欧氏距离(L2) | $$L_2(A, B) = \sqrt{\sum_{i=1}^{n} (a_i - b_i)^2}$$ | 衡量向量空间直线距离,容易受向量长度干扰,文本场景很少单独使用 |
实际使用中,向量数据库通常默认使用余弦相似度。
Indexing:构建向量索引
单纯存放向量无法实现快速相似检索,需要将向量存入专用优化的向量数据库(Vector DB) 并建立检索索引,整套流程称为 Indexing(索引构建)。
向量数据库分为本地轻量库、分布式企业库、云端托管库三类,本次代码使用轻量化本地库 Chroma。
- 主流向量数据库功能对比
| 向量数据库 | 特点 | 是否开源 | 适合场景 |
|---|---|---|---|
| Chroma | Python 原生集成、零额外服务、一键本地持久化,上手成本最低 | ✅ | 单机本地开发、小体量 PDF 知识库原型(本次代码使用) |
| FAISS | Meta 开源,检索速度快,无原生持久化文件夹能力 | ✅ | 本地高性能检索,需自行封装存储逻辑 |
| Milvus | 分布式架构,支持亿级海量向量存储 | ✅ | 企业级大规模生产部署 |
| Weaviate | 自带 HTTP 接口,支持图文多模态向量 | ✅ | 前后端分离、多语言调用项目 |
| Pinecone | 云端托管服务,无需本地部署维护 | ❌ | 商业线上项目,快速上线免运维 |
- 如何选型向量数据库
| 业务需求 | 推荐向量库 |
|---|---|
| 本地脚本调试、PDF 知识库、需要永久保存向量文件 | Chroma(本次实操) |
| 纯本地追求极致检索速度、可自行处理持久化 | FAISS |
| 对外提供接口、多语言程序调用向量库 | Weaviate / Qdrant |
| 百万 / 亿级海量文档、多机器分布式部署 | Milvus / Qdrant |
| 不想搭建本地服务、直接云端调用 | Pinecone、Zilliz Cloud |
向量库构建实践:
Chroma 无需单独安装后台服务,仅依靠 Python 依赖即可运行;支持把文本块、向量、文件页码等元数据全部持久化到本地文件夹,下次运行程序可直接加载存量知识库,不用重复解析 PDF、切分文本、向量化。
- 示例代码:
# 构建并持久化 Chroma 向量数据库
from langchain_chroma import Chroma
# 定义本地存储文件夹,向量库会生成在当前脚本同级目录
persist_directory = "./chroma_db_wheat"
print(f"\n正在将 {len(chunks)} 个 Chunk 翻译成向量,并存入本地档案柜 '{persist_directory}' ...")
vectordb = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=persist_directory
)
print("\n✅ 知识库构建完成!向量数据已永久保存在本地文件夹中。")
参数释义:
documents=chunks:经过文本分割器拆分好的文档块列表embedding=embeddings:提前配置完成的向量模型(本地BGE/OpenAI/Ollama均可兼容)persist_directory:本地文件夹路径,向量、原文、元数据全部落盘永久保存
执行结果:
代码执行后,项目目录会自动生成 chroma_db_wheat 文件夹,内部存储全部向量索引、原文内容、PDF 页码等元数据;以后 Agent 只需要读取这个文件夹,不需要重新读 PDF 和切分了。
3. 在主循环调用向量库
我们回到之前在的主代码文件,开始改动。
唤醒数据库
在代码的开头(或者在原来 get_weather 的位置),我们需要写一个新的函数 query_knowledge_base。这个函数的作用是接收大模型给的关键词,然后去你刚建好的 ChromaDB 里把最相关的 3 个 Chunk 捞出来。
用以下代码替换掉你原来的 get_weather 函数和在前文中创建的 retrieve 函数:
# 导入所需的库
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
# 1. 重新唤醒“翻译官”(必须和存入时使用的是同一个模型)
print("正在连接本地向量知识库...")
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
# 2. 连接刚才落盘的那个“档案柜”
persist_directory = "./chroma_db_wheat"
vectordb = Chroma(persist_directory=persist_directory, embedding_function=embeddings)
def query_knowledge_base(query: str) -> str:
"""
供大模型调用的真实检索工具:根据 query 从 ChromaDB 中查找最相关的资料
"""
print(f"\n[执行工具] 正在向量数据库中搜索与 '{query}' 相关的资料...")
# 执行余弦相似度检索,k=3 表示取出最相关的 3 个 Chunk
docs = vectordb.similarity_search(query, k=3)
if not docs:
return "知识库中未找到相关内容。"
# 把找到的 3 个 Chunk 拼成一段大文本返回给大模型
results = []
for i, doc in enumerate(docs):
# 获取页码信息(如果有的话)
page_num = doc.metadata.get('page', '未知')
results.append(f"【参考资料 {i+1}】第{page_num}页:\n{doc.page_content}")
return "\n".join(results)
修改大模型的“工具说明书”
大模型不知道我们写了新函数,我们需要在 tools 列表里给它发一份新的说明书,告诉它现在它拥有了查阅企业知识库的能力。
找到你原来的 tools = [...] 列表,替换为以下内容:
tools = [
{
"type": "function",
"function": {
"name": "query_knowledge_base",
"description": "查询本地知识库中的相关信息",
"parameters": {
"type": "object",
"required": ["query"],
"properties": {
"query": {
"type": "string",
"description": "用户的查询问题"
}
}
}
}
},
]
更新动态分发字典
当大模型说“我要调用 query_knowledge_base”时,你的代码得知道具体去执行哪个 Python 函数。
找到你核心循环里的 available_tools 字典,更新它:
available_tools = {
"query_knowledge_base": query_knowledge_base
}
重塑大模型人设
修改它的系统提示词,确保它认为自己是资料研究助手:
system_prompt = {
"role": "system",
"content": "你是一个严谨的资料研究助手。请严格根据用户提供的【参考资料】回答问题。如果资料中没有相关信息,请直接回答‘根据资料,我不知道’。"
}
其他
- 确保你
client.chat.completions.create里的tools=tools和tool_choice="auto"都是开启状态(没有被注释掉)。 - 记得删除我们在搓纯手工 RAG 核心流时加的手工知识库和 Chunking, Grounded answer prompt 强制引用,改为让大模型自己判断是否要引用。
二、本篇总结 & 下期预告
至此,Stage 2 三大模块之首 RAG 检索增强模块 完整完结。我们从理论原理、手写最简源码、工业级工程落地三个维度,完整实现了 Agent 知识库增强能力,彻底解决大模型幻觉、知识滞后、无溯源的核心痛点。
RAG 赋予了 Agent 查阅知识的能力,接下来我们将攻坚 Stage 2 第二个核心模块:自定义工具系统。让 Agent 不再局限于问答检索,可自主调用各类自定义业务工具,真正具备落地实操、处理复杂业务的能力。

浙公网安备 33010602011771号