【三个月 AI Agent 实战学习】Day 18:简单的 RAG 实现 —— 本地文档问答机器人
Day 18:简单的 RAG 实现 —— 本地文档问答机器人
欢迎来到第十八天!今天我们将把昨天学习的 Embedding 和向量相似度知识付诸实践,使用 LangChain 构建一个完整的本地文档问答机器人。这是 RAG 的最简实现:读取几个本地文本文件,将其切分成小块,存入向量数据库,然后根据用户问题检索相关片段,并让大模型基于这些片段生成回答。完成这个项目后,你将掌握 RAG 的基本流程,并为后续构建更复杂的记忆系统打下基础。

一、今日学习目标
- 掌握使用 LangChain 的
TextLoader加载本地文本文件。 - 学会使用
RecursiveCharacterTextSplitter对长文档进行切分。 - 使用
Chroma作为本地向量数据库,存储文档片段的 Embedding。 - 实现一个完整的 RAG 问答链:问题 → 检索 → 拼接上下文 → 生成回答。
- 理解 RAG 与直接 LLM 问答的区别:RAG 能基于外部知识回答,减少幻觉。
二、详细实现步骤
步骤 1:安装必要的库
我们需要安装 langchain、langchain-community(用于文档加载器)、langchain-openai(用于 ChatOpenAI 和 Embeddings)、chromadb(向量数据库)以及 tiktoken(用于 Token 计算,可选)。如果尚未安装,执行:
pip install langchain langchain-community langchain-openai chromadb
步骤 2:准备本地文档
在你的项目目录下创建一个文件夹,例如 docs/,并在其中放入几个文本文件(.txt)。内容可以是你感兴趣的任何主题,比如人工智能、历史、科技等。为了演示,我们创建两个文件:
docs/ai_intro.txt:关于人工智能的简单介绍。docs/ai_ethics.txt:关于人工智能伦理的内容。
你可以复制一些百科内容或自己编写几段话。确保文件编码为 UTF-8。
步骤 3:加载文档并切分
使用 LangChain 的 TextLoader 加载 docs/ 目录下的所有文本文件,然后使用 RecursiveCharacterTextSplitter 将文档切分成适当大小的块。
新建 simple_rag.py:
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import TextLoader, DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
load_dotenv()
# 加载 docs 目录下的所有 .txt 文件
loader = DirectoryLoader('docs', glob="**/*.txt", loader_cls=TextLoader, encoding='utf-8')
documents = loader.load()
print(f"加载了 {len(documents)} 个文档")
# 文本切分:每个块约 500 字符,重叠 100 字符
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""]
)
chunks = text_splitter.split_documents(documents)
print(f"切分为 {len(chunks)} 个块")
注意: DirectoryLoader 可以批量加载目录中的文件,glob 参数指定匹配模式。TextLoader 需要指定编码,否则 Windows 下可能乱码。
步骤 4:创建向量存储(Chroma)
我们需要一个 Embedding 模型将文本块转换为向量,并存入 Chroma 向量数据库。由于 DeepSeek 不提供 Embedding API,我们使用智谱 AI 的 Embedding(embedding-2),它兼容 OpenAI 格式。在 LangChain 中,我们可以使用 OpenAIEmbeddings 并指定 base_url 和 api_key 来适配智谱。
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
# 初始化智谱 Embedding
embeddings = OpenAIEmbeddings(
model="embedding-2",
api_key=os.getenv("ZHIPU_API_KEY"),
base_url="https://open.bigmodel.cn/api/paas/v4"
)
# 创建向量存储(将文本块写入 Chroma)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # 持久化目录,下次可直接加载
)
print("向量存储已创建")
Chroma 默认会将数据保存在内存中,但通过 persist_directory 可以保存到磁盘,避免重复计算 Embedding。下次加载时,可以使用 Chroma(persist_directory=..., embedding_function=...) 直接加载。
步骤 5:构建检索器
向量存储提供了 as_retriever() 方法,返回一个检索器对象,可以根据查询返回最相关的文档片段。
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 返回最相关的 3 个块
步骤 6:初始化 LLM 并构建 RAG 链
我们将使用 LangChain 的 RetrievalQA 链或更现代的 LCEL 表达式来构建 RAG 管道。这里展示两种方式:
方式一:使用 RetrievalQA(简单封装)
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
temperature=0.3
)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 将所有检索到的文档拼接到 prompt 中
retriever=retriever,
return_source_documents=True # 返回来源文档,便于调试
)
# 测试
query = "人工智能伦理主要涉及哪些问题?"
result = qa_chain.invoke({"query": query})
print("回答:", result["result"])
print("来源片段:", result["source_documents"])
方式二:使用 LCEL 自定义 RAG 链(更灵活)
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# 定义 prompt
template = """请根据以下上下文回答问题。如果无法从上下文中找到答案,请说“我不知道”。
上下文:
{context}
问题:{question}
回答:"""
prompt = ChatPromptTemplate.from_template(template)
# 构建链
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 测试
answer = rag_chain.invoke(query)
print("回答:", answer)
LCEL 方式更透明,我们可以自行控制格式化、拼接等逻辑。推荐使用这种方式,以便后续扩展。
步骤 7:运行并测试
运行 simple_rag.py,观察输出。你可以问不同的问题,检验检索效果和生成质量。例如:
- “什么是人工智能?”
- “人工智能在医疗领域有哪些应用?”
- “人工智能伦理中,算法偏见是怎么产生的?”
对于知识库中没有的内容,模型应该回答“我不知道”,从而体现 RAG 的诚实性。
三、常见问题与调试
Q1:加载文档时出现 UnicodeDecodeError。
→ 在 TextLoader 中明确指定 encoding='utf-8'。如果文件不是 UTF-8,可以尝试 gbk 等。
Q2:Chroma 向量数据库保存后,下次加载时报错。
→ 确保 persist_directory 路径正确,并且加载时使用相同的 embedding_function。例如:
vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
Q3:检索结果不相关或质量差。
→ 可能原因:
- 文档切分不合理:尝试调整
chunk_size和chunk_overlap。 - Embedding 模型不适合中文:智谱
embedding-2对中文支持较好,但也可以尝试其他模型。 - 缺少元数据:可以在加载文档时添加标题等元数据,帮助检索。
Q4:模型回答时忽略了上下文,直接凭记忆回答。
→ 在 Prompt 中强调“只根据上下文回答”,并降低 temperature。如果仍然如此,可以检查检索是否返回了相关片段。
Q5:如何处理 PDF 或 Word 文档?
→ 使用相应的加载器:PyPDFLoader、Docx2txtLoader 等。安装额外的依赖(如 pypdf、docx2txt)后,替换 TextLoader 即可。
Q6:向量数据库选择 Chroma 还是 FAISS?
→ Chroma 易于使用,支持持久化,适合原型开发;FAISS 性能更高,适合大规模数据。今天选用 Chroma 是因为它开箱即用。
四、今日总结与作业
今天你完成了:
- ✅ 使用 LangChain 加载了本地文本文件并进行了切分。
- ✅ 使用智谱 Embedding 和 Chroma 构建了向量存储。
- ✅ 实现了完整的 RAG 问答链,并能返回来源文档。
- ✅ 通过实际测试,体会了 RAG 如何让模型基于外部知识回答。
今日作业(必做):
- 在
docs/目录下添加至少 5 个不同主题的文本文件(每个 200-500 字),重新运行 RAG 问答,测试 5 个不同问题,记录回答质量和检索准确度。 - 将
chunk_size分别设置为 200、800、1500,对比检索效果和生成回答的质量,选择一个你认为最佳的设置,并说明理由。 - 修改 Prompt,让模型在回答时同时给出引用来源(例如标明“根据文档 1 和文档 2”),并测试效果。
- (思考题)RAG 中的检索器如何与后续的 Agent 记忆模块结合?请用 100 字以内描述你的设想。
明日预告: 我们将继续 LangChain 的学习,实现第一个真正的 Agent——使用 create_react_agent 创建一个带工具的数学专家。你将亲眼看到 ReAct 循环是如何工作的。
有任何问题欢迎随时提问!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)