系列教程说明
这是「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_size 和 overlap 的设置很关键:
- 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=”””你是一个专业的知识库助手。请根据以下参考资料回答问题。
要求:
- 只基于参考资料回答,不要编造
- 如果资料中没有相关内容,请明确说明
- 回答要简洁准确
参考资料:
{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))
`
使用方法:
- 创建
my_documents文件夹,放入你的PDF或TXT文件 - 设置环境变量
export OPENAI_API_KEY="your-key" - 运行程序,等待知识库构建完成
- 输入问题,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——晚课 你已完成!明天继续加油!
发表回复