LangChain使用RAG 入门:让大模型读懂你的私有文档

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 来源、页码等标签

📖 Document Loaders 官方文档

第 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 个字符,防止一句完整的话被切断

📖 Text Splitters 官方文档

第 3 步:向量化 --- 文本变成数字

python 复制代码
# 文本 → Embedding 模型 → 1024维浮点数向量
vectors = text_embedding.embed_documents(["苹果是水果"])
# → [[0.12, -0.34, 0.56, ...]]  共1024个数字

相似的文本,向量距离近;不相关的文本,向量距离远。这就是"语义搜索"的数学基础。

📖 Embeddings 官方文档

第 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 可以反复调用增量追加,不用每次重建。

📖 Vector Stores 官方文档

检索生成阶段(每次提问)

第 5 步:检索 --- 从库里搜相关内容

python 复制代码
retriever = vector_store.as_retriever(search_kwargs={"k": 2})
# retriever 内部自动做了:问题转向量 → Redis 相似度搜索 → 返回最相关文本块
# 等价于:embed_query(question) → vector_store.similarity_search(向量, k=2)

📖 Retrievers 官方文档

第 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 模型只接受 strList[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 就是纯文本
.pdf 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 自主判断要不要检索
多轮对话 ✅ 支持追问,带上下文
闲聊兜底 不调工具,直接闲聊回答

总结

  1. RAG = 外接知识库,解决大模型幻觉和私有数据问题
  2. 核心 6 步:加载 → 切分 → 向量化 → 存库 → 检索 → 生成
  3. 80 行代码就能跑通一个完整的知识库问答系统
  4. 换文件格式只换 Loader,下游代码不变
  5. 进一步用 Agent 包装,支持多轮对话和持续交互(见附录 E)
相关推荐
天天进步20152 小时前
Python全栈项目--校园智能宿舍管理系统
开发语言·python
测试员周周2 小时前
【AI测试智能体-面试】AI测试面试60题(附回答思路)
人工智能·python·功能测试·测试工具·单元测试·自动化·测试用例
用户8356290780512 小时前
使用 Python 操作 Word 评论和回复
后端·python
Zella折耳根2 小时前
复习篇-继承和接口
java·开发语言·python
诗词在线3 小时前
求推荐飞花令
大数据·人工智能·python
yijianace3 小时前
Python线程与多线程完全总结(从入门到理解并发本质)
开发语言·python
会Tk矩阵群控的小木4 小时前
基于Python的iMessage短信群发与社媒多账号统一管理系统实现
开发语言·windows·python·新媒体运营·开源软件·个人开发
质造者4 小时前
LangChain + Ollama + Tavily 实现旅游问答系统
linux·人工智能·python·langchain·rag
Solis程序员4 小时前
LangChain从入门到精通(1)
langchain