Day 18:简单的 RAG 实现 —— 本地文档问答机器人

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


在这里插入图片描述

一、今日学习目标

  1. 掌握使用 LangChain 的 TextLoader 加载本地文本文件。
  2. 学会使用 RecursiveCharacterTextSplitter 对长文档进行切分。
  3. 使用 Chroma 作为本地向量数据库,存储文档片段的 Embedding。
  4. 实现一个完整的 RAG 问答链:问题 → 检索 → 拼接上下文 → 生成回答。
  5. 理解 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 如何让模型基于外部知识回答。

今日作业(必做):

  1. 在 docs/ 目录下添加至少 5 个不同主题的文本文件(每个 200-500 字),重新运行 RAG 问答,测试 5 个不同问题,记录回答质量和检索准确度。
  2. 将 chunk_size 分别设置为 200、800、1500,对比检索效果和生成回答的质量,选择一个你认为最佳的设置,并说明理由。
  3. 修改 Prompt,让模型在回答时同时给出引用来源(例如标明“根据文档 1 和文档 2”),并测试效果。
  4. (思考题)RAG 中的检索器如何与后续的 Agent 记忆模块结合?请用 100 字以内描述你的设想。

明日预告: 我们将继续 LangChain 的学习,实现第一个真正的 Agent——使用 create_react_agent 创建一个带工具的数学专家。你将亲眼看到 ReAct 循环是如何工作的。

有任何问题欢迎随时提问!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