LangChain 是什么
LangChain 是目前生态最完整的 LLM 应用开发框架。它解决的核心问题是:让同一套应用代码能够组合不同模型、工具与数据源,并具备流式、异步、可观测等生产能力。
直接调用模型 API 写应用并不难,难的是工程化:不同厂商接口不统一、组合调用需要流式与重试、生产需要追踪调试、提示词与工具需要版本管理。LangChain 用三层价值回应这些问题:
统一抽象
把 ChatModel、Embeddings、VectorStore、Tool 等抽象成统一接口。切换厂商只改一行初始化代码,业务逻辑零改动。
组合能力
LCEL(LangChain Expression Language)用管道表达式把组件串成链,自动获得流式、并行、异步、重试与 fallback 能力。
生态集成
上百个官方与社区包接入各类模型厂商、向量库、数据源,配合 LangSmith 追踪与 LangGraph Platform 部署形成完整工具链。
包结构:谁依赖谁
1.x 的包结构是理解一切 import 报错的钥匙。记住一条主线:langchain-core 定义抽象,langchain 提供智能体构建块,集成包实现抽象,老功能住在 langchain-classic。
| 包名 | 职责 | 典型 import |
|---|---|---|
| langchain-core | 所有抽象的源头:Runnable、消息、提示词、BaseChatModel、BaseTool | from langchain_core.prompts import ChatPromptTemplate |
| langchain | 1.x 智能体构建块:create_agent、middleware、工具注入 | from langchain.agents import create_agent |
| langchain-{provider} | 厂商官方集成:openai、anthropic、google、ollama、deepseek 等 | from langchain_openai import ChatOpenAI |
| langchain-community | 社区维护的海量集成:向量库、加载器、旧工具 | from langchain_community.vectorstores import FAISS |
| langchain-classic | 0.x 时代的链、检索器、索引 API,仅供迁移期使用 | from langchain_classic.chains import LLMChain |
| langchain-text-splitters | 文档切分器 | from langchain_text_splitters import RecursiveCharacterTextSplitter |
| langgraph | 状态图编排,create_agent 的运行时底座 | from langgraph.graph import StateGraph |
LangChain 不是对 OpenAI SDK 的封装替代,而是跨厂商的抽象层与组合层。如果你的应用只用一个厂商、不需要组合与观测,直接用厂商 SDK 也完全合理;一旦需要 RAG、智能体、多厂商切换或生产追踪,抽象层的价值才会显现。这是工程选型,不是信仰问题。
模型接入与消息体系
所有 LLM 交互的起点:用统一接口初始化模型,理解消息类型与 content blocks,掌握调用、流式与用量元数据。
init_chat_model:一行代码切换厂商
1.x 推荐用 init_chat_model 初始化模型。"厂商:模型名" 的字符串格式会自动解析为对应的集成类,切换厂商时只改这一个字符串:
from langchain.chat_models import init_chat_model
# 字符串格式:"provider:model"
llm = init_chat_model("openai:gpt-4o-mini")
llm = init_chat_model("anthropic:claude-sonnet-4-5")
llm = init_chat_model("ollama:qwen2.5:7b") # 本地模型
# 也可以显式传参,适合统一配置温度等选项
llm = init_chat_model(
model="gpt-4o-mini",
model_provider="openai",
temperature=0,
max_tokens=2048,
)
# 运行时动态切换:configurable 字段
configurable_llm = init_chat_model(
temperature=0,
configurable_fields=("model", "model_provider", "temperature"),
)
fast = configurable_llm.with_config(
config={"configurable": {"model": "gpt-4o-mini", "model_provider": "openai"}}
)
smart = configurable_llm.with_config(
config={"configurable": {"model": "claude-sonnet-4-5", "model_provider": "anthropic"}}
)
等价的厂商显式写法在所有版本中都能用,老教程里更常见:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
消息类型:对话的最小单元
LangChain 用四种消息类型还原完整对话,这是所有链、智能体与记忆机制的公共货币:
| 类型 | 角色 | 典型来源 |
|---|---|---|
| SystemMessage | 系统指令,设定行为边界与人设 | 开发者硬编码,通常不进入历史 |
| HumanMessage | 用户输入 | 终端用户或上游程序 |
| AIMessage | 模型输出,可能携带 tool_calls 与推理内容 | 模型生成 |
| ToolMessage | 工具执行结果,回传给模型 | 工具节点执行后写入 |
from langchain.messages import HumanMessage, SystemMessage
resp = llm.invoke([
SystemMessage("你是一个严谨的技术文档助手,回答必须附带出处。"),
HumanMessage("什么是 Runnable?"),
])
print(resp.content) # 文本内容
print(resp.tool_calls) # 若模型决定调用工具,这里会有调用请求
print(resp.usage_metadata) # {'input_tokens': 36, 'output_tokens': 128, 'total_tokens': 164}
print(resp.response_metadata) # 原始厂商返回信息,排查问题时用
Content Blocks:多模态与 1.x 的标准内容块
消息的 content 不再只是字符串,而是一个块列表。1.x 引入了跨厂商统一的 content blocks 标准,文本、图片、音频、推理过程、工具调用都有统一的块类型:
from langchain.messages import HumanMessage
# 多模态输入:文本 + 图片混合成块列表
msg = HumanMessage(content=[
{"type": "text", "text": "这张架构图里有哪些组件?"},
{"type": "image", "source_type": "url", "url": "https://example.com/arch.png"},
])
resp = llm.invoke([msg])
# 1.x 统一的 content_blocks 视图(推荐在应用层消费)
for block in resp.content_blocks:
if block["type"] == "text":
print(block["text"])
elif block["type"] == "reasoning": # 推理模型的思考过程
print("[思考]", block["reasoning_text"][:50])
应用层尽量消费 content_blocks 而不是裸 content:前者是 1.x 的跨厂商标准格式,厂商差异已被抹平。旧代码里的字符串 content 仍然兼容,但新项目建议统一走标准块。
调用、流式与批量
所有 LangChain 组件共享同一组标准方法,这是后面 LCEL 能自由组合的前提:
# 单次调用
resp = llm.invoke([HumanMessage("用一句话解释向量检索")])
# 流式输出:逐 token 返回
for chunk in llm.stream([HumanMessage("写一首关于编译器的俳句")]):
print(chunk.text(), end="", flush=True)
# 批量调用:自动并发
results = llm.batch([
[HumanMessage("总结这段代码")],
[HumanMessage("总结这段文档")],
])
# 异步版本:高并发场景直接换前缀 a
# await llm.ainvoke(...) / async for chunk in llm.astream(...)
上面批量示例中的并发由 LangChain 自动调度。注意 batch 的默认并发数可用 max_concurrency 参数控制,免费额度的 API Key 尤其要设置,避免触发限流。
提示词工程
提示词是 LLM 应用里单位成本最高的杠杆。ChatPromptTemplate 把提示词变成带 schema 的可复用组件,支持变量、历史注入与少样本示例。
模板即组件
把提示词写成模板而不是硬编码字符串,有三个直接收益:变量强制声明、可作为链的一环参与流式、便于版本管理与评测。
from langchain_core.prompts import ChatPromptTemplate
from langchain.messages import HumanMessage
prompt = ChatPromptTemplate.from_messages([
("system", "你是{name}领域的高级工程师,回答简洁、可执行。"),
("human", "{question}"),
])
# 模板本身是 Runnable:可以单独调用、校验变量
msgs = prompt.invoke({"name": "数据库", "question": "如何定位慢查询?"})
print(msgs) # ChatPromptValue,含格式化好的消息列表
# 变量缺失会直接报错,而不是把 {question} 原样发给模型
# prompt.invoke({"name": "数据库"}) --> pydantic 校验错误
MessagesPlaceholder:把历史注入模板
多轮对话的关键是给模板留一个"历史插槽"。MessagesPlaceholder 就是这个插槽:
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "你是技术面试官,正在面试一位{level}工程师。"),
MessagesPlaceholder("history"), # 历史消息插槽
("human", "{question}"),
])
chain = prompt | llm
history = [
("ai", "请先自我介绍。"),
("human", "我有五年后端经验,主要做交易系统。"),
]
resp = chain.invoke({
"level": "资深",
"history": history,
"question": "谈谈你处理过的最复杂的线上故障。",
})
0.x 时代的 ConversationBufferMemory 等记忆类已废弃。现代做法只有两条:链场景在外层管理消息列表并填入 MessagesPlaceholder;智能体场景用 LangGraph checkpointer 持久化(第二章详解)。记忆本质是状态管理问题,不是组件问题。
少样本提示(Few-shot)
对格式敏感的任务(分类、抽取、风格模仿),给几个示例比写一段要求有效得多。FewShotChatMessagePromptTemplate 让示例集成为模板的一部分:
from langchain_core.prompts import (
ChatPromptTemplate, FewShotChatMessagePromptTemplate
)
examples = [
{"input": "这个退款流程太绕了", "output": "负面 | 体验"},
{"input": "客服响应很快,赞", "output": "正面 | 效率"},
{"input": "价格还行", "output": "中性 | 价格"},
]
example_prompt = ChatPromptTemplate.from_messages([
("human", "{input}"),
("ai", "{output}"),
])
few_shot = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples,
)
final_prompt = ChatPromptTemplate.from_messages([
("system", "你是客服评论分析器,按 情感 | 主题 格式输出。"),
few_shot, # 展开为多轮 human/ai 消息
("human", "{input}"),
])
chain = final_prompt | llm
print(chain.invoke({"input": "物流等了半个月"}).content)
# 输出类似:负面 | 物流
提示词工程质量清单
- 系统提示词放约束与角色,用户消息放具体内容。不要把任务参数混进 system。
- 输出格式要求交给结构化输出,不要靠提示词乞求 JSON。下一节的
with_structured_output才是正解。 - 变量名即文档。用
{document_text}而不是{x},模板可读性决定维护成本。 - 示例覆盖边界情况。少样本至少包含一个"难例",模型对边界的判断力主要来自示例。
- 把提示词当代码管理。进版本库、写测试(固定输入断言输出结构),提示词回归是线上事故高发区。
结构化输出
让模型输出直接成为可被程序消费的对象。这是从“聊天玩具”到“应用组件”的分水岭:下游代码拿到的是 Pydantic 实例,而不是一段要二次解析的文本。
用 Pydantic 定义输出契约,with_structured_output 负责让模型遵守契约并自动解析:
from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model
llm = init_chat_model("openai:gpt-4o-mini", temperature=0)
class CodeReview(BaseModel):
"""代码评审结果的结构化契约"""
severity: str = Field(description="严重程度:critical / warning / info")
issues: list[str] = Field(description="发现的问题列表,每条一句话")
suggestion: str = Field(description="最重要的修复建议")
confidence: float = Field(ge=0, le=1, description="置信度")
reviewer = llm.with_structured_output(CodeReview)
result = reviewer.invoke("""
def div(a, b):
return a / b
""")
# result 就是 CodeReview 实例
print(result.severity) # 'critical'
print(result.issues) # ['未处理除零异常', ...]
print(result.confidence) # 0.9
三件让结构化输出可靠的事,全部体现在上面的代码里:
- Field description 是写给模型看的提示词。每个字段都写清楚语义与取值范围,模型遵守契约的准确率会显著提升。
- 用类型系统表达约束。
Literal限定枚举、ge/le限定数值范围、Optional标记可空,越界输出会在解析层被拦下。 - 嵌套结构直接映射业务对象。支持任意深度的 Pydantic 嵌套,一次抽取完整业务实体。
三种底层方法
| 方法 | 原理 | 适用场景 |
|---|---|---|
| tool_calling(默认) | 借用工具调用机制携带 schema | 绝大多数现代模型的默认最优解 |
| json_schema | 厂商原生 JSON Schema 约束解码 | 原生支持严格模式的模型 |
| json_mode | 只保证输出合法 JSON,不保证 schema | 不支持前两者的模型,需在提示词中自述格式 |
结构化输出不是 100% 可靠。生产代码必须处理解析失败:要么 with_retry() 重试,要么构造一个带 with_fallbacks 的降级链(如换更便宜的模型重新抽取,或返回带默认值的空对象并打日志)。
LCEL 与 Runnable 体系
LCEL(LangChain Expression Language)用管道表达式组合组件。任何实现了 Runnable 协议的对象都可以放进管道,组合后的链自动获得流式、异步、批量、重试与可观测能力。
最小可用的链
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "你是资深 DevOps 工程师。"),
("human", "{question}"),
])
# | 运算符 = RunnableSequence
chain = prompt | llm | StrOutputParser()
chain.invoke({"question": "如何估算 Kubernetes 资源配额?"})
chain.stream({"question": "..."}) # 逐 token 流式
chain.batch([{"question": "..."}]) # 批量并发
# await chain.ainvoke({...}) # 异步版本同样存在
这段代码背后是 Runnable 协议的核心承诺:组合不损失能力。单独的模型支持流式,串上提示词与解析器后依然支持流式,token 会从模型一路透传到你的 for 循环。
用并行与透传组织数据流
经典 RAG 链是 LCEL 数据流的教科书案例:RunnableParallel 同时准备上下文与问题,RunnablePassthrough 把输入原样传递:
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
def format_docs(docs):
return "\n\n".join(d.page_content for d in docs)
# 字典字面量会被自动转换为 RunnableParallel
rag_chain = (
{
"context": retriever | format_docs, # 分支一:检索 + 格式化
"question": RunnablePassthrough(), # 分支二:原样透传
}
| prompt
| llm
| StrOutputParser()
)
rag_chain.invoke("什么是 LCEL?")
# .assign() 在不吞掉上游输出的前提下追加字段
chain_with_source = rag_chain.assign(
sources=lambda x: [d.metadata["source"] for d in retriever.invoke(x)]
)
Runnable 家族速查
| 组件 | 作用 | 典型用法 |
|---|---|---|
| RunnableLambda | 把普通函数变成 Runnable | RunnableLambda(lambda x: x["q"]) |
| RunnableParallel | 并行执行多个分支,结果合并为字典 | 字典字面量语法自动转换 |
| RunnablePassthrough | 透传输入,常配合 assign 追加字段 | 见上例 question 分支 |
| RunnableBranch | 按条件路由到不同链 | 路由器:闲聊走 A,技术问题走 B |
| RunnableWithFallbacks | 主链异常时降级到备用链 | chain.with_fallbacks([cheap_chain]) |
| with_retry() | 给任意 Runnable 加重试 | llm.with_retry(stop_after_attempt=3) |
| with_config() | 附加标签、回调、并发限制 | 给链打 LangSmith 标签 |
from langchain_core.runnables import RunnableBranch
# 条件路由:RunnableBranch 是 (condition, chain) 列表 + 默认分支
router = RunnableBranch(
(lambda x: "退款" in x["question"], refund_chain),
(lambda x: "技术" in x["question"], tech_chain),
general_chain, # 兜底
)
# 重试与降级:让链具备生产韧性
robust_chain = (prompt | llm | parser).with_retry(
stop_after_attempt=3, retry_if_exception_type=(TimeoutError,)
).with_fallbacks([fallback_chain])
# 查看链的输入输出 schema(调试利器)
print(rag_chain.input_schema.model_json_schema())
LCEL 擅长单向数据流:输入到输出一条路走到底。一旦需要循环(智能体的“调用工具后再回到模型”)、需要持久化状态、需要人工审批打断,就该切换到 LangGraph 的状态图模型。两者不是竞争关系:LCEL 的链可以直接作为 LangGraph 的节点,无缝过渡。
RAG 基础组件
LangChain 把 RAG 拆成五个标准环节:加载、切分、向量化、存储、检索。本章建立组件地图与最小可用管道,深度优化留给第三章。
加载与切分
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 加载:统一产出 Document(page_content, metadata)
docs = PyPDFLoader("handbook.pdf").load()
pages = WebBaseLoader("https://docs.langchain.com").load()
# 切分:递归分隔符,优先按段落、句子断开
splitter = RecursiveCharacterTextSplitter(
chunk_size=800, # 每块目标字符数
chunk_overlap=120, # 块间重叠,保住被切断的语义
separators=["\n\n", "\n", "。", ",", " ", ""], # 中文语料建议加中文标点
)
chunks = splitter.split_documents(docs)
print(len(chunks), chunks[0].metadata) # metadata 保留页码、来源等信息
加载与切分阶段写入的 metadata(来源、页码、章节、时间、权限标签)在检索期全部可以变成过滤条件与引用出处。宁可多写,不要事后补。
向量化与存储
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# 一行完成 向量化 + 入库
vectorstore = FAISS.from_documents(chunks, embeddings)
# 持久化与加载
vectorstore.save_local("faiss_index")
reloaded = FAISS.load_local(
"faiss_index", embeddings, allow_dangerous_deserialization=True
)
# 相似度检索
hits = vectorstore.similarity_search("如何配置 checkpointer?", k=4)
# MMR 检索:兼顾相关性与多样性,减少返回内容重复
hits = vectorstore.max_marginal_relevance_search(
"如何配置 checkpointer?", k=4, fetch_k=20
)
检索器:检索的统一接口
链与智能体消费的不是向量库,而是 Retriever 接口(输入 query,输出 Document 列表)。向量库通过 as_retriever() 转换,这让 BM25、混合检索、多查询检索都能以同一形态进入链:
retriever = vectorstore.as_retriever(
search_type="mmr", # "similarity" | "mmr" | "similarity_score_threshold"
search_kwargs={"k": 6, "fetch_k": 24},
)
# retriever 本身是 Runnable,可直接进入 LCEL 管道
docs = retriever.invoke("如何配置 checkpointer?")
# 接回上一节的 RAG 链即可运行
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt | llm | StrOutputParser()
)
MultiQueryRetriever(多查询扩展)、ContextualCompressionRetriever(压缩)、ParentDocumentRetriever(父子块)等高级检索器在 1.x 中位于 langchain_classic.retrievers,仍可正常使用,第三章会逐一展开。向量库选型(FAISS / Chroma / Qdrant / pgvector)也留到第三章对比。
工具体系
工具(Tool)是模型伸向外部世界的手。高质量的工具定义直接决定智能体上限:模型看到的只有工具名、描述与参数 schema。
@tool 装饰器:最小成本定义工具
from langchain.tools import tool
@tool
def get_weather(city: str, unit: str = "celsius") -> str:
"""查询指定城市的实时天气。
Args:
city: 城市名称,支持中文与拼音,如 "北京"、"shanghai"。
unit: 温度单位,celsius 或 fahrenheit,默认 celsius。
"""
# 真实实现:调用天气 API
return f"{city}今天晴,26°C"
# 框架自动生成了 schema,检查一下
print(get_weather.name) # 'get_weather'
print(get_weather.description) # docstring 的第一段
print(get_weather.args_schema.model_json_schema())
工具定义的三个要素,全部来自函数签名:函数名即工具名、docstring 即描述、类型注解即参数 schema。写给模型看的部分(名称、描述、参数说明)比实现本身更重要。
复杂参数:用 Pydantic 声明 args_schema
from pydantic import BaseModel, Field
from langchain.tools import tool
class SearchArgs(BaseModel):
query: str = Field(description="搜索关键词")
date_range: str = Field(
default="all",
description="时间范围:day / week / month / year / all",
)
max_results: int = Field(default=5, ge=1, le=20, description="返回条数上限")
@tool(args_schema=SearchArgs)
def web_search(**kwargs) -> str:
"""按关键词搜索互联网并返回结果摘要。"""
...
绑定与执行:bind_tools 与 ToolNode
定义工具只是第一步,还要让模型“看得见”工具并在决定调用后“执行”它:
from langchain.messages import HumanMessage
# 1. 绑定:把工具 schema 附加到模型
llm_with_tools = llm.bind_tools([get_weather, web_search])
# 2. 模型决定调用哪个工具、传什么参数
resp = llm_with_tools.invoke([HumanMessage("北京和上海今天多少度?")])
for tc in resp.tool_calls:
print(tc["name"], tc["args"])
# get_weather {'city': '北京', 'unit': 'celsius'}
# get_weather {'city': '上海', 'unit': 'celsius'}
# 3. 执行:直接调用工具对象
result = get_weather.invoke(resp.tool_calls[0]["args"])
# 4. 在 LangGraph 中,ToolNode 负责批量执行并回填 ToolMessage
from langgraph.prebuilt import ToolNode
tool_node = ToolNode([get_weather, web_search])
| 控制参数 | 含义 | 说明 |
|---|---|---|
| tool_choice="auto" | 模型自行决定是否调用 | 默认行为,智能体场景标准配置 |
| tool_choice="any" | 强制调用至少一个工具 | 路由、抽取等必走工具的场景 |
| tool_choice="工具名" | 强制调用指定工具 | 等价于结构化输出的工具调用法 |
隐藏参数:InjectedToolArg 与 InjectedState
有些参数不该暴露给模型(用户 ID、会话状态),却需要传给工具执行。InjectedToolArg 把参数标记为“程序注入,模型不可见”:
from typing import Annotated
from langchain_core.tools import InjectedToolArg
from langgraph.prebuilt import InjectedState
@tool
def query_orders(
state: Annotated[dict, InjectedState()], # 注入图状态,模型不可见
user_id: Annotated[str, InjectedToolArg], # 注入运行时参数,模型不可见
keyword: str, # 模型可见、模型填写
) -> str:
"""按关键词查询当前用户的订单。"""
return f"用户 {user_id} 的订单:[{keyword}] 相关共 3 条"
# 模型只看到 keyword 参数;user_id 与 state 由运行时注入
模型选错工具的根因大多不是模型能力,而是工具描述写得含糊或职责重叠。两条纪律:一,每个工具描述必须写清“什么时候该用我、什么时候不该用我”;二,两个工具的边界如果人都分不清,模型更分不清,先合并或重命名。
create_agent 与中间件
LangChain 1.x 构建智能体的标准方式。它把“模型、工具、循环”打包成一个高层 API,并用中间件体系打开定制空间。深度智能体内容见第五章,本章建立第一印象。
from langchain.agents import create_agent
agent = create_agent(
model="openai:gpt-4o-mini", # 直接传模型字符串或模型实例
tools=[get_weather, web_search],
system_prompt="你是天气与资讯助手,回答时引用数据来源。",
)
result = agent.invoke({
"messages": [{"role": "user", "content": "北京今天适合跑步吗?"}]
})
print(result["messages"][-1].content)
四行代码背后的循环:模型收到消息,决定是否调用工具;若有工具调用则执行并把结果回填;如此往复,直到模型不再请求工具,输出最终回答。因为构建在 LangGraph 之上,持久化、流式、人机协同都是内置能力:
from langgraph.checkpoint.memory import MemorySaver
# 持久化:传入 checkpointer 即获得多轮记忆与断点恢复
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_weather],
checkpointer=MemorySaver(),
)
config = {"configurable": {"thread_id": "user-42"}}
agent.invoke({"messages": [{"role": "user", "content": "我叫林可"}]}, config)
agent.invoke({"messages": [{"role": "user", "content": "我叫什么名字?"}]}, config)
# 第二次调用能答出“林可”,记忆由 checkpointer 维护
中间件:智能体的拦截层
中间件是 1.x 智能体体系的关键抽象:它在智能体执行周期的每个阶段提供挂钩,实现上下文工程、护栏与审计,而不用改写主循环:
| 挂钩 | 时机 | 典型用途 |
|---|---|---|
| before_agent | 智能体开始前 | 加载记忆、校验输入 |
| before_model | 每次调用模型前 | 动态改写提示词、裁剪历史 |
| wrap_model_call | 包住模型调用 | 换模型、改工具集、拦截请求响应 |
| wrap_tool_call | 包住工具执行 | 审计日志、参数过滤、错误处理 |
| after_model | 每次模型返回后 | 输出校验、护栏拦截 |
| after_agent | 智能体结束后 | 保存结果、清理资源 |
官方预置了三个最常用的中间件,开箱即用:
from langchain.agents import create_agent
from langchain.agents.middleware import (
PIIMiddleware, # 敏感信息脱敏或拦截
SummarizationMiddleware, # 历史过长时自动摘要压缩
HumanInTheLoopMiddleware, # 敏感工具调用前暂停等人审批
)
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[read_email, send_email],
middleware=[
PIIMiddleware("email", strategy="redact", apply_to_input=True),
SummarizationMiddleware(
model="openai:gpt-4o-mini",
trigger={"tokens": 4000}, # 超过 4k token 触发摘要
),
HumanInTheLoopMiddleware(
interrupt_on={"send_email": {"allowed_decisions": ["approve", "edit", "reject"]}},
),
],
)
老教程中的 AgentExecutor + create_tool_calling_agent 与 langgraph.prebuilt.create_react_agent 均已被 create_agent 取代(后者在 LangGraph 1.x 中已弃用)。差异细节与迁移对照表见本章第 10 节,智能体全景见第五章。
可观测性与部署
LLM 应用的调试难度来自非确定性:同一个输入在不同时刻可能走出不同的调用链。没有追踪,就只能盲猜;没有标准部署形态,智能体无法进入生产。
LangSmith:追踪、评估与监控
LangSmith 是官方可观测平台(有免费额度,也支持自托管)。开启追踪只需两个环境变量,无需改一行代码:
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="lsv2_pt_..."
# 此后所有 invoke / stream 自动上报到 LangSmith
在 LangSmith 的 UI 里你能看到每一次运行的完整调用树:哪个环节耗了多少 token、检索命中了哪些块、工具传了什么参数、每一步的原始提示词。生产监控与告警也建立在同一数据流上。本地不想依赖云端时,可用开源替代 Langfuse,接入方式同样是回调集成。
# 给运行打标签,便于按实验 / 客户 / 环境分组分析
result = chain.with_config(
tags=["rag-v3", "customer-a"],
metadata={"prompt_version": "2026-08-20"},
).invoke("什么是 LCEL?")
部署:LangGraph Platform 与 langgraph dev
0.x 时代的 LangServe 已进入维护模式,官方部署路径统一到 LangGraph 生态。本地开发用 langgraph dev,会启动带可视化调试界面的服务(LangGraph Studio);生产用 langgraph build 构建镜像,或直接使用 LangGraph Platform 托管。
// langgraph.json:声明图入口,dev/build 都读它
{
"dependencies": ["."],
"graphs": {
"assistant": "./app/agent.py:agent"
},
"env": ".env"
}
pip install "langgraph-cli[inmem]"
langgraph dev # http://127.0.0.1:2024,打开即见可视化调试界面
langgraph build # 生成可部署的容器镜像
性能优化、提示词迭代、检索调参的共同前置是“能看到每次运行的完整轨迹”。把接入 LangSmith(或 Langfuse)当作任何项目的第一步,后续所有章节的调试方法都建立在这个基础上。
版本演进与迁移
LangChain 的 API 变化史就是一部 LLM 应用工程进化史。能看懂“哪年的教程、哪套 API”是必备生存技能,否则会在新旧教程混杂的资料里反复踩坑。
2022-2023:单一包时代(0.0.x)
一个 langchain 包包含所有。LLMChain、ConversationChain、initialize_agent 是主角,链条是“类继承”风格。网上大量老教程停留在这个时代。
2024 年初:0.1 大拆包
拆出 langchain-core / langchain-community / 各厂商 partner 包,LCEL 成为官方主推,AgentExecutor 仍是智能体标准。
2024 年中后:0.2 与 0.3
0.2 进一步解耦社区包;0.3 全面转向 Pydantic 2 与 Python 3.9+。智能体推荐路径转向 langgraph.prebuilt.create_react_agent。
2025 年 10 月:1.0 定版
create_agent 成为智能体唯一推荐入口,中间件体系上线,content blocks 标准化,历史功能迁入 langchain-classic。LangGraph 同步 1.0,核心 API 冻结稳定。
新旧 API 迁移对照表
| 0.x 写法 | 1.x 写法 | 说明 |
|---|---|---|
LLMChain(llm=..., prompt=...) | prompt | llm | parser | 链一律用 LCEL 表达 |
initialize_agent(...) | create_agent(...) | 最早的字符串解析智能体早已移除 |
create_tool_calling_agent + AgentExecutor | create_agent | 0.3 时代的标准写法,已过时 |
langgraph.prebuilt.create_react_agent | langchain.agents.create_agent | 参数 prompt 改名 system_prompt |
ConversationBufferMemory 等记忆类 | LangGraph checkpointer / 中间件 | 记忆 = 状态管理 |
RunnableWithMessageHistory | checkpointer + thread_id | 同样的思想,更底层的实现 |
langchain.retrievers / langchain.indexes | langchain_classic.retrievers / .indexes | 功能保留,搬家到 classic |
langchain.embeddings 社区实现 | langchain_classic.embeddings | 同上 |
AgentExecutor(prompt=SystemMessage(...)) | create_agent(system_prompt=...) | 动态提示词用中间件实现 |
判断教程年代的快速信号:import 路径里出现 from langchain.chains、from langchain.agents import initialize_agent,说明是 0.0.x 时代的古董;出现 create_react_agent 但来自 langgraph.prebuilt,是 0.3 时代;只有 from langchain.agents import create_agent 才是 1.x 现代写法。
常见陷阱与 FAQ
九成是版本错位:教程用的 0.x,环境装的是 1.x。先确认教程年代,再对照上一节的迁移表改 import;确实需要老功能时装 langchain-classic。永远用 uv.lock 或 requirements.txt 锁定版本,不要在不同项目间共享环境。
三个手段叠加:一,Field description 写得更具体,枚举字段用 Literal;二,外层套 .with_retry(stop_after_attempt=3);三,准备 with_fallbacks 降级链(换更强模型或返回默认对象并记录日志)。若模型支持原生 JSON Schema 严格模式,优先选 method="json_schema"。
两个常见原因:一是调用的是 invoke 而不是 stream;二是链中某个环节是“聚合型”的(比如字典分支里一个分支慢,整个 step 要等它结束)。用 astream_events 能看到每个环节的耗时分布,定位阻塞点。
先检查三件事:工具 docstring 是否写清适用与不适用场景;参数名是否语义化(date_range 优于 dr);工具数量是否过多(超过一二十个工具时,模型选择准确率会下降,考虑拆分智能体或动态选择工具子集)。
单厂商单场景的简单脚本,直接用厂商 SDK 更轻;纯研究原型用 Notebook 手写循环也没问题。LangChain 的价值在“组合、切换、观测、生产化”四件事同时需要时才最大化。但即使不用框架,本手册的抽象思想(消息、工具、链、状态)依然适用,它们已是行业通用语言。
batch 遇错即停。生产任务改用逐条循环 + try/except,或用 LangSmith 数据集与评估接口管理批量任务,失败样本自动沉淀为回归测试集。
实战练习与资源
以下练习按难度递增,全部完成后,你对本章内容的掌握程度就达到了“能写进简历”的水平。