LangChain使用RAG 入门:让大模型读懂你的私有文档
读完这篇文章,你将能用 80 行代码搭建一个简单的 "上传 Word 文档 → AI 基于文档回答" 的知识库问答系统。
先看效果
我有一份《阿里巴巴 Java 开发手册》Word 文档。什么都不告诉大模型,直接问:
"00000 和 A0001 分别是什么意思?"
不用 RAG 的效果(纯大模型瞎猜):
erlang
00000 通常是错误码...A0001 可能表示认证失败...
------胡说八道。
用 RAG 的效果:
arduino
根据提供的文本内容:
- 00000 表示"方法内部的执行结果"或"success",通常是操作成功的标志
- A0001 表示"用户端错误",通常是用户输入参数错误导致的异常
------精准,全部来自文档。
这就是 RAG 的核心价值:让大模型基于你的私有文档回答,不瞎编。
RAG 是什么
RAG = Retrieval-Augmented Generation(检索增强生成)。
一句话:给大模型外接一个知识库。 提问时先从知识库里检索相关内容,再让大模型基于检索结果回答。
arduino
┌─────────────────────────────────────────────┐
│ 用户提问 │
│ "00000是什么意思?" │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ ① 检索(Retrieval) │
│ 问题转向量 → 向量数据库搜索 → 召回相关文本片段 │
│ 找到: "00000表示方法内部执行结果..." │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ ② 生成(Generation) │
│ Prompt = 问题 + 检索结果 → 大模型 → 答案 │
│ 答案: "00000表示操作成功的标志..." │
└─────────────────────────────────────────────┘
RAG 解决的三个痛点:
| 痛点 | 不用 RAG | 用 RAG |
|---|---|---|
| 知识截止日期 | "这个我不清楚" | 有文档就能答 |
| 私有数据 | 完全不知道 | 基于你的文档回答 |
| 幻觉(瞎编) | 可能编造 | 有原文依据 |
为什么不能直接关键词搜索?
很多新手会问:"Ctrl+F 不就行了?为什么非要向量?"
看这个例子------你的知识库里有这三句话:
markdown
1. 我喜欢吃苹果 ← 苹果 = 水果
2. 苹果是我最喜欢吃的水果 ← 苹果 = 水果
3. 我喜欢用苹果手机 ← 苹果 = 手机品牌
关键词搜索搜"苹果":三条全返回,但第 3 条(手机品牌)和前两条(水果)语义完全不同。
向量搜索 搜"水果":返回 1 和 2,第 3 条不会被召回------因为向量能捕获语义,不只是匹配字面。
向量 = 文本的"坐标"。语义越接近,坐标越靠近。这就是"以义搜义,而非以字搜字"。
完整代码:80 行跑通 RAG
python
import os
from pathlib import Path
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_redis import RedisConfig, RedisVectorStore
from redisvl.index import SearchIndex
from langchain_community.document_loaders import Docx2txtLoader
from llm import llm, text_embedding
# ========== 配置 ==========
REDIS_URL = os.getenv("REDIS_URI", "redis://localhost:6379")
VECTOR_INDEX_NAME = "alibaba_java"
DOC_FILE_PATH = Path(__file__).parent / "assets" / "alibaba-java.docx"
CHUNK_SIZE = 1000
CHUNK_OVERLAP = 100
RETRIEVE_K = 2
# ========== Prompt 模板 ==========
system_prompt = """
请使用以下提供的文本内容来回答问题。仅使用提供的文本信息,
如果文本中没有相关信息,请回答"抱歉,提供的文本中没有这个信息"。
文本内容:
{context}
"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{question}")
])
# ========== 1. 加载文档 ==========
loader = Docx2txtLoader(file_path=str(DOC_FILE_PATH))
doc_list = loader.load()
print(f"加载文档:{len(doc_list)} 个片段")
# ========== 2. 文本分片 ==========
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP, length_function=len
)
split_docs = text_splitter.split_documents(doc_list)
print(f"分片后:{len(split_docs)} 个文本块")
# ========== 3. 清理旧索引 ==========
try:
SearchIndex.from_existing(name=VECTOR_INDEX_NAME, redis_url=REDIS_URL).delete(drop=True)
except Exception:
pass
# ========== 4. 创建向量库并写入 ==========
redis_config = RedisConfig(index_name=VECTOR_INDEX_NAME, redis_url=REDIS_URL)
vector_store = RedisVectorStore(text_embedding, config=redis_config) # 建索引
vector_store.add_documents(split_docs) # 写入文档
# 后期新增文档只需:vector_store.add_documents(new_docs)
# ========== 5. 构建检索器 ==========
retriever = vector_store.as_retriever(search_kwargs={"k": RETRIEVE_K})
# retriever 内部自动将问题转向量 → Redis 相似度搜索 → 返回最相关文本块
# ========== 6. 组装 RAG 链路 ==========
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
)
# ========== 7. 问答 ==========
if __name__ == "__main__":
question = "00000和A0001分别是什么意思"
result = rag_chain.invoke(question)
print(f"\n【问题】{question}")
print(f"【答案】{result.content}")
这就跑通了。 下面讲这 80 行代码每一步在干什么。
核心流程拆解
RAG 就 6 步,分两大阶段:
入库阶段(跑一次)
第 1 步:数据加载 --- 把文件读进来
python
loader = Docx2txtLoader(file_path="文档.docx")
doc_list = loader.load()
# → [Document(page_content="..."), Document(page_content="..."), ...]
不管 PDF、Word、TXT,Loader 统一输出 List[Document]。Document 就两个字段:
| 字段 | 含义 |
|---|---|
page_content |
文本正文 |
metadata |
来源、页码等标签 |
第 2 步:文本切分 --- 长文档切小块
python
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
split_docs = text_splitter.split_documents(doc_list)
两个关键参数:
chunk_size=1000:每块最多 1000 个字符chunk_overlap=100:相邻块重叠 100 个字符,防止一句完整的话被切断
第 3 步:向量化 --- 文本变成数字
python
# 文本 → Embedding 模型 → 1024维浮点数向量
vectors = text_embedding.embed_documents(["苹果是水果"])
# → [[0.12, -0.34, 0.56, ...]] 共1024个数字
相似的文本,向量距离近;不相关的文本,向量距离远。这就是"语义搜索"的数学基础。
第 4 步:存入向量数据库
python
# 创建向量库实例(建索引)
vector_store = RedisVectorStore(text_embedding, config=redis_config)
# 写入文档(自动向量化 + 存入 Redis)
vector_store.add_documents(split_docs)
# 后期有新文档直接追加:vector_store.add_documents(new_docs)
拆成两步的好处:add_documents 可以反复调用增量追加,不用每次重建。
检索生成阶段(每次提问)
第 5 步:检索 --- 从库里搜相关内容
python
retriever = vector_store.as_retriever(search_kwargs={"k": 2})
# retriever 内部自动做了:问题转向量 → Redis 相似度搜索 → 返回最相关文本块
# 等价于:embed_query(question) → vector_store.similarity_search(向量, k=2)
第 6 步:组装 Prompt + LLM 生成
python
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
)
最终发给大模型的 Prompt 长这样:
erlang
请使用以下提供的文本内容来回答问题...
文本内容:
[检索到的文本块1] 00000表示方法内部执行结果,通常是成功标志...
[检索到的文本块2] A0001表示用户端错误,通常是参数错误...
回答: 00000和A0001分别是什么意思?
LLM 基于"看到的资料"回答,不会瞎编。
LCEL 管线怎么读?
新手看到 | 和 RunnablePassthrough 通常会懵。用数据流的方式理解:
python
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
)
等价于:
arduino
用户输入: "00000是什么意思?"
│
▼
{context: retriever, question: RunnablePassthrough()}
│
├─ context ← retriever.invoke("00000是什么意思?") → 检索结果
└─ question ← "00000是什么意思?"(原样透传)
│
▼
prompt ← 把 context 和 question 填进模板,拼成完整 Prompt
│
▼
llm ← 大模型生成答案
│
▼
答案: "00000表示操作成功的标志..."
|= 管道符,把上一步的输出传给下一步RunnablePassthrough()= 原样透传,不做任何修改{"context": ..., "question": ...}= 并行执行两个任务,结果拼成字典
用 10 个字记住 RAG
加载 → 切分 → 向量化 → 存库 → 检索 → 生成
前四步跑一次入库,后两步每次提问执行。
动手前的准备
1. 启动 Redis Stack:
bash
docker run -d --name redis-stack -p 6379:6379 redis/redis-stack-server:latest
必须是 Redis Stack(带 RediSearch 模块),普通 Redis 不行。
2. 安装依赖:
bash
pip install langchain-core langchain-text-splitters langchain-redis langchain-community redisvl python-docx
3. 配置大模型(llm.py):
python
# llm.py
from langchain.chat_models import init_chat_model
from langchain.embeddings import init_embeddings
llm = init_chat_model("openai:deepseek-ai/DeepSeek-V3")
text_embedding = init_embeddings(
"openai:text-embedding-v4",
dimensions=1024,
api_key="你的阿里云百炼API_KEY",
base_url="你的阿里云百炼BASE_URL",
)
常见踩坑
Q1:redis.exceptions.ConnectionError: Connection refused
bash
# 检查 Redis 是否启动
docker ps | grep redis-stack
# 没启动就启动它
docker run -d --name redis-stack -p 6379:6379 redis/redis-stack-server:latest
Q2:跑一次后数据重复了
每次跑 add_documents 都是追加。测试阶段建议每次先清旧索引(代码第 3 步已处理)。
Q3:检索结果不相关
调 chunk_size:太小语义不完整,太大召回精度下降。从 500~1000 开始试,同时检查 chunk_overlap 不要为 0。
Q4:Embedding 报 InvalidParameter
Embedding 模型只接受 str 或 List[str],别传 Document 对象。embed_documents 用文本,add_documents 用 Document。
Q5:换了文档格式怎么改?
只改第 1 步的 Loader,后面代码不动。速查 → 附录 A。
下一步:让你的 RAG 更好用
掌握基础后,可以进阶的方向:
| 方向 | 一句话 |
|---|---|
| 多轮对话 | 加上 chat history,支持追问 |
| 混合检索 | 关键词 + 向量双路召回,精度更高 |
| 重排序 (Rerank) | 召回后二次精排,把最相关的排前面 |
| 多模态 RAG | 图片、表格也能检索 |
| 本地模型 | 用 Ollama + BGE 做私有化部署,零 API 费用 |
附录 A:文档加载器选型速查
| 格式 | 推荐 Loader | 一句话理由 |
|---|---|---|
| .txt | TextLoader | 直接读,最快 |
| .docx | Docx2txtLoader | 轻量够用 |
| .md | TextLoader | md 就是纯文本 |
| PyPDFLoader | 纯文本 PDF 首选 | |
| .csv | CSVLoader | 按行入库 |
| .json | JSONLoader + jq | 按字段提取 |
复杂 PDF(扫描件、多栏、表格)推荐用 DoclingLoader。
附录 B:三种入库写入方式
| 方法 | 场景 |
|---|---|
add_texts(texts, metadatas) |
手写文本,Demo 用 |
add_documents(docs) |
增量追加,99% 生产场景 |
from_documents(docs, config) |
一步建库 + 写入 |
文本转 Document(统一走 Document 链路):
python
from langchain_core.documents import Document
docs = [Document(page_content="文本内容", metadata={"source": "db"})]
vector_store.add_documents(docs)
附录 C:文本分割器详解
RecursiveCharacterTextSplitter 工作逻辑:
段落太长 → 切成句子 → 句子还长 → 切成词语 → 直到满足 chunk_size
python
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每块最大字符数
chunk_overlap=100, # 相邻块重叠字符数
separators=["\n\n", "\n", "。", ",", " ", ""] # 分割优先级(默认)
)
| 场景 | 推荐 chunk_size | 推荐 overlap |
|---|---|---|
| 通用 RAG | 500~1000 | 50~100 |
| 短问答 FAQ | 200~500 | 0~50 |
| 长文档摘要 | 1000~2000 | 100~200 |
附录 D:接入你的数据
把第 1 步的 Loader 替换成对应的就行:
python
# PDF
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("手册.pdf")
# Markdown
from langchain_community.document_loaders import TextLoader
loader = TextLoader("笔记.md")
# 网页
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://example.com/doc")
Loader 换了,后面代码一行不用改------这就是 Document 统一接口的好处。
附录 E:升级为 Agent 交互式问答
前面是"一行代码一个问答"。如果想像聊天机器人一样持续对话,把 RAG 检索包装成 Agent 工具:
python
import os
from pathlib import Path
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_redis import RedisConfig, RedisVectorStore
from redisvl.index import SearchIndex
from langchain_community.document_loaders import Docx2txtLoader
from llm import llm, text_embedding
# ========== 配置 ==========
REDIS_URL = os.getenv("REDIS_URI", "redis://localhost:6379")
VECTOR_INDEX_NAME = "alibaba_java"
DOC_FILE_PATH = Path(__file__).parent / "assets" / "alibaba-java.docx"
CHUNK_SIZE = 1000
CHUNK_OVERLAP = 100
RETRIEVE_K = 2
# ========== 1. 加载文档 ==========
print("=== 加载文档 ===")
loader = Docx2txtLoader(file_path=str(DOC_FILE_PATH))
doc_list = loader.load()
print(f"加载文档:{len(doc_list)} 个片段")
# ========== 2. 文本分片 ==========
print("=== 文本分片 ===")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP, length_function=len
)
split_docs = text_splitter.split_documents(doc_list)
print(f"分片后:{len(split_docs)} 个文本块")
# ========== 3. 清理旧索引 + 创建向量库 ==========
print("=== 创建向量库 ===")
try:
SearchIndex.from_existing(name=VECTOR_INDEX_NAME, redis_url=REDIS_URL).delete(drop=True)
except Exception:
pass
redis_config = RedisConfig(index_name=VECTOR_INDEX_NAME, redis_url=REDIS_URL)
vector_store = RedisVectorStore(text_embedding, config=redis_config)
vector_store.add_documents(split_docs)
print(f"已写入 {len(split_docs)} 条记录")
# ========== 4. 定义搜索工具 ==========
retriever = vector_store.as_retriever(search_kwargs={"k": RETRIEVE_K})
@tool
def search_document(query: str) -> str:
"""在《阿里巴巴 Java 开发手册》中搜索相关内容。当用户询问编码规范、命名规则、异常处理等 Java 开发相关问题时,调用此工具检索文档。
:param query: 搜索查询,最好是完整的问题或关键词
"""
docs = retriever.invoke(query)
if not docs:
return "未找到相关内容"
return "\n\n".join(d.page_content for d in docs)
# ========== 5. 创建 Agent ==========
print("=== 启动 RAG Agent ===\n")
agent = create_agent(
llm,
tools=[search_document],
system_prompt="你是 Java 开发助手。用户的问题如果涉及编码规范、命名、异常处理、日志等,使用 search_document 工具检索《阿里巴巴 Java 开发手册》,然后基于检索结果回答。如果文档中没有,如实告知。",
)
# ========== 6. 交互式问答 ==========
while True:
try:
question = input("你:").strip()
if not question:
continue
if question.lower() in ("exit", "quit", "q"):
print("再见!")
break
result = agent.invoke({"messages": [("user", question)]})
answer = result["messages"][-1].content
print(f"\n助手:{answer}\n")
except KeyboardInterrupt:
print("\n再见!")
break
和基础版的关键区别:
| 基础版(33-rag-flow.py) | Agent 版(34-rag-agent.py) | |
|---|---|---|
| 问答方式 | 硬编码一个问题,跑完结束 | 交互式循环,输 exit 退出 |
| 检索触发 | LCEL 管道里每次必调 | Agent 自主判断要不要检索 |
| 多轮对话 | ❌ | ✅ 支持追问,带上下文 |
| 闲聊兜底 | 无 | 不调工具,直接闲聊回答 |
总结
- RAG = 外接知识库,解决大模型幻觉和私有数据问题
- 核心 6 步:加载 → 切分 → 向量化 → 存库 → 检索 → 生成
- 80 行代码就能跑通一个完整的知识库问答系统
- 换文件格式只换 Loader,下游代码不变
- 进一步用 Agent 包装,支持多轮对话和持续交互(见附录 E)