Day52——晚课 RAG实战:用代码搭建文档问答系统

作者:

系列教程说明

这是「AI零基础学习」系列教程的第52篇,共140篇。每天早课讲理论,晚课做实战。

上篇回顾

今天早课我们学习了RAG的核心概念:检索增强生成让AI不再”凭记忆回答”,而是先从你的文档中找到相关内容,再基于这些内容生成回答。

三个关键点回顾:

  • 向量数据库是RAG的”记忆库”,把文字变成数字向量存储和检索
  • Embedding模型负责把文字转化为向量,是RAG的”翻译官”
  • 检索+生成两步走:先找到最相关的文档片段,再交给大模型组织回答

晚课的目标很明确:用Python代码,从零搭建一个能读PDF文档、回答问题的RAG系统


实操一:3分钟体验——用ChromaDB搭建最简RAG

我们先用最少的代码跑通一个RAG流程,感受一下”AI读文档”是什么体验。

安装依赖(在终端执行):

`bash

pip install chromadb openai

`

核心代码(新建 rag_demo.py):

`python

import chromadb

from openai import OpenAI

# 第一步:创建向量数据库

client = chromadb.Client()

collection = client.create_collection(“my_docs”)

# 第二步:准备文档(模拟你的知识库)

documents = [

“Python是一种解释型编程语言,由Guido van Rossum于1991年创建。”,

“机器学习是AI的子领域,让计算机从数据中学习规律。”,

“RAG是检索增强生成的缩写,结合了信息检索和文本生成。”,

“向量数据库专门存储和检索高维向量数据,常用于AI应用。”,

“LangChain是一个用于构建LLM应用的开源框架。”

]

# 第三步:把文档存入向量数据库(ChromaDB自动处理向量化)

collection.add(

documents=documents,

ids=[f”doc_{i}” for i in range(len(documents))]

)

# 第四步:提问并检索相关文档

question = “什么是RAG?”

results = collection.query(query_texts=[question], n_results=2)

context = “n”.join(results[“documents”][0])

# 第五步:用大模型生成回答

llm = OpenAI()

response = llm.chat.completions.create(

model=”gpt-4o-mini”,

messages=[

{“role”: “system”, “content”: f”根据以下参考资料回答问题:n{context}”},

{“role”: “user”, “content”: question}

]

)

print(response.choices[0].message.content)

`

运行 python rag_demo.py,你会看到AI基于你提供的文档内容回答问题,而不是凭空编造。

关键理解:ChromaDB在后台自动把文字转成向量并存储,query_texts 参数会自动把你的问题也转成向量,然后做相似度匹配。整个过程对你来说是透明的。


实操二:读取真实文档——从PDF到知识库

实际应用中,你的资料不会写在代码里,而是保存为PDF、Word、TXT等文件。我们来实现自动读取PDF并构建知识库

安装额外依赖

`bash

pip install pypdf

`

代码实现

`python

from pypdf import PdfReader

import chromadb

from openai import OpenAI

# 第一步:读取PDF文件

def load_pdf(pdf_path):

reader = PdfReader(pdf_path)

text = “”

for page in reader.pages:

text += page.extract_text() + “n”

return text

# 第二步:把长文档切成小段(chunk)

def split_text(text, chunk_size=500, overlap=50):

chunks = []

start = 0

while start < len(text):

end = start + chunk_size

chunks.append(text[start:end])

start = end – overlap # 前后段重叠,避免切断语义

return chunks

# 第三步:构建向量知识库

def build_knowledge_base(pdf_path):

text = load_pdf(pdf_path)

chunks = split_text(text)

client = chromadb.Client()

collection = client.create_collection(“pdf_docs”)

collection.add(

documents=chunks,

ids=[f”chunk_{i}” for i in range(len(chunks))]

)

return collection

# 第四步:提问函数

def ask(question, collection, top_k=3):

results = collection.query(query_texts=[question], n_results=top_k)

context = “n—n”.join(results[“documents”][0])

llm = OpenAI()

response = llm.chat.completions.create(

model=”gpt-4o-mini”,

messages=[

{“role”: “system”, “content”: f”你是文档问答助手。根据参考资料回答,如果资料中没有相关内容请说明。nn参考资料:n{context}”},

{“role”: “user”, “content”: question}

]

)

return response.choices[0].message.content

# 使用示例

collection = build_knowledge_base(“your_document.pdf”)

answer = ask(“这份文档的主要内容是什么?”, collection)

print(answer)

`

重点技巧chunk_sizeoverlap 的设置很关键:

  • chunk_size太大:检索精度下降,可能匹配到不相关的内容
  • chunk_size太小:上下文不完整,AI回答缺乏背景信息
  • overlap:前后段重叠,确保跨越边界的语义不丢失
  • 经验值:500字 + 50字重叠适合大多数中文文档

