LLM 工程手册LLM ENGINEERING

CHAPTER 01框架基础 · FRAMEWORK

LangChain 框架全景

组件生态、LCEL 表达式、工具调用与 create_agent。掌握 LLM 应用开发的底座,基于 1.x 现代接口,并讲清新旧版本差异。

开始阅读 难度入门 先修Python 基础 2.5小时
01.1

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

langchain create_agent · middleware · tools · messages · chat_models langchain-core Runnable · Prompt · Messages · BaseChatModel · BaseTool 抽象 集成生态 langchain-openai · langchain-anthropic · langchain-community · langchain-classic 工具链 LangSmith 追踪 / 评估 / 监控 LangGraph 编排 / 持久化 / 部署 LangSmith Eval 数据集 / 回归测试
包结构:主包与集成包都依赖 langchain-core 定义的抽象;工具链横跨所有层。你的应用代码通常只 import 主包与具体的集成包。
包名职责典型 import
langchain-core所有抽象的源头:Runnable、消息、提示词、BaseChatModel、BaseToolfrom langchain_core.prompts import ChatPromptTemplate
langchain1.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-classic0.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
与直接调 SDK 的关系

LangChain 不是对 OpenAI SDK 的封装替代,而是跨厂商的抽象层与组合层。如果你的应用只用一个厂商、不需要组合与观测,直接用厂商 SDK 也完全合理;一旦需要 RAG、智能体、多厂商切换或生产追踪,抽象层的价值才会显现。这是工程选型,不是信仰问题。

01.2

模型接入与消息体系

所有 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 尤其要设置,避免触发限流。

01.3

提示词工程

提示词是 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},模板可读性决定维护成本。
  • 示例覆盖边界情况。少样本至少包含一个"难例",模型对边界的判断力主要来自示例。
  • 把提示词当代码管理。进版本库、写测试(固定输入断言输出结构),提示词回归是线上事故高发区。
01.4

结构化输出

让模型输出直接成为可被程序消费的对象。这是从“聊天玩具”到“应用组件”的分水岭:下游代码拿到的是 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 的降级链(如换更便宜的模型重新抽取,或返回带默认值的空对象并打日志)。

01.5

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 把输入原样传递:

RunnableParallel(并行分支) retriever | format_docs 检索并格式化为 context RunnablePassthrough() 问题原样透传为 question ChatPromptTemplate 把 context 与 question 填入模板 ChatModel 生成回答 StrOutputParser 输出纯字符串
经典 RAG 链的数据流:并行分支准备变量,模板与模型依次执行,解析器收尾。整个链条自动支持流式与异步。
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把普通函数变成 RunnableRunnableLambda(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 的边界

LCEL 擅长单向数据流:输入到输出一条路走到底。一旦需要循环(智能体的“调用工具后再回到模型”)、需要持久化状态、需要人工审批打断,就该切换到 LangGraph 的状态图模型。两者不是竞争关系:LCEL 的链可以直接作为 LangGraph 的节点,无缝过渡。

01.6

RAG 基础组件

LangChain 把 RAG 拆成五个标准环节:加载、切分、向量化、存储、检索。本章建立组件地图与最小可用管道,深度优化留给第三章。

加载 DocumentLoader 切分 TextSplitter 向量化 Embeddings 存储 VectorStore 检索 Retriever
索引期(前四步,离线执行)与查询期(检索 + 生成)。第三章将逐环节深化。

加载与切分

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 是免费的质量杠杆

加载与切分阶段写入的 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)也留到第三章对比。

01.7

工具体系

工具(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 由运行时注入
工具设计第一原则

模型选错工具的根因大多不是模型能力,而是工具描述写得含糊或职责重叠。两条纪律:一,每个工具描述必须写清“什么时候该用我、什么时候不该用我”;二,两个工具的边界如果人都分不清,模型更分不清,先合并或重命名。

01.8

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"]}},
        ),
    ],
)
与旧 API 的关系

老教程中的 AgentExecutor + create_tool_calling_agentlanggraph.prebuilt.create_react_agent 均已被 create_agent 取代(后者在 LangGraph 1.x 中已弃用)。差异细节与迁移对照表见本章第 10 节,智能体全景见第五章。

