LangChain 进阶用法:从入门到精通

open-source进阶15 分钟阅读2026/8/13

上周我接到一个需求:给公司内部构建一个能读取我们技术文档、记住对话上下文、还能查询线上日志系统的智能助手。我一开始直接用 OpenAI 的原生 API 硬写,结果三天下来代码已经乱成了一锅粥——提示词拼接到处都是、对话历史管理全靠手动塞数组、文档解析和检索逻辑跟业务代码耦合得一塌糊涂。就在我准备重构的时候,同事推荐了 LangChain。

说实话,一开始我是抗拒的。这框架概念多得吓人——Chain、Agent、Memory、Tool、Retriever……光看文档就头大。但硬着头皮用下来,发现它确实解决了我手头的核心问题。这篇我就把自己从踩坑到真正用顺手的整个过程写下来,帮你避开我走过的弯路。

环境搭建:别小看这一步

我第一个坑就踩在环境上。LangChain 更新极快,很多半年前的教程代码现在已经跑不通了。我的建议是:一定要锁定版本

pip install langchain==0.1.20 langchain-openai==0.1.6 langchain-community==0.1.8

为什么要分三个包?LangChain 在 2023 年底做了拆分,核心逻辑放 langchain,OpenAI 相关放 langchain-openai,第三方集成放 langchain-community。很多老教程还在用 from langchain.llms import OpenAI,这已经过时了,现在应该用 langchain_openai 包。

然后是 API Key 的配置。如果你用代理:

import os
os.environ["OPENAI_API_KEY"] = "sk-xxx"
os.environ["OPENAI_API_BASE"] = "https://your-proxy-url.com/v1"

别把 API Key 硬编码在代码里,这是新手最常见的坏习惯。用环境变量或者 .env 文件来管理。

第一个突破:提示词模板

我之前最头疼的问题之一就是提示词管理。到处都是 f-string 拼接,改一个提示词要在代码里翻半天。LangChain 的 PromptTemplate 解决了这个问题:

from langchain_core.prompts import ChatPromptTemplate

# 构建一个可复用的提示词模板
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个专业的{role},用简洁准确的中文回答问题。"),
    ("human", "{question}")
])

# 填充变量生成完整的提示词
formatted = prompt.invoke({"role": "运维工程师", "question": "如何排查内存泄漏?"})
print(formatted)

看起来简单,但当你有十几个提示词需要管理时,这种模板化的好处就体现出来了——变量名清晰、可复用、可测试。我后来把所有提示词都抽成了模板文件,维护起来舒服多了。

还有一个我后来才发现的好东西:ChatPromptTemplate.from_messages() 支持直接传入对话历史:

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个有帮助的助手。"),
    MessagesPlaceholder("chat_history"),  # 这里会插入对话历史
    ("human", "{question}")
])

这个 MessagesPlaceholder 在后面做 Memory 集成时非常关键,先记住它。

LCEL:LangChain 的灵魂

这是我最想讲的部分,也是我踩坑最久的地方。

LCEL(LangChain Expression Language)是 LangChain 的核心语法,用管道符 | 把组件串联起来。一开始我看到这种写法是完全懵的:

chain = prompt | model | output_parser

什么意思?其实就是数据流:prompt 的输出传给 model,model 的输出传给 output_parser。跟 Linux 的管道是一个思路。

让我用一个完整的例子说明:

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

# 1. 创建模型
model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

# 2. 创建提示词模板
prompt = ChatPromptTemplate.from_template(
    "用三句话解释以下技术概念:{concept}"
)

# 3. 创建输出解析器
output_parser = StrOutputParser()

# 4. 用 LCEL 组装链
chain = prompt | model | output_parser

# 5. 调用
result = chain.invoke({"concept": "向量数据库"})
print(result)

输出类似:

向量数据库是一种专门用于存储和检索高维向量数据的数据库系统。它通过计算向量之间的相似度(如余弦相似度)来实现快速检索,广泛应用于语义搜索和推荐系统。与传统关系型数据库不同,向量数据库的查询核心是"找最相似的"而非"找完全匹配的"。

LCEL 的强大之处在于,每个组件都是可独立运行和测试的。你可以单独测试 prompt.invoke(),单独测试 model.invoke(),出了问题能快速定位是哪个环节的锅。

我之前犯过一个错:在 prompt 模板里写了一个变量名,invoke 时传了另一个名字,结果报错。如果用 LCEL,这种错误在调用时就会立刻暴露,而不是像以前拼字符串那样,问题一直隐藏到模型返回莫名其妙的结果才发现。

文档加载与拆分:RAG 的地基

回到我最初的需求——让模型能读取我们的技术文档。这就涉及文档加载和拆分了。

先说加载。LangChain 支持几十种文档格式,我常用的几个:

from langchain_community.document_loaders import TextLoader, PyPDFLoader, UnstructuredMarkdownLoader

# 加载文本文件
text_loader = TextLoader("./docs/deployment-guide.txt")
text_docs = text_loader.load()

