欢迎光临
我们一直在努力

LangChain 从入门到实战:从 0 构建一个可追溯的本地知识库 RAG 问答系统

LangChain 从入门到实战:从 0 构建一个可追溯的本地知识库 RAG 问答系统

本文面向第一次接触 LangChain 的 Python 开发者。我们不会停留在“调用一次大模型”,而是从核心概念、环境搭建开始,最终完成一个能检索本地资料、基于资料回答、展示引用来源的 RAG 应用。

文章按 LangChain 当前主线 API 编写,更新时间:2026 年 7 月。LangChain 迭代较快,安装时建议使用虚拟环境并锁定主版本。

前言:为什么调用大模型之后,还需要 LangChain?

只调用一次大模型 API 并不困难:准备一段提示词,发送请求,读取回答即可。真正的 AI 应用却通常还需要处理下面这些问题:

  • 怎样在不同模型供应商之间切换?
  • 怎样组织系统提示词、用户消息和历史消息?
  • 怎样让模型查询 PDF、Markdown、数据库或业务 API?
  • 怎样保存对话状态,并支持流式输出?
  • 怎样记录每一步调用,定位“为什么答错了”?
  • 怎样让模型决定何时调用搜索、计算器等工具?

LangChain 的价值不在于“让模型变聪明”,而在于给模型周围的应用逻辑提供统一组件和编排方式。

可以把一个大模型应用粗略理解成:

用户问题
↓
提示词 / 历史消息 / 外部知识
↓
大模型 ←→ 工具、数据库、搜索服务
↓
结构化结果 / 流式回答 / 运行轨迹

LangChain 负责连接这些环节;大模型仍然负责理解与生成,向量数据库负责检索,业务系统负责提供真实数据。它不是模型,也不是数据库。


一、先理清 LangChain、LangGraph 和 LangSmith

初学者最容易被这几个名称绕晕。

项目解决的问题适合场景
LangChain 模型、消息、工具、检索器和 Agent 的统一接口 快速开发大多数 LLM 应用
LangGraph 用图和状态机编排可持久化、可中断的复杂流程 多步骤工作流、长时间任务、人工审批
LangSmith 追踪、调试、评估和监控模型应用 定位错误、对比提示词、线上观测

当前 LangChain 的 Agent 构建在 LangGraph 之上。简单项目先学 LangChain 即可;当流程包含分支、循环、持久化和人工介入时,再深入 LangGraph。


二、必须掌握的核心组件

1. Model:模型统一接口

LangChain 将不同供应商的聊天模型包装成相近的调用方式。换模型时,业务代码通常不需要整体重写。

response = model.invoke("请用一句话解释 RAG")
print(response.content)

需要注意:统一接口不代表所有模型能力完全相同。工具调用、结构化输出、上下文窗口和多模态能力仍以具体模型为准。

2. Message:消息

聊天模型接收的不是一整段普通字符串,而是一组带角色的消息:

  • SystemMessage:规定身份、边界和回答规则;
  • HumanMessage:用户输入;
  • AIMessage:模型回复,可能包含工具调用信息;
  • ToolMessage:工具执行结果。

3. Prompt Template:提示词模板

模板将动态变量安全地注入提示词,使提示词从“拼字符串”升级为可复用组件。

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
("system", "你是一名{role},回答必须简洁准确。"),
("human", "{question}"),
])

4. Document:文档

LangChain 使用 Document 表示一段文本及其元数据:

from langchain_core.documents import Document

doc = Document(
page_content="员工每年享有 10 天带薪年假。",
metadata={"source": "员工手册", "section": "休假制度"},
)

metadata 非常重要。来源、页码、章节、时间和权限标签都应该保留下来,后续才能引用来源和做过滤检索。

5. Embedding:文本向量

Embedding 模型把文本转换成数值向量。语义相近的文本,在向量空间中的距离通常也更近。

“年假有多少天” ─┐
├─ 向量距离较近
“带薪休假额度” ─┘