01.9

可观测性与部署

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)当作任何项目的第一步,后续所有章节的调试方法都建立在这个基础上。

01.10

版本演进与迁移

LangChain 的 API 变化史就是一部 LLM 应用工程进化史。能看懂“哪年的教程、哪套 API”是必备生存技能,否则会在新旧教程混杂的资料里反复踩坑。

22

2022-2023:单一包时代(0.0.x)

一个 langchain 包包含所有。LLMChain、ConversationChain、initialize_agent 是主角,链条是“类继承”风格。网上大量老教程停留在这个时代。

24.1

2024 年初:0.1 大拆包

拆出 langchain-core / langchain-community / 各厂商 partner 包,LCEL 成为官方主推,AgentExecutor 仍是智能体标准。

24.9

2024 年中后:0.2 与 0.3

0.2 进一步解耦社区包;0.3 全面转向 Pydantic 2 与 Python 3.9+。智能体推荐路径转向 langgraph.prebuilt.create_react_agent。

25.10

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 + AgentExecutorcreate_agent0.3 时代的标准写法,已过时
langgraph.prebuilt.create_react_agentlangchain.agents.create_agent参数 prompt 改名 system_prompt
ConversationBufferMemory 等记忆类LangGraph checkpointer / 中间件记忆 = 状态管理
RunnableWithMessageHistorycheckpointer + thread_id同样的思想,更底层的实现
langchain.retrievers / langchain.indexeslangchain_classic.retrievers / .indexes功能保留,搬家到 classic
langchain.embeddings 社区实现langchain_classic.embeddings同上
AgentExecutor(prompt=SystemMessage(...))create_agent(system_prompt=...)动态提示词用中间件实现
看教程先看版本

判断教程年代的快速信号:import 路径里出现 from langchain.chainsfrom langchain.agents import initialize_agent,说明是 0.0.x 时代的古董;出现 create_react_agent 但来自 langgraph.prebuilt,是 0.3 时代;只有 from langchain.agents import create_agent 才是 1.x 现代写法。

01.11

常见陷阱与 FAQ

ImportError / ModuleNotFoundError 频繁出现怎么办?

九成是版本错位:教程用的 0.x,环境装的是 1.x。先确认教程年代,再对照上一节的迁移表改 import;确实需要老功能时装 langchain-classic。永远用 uv.lockrequirements.txt 锁定版本,不要在不同项目间共享环境。

with_structured_output 偶尔抛解析异常,怎么稳住?

三个手段叠加:一,Field description 写得更具体,枚举字段用 Literal;二,外层套 .with_retry(stop_after_attempt=3);三,准备 with_fallbacks 降级链(换更强模型或返回默认对象并记录日志)。若模型支持原生 JSON Schema 严格模式,优先选 method="json_schema"

为什么我的链没有流式输出,要等很久才一次性返回?

两个常见原因:一是调用的是 invoke 而不是 stream;二是链中某个环节是“聚合型”的(比如字典分支里一个分支慢,整个 step 要等它结束)。用 astream_events 能看到每个环节的耗时分布,定位阻塞点。

@tool 定义的工具有时模型不调用,或参数填错?

先检查三件事:工具 docstring 是否写清适用与不适用场景;参数名是否语义化(date_range 优于 dr);工具数量是否过多(超过一二十个工具时,模型选择准确率会下降,考虑拆分智能体或动态选择工具子集)。

什么时候不该用 LangChain?

单厂商单场景的简单脚本,直接用厂商 SDK 更轻;纯研究原型用 Notebook 手写循环也没问题。LangChain 的价值在“组合、切换、观测、生产化”四件事同时需要时才最大化。但即使不用框架,本手册的抽象思想(消息、工具、链、状态)依然适用,它们已是行业通用语言。

批处理跑一半报错,怎么保留已完成结果?

batch 遇错即停。生产任务改用逐条循环 + try/except,或用 LangSmith 数据集与评估接口管理批量任务,失败样本自动沉淀为回归测试集。

01.12

实战练习与资源

以下练习按难度递增,全部完成后,你对本章内容的掌握程度就达到了“能写进简历”的水平。

延伸资源