实操三:用LangChain快速搭建生产级RAG

手写RAG代码帮助理解原理,但实际项目中推荐用成熟的框架。LangChain是目前最流行的LLM应用框架,它把RAG的各个环节都封装好了。

`bash

pip install langchain langchain-community langchain-openai chromadb pypdf

`

LangChain版RAG代码

`python

from langchain_community.document_loaders import PyPDFLoader

from langchain.text_splitter import RecursiveCharacterTextSplitter

from langchain_community.vectorstores import Chroma

from langchain_openai import OpenAIEmbeddings, ChatOpenAI

from langchain.chains import RetrievalQA

# 1. 加载文档

loader = PyPDFLoader(“your_document.pdf”)

pages = loader.load()

# 2. 切分文本

splitter = RecursiveCharacterTextSplitter(

chunk_size=500,

chunk_overlap=50,

separators=[“nn”, “n”, “。”, “!”, “?”, “;”, ” “]

)

docs = splitter.split_documents(pages)

# 3. 创建向量数据库(使用OpenAI的Embedding模型)

embeddings = OpenAIEmbeddings()

vectorstore = Chroma.from_documents(docs, embeddings)

# 4. 创建问答链

llm = ChatOpenAI(model=”gpt-4o-mini”)

qa_chain = RetrievalQA.from_chain_type(

llm=llm,

retriever=vectorstore.as_retriever(search_kwargs={“k”: 3}),

return_source_documents=True

)

# 5. 提问

result = qa_chain.invoke({“query”: “文档中提到了哪些关键概念?”})

print(“回答:”, result[“result”])

print(“来源:”, [doc.metadata for doc in result[“source_documents”]])

`

LangChain的优势

  • 一行代码换模型:把 ChatOpenAI 换成其他模型即可
  • 自动处理分块策略RecursiveCharacterTextSplitter 智能切分
  • 来源追踪return_source_documents=True 让你知道答案来自哪一页
  • 支持多种文档格式:PDF、Word、TXT、网页、Markdown都有对应的Loader

实操四:完整项目——搭建个人知识库问答系统

现在我们把前面学到的所有内容整合起来,搭建一个可以实际使用的个人知识库

项目目标:把多个PDF文件导入系统,支持自然语言提问,返回答案并标注来源。

`python

import os

from langchain_community.document_loaders import (

PyPDFLoader,

DirectoryLoader,

TextLoader

)

from langchain.text_splitter import RecursiveCharacterTextSplitter

from langchain_community.vectorstores import Chroma

from langchain_openai import OpenAIEmbeddings, ChatOpenAI

from langchain.chains import RetrievalQA

from langchain.prompts import PromptTemplate

class PersonalKB:

def __init__(self, db_path=”./my_kb”):

self.embeddings = OpenAIEmbeddings()

self.db_path = db_path

self.vectorstore = None

self.qa_chain = None

def load_documents(self, folder_path):

“””加载文件夹中的所有文档”””

all_docs = []

# 支持PDF

pdf_loader = DirectoryLoader(folder_path, glob=”*.pdf”, loader_cls=PyPDFLoader)

all_docs.extend(pdf_loader.load())

# 支持TXT

txt_loader = DirectoryLoader(folder_path, glob=”*.txt”, loader_cls=TextLoader)

all_docs.extend(txt_loader.load())

print(f”已加载 {len(all_docs)} 个文档片段”)

return all_docs

def build(self, folder_path):

“””构建知识库”””

docs = self.load_documents(folder_path)

splitter = RecursiveCharacterTextSplitter(

chunk_size=500,

chunk_overlap=50,

separators=[“nn”, “n”, “。”, “!”, “?”, ” “]

)

splits = splitter.split_documents(docs)

print(f”切分为 {len(splits)} 个文本块”)

self.vectorstore = Chroma.from_documents(

splits, self.embeddings, persist_directory=self.db_path

)

# 自定义提示词模板

prompt = PromptTemplate(

template=”””你是一个专业的知识库助手。请根据以下参考资料回答问题。

要求:

  1. 只基于参考资料回答,不要编造
  2. 如果资料中没有相关内容,请明确说明
  3. 回答要简洁准确

参考资料:

{context}

问题:{question}

回答:”””,

input_variables=[“context”, “question”]

)

self.qa_chain = RetrievalQA.from_chain_type(

llm=ChatOpenAI(model=”gpt-4o-mini”, temperature=0),

retriever=self.vectorstore.as_retriever(search_kwargs={“k”: 4}),

return_source_documents=True,

chain_type_kwargs={“prompt”: prompt}

)

print(“知识库构建完成!”)

def ask(self, question):

“””提问”””

if not self.qa_chain:

return “请先调用 build() 构建知识库”

result = self.qa_chain.invoke({“query”: question})

# 格式化输出

answer = result[“result”]

sources = set()

for doc in result[“source_documents”]:

source = doc.metadata.get(“source”, “未知”)

page = doc.metadata.get(“page”, “?”)

sources.add(f”{os.path.basename(source)} 第{page}页”)

return f”📝 回答:{answer}nn📚 来源:{‘, ‘.join(sources)}”

# 使用示例

kb = PersonalKB()

kb.build(“./my_documents”) # 把PDF/TXT文件放在这个文件夹

# 开始提问

while True:

q = input(“n请输入问题(输入q退出):”)

if q.lower() == ‘q’:

break

print(kb.ask(q))

`