Embedding 模型和负责回答问题的聊天模型是两类模型,不要混为一谈。

6. Vector Store 与 Retriever

  • Vector Store 保存文本块及其向量,并执行相似度搜索;
  • Retriever 接收自然语言问题,返回相关 Document;
  • Retriever 不一定来自向量数据库,也可以连接关键词搜索、API 或现有知识系统。

7. Tool 与 Agent

Tool 是模型可以调用的函数;Agent 则让模型在运行时决定调用哪个工具、传什么参数、是否继续执行。

固定流程适合普通 Chain,动态决策适合 Agent。能用确定性流程解决的问题,不必一开始就上 Agent。


三、RAG 到底是什么?

RAG 全称是 Retrieval-Augmented Generation,即检索增强生成。

普通问答:

问题 → 大模型 → 回答

RAG 问答:

问题 → 检索相关资料 → 问题 + 资料 → 大模型 → 有依据的回答

它主要解决两个问题:

  • 模型训练数据有时间边界,不知道企业内部或刚刚更新的知识;
  • 模型上下文有限,无法每次把整个知识库全部塞进提示词。
  • 典型的离线建库流程是:

    加载文档 → 文本切分 → 生成向量 → 写入向量库

    在线问答流程是:

    用户提问 → 问题向量化 → 召回相关文本块 → 拼接上下文 → 模型回答

    RAG 不是“彻底消灭幻觉”。它只是给模型提供依据。检索不到、切分不合理、资料过期或提示词约束不足,都会导致错误。


    四、从 0 搭建开发环境

    1. 创建项目

    mkdir langchain-rag-demo
    cd langchain-rag-demo

    python -m venv .venv

    激活虚拟环境:

    # Windows PowerShell
    .venv\\Scripts\\Activate.ps1

    # macOS / Linux
    source .venv/bin/activate

    建议使用 Python 3.10 或更高版本。

    2. 安装依赖

    pip install "langchain>=1,<2" "langchain-core>=1,<2" "langchain-openai>=1,<2" "langchain-text-splitters>=1,<2" "python-dotenv>=1,<2"

    为什么要安装多个包?因为 LangChain 采用模块化设计:核心抽象、模型供应商、文本切分器分别发布,项目只安装真正需要的集成。

    3. 配置环境变量

    新建 .env:

    OPENAI_API_KEY=你的_API_Key
    OPENAI_CHAT_MODEL=填写你的聊天模型名称
    OPENAI_EMBEDDING_MODEL=填写你的向量模型名称

    # 使用 OpenAI 兼容服务时再填写,官方服务可以留空
    # OPENAI_BASE_URL=https://your-provider.example/v1

    不要把真实密钥直接写进 Python 文件,也不要提交到 Git 仓库。记得在 .gitignore 中添加:

    .env
    .venv/
    __pycache__/

    聊天模型和向量模型必须由所使用的服务实际支持。某些“OpenAI 兼容接口”只兼容聊天接口,并不提供 Embedding API,使用前需要检查供应商文档。


    五、第一步:完成最小模型调用

    新建 hello_langchain.py:

    import os

    from dotenv import load_dotenv
    from langchain_openai import ChatOpenAI

    load_dotenv()

    model = ChatOpenAI(
    model=os.environ["OPENAI_CHAT_MODEL"],
    api_key=os.environ["OPENAI_API_KEY"],
    base_url=os.getenv("OPENAI_BASE_URL") or None,
    temperature=0,
    )

    response = model.invoke("请用不超过 50 个字解释 LangChain。")
    print(response.content)

    运行:

    python hello_langchain.py

    这里有两个值得注意的细节:

    • invoke() 是 LangChain Runnable 体系中的标准同步调用方法;
    • temperature=0 会降低回答的随机性,但不等于保证事实正确。

    六、第二步:用提示词模板和 LCEL 组织流程

    LangChain Expression Language,简称 LCEL,使用管道符 | 连接 Runnable 组件。

    from langchain_core.output_parsers import StrOutputParser
    from langchain_core.prompts import ChatPromptTemplate

    prompt = ChatPromptTemplate.from_messages([
    ("system", "你是 Python 教师,使用通俗中文回答。"),
    ("human", "请解释:{topic}"),
    ])

    chain = prompt | model | StrOutputParser()

    answer = chain.invoke({"topic": "向量数据库为什么能做语义搜索"})
    print(answer)

    数据依次经过三个组件:

    字典参数 → ChatPromptTemplate → ChatModel → StrOutputParser → 字符串

    这种写法的优点是组件边界清晰,而且大部分 Runnable 都支持:

    • invoke():单次同步调用;
    • ainvoke():单次异步调用;
    • batch():批量调用;
    • stream():流式调用。

    七、完整实战:本地员工手册 RAG 问答系统

    我们的目标是构建一个命令行程序:

  • 读取本地 Markdown 知识库;
  • 将长文档切成多个文本块;
  • 生成 Embedding 并存入内存向量库;
  • 根据问题检索最相关的 3 个文本块;
  • 要求模型只能依据资料回答;
  • 输出回答和资料来源。
  • 1. 项目结构

    langchain-rag-demo/
    ├── .env
    ├── knowledge.md
    ├── rag_app.py
    └── requirements.txt

    2. 准备知识库

    新建 knowledge.md:

    # 星河科技员工手册

    ## 工作时间

    公司实行每日 8 小时、每周 5 天工作制。标准工作时间为 9:00 至 18:00,
    午休时间为 12:00 至 13:00。员工每周可以申请两天远程办公,需提前一天
    在办公系统中提交申请并获得直属主管批准。

    ## 年假制度

    入职满一年但不满十年的员工,每个自然年享有 10 天带薪年假;入职满十年
    的员工享有 15 天带薪年假。年假最小申请单位为半天,原则上应提前 3 个
    工作日提交申请。未使用的年假最多可结转 5 天,并须在次年 3 月 31 日前用完。

    ## 报销制度

    报销实行逐级审批。单笔金额不超过 500 元的日常费用,由直属主管审批;超过
    500 元且不超过 5000 元的费用,还需要部门负责人审批;超过 5000 元的费用
    还需要财务负责人审批。所有报销应在费用发生后 30 天内提交,并附合法有效的发票。

    ## 设备借用

    员工可以在 IT 服务台借用显示器、键盘和测试手机。测试手机最长借用 14 天,
    如需续借,应在到期前提交申请。设备损坏或丢失后,应立即联系 IT 服务台。

    3. 编写完整程序

    新建 rag_app.py:

    import os
    from pathlib import Path

    from dotenv import load_dotenv
    from langchain_core.documents import Document
    from langchain_core.output_parsers import StrOutputParser
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.vectorstores import InMemoryVectorStore
    from langchain_openai import ChatOpenAI, OpenAIEmbeddings
    from langchain_text_splitters import RecursiveCharacterTextSplitter

    load_dotenv()

    def require_env(name: str) –> str:
    """读取必需的环境变量,缺失时给出清晰错误。"""
    value = os.getenv(name)
    if not value:
    raise RuntimeError(f"缺少环境变量:{name}")
    return value

    api_key = require_env("OPENAI_API_KEY")
    chat_model_name = require_env("OPENAI_CHAT_MODEL")
    embedding_model_name = require_env("OPENAI_EMBEDDING_MODEL")
    base_url = os.getenv("OPENAI_BASE_URL") or None

    # 1. 初始化聊天模型和向量模型
    llm = ChatOpenAI(
    model=chat_model_name,
    api_key=api_key,
    base_url=base_url,
    temperature=0,
    )

    embeddings = OpenAIEmbeddings(
    model=embedding_model_name,
    api_key=api_key,
    base_url=base_url,
    )

    # 2. 加载文档,并保留来源信息
    knowledge_path = Path(__file__).with_name("knowledge.md")
    raw_text = knowledge_path.read_text(encoding="utf-8")

    documents = [
    Document(
    page_content=raw_text,
    metadata={"source": knowledge_path.name},
    )
    ]

    # 3. 将长文档切分为适合检索的小块
    splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=100,
    add_start_index=True,
    separators=["\\n## ", "\\n### ", "\\n\\n", "\\n", "。", ";", ",", " ", ""],
    )
    chunks = splitter.split_documents(documents)

    for index, chunk in enumerate(chunks):
    chunk.metadata["chunk_id"] = index

    # 4. 建立内存向量库
    vector_store = InMemoryVectorStore(embeddings)
    vector_store.add_documents(documents=chunks)

    # 5. 将向量库包装成检索器
    retriever = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 3},
    )

    # 6. 定义“只能根据资料回答”的提示词
    prompt = ChatPromptTemplate.from_messages([
    (
    "system",
    """你是企业制度问答助手。请严格根据给定资料回答问题。
    如果资料中没有答案,直接回答“根据现有资料无法确定”,不要猜测。
    回答应简洁、准确;涉及条件、时间或金额时必须保留关键数字。

    资料:
    {context}""",
    ),
    ("human", "问题:{question}"),
    ])

    answer_chain = prompt | llm | StrOutputParser()

    def format_documents(docs: list[Document]) –> str:
    """将检索结果整理成适合放入提示词的文本。"""
    parts = []
    for doc in docs:
    source = doc.metadata.get("source", "未知来源")
    chunk_id = doc.metadata.get("chunk_id", "?")
    parts.append(
    f"[来源:{source},文本块:{chunk_id}]\\n{doc.page_content}"
    )
    return "\\n\\n".join(parts)

    def ask(question: str) –> tuple[str, list[Document]]:
    """先检索,再生成答案,同时返回引用文档。"""
    retrieved_docs = retriever.invoke(question)
    answer = answer_chain.invoke({
    "question": question,
    "context": format_documents(retrieved_docs),
    })
    return answer, retrieved_docs

    if __name__ == "__main__":
    print(f"知识库加载完成,共生成 {len(chunks)} 个文本块。")
    print("输入 exit 退出。\\n")

    while True:
    question = input("你:").strip()
    if question.lower() in {"exit", "quit", "q"}:
    break
    if not question:
    continue

    answer, sources = ask(question)
    print(f"\\n助手:{answer}")
    print("引用来源:")
    for doc in sources:
    print(
    f"- {doc.metadata.get('source')}"
    f" / chunk={doc.metadata.get('chunk_id')}"
    f" / start={doc.metadata.get('start_index')}"
    )
    print()

    4. 运行项目

    python rag_app.py

    可以测试以下问题:

    你:入职两年的员工有几天年假?
    助手:入职满一年但不满十年的员工,每个自然年享有 10 天带薪年假。

    你:一笔 6000 元的费用需要谁审批?
    助手:需要直属主管、部门负责人以及财务负责人审批。

    你:公司每月提供多少餐补?
    助手:根据现有资料无法确定。

    第三个问题很重要:一个合格的知识库助手不仅要知道“如何回答”,还要知道“资料不足时拒绝猜测”。


    八、逐行理解 RAG 的关键设计

    1. 为什么要切分文本?

    如果直接把整本手册生成一个向量,多个主题会被压缩在同一个表示中,检索精度通常较差;如果文本块太小,又会丢失上下文。

    本文使用:

    chunk_size=500
    chunk_overlap=100

    这不是通用最优值。中文制度文档可以从 300~800 字符开始实验;代码、表格和扫描 PDF 应使用更有针对性的切分策略。

    chunk_overlap 让相邻文本块保留一部分重复内容,降低关键信息刚好被边界切断的概率,但重叠过大会增加存储量并产生重复召回。

    2. 为什么使用 RecursiveCharacterTextSplitter?

    它会按照给定分隔符逐级尝试切分,优先保留章节、段落和句子边界。对于一般文本,它通常是比“固定长度硬切”更稳妥的起点。

    3. 为什么要保留来源?

    模型生成的“引用”不一定真实。可靠做法是由程序读取检索结果的 metadata,并在界面上展示来源,而不是让模型凭空编造页码。

    4. 为什么演示使用内存向量库?

    InMemoryVectorStore 零配置,适合学习和小型实验。但程序关闭后索引会丢失,而且每次启动都会重新调用 Embedding API。

    生产项目应该根据数据规模和运维条件选择 Chroma、FAISS、PGVector、Milvus、Qdrant、Pinecone 等持久化方案,并将“建库”和“查询”拆成两个独立流程。

    5. 为什么显式写“检索 → 生成”?

    这是一种 2-Step RAG:每个问题都先检索,再生成。它的调用次数和延迟更可预测,特别适合 FAQ、企业制度和产品文档机器人。

    如果让 Agent 自己决定何时检索,就变成 Agentic RAG,灵活性更高,但行为、延迟和成本也更难预测。


    九、进阶:把知识库封装成 Agent 工具

    当应用不只有一个知识库,还需要查询天气、订单、数据库或计算器时,可以把检索能力封装成 Tool。

    from langchain.agents import create_agent
    from langchain.tools import tool

    @tool
    def search_employee_handbook(question: str) –> str:
    """查询员工手册中的工作时间、休假、报销和设备制度。"""
    docs = retriever.invoke(question)
    return format_documents(docs)

    agent = create_agent(
    model=llm,
    tools=[search_employee_handbook],
    system_prompt=(
    "你是企业助手。涉及员工制度时必须先调用员工手册工具;"
    "工具没有提供答案时,不得猜测。"
    ),
    )

    result = agent.invoke({
    "messages": [
    {"role": "user", "content": "测试手机最多可以借多少天?"}
    ]
    })

    print(result["messages"][–1].content)

    函数的 docstring 会帮助模型判断工具用途,所以工具说明应具体、准确,并明确适用范围。


    十、从 Demo 走向生产,还需要做什么?

    1. 将建库与问答分离

    实际系统不应该每次启动都重新读取全部文件和生成向量。推荐拆分为:

    index.py:文档发生变化时执行,负责解析、切分、向量化和入库
    app.py:持续提供查询服务,只负责检索和生成

    同时记录文件哈希或更新时间,只更新发生变化的文档。

    2. 增加 metadata 过滤

    为文档添加 department、version、effective_date、permission 等字段。检索前先根据用户权限和制度版本过滤,避免召回无权访问或已失效的资料。

    3. 评估检索质量

    至少准备一组“问题—标准答案—预期来源”的测试集,并分别观察:

    • Recall@K:正确资料有没有进入前 K 个结果;
    • 上下文相关性:召回内容是否真正回答问题;
    • 答案忠实度:回答是否能被上下文支持;
    • 无答案拒答率:资料缺失时是否停止猜测;
    • 延迟、Token 和费用。

    只评价最终回答,会把“检索错误”和“生成错误”混在一起。

    4. 防御 Prompt Injection

    外部文档同样是不可信输入。文档中可能出现“忽略系统指令”“发送密钥”等恶意文本。应采取以下措施:

    • 明确告诉模型:检索内容是资料,不是可执行指令;
    • 工具采用最小权限,读写工具分离;
    • 敏感操作必须经过参数校验和人工确认;
    • 不把 API Key、系统提示词和私密上下文暴露给模型工具;
    • 对上传文件做类型、大小、权限和内容检查。

    5. 加入可观测性

    RAG 是多阶段系统。线上出现错误时,需要看到问题、召回文本、提示词、模型输出、耗时和 Token 使用情况。可以使用 LangSmith 或企业已有的链路追踪系统。

    6. 控制上下文和成本

    k 越大不一定越好。召回过多内容会增加费用,也可能让无关信息干扰模型。可以组合以下方法:

    • 相似度阈值;
    • MMR,兼顾相关性与多样性;
    • 混合检索,结合关键词与向量搜索;
    • Reranker,对初步召回结果重新排序;
    • Parent Document Retrieval,兼顾精确召回和完整上下文。

    十一、常见报错与避坑指南

    1. ModuleNotFoundError

    新版本把不少集成拆到了独立包中:

    # 聊天模型与 Embedding
    from langchain_openai import ChatOpenAI, OpenAIEmbeddings

    # 文本切分
    from langchain_text_splitters import RecursiveCharacterTextSplitter

    # 核心文档对象
    from langchain_core.documents import Document

    看到多年以前的 from langchain.xxx import … 教程时,应先核对当前官方文档。

    2. 旧教程中的 Chain 已弃用或不再推荐

    一些旧文章大量使用 LLMChain、RetrievalQA 或 ConversationalRetrievalChain。理解它们没有坏处,但新项目更适合使用 Runnable/LCEL、显式 RAG 流程或当前的 create_agent API。

    3. 聊天正常,Embedding 报错

    常见原因是兼容服务不支持 Embedding,或者填写了聊天模型名称。检查:

    • Base URL 是否正确;
    • Embedding 接口是否存在;
    • OPENAI_EMBEDDING_MODEL 是否是向量模型;
    • API Key 是否有该模型权限。

    4. 中文检索结果不准

    依次检查:

  • 向量模型是否支持中文;
  • PDF 是否真的提取出了文本;
  • 文本块是否过长或过短;
  • 标题、页码等 metadata 是否保留;
  • 查询与文档是否需要关键词 + 向量混合检索;
  • 是否需要对召回结果进行重排。
  • 扫描版 PDF 只有图片,普通 PDF 解析器无法直接获得文字,需要先 OCR。

    5. 回答里出现资料没有的数字

    可从四个层面处理:

    • 提示词明确要求“无依据就拒答”;
    • 展示程序生成的真实引用来源;
    • 对关键字段使用结构化输出与规则校验;
    • 在高风险场景引入人工复核。

    不要把“temperature 设置为 0”当成事实正确性的保证。


    十二、学习路线建议

    推荐按下面的顺序学习:

  • Python、HTTP API、环境变量和异步基础;
  • Model、Message、Prompt、Output Parser;
  • Runnable 与 LCEL;
  • Document、Splitter、Embedding、Vector Store、Retriever;
  • 2-Step RAG 与评估;
  • Tool Calling 与 create_agent;
  • Memory、Streaming、Structured Output;
  • LangGraph 的状态、节点、边、持久化与人工介入;
  • LangSmith 或其他可观测与评估方案。
  • 先把确定性的 RAG 流程做正确,再学习 Agent,理解会顺畅很多。


    总结

    本文完成了从概念到代码的完整闭环:

    • LangChain 是大模型应用的组件与编排框架,不是模型本身;
    • Runnable/LCEL 提供统一的调用和组合方式;
    • RAG 的核心链路是“加载、切分、向量化、检索、生成”;
    • metadata、引用来源、拒答策略和评估与模型调用同样重要;
    • 固定问答优先使用 2-Step RAG,多工具动态决策再考虑 Agent;
    • Demo 上线前必须补齐持久化、权限、评估、安全和可观测性。

    真正高质量的 AI 应用,不是“接通 API 就结束”,而是让每个答案有依据、每次失败可定位、每项成本可衡量。

    参考资料

    • LangChain 官方概览:https://docs.langchain.com/oss/python/langchain/overview
    • LangChain Retrieval:https://docs.langchain.com/oss/python/langchain/retrieval
    • Semantic Search 教程:https://docs.langchain.com/oss/python/langchain/knowledge-base
    • LangChain Python API Reference:https://reference.langchain.com/python/
    赞(0)
    未经允许不得转载:171主机测评 » LangChain 从入门到实战:从 0 构建一个可追溯的本地知识库 RAG 问答系统
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址