# 加载 PDF
pdf_loader = PyPDFLoader("./docs/api-reference.pdf")
pdf_docs = pdf_loader.load()

# 加载 Markdown
md_loader = UnstructuredMarkdownLoader("./docs/architecture.md")
md_docs = md_loader.load()

每个加载器返回的都是 Document 对象列表,每个对象有 page_content(文本内容)和 metadata(元数据,如文件名、页码等)。

然后是拆分,这一步比你想的重要得多。我一开始直接把整篇文档塞给模型,结果 token 直接爆了,而且检索精度也很差——一大段文本里只有一小部分跟问题相关,但模型被无关信息干扰严重。

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # 每块最多 500 字符
    chunk_overlap=50,    # 相邻块重叠 50 字符,避免语义断裂
    separators=["\n\n", "\n", "。", ",", " "]  # 按优先级尝试分割
)

chunks = splitter.split_documents(pdf_docs)
print(f"原文档 {len(pdf_docs)} 页,拆分后 {len(chunks)} 块")

RecursiveCharacterTextSplitter 是我最推荐的拆分器。它会按 separators 列表的优先级尝试分割——先按双换行(段落),再按单换行,再按句号……这样能尽量保持语义完整。chunk_overlap 的重叠设计也很巧妙,避免关键信息正好被切断。

我实际测试发现,chunk_size 设在 300-800 之间效果最好,太小了上下文不够,太大了检索噪声多。具体数值要看你的文档类型和嵌入模型,需要实验调整。

记忆模块:让对话有上下文

我之前手动管理对话历史的方式简直原始——维护一个列表,每次调用模型前把历史拼进去,超过 token 限制就截断。LangChain 的 Memory 模块把这个过程标准化了:

from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables import RunnableWithMessageHistory

# 用字典存储不同会话的历史
history_store = {}

def get_session_history(session_id: str):
    if session_id not in history_store:
        history_store[session_id] = InMemoryChatMessageHistory()
    return history_store[session_id]

# 带历史记录的链
chain_with_history = RunnableWithMessageHistory(
    prompt | model | output_parser,
    get_session_history,
    input_messages_key="question",
    history_messages_key="chat_history"
)

# 第一次对话
response1 = chain_with_history.invoke(
    {"question": "我们公司的日志系统叫什么?"},
    config={"configurable": {"session_id": "user-001"}}
)

# 第二次对话,模型能记住之前的上下文
response2 = chain_with_history.invoke(
    {"question": "怎么用它查昨天的错误日志?"},  # "它"指代上文提到的日志系统
    config={"configurable": {"session_id": "user-001"}}
)

这里有个关键点:session_id 让你能区分不同用户/会话的历史。在生产环境中,history_store 应该换成 Redis 或数据库,InMemoryChatMessageHistory 重启就没了。

实战组合:一个能查文档的助手

把上面的东西组合起来,我已经能构建一个基本可用的文档问答助手了:

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader

# 1. 加载并拆分文档
loader = TextLoader("./docs/tech-faq.txt")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(docs)

# 2. 创建向量数据库
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(chunks, embeddings, persist_directory="./chroma_db")
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# 3. 构建问答链
prompt = ChatPromptTemplate.from_template("""
基于以下文档内容回答问题。如果文档中没有相关信息,请说"我没有找到相关信息"。

文档内容:
{context}

问题:{question}

回答:
""")

model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# LCEL 组装:先并行获取检索结果和原始问题,再传入 prompt
chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

# 4. 使用
answer = chain.invoke("部署流程是什么?")
print(answer)

这个链的执行流程:用户问题 → retriever 检索相关文档块 → 格式化为字符串 → 填入 prompt → 模型生成回答 → 解析输出。整个数据流清晰可见。

实用建议与诚实评估

用了几个月 LangChain,说说我的真实感受:

值得用的地方:

  • LCEL 的管道语法确实让数据流清晰了很多,调试方便
  • 文档加载和拆分这套工具省了大量重复代码
  • 向量数据库的统一接口让你切换底层实现时改动很小
  • 社区活跃,遇到问题基本能搜到解决方案

需要注意的坑:

  • 版本迭代太快,三个月前的代码可能就跑不通了。一定要锁定版本,升级前仔细看 Changelog
  • 框架抽象层有时候太厚,出了问题排查链路很长。建议先搞懂 LCEL 的数据流,别把 Chain 当黑盒
  • 对于简单场景(比如只是调个 API 加个提示词),用 LangChain 反而增加了复杂度,不如直接写
  • Memory 模块在生产环境需要自己接持久化存储,内置的内存方案不能直接上生产

我的建议: 先从 LCEL + PromptTemplate + 一个文档加载器开始,把基础跑通。别一上来就搞 Agent 和 Tool,那些概念虽然酷,但复杂度陡增。等你把链的数据流搞明白了,再逐步加 Memory、Retriever,最后才是 Agent。循序渐进,才不会从入门到放弃。

相关 Agent

M

Meta AI

Meta AI 是一个开源AI平台,用于研究和开发先进的语言模型及生成式AI工具。

了解更多 →