使用方法

  1. 创建 my_documents 文件夹,放入你的PDF或TXT文件
  2. 设置环境变量 export OPENAI_API_KEY="your-key"
  3. 运行程序,等待知识库构建完成
  4. 输入问题,AI会基于你的文档回答

实操五:免费方案——用本地模型替代OpenAI

不想花钱调API?用开源模型也能搭RAG。

方案一:Ollama + 本地Embedding

`bash

# 安装Ollama

curl -fsSL https://ollama.ai/install.sh | sh

# 拉取模型

ollama pull nomic-embed-text # Embedding模型

ollama pull qwen2:7b # 中文对话模型

`

`python

from langchain_community.embeddings import OllamaEmbeddings

from langchain_community.llms import Ollama

# 替换为本地模型

embeddings = OllamaEmbeddings(model=”nomic-embed-text”)

llm = Ollama(model=”qwen2:7b”)

`

方案二:完全免费的API

`python

# 使用免费的HuggingFace Embedding

from langchain_community.embeddings import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(

model_name=”shibing624/text2vec-base-chinese”

)

`

三种方案对比

方案 成本 效果 适合场景

|——|——|——|———-|

OpenAI API 按量付费 最好 正式项目
Ollama本地 免费(需GPU) 较好 隐私敏感数据
HuggingFace 免费 一般 学习和测试

今日总结

今天我们完成了RAG系统的完整实战,三个核心收获:

第一,RAG的核心流程就是三步:读取文档 → 切分并存储向量 → 检索+生成回答。理解了这个流程,任何框架都只是工具。

第二,chunk策略决定质量:文本切分的大小、重叠、分隔符都会影响最终效果。500字+50字重叠是不错的起点。

第三,从手写到框架是自然进阶:先用原生代码理解原理,再用LangChain提升效率,最后根据需求选择本地或云端方案。

今日行动项

初级:用实操一的代码,把3段你喜欢的文字存入ChromaDB,体验向量检索的效果

中级:找一份PDF文档,用实操二的代码搭建一个简单的文档问答系统

进阶:用实操四的PersonalKB类,把你所有的学习笔记导入,搭建个人知识库


📖 地铁深读:为什么chunk切分是RAG的灵魂?

很多初学者搭建RAG时,把注意力都放在选什么Embedding模型、用什么大模型上。但在实际项目中,chunk切分策略往往才是决定RAG效果的关键因素

一个真实的例子:假设你在处理一本技术手册,其中有一段话:

“本系统支持MySQL和PostgreSQL两种数据库。MySQL的配置文件位于/etc/mysql/my.cnf,PostgreSQL的配置文件位于/etc/postgresql/postgresql.conf。”

如果chunk_size设得太小,这段话可能被切成两半,前半段提到MySQL,后半段提到PostgreSQL。当用户问”PostgreSQL的配置文件在哪”时,检索到的可能是前半段(只提到MySQL),导致回答错误。

解决方案

  • 语义切分:按段落、章节等自然边界切分,而不是机械地按字数
  • 父文档检索:先用小chunk精确匹配,再返回包含该chunk的大chunk给LLM
  • 重叠策略:确保关键信息不会正好落在切分边界上

LangChain的 RecursiveCharacterTextSplitter 就是按层级分隔符切分的:先尝试按 nn(段落),再按 n(行),最后按句号、空格等。这比简单的固定长度切分聪明得多。

进阶思考:如果你的文档中有表格、代码块、公式等特殊内容,普通的文字切分可能会破坏它们的结构。这时候需要用专门的解析器(如 UnstructuredPDFLoader)来处理。

记住:RAG的效果 = 检索质量 × 生成质量。检索质量的上限,很大程度上取决于你的chunk策略。


下篇预告

明天早上8点:Day53——早课 AI Agent:自主执行任务

今晚5点:Day52——晚课 你已完成!明天继续加油!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注