← 返回博客
python2026-09-16 11:43:233 分钟 · 891 0

LangChain 原理与实战(三):RAG 检索增强,从文档到答案

RAG 让模型回答它没背过的内容:加载文档、切成块、向量化存进库、按问题检索、把相关块塞回提示词再生成。这篇把这条链路每一环讲透,并跑通一个基于 Chroma 的最小问答。

前两篇的链,知识全在模型脑袋里(训练数据)。可真实业务要答的是你自己的文档:产品手册、内部 Wiki、合同扫描件。这些模型没见过,硬问只会编。RAG(Retrieval-Augmented Generation,检索增强生成)就是补这块:先去资料库里捞相关片段,再让模型基于这些片段作答。

整条链路是固定的五步:加载 → 切分 → 向量化 → 检索 → 生成。下面逐个走。

第一步:加载文档

DocumentLoader 负责把各种格式读成统一的 Document 对象,里面有 page_content(文本)和 metadata(来源、页码等)。

from langchain_community.document_loaders import TextLoader

loader = TextLoader("intro.txt", encoding="utf-8")
docs = loader.load()
print(type(docs[0]), docs[0].page_content[:50])

PDF、Markdown、网页、Notion 都有对应 loader,接口一致,换格式只换这一行。

第二步:切分

模型上下文有限,整本手册塞不进去,得切成块。RecursiveCharacterTextSplitter 按段落、句子、字符逐级切,尽量不断在句子中间。

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
)
chunks = splitter.split_documents(docs)
print(len(chunks), chunks[0].page_content[:30])

chunk_size 是每块字符上限,chunk_overlap 让相邻块重叠一点,避免一句话被切断后语义丢失。中文可以按字符数估,500 字一块通常够用。

第三步:向量化与存储

切块后,每块文本要变成一串数字(向量),语义相近的文本向量也相近。这个映射由 Embeddings 模型做。

from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(chunks, embeddings, persist_directory="./db")

Chroma.from_documents 会一边向量化一边建库,本地落盘到 ./db。换 FAISS、Milvus 只是换这一行的类,下游接口不变。Embeddings 模型也随意换,比如用本地的 OllamaEmbeddings 或国内的兼容端点。

第四步:检索

库建好后,来一个问题,先把问题向量化,再按相似度捞最相关的几块:

retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
hits = retriever.invoke("退款政策怎么写?")
print([h.page_content for h in hits])

k 是返回块数。这里只是检索,还没调生成模型。先单独跑这步能看到捞回来的是什么,确认切分和向量化没歪。

第五步:检索 + 生成拼成链

LangChain 给了现成的组合函数,把"检索相关块"和"基于块作答"接起来:

from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "只根据下面的上下文回答,不知道就说不知道,不要编。\n{context}"),
    ("human", "{input}"),
])

model = ChatOpenAI(model="gpt-4o-mini")
question_chain = create_stuff_documents_chain(model, prompt)
rag = create_retrieval_chain(retriever, question_chain)

ans = rag.invoke({"input": "退款政策怎么写?"})
print(ans["answer"])

create_stuff_documents_chain 把捞回的块直接"塞"进提示词(stuff 就是这个意思),create_retrieval_chain 在外层先跑检索、再把结果喂给内层。返回里除了 answer 还有 context,调试时能看模型到底参考了哪几块。

几个容易踩的点

  • 块太大,单块塞满上下文,检索精度下降;块太小,一句话被拆两半,语义不全。500 字上下是常见起点,按效果调。
  • 检索回来就信?先 printcontext,确认捞对了再接生成,否则模型背锅你排查半天。
  • 不相关的块进提示词,反而带偏模型。给 system 提示词加一句"不知道就说不知道",能压住编造。

这一篇记住什么

RAG 五步是加载、切分、向量化、检索、生成。前半段(前三步)产出向量库,后半段(后两步)是运行时。库建一次,问答跑无数次。create_retrieval_chain 把后半段封成一个链,你只管 invoke 问题。

下一篇给模型装上"手":让它自己决定调哪个工具,而不用你把每一步写死。那是 Agent 的部分。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/212