LangChain · LangGraph · LangSmith 面经

官方文档 · 中文讲解 + 英文术语 · 每题可跳原文

128题目
22/22主题簇
128深挖·拓展题
100%构建进度

🗂️ 数据源(由 sources.json 控制)

改 sources.json 开关源 · 全文镜像在 sources/
LangChain 文档
官方文档 · .md
62 本地文件
sources/langchain

🧠 知识脉络图(点击跳转)

颜色=该章主导频率 🔥高频/中频/低频 · 数字=题量

📊 章节频率热力(🔥高频/中频/低频 占比)

模型·消息·工具·Agent14
结构化输出与记忆12
检索·多智能体·上下文12
基础与范式11
图与函数式 API12
状态·持久化·记忆12
流式·中断·子图·时间旅行12
执行模型·实战·工具15
可观测6
评估12
监控·提示·部署10

第一部分 · LangChain (OSS)

第1章 模型·消息·工具·Agent

🔥高频

总览与 Agent 运行时

LangChain overview Agents Runtime
QLangChain 里 "Agent = Model + Harness" 这个公式怎么理解?create_agent 到底提供了什么?深挖·拓展🔥高频
create_agent harness 架构心智模型
⏱️ 现行
LangChain 把 agent 拆成两部分:模型(负责推理和决策)和 harness(模型周围的一切)。create_agent 提供的正是这个 harness——一个最小、但高度可配置的运行外壳,包含 prompt、tools 以及任何塑造行为的 middleware。这样设计的核心权衡是"从原语出发、按需组合":框架不预置一大堆你用不到的能力,而是让你从 model=tools=system_prompt= 这些基本参数起步,再通过 middleware 增量扩展。好处是简单场景保持极简、可移植,复杂场景又能逐层加码;代价是需要开发者理解 harness 各层的职责边界,而不是拿到一个"黑盒 agent"。它同时内建对 OpenAI、Anthropic、Google 等多家 provider 的支持,因此换模型时应用改动很小。
术语 create_agent(创建 agent 的工厂函数,即 harness 本身); harness(模型外围的运行外壳:prompt+tools+middleware); middleware(在 agent loop 特定时机介入、塑造行为的可组合单元)
📖 "Agent = Model + Harness. LangChain provides create_agent: a minimal, highly configurable harness. The harness is everything around the model loop: the prompt, the tools, and any middleware that shapes behavior." — LangChain overview
📖 "Start with the primitives and compose exactly what your use case needs." — LangChain overview
🧪 实例 最小可用 agent——只给模型、一个工具和系统提示:
python
from langchain.agents import create_agent

def get_weather(city: str) -> str:
    """Get weather for a given city."""
    return f"It's always sunny in {city}!"

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[get_weather],
    system_prompt="You are a helpful assistant",
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "What's the weather in San Francisco?"}]}
)
print(result["messages"][-1].content_blocks)
🔍 追问 为什么不直接给一个"全功能 agent"而要暴露原语? → 因为不同用例需要的能力差异很大;最小 harness + 按需 middleware 让你"只拿需要的、跳过其余",既保持简单又保持可移植。
📚 拓展阅读
Q什么是 agent?它的运行循环(agent loop)是怎样的?深挖·拓展🔥高频
agent-loop tool-calling 核心概念
⏱️ 现行
在 LangChain 的定义里,agent 就是"一个模型在循环中不断调用工具,直到给定任务完成"。围绕这个 loop 的所有东西——模型、它的 prompt、它的 tools,以及塑造其行为的 middleware——统称 harness。harness 的职责被一句话概括:在正确的时机把正确的上下文喂给模型。这个心智模型的价值在于把"智能"和"工程外壳"分离:模型负责在每一步决定是继续调工具还是给出最终答案,而 harness 负责管理上下文、注入工具、执行策略。权衡在于——loop 的自主性越强,越需要 harness 层面的约束(如调用次数上限、审批点)来保证可控,这也是后续 middleware 存在的根本原因。
术语 agent loop(模型循环调用工具直至任务完成的过程); task complete(loop 的终止条件); context at the right time(harness 的核心职责:适时提供正确上下文)
📖 "An agent is a model calling tools in a loop until a given task is complete." — Agents
📖 "The job of a harness: get the model the right context at the right time for the given task." — Agents
📖 "A harness is everything around that loop: the model, its prompt, its tools, and any middleware that shapes its behavior." — Agents
🧪 实例 agent loop 的机制示意:
flowchart LR
    U[User message] --> M[Model]
    M -->|tool_calls| T[Tools]
    T -->|results| M
    M -->|task complete| A[Final answer]
🔍 追问 loop 什么时候停? → 当模型不再发起 tool 调用、判定"任务完成"并给出最终响应时;harness 也可通过 middleware(如调用次数上限)强制终止。
📚 拓展阅读
Qcreate_agent 的核心组件有哪些?model / tools / system_prompt / response_format 各接受什么?深挖·拓展🔥高频
核心组件 structured-output 参数
⏱️ 现行
harness 的基础配置由四个参数直接给出。model= 接受一个 "provider:model" 形式的标识字符串或一个已初始化的模型实例,用来选定 agent 使用的模型;tools= 接受任意 Python callable、LangChain tool 或 tool dict,把工具提供给 agent;system_prompt= 接受字符串或 SystemMessage,塑造 agent 处理任务的方式(若要在运行时动态生成 prompt 则用 middleware);response_format= 让 agent 返回一个经过校验的 schema(结构化输出)。这种"字符串标识 vs 实例"的双形态设计是关键权衡:字符串标识让快速切换 provider、保持可移植变得几乎零成本,而传入实例则给你对模型参数、认证等的完全控制。更高级的能力则统一走 middleware 扩展,而非往这四个参数上堆叠。
术语 provider:model(模型标识字符串,如 anthropic:claude-sonnet-4-6); response_format(声明结构化输出 schema 的参数); structured_response(结果中承载校验后结构化输出的字段); SystemMessage(system_prompt 可接受的消息类型)
📖 "Pass a model identifier string ("provider:model") or an initialized model instance to select the model for your agent." — Agents
📖 "To provide the agent with tools, pass any Python callable, LangChain tool, or tool dict." — Agents
📖 "Return a validated schema from the agent using response_format=." — Agents
🧪 实例response_format 拿到校验过的结构化结果:
python
from pydantic import BaseModel
from langchain.agents import create_agent


class Answer(BaseModel):
    summary: str
    confidence: float


agent = create_agent(model="anthropic:claude-sonnet-4-6", tools=tools, response_format=Answer)
result = agent.invoke({"messages": [{"role": "user", "content": "Summarize AI trends"}]})
result["structured_response"]  # Answer(summary=..., confidence=...)
🔍 追问 想在运行时根据用户动态改 system prompt 怎么办? → 不要塞进 system_prompt= 静态字符串,改用 middleware(如 dynamic_prompt)在运行时生成。
📚 拓展阅读
Qinvoke 和 streaming 有何区别?thread_idcheckpointercontext 各自管什么?深挖·拓展🔥高频
invoke streaming thread_id context
⏱️ 现行
invoke 在一次运行结束时返回最终响应;但如果 agent 中途执行了多次工具调用,用户往往需要在完成前看到进度,这时用 streaming 把中间消息和工具活动实时呈现出来。二者的分工清晰:invoke 简单、拿最终结果;stream 复杂一点、换来可观测的中间过程。会话状态则由两个正交的概念管理:thread_id 界定"会话"范围(消息历史、checkpoint),配合 checkpointer 才能持久化并恢复对话历史(本地需显式传入如 InMemorySaver(),部署在 LangSmith 上会自动配置);而 context 承载"每次运行"的数据(如 user id、API key、feature flag),供 tools 和 middleware 在调用时读取,其结构用 context_schema 定义。二者常一起传:一个管跨轮的会话记忆,一个管单轮的注入依赖。
术语 thread_id(界定会话范围,关联消息历史与 checkpoint); checkpointer(持久化/恢复对话历史的组件,如 InMemorySaver); context(每次运行注入的数据,供 tools/middleware 读取); context_schema(定义 context 数据结构)
📖 "invoke returns the final response at the end of a run. If an agent executes multiple tool calls, users often need progress updates before completion. Use streaming to surface intermediate messages and tool activity as they happen." — Agents
📖 "thread_id scopes the *conversation* (message history, checkpoints), while context carries *per-run* data your tools and middleware read at invocation time. Both are commonly passed together." — Agents
🧪 实例 用同一个 thread_id 跨轮保留历史(需 checkpointer):
python
from langchain.agents import create_agent
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model="anthropic:claude-sonnet-4-6",
    tools=[],
    checkpointer=InMemorySaver(),
)

config = {"configurable": {"thread_id": str(uuid7())}}

result = agent.invoke(
    {"messages": [{"role": "user", "content": "What's the weather in San Francisco?"}]},
    config=config,
)

# A follow-up turn on the same conversation: reuse the same thread_id to keep history
result = agent.invoke(
    {"messages": [{"role": "user", "content": "What about tomorrow?"}]},
    config=config,
)
🔍 追问 只传 thread_id 但没配 checkpointer 会怎样? → 无法持久化对话历史;持久化历史要求 agent 配置了 checkpointer,本地需显式传入,部署到 LangSmith 时会自动 provision。
📚 拓展阅读
  • Agents — invocation、streaming、thread_id 与 context 的用法
  • Runtime — context 如何在 tools/middleware 中被读取
Qcreate_agent 的 Runtime 对象是什么?为什么说 context 是"依赖注入"?深挖·拓展中频
Runtime dependency-injection ToolRuntime
⏱️ 现行
create_agent 底层跑在 LangGraph 的 runtime 之上,LangGraph 暴露一个 Runtime 对象,携带五类信息:Context(静态信息,如 user id、db 连接等一次调用的依赖)、Store(用于长期记忆的 BaseStore 实例)、Stream writer(用 "custom" 流模式写出信息)、Execution info(当前执行的身份与重试信息:thread ID、run ID、attempt number)、Server info(跑在 LangGraph Server 上时的服务端元数据)。之所以称为"依赖注入",是因为你不必在工具里硬编码值或依赖全局状态,而是在调用 agent 时把 db 连接、user id、配置等运行时依赖注入进去——这让工具更可测试、可复用、更灵活。工具内通过 ToolRuntime 参数拿到 Runtime,middleware 里则通过 Runtime 参数或 ModelRequest 拿到。注意 server_info 只在 LangGraph Server 上有值,本地开发时为 None
术语 Runtime(LangGraph 暴露的运行时对象,聚合 context/store/execution info 等); ToolRuntime(工具内访问 Runtime 的参数类型); context_schema(声明 context 结构); execution_info(thread ID/run ID/attempt number 等执行身份)
📖 "Runtime context provides dependency injection for your tools and middleware." — Runtime
📖 "static information like user id, db connections, or other dependencies for an agent invocation" — Runtime
📖 "server_info is None when not running on LangGraph Server (e.g., during local development)." — Runtime
🧪 实例 工具通过 ToolRuntime 读取 context 和长期记忆:
python
from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime

@dataclass
class Context:
    user_id: str

@tool
def fetch_user_email_preferences(runtime: ToolRuntime[Context]) -> str:
    """Fetch the user's email preferences from the store."""
    user_id = runtime.context.user_id

    preferences: str = "The user prefers you to write a brief and polite email."
    if runtime.store:
        if memory := runtime.store.get(("users",), user_id):
            preferences = memory.value["preferences"]

    return preferences
🔍 追问 在本地开发时访问 runtime.server_info 会拿到什么? → 拿到 None,因为 server info 只在跑于 LangGraph Server 时才有;此外 execution_info/server_info 需要 deepagents>=0.5.0(或 langgraph>=1.1.5)。
📚 拓展阅读
Q为什么用 middleware 来配置 harness?它覆盖哪些能力域?深挖·拓展中频
middleware context-management guardrails fault-tolerance
⏱️ 现行
create_agent 高度可扩展,middleware 是定制的核心原语:每一片只负责一个关注点,在 agent loop 的正确时机挂钩,并能与任意其它 middleware 自由组合——按需取用、其余跳过。常见模式被预置成一等公民的 middleware,其余可写成 custom middleware。它覆盖几大能力域:Execution environment(工具、文件系统、沙箱、代码执行);Context management(摘要、记忆、skills、prompt 缓存——因为每次模型调用的上下文窗口是固定的,随 agent 运行会被历史和工具结果填满,摘要在溢出前压缩历史);Planning and delegation(todo 列表与子 agent 做并行、隔离的工作);Fault tolerance(重试、回退、调用上限,把限流/超时/瞬时错误在基础设施层处理掉,业务代码不必到处 try/catch);Guardrails(PII 检测与内容控制,有些策略必须确定性地强制执行,不能只写在 prompt 里);Steering(在高影响动作前引入 human-in-the-loop 审批)。这种"单一职责 + 可组合"的设计避免了 harness 变成僵化的大而全框架。
术语 middleware(定制 harness 的核心原语,单一职责、可组合); SummarizationMiddleware(上下文溢出前压缩历史); PIIMiddleware(确定性地执行 PII/内容策略); HumanInTheLoopMiddleware(高影响动作前的人工审批)
📖 "create_agent is highly extensible. Middleware is the primitive for customization: each piece handles one concern, hooks into the agent loop at the right moment, and composes freely with any other." — Agents
📖 "Every model call has a fixed context window." — Agents
📖 "Some policies can't live in a prompt—they need to be enforced deterministically regardless of what the model does." — Agents
🧪 实例 叠加容错与守卫 middleware:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRetryMiddleware, ToolRetryMiddleware

agent = create_agent(
    model="anthropic:claude-sonnet-4-6",
    tools=[search],
    middleware=[
        ModelRetryMiddleware(max_retries=3),
        ToolRetryMiddleware(max_retries=2),
    ],
)
🔍 追问 为什么 PII/合规策略要用 middleware 而不是写进 system prompt? → 因为有些策略必须无论模型怎么做都被确定性地强制执行,guardrail 会在工具结果进入模型上下文前拦截并应用规则,prompt 无法保证这种确定性。
📚 拓展阅读
QLangChain、LangGraph、Deep Agents 三者如何选型?深挖·拓展低频
选型 LangGraph DeepAgents
⏱️ 现行
三者是同一栈上不同抽象层级的选择。Deep Agents 是"开箱即用"的起点,自带自动上下文压缩、虚拟文件系统、子 agent 派生等能力,而它本身就构建在 LangChain 的 agents 之上,你也可以直接用后者。LangChain 的 create_agent 适合需要一个高度可定制、能轻松贴合用例与数据的 harness 的场景。LangGraph 是底层编排框架,面向"确定性工作流与 agentic 工作流相结合"的高级需求。选型逻辑因此是一条抽象梯度:想快速拿到功能齐全的 agent 选 Deep Agents;想要可裁剪的 harness 选 LangChain;想要对控制流做低层次编排选 LangGraph。值得注意的是 LangChain 的 agents 正是构建在 LangGraph 之上,从而继承其持久化执行、human-in-the-loop、persistence 等能力——这解释了为什么三者能平滑衔接而非互斥。无论用哪个框架,都可以用 LangSmith 做 trace、debug 和评估。
术语 Deep Agents(batteries-included 的 agent,含自动上下文压缩/虚拟文件系统/子 agent); LangGraph(底层编排框架,混合确定性与 agentic 工作流); LangSmith(跨框架的 trace/debug/评估工具)
📖 "Start with Deep Agents for a "batteries-included" agent with features like automatic context compression, a virtual filesystem, and subagent-spawning." — LangChain overview
📖 "Use LangGraph, our low-level orchestration framework, for advanced needs combining deterministic and agentic workflows." — LangChain overview
📖 "LangChain's agents are built on top of LangGraph. This allows us to take advantage of LangGraph's durable execution, human-in-the-loop support, persistence, and more." — LangChain overview
🧪 实例 选型决策示意:
flowchart TD
    Q{需求?} -->|开箱即用、长任务| DA[Deep Agents]
    Q -->|可裁剪 harness、贴合用例| LC[LangChain create_agent]
    Q -->|低层编排、确定性+agentic| LG[LangGraph]
    DA -.built on.-> LC
    LC -.built on.-> LG
🔍 追问 既然 LangChain agents 建在 LangGraph 上,为什么还单独用 LangGraph? → 当你需要把确定性工作流与 agentic 工作流深度结合、做低层次编排时,直接用 LangGraph 这个 orchestration framework 更合适;create_agent 更偏"可配置 harness"这一层。
📚 拓展阅读
🔥高频

模型·消息·工具

Models Messages Tools
QLangChain 里如何初始化一个聊天模型?为什么能做到"换 provider 不改业务代码",又怎么保证连接的健壮性?深挖·拓展🔥高频
init_chat_model provider-abstraction resilience
⏱️ 现行
LangChain 的核心设计是"标准模型接口":每个 provider 都由独立集成包实现同一套接口,因此 init_chat_model("claude-sonnet-4-6")ChatAnthropic(model=...) 得到的对象暴露一致的 invoke/stream/batch/bind_tools/with_structured_output 方法,切换供应商时上层逻辑无需改写;甚至新模型名可以立即使用,因为 provider 包会把模型名直接透传给底层 API。你可以用 "{model_provider}:{model}" 形式(如 openai:o1)在单个参数里同时指定 provider 和模型,其余标准参数(temperaturemax_tokenstimeoutmax_retries 等)作为 **kwargs 内联传入。健壮性方面,chat model 会对失败请求自动做指数退避重试:默认最多 6 次,只对网络错误、限流(429)、服务端错误(5xx)重试,而 401/404 这类客户端错误不重试——这是"可恢复 vs 不可恢复"的权衡,避免把鉴权错误反复打到服务端。对于跑在不稳定网络上的长任务,官方建议把 max_retries 调到 10–15 并配合 checkpointer 保留进度。这种"统一接口 + 内置弹性"让你可以从 standalone 调用平滑扩展到 agent 工作流。
术语 init_chat_model(初始化聊天模型的入口函数); standard interface(所有 provider 共享的统一接口); model_provider:model(在单参数里同时指定供应商与模型的写法); max_retries(自动重试次数,默认 6); exponential backoff(指数退避重试策略)
📖 "Each provider package implements the same standard interface, so you can swap providers without rewriting application logic." — Models
📖 "Retries use exponential backoff with jitter. Network errors, rate limits (429), and server errors (5xx) are retried automatically. Client errors such as 401 (unauthorized) or 404 are not retried." — Models
🧪 实例 用 provider 前缀 + 调高重试来应对弱网长任务:
python
from langchain.chat_models import init_chat_model

model = init_chat_model(
    "google_genai:gemini-3.5-flash",
    max_retries=10,  # Increase for unreliable networks (default: 6)
    timeout=120,  # Seconds; increase for slow connections
)
🔍 追问 init_chat_model 和直接实例化 ChatOpenAI 有什么区别? → 二者等价地实现同一标准接口;init_chat_model 用字符串+kwargs 更适合"配置化/可切换 provider",直接类实例化则便于访问 provider 专有字段(如 ChatOpenAIuse_responses_api)。
📚 拓展阅读
Q讲讲 LangChain 的 tool calling 完整流程:bind_tools、tool 执行循环、以及 tool_call_id 的作用。深挖·拓展🔥高频
tool-calling bind_tools agent-loop
⏱️ 现行
模型本身不执行工具,它只是"请求"调用工具——tool 是"schema(名字/描述/参数定义,通常是 JSON schema)+ 可执行函数"的配对。你用 model.bind_tools([...]) 把工具绑定到模型,之后模型可以在每次 invoke 时自行决定调用哪些绑定工具,结果放在返回 AIMessagetool_calls 字段里。关键点在于:当你脱离 agent 单独用模型时,执行工具并把结果喂回模型是你自己的责任,这就是"tool 执行循环"——Step1 模型产出 tool_calls,Step2 你逐个执行工具得到 ToolMessage,Step3 把这些消息追加进历史再次 invoke 让模型据此生成最终回答;而用 agent(create_agent)时这个编排循环由 agent 帮你完成。每个 ToolMessage 都带一个 tool_call_id,必须与原始 tool call 的 id 匹配,模型才能把"结果"和"请求"正确关联起来(尤其在并行工具调用时)。权衡上,默认模型可自由选择工具,也支持 tool_choice="any" 或指定某工具强制调用,以及 parallel_tool_calls=False 关闭并行;并行能同时从多个源取数、更快,但不是所有场景都适合,模型会根据操作独立性判断。
术语 bind_tools(把工具 schema 绑定给模型); tool_calls(AIMessage 中模型发起的调用请求列表); tool_call_id(关联请求与结果的唯一 id); tool_choice(强制调用某工具或任意工具); parallel_tool_calls(是否允许并行调用)
📖 "Models can request to call tools that perform tasks such as fetching data from a database, searching the web, or running code." — Models
📖 "Each ToolMessage returned by the tool includes a tool_call_id that matches the original tool call, helping the model correlate results with requests." — Models
🧪 实例 一次完整的三步执行循环:
python
model_with_tools = model.bind_tools([get_weather])

# Step 1: Model generates tool calls
messages = [{"role": "user", "content": "What's the weather in Boston?"}]
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)

# Step 2: Execute tools and collect results
for tool_call in ai_msg.tool_calls:
    tool_result = get_weather.invoke(tool_call)
    messages.append(tool_result)

# Step 3: Pass results back to model for final response
final_response = model_with_tools.invoke(messages)
print(final_response.text)
sequenceDiagram
    participant U as User
    participant M as Model
    participant T as Tools
    U->>M: 提问
    M->>M: 决定需要哪些工具
    M->>T: tool_calls (含 id)
    T-->>M: ToolMessage (tool_call_id 匹配)
    M->>U: 基于结果生成最终回答
🔍 追问 为什么强调工具名用 snake_case? → 某些 provider 会拒绝含空格或特殊字符的名字并报错,坚持字母数字/下划线/连字符能提升跨 provider 兼容性。
📚 拓展阅读
QLangChain 有哪四种消息类型?各自职责是什么?为什么要有统一的标准消息类型?深挖·拓展🔥高频
messages roles standard-type
⏱️ 现行
Message 是模型的基本上下文单元,既是输入也是输出,携带内容(content)与元数据(metadata),用来表示一次对话的状态。四种角色各司其职:SystemMessage 是给模型的初始指令,定基调、定角色、定回答规范;HumanMessage 表示用户输入,可含文本、图像、音频、文件等多模态内容;AIMessage 是模型的输出,可能包含文本、tool_calls、以及 usage_metadata(token 计数)和 response_metadata 等 provider 元数据;ToolMessage 用来把单次工具执行的结果传回模型,必须带上与 AIMessage 中 tool call 匹配的 tool_call_id(还可用 artifact 存放不发给模型、仅供程序下游使用的补充数据)。之所以要标准消息类型,是因为它在所有 provider 上表现一致,屏蔽了各家 API 的差异——你可以手动构造 AIMessage 插入历史"伪装成模型输出",因为不同 provider 对各类消息的权重/上下文处理不同。此外 v1 引入的 content_blocks 属性会把 content 惰性解析成跨 provider 的标准化、类型安全表示,同时保持向后兼容。
术语 SystemMessage(系统指令,设定行为); HumanMessage(用户输入); AIMessage(模型输出,含 tool_calls/usage_metadata); ToolMessage(单次工具结果,需 tool_call_id); content_blocks(标准化内容块属性); artifact(不发给模型的附带数据)
📖 "Messages are the fundamental unit of context for models in LangChain. They represent the input and output of models, carrying both the content and metadata needed to represent the state of a conversation when interacting with an LLM." — Messages
📖 "Tool messages are used to pass the results of a single tool execution back to the model." — Messages
📖 "Content blocks were introduced as a new property on messages in LangChain v1 to standardize content formats across providers while maintaining backward compatibility with existing code." — Messages
🧪 实例 手动拼装一轮"工具请求 + 工具结果"消息序列:
python
from langchain.messages import AIMessage, HumanMessage, ToolMessage

ai_message = AIMessage(
    content=[],
    tool_calls=[{"name": "get_weather", "args": {"location": "San Francisco"}, "id": "call_123"}]
)
tool_message = ToolMessage(content="Sunny, 72°F", tool_call_id="call_123")  # Must match the call ID

messages = [
    HumanMessage("What's the weather in San Francisco?"),
    ai_message,   # Model's tool call
    tool_message, # Tool execution result
]
response = model.invoke(messages)
🔍 追问 直接传字符串和传消息列表有何区别? → 传字符串是"单个 HumanMessage"的快捷方式,适合无需保留历史的一次性请求;多轮对话、系统指令或多模态内容则应传消息对象列表。
📚 拓展阅读
Qinvoke、stream、batch 三种调用方式分别适合什么场景?stream 的 chunk 怎么聚合成完整消息?深挖·拓展🔥高频
invocation streaming batch
⏱️ 现行
chat model 必须被调用才能产出输出,共有三种主要调用方式,各有取舍。invoke() 最直接,传单条或一列消息,等模型生成完整回复后返回一个 AIMessage,适合需要完整结果再处理的场景。stream() 返回迭代器,边生成边逐块 yield AIMessageChunk,通过渐进式展示显著改善长回复的用户体验;重要的是每个 chunk 被设计成可用加法求和聚合成完整消息(full = chunk if full is None else full + chunk),聚合后的消息与 invoke 产出的等价,可加入历史继续对话——但流式要求"链路上每一步都能处理 chunk 流",若某步需要先把完整输出存进内存就无法流式。batch() 把一批相互独立的请求并行发出,能显著提升吞吐并降低成本,默认只返回整批的最终结果;若想按完成顺序拿到每条结果可用 batch_as_completed()(结果可能乱序,但每条带输入索引可重排),还能用 RunnableConfigmax_concurrency 限制并发数。注意这个 batch() 是客户端并行,与 OpenAI/Anthropic 的服务端批处理 API 是两回事。
术语 invoke()(一次性返回完整 AIMessage); stream()(逐块返回 AIMessageChunk); AIMessageChunk(可求和聚合的流式片段); batch()(客户端并行批量调用); batch_as_completed()(按完成顺序产出,结果可能乱序); max_concurrency(并发上限)
📖 "A chat model must be invoked to generate an output. There are three primary invocation methods, each suited to different use cases." — Models
📖 "Most models can stream their output content while it is being generated. By displaying output progressively, streaming significantly improves user experience, particularly for longer responses." — Models
📖 "Batching a collection of independent requests to a model can significantly improve performance and reduce costs, as the processing can be done in parallel:" — Models
🧪 实例 把流式 chunk 累加成完整消息:
python
full = None  # None | AIMessageChunk
for chunk in model.stream("What color is the sky?"):
    full = chunk if full is None else full + chunk
    print(full.text)

print(full.content_blocks)
# [{"type": "text", "text": "The sky is typically blue..."}]
🔍 追问 agent 节点里用了 model.invoke() 还能整体流式吗? → 能。LangChain 的"auto-streaming"会在检测到整体处于流式模式时自动切到内部流式,通过回调把中间输出实时暴露给 stream() / astream_events(),对 invoke 调用方代码无感。
📚 拓展阅读
Qwith_structured_output 支持哪些 schema 类型和 method?json_schema/function_calling/json_mode 有何区别?深挖·拓展中频
structured-output pydantic json-schema
⏱️ 现行
结构化输出让模型的回复被约束为符合给定 schema 的格式,便于后续解析与处理。LangChain 用 model.with_structured_output(schema) 实现,支持三类 schema:Pydantic 模型(功能最全,带字段校验、描述、嵌套结构,且自动校验)、TypedDict(更轻量、无需运行时校验)、以及原生 JSON Schema(最大可控性与互操作性,但需手动校验)。底层 method 有三种权衡:json_schema 使用 provider 提供的专用结构化输出特性(最可靠);function_calling 通过强制一次遵循该 schema 的 tool call 来"派生"结构化输出(复用 tool calling 能力);json_mode 是部分 provider 上 json_schema 的前身,能产出合法 JSON 但 schema 必须写进 prompt 里描述。此外设 include_raw=True 可同时拿到解析后的对象和原始 AIMessage(形如 {"raw":..., "parsed":..., "parsing_error":...}),从而访问 token 计数等响应元数据。选型上:要校验和丰富类型用 Pydantic,追求简单用 TypedDict,跨系统互操作用 JSON Schema。
术语 with_structured_output(约束输出为指定 schema); Pydantic(带自动校验的富 schema); TypedDict(轻量无校验 schema); json_schema(provider 原生结构化特性); function_calling(用强制工具调用派生结构); json_mode(需在 prompt 描述 schema 的旧模式); include_raw(同时返回原始消息)
📖 "Models can be requested to provide their response in a format matching a given schema. This is useful for ensuring the output can be easily parsed and used in subsequent processing." — Models
📖 "'json_mode': A precursor to 'json_schema' offered by some providers. Generates valid JSON, but the schema must be described in the prompt." — Models
📖 "Pydantic models provide automatic validation. TypedDict and JSON Schema require manual validation." — Models
🧪 实例 用 Pydantic 约束电影信息输出:
python
from pydantic import BaseModel, Field

class Movie(BaseModel):
    """A movie with details."""
    title: str = Field(description="The title of the movie")
    year: int = Field(description="The year the movie was released")
    director: str = Field(description="The director of the movie")
    rating: float = Field(description="The movie's rating out of 10")

model_with_structure = model.with_structured_output(Movie)
response = model_with_structure.invoke("Provide details about the movie Inception")
🔍 追问 想在拿到结构化结果的同时读取 token 用量怎么办? → 传 include_raw=True,返回值里 raw 是完整 AIMessage,可从其 usage_metadata 读取 token 计数,parsed 是解析后的对象。
📚 拓展阅读
Q怎么用 @tool 定义一个工具?为什么类型注解是必需的?工具可以返回哪些类型?深挖·拓展中频
@tool tool-schema return-values
⏱️ 现行
最简单的建工具方式是 @tool 装饰器:默认把函数的 docstring 作为工具描述,帮助模型判断何时使用;而类型注解是必需的,因为它们定义了工具的输入 schema,模型据此知道要传什么参数,所以 docstring 要写得信息充分且简洁。你可以用 @tool("web_search") 覆盖工具名、用 description= 覆盖描述,或用 args_schema= 传 Pydantic 模型/JSON Schema 定义复杂输入。命名建议 snake_case,因为有些 provider 会拒绝含空格或特殊字符的名字。返回值有三种取舍:返回 string 用于人类可读文本(会被转成 ToolMessage 供模型阅读并决定下一步);返回 object(如 dict)用于结构化数据,模型可读取具体字段并推理;返回 Command 用于需要写入 graph state 的场景(可选带 ToolMessage 让模型看到工具成功)。此外工具还能返回标准 content blocks 实现多模态结果(文本+图像等)。注意 configruntime 是保留参数名,不能用作工具参数,访问运行时信息要用 ToolRuntime
术语 @tool(把函数变成工具的装饰器); docstring(默认作为工具描述); type hints(定义输入 schema,必需); args_schema(用 Pydantic/JSON Schema 定义复杂输入); snake_case(推荐的工具命名风格); Command(用于写 state 的返回值)
📖 "Type hints are required as they define the tool's input schema. The docstring should be informative and concise to help the model understand the tool's purpose." — Tools
📖 "Return a string when the tool should provide plain text for the model to read and use in its next response." — Tools
🧪 实例 一个带类型注解和 docstring 的基础工具:
python
from langchain.tools import tool

@tool
def search_database(query: str, limit: int = 10) -> str:
    """Search the customer database for records matching the query.

    Args:
        query: Search terms to look for
        limit: Maximum number of results to return
    """
    return f"Found {limit} results for '{query}'"
🔍 追问 工具想返回结构化数据而不是文本会怎样? → 返回 dict 等 object 会被序列化后作为工具输出发回,模型能读取特定字段并推理;和返回 string 一样不会直接改动 graph state。
📚 拓展阅读
Q工具怎么访问对话 state、context 和长期 store?return_direct 又解决什么问题?深挖·拓展中频
ToolRuntime state-context-store return_direct
⏱️ 现行
工具真正强大在于能访问运行时信息——对话历史、用户数据、持久记忆。方式是给工具签名加一个 runtime: ToolRuntime 参数,它会被自动注入且对 LLM 隐藏,不出现在工具 schema 里,因此模型只看到你真正的业务参数。通过它可以拿到:runtime.state(短期记忆,当前对话的可变数据,如 messages)、runtime.context(调用时传入的不可变配置,如 user_id/session)、runtime.store(长期记忆,跨会话持久,用 namespace/key 组织)、以及 stream_writerexecution_infoserver_infotool_call_id 等。要更新 state 就返回 Command,并在其中放一个带 runtime.tool_call_idToolMessage 让模型看到结果;由于 LLM 可能并行调用多个工具,更新同一字段时应为其定义 reducer 来解决冲突。return_direct=True 则用于"短路"agent 循环:工具执行后直接把输出作为最终回答返回调用方,不再经模型二次处理,适合工具输出本身就是完整答案、想省一次模型调用、或需要确定性未经改写的输出;但正因模型不再处理其输出,它不适合结果还需进一步推理、总结或与其他工具链式调用的场景。若一轮里调了多个工具,只有当全部工具都 return_direct=True 时才生效。
术语 ToolRuntime(注入运行时信息、对模型隐藏的参数); runtime.state(短期记忆/对话状态); runtime.context(不可变的 per-run 配置); runtime.store(跨会话长期记忆); reducer(解决并行更新同一 state 字段的冲突); return_direct(短路 agent 循环直接返回)
📖 "Add runtime: ToolRuntime to your tool signature to access state. This parameter is automatically injected and hidden from the LLM - it won't appear in the tool's schema." — Tools
📖 "Set return direct on a tool to short-circuit the agent loop: the agent returns the tool's output to the caller immediately, without sending it back through the model for further processing." — Tools
📖 "Because the model does not process the tool's output, return_direct=True is not suitable for tools whose results require further reasoning, summarization, or chaining with other tool calls." — Tools
🧪 实例 通过 ToolRuntime 读 context 并返回 Command 更新 state:
python
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command

@tool
def set_language(language: str, runtime: ToolRuntime) -> Command:
    """Set the preferred response language."""
    return Command(
        update={
            "preferred_language": language,
            "messages": [
                ToolMessage(
                    content=f"Language set to {language}.",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
        }
    )
🔍 追问 以前用的 InjectedState/InjectedStore/get_runtime() 还要用吗? → 官方推荐统一改用 ToolRuntime,它用一个显式接口覆盖 state、context、store 和执行元数据,替代旧的多种注入模式。
📚 拓展阅读

第2章 结构化输出与记忆

🔥高频

结构化输出·中间件·护栏

Structured output Overview Guardrails
Qcreate_agentresponse_format 支持哪几种结构化输出策略?它如何自动选择?深挖·拓展🔥高频
structured-output response_format create_agent
⏱️ 现行
结构化输出让 agent 返回可预测的固定格式数据,把结果放进 agent 最终 state 的 structured_response 键,从而免去解析自然语言。response_format 接受四种取值:ToolStrategy(用 tool calling 实现结构化输出)、ProviderStrategy(用 provider 原生结构化输出能力)、直接传 schema 类型(type[StructuredResponseT],让 LangChain 按模型能力自动挑最佳策略)、以及 None(不显式要求结构化输出)。当直接传入一个 schema 类型时,若模型与 provider 支持原生结构化输出(如 OpenAI、Anthropic (Claude)、xAI (Grok))则选 ProviderStrategy,否则对所有其它模型退回 ToolStrategy。权衡在于:provider 原生方式由模型厂商强制执行 schema,可靠性最高、校验最严,可用时应优先;而 tool calling 方式覆盖面最广(几乎所有支持工具调用的现代模型都能用),是通用兜底。在 langchain>=1.1 中是否支持原生结构化输出会从模型的 profile data 动态读取,读不到时可手动指定 profile。schema 支持 Pydantic models、Dataclasses、TypedDict、JSON Schema——Pydantic 返回校验过的实例,其余返回 dict。
术语 response_format(控制 agent 如何返回结构化数据的参数); ProviderStrategy(用 provider 原生结构化输出能力的策略); ToolStrategy(用工具调用实现结构化输出的策略); structured_response(state 中承载结构化结果的键); profile(模型能力档案数据)
📖 "Structured output allows agents to return data in a specific, predictable format." — Structured output
📖 "Provider-native structured output provides high reliability and strict validation because the model provider enforces the schema. Use it when available." — Structured output
🧪 实例 直接把 Pydantic 模型传给 response_format,模型支持原生时自动选 ProviderStrategy
python
from pydantic import BaseModel, Field
from langchain.agents import create_agent


class ContactInfo(BaseModel):
    """Contact information for a person."""
    name: str = Field(description="The name of the person")
    email: str = Field(description="The email address of the person")
    phone: str = Field(description="The phone number of the person")

agent = create_agent(
    model="gpt-5.5",
    response_format=ContactInfo  # Auto-selects ProviderStrategy
)

result = agent.invoke({
    "messages": [{"role": "user", "content": "Extract contact info from: John Doe, john@example.com, (555) 123-4567"}]
})

print(result["structured_response"])
# ContactInfo(name='John Doe', email='john@example.com', phone='(555) 123-4567')
🔍 追问response_format=ProductReviewresponse_format=ProviderStrategy(ProductReview) 有区别吗? → 若 provider 原生支持该模型的结构化输出,两者功能等价;任一情况下若不支持,agent 都会退回 tool calling 策略。
📚 拓展阅读
QToolStrategy 生成结构化输出出错时,handle_errors 怎样控制重试?深挖·拓展🔥高频
ToolStrategy handle_errors retry
⏱️ 现行
用 tool calling 生成结构化输出时模型可能犯错,LangChain 提供智能重试机制自动处理这些错误。典型错误有两类:模型错误地同时调用了多个结构化输出工具(MultipleStructuredOutputsError),以及输出不匹配 schema 的校验错误(StructuredOutputValidationError)。两种情况 agent 都会以 ToolMessage 形式回灌错误反馈并提示模型重试。控制点是 ToolStrategy.handle_errors,默认 TrueTrue 用默认错误模板捕获所有错误;传字符串则总是用这条固定消息提示重试;传异常类型 type[Exception] 或异常元组则仅在抛出的是指定类型时用默认消息重试、其余异常照常抛出;传 Callable[[Exception], str] 可根据异常类型返回自定义消息;传 False 则不重试、让异常直接向上传播。这样设计的权衡在于:宽松捕获(True/字符串)提升鲁棒性、让 agent 自愈,但可能掩盖真实 bug;收窄到特定异常类型或直接 False 则把控制权交还调用方,便于在关键路径上快速失败。此外 tool_message_content 可自定义结构化输出成功时写入对话历史的那条工具消息,默认会展示结构化响应数据。
术语 handle_errors(结构化输出校验失败的错误处理策略,默认 True); StructuredOutputValidationError(schema 校验失败异常); MultipleStructuredOutputsError(返回多个结构化输出的异常); tool_message_content(自定义成功时的工具消息内容); ToolMessage(回灌错误/结果的工具消息)
📖 "Models can make mistakes when generating structured output via tool calling. LangChain provides intelligent retry mechanisms to handle these errors automatically." — Structured output
📖 "Error handling strategy for structured output validation failures. Defaults to True." — Structured output
🧪 实例 用自定义函数按异常类型返回不同提示。
python
from langchain.agents.structured_output import StructuredOutputValidationError
from langchain.agents.structured_output import MultipleStructuredOutputsError

def custom_error_handler(error: Exception) -> str:
    if isinstance(error, StructuredOutputValidationError):
        return "There was an issue with the format. Try again."
    elif isinstance(error, MultipleStructuredOutputsError):
        return "Multiple structured outputs were returned. Pick the most relevant one."
    else:
        return f"Error: {str(error)}"
🔍 追问 想让所有错误都直接抛出、完全不重试怎么配置? → 传 handle_errors=False,此时 all errors raised,异常不再被捕获。
📚 拓展阅读
QLangChain 的 middleware 是什么?它挂在 agent loop 的什么位置、和 LangGraph 是什么关系?深挖·拓展🔥高频
middleware agent-loop langgraph
⏱️ 现行
Middleware 提供了一种更紧密控制 agent 内部行为的方式,常用于:日志/分析/调试等行为追踪;prompt、tool selection、输出格式的变换;重试、fallback、提前终止逻辑;以及 rate limit、guardrails、PII detection。核心 agent loop 是「调用模型 → 让它选择工具执行 → 当不再调用工具时结束」,middleware 在这些步骤的每一步前后都暴露 hook。关键点是 middleware 并非独立运行时:hook 运行在 create_agent 返回的、已编译的 LangGraph 内部。因此你可以把整个 agent(连同全部 middleware)作为一个 node 或 subgraph 直接塞进更大的 StateGraph,而每个 middleware hook 会继续运行——HITL 中断、summarization、PII 脱敏、重试及任何自定义 hook 都随 agent node 一起「旅行」。这一设计的价值在于:当外层拓扑不只是「循环到完成」(例如先分类输入再路由到多个 agent、并行 fan-out、或用确定性步骤拼接 agent 调用)时,你无需放弃 middleware 就能把 agent 组合进图中。用法上只需把 middleware 列表传给 create_agentmiddleware 参数,它们按顺序生效。
flowchart LR
    U[User input] --> B[before hooks]
    B --> M[Model call]
    M --> T{Tools?}
    T -- yes --> X[Tool execution]
    X --> M
    T -- no --> A[after hooks]
    A --> O[Output]
术语 middleware(紧密控制 agent 内部行为的机制); agent loop(调模型→选工具→无工具则结束的核心循环); hook(在各步骤前后暴露的挂载点); StateGraph(可容纳 agent node/subgraph 的 LangGraph 图); subgraph(把整个 agent 作为子图组合)
📖 "Middleware provides a way to more tightly control what happens inside the agent." — Overview
📖 "The core agent loop involves calling a model, letting it choose tools to execute, and then finishing when it calls no more tools:" — Overview
📖 "Middleware is not a separate runtime: hooks run inside the compiled" — Overview
🧪 实例 把带 HITL middleware 的 email agent 作为节点嵌入更大的 StateGraph。
python
from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.graph import START, StateGraph

# Assumes read_email, send_email, classify_node, and route are defined elsewhere.
email_agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[read_email, send_email],
    middleware=[HumanInTheLoopMiddleware(interrupt_on={"send_email": True})],
)

graph = (
    StateGraph(AgentState)
    .add_node("classify", classify_node)
    .add_node("email_agent", email_agent)
    .add_edge(START, "classify")
    .add_conditional_edges("classify", route)
    .compile()
)
🔍 追问 HumanInTheLoopMiddlewareinterrupt_on 键是按什么匹配工具的? → 按每个工具的 .name 匹配;Python 中 @tool 装饰的函数取函数名(如 "send_email"),TypeScript 中匹配传给 tool({...}, { name })name
📚 拓展阅读
QLangChain 的 guardrails 有哪两类实现思路?如何用 before/after agent hook 做自定义护栏?深挖·拓展🔥高频
guardrails middleware before_agent after_agent
⏱️ 现行
Guardrails 通过在 agent 执行的关键点校验和过滤内容,帮助构建安全、合规的 AI 应用,用于检测敏感信息、执行内容策略、校验输出、阻止不安全行为——常见场景包括防 PII 泄漏、检测阻断 prompt injection、拦截有害内容、执行业务与合规规则、校验输出质量。护栏借助 middleware 实现,在策略性节点拦截执行:agent 开始前、完成后,或围绕模型与工具调用。实现上有两条互补路径:确定性护栏(deterministic,用 regex、关键词匹配或显式检查等基于规则的逻辑,快、可预测、成本低,但可能漏掉微妙的违规)与基于模型的护栏(model-based,用 LLM 或分类器做语义理解,能抓住规则漏掉的细微问题,但更慢更贵)。自定义护栏落地为在 agent 执行前后运行的中间件:before_agent hook 在每次调用开始时只校验一次请求,适合鉴权、限流、拦截不当请求等会话级检查;after_agent hook 在返回用户前对最终输出校验一次,适合基于模型的安全检查、质量校验或最终合规扫描。二者都可配 @hook_config(can_jump_to=["end"]) 并返回 "jump_to": "end" 提前终止。多个护栏可堆叠进 middleware 数组按顺序执行,构成分层防护(如:确定性输入过滤 → PII 保护 → 敏感工具人工审批 → 基于模型的安全检查)。
术语 deterministic guardrails(基于规则、快而廉但可能漏检的护栏); model-based guardrails(用 LLM/分类器做语义判断、慢而贵的护栏); before_agent(每次调用开始时校验一次的 hook); after_agent(返回前校验最终输出的 hook); hook_config(can_jump_to=["end"])(允许 hook 跳转到 end 提前终止)
📖 "Guardrails help you build safe, compliant AI applications by validating and filtering content at key points in your agent's execution." — Guardrails
📖 "Use rule-based logic like regex patterns, keyword matching, or explicit checks. Fast, predictable, and cost-effective, but may miss nuanced violations." — Guardrails
📖 "Use LLMs or classifiers to evaluate content with semantic understanding. Catch subtle issues that rules miss, but are slower and more expensive." — Guardrails
🧪 实例 用 decorator 写一个确定性 before-agent 护栏,命中禁词就直接跳到 end。
python
from typing import Any

from langchain.agents.middleware import before_agent, AgentState, hook_config
from langgraph.runtime import Runtime

banned_keywords = ["hack", "exploit", "malware"]

@before_agent(can_jump_to=["end"])
def content_filter(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    """Deterministic guardrail: Block requests containing banned keywords."""
    if not state["messages"]:
        return None

    first_message = state["messages"][0]
    if first_message.type != "human":
        return None

    content = first_message.content.lower()

    for keyword in banned_keywords:
        if keyword in content:
            return {
                "messages": [{
                    "role": "assistant",
                    "content": "I cannot process requests containing inappropriate content. Please rephrase your request."
                }],
                "jump_to": "end"
            }

    return None
🔍 追问 基于模型的 after-agent 护栏判定 UNSAFE 后一般怎么处理? → 示例里让 safety_model 只回 'SAFE'/'UNSAFE',若含 "UNSAFE" 就把 last_message.content 替换成拒答文案再返回。
📚 拓展阅读
QPIIMiddleware 支持哪些处理策略?apply_to_input/apply_to_output 有何区别?深挖·拓展中频
PIIMiddleware pii redact mask
⏱️ 现行
LangChain 提供内置的 PII middleware 用于检测和处理对话中的个人可识别信息(PII),可识别 email、credit_card(Luhn 校验)、ip、mac_address、url 等内置类型,也支持通过 detector 传自定义函数或 regex。它对检测到的 PII 支持四种策略:redact 替换为 [REDACTED_{PII_TYPE}]mask 部分遮蔽(如只留后 4 位);hash 替换为确定性哈希;block 检测到即抛异常。作用范围由三个开关控制:apply_to_input(模型调用前检查用户消息,默认 True)、apply_to_output(模型调用后检查 AI 消息,默认 False)、apply_to_tool_results(工具执行后检查工具结果消息,默认 False)。权衡在于:redact/mask/hash 保留对话可继续进行而抹去敏感信息,适合合规日志脱敏;block 最严格、直接阻断,适合绝不允许出现的凭据(如用 regex sk-[a-zA-Z0-9]{32} 拦 API key)。此外在 langchain>=1.3.2 下开启 apply_to_output=True 时,PIIMiddleware 还会通过注册的 stream transformer 对流式 wire output(文本增量、工具调用参数、工具输出、state 快照)做脱敏。该中间件适用于医疗、金融等有合规要求、需清洗日志或处理敏感用户数据的场景。
术语 PIIMiddleware(内置 PII 检测处理中间件); redact(替换为 [REDACTED_{PII_TYPE}]); mask(部分遮蔽如后 4 位); hash(确定性哈希替换); block(检测即抛异常); apply_to_input/apply_to_output/apply_to_tool_results(输入/输出/工具结果的作用范围开关)
📖 "LangChain provides built-in middleware for detecting and handling Personally Identifiable Information (PII) in conversations. This middleware can detect common PII types like emails, credit cards, IP addresses, and more." — Guardrails
🧪 实例 对不同 PII 类型分别用 redact/mask/block 三种策略。
python
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware


agent = create_agent(
    model="gpt-5.5",
    tools=[customer_service_tool, email_tool],
    middleware=[
        PIIMiddleware("email", strategy="redact", apply_to_input=True),
        PIIMiddleware("credit_card", strategy="mask", apply_to_input=True),
        PIIMiddleware("api_key", detector=r"sk-[a-zA-Z0-9]{32}", strategy="block", apply_to_input=True),
    ],
)
🔍 追问 不传 strategy 时默认是哪种? → 默认 "redact"(配置表中 strategy 的 Default 即 "redact")。
📚 拓展阅读
Q如何用 HumanInTheLoopMiddleware 为敏感工具加人工审批?为什么需要 checkpointer 和 thread_id?深挖·拓展中频
HumanInTheLoop interrupt checkpointer
⏱️ 现行
LangChain 提供内置的 human-in-the-loop middleware,在执行敏感操作前要求人工批准,这是应对高风险决策最有效的护栏之一,适用于金融交易与转账、删除或修改生产数据、向外部方发送通信等有重大业务影响的操作。配置核心是 interrupt_on,它是一张「工具名 → 是否需审批」的表:对 send_emaildelete_databaseTrue 表示需批准,对 searchFalse 表示安全操作自动放行。机制上 agent 会在执行需审批工具前暂停并等待批准。因为暂停要跨越一次「中断—恢复」,必须持久化 state:示例传入 checkpointer=InMemorySaver() 保存中断间状态,并要求提供 thread ID(config = {"configurable": {"thread_id": "some_id"}})来标识会话。第一次 invoke 触发中断,随后用相同 thread ID 调用 Command(resume={"decisions": [{"type": "approve"}]}) 恢复被暂停的对话。这样设计把「是否放行」的最终决定权交给人,权衡是牺牲了全自动的吞吐,换来对不可逆或高影响操作的可控性;可与 PII、内容过滤、模型安全检查等其它护栏分层堆叠。
术语 HumanInTheLoopMiddleware(敏感操作前要求人工批准的中间件); interrupt_on(工具名→是否需审批的映射); checkpointer(跨中断持久化 state,如 InMemorySaver); thread_id(标识并恢复会话的线程 ID); Command(resume=...)(携带审批决定恢复被暂停对话)
📖 "LangChain provides built-in middleware for requiring human approval before executing sensitive operations. This is one of the most effective guardrails for high-stakes decisions." — Guardrails
🧪 实例 对 send_email/delete_database 需审批、search 自动放行,并用 checkpointer + thread_id 恢复。
python
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command


agent = create_agent(
    model="gpt-5.5",
    tools=[search_tool, send_email_tool, delete_database_tool],
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "send_email": True,
                "delete_database": True,
                "search": False,
            }
        ),
    ],
    checkpointer=InMemorySaver(),
)

config = {"configurable": {"thread_id": "some_id"}}
result = agent.invoke(
    {"messages": [{"role": "user", "content": "Send an email to the team"}]},
    config=config
)
result = agent.invoke(
    Command(resume={"decisions": [{"type": "approve"}]}),
    config=config
)
🔍 追问 若不配 checkpointer 会怎样? → 中断间的 state 无法持久化;HITL 依赖 thread ID 做持久化以在同一线程上恢复被暂停的对话,缺少持久化就无法完成暂停后再恢复的审批流程。
📚 拓展阅读
🔥高频

短期与长期记忆

Short-term memory Long-term memory
Q短期记忆(short-term memory)和长期记忆(long-term memory)在 LangChain 里到底有什么区别?深挖·拓展🔥高频
memory short-term long-term
⏱️ 现行
两者的根本差异在于作用域(scope)与持久化粒度。短期记忆让应用在单个 thread(会话)内记住之前的交互,它由 agent 的 state 承载(核心是 messages 键),通过 checkpointer 把 state 持久化到数据库或内存,从而可以随时恢复该 thread;它在 agent 被调用或某个步骤(如一次 tool 调用)完成时更新,并在每个步骤开始时被读取。长期记忆则跨会话、跨 session 存储和召回信息:它不绑定单个 thread,而是建立在 LangGraph stores 之上,把数据以 JSON 文档形式按 namespace 和 key 组织。权衡在于:短期记忆天然承担"对话历史"这一最常见形态,但长对话可能超出 LLM 的上下文窗口;长期记忆解决的是"跨线程记住用户偏好/应用级数据"的问题,需要显式的 store 读写而不是自动进入每一步的 state。选择哪一个,取决于你要记住的信息是只属于当前这一场对话,还是要在下一次全新会话里也能被召回。
术语 thread(会话线程,像邮件把一组消息归到同一对话); checkpointer(把 state 持久化以便恢复 thread 的组件); state(agent 短期记忆的载体,含 messages); store(长期记忆的底层存储,按 namespace/key 存 JSON)
📖 "Short term memory lets your application remember previous interactions within a single thread or conversation." — Short-term memory
📖 "Unlike short-term memory, which is scoped to a single thread, long-term memory persists across threads and can be recalled at any time." — Long-term memory
🧪 实例
flowchart TD
    subgraph 短期记忆
      T1["thread_id=1
state.messages"] --> CP[(checkpointer)] end subgraph 长期记忆 S[(store
namespace/key → JSON)] end A[agent] --> T1 A -.跨会话读写.-> S T2["thread_id=2 全新会话"] -.仍可召回.-> S
🔍 追问 短期记忆什么时候会被更新和读取? → 在 agent 被调用或一个步骤(如 tool 调用)完成时更新,并在每个步骤开始时读取 state。
📚 拓展阅读
Q怎样给一个 agent 加上短期记忆?生产环境应该怎么配 checkpointer?深挖·拓展🔥高频
checkpointer create_agent persistence
⏱️ 现行
给 agent 加短期记忆(即 thread 级持久化)的唯一入口是在 create_agent 时传入一个 checkpointer。LangChain 把短期记忆作为 agent state 的一部分来管理,把这些数据存进 graph 的 state 后,agent 既能拿到某个对话的完整上下文,又能在不同 thread 之间保持隔离;state 再通过 checkpointer 持久化到数据库(或内存),这样 thread 可以随时恢复。开发/演示时可以用 InMemorySaver,但它只存在内存字典里;生产环境应使用数据库支撑的 checkpointer,例如 PostgresSaver.from_conn_string(DB_URI),并调用 checkpointer.setup() 自动建表。调用时通过 thread_config = {"configurable": {"thread_id": "1"}} 指定 thread_id,同一个 thread_id 的多次 invoke 才会共享记忆——这正是"我叫 Bob"之后再问"我叫什么"能答对的原因。权衡点:InMemorySaver 零依赖但进程重启即丢;Postgres/SQLite/Cosmos DB 等后端换来跨进程、可恢复的持久性。
术语 checkpointer(state 持久化组件); InMemorySaver(内存字典型 checkpointer,非生产用); PostgresSaver(数据库支撑的 checkpointer); thread_id(标识会话,同 id 共享短期记忆)
📖 "To add short-term memory (thread-level persistence) to an agent, you need to specify a checkpointer when creating an agent." — Short-term memory
📖 "State is persisted to a database (or memory) using a checkpointer so the thread can be resumed at any time." — Short-term memory
🧪 实例
python
from langchain.agents import create_agent
from langgraph.checkpoint.postgres import PostgresSaver

def get_user_info() -> str:
    """Look up information about the current user."""
    return "No user profile on file."

DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    checkpointer.setup() # auto create tables in PostgreSQL
    agent = create_agent(
        "gpt-5.5",
        tools=[get_user_info],
        checkpointer=checkpointer,
    )
🔍 追问 为什么不同 thread 之间记忆不会串? → 因为短期记忆存在 graph state 里,agent 在拿到某会话完整上下文的同时,对不同 thread 保持隔离(靠 thread_id 区分)。
📚 拓展阅读
Q长对话会撑爆上下文窗口,LangChain 给了哪些常见处理策略?各自的取舍是什么?深挖·拓展🔥高频
context-window trim summarize middleware
⏱️ 现行
开启短期记忆后,消息列表会随对话增长,长对话可能超出 LLM 的上下文窗口——即使模型支持很长的上下文,大多数 LLM 在长上下文下表现仍然很差,会被陈旧或跑题内容"分心",同时响应更慢、成本更高。官方给出四类常见方案:Trim messages(调用 LLM 前按 token 数截断,保留首条 + 最近 N 条,用 @before_model 中间件实现);Delete messages(用 RemoveMessage 从 LangGraph state 里永久删除指定消息或全部消息);Summarize messages(用 chat model 把早期历史压成摘要替换掉,内置 SummarizationMiddleware);以及自定义策略(如消息过滤)。核心取舍在于:trim 和 delete 简单快速,但正如原文指出的,culling 消息队列可能丢失信息;summarize 更复杂、要多花一次模型调用,但能在压缩体积的同时尽量保留信息。删除时还必须保证结果消息历史合法——比如某些 provider 要求以 user 消息开头,带 tool call 的 assistant 消息后必须跟对应的 tool 结果消息。
术语 @before_model(模型调用前运行的中间件装饰器); RemoveMessage(从 state 删消息的标记); REMOVE_ALL_MESSAGES(删除全部消息的哨兵 id); SummarizationMiddleware(内置摘要中间件); add_messages reducer(让 RemoveMessage 生效所需的 state reducer)
📖 "Even if your model supports the full context length, most LLMs still perform poorly over long contexts." — Short-term memory
📖 "The problem with trimming or removing messages, as shown above, is that you may lose information from culling of the message queue." — Short-term memory
🧪 实例
python
from langchain.agents.middleware import SummarizationMiddleware

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[
        SummarizationMiddleware(
            model="gpt-5.4-mini",
            trigger=("tokens", 4000),
            keep=("messages", 20)
        )
    ],
    checkpointer=checkpointer,
)
🔍 追问RemoveMessage 删消息为什么要求 state key 带 add_messages reducer? → 因为 RemoveMessage 要靠 add_messages reducer 才能对消息列表生效,默认的 AgentState 已经提供了这一点。
📚 拓展阅读
Q长期记忆是怎么存的?namespace 和 key 起什么作用?深挖·拓展🔥高频
store namespace key json
⏱️ 现行
长期记忆建立在 LangGraph stores 之上,把数据保存为按 namespace 和 key 组织的 JSON 文档。每条记忆被放在一个自定义 namespace(类似文件夹)下,并有一个独立的 key(像文件名);namespace 里常包含 user ID、org ID 或其它标签,便于组织信息。这种结构带来层级化的记忆组织,而跨 namespace 的检索则通过内容 filter 来支持。用法上,先 create_agent(store=store) 把 store 传给 agent;store 提供 put(namespace, key, value) 写入、get(namespace, key) 按 ID 取回、search(namespace, filter=..., query=...) 在 namespace 内按内容等值过滤并按向量相似度排序。开发用 InMemoryStore(存内存字典),生产用 DB 支撑的 store 如 PostgresStore。若要做语义检索,可给 store 配 IndexConfig(embed=embed, dims=...) 提供 embedding 函数。权衡在于:namespace/key 的平面 put/get 简单直接,而配置了索引的 search 能按向量相似度召回,代价是要接入 embedding。
术语 namespace(记忆分组,似文件夹,常含 user/org id); key(namespace 内的唯一键,似文件名); put/get/search(store 的写入/按 id 取/按内容+相似度检索); IndexConfig(配置 embedding 与维度以支持向量检索)
📖 "Long-term memory is built on LangGraph stores, which save data as JSON documents organized by namespace and key." — Long-term memory
📖 "Each memory is organized under a custom namespace (similar to a folder) and a distinct key (like a file name)." — Long-term memory
📖 "This structure enables hierarchical organization of memories. Cross-namespace searching is then supported through content filters." — Long-term memory
🧪 实例
python
from langgraph.store.base import IndexConfig
from langgraph.store.memory import InMemoryStore

store = InMemoryStore(index=IndexConfig(embed=embed, dims=2))
user_id = "my-user"
application_context = "chitchat"
namespace = (user_id, application_context)
store.put(
    namespace,
    "a-memory",
    {
        "rules": [
            "User likes short, direct language",
            "User only speaks English & python",
        ],
        "my-key": "my-value",
    },
)
item = store.get(namespace, "a-memory")
items = store.search(
    namespace, filter={"my-key": "my-value"}, query="language preferences"
)
🔍 追问 想跨不同 namespace 找记忆怎么办? → 通过内容 filter 做跨 namespace 检索(content filters),这正是 namespace/key 层级结构支持的能力。
📚 拓展阅读
Q工具(tool)里怎么读写 agent 的记忆?短期 state 和长期 store 分别怎么访问?深挖·拓展中频
tools ToolRuntime state store
⏱️ 现行
无论短期还是长期记忆,工具都通过 runtime(类型 ToolRuntime)参数访问,而这个参数对模型是隐藏的——它不出现在 tool 签名里,所以模型看不到,但工具内部可以用它拿到 state 和 store。短期记忆:读用 runtime.state["..."];写则从工具直接返回 state 更新(常用 Command(update={...})),这对持久化中间结果、把信息传给后续工具或 prompt 很有用。长期记忆:通过 runtime.store 读写,runtime.store.get((namespace,), key) 取回一个带 value 和 metadata 的对象,runtime.store.put((namespace,), key, data) 写入;user_id 之类往往从 runtime.context 取。要让工具能访问 store,必须在 create_agent(store=store) 时把同一个 store 传进去。权衡:短期 state 写入随对话在 thread 内自动流转、随 checkpointer 落盘;长期 store 写入跨会话可召回,但需要显式 namespace/key 管理和显式 put/get。
术语 ToolRuntime(工具运行时,承载 state/store/context,对模型隐藏); runtime.state(读短期记忆); Command(update=...)(从工具返回 state 更新); runtime.store(读写长期记忆); runtime.context(取 user_id 等上下文)
📖 "The runtime parameter is hidden from the tool signature (so the model doesn't see it), but the tool can access the state through it." — Short-term memory
📖 "To modify the agent's short-term memory (state) during execution, you can return state updates directly from the tools." — Short-term memory
🧪 实例
python
from langchain.tools import ToolRuntime, tool

@tool
def save_user_info(user_info: UserInfo, runtime: ToolRuntime[Context]) -> str:
    """Save user info."""
    assert runtime.store is not None
    store = runtime.store
    user_id = runtime.context.user_id
    # Store data in the store (namespace, key, data)
    store.put(("users",), user_id, dict(user_info))
    return "Successfully saved user info."
🔍 追问 工具里 runtime.store.get(...) 返回的是什么? → 返回一个 StoreValue 对象,带有 value 和 metadata;代码里常写 str(user_info.value) if user_info else "Unknown user"
📚 拓展阅读
Q如何扩展 agent 的 state 来存自定义字段(而不只是对话历史)?深挖·拓展中频
AgentState state_schema custom-state
⏱️ 现行
默认情况下 agent 用 AgentState 管理短期记忆,具体就是通过 messages 键承载对话历史。当你需要在 state 里额外记住结构化字段(如 user_idpreferences)时,可以继承 AgentState 定义自定义 state schema,再通过 create_agentstate_schema 参数传入。这样自定义字段既能在 invoke 时随 messages 一起传入,也能在工具里通过 runtime.state[...] 读取,从而把"用户是谁""偏好什么"这类信息和对话历史一起纳入短期记忆并随 checkpointer 持久化。取舍:扩展 state 让你在单个 thread 内携带任意结构化上下文而无需塞进 messages 文本,但它仍是短期记忆——绑定 thread,跨会话要召回的信息应改用 store。注意 messages 键之所以能正确合并增量更新,是因为默认 AgentState 自带 add_messages reducer。
术语 AgentState(默认 state,含 messages 键); state_schema(向 create_agent 传入自定义 state 的参数); CustomAgentState(继承 AgentState 增加字段的子类); add_messages reducer(合并消息更新,默认已提供)
📖 "By default, agents use AgentState to manage short term memory, specifically the conversation history via a messages key." — Short-term memory
📖 "You can extend AgentState to add additional fields." — Short-term memory
🧪 实例
python
from langchain.agents import create_agent, AgentState
from langgraph.checkpoint.memory import InMemorySaver

class CustomAgentState(AgentState):
    user_id: str
    preferences: dict

agent = create_agent(
    "gpt-5.5",
    tools=[get_user_info],
    state_schema=CustomAgentState,
    checkpointer=InMemorySaver(),
)

result = agent.invoke(
    {
        "messages": [{"role": "user", "content": "Hello"}],
        "user_id": "user_123",
        "preferences": {"theme": "dark"}
    },
    {"configurable": {"thread_id": "1"}})
🔍 追问 自定义 state 字段和长期记忆有什么关系? → 没有直接关系;自定义 state 仍是短期记忆、绑定单个 thread,要跨会话召回得用长期记忆的 store。
📚 拓展阅读

第3章 检索·多智能体·上下文

QLangChain 里 RAG 为什么存在?2-Step、Agentic、Hybrid 三种架构各自的取舍是什么?深挖·拓展🔥高频
RAG 检索架构 Agentic RAG
⏱️ 现行
RAG 的动机源于 LLM 的两个硬约束——上下文有限(无法一次吞下整个语料库)与知识静态(训练数据冻结在某个时间点),检索通过在查询时拉取相关外部知识来弥补,从而把答案 grounding 在具体信息上。LangChain 给出三种落地形态,本质是在控制力、灵活性、延迟三者间权衡:2-Step RAG 让检索永远先于生成,流程固定、可预测,最大 LLM 调用次数已知且有上限,因此延迟最稳、控制力高但灵活性低,适合 FAQ、文档机器人;Agentic RAG 把"何时检索、如何检索"的决策交给 LLM 驱动的 agent 在推理过程中动态判断,灵活性最高但延迟可变、控制力低,适合能访问多种工具的研究助手;Hybrid RAG 折中,插入查询增强、检索校验、答案校验等中间步骤并允许多轮迭代,控制与灵活各占一半。需要注意官方特别提示:2-Step 延迟"更可预测"的前提是 LLM 推理为主导因素,真实延迟还会受检索步骤的 API 响应、网络、数据库查询影响。
术语 Retrieval-Augmented Generation (RAG)(检索增强生成,用外部知识增强 LLM 回答); 2-Step RAG(检索先于生成的固定两步流水线); Agentic RAG(由 agent 动态决定检索时机与方式); Hybrid RAG(带校验与迭代的混合架构)
📖 "Retrieval addresses these problems by fetching relevant external knowledge at query time. This is the foundation of Retrieval-Augmented Generation (RAG): enhancing an LLM’s answers with context-specific information." — Retrieval
📖 "In 2-Step RAG, the retrieval step is always executed before the generation step." — Retrieval
🧪 实例 三种架构在数据流上的差异,可用最简 mermaid 对照 2-Step 与 Agentic 的核心区别:
flowchart LR
  subgraph TwoStep[2-Step RAG]
    A1[User Question] --> A2[Retrieve] --> A3[Generate] --> A4[Answer]
  end
  subgraph Agentic[Agentic RAG]
    B1[Question] --> B2[Agent LLM]
    B2 --> B3{Need info?}
    B3 -- Yes --> B4[Search tool]
    B4 --> B2
    B3 -- No --> B5[Final answer]
  end
🔍 追问 Agentic RAG 让一个普通 agent 具备检索能力,最少需要什么? → 只需要给它一个或多个能获取外部知识的 tool(如文档加载器、Web API、数据库查询),无需在回答前预先检索。
📚 拓展阅读
QAgentic RAG 的工作机制是什么?它和固定流水线相比强在哪、代价是什么?深挖·拓展🔥高频
Agentic RAG tool-calling agent-loop
⏱️ 现行
Agentic RAG 把 RAG 与 agent 的分步推理结合起来:不再在回答前一次性检索,而是由 LLM 驱动的 agent 边推理边判断何时以及如何检索。机制上,agent 在 agent loop 中反复"模型调用 → 判断是否需要外部信息 → 调用检索工具 → 判断信息是否足够 → 不够则回到模型继续、足够则生成最终答案"。它的强项是灵活:面对开放式、需要多源或多轮取证的问题能自适应,只要给足工具就能覆盖各种知识来源;代价是延迟可变、控制力弱——因为 LLM 调用次数不再有硬上限,行为也更难预测。实现上极其轻量:官方强调启用 RAG 行为唯一需要的就是给 agent 挂上能取外部知识的 tool,例如示例中一个 fetch_url/fetch_documentation 工具加一段 system prompt 即可,扩展示例还会先把 llms.txt 的内容预取进 system prompt(这一步不需要 LLM 请求),并用 ALLOWED_DOMAINS 白名单约束抓取范围以防越权。
术语 agent loop(模型调用与工具执行交替的循环,直到 LLM 决定结束); create_agent(LangChain 构造 agent 的入口); fetch_documentation(示例中动态抓取并转换文档的工具); llms.txt(列出可用文档 URL 的清单文件)
📖 "Instead of retrieving documents before answering, an agent (powered by an LLM) reasons step-by-step and decides when and how to retrieve information during the interaction." — Retrieval
📖 "The only thing an agent needs to enable RAG behavior is access to one or more tools that can fetch external knowledge—such as documentation loaders, web APIs, or database queries." — Retrieval
🧪 实例 最小 Agentic RAG——把一个抓取工具交给 agent:
python
import requests
from langchain.tools import tool
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent


@tool
def fetch_url(url: str) -> str:
    """Fetch text content from a URL"""
    response = requests.get(url, timeout=10.0)
    response.raise_for_status()
    return response.text

system_prompt = """\
Use fetch_url when you need to fetch information from a web-page; quote relevant snippets.
"""

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[fetch_url], # A tool for retrieval [!code highlight]
    system_prompt=system_prompt,
)
🔍 追问 扩展示例为什么用 ALLOWED_DOMAINS 做域名白名单? → 因为工具会按 LLM 给的 URL 真实发起网络请求,白名单把抓取限制在受信任域名内,避免 agent 被诱导访问不允许的地址。
📚 拓展阅读
Q什么是 Context Engineering?为什么它被称作 AI 工程师的头号工作?它把可控上下文分成哪几类?深挖·拓展🔥高频
上下文工程 middleware 可靠性
⏱️ 现行
官方定义 context engineering 是"以正确的格式,把正确的信息与工具提供给 LLM,好让它完成任务"。之所以是头号工作,是因为 agent 之所以不可靠,通常是内部那次 LLM 调用采取了错误动作,而根因往往不是模型能力不够,而是没有把"对的"上下文传给它——缺乏正确上下文正是可靠 agent 的头号阻碍。LangChain 把可控上下文分为三类,并按"是否跨轮持久"区分:Model Context(进入模型调用的内容——指令、消息历史、工具、响应格式,属transient,只影响单次调用)、Tool Context(工具能读写什么——对 state/store/runtime context 的读写,属 persistent)、Life-cycle Context(模型与工具调用之间发生的事——摘要、护栏、日志等,属 persistent)。这三类可控上下文都能从三种数据源取数:Runtime Context(静态配置/会话级)、State(短期记忆/会话级)、Store(长期记忆/跨会话)。落地机制统一靠 middleware——它让你钩入 agent 生命周期的任意步骤,去更新上下文或跳转到不同步骤。取舍上官方给出的实践原则是先从静态 prompt 与工具起步,只在需要时再加动态能力,一次加一个特性并监控调用数、token 与延迟。
术语 Context engineering(提供正确信息与工具的正确格式化); Transient(单次调用可见、不改 state); Persistent(跨轮保存进 state 的改动); Runtime Context / State / Store(静态配置 / 短期记忆 / 长期记忆三种数据源); middleware(钩入生命周期实现上下文工程的机制)
📖 "Context engineering is providing the right information and tools in the right format so the LLM can accomplish a task." — Context engineering in agents
📖 "When agents fail, it's usually because the LLM call inside the agent took the wrong action / didn't do what we expected." — Context engineering in agents
🧪 实例 三类可控上下文与三种数据源的关系可视化:
flowchart TD
  subgraph Controllable[可控上下文]
    MC[Model Context
transient] TC[Tool Context
persistent] LC[Life-cycle Context
persistent] end subgraph Sources[数据源] RC[Runtime Context
会话级静态配置] ST[State
短期记忆] SR[Store
长期记忆] end MC --> RC & ST & SR TC --> RC & ST & SR LC --> ST & SR
🔍 追问 三种数据源的作用域(scope)分别是什么? → Runtime Context 与 State 都是 conversation-scoped(会话内),Store 是 cross-conversation(跨会话)。
📚 拓展阅读
QModel Context 的 transient 更新和 Life-cycle 的 persistent 更新有什么区别?以 SummarizationMiddleware 为例说明。深挖·拓展中频
middleware transient-vs-persistent 摘要
⏱️ 现行
关键区别在"改动是否落进 state"。Model Context 里用 wrap_model_call 做的更新是 transient——只改单次调用发给模型的 messages/tools/prompt,不改动 state 里保存的内容,下一轮就恢复;这适合注入临时上下文(如把上传文件描述、合规规则、写作风格追加到消息末尾,因为模型对最后的消息关注度更高)而不污染历史。而 Life-cycle Context 的改动是 persistent:通过 before_model/after_model/wrap_tool_call 等生命周期钩子或返回带 CommandExtendedModelResponse,把变化永久写进 state。最典型的持久化模式是摘要:SummarizationMiddleware 在对话超过 token 阈值时,用一次独立 LLM 调用把较早的消息压缩成摘要,永久替换掉原消息并保留最近若干条,之后所有轮次看到的都是摘要而非原文。取舍上,transient 便宜且安全(不改历史、可随时回退),但每轮都要重算;persistent 一次性削减长期 token 成本,但不可逆地丢失了原始细节。
术语 wrap_model_call(包裹单次模型调用、做 transient 修改的中间件装饰器); SummarizationMiddleware(内置摘要中间件,按 token 触发、保留最近消息); Command(用于从中间件写 state / 控制流程的对象); trigger / keep(摘要的触发 token 阈值与保留消息数配置)
📖 "The examples above use wrap_model_call to make transient updates - modifying what messages are sent to the model for a single call without changing what's saved in state." — Context engineering in agents
📖 "The summarized conversation history is permanently updated - future turns will see the summary instead of the original messages." — Context engineering in agents
🧪 实例 用内置摘要中间件在超过 4000 token 时压缩历史、保留最近 20 条:
python
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[
        SummarizationMiddleware(
            model="gpt-5.4-mini",
            trigger={"tokens": 4000},
            keep={"messages": 20},
        ),
    ],
)
🔍 追问 想在 model call 层做持久化的 state 更新而不只是 transient,怎么办? → 从 wrap_model_call 返回一个带 CommandExtendedModelResponse 来注入 state 更新,或改用 before_model/after_model/wrap_tool_call 等生命周期钩子更新对话历史。
📚 拓展阅读
QMCP 是什么?LangChain 怎么接入 MCP 工具?MultiServerMCPClient 的有状态/无状态语义要注意什么?深挖·拓展🔥高频
MCP langchain-mcp-adapters transport session
⏱️ 现行
MCP(Model Context Protocol)是一个开放协议,标准化了应用如何向 LLM 提供工具与上下文;LangChain 通过 langchain-mcp-adapters 库让 agent 使用一个或多个 MCP 服务器上定义的工具,client.get_tools() 会把 MCP 工具转换成 LangChain 工具直接喂给 create_agent。核心机制要抓住两点。其一是会话语义MultiServerMCPClient 默认无状态——每次工具调用都新建一个 MCP ClientSession、执行、再清理;即便 stdio 传输本身是有状态的(子进程在连接生命周期内持续存在),在不显式管理会话时每次调用仍会新建 session。若需要跨调用维持上下文(有状态服务器),要用 client.session() 显式创建持久 ClientSession。其二是传输http(即 streamable-http)用 HTTP 请求通信、适合远程服务器,可通过 headers 传认证头、通过实现 httpx.Auth 做自定义认证;stdio 把服务器作为子进程用标准输入输出通信、适合本地工具。错误处理上默认很"温柔":MCP 工具执行出错(CallToolResult(isError=True))会以 status="error" 的 tool message 回传给模型让其重试,而不是抛异常;只有传输、会话、内容转换失败才总是抛出。想让工具执行错误也抛异常,可设 handle_tool_errors=False
术语 MultiServerMCPClient(一次连接多个 MCP 服务器的客户端,默认无状态); stdio / http(streamable-http)(子进程标准输入输出 / HTTP 两种传输); ClientSession(MCP 会话,有状态服务器需 client.session() 显式持有); handle_tool_errors(控制工具执行错误是回传还是抛异常)
📖 "Model Context Protocol (MCP) is an open protocol that standardizes how applications provide tools and context to LLMs." — Model Context Protocol (MCP)
📖 "MultiServerMCPClient is stateless by default. Each tool invocation creates a fresh MCP ClientSession, executes the tool, and then cleans up." — Model Context Protocol (MCP)
🧪 实例 一个客户端同时接 stdio 的 math 服务器和 http 的 weather 服务器:
python
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

async def main():
    client = MultiServerMCPClient(
        {
            "math": {
                "transport": "stdio",
                "command": "python",
                "args": ["/path/to/math_server.py"],
            },
            "weather": {
                "transport": "http",
                "url": "http://localhost:8000/mcp",
            }
        }
    )
    tools = await client.get_tools()
    agent = create_agent("claude-sonnet-4-6", tools)
🔍 追问 把 MCP 工具错误回传给模型的能力对库版本有要求吗? → 需要 langchain-mcp-adapters>=0.3.0,更早的版本会抛 ToolException
📚 拓展阅读
QMCP tool interceptor 解决什么问题?它的"洋葱"组合顺序与错误处理语义是怎样的?深挖·拓展中频
MCP interceptor runtime-context 重试
⏱️ 现行
MCP 服务器是独立进程,拿不到 LangGraph 的 store、context、agent state 等运行时信息;interceptor 正是用来桥接这道鸿沟——在 MCP 工具执行期间把 ToolRuntime 上下文(tool call ID、state、config、store)暴露给你,从而实现注入用户凭据、按存储偏好个性化参数、按认证状态拦截敏感工具等模式,同时它还提供类中间件的控制力:改请求、加重试、动态加头、甚至短路执行。写法是一个 async 函数接收 requesthandler,可在调 handler 前改请求(用不可变的 request.override())、调后改响应、或完全跳过 handler;返回 Command 还能更新 state 或用 goto="__end__" 提前结束。组合上多个 interceptor 遵循洋葱顺序:列表中第一个是最外层,执行顺序为 outer before → inner before → 工具执行 → inner after → outer after。错误语义要特别小心:工具执行错误(CallToolResult(isError=True))默认不抛异常,所以靠捕获异常的 interceptor 不会对它触发;要在这里把它当异常捕获,需设 handle_tool_errors=False。真正会抛的传输/运行时失败才适合用重试 interceptor(如指数退避)兜底。
术语 MCPToolCallRequest(interceptor 收到的请求对象,带 name/args/runtime); request.override()(不可变地生成修改后的请求); onion order(第一个 interceptor 为最外层的组合顺序); handle_tool_errors=False(让工具执行错误抛异常以便被捕获)
📖 "MCP servers run as separate processes—they can't access LangGraph runtime information like the store, context, or agent state. Interceptors bridge this gap by giving you access to this runtime context during MCP tool execution." — Model Context Protocol (MCP)
📖 "Tool execution errors (CallToolResult(isError=True)) do not raise by default, so exception-catching interceptors never trigger on them. To catch those as exceptions here, set handle_tool_errors=False." — Model Context Protocol (MCP)
🧪 实例 一个把用户凭据注入 MCP 工具参数的 interceptor:
python
from dataclasses import dataclass
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langchain.agents import create_agent

@dataclass
class Context:
    user_id: str
    api_key: str

async def inject_user_context(request: MCPToolCallRequest, handler):
    """Inject user credentials into MCP tool calls."""
    runtime = request.runtime
    user_id = runtime.context.user_id
    modified_request = request.override(
        args={**request.args, "user_id": user_id}
    )
    return await handler(modified_request)

client = MultiServerMCPClient({...}, tool_interceptors=[inject_user_context])
🔍 追问 tool_interceptors=[outer_interceptor, inner_interceptor] 的实际执行打印顺序是什么? → outer: before -> inner: before -> tool execution -> inner: after -> outer: after,即列表第一个是最外层。
📚 拓展阅读
中频

多智能体·HITL·流式

Multi-agent Human-in-the-loop Streaming
Q什么时候才真正需要多智能体(multi-agent)?开发者说要"多智能体"时,本质上想解决什么问题?深挖·拓展🔥高频
multi-agent context-engineering
⏱️ 现行
多智能体系统通过协调多个专精组件来完成复杂工作流,但文档明确提醒:并非每个复杂任务都需要它——一个配了合适(有时是动态)工具与 prompt 的单一 agent 往往能达到相似效果,所以引入多智能体前要先确认收益。开发者口中的"多智能体"通常是在追求三类能力:Context management(在不撑爆上下文窗口的前提下提供专精知识,因为上下文既不是无限也不是零延迟,必须有模式来"按需只暴露相关信息")、Distributed development(让不同团队以清晰边界各自独立开发维护能力再组合)、以及 Parallelization(为子任务派生专精 worker 并发执行以加速)。典型适用场景是:单 agent 工具太多而选错工具、任务需要"长 prompt + 领域工具"的专精知识、或需要强制"满足条件才解锁能力"的顺序约束。设计的核心是 context engineering——决定每个 agent 能看到什么信息,系统质量取决于每个 agent 是否拿到了它任务所需的正确数据。

flowchart TD
    U[用户需求] --> Q{单 agent 能否胜任?}
    Q -->|工具少/上下文可控| S[单 agent + 动态工具/prompt]
    Q -->|工具过多/需专精/需并行| M[多智能体]
    M --> C[Context management 按需暴露知识]
    M --> D[Distributed development 团队独立维护]
    M --> P[Parallelization 并发子任务]
术语 Context management(上下文管理,按需只暴露相关信息避免撑爆窗口); Distributed development(分布式开发,团队各自维护能力再组合); Parallelization(并行化,为子任务派生 worker 并发执行); context engineering(上下文工程,决定每个 agent 看到什么)
📖 "Multi-agent systems coordinate specialized components to tackle complex workflows. However, not every complex task requires this approach—a single agent with the right (sometimes dynamic) tools and prompt can often achieve similar results." — Multi-agent
📖 "Here are the main patterns for building multi-agent systems, each suited to different use cases:" — Multi-agent
🧪 实例 面试常见反问"要不要上多智能体?"——若一个 agent 挂了几十个工具、经常选错,或需要 Python/JS/Rust 各带约 2000 tokens 文档的专精知识,就是引入多智能体的信号;反之简单聚焦任务应留在单 agent。文档给出五种可组合的模式:Subagents、Handoffs、Skills、Router、Custom workflow,且强调"You can mix patterns"(子代理架构可以调用运行 custom workflow 或 router 的工具)。
🔍 追问 五种模式里哪个支持"子代理直接与用户对话(Direct user interaction)"? → 按选择表,Handoffs 和 Skills 是 ⭐⭐⭐⭐⭐,Subagents 只有 ⭐(子代理不直接面向用户,结果回流主 agent)。
📚 拓展阅读
QSubagents、Handoffs、Skills、Router 在模型调用次数与 token 上有什么权衡?如何按场景选型?深挖·拓展🔥高频
multi-agent performance trade-off
⏱️ 现行
文档用三个场景量化了各模式的开销。One-shot("Buy coffee"):Handoffs、Skills、Router 各 3 次模型调用最省,Subagents 要 4 次——因为结果要经主 agent 回流,多出的一次换来集中式控制。Repeat request(重复请求):有状态的 Handoffs / Skills 因为 agent 或 skill 上下文已"存活/已加载",第二轮只需 2 次调用,比无状态模式省 40–50%;Subagents 是 stateless by design,每次重跑完整流程(4+4=8 次),强隔离但成本恒定;Router 也是无状态,每次都要一次 LLM 路由调用。Multi-domain(并行对比多语言):能并行的 Subagents、Router 最高效(各 5 次、约 9K tokens);Skills 调用次数最少(3 次)但因上下文累积 token 反而最高(约 15K,每次都要处理全部 skill 文档);Handoffs 必须顺序执行、无法并行咨询多个领域,达 7+ 次、约 14K+ tokens,最不划算。因此选型逻辑是:单次/重复请求偏向 Handoffs、Skills、Router;需要并行执行或大上下文领域隔离偏向 Subagents、Router;简单聚焦任务用 Skills。
术语 Model calls(模型调用次数,越多延迟与单请求 API 成本越高); Tokens processed(处理 token 总量,跨所有调用的上下文占用); stateless by design(Subagents 天生无状态,每次调用重走同一流程); context isolation(上下文隔离,子代理只带自身相关上下文)
📖 "Handoffs, Skills, and Router are most efficient for single tasks (3 calls each). Subagents adds one extra call because results flow back through the main agent—this overhead provides centralized control." — Multi-agent
📖 "Stateful patterns (Handoffs, Skills) save 40-50% of calls on repeat requests. Subagents maintain consistent cost per request—this stateless design provides strong context isolation but at the cost of repeated model calls." — Multi-agent
📖 "For multi-domain tasks, patterns with parallel execution (Subagents, Router) are most efficient." — Multi-agent
🧪 实例 文档汇总表(三场景对比)——
text
Pattern     One-shot   Repeat request   Multi-domain
Subagents   4 calls    8 calls (4+4)    5 calls, 9K tokens
Handoffs    3 calls    5 calls (3+2)    7+ calls, 14K+ tokens
Skills      3 calls    5 calls (3+2)    3 calls, 15K tokens
Router      3 calls    6 calls (3+3)    5 calls, 9K tokens
🔍 追问 为什么 Skills 在 multi-domain 里调用最少却 token 最高? → 因为 skill 文档加载后进入对话历史,"every subsequent call processes all 6K tokens of skill documentation",上下文累积导致 Subagents 因隔离反而少处理约 67% 的 token。
📚 拓展阅读
  • Multi-agent 性能对比 — one-shot/repeat/multi-domain 三场景的调用与 token 数据
  • Streaming — 多 LLM 场景下如何区分不同子代理产出的消息
QHuman-in-the-Loop 的四种 interrupt 决策类型分别是什么?reject 和 respond 有何本质区别?深挖·拓展🔥高频
human-in-the-loop middleware interrupt
⏱️ 现行
HITL middleware 为 agent 的工具调用加入人工监督:当模型提议一个可能需要复核的动作(如写文件、执行 SQL)时,中间件对照可配置策略检查每个 tool call,需要介入就发出 interrupt 暂停执行。人工决策决定接下来发生什么,共四种内建方式:approve(按原参数执行,如原样发送邮件草稿)、edit(执行前修改参数,如改收件人)、reject(完全跳过该 tool call 并把拒绝反馈返回给 agent,如拒绝删文件并说明原因)、respond(把人的消息作为合成的 tool 结果直接返回、不执行工具,用于 "ask user" 类工具)。关键区别在语义:reject 表示人在否决动作;respond 只用于人"充当工具本身"(如回答 ask_user 提示),其 message 会被当作成功的 tool 结果——所以绝不能用 respond 去否决有副作用的工具,否则模型会误以为工具已成功执行。每个工具可用的决策类型由你在 interrupt_on 里配置的策略决定;当多个 tool call 同时暂停时,每个动作需要单独决策,且决策顺序必须与动作在 interrupt 请求中出现的顺序一致。
术语 approve(批准,按原参数执行); edit(编辑,执行前改参数,改动要保守); reject(拒绝,跳过执行并返回反馈); respond(应答,把人的回复作为合成 tool 结果返回); interrupt_on(工具→允许决策类型的映射策略)
📖 "A human decision then determines what happens next: the action can be approved as-is (approve), modified before running (edit), rejected with feedback (reject), or responded to directly (respond) for "ask user" style tools." — Human-in-the-loop
📖 "Use reject when the human is denying the requested action. Use respond only when the human is acting as the tool, such as answering an ask_user prompt. Do not use respond to deny side-effecting tools, because its message is treated as a successful tool result." — Human-in-the-loop
🧪 实例 恢复执行时,决策以列表形式提供,一个动作对应一个决策:
python
agent.invoke(
    Command(
        resume={"decisions": [{"type": "approve"}]}  # or "reject"
    ),
    config=config,  # Same thread ID to resume the paused conversation
    version="v2",
)
🔍 追问 为什么文档提醒 edit 时"改动要保守"? → 因为对原始参数做重大修改可能让模型重新评估其方法,"potentially execute the tool multiple times or take unexpected actions"(多次执行工具或采取意外动作)。
📚 拓展阅读
QHITL 为什么必须配置 checkpointer?如何用 when 谓词实现"条件中断"只拦截高危调用?深挖·拓展中频
human-in-the-loop checkpointer conditional-interrupt
⏱️ 现行(`langchain>=1.3.3`)
HITL 依托 LangGraph 的持久化层:中断时图状态被保存下来,使执行可以安全暂停、稍后恢复,因此你必须配置 checkpointer 来跨 interrupt 持久化图状态——原型阶段可用 InMemorySaver,生产环境应换成 AsyncPostgresSaverMongoDBSaver 这类持久化 checkpointer,并在调用时通过 config 传入 thread ID 把执行与某个会话线程绑定,从而能暂停与恢复。默认情况下 interrupt_on 里列出的每个 tool call 都会暂停复核;若只想拦截部分调用,可给某工具的 InterruptOnConfig 加一个 when 谓词:它接收 ToolCallRequest、返回 True 表示中断、False 表示自动放行,因此可以基于工具参数来 gate。当 when 返回 False 时该调用直接运行不中断;返回 True 或省略 when 时照常暂停;而返回 False 的调用根本不会进入 interrupt 批次,所以复核者只会看到真正需要决策的动作。这样既保留了对高危动作(写工作区外路径、非 SELECT 的写 SQL)的人工把关,又避免了对安全只读操作的无谓打断。注意条件中断需要 langchain>=1.3.3
术语 checkpointer(检查点器,跨中断持久化图状态); thread ID(线程 ID,把执行绑定到某会话以便暂停恢复); when(谓词,接收 ToolCallRequest 返回是否中断); InterruptOnConfig(单工具的审批配置对象); ToolCallRequest(工具调用请求,含 args 可供 when 判断)
📖 "You must configure a checkpointer to persist the graph state across interrupts." — Human-in-the-loop
📖 "When the when predicate returns False, the call runs without interrupting. When it returns True, or when you omit when, the call pauses as usual. Calls that evaluate to False are never added to the interrupt batch, so a reviewer only sees the actions that need a decision." — Human-in-the-loop
🧪 实例 只在"写工作区外路径"或"非 SELECT 的 SQL"时暂停:
python
def is_write_query(request: ToolCallRequest) -> bool:
    """Pause SQL that isn't a read-only SELECT."""
    query = request.tool_call["args"].get("query", "")
    return not query.lstrip().upper().startswith("SELECT")
🔍 追问 执行生命周期里中断发生在哪个 hook? → middleware 定义 after_model hook,在模型生成响应之后、任何 tool call 执行之前运行;若有调用需人工输入,它构建含 action_requestsreview_configsHITLRequest 并调用 interrupt,等待人工决策后再执行、拒绝或直接返回人的回复。
📚 拓展阅读
QLangChain 流式的三种 stream mode(updates / messages / custom)各输出什么?为什么流式对 LLM 应用重要?深挖·拓展🔥高频
streaming stream-mode UX
⏱️ 现行(新应用推荐 event streaming)
流式对基于 LLM 的应用至关重要——通过在完整响应就绪前就渐进式地显示输出,流式显著改善用户体验,尤其是在应对 LLM 延迟时。LangChain 的流式系统能把 agent 运行的实时反馈推给应用,支持传给 stream/astream 的三种 stream mode:updates 在每个 agent step 后流式输出状态更新,若同一 step 有多个更新(如多个节点运行)会分别流出;messages 从任何调用了 LLM 的图节点流出 (token, metadata) 元组,用来逐 token 展示 LLM 输出与工具调用;custom 用图节点内的 stream writer 流出用户自定义数据(如 "Fetched 10/100 records" 进度)。可以用列表同时指定多种模式(如 stream_mode=["updates", "custom"])。值得注意的是文档为新应用推荐 v1.3 引入的 event streaming——它按投影(messages、values、tool calls、subgraphs)给出各自独立的迭代器,让你独立消费而不必再对 stream_mode chunk 分支判断。传 thread_id(配合 checkpointer)可让对话被检查点化,后续轮次能恢复同一历史。
术语 updates(每个 agent step 后的状态更新); messages(LLM 逐 token 输出,(token, metadata) 元组); custom(节点内 stream writer 发出的自定义数据); event streaming(v1.3 引入的按投影分离迭代器的 API); get_stream_writer(在工具内获取 writer 以流出自定义更新)
📖 "Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs." — Streaming
📖 "To stream tokens as they are produced by the LLM, use stream_mode="messages"." — Streaming
🧪 实例 工具内用 stream writer 流出自定义进度:
python
from langgraph.config import get_stream_writer

def get_weather(city: str) -> str:
    """Get weather for a given city."""
    writer = get_stream_writer()
    writer(f"Looking up data for city: {city}")
    writer(f"Acquired data for city: {city}")
    return f"It's always sunny in {city}!"
🔍 追问 想同时拿"逐 token 的部分 JSON"和"解析完成的 tool calls"该怎么配? → 若消息被 state 跟踪,用 stream_mode=["messages", "updates"]:messages 流增量 chunk、updates 流状态里的完成消息;否则用 custom updates 或在循环里聚合 chunk(按 chunk_position == "last" 判断结束)。
📚 拓展阅读
Q流式如何与 HITL 中断协同?多 LLM(子代理)场景下怎样区分 token 来源?深挖·拓展中频
streaming human-in-the-loop sub-agents
⏱️ 现行
流式与 HITL 可以协同:你可以在 agent 运行并处理中断的同时用 stream_events() 流出实时更新——用 stream.messages 流 LLM token,用 stream.values 检查 agent 状态快照里的中断;run 是否因人工输入而暂停可由 stream.interrupted 判断,stream.interrupts 取出待复核动作,随后用 Command(resume=...) 携带决策在同一流式循环里恢复。若用较低层的 agent.stream(..., stream_mode=["messages", "updates"]),则在 "updates" 模式下收集 __interrupt__ 源产生的中断,再对每个中断给出与动作同序的决策(可对一个 tool call 选 edit、对另一个选 approve),最后把 Command(resume=decisions) 传回相同的循环恢复。对于多 LLM 场景,当一个 agent 中任何位置存在多个 LLM 时,往往需要在消息生成时区分其来源:做法是给每个 agent 创建时传 name,该名字随后在 "messages" 模式的 metadata 里以 lc_agent_name 键出现,并且会附加到该 agent 产生的 AIMessage 上;再配合 subgraphs=True 就能在流里追踪当前活跃的是 supervisor 还是某个子代理。此外若想让某些模型不逐 token 流出(如混用支持/不支持流式的模型、或部署到 LangSmith 时控制哪些 agent 输出流给客户端),可在初始化模型时设 streaming=False(不支持该参数的模型改用 disable_streaming=True)。
术语 stream_events()(事件流方法,配合 stream.messages/stream.values 消费); stream.interrupted(布尔,指示 run 是否因人工输入暂停); __interrupt__(updates 流中携带中断的源节点名); lc_agent_name(messages 元数据里标识产出 agent 的键); subgraphs=True(流出子图/子代理输出); streaming=False(关闭单个模型的 token 流式)
📖 "You can stream real-time updates while the agent runs and handles interrupts using stream_events(). Use stream.messages to stream LLM tokens and stream.values to check agent state snapshots for interrupts." — Human-in-the-loop
📖 "When there are multiple LLMs at any point in an agent, it's often necessary to disambiguate the source of messages as they are generated." — Streaming
🧪 实例 在流式循环里按 agent 名切换标签(子代理流式):
python
if agent_name := metadata.get("lc_agent_name"):
    if agent_name != current_agent:
        print(f"🤖 {agent_name}: ")
        current_agent = agent_name

配合 subgraphs=True,输出会先打印 🤖 supervisor:,进入子代理时切到 🤖 weather_agent:
🔍 追问agent.stream 处理 HITL 中断时,中断从哪个"源"收集? → 在 "updates" 模式里判断 source == "__interrupt__",把 update 追加进 interrupts 列表并渲染 action_requests 里的 description
📚 拓展阅读

第二部分 · LangGraph

第1章 基础与范式

🔥高频

概览·为什么·图式思维

LangGraph overview 原文 Thinking in LangGraph
QLangGraph 到底是什么?它在 LangChain 技术栈里处于什么位置?深挖·拓展🔥高频
定位 orchestration runtime
⏱️ 现行
LangGraph 是一个低层次的编排框架与运行时(orchestration framework and runtime),专门用来构建、管理和部署长时运行、有状态的 agent。它的设计哲学是"只做编排、不做抽象"——它不替你抽象 prompt,也不替你抽象 agent 架构,而是把注意力集中在 agent 编排真正需要的底层能力上:durable execution(持久化执行)、streaming、human-in-the-loop、comprehensive memory 以及生产级部署。正因为它非常底层,官方建议你在使用前先熟悉 models 和 tools 这些组件;如果你只是入门 agent 或想要更高层的抽象,应该直接用 LangChain 的 prebuilt agents,而不是直接写 LangGraph。权衡在于:LangGraph 给你最大的控制力和可靠性(可从失败中恢复、可长时间运行、可精确追踪状态转移),代价是需要你自己把工作流拆成节点、自己设计 state、自己接线。它可以独立使用,也可以和 LangChain / LangSmith 无缝集成。
术语 orchestration(编排,指对 agent 各步骤执行顺序与状态流转的调度); runtime(运行时,负责实际驱动图的执行与持久化); durable execution(持久化执行,可从中断处恢复); stateful(有状态的,运行过程中的数据被保存下来)
📖 "LangGraph is a low-level orchestration framework and runtime for building, managing, and deploying long-running, stateful agents." — LangGraph overview
📖 "LangGraph is focused on the underlying capabilities important for agent orchestration: durable execution, streaming, human-in-the-loop, and more." — LangGraph overview
📖 "LangGraph does not abstract prompts or architecture, and provides the following central benefits:" — LangGraph overview
🧪 实例 官方的 hello world 展示了最小图的骨架——先建 StateGraph,加节点,用 START/END 接线,再 compile
python
from langgraph.graph import StateGraph, MessagesState, START, END

def mock_llm(state: MessagesState):
    return {"messages": [{"role": "ai", "content": "hello world"}]}

graph = StateGraph(MessagesState)
graph.add_node(mock_llm)
graph.add_edge(START, "mock_llm")
graph.add_edge("mock_llm", END)
graph = graph.compile()

graph.invoke({"messages": [{"role": "user", "content": "hi!"}]})
🔍 追问 用 LangGraph 必须用 LangChain 吗? → 不必。文档明确说 "you don't need to use LangChain to use LangGraph",它由 LangChain Inc 构建但可以脱离 LangChain 独立使用;只是文档中常用 LangChain 组件来接入 models 和 tools。
📚 拓展阅读
Q"Thinking in LangGraph" 的核心思维模型是什么?如何把一个业务流程翻译成 LangGraph 图?深挖·拓展🔥高频
图式思维 node state edge
⏱️ 现行
LangGraph 的思维方式可以概括为三步:先把 agent 拆成离散步骤(nodes)再描述每个节点的决策与转移最后用一个所有节点都能读写的共享 state 把节点连起来。落到实践上,官方把它拆成固定的五步:Step 1 把工作流画成离散步骤(每步成为一个 node,即"做一件具体事情的函数");Step 2 判断每个节点属于哪类操作(LLM 步骤 / 数据步骤 / 动作步骤 / 用户输入步骤)及它需要什么上下文;Step 3 设计 state(哪些数据要跨步骤持久化就放进 state,能推导出来的就别存);Step 4 把每个节点实现为函数(拿 state、干活、返回更新);Step 5 接线,因为路由发生在节点内部,所以只需要几条必要的边。这种把控制流"显式化"的做法带来的好处是:分步拆解天然支持流式进度、可暂停可恢复的 durable execution,以及可在步骤之间检视 state 的清晰调试;权衡则是你要自己承担拆分和接线的设计成本。关键点在于——图里画的箭头只是"可能的路径",真正走哪条路的决定发生在节点内部(通常通过返回 Command 对象)。
术语 node(节点,一个做具体事情的 Python 函数); state(状态,所有节点共享的读写内存); edge(边,节点间的连接); Command(节点返回的对象,同时携带 state 更新和下一跳目标)
📖 "When you build an agent with LangGraph, you will first break it apart into discrete steps called nodes. Then, you will describe the different decisions and transitions from each of your nodes. Finally, you connect nodes together through a shared state that each node can read from and write to." — Thinking in LangGraph
📖 "The arrows in this diagram show possible paths, but the actual decision of which path to take happens inside each node." — Thinking in LangGraph
🧪 实例 客服邮件 agent 的工作流拆解(Step 1 的思维产物):
flowchart TD
    A[START] --> B[Read Email]
    B --> C[Classify Intent]
    C -.-> D[Doc Search]
    C -.-> E[Bug Track]
    C -.-> F[Human Review]
    D --> G[Draft Reply]
    E --> G
    F --> G
    G -.-> H[Human Review]
    G -.-> I[Send Reply]
    H --> J[END]
    I --> J[END]

其中 Read Email 总是走向 Classify Intent(固定转移),而 Classify IntentDraft ReplyHuman Review 会在节点内部做决策决定下一跳。
🔍 追问 为什么接线时边这么少? → 因为 "The graph structure is minimal because routing happens inside nodes through" Command 对象;文档进一步说 "Each node declares where it can go using type hints like Command[Literal["node1", "node2"]], making the flow explicit and traceable."——每个节点用类型提示声明它能去哪,使控制流显式且可追踪。
📚 拓展阅读
Q设计 state 时的关键原则是什么?为什么要"存原始数据而非格式化文本"?深挖·拓展🔥高频
state设计 raw-data prompt
⏱️ 现行
state 是所有节点共享的内存,可以把它想成 agent 边工作边记录一切所学与所决的"笔记本"。判断什么该进 state 的标准很简单:需要跨步骤持久化就放进 state;能从其它数据推导出来就在需要时现算,不要存。对邮件 agent 而言,原始邮件与发件人信息(后面无法重建)、分类结果(多个下游节点需要)、检索结果与客户数据(重新拉取代价高)、草稿回复(需要贯穿审核环节)、执行元数据(用于调试与恢复)都属于要存的内容。而一条核心原则是:state 应存储原始数据(raw data),而不是格式化文本;prompt 在需要它的节点内部再临时拼装。这样做的好处是:不同节点可以用不同方式格式化同一份数据;你可以修改 prompt 模板而不动 state schema;调试更清晰(能精确看到每个节点收到了什么数据);agent 可以演进而不破坏既有 state。权衡是:节点内部要多写一步"按需格式化"的代码,但换来的是 schema 的稳定与可演进性。
术语 state(共享内存/shared memory,跨节点读写); raw data(原始数据,未经格式化的裸数据); schema(状态结构定义,如用 TypedDict 声明); format prompts on-demand(在节点内按需拼装 prompt)
📖 "State is the shared memory accessible to all nodes in your agent." — Thinking in LangGraph
📖 "A key principle: your state should store raw data, not formatted text. Format prompts inside nodes when you need them." — Thinking in LangGraph
🧪 实例 邮件 agent 的 state 只装原始数据,分类结果直接以 LLM 返回的字典整体存入:
python
from typing import TypedDict, Literal

class EmailClassification(TypedDict):
    intent: Literal["question", "bug", "billing", "feature", "complex"]
    urgency: Literal["low", "medium", "high", "critical"]
    topic: str
    summary: str

class EmailAgentState(TypedDict):
    email_content: str
    sender_email: str
    email_id: str
    classification: EmailClassification | None
    search_results: list[str] | None   # 原始文档块,非格式化文本
    customer_history: dict | None       # CRM 原始数据
    draft_response: str | None
    messages: list[str] | None
🔍 追问 那 prompt 里的格式化文本放哪? → 放在节点函数内部按需拼装。例如 draft_response 节点会把 search_results 现场拼成 - doc 列表再塞进 prompt,state 里只保留裸的字符串列表。
📚 拓展阅读
QLangGraph 里如何处理不同类型的错误?有哪些策略?深挖·拓展中频
错误处理 retry interrupt
⏱️ 现行
LangGraph 的哲学是"错误是流程的一部分",不同错误由不同角色、用不同策略修复。文档给出一张对照表:Transient errors(网络抖动、限流)由系统自动处理,加 RetryPolicy 重试(可配合 timeout= 限制单次尝试);LLM-recoverable errors(工具失败、解析问题)由 LLM 修复,把错误写进 state 再 loop back,让 LLM 看到出错原因并调整;User-fixable errors(信息缺失、指令不清)由人来修,用 interrupt() 暂停等待用户输入;Recoverable failure after retries(重试耗尽后仍失败)由开发者声明式处理,用 error_handler 跑补偿/恢复分支(需要 langgraph>=1.2);Unexpected errors(未知问题)则让它直接冒泡(bubble up)以便调试,不要捕获你处理不了的东西。这种分层设计的权衡在于:把可预期的失败就地消化以提升可靠性,同时刻意不掩盖未知错误以保留可调试性。
术语 RetryPolicy(重试策略,含 max_attempts、initial_interval 等); interrupt()(暂停执行等待人输入); error_handler(重试耗尽后的补偿/恢复回调,需 langgraph>=1.2); Command(携带 state 更新与 goto 目标的返回对象); bubble up(让异常向上冒泡以供调试)
📖 "Transient failures get retries, LLM-recoverable errors loop back with context, user-fixable problems pause for input, and unexpected errors bubble up for debugging." — Thinking in LangGraph
🧪 实例 为可能有瞬时故障的节点加重试策略:
python
from langgraph.types import RetryPolicy

workflow.add_node(
    "search_documentation",
    search_documentation,
    retry_policy=RetryPolicy(max_attempts=3, initial_interval=1.0)
)

LLM-recoverable 的情形则把错误塞回 state 并 goto="agent" 让模型重试:
python
from langgraph.types import Command

def execute_tool(state: State) -> Command[Literal["agent", "execute_tool"]]:
    try:
        result = run_tool(state['tool_call'])
        return Command(update={"tool_result": result}, goto="agent")
    except ToolError as e:
        return Command(update={"tool_result": f"Tool error: {str(e)}"}, goto="agent")
🔍 追问 遇到需要用户补充账号 ID 这类错误怎么办? → 用 interrupt() 暂停,弹出请求让用户提供信息,再 goto 回本节点继续;这属于 User-fixable 一类。
📚 拓展阅读
  • Fault tolerance — retry 生命周期与 error_handler 补偿(saga)模式全解
  • Interruptsinterrupt() 暂停/恢复用于 user-fixable 错误
Q节点粒度该怎么切?"节点越小越好"还是越大越好?深挖·拓展中频
node粒度 checkpoint resilience observability
⏱️ 现行
节点粒度的本质是resilience(韧性)与 observability(可观测性)之间的权衡。机制上,LangGraph 的持久化层在节点边界创建 checkpoint;当工作流在中断或失败后恢复时,它会从执行停下的那个节点的开头重新开始。因此节点越小 → checkpoint 越频繁 → 出问题时要重跑的工作越少;反过来,如果把多个操作塞进一个大节点,靠近末尾的失败会导致从该节点开头把所有东西重新执行一遍。官方为邮件 agent 选择细粒度拆分的具体理由包括:隔离外部服务(Doc Search、Bug Track 调外部 API,单独成节点便于加重试且不影响别的节点)、中间可见性Classify Intent 独立成节点让你能在动作前检视 LLM 的判断)、不同失败模式(LLM 调用、数据库查询、发邮件各有不同重试策略,分开才能独立配置)、可复用与可测试(小节点更易隔离测试和复用)。一个常被担心的点是"节点多会不会更慢"——不会,因为 LangGraph 默认在后台异步写 checkpoint(async durability mode),图不必等 checkpoint 完成就继续跑;你也可切到 "exit" 模式仅在结束时写、或 "sync" 模式阻塞到写完为止。
术语 checkpoint(检查点,在节点边界保存的状态快照); node boundary(节点边界,checkpoint 的创建点); resilience(韧性,失败后少重跑的能力); observability(可观测性,能检视中间状态); async durability mode(异步持久化模式,后台写 checkpoint)
📖 "The answer involves trade-offs between resilience and observability." — Thinking in LangGraph
📖 "Smaller nodes mean more frequent checkpoints, which means less work to repeat if something goes wrong." — Thinking in LangGraph
🧪 实例 一个反例的取舍——你可以把 Read EmailClassify Intent 合并成一个节点,但代价是"失去在分类前检视原始邮件的能力,并且该节点内任何失败都会把两步都重跑一遍"。对多数应用而言,分开带来的可观测性与调试收益值得这点开销。
🔍 追问 那 caching(比如缓存检索结果)算 LangGraph 的框架特性吗? → 不算。文档指出缓存是"application-level decision,不是 LangGraph 框架特性",你在节点函数内部根据自己需求实现,LangGraph 不做规定。
📚 拓展阅读
QLangGraph 的核心收益有哪些?它的设计灵感来自哪里?深挖·拓展低频
core-benefits persistence HITL Pregel
⏱️ 现行
LangGraph 为任何长时运行、有状态的工作流或 agent 提供底层支撑基础设施,它不抽象 prompt 和架构,而是提供五项核心收益:Persistence(让 agent 能在失败中存活、可长时间运行并从中断处恢复)、Human-in-the-loop(在任意点检视和修改 agent state 以引入人工监督)、Comprehensive memory(同时具备用于持续推理的短期工作记忆和跨会话的长期记忆)、Debugging with LangSmith(借可视化工具追踪执行路径、捕获状态转移、提供运行时指标)、Production-ready deployment(用可扩展基础设施自信部署有状态、长时运行的 agent 系统)。这些能力共同回答了"为什么要用一个专门的编排运行时而不是自己写循环"——因为可靠地做到持久化、人机协同、记忆和可观测性本身就很难。设计上,LangGraph 受 Google 的 PregelApache Beam 启发,其公开接口则借鉴了 NetworkX;它由 LangChain 的创造者 LangChain Inc 打造,但可以脱离 LangChain 使用。
术语 Persistence(持久化,失败后恢复); Human-in-the-loop(人在环中,可检视修改 state); short-term / long-term memory(短期工作记忆/长期跨会话记忆); Pregel(Google 大规模图计算模型,LangGraph 的灵感来源); Apache Beam(统一批流数据处理模型)
📖 "LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent." — LangGraph overview
📖 "LangGraph is inspired by Pregel and Apache Beam." — LangGraph overview
📖 "LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain." — LangGraph overview
🧪 实例 human-in-the-loop 的收益具体体现在 interrupt()——图碰到它就暂停、把一切存进 checkpointer 并等待;它可以在数天后恢复,thread_id 确保该会话的所有 state 被一起保存:
python
config = {"configurable": {"thread_id": "customer_123"}}
stream = app.stream_events(initial_state, config, version="v3")
# 图会在 human_review 处暂停
print(f"human review interrupt:{stream.interrupts}")
🔍 追问 LangGraph 能独立于 LangChain 使用吗? → 能。它虽由 LangChain Inc 构建,但 "can be used without LangChain";同时它又能与任何 LangChain 产品(如 LangSmith 的 observability 与 deployment)无缝集成。
📚 拓展阅读
QLangGraph 的 Graph API 与 Functional API 有什么区别?在什么场景下应该分别选用它们?深挖·拓展🔥高频
Graph API Functional API 选型
⏱️ 现行
LangGraph 提供两套构建 agent 工作流的 API:Graph API 与 Functional API,二者共享同一套底层 runtime、可在同一应用里混用,但面向不同的用例和开发偏好。Graph API 采用声明式(declarative)思路——你显式定义节点(node)、边(edge)和共享 state,从而得到一张可视化的图结构;它适合复杂的决策树与分支、需要在多个节点间共享/协调 state、需要并行执行再汇合、以及团队协作时用可视化辅助理解与文档化的场景。Functional API 则采用命令式(imperative)思路,把 LangGraph 特性嵌入标准的过程式代码中(用 @entrypoint@task),因此适合对既有过程式代码做最小改动、线性且分支简单的工作流、快速原型(无需先定义 state schema),以及 state 天然局限于单个函数、不需要广泛共享的情况。二者的权衡本质是"显式结构与可视化"对"最少样板代码与贴近普通 Python 控制流"的取舍;关键是二者提供的核心能力(持久化、streaming、human-in-the-loop、memory)完全相同,只是用不同范式打包,所以选型取决于开发风格而非能力缺失。当 functional 工作流变复杂时可迁移到 Graph API,反之当图对简单线性流程显得过度设计时可回退到 Functional API。
术语 Graph API(声明式图 API,定义 node/edge/state); Functional API(命令式函数 API,用 @entrypoint/@task); declarative(声明式,描述结构); imperative(命令式,描述流程步骤)
📖 "LangGraph provides two different APIs to build agent workflows: the Graph API and the Functional API. Both APIs share the same underlying runtime and can be used together in the same application, but they are designed for different use cases and development preferences." — Choosing between Graph and Functional APIs
📖 "Both APIs provide the same core LangGraph features (persistence, streaming, human-in-the-loop, memory) but package them in different paradigms to suit different development styles and use cases." — Choosing between Graph and Functional APIs
🧪 实例 Functional API 用标准 Python 控制流即可完成线性分支工作流,无需定义 state schema:
python
from langgraph.func import entrypoint, task

@task
def process_user_input(user_input: str) -> dict:
    # Existing function with minimal changes
    return {"processed": user_input.lower().strip()}

@entrypoint(checkpointer=checkpointer)
def workflow(user_input: str) -> str:
    # Standard Python control flow
    processed = process_user_input(user_input).result()

    if "urgent" in processed["processed"]:
        response = handle_urgent_request(processed).result()
    else:
        response = handle_normal_request(processed).result()

    return response
🔍 追问 两套 API 能否混用?如何配合? → 可以。文档明确指出可在同一应用中同时使用两套 API,例如用 Graph API 做复杂多 agent 协调、用 Functional API 做简单数据处理,并在图的节点里调用 functional 的结果(data_processor.invoke(...))。
📚 拓展阅读
Q为什么说在 LangGraph 中"每一次上线的改动都是对现有 checkpoint 的向后兼容 API 变更"?它与传统工作流引擎有何不同?深挖·拓展🔥高频
向后兼容 checkpoint in-flight
⏱️ 现行
LangGraph 把最新部署的图直接运行在为现有 thread 已经持久化(persisted)的 state 上,因此你上线的每一次改动,本质上都是相对于既有 checkpoint 的一次向后兼容 API 变更。这与传统工作流引擎不同:后者会把一次 run 钉死在它启动时的代码版本上,而 LangGraph 会立即把最新的图应用到*每一个* thread——既包括新 thread,也包括从 checkpoint 恢复的 thread。这样做的好处是"无需仪式"地让 bug 修复传播到进行中的对话与 agent;代价是你必须推理每次改动如何与在旧版本代码下启动的 run 交互。文档给出三类兼容性问题(大致按遇到顺序):Technical compatibility(最常见,新代码必须仍能对既有 State 加载并执行)、Business compatibility(较少见,既有 run 即使代码已改也应继续走旧业务逻辑)、Non-determinism(只适用于 Functional API)。理解这一点的意义在于:因为 LangGraph 的恢复模型是"反序列化 saved state → 按名字分派到 node → 期望 node 返回符合 state schema 的值",所以任何破坏这一契约的改动都会让恢复中的 run 失败。
术语 checkpoint(检查点,持久化的 thread state 快照); in-flight run(进行中/未完成的运行); Technical/Business compatibility(技术/业务兼容性); drain(排空,等待旧 thread 跑完)
📖 "Because LangGraph runs the latest deployed graph against state that has been persisted for existing threads, every change you ship is effectively a backward-compatible API change with respect to your existing checkpoints." — Backward compatibility
📖 "Unlike workflow engines that pin a run to the version of code it started with, LangGraph applies the latest graph immediately to *every* thread, both new threads and threads that resume from a checkpoint." — Backward compatibility
🧪 实例 三类兼容性问题的判定路径:
flowchart TD
    A[上线一次图代码改动] --> B{新代码能否对旧 State
加载并执行?} B -- 否 --> T[Technical compatibility
如改节点名/State key] B -- 是 --> C{改动是否改变了
旧 thread 的业务含义?} C -- 是 --> D[Business compatibility
用 flow_version 分支] C -- 否 --> E{是否用 Functional API
或 node 内 task/interrupt?} E -- 是 --> F[Non-determinism
注意 task 顺序/副作用] E -- 否 --> G[安全]
🔍 追问 边(edge)拓扑的改动会破坏进行中的 thread 吗? → 不会。edge 拓扑本身不写入 checkpoint,在仍然存在的节点之间增删或改路由对 in-flight thread 是安全的;唯一能破坏被中断 thread 的拓扑改动是重命名或删除一个 node。
📚 拓展阅读
Q部署一个 LangGraph 应用需要哪些组成部分?langgraph.json 的作用是什么?深挖·拓展中频
应用结构 langgraph.json 部署
⏱️ 现行
一个 LangGraph 应用由一个或多个 graph、一个配置文件 langgraph.json、一个指定依赖的文件,以及一个可选的用于指定环境变量的 .env 文件构成。要用 LangSmith 部署,需要提供四类信息:(1) 一个 langgraph.json 配置文件,指定应用要用的依赖、graph 和环境变量;(2) 实现应用逻辑的 graph;(3) 一个指定运行所需依赖的文件;(4) 运行所需的环境变量。其中 langgraph.json 是一个 JSON 文件,集中声明部署所需的依赖、graph、环境变量及其他设置——dependencies 键指定运行所需依赖(可包含本地自定义包如 ./your_package 与三方包如 langchain_openai),graphs 键指定部署后可用的图(每个图由唯一名字和一个指向"编译好的图"或"生成图的函数"的路径标识,如 ./your_package/your_file.py:agent),env 键指向 .env。这种"配置即部署契约"的设计把代码与部署解耦:LangGraph CLI 默认使用当前目录下的 langgraph.json;本地调试时可在 env 键里配置环境变量,而生产部署通常应在部署环境里配置环境变量。此外还可用 dockerfile_lines 键指定额外的二进制或系统库。
术语 langgraph.json(部署配置文件); dependencies 键(依赖声明); graphs 键(图注册,名字→路径); env 键(环境变量来源); dockerfile_lines(附加系统库/二进制)
📖 "A LangGraph application consists of one or more graphs, a configuration file (langgraph.json), a file that specifies dependencies, and an optional .env file that specifies environment variables." — Application structure
📖 "Each graph is identified by a name (which should be unique) and a path for either: (1) the compiled graph or (2) a function that makes a graph is defined." — Application structure
🧪 实例 一个最小的 langgraph.json:依赖含一个本地包与 langchain_openai,从 ./your_package/your_file.pyagent 变量加载单个图,环境变量从 .env 载入:
json
{
  "dependencies": ["langchain_openai", "./your_package"],
  "graphs": {
    "my_agent": "./your_package/your_file.py:agent"
  },
  "env": "./.env"
}
🔍 追问 本地调试和生产部署在环境变量配置上有什么区别? → 本地运行部署的应用时可以在 langgraph.jsonenv 键里配置环境变量;而生产部署通常应当在部署环境里配置环境变量,而非写进配置文件。
📚 拓展阅读
Q当图代码改动技术上有效、但会改变旧 thread 的业务含义时(例如新插入一个步骤),如何避免把新逻辑追溯地套用到旧 thread 上?深挖·拓展低频
业务兼容 flow_version conditional edge
⏱️ 现行
有时一次改动在技术上完全有效——每个既有 checkpoint 仍能加载、每个 node 仍能解析——但新图的*含义*与旧图不同:新行为对新 thread 是正确的,你却不想把它追溯地套用到在旧逻辑下启动的 thread。例如图原本是 intake → triage → respond,你想在 triagerespond 之间插入一个新的 policy_check:已经越过 triage 的旧 thread 应继续直达 respond(旧流程),而新 thread 应走完整的新流程。推荐模式是在 thread 启动时把相关的"行为版本(behavioral version)"记录到 state 上,然后用一个 conditional edge 基于它分支。做法是在 intake 节点里用 state.get("flow_version", 2) 给新 thread 打上当前版本戳,恢复越过 intake 的旧 thread 会保留其 saved state 里已有的值(或在读取时 fall through 到 v1 默认);分支函数 after_triage 据此决定去 policy_check 还是直接 respond。等所有 v1 thread 跑完后,就可以移除版本标志与 conditional edge。关键约束是:这一模式只有在你于*thread 启动时*、在任何需要版本化的分支*之前*设置版本才有效;设置得太晚会导致旧 thread 在需要该值时并未设置它。
术语 business compatibility(业务兼容性); flow_version(记录在 state 上的行为版本戳); conditional edge(条件边,按状态路由); NotRequired(TypedDict 可选字段,旧 checkpoint 仍能校验通过)
📖 "The recommended pattern is to record the relevant *behavioral version* on the state at thread start, then branch on it with a conditional edge:" — Backward compatibility
📖 "This pattern only works if you set the version *at thread start*, before any branch that needs to be versioned. Setting it later means existing threads will not have it set when they need it." — Backward compatibility
🧪 实例intake 打版本戳,after_triage 据版本决定是否插入 policy_check
python
def intake(state: State) -> dict:
    # Stamp new threads with the current flow version. Existing threads
    # that resume past `intake` keep whatever value was already saved.
    return {"flow_version": state.get("flow_version", 2)}


def after_triage(state: State) -> str:
    if state.get("flow_version", 1) >= 2:
        return "policy_check"
    return "respond"
🔍 追问 新增 state 字段应该怎么定义才不破坏旧 checkpoint? → 用 NotRequired(或 Optional[...] = None)新增字段,这样旧 checkpoint 仍能通过 schema 校验;移除则当作 deprecation,至少保留一个 drain cycle。
📚 拓展阅读
Q修改带有 in-flight run 的 Functional API @entrypoint 时,为什么容易引入非确定性(non-determinism)?有哪些安全做法?深挖·拓展中频
Functional API non-determinism replay @task
⏱️ 现行
非确定性这一类兼容性问题只适用于 Functional API,以及在 Graph API 的 node 内部使用的 taskinterrupt 调用。Functional API 的 @entrypoint 会编译成单个 node,当 run 恢复时它会从头 replay(重放) entrypoint 函数体,并用缓存的 @task 结果跳过已经完成的工作。有两类改动会破坏这一模型:其一是在 resume point 之前"增加、删除或重排 @task 调用或 interrupt 调用"——因为 LangGraph 是按调用在 replay 中的位置来匹配缓存结果和 resume 值的,位置一移就可能把错误的缓存值重放到不同的调用上;其二是"在 @task 之外引入非确定性操作",例如 time.time()random.random() 或直接内联在 entrypoint 体里的网络调用——replay 时它们会产生与首次运行不同的值,从而可能改变控制流。相比之下,普通的 Graph API node 在恢复时从 node 函数开头重新执行,只需把副作用设计成幂等(idempotent),除非该 node 里用了 task 或 interrupt,否则不必保持 task 调用顺序。若必须对有 in-flight run 的 @entrypoint 做非平凡改动,最安全的选项是:让 in-flight run 先排空(drain)再部署;把新逻辑包进一个新的 @task 使其结果被独立 checkpoint;或者在 langgraph.json 里用新的 graph 名字注册一个新 entrypoint,并把新 thread 路由到它。
术语 non-determinism(非确定性); replay(恢复时从头重放 entrypoint 体); @task(其结果被缓存/checkpoint 的任务); resume point(恢复点); idempotent(幂等,可安全重复执行)
📖 "A Functional API entrypoint compiles to a single node that replays the entrypoint body from the beginning when a run resumes, using cached @task results to skip work that has already been done." — Backward compatibility
📖 "Introducing non-deterministic operations outside of a @task, such as time.time(), random.random(), or a network call inlined in the entrypoint body. On replay these produce different values than they did on the first run, which can change the control flow." — Backward compatibility
🧪 实例 对有 in-flight run 的 entrypoint 做非平凡改动时的三种安全策略:
text
1. drain:让进行中的 run 先跑完再部署改动
2. 新 @task:把新逻辑包进新的 @task,使其结果被独立 checkpoint
3. 新 graph 名:在 langgraph.json 里用新名字注册新 entrypoint,只把新 thread 路由过去
🔍 追问 普通 Graph API node 在恢复时需要保证 task 调用顺序吗? → 不需要(除非该 node 内使用了 task 或 interrupt)。普通 node 会从 node 函数开头重新执行,你只需把副作用设计成幂等,而不必保持 task 调用顺序。
📚 拓展阅读

第2章 图与函数式 API

🔥高频

图 API(节点/边/状态)

Graph API overview Use the graph API
QLangGraph 用哪三个核心组件建模 agent 工作流?它们如何配合驱动一次执行?深挖·拓展🔥高频
StateGraph State Nodes Edges
⏱️ 现行
LangGraph 把 agent 工作流建模成一张图,由三件东西组成:State(表示应用当前快照的共享数据结构,通常用一份共享 schema 定义)、Nodes(编码 agent 逻辑的函数,接收当前 state、做计算或副作用、返回对 state 的更新)、Edges(根据当前 state 决定下一个执行哪个 node 的函数,可以是条件分支或固定转移)。关键在于 node 和 edge "只不过是函数",里面可以放 LLM 也可以是普通代码,一句话概括就是 *nodes 干活、edges 决定接下来干什么*。底层用 message passing 算法驱动:一个 node 完成后沿边把消息发给下游 node,受到消息的 node 再执行,如此推进;整个过程按离散的 super-step 迭代,同一 super-step 内并行的 node 一起跑、串行的 node 属于不同 super-step。图从所有 node 处于 inactive 开始,node 在某条入边收到新消息时变 active 并运行,当没有 node 活跃且没有消息在途时执行终止。这样的设计把"状态管理"从业务逻辑里剥离出来,让复杂的循环与分支工作流可以被检查点、可视化和调试。
术语 State(图的共享状态快照); Nodes(承载逻辑的函数); Edges(决定路由的函数); message passing(消息传递,节点间通过沿边传消息推进); super-step(超步,一次对图节点的迭代)
📖 "At its core, LangGraph models agent workflows as graphs." — Graph API overview
📖 "In short: *nodes do the work, edges tell what to do next*." — Graph API overview
📖 "A super-step can be considered a single iteration over the graph nodes. Nodes that run in parallel are part of the same super-step, while nodes that run sequentially belong to separate super-steps." — Graph API overview
🧪 实例 一个最小的三步图,把 State、Node、Edge 串起来:
python
from typing import TypedDict
from langgraph.graph import START, StateGraph

class State(TypedDict):
    value_1: str
    value_2: int

def step_1(state: State):
    return {"value_1": "a"}

builder = StateGraph(State)
builder.add_node(step_1)
builder.add_edge(START, "step_1")
graph = builder.compile()
flowchart LR
    START([START]) --> A[Node: 干活]
    A -->|Edge: 决定下一步| B[Node]
    B --> END([END])
🔍 追问 为什么必须先 compile 才能用图? → compile 会对图结构做基本检查(比如有没有孤立节点),也是指定 checkpointer、断点等运行期参数的地方;文档明确要求"你必须先编译图才能使用它"。
📚 拓展阅读
QState 的 schema 与 reducer 分别是什么?默认 reducer 和自定义 reducer 有什么区别?深挖·拓展🔥高频
State reducer Annotated
⏱️ 现行
定义图时第一件事就是定义 State,它由两部分构成:图的 schema(可用 TypedDictdataclass 或 Pydantic BaseModel)和每个 key 各自独立的 reducer 函数,后者规定"节点返回的更新如何被应用到 state 上"。所有 node 都基于这份 schema 读写,并只需返回"部分更新"而非整份 state。每个 reducer 是一个二元函数,左参数是该 key 在 state 里已累积的当前值,右参数是节点返回的更新;LangGraph 对每个被更新的 key 调用 reducer 并把返回值存为新值。若某个 key 没有显式指定 reducer,则默认认为所有更新都覆盖旧值——默认 reducer 丢弃左参数、只保留右参数。自定义 reducer 则是把左右参数组合起来而不是替换,适合累加类场景,比如往列表里追加。典型做法是用 Annotated[list, operator.add]bar 这个 key 挂上 operator.add,这样两次更新会把列表拼接而不是覆盖。权衡在于:覆盖语义简单直观但会丢历史;累加语义能保留过程数据,但要注意并行 super-step 的更新顺序不保证一致。
术语 schema(状态结构,TypedDict/dataclass/Pydantic); reducer(应用更新的二元函数); left/right 参数(累积值 / 节点更新); default reducer(覆盖式默认合并); Annotated(给 key 绑定 reducer 的类型注解)
📖 "The State consists of the [schema of the graph](#schema) as well as [reducer functions](#reducers) which specify how to apply updates to the state." — Graph API overview
📖 "If no reducer function is explicitly specified then it is assumed that all updates to that key should override it." — Graph API overview
📖 "A custom reducer combines the left and right arguments instead of replacing the state value, which is useful for accumulating values, such as appending updates to a list." — Graph API overview
🧪 实例bar 挂上 operator.add 后,第一个节点返回 {"foo": 2}、第二个返回 {"bar": ["bye"]},输入 {"foo": 1, "bar": ["hi"]} 最终会得到 {"foo": 2, "bar": ["hi", "bye"]}:
python
from operator import add
from typing import Annotated
from typing_extensions import TypedDict

class State(TypedDict):
    foo: int
    bar: Annotated[list[str], add]
🔍 追问 如果想临时绕过 reducer 直接覆盖某个累加字段怎么办? → 用 Overwrite 类型包裹返回值即可让 reducer 被跳过、直接设值;但同一 super-step 内只能有一个并行节点对同一 key 用 Overwrite,否则会抛 InvalidUpdateError
📚 拓展阅读
Q为什么在图 state 里存消息列表要用 add_messages 而不是 operator.add?深挖·拓展🔥高频
add_messages MessagesState messages
⏱️ 现行
很多 LLM 应用需要把对话历史以消息列表形式存进 state。给 messages 这个 channel 挂 reducer 是关键:如果不指定 reducer,每次更新都会用最新值覆盖整个消息列表;如果只想追加,用 operator.add 就够。但一旦涉及人机协作(human-in-the-loop)要手动更新已有消息,operator.add 会把你的手动更新当成新消息追加,而不是替换原消息。为解决这个问题,LangGraph 提供预置的 add_messages:对全新消息它会追加到列表,同时能正确处理对已有消息的更新——它靠追踪 message ID 来判断该追加还是覆盖。此外 add_messages 还会在收到 messages channel 更新时尝试把消息反序列化成 LangChain 的 Message 对象,因此既支持传 HumanMessage(...) 对象,也支持传 {"type": "human", "content": ...} 这样的字典简写;正因为总会反序列化成 Message,访问属性要用点号如 state["messages"][-1].content。由于这种需求太常见,还有预置的 MessagesState(单一 messages key + add_messages),实践中人们常继承它再加字段。
术语 add_messages(按 ID 追加/覆盖消息的预置 reducer); MessagesState(内置的消息状态基类); message ID(区分新增与更新的依据); 序列化(字典简写与 Message 对象互转)
📖 "If you don't specify a reducer, every state update will overwrite the list of messages with the most recently provided value." — Graph API overview
📖 "For brand new messages, it will simply append to existing list, but it will also handle the updates for existing messages correctly." — Graph API overview
🧪 实例add_messages 定义状态,并在 node 里只返回增量消息:
python
from langchain.messages import AnyMessage
from langgraph.graph.message import add_messages
from typing import Annotated
from typing_extensions import TypedDict

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    extra_field: int

def node(state: State):
    new_message = AIMessage("Hello!")
    return {"messages": [new_message], "extra_field": 10}
🔍 追问 想省事又要带 messages,有没有现成状态类? → 有,直接继承 MessagesState 再加自己的字段,例如 class State(MessagesState): extra_field: int
📚 拓展阅读
Q一个 node 函数能接收哪些参数?节点在 interrupt/retry 后重跑时要注意什么?深挖·拓展🔥高频
Nodes Runtime idempotency re-execution
⏱️ 现行
在 LangGraph 里 node 就是 Python 函数(同步或异步),可接收三类参数:state(图的当前状态)、config(RunnableConfig,含 thread_idtags 等追踪信息)、runtime(Runtime 对象,含运行期 context 以及 storestream_writerexecution_info 等)。函数通过 add_node 加入图,幕后会被包成 RunnableLambda 从而获得批处理、异步和原生追踪能力;不指定名字时节点名默认取函数名。一个重要约束是:节点应当直接返回对 state 的更新,而不是原地修改 state。关于容错:当图带 checkpointer 时,检查点保存在 super-step 边界而非节点函数内部,所以一旦执行在 interrupt 或 retry 后恢复,受影响的节点会从其函数开头整个重跑,暂停点之前的代码和副作用会再次执行。因此要把节点逻辑设计成幂等(idempotent)——比如插入数据库行时用 upsert、幂等键或先读后写,避免重复产生副作用。此外图结构(节点/边)的增删改不受确定性规则约束,可以在不破坏已有线程恢复的前提下修改拓扑。
术语 RunnableConfig(携带 thread_id、tags 的配置对象); Runtime(运行期上下文对象); RunnableLambda(函数被包装后的可运行体); idempotency(幂等,重跑不破坏状态); super-step 边界(检查点保存的位置)
📖 "In LangGraph, nodes are Python functions (either synchronous or asynchronous) that accept the following arguments:" — Graph API overview
📖 "If execution stops and later resumes (for example after an interrupt or a retry), the affected node runs again from the start of its function. Code and side effects before the pause run again." — Graph API overview
📖 "Nodes should return updates to the state directly, instead of mutating the state." — Use the graph API
🧪 实例 通过 runtime 读取运行期 context 的节点:
python
from langgraph.runtime import Runtime

def node_with_runtime(state: State, runtime: Runtime[Context]):
    print("In node: ", runtime.context.user_id)
    return {"results": f"Hello, {state['input']}!"}
🔍 追问 节点里有多个操作,怎么让恢复时跳过已完成的部分? → 把每个操作实现为 @task,带 checkpointer 时 task 结果会被检查点化,恢复线程时可跳过节点内已完成的 task 工作。
📚 拓展阅读
Q普通边、条件边和入口点分别怎么用?一个节点有多条出边会怎样?深挖·拓展🔥高频
add_edge add_conditional_edges START parallel
⏱️ 现行
边决定逻辑如何路由以及图何时停止,主要有几类:普通边(直接从一个节点到下一个)、条件边(调用路由函数决定去哪些节点)、入口点(用户输入到来时先跑哪个节点)、条件入口点(用逻辑决定首个节点)。想"总是"从 A 到 B 就用 add_edge("node_a", "node_b");想"可选地"路由或终止就用 add_conditional_edges("node_a", routing_function),该路由函数和节点一样接收当前 state 并返回一个值,默认把返回值当作下一个节点名(可返回列表),也可传一个字典把路由函数输出映射到节点名。入口点用从虚拟 START 节点连一条边来指定,例如 add_edge(START, "node_a");条件入口点则从 STARTadd_conditional_edges。一个关键机制是:节点可以有多条出边,一旦有多条出边,所有目标节点都会作为下一个 super-step 的一部分并行执行,这正是 fan-out 的基础。权衡上,官方建议每个节点只选一种路由机制——静态用普通边、动态用条件边或 Command,不要混用,否则两条路径都可能执行,行为更难推理。
术语 Normal Edges(普通边,静态转移); Conditional Edges(条件边,动态路由); routing_function(接收 state 返回下一节点的函数); Entry Point(从 START 出发的入口边); fan-out(多出边并行触发)
📖 "A node can have multiple outgoing edges. If a node has multiple outgoing edges, all of those destination nodes will be executed in parallel as a part of the next superstep." — Graph API overview
📖 "Normal Edges: Go directly from one node to the next." — Graph API overview
📖 "Similar to nodes, the routing_function accepts the current state of the graph and returns a value." — Graph API overview
🧪 实例 用条件边按 state 里的 which 字段在 b/c 之间选路:
python
def conditional_edge(state: State) -> Literal["b", "c"]:
    return state["which"]

builder.add_conditional_edges("a", conditional_edge)
🔍 追问 条件边能一次路由到多个目标吗? → 能,路由函数返回一个节点名列表即可,例如 return ["c", "d"],这些节点会在下一 super-step 并行执行。
📚 拓展阅读
QCommand 和条件边有什么区别?什么时候该用哪个?深挖·拓展🔥高频
Command goto update dynamic-routing
⏱️ 现行
Command 是控制图执行的多用途原语,接收四个参数:update(施加状态更新,类似节点返回更新)、goto(导航到指定节点,类似条件边)、graph(在子图里指向父图,配 Command.PARENT)、resume(interrupt 后提供恢复值)。它用在三处:从节点返回、作为 invoke/stream 的输入(仅 resume 语义)、从工具返回。核心区别在于:条件边只能"路由不改状态",而 Command 能在同一个函数里既更新状态又决定下一个节点——所以文档的判据是:当你需要"同时"更新状态并路由到别的节点时用 Command,只需要路由不改状态就用条件边。使用时有两个坑要记牢:一是返回 Command 的节点必须加返回类型注解列出可去的节点名(如 Command[Literal["my_other_node"]]),这是图渲染和让 LangGraph 知道可达关系所必需的;二是 Command 只是"增加"动态边,用 add_edge 定义的静态边仍会执行,若节点同时返回 Command(goto=...) 又有静态边,两条路径都会跑。因此每个节点要么用 Command、要么用静态边路由,不要两者混用。
术语 Command(合并状态更新与控制流的原语); update(状态更新参数); goto(路由到指定节点); Command.PARENT(导航到父图); 返回类型注解(声明可达节点,供渲染)
📖 "Use Command when you need to both update state and route to a different node. If you only need to route without updating state, use [conditional edges](#conditional-edges) instead." — Graph API overview
📖 "Command only adds dynamic edges—static edges defined with add_edge / addEdge still execute." — Graph API overview
🧪 实例 在节点里同时更新状态并路由,替代条件边:
python
def node_a(state: State) -> Command[Literal["node_b", "node_c"]]:
    value = random.choice(["b", "c"])
    goto = "node_b" if value == "b" else "node_c"
    return Command(
        update={"foo": value},
        goto=goto,
    )
🔍 追问 在工具里返回 Command 更新 messages 有什么强制要求? → 必须在 Command.update 里包含 messages(或消息历史 key),且该列表必须含一个 ToolMessage,否则消息历史不合法(带工具调用的 AI 消息后必须跟工具结果消息)。
📚 拓展阅读
Qmap-reduce 场景下边数未知,LangGraph 用什么机制做动态扇出?深挖·拓展中频
Send map-reduce fan-out conditional-edges
⏱️ 现行
默认情况下 node 和 edge 是提前定义好、都操作同一份共享 state 的;但有些场景边的具体条数事先不知道,而且下游节点需要"不同版本的 state 同时存在"——典型就是 map-reduce:第一个节点生成一个对象列表,你想对列表里每个对象各跑一次某节点,而对象数量事先未知(即边数未知),且每个下游节点的输入 state 应该不同(每个对象一份)。为支持这种模式,LangGraph 允许从条件边返回 Send 对象。Send 接收两个参数:第一个是目标节点名,第二个是要传给该节点的 state。于是路由函数可以基于运行期的列表长度返回一批 Send,实现数量在运行时才确定的扇出,每个分支拿到定制化的输入状态;这些分支在同一 super-step 内并行执行,产出再通过带 reducer(如 operator.add)的 key 汇聚(fan-in)。相比静态边或普通条件边,Send 的价值就在于"边数和输入都在运行时才决定",突破了图必须提前静态声明所有边的限制。
术语 Send(向指定节点投递定制 state 的对象); map-reduce(先展开后聚合的模式); fan-out/fan-in(扇出并行、扇入汇聚); conditional edges(返回 Send 的载体)
📖 "To support this design pattern, LangGraph supports returning Send objects from conditional edges. Send takes two arguments: first is the name of the node, and second is the state to pass to that node." — Graph API overview
🧪 实例subjects 列表里每个主题各扇出一次 generate_joke:
python
from langgraph.types import Send

def continue_to_jokes(state: OverallState):
    return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]

builder.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"])
🔍 追问 扇出的多个分支产出的 joke 怎么合并成一个列表? → 在 state 里用带 reducer 的 key,如 jokes: Annotated[list[str], operator.add],各分支返回的单元素列表会被 operator.add 依次拼接汇聚。
📚 拓展阅读
Q什么是 LangGraph 的 Functional API?它和 Graph API 有哪些关键差异?深挖·拓展🔥高频
functional-api graph-api architecture
⏱️ 现行
Functional API 的目标是让你以最小的代码改动,把 LangGraph 的核心能力(persistence、memory、human-in-the-loop、streaming)加到已有代码里。它不像多数编排框架那样强迫你把代码重构成显式的 pipeline 或 DAG,而是允许你继续用 iffor、函数调用等标准语言原语来表达分支和控制流。它只有两个构建块:@entrypoint(工作流入口)和 @task(离散工作单元)。与 Graph API 的取舍在于:控制流上,Functional API 不需要你思考图结构,通常能减少代码量;短期记忆上,Graph API 要求声明 State 并可能定义 reducers,而 @entrypoint/@task 的状态被限定在函数作用域内、不跨函数共享,因此不需要显式状态管理;checkpoint 上,Graph API 每个 superstep 后生成新 checkpoint,而 Functional API 在 task 执行时把结果写入该 entrypoint 已有的 checkpoint 而非新建;可视化上,Graph API 便于把工作流画成图,Functional API 因图是运行时动态生成而不支持可视化。两套 API 共享同一底层 runtime,可以在同一应用中混用。

flowchart LR
  A["@entrypoint\n工作流入口"] --> B["@task\n离散工作单元"]
  B --> C["future 对象\n.result() / await"]
  C --> D["checkpoint\n保存 task 结果"]
术语 Functional API(以函数装饰器组织工作流的 API); @entrypoint(工作流入口装饰器); @task(离散工作单元装饰器); superstep(Graph API 中一个执行步进单位); runtime(两套 API 共享的底层执行引擎)
📖 "The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write." — Functional API overview
📖 "In the Functional API, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint." — Functional API overview
📖 "Both APIs share the same underlying runtime, so you can use them together in the same application." — Functional API overview
🧪 实例 一个写文章并中断请人工审核的最小工作流,write_essay 是 task,workflow 是 entrypoint:
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import interrupt

@task
def write_essay(topic: str) -> str:
    """Write an essay about the given topic."""
    time.sleep(1) # A placeholder for a long-running task.
    return f"An essay about topic: {topic}"

@entrypoint(checkpointer=InMemorySaver())
def workflow(topic: str) -> dict:
    """A simple workflow that writes an essay and asks for a review."""
    essay = write_essay("cat").result()
    is_approved = interrupt({
        "essay": essay,
        "action": "Please approve/reject the essay",
    })
    return {"essay": essay, "is_approved": is_approved}
🔍 追问 什么时候更适合选 Graph API 而不是 Functional API? → 当你更偏好声明式风格、需要显式共享 State 与 reducers,或需要把工作流可视化成图来调试/分享时,Graph API 更合适;两者可混用。
📚 拓展阅读
Q@entrypoint@task 各自的职责是什么?task 有哪些调用约束?深挖·拓展🔥高频
entrypoint task future
⏱️ 现行
@entrypoint 把一个函数标记为工作流起点,封装工作流逻辑并管理执行流(包括处理长任务和 interrupts);被它装饰后会产出一个 Pregel 实例来管理 streaming、resumption、checkpointing。entrypoint 函数必须只接受一个位置参数作为工作流输入,若要传多份数据就用字典。@task 代表一个离散工作单元(如一次 API 调用或数据处理),可在 entrypoint 内异步执行。调用 task 的关键机制是:它立即返回一个 future(结果的占位符),你可以用 result() 同步等待或用 await 异步获取。约束上,task 只能从 entrypoint、另一个 task 或 state graph node 内部调用,不能从主应用代码直接调用。这个设计的取舍在于:正因为 task 的结果会被写入 checkpoint,工作流才能从上次保存的状态恢复而不必重算——这是把「值得持久化/需重试/需观测/需并行」的工作单独包成 task 的根本理由。序列化上,entrypoint 的输入输出、以及 task 的输出都必须 JSON-serializable,否则在配置了 checkpointer 时会在运行时报错。
术语 Pregel(entrypoint 产出的执行管理实例); future(task 立即返回的结果占位符); result()(同步等待 future 结果); checkpointer(启用持久化与 HIL 的组件); JSON-serializable(可 JSON 序列化,checkpoint 的前提)
📖 "@entrypoint: Marks a function as the starting point of a workflow, encapsulating logic and managing execution flow, including handling long-running tasks and interrupts." — Functional API overview
📖 "When you call a task, it returns *immediately* with a future object. A future is a placeholder for a result that will be available later." — Functional API overview
📖 "Tasks *cannot* be called directly from the main application code." — Functional API overview
🧪 实例 一个把「判断奇偶」和「格式化消息」两个 task 组合起来的 entrypoint:
python
@task
def is_even(number: int) -> bool:
    return number % 2 == 0

@task
def format_message(is_even: bool) -> str:
    return "The number is even." if is_even else "The number is odd."

@entrypoint(checkpointer=checkpointer)
def workflow(inputs: dict) -> str:
    """Simple workflow to classify a number."""
    even = is_even(inputs["number"]).result()
    return format_message(even).result()
🔍 追问 entrypoint 要传多个输入怎么办? → 输入被限制为函数的第一个参数,需要多份数据时用字典包起来,如 my_workflow.invoke({"value": 1, "another_value": 2})
📚 拓展阅读
Q为什么在 Functional API 里必须把副作用和非确定性操作放进 task?resume 时到底发生了什么?深挖·拓展🔥高频
determinism replay idempotency human-in-the-loop
⏱️ 现行
核心机制是 replay:当你 resume 一个工作流时,代码并不是从停下的那一行继续,而是回到 checkpoint 边界,从 entrypoint 开头重新向前「重放」,直到再次到达暂停点。重放过程中,LangGraph 会从 checkpointer 里恢复已完成的 task 和 subgraph 结果,而不是重新计算它们,从而保留跨暂停的步骤顺序(包括长任务或非确定性 task 的输出)。正因如此,任何副作用(写文件、发邮件、API 调用)或非确定性操作(随机数、读时钟)如果直接写在 entrypoint 里,就会在 resume 重放时被再次执行——这通常不是你想要的;而包进 task 后,resume 会直接取 checkpoint 里的已存结果。对于 human-in-the-loop 尤其关键:LangGraph 为每个 task/entrypoint 维护一个 resume 值列表,interrupt 与 resume 值的匹配是严格按索引(index-based)的,所以 interrupt 的出现顺序必须与 resume 值顺序一致,否则一个 interrupt 可能被错配到错误的 resume 值。此外,因为未完成就中断的 task 在 resume 时可能重跑,副作用还应设计成幂等(idempotent),用幂等键或先校验已有结果来避免重复写入。
术语 replay(从 checkpoint 边界重放到暂停点); checkpoint boundary(重放的回退基准点); determinism(可确定性重放的前提); index-based(interrupt 与 resume 值按序号匹配); idempotency(重跑产生相同结果、避免重复副作用)
📖 "When you resume a workflow run, the code does NOT resume from the same line of code where execution stopped. Execution returns to a checkpoint boundary, and the workflow replays forward until it reaches the pause again." — Functional API overview
📖 "To use features like human-in-the-loop, you must place non-deterministic work (for example, random values) and side effects (for example, file writes or API calls) in tasks." — Functional API overview
📖 "This matching is strictly index-based, so the order of the resume values should match the order of the interrupts." — Functional API overview
🧪 实例 错误 vs 正确地处理「读当前时间」这种非确定性操作:
python
# 正确:把非确定性的取时间包成 task,resume 时返回同一结果
import time
from langgraph.func import task

@task
def get_time() -> float:
    return time.time()

@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
    t0 = inputs["t0"]
    t1 = get_time().result()
    delta_t = t1 - t0
    if delta_t > 1:
        result = slow_task(1).result()
        value = interrupt("question")
    else:
        result = slow_task(2).result()
        value = interrupt("question")
    return {"result": result, "value": value}
🔍 追问 直接把 with open(...) 写在 entrypoint 里有什么后果? → resume 工作流时这段代码会被再执行一次(副作用重复触发),应把写文件封装进一个 task,再 write_to_file().result()
📚 拓展阅读
QFunctional API 的短期记忆怎么工作?previous 参数和 entrypoint.final 分别解决什么问题?深挖·拓展中频
short-term-memory previous entrypoint-final checkpointer
⏱️ 现行
当 entrypoint 配置了 checkpointer 时,它会在同一 thread id 的多次调用之间,把信息存进 checkpoints,从而让你通过 previous 参数访问上一次调用的状态。默认情况下,previous 就是上一次调用的返回值——这带来一个耦合:返回给调用方的值,同时也是被存进 checkpoint、下次作为 previous 传入的值。entrypoint.final 正是用来解耦这两者的特殊原语:它返回两个值,第一个是 entrypoint 返回给调用方的值,第二个是要保存进 checkpoint 的值,类型标注为 entrypoint.final[return_type, save_type]。这在「你想返回一个计算结果(如摘要/状态),但要保存另一个内部值供下次使用」或「你要控制下次 previous 收到什么」时非常有用。取舍在于:不用 final 时逻辑更简单但返回值与持久化值必须相同;用 final 则获得对「对外返回」和「对内持久化」的独立控制,代价是要显式声明两个值。
术语 thread id(区分不同会话/线程的标识); previous(注入的上一次调用状态,默认为上次返回值); entrypoint.final(解耦返回值与保存值的原语); entrypoint.final[return_type, save_type](final 的类型标注)
📖 "By default, the previous parameter is the return value of the previous invocation." — Functional API overview
📖 "The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. The type annotation is entrypoint.final[return_type, save_type]." — Functional API overview
🧪 实例entrypoint.final 让「返回给调用方的值」与「存进 checkpoint 的值」不同:
python
@entrypoint(checkpointer=checkpointer)
def accumulate(n: int, *, previous: int | None) -> entrypoint.final[int, int]:
    previous = previous or 0
    total = previous + n
    # 返回旧的 previous 给调用方,但把新 total 存进 checkpoint
    return entrypoint.final(value=previous, save=total)

config = {"configurable": {"thread_id": "my-thread"}}
print(accumulate.invoke(1, config=config))  # 0
print(accumulate.invoke(2, config=config))  # 1
print(accumulate.invoke(3, config=config))  # 3
🔍 追问 不用 entrypoint.final、只用 previous 做累加会怎样? → previous 就等于上次返回值,例如 return number + previous,第一次 invoke(1) 得 1,第二次 invoke(2) 得 3(previous 为上次的 1)。
📚 拓展阅读
QFunctional API 如何做并行、重试、超时与缓存?深挖·拓展中频
parallel retry-policy timeout cache
⏱️ 现行
并行靠的是 task 的 future 语义:并发地调用多个 task 拿到 future 列表,再统一 result() 等待,即可对 IO 密集型工作(如并发调多个 LLM API)提升性能。重试用 RetryPolicy,把它传给 @task(retry_policy=...),默认策略针对特定网络错误做了优化,你也可以用 retry_on=ValueError 指定要重试的异常。超时用 timeout 参数(秒数或 datetime.timedelta)加在 @task@entrypoint 上,限制单次异步尝试的运行时长;注意超时只支持异步 task/entrypoint,给同步函数设 timeout 会在声明时报错;超时会抛 NodeTimeoutError(它是 Python 内建 TimeoutError 的子类),且 timeout 对每次尝试独立计时——每次重试都会重置计时器。缓存用 @task(cache_policy=CachePolicy(ttl=...)) 配合 entrypoint 上的 cachettl 以秒计、过期后失效,可避免同参数重复计算。这些机制与 checkpoint 恢复叠加:出错后用同一 thread id 传 None 重跑时,已完成的 task(如某个 slow_task)不会重算,其结果直接从 checkpoint 取回。
术语 RetryPolicy(重试策略,含 retry_on 指定异常); timeout(单次异步尝试时限,秒或 timedelta); NodeTimeoutError(超时异常,TimeoutError 子类); CachePolicy(带 ttl 的缓存策略); ttl(缓存存活秒数,过期失效)
📖 "Tasks can be executed in parallel by invoking them concurrently and waiting for the results. This is useful for improving performance in IO bound tasks (e.g., calling APIs for LLMs)." — Use the functional API
📖 "Timeouts are supported only for async tasks and entrypoints. If you set timeout on a sync function, LangGraph raises an error when the task or entrypoint is declared." — Use the functional API
📖 "When we resume execution, we won't need to re-run the slow_task as its result is already saved in the checkpoint." — Use the functional API
🧪 实例 给异步 task 同时配超时和重试策略:
python
import asyncio
from langgraph.errors import NodeTimeoutError
from langgraph.func import entrypoint, task
from langgraph.types import RetryPolicy

@task(
    timeout=1.0,
    retry_policy=RetryPolicy(retry_on=NodeTimeoutError),
)
async def call_api(url: str) -> str:
    await asyncio.sleep(2)
    return f"result from {url}"

@entrypoint(timeout=5.0)
async def workflow(inputs: dict) -> str:
    return await call_api(inputs["url"])
🔍 追问 出错后要怎么 resume 才能不重跑已完成的 task? → 用同一 thread id(config)以 None 作为输入重新 invoke,如 main.invoke(None, config=config),已保存到 checkpoint 的 task 结果会被复用。
📚 拓展阅读

第3章 状态·持久化·记忆

🔥高频

持久化与检查点

Persistence Checkpointers
QLangGraph 的持久化层由哪两套系统组成?Checkpointer 与 Store 分别负责什么、如何选型?深挖·拓展🔥高频
persistence checkpointer store memory
⏱️ 现行
LangGraph 用两套互补的持久化系统来把信息保留到单次 graph run 之外。Checkpointer 把一个 thread 的 graph state 以 checkpoint 形式持久化,面向"短期、thread 作用域"的记忆,典型场景是对话连续性、human-in-the-loop、time travel 和容错;它的访问方式是在 graph config 里传一个 thread_id,持久化的是 graph state 的快照。Store 则持久化"graph state 之外、由应用自定义"的 key-value 数据,面向"长期、跨 thread"的记忆,典型场景是用户偏好、事实和共享知识,访问方式是在节点或应用代码里读写 item。两者的关键权衡是作用域:checkpointer 只跟踪当前 thread,而 store 跨 thread。因此大多数应用两者都用——checkpointer 负责当前 thread,store 负责跨 thread 的持久信息。注意当使用 Agent Server 时,持久化基础设施由 server 自动处理,你无需手动实现或配置 checkpointer/store。
术语 Checkpointer(检查点保存器,持久化 thread 的 state 快照); Store(长期存储,持久化应用自定义的跨 thread 数据); thread_id(线程标识,checkpointer 的访问入口与主键); short-term / long-term memory(短期/长期记忆)
📖 "LangGraph's persistence layer gives agents short-term memory through checkpointers and long-term memory through stores." — Persistence
📖 "Most applications can use both: a checkpointer tracks the current thread, and a store tracks durable information across threads." — Persistence
🧪 实例 编译 graph 时同时挂上 checkpointer 与 store:
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore

checkpointer = InMemorySaver()
store = InMemoryStore()

graph = builder.compile(checkpointer=checkpointer, store=store)

result = graph.invoke(
    {"messages": [{"role": "user", "content": "Hi, my name is Bob."}]},
    {"configurable": {"thread_id": "thread-1"}},
)
🔍 追问 为什么单靠 checkpointer 无法实现"跨对话记住用户偏好"? → 因为 checkpointer 的作用域是单个 thread(short-term, thread-scoped),换一个 thread_id 就看不到之前 thread 的 state;跨 thread 的持久信息要放到 store 里。
📚 拓展阅读
  • Checkpointers — checkpointer 的完整指南,讲如何持久化和检视 thread state
  • Stores — store 的完整指南,讲跨 thread 的长期记忆
  • Agent Server — 使用 Agent Server 时持久化基础设施由 server 自动处理
Q什么是 checkpoint 和 super-step?一次简单 graph 运行会产生几个 checkpoint?深挖·拓展🔥高频
checkpoint super-step thread StateSnapshot
⏱️ 现行
checkpointer 在每个 super-step 边界保存一份 graph state 的快照,这份快照就是 checkpoint,用 StateSnapshot 对象表示。super-step 是 graph 的一次"tick",该 step 上被调度的所有节点(可能并行)都会执行;对于 START -> A -> B -> END 这样的顺序 graph,输入、节点 A、节点 B 各是一个独立的 super-step,每个 super-step 之后都产生一个 checkpoint。理解 super-step 边界很重要,因为你只能从一个 checkpoint(即 super-step 边界)恢复执行,这直接关系到 time travel。这些 checkpoint 按 thread 组织——thread 是每个 checkpoint 被分配的唯一 ID,承载一系列 run 累积下来的 state;调用带 checkpointer 的 graph 时你必须在 config 的 configurable 部分指定 thread_id,否则 checkpointer 无法保存 state 或在 interrupt 后恢复,因为它用 thread_id 作为存取 checkpoint 的主键。以文档中带 reducer 的示例 graph 为例,运行后恰好有 4 个 checkpoint:一个以 START 为下一节点的空 checkpoint、一个含用户输入且下一节点是 node_a 的、一个含 node_a 输出且下一节点是 node_b 的、以及一个含 node_b 输出且没有下一节点的。
术语 checkpoint(检查点,某时刻 thread state 的快照); super-step(超步,graph 的一次 tick,所有被调度节点在其中执行); StateSnapshot(表示 checkpoint 的对象); thread(线程,按唯一 ID 组织 checkpoint 序列); reducer(归约器,让带 reducer 的 channel 累积而非覆盖值)
📖 "A checkpointer saves a snapshot of graph state at each super-step, organized into threads." — Checkpointers
📖 "Understanding super-step boundaries is important for time travel, because you can only resume execution from a checkpoint (i.e., a super-step boundary)." — Checkpointers
📖 "The checkpointer uses thread_id as the primary key for storing and retrieving checkpoints." — Checkpointers
🧪 实例 4 个 checkpoint 的产生过程(顺序 graph,baradd reducer):
python
from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
graph = workflow.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "1"}}
graph.invoke({"foo": "", "bar":[]}, config)
# 之后共 4 个 checkpoint,最后一个: {'foo': 'b', 'bar': ['a', 'b']},无下一节点

末个 checkpoint 里 bar 同时含两个节点的输出 ['a', 'b'],正是因为 bar channel 配了 reducer。
🔍 追问 为什么 bar 的最终值是 ['a', 'b'] 而不是 ['b']? → 因为 bar channel 定义了 reducer(add),update_state/节点更新会经过 reducer 累积值,而不是覆盖。
📚 拓展阅读
QLangGraph 的三种 durability mode 是什么?exit / async / sync 如何在性能和数据一致性之间权衡?深挖·拓展🔥高频
durability fault-tolerance performance
⏱️ 现行
LangGraph 提供三种 durability mode,让你在性能和数据一致性之间做平衡,可以在任意 graph 执行方法上通过 durability= 指定。按从"最不持久"到"最持久"排列:"exit" 只在 graph 执行退出时(成功、报错或因 human-in-the-loop interrupt)才持久化,这给长跑 graph 带来最佳性能,但中间态不保存,所以进程崩溃等系统故障发生在执行中途时无法恢复;"async" 在下一步执行的同时异步持久化,性能和持久性都不错,但存在一个小风险——如果进程在执行中崩溃,LangGraph 可能没写入 checkpoint;"sync" 在下一步开始前同步持久化,确保每个 checkpoint 都在继续执行前写入,以一定性能开销换取高持久性。核心权衡就是"写盘时机 vs 崩溃可恢复性":越早、越同步地写盘,崩溃后能恢复的中间态越多,但吞吐越低。
术语 durability mode(持久化模式,控制 checkpoint 写入时机); exit(仅在执行退出时持久化,性能最优); async(下一步执行时异步持久化); sync(下一步开始前同步持久化,持久性最高)
📖 "LangGraph supports three durability modes that let you balance performance and data consistency." — Checkpointers
📖 "LangGraph persists changes synchronously before the next step starts. This ensures that LangGraph writes every checkpoint before continuing execution, providing high durability at the cost of some performance overhead." — Checkpointers
🧪 实例stream 调用上显式指定同步持久化模式:
python
graph.stream(
    {"input": "test"},
    durability="sync"
)

选型经验:长跑但可整体重跑的批处理用 exit 换吞吐;要能从崩溃恢复中途状态的关键流程用 sync;多数在线场景用 async 折中。
🔍 追问exit 模式时进程在执行中途崩溃会怎样? → 中间 state 没有保存,无法从系统故障(如进程崩溃)中途恢复,只能整体重跑。
📚 拓展阅读
  • Checkpointers — durability modes 章节的原文出处
  • Interrupts — human-in-the-loop interrupt 也会触发 exit 模式的持久化
Q什么是 pending writes?它如何为同一 super-step 内的部分节点失败提供容错?深挖·拓展🔥高频
pending-writes fault-tolerance super-step checkpoint_writes
⏱️ 现行
pending writes 是 LangGraph 在节点级(task 级)持久化的写入,用于容错。当一个 graph 节点在某个 super-step 中途失败时,LangGraph 会存下该 super-step 上其他已成功完成节点的 pending checkpoint writes;当你从该 super-step 恢复执行时,就不必重跑那些已成功的节点。机制上,除了 super-step 级的 checkpoint,LangGraph 还在节点级持久化写入:每个节点在 super-step 内完成时,其输出会作为 task 条目写入 checkpointer 的 checkpoint_writes 表,并链接到正在进行的 checkpoint;正是这些 per-task 写入让 pending writes 恢复成为可能——同一 super-step 内另一个节点失败时,已成功节点的写入已经落盘,恢复时无需重算。完整的 state 快照则在整个 super-step 完成后才提交。要注意这些 task 写入不是完整的 StateSnapshot checkpoint,所以 time travel 仍然是从 super-step 边界的完整 checkpoint 恢复,而不是从 task 写入恢复。
术语 pending writes(挂起写入,已成功节点的节点级写入,用于恢复时跳过重跑); checkpoint_writes(存 task 级写入的表); task/node-level writes(节点级写入,不是完整 StateSnapshot); fault-tolerance(容错,节点失败后从上一成功步恢复)
📖 "When a graph node fails mid-execution at a given [super-step](#super-steps), LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that super-step. When you resume graph execution from that super-step you don't re-run the successful nodes." — Checkpointers
📖 "These per-task writes are what enable [pending writes](#pending-writes) recovery: if another node in the same super-step fails, the successful nodes' writes are already durable and don't need to be re-run on resume." — Checkpointers
🧪 实例 一个 super-step 内并行跑 node_x 和 node_y,node_y 抛错:
text
super-step k: [node_x (成功), node_y (失败)]
  -> node_x 的输出作为 task 条目写入 checkpoint_writes 表(pending write)
  -> 从 super-step k 恢复时: node_x 不重跑,只重跑 node_y

底层由 checkpointer 的 .put_writes 方法存储这些链接到 checkpoint 的中间写入。
🔍 追问 pending writes 能用于 time travel 从任意 task 恢复吗? → 不能。task 写入不是完整的 StateSnapshot checkpoint,time travel 只能从 super-step 边界的完整 checkpoint 恢复。
📚 拓展阅读
Q如何读取和修改 graph state?update_state 与 replay 的语义分别是什么?深挖·拓展中频
get_state update_state replay time-travel StateSnapshot
⏱️ 现行
与保存的 graph state 交互时必须指定 thread 标识。graph.get_state(config) 返回该 thread 最新 checkpoint 对应的 StateSnapshot(若在 config 里带了 checkpoint_id 则返回该特定 checkpoint);graph.get_state_history(config) 返回该 thread 的全部 StateSnapshot 列表,按时间倒序、最新的排在第一。StateSnapshot 的字段包括 values(该 checkpoint 的 state channel 值)、next(接下来要执行的节点名,空 () 表示 graph 已完成)、configmetadata(含 sourcewritesstep)、created_atparent_configtasks。修改 state 用 update_state:它会创建一个带更新值的新 checkpoint,而不修改原 checkpoint;更新被当作一次节点更新处理,值会经过定义了的 reducer 函数,所以带 reducer 的 channel 是累积而非覆盖;还可用 as_node 控制这次更新被当作来自哪个节点,从而影响下一个执行的节点。replay 则是从某个先前 checkpoint 重新执行:传入先前的 checkpoint_id,该 checkpoint 之前的节点被跳过(结果已保存),之后的节点重新执行,包括 LLM 调用、API 请求和 interrupt——interrupt 在 replay 时总是被重新触发。
术语 get_state(取最新或指定 checkpoint 的快照); get_state_history(取全部快照,倒序); update_state(创建新 checkpoint,不改原 checkpoint); as_node(控制更新被当作来自哪个节点); replay(从先前 checkpoint 重执行,之前节点跳过)
📖 "You can edit the graph state using update_state. This creates a new checkpoint with the updated values — it does not modify the original checkpoint." — Checkpointers
📖 "Nodes before the checkpoint are skipped (their results are already saved). Nodes after the checkpoint re-execute, including any LLM calls, API requests, or interrupts — which are always re-triggered during replay." — Checkpointers
🧪 实例 取特定 checkpoint、并在历史里按条件定位:
python
# 取指定 checkpoint_id 的快照
config = {"configurable": {"thread_id": "1", "checkpoint_id": "1ef663ba-28fe-6528-8002-5a559208592c"}}
graph.get_state(config)

# 在历史中找由 update_state 创建的 fork
history = list(graph.get_state_history(config))
forks = [s for s in history if s.metadata["source"] == "update"]
🔍 追问 replay 会重放已经保存过结果的 LLM 调用吗? → 不会重放 checkpoint 之前的节点(结果已保存),但 checkpoint 之后重执行的节点会重新触发 LLM 调用、API 请求和 interrupt。
📚 拓展阅读
QLangGraph 提供哪些 checkpointer 实现?它们如何序列化 state,以及如何做加密?深挖·拓展中频
checkpointer-libraries BaseCheckpointSaver serializer encryption
⏱️ 现行
checkpointing 底层由符合 BaseCheckpointSaver 接口的 checkpointer 对象驱动,LangGraph 以独立可安装库的形式提供多个实现:langgraph-checkpoint 是基础接口(含供实验用的内存实现 InMemorySaver,LangGraph 自带);langgraph-checkpoint-sqlite 用 SQLite,适合实验和本地工作流,需单独安装;langgraph-checkpoint-postgres 是用于生产、LangSmith 也在用的 Postgres 高级实现,需单独安装;还有 langchain-azure-cosmosdb 用 Azure Cosmos DB。每个 checkpointer 都实现 .put/.put_writes/.get_tuple/.list 等方法(异步执行时用 .aput 等异步版本)。保存 state 时需要序列化 channel 值,由 serializer 对象完成:默认实现 JsonPlusSerializer 底层用 ormsgpack 和 JSON,处理 LangChain/LangGraph 原语、datetime、enum 等多种类型;对 msgpack 编码器不支持的对象(如 Pandas dataframe),可用 pickle_fallback 回退到 pickle。加密方面,checkpointer 可选地加密全部持久化 state:向任意 BaseCheckpointSaverserde 参数传入 EncryptedSerializer 实例即可,最简单的是用 from_pycryptodome_aes,它从环境变量 LANGGRAPH_AES_KEY 读取 AES key;在 LangSmith 上只要存在该环境变量,加密就会自动启用。
术语 BaseCheckpointSaver(所有 checkpointer 遵循的基接口); InMemorySaver / SqliteSaver / PostgresSaver(内存/SQLite/Postgres 实现); JsonPlusSerializer(默认序列化器,基于 ormsgpack + JSON); pickle_fallback(对不支持类型回退 pickle); EncryptedSerializer(加密序列化器,读 LANGGRAPH_AES_KEY)
📖 "Under the hood, checkpointing is powered by checkpointer objects that conform to BaseCheckpointSaver interface." — Checkpointers
📖 "The default serializer, JsonPlusSerializer, uses ormsgpack and JSON under the hood, which is not suitable for all types of objects." — Checkpointers
🧪 实例 用 AES 加密序列化器包住 SqliteSaver:
python
import sqlite3

from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
from langgraph.checkpoint.sqlite import SqliteSaver

serde = EncryptedSerializer.from_pycryptodome_aes()  # reads LANGGRAPH_AES_KEY
checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db"), serde=serde)

生产环境常识:MemorySaver/InMemorySaver 把 checkpoint 存在 RAM 中,进程重启后全部丢失,应改用 PostgresSaver(异步支持)或 SqliteSaver(本地文件,开发用)。
🔍 追问 想序列化 Pandas dataframe 这类默认不支持的对象怎么办? → 用 JsonPlusSerializer(pickle_fallback=True),对 msgpack 编码器不支持的对象回退到 pickle。
📚 拓展阅读
🔥高频

Store·记忆·工作流与智能体

Stores Memory Workflows and agents
QLangGraph 的 Store 和 Checkpointer 有什么区别?为什么长期记忆要单独用 Store?深挖·拓展🔥高频
Store Checkpointer 跨线程记忆
⏱️ 现行
Checkpointer 和 Store 是 LangGraph 记忆体系里正交的两层。Checkpointer 保存的是绑定到单个 thread 的完整 graph state,本质是"这一段对话的现场快照",用来支撑多轮对话的短期记忆和断点续跑;而 Store 保存的是任意的 key-value 数据,可以从任何 thread 访问,用来承载用户偏好、积累的知识、需要跨越单次会话存活的事实。这两者的分工决定了它们通常一起用:用 thread_id 定位某个会话的 checkpoint,用 user_id 作为 namespace 定位这个用户跨会话共享的记忆。之所以要把长期记忆单独抽成 Store 而不是塞进 state,是因为 state 的生命周期和 thread 绑定——换个 thread 状态就没了;而长期记忆的诉求恰恰是"换了 thread、开了新会话,只要 user_id 相同还能拿到同样的记忆"。生产环境不要用 InMemoryStore(仅适合开发测试),应换成 PostgresStoreMongoDBStoreRedisStore 这类持久化后端,它们都继承自 BaseStore
术语 Checkpointer(检查点器,保存 thread 级完整状态); Store(跨 thread 的 key-value 长期记忆); thread_id(会话/线程标识); user_id(常用作记忆 namespace 的用户标识); BaseStore(所有 store 实现的基类与类型标注)
📖 "Stores let agents persist information across threads, including user preferences, accumulated knowledge, and facts that should survive beyond a single conversation. Unlike checkpointers, which save the full graph state scoped to one thread, stores hold arbitrary key-value data accessible from any thread." — Stores
📖 "The store works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the store allows you to store arbitrary information for access *across* threads." — Stores
🧪 实例 编译时同时挂上两者,就能既有会话记忆又有跨会话记忆:
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore

checkpointer = InMemorySaver()
store = InMemoryStore()
graph = builder.compile(checkpointer=checkpointer, store=store)

调用时传 thread_id 定位会话、传 user_id(经 Context)定位记忆;换 thread_id="2" 的新会话,只要 user_id 不变仍能读到之前存下的记忆。
🔍 追问 用 Agent Server(LangSmith)部署时还要手动配 Store 吗? → 不用,API 会在背后自动处理所有存储基础设施;但要开启语义搜索仍需在 langgraph.json 里配置 index。
📚 拓展阅读
QStore 里的 namespace、put/search 是怎么组织记忆的?怎么开启语义搜索?深挖·拓展🔥高频
namespace 语义搜索 embedding
⏱️ 现行
Store 的记忆用一个字符串 tuple 做 namespace 来分区,例子里是 (<user_id>, "memories"),但 namespace 可以是任意长度、代表任何东西,不必是用户维度。写入用 store.put,传 namespace、一个作为唯一标识的 key(memory_id)、以及作为记忆本体的 value(一个 dict)。读取用 store.search,不带 query/filter 时按 namespace 前缀返回该用户的记忆列表,最多返回 limit 条(默认 10)。在这之上,Store 还支持语义搜索——它让你基于"含义"而不是精确匹配来找记忆,机制是给 store 配一个 embedding 模型(index 里指定 embeddims、要嵌入的 fields),此后 search 就能接受自然语言 query 并按向量相似度排序。权衡点在于:可以用 fields 或 put 时的 index 参数精细控制哪些字段被嵌入,甚至 index=False 存一条"可检索但不可语义搜索"的记忆——这样既省 embedding 成本,又能让系统信息之类不需要语义命中的数据照样存下来。
术语 namespace(字符串 tuple 构成的记忆分区键); store.put(写入 namespace+key+value); store.search(按前缀或语义查询返回记忆列表); index(配置 embedding 模型开启语义搜索); fields(指定哪些字段参与嵌入)
📖 "Memories are namespaced by a tuple, which is (<user_id>, \"memories\") in the following example. The namespace can be any length and represent anything, does not have to be user specific." — Stores
📖 "Beyond simple retrieval, the store also supports semantic search, allowing you to find memories based on meaning rather than exact matches. To enable this, configure the store with an embedding model:" — Stores
🧪 实例 配好 embedding 后用自然语言查最相关的记忆:
python
from langchain.embeddings import init_embeddings
from langgraph.store.memory import InMemoryStore

embeddings = init_embeddings("openai:text-embedding-3-small")
store = InMemoryStore(index={"embed": embeddings, "dims": 1536})

store.put(("user_123", "memories"), "1", {"text": "I love pizza"})
store.put(("user_123", "memories"), "2", {"text": "I am a plumber"})

items = store.search(("user_123", "memories"), query="I'm hungry", limit=1)

"I'm hungry" 语义上与 "I love pizza" 更近,会被排在前面。
🔍 追问 想存一条数据但不希望它被语义搜索命中怎么办? → put 时传 index=False,它仍可被普通检索取回,但不会被嵌入、不参与语义搜索。
📚 拓展阅读
QLangGraph 里短期记忆和长期记忆分别是什么?各自怎么加?深挖·拓展🔥高频
短期记忆 长期记忆 持久化
⏱️ 现行
LangGraph 把记忆分成两类,对应两种诉求。短期记忆就是 thread 级持久化,它作为 agent state 的一部分,让 agent 能追踪多轮对话——实现方式是编译时传一个 checkpointer(开发用 InMemorySaver,生产用数据库支撑的 PostgresSaver/MongoDBSaver/RedisSaver 等),调用时带上 thread_id,同一 thread 的后续调用就能记住之前说过的话(比如"我叫 Bob"→后面问"我叫什么"能答上来)。长期记忆则用来跨会话存储用户级或应用级数据,实现方式是编译时传一个 store,在节点里通过 Runtime 对象拿到 runtime.storeasearch/aput。两者可以同时挂载:checkpointer 管"这轮对话记住了什么",store 管"这个用户跨所有会话该记住什么"。设计上之所以分层,是因为对话现场适合随 thread 走、可回滚、可续跑,而用户画像类知识必须独立于任何单次会话长期存活。此外在子图场景里,只需给父图编译时提供 checkpointer,LangGraph 会自动把它传播给子图。
术语 短期记忆(thread 级持久化,支撑多轮对话); 长期记忆(跨会话的用户/应用级数据); InMemorySaver(内存 checkpointer,开发用); Runtime(节点里注入的运行时对象,可取 store 与 context); store=(编译时挂载长期记忆后端)
📖 "Short-term memory (thread-level persistence) enables agents to track multi-turn conversations." — Memory
📖 "Use long-term memory to store user-specific or application-specific data across conversations." — Memory
🧪 实例 节点里通过 Runtime 同时读写长期记忆:
python
async def call_model(state: MessagesState, runtime: Runtime[Context]):
    user_id = runtime.context.user_id
    namespace = (user_id, "memories")
    memories = await runtime.store.asearch(
        namespace, query=state["messages"][-1].content, limit=3
    )
    info = "\n".join([d.value["data"] for d in memories])
    await runtime.store.aput(
        namespace, str(uuid.uuid4()), {"data": "User prefers dark mode"}
    )
🔍 追问 子图也要各自传 checkpointer 吗? → 不需要,只给父图编译时传 checkpointer,LangGraph 会自动把它传播给子图。
📚 拓展阅读
  • persistence — thread 级持久化机制
  • memory — 记忆概念总览
  • Stores — 长期记忆 store 的完整用法
Q长对话超出上下文窗口时,怎么管理短期记忆?深挖·拓展中频
上下文窗口 trim_messages RemoveMessage
⏱️ 现行
短期记忆开启后,长对话会撑爆 LLM 的上下文窗口,官方给了几条常见对策:trim(裁掉前/后 N 条消息,在调用 LLM 之前)、delete(从 LangGraph state 永久删除消息)、summarize(把早期历史总结成一条摘要来替换)、以及管理 checkpoints 来存取消息历史。trim 的思路是数 token、逼近上限就截断——用 trim_messages 指定要保留的 token 数和 strategy(如 "last" 保留最后 max_tokens),并用 start_on/end_on 控制边界以保证裁剪后的消息序列仍然合法。delete 用 RemoveMessage,它要求 state key 用 add_messages reducer(如 MessagesState),可以按 id 删指定消息,也可以用 REMOVE_ALL_MESSAGES 清空全部。这里的核心权衡是:不管 trim 还是 delete,都必须保证结果消息历史对目标 LLM 供应商合法——有些供应商要求历史以 user 消息开头,大多数要求带 tool call 的 assistant 消息后面必须跟对应的 tool 结果消息,否则会报错。
术语 trim_messages(按 token 数裁剪消息的工具函数); count_tokens_approximately(近似 token 计数器); RemoveMessage(用于从 state 删除消息); REMOVE_ALL_MESSAGES(删除全部消息的哨兵); add_messages(支持删除的消息 reducer)
📖 "With [short-term memory](#add-short-term-memory) enabled, long conversations can exceed the LLM's context window." — Memory
📖 "One way to decide when to truncate messages is to count the tokens in the message history and truncate whenever it approaches that limit." — Memory
🧪 实例 调用模型前裁剪到 128 token、保留最近消息:
python
from langchain_core.messages.utils import trim_messages, count_tokens_approximately

def call_model(state: MessagesState):
    messages = trim_messages(
        state["messages"],
        strategy="last",
        token_counter=count_tokens_approximately,
        max_tokens=128,
        start_on="human",
        end_on=("human", "tool"),
    )
    response = model.invoke(messages)
    return {"messages": [response]}
🔍 追问 删消息时最容易踩的坑是什么? → 删完必须保证历史合法:很多供应商要求带 tool call 的 assistant 消息后必须紧跟对应的 tool 结果消息,部分供应商要求历史以 user 消息开头。
📚 拓展阅读
Q工作流(workflow)和智能体(agent)有什么本质区别?常见的工作流模式有哪些?深挖·拓展低频
workflow agent 编排模式
⏱️ 现行
二者的分界在于"路径由谁决定"。工作流有预先确定的代码路径、被设计成按某个既定顺序执行,适合可拆成小的、可验证步骤的明确任务;而智能体是动态的,自己定义流程和工具使用,在持续的反馈循环中运行,拥有比工作流更高的自主性,能自行决定用哪些工具、如何解题——但你仍可限定它的工具集和行为准则。文档给出几种常见工作流模式:Prompt chaining(每次 LLM 调用处理上一次的输出,用于可分解为可验证步骤的任务)、Parallelization(多个 LLM 同时工作,或拆分独立子任务提速、或多次运行同一任务增强置信度)、Routing(先对输入分类再路由到专门化流程)、Orchestrator-worker(编排者拆解任务、派发给 worker、再综合输出,适合子任务无法预先定义的场景)、Evaluator-optimizer(一个 LLM 生成、另一个评估,不达标就带反馈重做直到通过)。选型逻辑是:问题和解法可预测、可编排就用工作流以获得可控性;问题和解法不可预测就用 agent 换取灵活性。
术语 workflow(预定路径、按固定顺序执行); agent(动态自主、自定义流程与工具); Prompt chaining(链式串联 LLM 调用); Routing(按输入类型路由到专门流程); Evaluator-optimizer(生成-评估-反馈的迭代环)
📖 "Workflows have predetermined code paths and are designed to operate in a certain order." — Workflows and agents
📖 "Agents have more autonomy than workflows, and can make decisions about the tools they use and how to solve problems. You can still define the available toolset and guidelines for how agents behave." — Workflows and agents
🧪 实例 agent 的核心是"LLM 决定是否继续调工具"的条件循环:
flowchart LR
    START --> llm_call
    llm_call -->|有 tool_calls| tool_node
    llm_call -->|无 tool_calls| END
    tool_node --> llm_call

对应代码里 should_continue 检查 last_message.tool_calls:有则回到 tool_node 执行工具,否则返回 END 回复用户。
🔍 追问 构建工作流/agent 对底座 LLM 有什么要求? → 要用支持结构化输出和工具调用(tool calling)的 chat model,文档示例用的是 Anthropic 的 ChatAnthropic
📚 拓展阅读
QOrchestrator-worker 模式怎么用 Send API 动态生成 worker?它和 Parallelization 有何不同?深挖·拓展中频
Orchestrator-worker Send 并行
⏱️ 现行
在 orchestrator-worker 配置里,编排者负责把任务拆成子任务、把子任务派发给 worker、再把各 worker 的输出综合成最终结果。它和 parallelization 的关键差异在于"子任务能不能预先定义":parallelization 适合子任务固定已知的场景,而 orchestrator-worker 提供更大灵活性,常用于子任务无法像 parallelization 那样预先定义的情况——比如写代码、或需要跨未知数量的多个文件更新内容。LangGraph 内建支持这一模式,核心是 Send API:它让你动态创建 worker 节点并给每个 worker 发送特定输入。机制上每个 worker 有自己的 state,而所有 worker 的输出都写入一个共享的 state key(用 Annotated[list, operator.add] reducer 让多个 worker 并行地往同一个 key 追加),这样 orchestrator 就能拿到全部 worker 输出并综合成最终结果。典型写法是在条件边函数里遍历 sections 列表,对每个 section 用 Send("llm_call", {"section": s}) 并行 kick off。
术语 Orchestrator-worker(编排者拆分-派发-综合的模式); Send API(动态创建 worker 节点并投递输入); 共享 state key(worker 输出汇聚处,用 reducer 并行追加); operator.add(让多 worker 并行写同一 key 的 reducer); assign_workers(条件边函数,批量发起 Send)
📖 "The Send API lets you dynamically create worker nodes and send them specific inputs. Each worker has its own state, and all worker outputs are written to a shared state key that is accessible to the orchestrator graph." — Workflows and agents
📖 "Orchestrator-worker workflows provide more flexibility and are often used when subtasks cannot be predefined the way they can with [parallelization](#parallelization)." — Workflows and agents
🧪 实例 用条件边 + Send 给每个报告 section 并行分配一个 worker:
python
from langgraph.types import Send

def assign_workers(state: State):
    """Assign a worker to each section in the plan"""
    return [Send("llm_call", {"section": s}) for s in state["sections"]]

orchestrator_worker_builder.add_conditional_edges(
    "orchestrator", assign_workers, ["llm_call"]
)

worker 把结果写回 completed_sections(声明为 Annotated[list, operator.add]),synthesizer 再拼成最终报告。
🔍 追问 为什么 worker 输出的 state key 要用 operator.add reducer? → 因为所有 worker 并行地往这同一个 key 写,reducer 保证并行追加能被正确合并而不是互相覆盖。
📚 拓展阅读

第4章 流式·中断·子图·时间旅行

🔥高频

流式与事件流

Streaming Event streaming
QLangGraph 的 stream_mode 一共有哪几种模式?各自输出什么、有什么取舍?深挖·拓展🔥高频
streaming stream_mode
⏱️ 现行
stream()/astream() 通过传入一个或多个 stream mode 控制你收到什么数据,共七种:values(每步后的完整 state 快照)、updates(每步后各节点返回的增量,同一步多次更新会分别流出)、messages(LLM 调用产生的 (token, metadata) 二元组,用于逐 token 流式)、custom(节点内经 get_stream_writer 发出的任意数据)、checkpoints(检查点事件,格式同 get_state(),需 checkpointer)、tasks(任务 start/finish 事件含结果与错误,需 checkpointer)、debug(信息最全,组合 checkpointstasks 并附加额外元数据)。取舍点在于:values 给你完整状态但冗余更大,updates 只给变化更省带宽但需自己拼装全局视图;checkpoints/tasks/debug 依赖 checkpointer,debug 最全但最重,若只需子集应直接用 checkpointstasks。可以把多种模式作为列表一次传入,在 v2 下用 chunk["type"] 区分。
术语 values(每步完整状态快照); updates(每步节点增量更新); messages(LLM token+metadata 二元组); custom(用户自定义数据); checkpoints(检查点事件,需 checkpointer); tasks(任务起止事件,需 checkpointer); debug(最全,合并 checkpoints 与 tasks)
📖 "It exposes graph execution through stream modes such as updates, values, messages, custom, checkpoints, tasks, and debug." — Streaming
📖 "The debug mode combines checkpoints and tasks events with additional metadata. Use checkpoints or tasks directly if you only need a subset of the debug information." — Streaming
🧪 实例 同时订阅 updatescustom,按类型分流处理:
python
for chunk in graph.stream(inputs, stream_mode=["updates", "custom"], version="v2"):
    if chunk["type"] == "updates":
        for node_name, state in chunk["data"].items():
            print(f"Node `{node_name}` updated: {state}")
    elif chunk["type"] == "custom":
        print(f"Custom event: {chunk['data']}")
🔍 追问 checkpointstasks 模式为什么用不了会报错? → 这两种(以及 debug)都需要在 compile(checkpointer=...) 时提供 checkpointer,否则没有检查点/任务事件可流。
📚 拓展阅读
QEvent streaming(stream_events)与传统 streaming(stream_mode)是什么关系?什么时候用哪个?深挖·拓展🔥高频
event-streaming projections
⏱️ LangGraph v1.2 起
二者是分层关系:底层 streaming 从 Pregel 引擎直接吐出原始执行事件(updates/values/messages/…),而 event streaming 位于其上一层,把这些原始事件规范化、经 stream transformer 管线处理后暴露成"带类型的投影"(typed projections)。stream_events(input, version="v3") 返回一个 run stream 对象,上面挂着 stream.messages(聊天模型消息与 token 增量)、stream.values(状态快照并可 await 最终值)、stream.output(最终输出)、stream.subgraphs(嵌套子图执行,无需解析 namespace 字符串)、stream.interrupts/stream.interrupted(HITL 中断)、stream.extensions(自定义 transformer 投影)。关键优势是多个消费者可以并发读取同一底层事件流互不消耗——读 stream.messages 不会耗掉 stream.values 需要的事件,因此不必再像 stream_mode 那样在一个循环里按 chunk 类型分支。取舍:需要底层原始模式的精细控制时用 streaming;应用代码更受益于类型化投影时用 event streaming(官方推荐新应用用后者)。
术语 stream_events(version="v3")(事件流入口,返回 run stream 对象); typed projections(类型化投影,如 stream.messages/values/output); stream transformers(事件规范化后的投影层); Pregel engine(底层执行引擎,发出原始事件); stream.extensions(自定义 transformer 投影出口)
📖 "Event streaming is the recommended in-process streaming model for most LangGraph application code. It returns a run stream object that can be consumed in multiple ways at the same time." — Event streaming
📖 "Multiple consumers can read these projections concurrently. Reading stream.messages does not consume events needed by stream.values, stream.subgraphs, or stream.output." — Event streaming
📖 "Use streaming when you need low-level access to those modes; use event streaming when application code benefits from typed projections." — Event streaming
🧪 实例 两层的数据流可视化,以及 v3 快速上手:
flowchart TD
    A[Pregel engine 运行图步骤] -->|emits| B[Raw Pregel events]
    B -->|sent to| C[Event router]
    C -->|cascades through| D[Stream transformers]
    D -->|produces| E[Event Stream 投影]
python
stream = graph.stream_events({
    "messages": [{"role": "user", "content": "What is 42 * 17?"}],
}, version="v3")

for message in stream.messages:
    for token in message.text:
        print(token, end="", flush=True)

final_state = stream.output
🔍 追问 在同步代码里想按严格到达顺序同时消费多个投影怎么做? → 用 stream.interleave("values", "messages", "subgraphs"),它按到达顺序产出 (name, item);异步则用 astream_events 配合 asyncio.gather
📚 拓展阅读
Qmessages 模式如何逐 token 流式输出 LLM?怎样按调用/节点过滤,或彻底排除某次输出?深挖·拓展🔥高频
messages llm-tokens filter
⏱️ 现行
messages 模式能从图的任意部分(节点、工具、子图、任务)逐 token 流式输出 LLM;每个 chunk 的 data(message_chunk, metadata) 二元组,message_chunk 是 token/消息片段,metadata 含图节点与 LLM 调用信息(注意:即便 LLM 用 .invoke 而非 .stream 也会发出 message 事件)。过滤有三条常用路径:一是给 init_chat_model(..., tags=['joke']) 打标签,再按 metadata["tags"] 过滤;二是按 metadata["langgraph_node"] 只保留特定节点的 token;三是用 nostream 标签把某次调用的输出彻底排除出流——被标记的调用仍会运行并产出结果,只是其 token 不在 messages 模式中发出,适用于"需要 LLM 结果做内部处理(如结构化输出)但不想推给客户端"或"同内容已走别的通道、避免重复"。注意 Python < 3.11 的异步场景必须显式把 RunnableConfig 传进 ainvoke(),否则回调无法传播、流式失效。
术语 (message_chunk, metadata)(token 片段+调用元数据); tags(LLM 调用标签,用于按调用过滤); langgraph_node(metadata 字段,用于按节点过滤); nostream(排除某次 LLM 输出的标签)
📖 "Use the messages streaming mode to stream Large Language Model (LLM) outputs token by token from any part of your graph, including nodes, tools, subgraphs, or tasks." — Streaming
📖 "Use the nostream tag to exclude LLM output from the stream entirely. Invocations tagged with nostream still run and produce output; their tokens are simply not emitted in messages mode." — Streaming
🧪 实例 只打印带 joke 标签那次 LLM 的 token:
python
async for chunk in graph.astream(
    {"topic": "cats"},
    stream_mode="messages",
    version="v2",
):
    if chunk["type"] == "messages":
        msg, metadata = chunk["data"]
        if metadata["tags"] == ["joke"]:
            print(msg.content, end="|", flush=True)
🔍 追问 只想要某个节点(如 write_poem)的 token 怎么写? → stream_mode="messages" 后按 metadata["langgraph_node"] == "write_poem" 过滤 msg.content
📚 拓展阅读
Qv2 的 StreamPart 统一格式解决了什么痛点?与 v1 相比有哪些差异?深挖·拓展🔥高频
version-v2 StreamPart
⏱️ LangGraph ≥ 1.1
v1(默认)下输出形态随选项而变——单模式返回原始 dict、多模式返回 (mode, data) 元组、开子图返回 (namespace, data) 元组,消费端要为不同组合写不同解包逻辑。传 version="v2" 后每个 chunk 都是形状一致的 StreamPart dict:type(哪种模式)、ns(namespace 元组,子图事件才填充)、data(实际载荷,类型随模式而变),无论几种模式、是否开子图都相同。这带来两个好处:一是类型收窄(type narrowing),按 chunk["type"] 过滤即可让编辑器/类型检查器把 part["data"] 收窄到该模式的具体类型;二是子图事件用同一 StreamPart,只需看 ns 区分根图与子图。invoke 侧 v2 返回 GraphOutput(带 .value.interrupts),把状态与中断元数据分离,取代 v1 把中断塞在结果 dict 的 __interrupt__ 键里的做法;此外 v2 的 values 模式会把 Pydantic/dataclass 状态强制转换回对应实例类型。
术语 StreamPart(统一 chunk 结构,含 type/ns/data); ns(namespace 元组,子图事件填充); type narrowing(按 type 收窄 data 类型); GraphOutput(v2 invoke 返回,含 .value 与 .interrupts); __interrupt__(v1 中断所在键,v2 已分离)
📖 "Every chunk is a StreamPart dict with a consistent shape — regardless of stream mode, number of modes, or subgraph settings:" — Streaming
📖 "With v1 (default), the output format changes based on your streaming options (single mode returns raw data, multiple modes return (mode, data) tuples, subgraphs return (namespace, data) tuples). With v2, the format is always the same:" — Streaming
🧪 实例 v2 与 v1 单模式输出对比:
python
# v2:统一 StreamPart
for chunk in graph.stream(inputs, stream_mode="updates", version="v2"):
    print(chunk["type"])  # "updates"
    print(chunk["ns"])    # ()
    print(chunk["data"])  # {"node_name": {"key": "value"}}

# v1:原始 dict
for chunk in graph.stream(inputs, stream_mode="updates"):
    print(chunk)  # {"node_name": {"key": "value"}}
🔍 追问 v2 里怎么拿 invoke 的中断?旧的 result["__interrupt__"] 还能用吗? → 用 result.interrupts(以及 result.value);dict 式访问仍向后兼容但已 deprecated,未来版本会移除。
📚 拓展阅读
Q如何从节点/工具发送自定义数据,并对接不实现 LangChain 接口的任意 LLM?深挖·拓展中频
custom get_stream_writer any-llm
⏱️ 现行
发送自定义数据分两步:在节点或工具内用 get_stream_writer() 拿到 writer 并 emit 键值对,再在 .stream()/.astream() 时设 stream_mode="custom"(可与其他模式组合如 ["updates", "custom"],但至少要有一个 "custom")。这条 custom 通道的最大价值是能流式对接"任意 LLM API"——即使该 API 不实现 LangChain 聊天模型接口:在节点里遍历你自己的流式客户端产出的 chunk,用 writer({...}) 把每块推进流,消费端按 chunk["type"] == "custom"chunk["data"]。这让 LangGraph 能整合原始 LLM 客户端或提供自有流式接口的外部服务。注意 Python < 3.11 的异步代码里 get_stream_writer 不可用,必须给节点/工具加一个 writer 参数由 LangGraph 自动注入(StreamWriter 类型)。
术语 get_stream_writer(节点内取 stream writer 发自定义数据); stream_mode="custom"(接收自定义数据); StreamWriter(Python<3.11 异步下手动注入的 writer 参数); custom_llm_chunk(把任意 LLM token 塞进流的自定义键示例)
📖 "You can use stream_mode="custom" to stream data from any LLM API—even if that API does not implement the LangChain chat model interface." — Streaming
📖 "In async code running on Python \< 3.11, get_stream_writer will not work." — Streaming
🧪 实例 在节点里桥接任意流式客户端:
python
from langgraph.config import get_stream_writer

def call_arbitrary_model(state):
    writer = get_stream_writer()
    for chunk in your_custom_streaming_client(state["topic"]):
        writer({"custom_llm_chunk": chunk})
    return {"result": "completed"}
🔍 追问 在 Python 3.10 的异步节点里发自定义数据会怎样? → get_stream_writer 不生效,必须在函数签名里加 writer: StreamWriter 参数,LangGraph 会自动把 stream writer 传进来。
📚 拓展阅读
Q自定义 stream transformer 是什么?required_stream_modesStreamChannel 各起什么作用?深挖·拓展中频
transformers StreamChannel
⏱️ event streaming
stream transformer 是 event streaming 的投影层:它们观察 protocol event、维护自身状态,并暴露对一次 run 的派生视图(如工具活动、token 累计、进度、artifacts)。内置投影(stream.messages/values/subgraphs/output)本身就是遵循同一契约的 transformer,用户 transformer 通过 compile 期或 call 期注册叠加,其投影出现在 stream.extensions 下。接口有 init()(建投影对象)、process(event)(观察每个事件,仅当想抑制原事件时返回 false)、finalize()(成功结束时收尾)、fail()(传播错误)。两个关键机制:required_stream_modes 声明该 transformer 需要哪些 Pregel 模式——运行时取所有已注册 transformer 的并集作为 .stream()stream_mode,没有任何 transformer 请求的模式根本不会发出,所以声明 ("custom",) 才是让 custom 事件流动起来的原因;StreamChannel 是 transformer 推送流式值的投影原语,构造时传名字则每次 push() 还会作为 custom:<name> 事件流入主事件流(要求可序列化),不传名字则只是侧通道投影,适合装 promise、异步迭代器、类实例等不可序列化的进程内句柄。
术语 StreamTransformer(投影层接口,含 init/process/finalize/fail); process(event)(逐事件观察,返回 false 抑制原事件); required_stream_modes(声明需要的 Pregel 模式,取并集驱动上游发射); StreamChannel(推送流式值的投影原语); stream.extensions(用户 transformer 投影出口)
📖 "Stream transformers are the projection layer in event streaming. They observe protocol events, keep their own state, and expose derived views of a run" — Event streaming
📖 "Modes that no transformer requests are never emitted — declaring ("custom",) is what causes custom events to flow through the run at all." — Event streaming
🧪 实例 transformer 管线与一个声明 custom 模式的最小 transformer:
flowchart TD
    A[Pregel modes] --> B[Events]
    B --> C[Built-in projections]
    C --> D[User transformers]
    D --> E[Run projections]
python
class CustomTransformer(StreamTransformer):
    required_stream_modes = ("custom",)

    def process(self, event: ProtocolEvent) -> bool:
        if event["method"] == "custom":
            ...
        return True
🔍 追问StreamChannel 传名字和不传名字有什么区别? → 传名字(StreamChannel("name"))时每次 push 既进 stream.extensions.<name> 又作为 custom:<name> 进主事件流(载荷须可序列化);不传名字只做侧通道投影,适合放不可序列化的进程内句柄。
📚 拓展阅读
🔥高频

中断·子图·时间旅行

Interrupts Subgraphs Use time-travel
QLangGraph 的 interrupt() 是怎么暂停图执行并恢复的?它和静态断点有什么区别?深挖·拓展🔥高频
interrupt HITL checkpointer
⏱️ 现行
interrupt() 让你在图节点内部任意位置暂停执行、等待外部输入后再继续,是实现 human-in-the-loop 的核心机制。调用时它接受任意 JSON-serializable 的值并把该值抛给调用方;LangGraph 用 persistence 层把当前图状态保存下来,然后无限期等待,直到你用 Command(resume=...) 重新 invoke 图,这个 resume 值就成为节点内 interrupt() 调用的返回值。要工作必须满足三个前提:一个 checkpointer 来持久化状态(生产环境要用持久化后端)、config 里的 thread_id 让运行时知道从哪个状态恢复、以及在想暂停的地方调用 interrupt()(payload 必须可序列化)。thread_id 本质上是你的持久化游标:复用同一个 thread_id 会恢复同一 checkpoint,换新值则开一个空状态的新 thread。它与静态断点(interrupt_before / interrupt_after,只能在节点前后暂停)的关键区别是 dynamic——可放在代码任意处并根据应用逻辑做条件判断;因此官方明确不推荐用静态断点做 HITL,静态断点只适合调试单步跟踪。权衡在于:用 stream_events(..., version="v3") 驱动可以从 stream.interrupts / stream.interrupted 拿到 payload 和暂停标志,而默认 invoke() 则把中断放在 result["__interrupt__"] 下。
术语 interrupt()(在节点内暂停并把值抛给调用方的函数); Command(resume=...)(唯一用作 invoke/stream 输入的 Command 形态,其值成为 interrupt 的返回值); thread_id(持久化游标,决定加载哪个 checkpoint); stream.interrupted(运行因等待输入而暂停时为 True)
📖 "Interrupts allow you to pause graph execution at specific points and wait for external input before continuing. This enables human-in-the-loop patterns where you need external input to proceed." — Interrupts
📖 "Unlike static breakpoints (which pause before or after specific nodes), interrupts are dynamic: they can be placed anywhere in your code and can be conditional based on your application logic." — Interrupts
📖 "The thread_id you choose is effectively your persistent cursor. Reusing it resumes the same checkpoint; using a new value starts a brand-new thread with an empty state." — Interrupts
🧪 实例 审批节点暂停后,用同一 thread_id 恢复:
python
from langgraph.types import Command, interrupt

def approval_node(state):
    approved = interrupt("Do you approve this action?")
    return {"approved": approved}

config = {"configurable": {"thread_id": "thread-1"}}
stream = graph.stream_events({"input": "data"}, config=config, version="v3")
if stream.interrupted:
    print(stream.interrupts)   # (Interrupt(value='Do you approve this action?'),)
resumed = graph.stream_events(Command(resume=True), config=config, version="v3")
🔍 追问 为什么 Command(update=...) 不能作为 invoke 的输入来做多轮对话? → 因为只有 Command(resume=...) 被设计成 invoke/stream/stream_events 的输入;update/goto/graph 是给节点函数返回用的,多轮对话应传普通输入 dict。
📚 拓展阅读
Q恢复时节点会从头重跑,这带来哪些"中断规则"?为什么不能 while 循环校验、也不能包裹 try/except?深挖·拓展🔥高频
interrupt 幂等 节点重放
⏱️ 现行
interrupt() 通过抛出一个特殊异常来暂停——异常沿调用栈上抛被运行时捕获,运行时保存状态并等待输入。恢复时运行时会从节点开头整体重启节点,而不是从 interrupt 那一行继续,因此 interrupt 之前的所有代码都会再次执行。由此推导出几条硬规则:(1) 不能用 bare try/except 包裹 interrupt,否则会吞掉那个暂停异常,中断就传不回图——应把 interrupt 与易错代码分开,或只捕获具体异常类型;(2) 同一节点内多个 interrupt 的匹配是严格按索引进行的,所以不能条件性跳过或用非确定性逻辑循环 interrupt(否则第一次恢复回放 1 次、第二次回放 2 次……导致循环体指数级重复执行),校验输入要改用"每次调用只 interrupt() 一次 + 把重问问题存进 state + conditional edge 回环"的模式;(3) 只传简单可序列化的值给 interrupt,不能传函数/类实例;(4) interrupt 之前的副作用应当幂等——比如用 upsert 而非 create,或把副作用放到 interrupt 之后甚至单独节点,否则每次恢复重放都会重复写库、产生重复记录。核心权衡:节点重放换来了无 checkpointer 依赖的可恢复性,但把幂等性与调用顺序稳定性的责任压给了开发者。
术语 节点重放(恢复时整节点从头再跑一遍); index-based matching(多 interrupt 按索引匹配 resume 值); 幂等(同一操作多次执行结果不变); conditional edge(用条件边回环取代 while 校验循环)
📖 "Matching is strictly index-based, so the order of interrupt calls within the node is important." — Interrupts
📖 "The result is exponential re-execution of any code inside the loop body." — Interrupts
📖 "Depending on which checkpointer is used, complex values may not be serializable (e.g. you can't serialize a function)." — Interrupts
🧪 实例 正确的一次性校验 + 条件边回环:
python
def get_age_node(state):
    question = state.get("pending_question") or "What is your age?"
    answer = interrupt(question)  # 每次调用恰好一次
    if isinstance(answer, int) and answer > 0:
        return {"age": answer, "pending_question": None}
    return {"pending_question": f"'{answer}' is not a valid age. Please enter a positive number."}

def route(state):
    return END if state.get("age") is not None else "collect_age"
🔍 追问 为什么把 create/append 类副作用放 interrupt 之前是错的? → 因为节点会重放,create 记录/append 列表在每次恢复时都会重复执行,产生重复记录;应改用幂等 upsert 或把副作用移到 interrupt 之后只在批准后运行。
📚 拓展阅读
Q父图与子图有哪两种通信/接入方式?各自何时用?深挖·拓展🔥高频
subgraph state-schema add_node
⏱️ 现行
加子图时要先决定父图和子图如何通信,官方给出两种模式。第一种"在节点内调用子图":当父子 state schema 不同(没有共享 key)、或需要在两者之间转换状态时使用——你写一个 wrapper 节点函数,先把父状态映射成子图输入,invoke 子图,再把子图输出映射回父状态返回。这在多智能体系统里很常见,因为你想给每个 agent 保留私有的消息历史。第二种"把子图作为节点加入":当父子 共享 state key 时,直接把编译好的子图传给 add_node,不需要 wrapper——子图自动读写父图相同的 state channel,例如多 agent 常通过共享的 messages key 通信。两者的权衡在于隔离性 vs 便利性:调用式给你完全的状态转换控制和私有历史,但要手写映射;作为节点接入零样板,但父子必须共享 channel。子图还能多层嵌套(parent → child → grandchild),每层只能访问自己 schema 里的 key。
术语 subgraph(被当作另一张图的一个节点使用的图); wrapper 节点函数(在 schema 不同时做父↔子状态映射的函数); add_node(subgraph)(共享 key 时直接把编译子图作为节点); shared state keys(父子共用的 state channel)
📖 "When adding subgraphs, you need to define how the parent graph and the subgraph communicate:" — Subgraphs
📖 "When the parent graph and subgraph have different state schemas (no shared keys), invoke the subgraph inside a node function." — Subgraphs
📖 "When the parent graph and subgraph share state keys, you can pass a compiled subgraph directly to add_node. No wrapper function is needed—the subgraph reads from and writes to the parent's state channels automatically." — Subgraphs
🧪 实例 两种接法对比:
python
# 方式一:schema 不同 —— 节点内调用并做映射
def call_subgraph(state):
    subgraph_output = subgraph.invoke({"bar": state["foo"]})
    return {"foo": subgraph_output["bar"]}
builder.add_node("node_1", call_subgraph)

# 方式二:共享 key —— 直接作为节点
builder.add_node("node_1", subgraph)
flowchart TD
    A[父图状态] -->|schema 不同| B[wrapper 节点
映射→invoke→映射回] A -->|共享 key| C[add_node子图
直接读写父 channel]
🔍 追问 分布式开发中为什么子图有用? → 只要遵守子图接口(输入/输出 schema),不同团队可以各自独立开发各部分,父图无需知道子图内部细节即可搭建。
📚 拓展阅读
Q子图的 checkpointer 参数有哪三种取值?分别对应什么持久化行为?深挖·拓展🔥高频
subgraph checkpointer persistence
⏱️ 现行
编译子图时 .compile()checkpointer 参数控制子图持久化,共三档。Per-invocation(默认,checkpointer=None 或省略):每次调用从头开始,但在单次调用内继承父图 checkpointer,从而支持 interrupt 暂停/恢复和单次调用内的 durable execution——这是大多数应用(包括把子 agent 当工具调用的多智能体系统)的推荐选择,它支持中断、持久化和并行调用,同时保持每次调用互相隔离。Per-thread(checkpointer=True):状态在同一 thread 的多次调用间累积,每次调用接着上次继续,适合需要多轮对话记忆的子 agent(如逐步积累上下文的研究助手);但它不支持并行工具调用,因为并行调用会写同一 namespace 造成 checkpoint 冲突,需用 ToolCallLimitMiddleware 之类手段防止。Stateless(checkpointer=False):完全不 checkpoint,像普通函数调用,没有中断也没有 durable execution,进程崩溃就得从头重跑。权衡是隔离/新鲜 vs 记忆累积 vs 零开销:注意父图必须带 checkpointer,子图的中断、状态检查、多轮记忆才生效。
术语 Per-invocation(默认,每次调用新鲜但单次内继承父 checkpointer,支持 HITL); Per-thread(checkpointer=True,跨调用累积状态但不支持并行调用); Stateless(checkpointer=False,无持久化、不可暂停恢复); namespace 冲突(并行写同一 checkpoint namespace 引发的冲突)
📖 "Omit checkpointer or set it to None. Each call starts fresh, but within a single call the subgraph inherits the parent's checkpointer and can use interrupt() to pause and resume." — Subgraphs
📖 "Per-thread subgraphs do not support parallel tool calls. When an LLM has access to a per-thread subagent as a tool, it may try to call that tool multiple times in parallel (for example, asking the fruit expert about apples and bananas simultaneously). This causes checkpoint conflicts because both calls write to the same namespace." — Subgraphs
📖 "Use this when you want to run a subagent like a plain function call with no checkpointing overhead. The subgraph cannot pause/resume and does not benefit from" — Subgraphs
🧪 实例 三种编译方式:
python
subgraph = builder.compile(checkpointer=None)   # per-invocation(默认)
subgraph = builder.compile(checkpointer=True)   # per-thread,累积记忆
subgraph = builder.compile(checkpointer=False)  # stateless,无中断/无恢复
🔍 追问 客服机器人里"账单专家"子 agent 该记住之前的问题还是每次从头? → 若需多轮记忆(逐步积累上下文)用 per-thread;若处理彼此独立的一次性请求(如"查这个客户的订单")用默认 per-invocation。
📚 拓展阅读
Q时间旅行的 Replay 和 Fork 有什么区别?为什么说 update_state 不是回滚?深挖·拓展中频
time-travel replay fork checkpoint
⏱️ 现行
LangGraph 通过 checkpoint 支持时间旅行,分 Replay(从某个历史 checkpoint 重试)和 Fork(从历史 checkpoint 改状态后分叉、探索另一条路径)。两者都靠从某个先前 checkpoint 恢复来工作:checkpoint 之前的节点不会重新执行(结果已保存),之后的节点会重新执行——包括 LLM 调用、API 请求和 interrupt,因此可能产生不同结果。这也是关键警告:Replay 会真正重跑节点而非读缓存,从最终 checkpoint(没有 next 节点)replay 是 no-op。Replay 用 get_state_history 找到目标 checkpoint 再用它的 config 调 invoke。Fork 则先对某历史 checkpoint 调 update_state 生成分叉,再用 None invoke 继续。特别要理解:update_state 不会回滚 thread,它是新建一个从指定点分叉的 checkpoint,原执行历史完整保留。update_state 时值会用指定节点的 writers(含 reducers)应用,checkpoint 记录该节点产生了此更新,执行从该节点的后继恢复;默认从 checkpoint 版本历史推断 as_node,但并行分支/无历史/跳节点时需显式指定。权衡:时间旅行让你无损探索多条分支,代价是后续节点(和其副作用/LLM 成本)会重跑。
术语 Replay(从历史 checkpoint 用其 config 重试,重跑其后节点); Fork(用 update_state 改状态后分叉出新分支); get_state_history(逆时序列出可回放的 checkpoint); as_node(指定此更新算作哪个节点产生,决定从哪恢复)
📖 "Both work by resuming from a prior checkpoint. Nodes before the checkpoint are not re-executed (results are already saved)." — Use time-travel
📖 "Replay re-executes nodes—it doesn't just read from cache." — Use time-travel
📖 "update_state does not roll back a thread. It creates a new checkpoint that branches from the specified point. The original execution history remains intact." — Use time-travel
🧪 实例 在 write_joke 前分叉、改 topic 后继续:
python
history = list(graph.get_state_history(config))
before_joke = next(s for s in history if s.next == ("write_joke",))

fork_config = graph.update_state(before_joke.config, values={"topic": "chickens"})
fork_result = graph.invoke(None, fork_config)   # write_joke 用新 topic 重跑
🔍 追问 什么时候必须显式传 as_node? → 并行分支多个节点同步更新致 LangGraph 无法判断谁最后(InvalidUpdateError)、fresh thread 无执行历史、或想跳节点(把 as_node 设成后面的节点让图以为它已跑过)。
📚 拓展阅读
  • Checkpoints — 时间旅行所依赖的 checkpoint 机制
  • Reducers — update_state 应用值时用到的 reducer
  • Test — 在 fresh thread 上用 as_node 搭建测试状态
Q时间旅行遇到 interrupt 和 subgraph 时行为如何?子图能否回放到内部两节点之间?深挖·拓展中频
time-travel interrupt subgraph super-step
⏱️ 现行
若图用 interrupt 做 HITL,时间旅行时中断总会被重新触发:含 interrupt 的节点重新执行,interrupt() 暂停并等待一个新的 Command(resume=...);这意味着 replay 或 fork 后到达该节点会再次停下,你可以用不同答案 resume 来探索分支。对多点收集输入(如多步表单),可以从两个 interrupt 之间 fork,只改后面的答案而不用重问前面的问题。子图的时间旅行粒度取决于子图有没有自己的 checkpointer:默认情况下子图继承父 checkpointer,父图把整个子图当作单个 super-step——整段子图执行只有一个父级 checkpoint,从子图之前做时间旅行会让整个子图从头重跑,你无法回放到子图内部两节点之间,只能从父层做时间旅行。若给子图设 checkpointer=True,它就有了自己的 checkpoint 历史,在子图内部每步都建 checkpoint,于是能从其中某个具体点(例如两个 interrupt 之间)做时间旅行——用 get_state(config, subgraphs=True) 拿到子图自己的 checkpoint config 再 fork。权衡是回放粒度 vs 开销:默认粗粒度、省 checkpoint;开子图 checkpointer 换来细粒度时间旅行。
术语 super-step(默认子图整段执行在父层只对应一个 checkpoint); interrupt 重触发(时间旅行时含中断的节点必重跑并再次暂停); checkpointer=True(给子图独立 checkpoint 历史以支持内部时间旅行); get_state(..., subgraphs=True)(取子图自身 checkpoint config)
📖 "The node containing the interrupt re-executes, and interrupt() pauses for a new Command(resume=...)." — Use time-travel
📖 "By default, a subgraph inherits the parent's checkpointer. The parent treats the entire subgraph as a single super-step — there is only one parent-level checkpoint for the whole subgraph execution. Time traveling from before the subgraph re-executes it from scratch." — Use time-travel
📖 "Set checkpointer=True on the subgraph to give it its own checkpoint history. This creates checkpoints at each step within the subgraph, allowing you to time travel from a specific point inside it — for example, between two interrupts." — Use time-travel
🧪 实例 从子图自身 checkpoint fork(需 checkpointer=True):
python
graph.invoke({"value": []}, config)            # 停在 step_a interrupt
graph.invoke(Command(resume="Alice"), config)  # 停在 step_b interrupt

parent_state = graph.get_state(config, subgraphs=True)
sub_config = parent_state.tasks[0].state.config
fork_config = graph.update_state(sub_config, {"value": ["forked"]})
result = graph.invoke(None, fork_config)   # step_b 重跑,step_a 结果保留
🔍 追问 多步表单里想只改"年龄"不重问"姓名"怎么做? → 从两个 interrupt 之间的 checkpoint(s.next == ("ask_age",))fork,ask_name 的结果被保留,ask_age 停在 interrupt 等待新答案。
📚 拓展阅读

第5章 执行模型·实战·工具

中频

Pregel 执行模型与容错

LangGraph runtime Fault tolerance
QLangGraph 的 Pregel runtime 是怎么组织执行的?一个 step 有哪几个阶段?深挖·拓展🔥高频
Pregel BSP 执行模型
⏱️ 现行
Pregel 是 LangGraph 的运行时,把 actors(即 PregelNode)和 channels 组合进同一个应用:actor 从 channel 读数据、往 channel 写数据。它按 Bulk Synchronous Parallel(Pregel 算法)模型把执行切成一个个 step,每个 step 有三个阶段——Plan(决定这一步跑哪些 actor:第一步选订阅 input channel 的 actor,后续步选订阅"上一步被更新的 channel"的 actor)、Execution(并行执行所有选中的 actor,直到全部完成、其一失败或超时;这一阶段内 channel 的写入对 actor 不可见)、Update(用本步 actor 写入的值更新 channel)。如此重复直到没有 actor 被选中,或达到最大步数。关键权衡在于:因为写入要等到下一步才可见(update 阶段统一提交),同一 step 内并行的 actor 看到的是一致的上一步快照,避免了竞态,这也是 BSP 的"超步"语义;编译 StateGraph 或创建 @entrypoint 都会产出一个可被 invoke 的 Pregel 实例。

flowchart LR
    plan[Plan: 选 actor] --> exec[Execution: 并行执行]
    exec --> update[Update: 提交 channel 写入]
    update -->|有 actor 被选中| plan
    update -->|无 actor / 达到最大步数| done([结束])
术语 actor/PregelNode(订阅并读写 channel 的执行单元,实现 Runnable 接口); channel(actor 间通信的载体,有 value type、update type 和 update function); Bulk Synchronous Parallel(超步并行模型); step(一个 plan/execution/update 循环)
📖 "Pregel organizes the execution of the application into multiple steps, following the Pregel Algorithm/Bulk Synchronous Parallel model." — LangGraph runtime
📖 "Execute all selected actors in parallel, until all complete, or one fails, or a timeout is reached. During this phase, channel updates are invisible to actors until the next step." — LangGraph runtime
🧪 实例 直接用 Pregel API 构造一个双节点管道,node1 订阅 ab,node2 订阅 bc,两个 step 依次触发:
python
from langgraph.channels import LastValue, EphemeralValue
from langgraph.pregel import Pregel, NodeBuilder

node1 = NodeBuilder().subscribe_only("a").do(lambda x: x + x).write_to("b")
node2 = NodeBuilder().subscribe_only("b").do(lambda x: x + x).write_to("c")

app = Pregel(
    nodes={"node1": node1, "node2": node2},
    channels={"a": EphemeralValue(str), "b": LastValue(str), "c": EphemeralValue(str)},
    input_channels=["a"],
    output_channels=["b", "c"],
)
app.invoke({"a": "foo"})  # {'b': 'foofoo', 'c': 'foofoofoofoo'}
🔍 追问 图里怎么形成一个 cycle(循环)? → 让一个 chain 写它自己订阅的 channel;执行会一直持续,直到往该 channel 写入一个 None 值为止。
📚 拓展阅读
QLangGraph 有哪几种 channel?各自适合什么场景?深挖·拓展🔥高频
channel reducer state
⏱️ 现行
Channel 用于 actor 之间通信,每个 channel 有 value type、update type 和一个把更新序列合并进存储值的 update function。核心类型有四种:LastValue 是默认类型,只保存最后一次写入、覆盖旧值,适合输入输出或把数据从一步传到下一步;Topic 是可配置的 PubSub channel,能在 actor 间发送多个值或跨步累积输出,可配置去重或用 accumulate=True 累积一次运行内的所有写入;BinaryOperatorAggregate 保存一个持久值,每次更新都用二元运算符把当前值和新更新合并,用来跨步计算 running aggregate;DeltaChannel(需 langgraph>=1.2,beta)只保存每步的增量 delta 而非完整累积值,专治那些"写得频繁且随时间累积很大"的 channel(如长线程里的消息列表)——没有 delta 存储时,每个 checkpoint 都要重新序列化整个列表,而 DeltaChannel 每步只存新写入的部分。权衡上,DeltaChannel 用 bulk reducer(一次收到当前 state 和本步全部写入的序列),要求 reducer 必须满足结合律(associative),否则批处理方式不同会导致重建状态不一致;而且它的 reducer 在"重建时"而非"写入时"运行,所以 reducer 必须是 (state, writes) 的纯函数,不能依赖副作用/随机数/uuid.uuid4() 这类每次重建都会重新执行的东西。
术语 LastValue(默认,覆盖式保存最后值); Topic(PubSub,可去重/累积); BinaryOperatorAggregate(写入时用二元算子聚合并落盘); DeltaChannel(只存增量,重建时跑 bulk reducer); snapshot_frequency(每 K 步写一次全量快照以限定读取深度)
📖 "LastValue is the default channel type. It stores the last value written to it, overwriting any previous value." — LangGraph runtime
📖 "Without delta storage, the full list is re-serialized into every checkpoint; with DeltaChannel, only the new messages written at each step are stored." — LangGraph runtime
📖 "The reducer runs on reconstruction, not on write." — LangGraph runtime
🧪 实例Annotated 给 state 字段挂一个 DeltaChannel,并用 snapshot_frequency 限定读取延迟:
python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.channels import DeltaChannel

class State(TypedDict):
    messages: Annotated[
        list[str],
        DeltaChannel(my_reducer, snapshot_frequency=5),
    ]
🔍 追问 为什么 snapshot_frequency 值越大越省存储却越慢? → 没有快照时读 DeltaChannel 要重放全部写历史,对 N 步线程是 O(N);snapshot_frequency=K 每 K 步写一次全量快照,把读取深度限制在至多 K 步。值越高快照越少(省存储)但重放越深(读延迟高),值越低则相反。
📚 拓展阅读
QLangGraph 的 retry policy 默认在什么异常上重试?怎么定制?深挖·拓展🔥高频
RetryPolicy 容错 backoff
⏱️ 现行
retry policy 会根据异常类型和 backoff 设置自动重跑失败的节点尝试,通过 add_noderetry_policy= 传入。默认 retry_ondefault_retry_on,策略是"除下列异常(及其子类)外,对任何异常都重试"——排除的包括 ValueErrorTypeErrorArithmeticErrorImportErrorLookupErrorNameErrorSyntaxErrorRuntimeError 等编程性错误;对 requests/httpx 这类 HTTP 库,只在 5xx 状态码上重试;NodeTimeoutError 默认可重试。参数上 max_attempts 默认 3(含首次)、initial_interval 默认 0.5 秒、backoff_factor 默认 2.0(每次重试后乘到间隔上)、max_interval 默认 128 秒、jitter 默认 True(加随机抖动避免惊群)。设计取舍在于:默认策略故意不重试编程错误(ValueError/TypeError 等)因为重试它们没意义,只对疑似瞬态的失败(网络、5xx、超时)重试;要定制可给 retry_on 传一个可调用对象或异常类型,并可 import default_retry_on 在其基础上扩展。此外节点内可通过 runtime.execution_info.node_attempt(1-indexed)读当前尝试次数,便于在主调用反复失败时切到 fallback。
术语 RetryPolicy(重试策略对象); default_retry_on(默认重试判定函数); max_attempts(含首次的最大尝试数,默认 3); backoff_factor(间隔倍增因子,默认 2.0); jitter(随机抖动); execution_info.node_attempt(当前尝试序号)
📖 "By default, retry_on uses default_retry_on, which retries on any exception except the following (and their subclasses):" — Fault tolerance
📖 "For exceptions from popular HTTP libraries such as requests and httpx, it only retries on 5xx status codes." — Fault tolerance
🧪 实例node_attempt 在重试后切换到 fallback API:
python
def my_node(state: State, runtime: Runtime) -> State:
    if runtime.execution_info.node_attempt > 1:
        return {"result": call_fallback_api()}
    return {"result": call_primary_api()}

builder.add_node("my_node", my_node, retry_policy=RetryPolicy(max_attempts=3))
🔍 追问 没有配置 retry policy 时还能读到 execution_info 吗? → 能,execution_info 即使没有 retry policy 也可用,此时 node_attempt 默认为 1。
📚 拓展阅读
QLangGraph 的 node timeout 有哪两种?为什么只对 async 节点生效?深挖·拓展中频
TimeoutPolicy 超时 async
⏱️ 现行(需 langgraph>=1.2)
add_nodetimeout= 限制单次节点尝试能跑多久,可传秒数、timedeltaTimeoutPolicyTimeoutPolicy 区分两种上限:run_timeout 是对单次尝试的硬性墙钟上限,无论节点是否活跃都永不刷新,超限就抛 NodeTimeoutError;idle_timeout 是"重置进度"式上限,只在节点连续一段时间没有可观测进度时才触发——默认 refresh_on="auto" 下,状态写入、stream 输出、子任务调度、runtime stream-writer 调用、以及节点或其后代产生的任何 LangChain callback 事件(LLM token、工具调用等)都会重置 idle 时钟。两者可同时设,谁先触发谁取消尝试。关键限制是超时只对 async 节点生效:带 timeout 的 sync 节点在 compile 时就被拒绝,因为同步阻塞代码无法被协作式取消,要包阻塞 I/O 需在 async 节点里用 asyncio.to_thread。超时与 retry 天然组合:NodeTimeoutError 默认可重试,每次新尝试超时时钟重置,且超时尝试的写入在下次重试前会被清除,避免脏写。对于不自然产生进度信号的长任务,可用 refresh_on="heartbeat" 把刷新源收窄到显式的 runtime.heartbeat() 调用(该调用在非 idle-timed 尝试外是 no-op)。
术语 run_timeout(永不刷新的硬墙钟上限); idle_timeout(无进度才触发的可重置上限); refresh_on("auto" 广义进度信号 / "heartbeat" 仅显式心跳); NodeTimeoutError(超时异常,带 node/elapsed/kind 等结构化字段); runtime.heartbeat()(手动重置 idle 时钟)
📖 "Node timeouts only apply to async nodes. Sync nodes with a timeout are rejected at compile time. To wrap blocking I/O, use asyncio.to_thread inside an async node." — Fault tolerance
📖 "run_timeout is a hard wall-clock cap on a single attempt. It is never refreshed, regardless of node activity:" — Fault tolerance
🧪 实例 在长任务里手动打心跳配合 heartbeat 模式:
python
async def long_running_node(state: State, runtime: Runtime) -> State:
    for batch in fetch_batches():
        process(batch)
        runtime.heartbeat()
    return {"result": "done"}

builder.add_node(
    "long_running_node",
    long_running_node,
    timeout=TimeoutPolicy(idle_timeout=30, refresh_on="heartbeat"),
)
🔍 追问 map-reduce 里想给某次 push 单独设更紧的超时怎么办? → 用 Send 时可直接在 Send 上传 timeout= 覆盖目标节点的静态超时;省略则沿用 add_node 时设的节点超时,从而"节点设默认、单次调用收紧"。
📚 拓展阅读
Qretry 都用尽后还想优雅恢复怎么办?error_handler 和 Command 如何实现 Saga 补偿?深挖·拓展中频
error_handler Command Saga
⏱️ 现行(需 langgraph>=1.2)
这三种机制按固定顺序组合:节点尝试抛出任何异常(包括超时的 NodeTimeoutError)时,先由 retry policy 决定是否重试;只有重试用尽后 error handler 才运行。error handler 通过 add_nodeerror_handler= 注册,收到当前 state,可更新 state 或用 Command 路由到别的节点——这正适合补偿流(Saga 模式):与其中止整张图,不如优雅恢复。retry policy 和 error handler 保持解耦:何时重试、何时补偿可独立配置;handler 只在 retry 用尽后触发,若没配 retry policy 则失败即触发。handler 可通过类型注解 error: NodeError 注入失败上下文(和 runtime: Runtime 同一模式),NodeError 是有 nodeerror 两字段的 frozen dataclass,该参数是 opt-in 的。要注意几个边界:节点里 interrupt() 抛出的中断不会走 error handler,它用 GraphBubbleUp 机制暂停执行做 human-in-the-loop,绕过 retry 和 error handler;失败溯源会被 checkpoint,即使进程在 handler 完成前崩溃,resume 时 handler 仍看到同样的 NodeError;handler 自身抛异常则像节点没有 handler 一样向上冒泡;每个节点至多一个 error handler。此外用 set_node_defaults() 可一处配置图级默认(retry_policy/error_handler/timeout/cache_policy),per-node 值总是覆盖默认,但默认不被 subgraph 继承。
术语 error_handler(retry 用尽后的恢复函数); NodeError(含 node/error 的 frozen dataclass); Command(更新 state 并 goto 路由); Saga/compensation(补偿而非中止的模式); set_node_defaults(图级默认,不被 subgraph 继承); interrupt()(经 GraphBubbleUp 暂停,不进 handler)
📖 "Only after retries are exhausted does the error handler run." — Fault tolerance
📖 "interrupt() raised inside a node is not routed to the error handler." — Fault tolerance
🧪 实例 支付节点重试 ConnectionError 用尽后,补偿并路由到 finalize 而非中止全图:
python
def payment_error_handler(state: State, error: NodeError) -> Command:
    return Command(
        update={"status": f"compensated_after_{error.node}: {error.error}"},
        goto="finalize",
    )

graph = (
    StateGraph(State)
    .add_node(
        "charge_payment",
        charge_payment,
        retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError),
        error_handler=payment_error_handler,
    )
    .add_node("finalize", finalize)
    .compile()
)
🔍 追问 想在超步边界优雅停机、之后再续跑怎么做? → 用 RunControl 作为 control= 传给 invoke/stream,任意线程调 request_drain() 发信号;drain 是协作式的、在超步之间生效,不抢占正在跑的节点,若还有超步会抛 GraphDrained 并保存可 resume 的 checkpoint,之后用同一 thread_idinvoke(None, config) 续跑。
📚 拓展阅读
Q什么是 Agentic RAG?它相较于"固定管道 RAG"多了哪一步决策,LangGraph 里这条图是怎么运转的?深挖·拓展🔥高频
agentic-rag langgraph retrieval
⏱️ 现行
Agentic RAG 的核心不是"每次都检索",而是把"要不要检索"交给 LLM 自己决策——检索被包成一个工具,模型通过 .bind_tools([retriever_tool]) 拿到它,在 generate_query_or_respond 节点里自行判断是直接回答用户,还是发出一次工具调用去向量库取上下文。这样做的权衡在于:纯管道 RAG 对每个问题都强制检索,面对"hello!"这类闲聊会浪费一次向量检索且可能污染上下文;而 agentic 方式把这个开关下放给模型,代价是多一轮 LLM 推理和不确定性。更关键的是它引入了自我纠错的闭环:检索回来的文档先经过 grade_documents 这个条件边打分(用结构化输出 GradeDocuments 给出 yes/no),相关就进入 generate_answer,不相关则说明原始问题表述不好,走 rewrite_question 改写后再回到 generate_query_or_respond 重新决策。因此整张图是一个可循环的状态机,而非一条直线。
术语 agentic RAG(让模型自主决定是否检索的检索代理); bind_tools(把检索器作为工具绑定给 chat model); grade_documents(判定检索文档相关性的条件边); GradeDocuments(binary_score 的结构化输出 schema); rewrite_question(文档不相关时改写原问题的节点); MessagesState(带 messages 键的图状态)
📖 "agents are useful when you want an LLM to make a decision about whether to retrieve context from a vectorstore or respond to the user directly." — Build a custom RAG agent with LangGraph
📖 "The retriever tool can return potentially irrelevant documents, which indicates a need to improve the original user question." — Build a custom RAG agent with LangGraph
🧪 实例 图的装配核心是两条条件边:一条决定"检索还是直接回答",另一条(grade_documents)决定"回答还是改写重试"。
flowchart TD
    START --> Q[generate_query_or_respond]
    Q -->|有 tool_calls| R[retrieve]
    Q -->|无 tool_calls| END1[END]
    R --> G{grade_documents}
    G -->|relevant| A[generate_answer]
    G -->|not relevant| W[rewrite_question]
    W --> Q
    A --> END2[END]
🔍 追问 为什么 grade_documents 要用 with_structured_output(GradeDocuments) 而不是让模型自由回答? → 因为条件边需要一个确定的、可分支的信号(yes/no)来路由到 generate_answerrewrite_question,结构化输出把"相关性判断"约束成一个 binary_score 字段,避免解析自由文本的脆弱性。
📚 拓展阅读
Q已经有 prebuilt SQL agent,为什么还要在 LangGraph 里手写一个自定义 SQL agent?自定义带来了什么控制力?深挖·拓展🔥高频
sql-agent langgraph tool-calling
⏱️ 现行
prebuilt agent 上手快,但它只能靠 system prompt 去"约束"行为——比如叮嘱模型总是先调 list-tables、执行前先跑 query-checker,这是软约束,模型可能不听。自定义的价值在于把这些步骤拆成图里的专用节点(list tables、get schema、generate query、check query),从而做到硬约束:一是可以在需要时强制工具调用(如 call_get_schemabind_tools(..., tool_choice="any") 强制模型必须调 schema 工具),二是可以为每一步定制各自的 prompt。这就是"用结构换控制"的权衡:prebuilt 灵活省事但不可控,自定义节点多、代码多,但每一步的行为、顺序和提示都由图的边显式钉死。整体仍是一个 ReAct 风格的循环——generate_query 后用条件边 should_continue 判断:若模型给出了 tool_calls 就去执行(或先 check),否则说明模型已经产出最终答案,直接结束。
术语 prebuilt agent(高层封装、靠 system prompt 约束的现成 agent); dedicated nodes(把每步工具调用拆成专用图节点); tool_choice="any"(强制模型必须发出工具调用); should_continue(依据有无 tool_calls 路由的条件边); ReAct(推理-行动交替的 agent 结构)
📖 "We can enforce a higher degree of control in LangGraph by customizing the agent." — Build a custom SQL agent
📖 "Putting these steps in dedicated nodes lets us (1) force tool-calls when needed, and (2) customize the prompts associated with each step." — Build a custom SQL agent
📖 "lets us get started quickly, but we relied on the system prompt to constrain its behavior" — Build a custom SQL agent
🧪 实例 自定义 SQL agent 的图把"列表→取 schema→生成查询→检查→执行"钉成显式流水线,且 run_query 后回到 generate_query 形成纠错循环。
flowchart TD
    START --> LT[list_tables]
    LT --> CGS[call_get_schema]
    CGS --> GS[get_schema]
    GS --> GQ[generate_query]
    GQ -->|有 tool_calls| CQ[check_query]
    GQ -->|无 tool_calls| END[END]
    CQ --> RQ[run_query]
    RQ --> GQ
🔍 追问 generate_query 节点为什么故意不强制 tool_choice,而 call_get_schema/check_query 却强制? → 因为取 schema 和查前检查是必须发生的固定动作,适合强制;而生成查询这一步既可能要再发查询,也可能已经拿到结果要"自然地"给出最终答复,强制工具调用会让它永远无法结束(源码注释:"to allow the model to respond naturally when it obtains the solution")。
📚 拓展阅读
Q让 LLM 执行自己生成的 SQL 有什么风险?文档建议怎么把风险压到最低?深挖·拓展🔥高频
security sql-agent least-privilege
⏱️ 现行
只要系统要执行"模型生成的 SQL",就存在固有风险——模型可能被诱导写出破坏性或越权的查询。文档的核心防线是最小权限原则:把数据库连接的权限尽可能收窄到 agent 真正需要的范围,这能缓解但不能消除风险。教程里给出的数据库工具明确声明是"仅供演示的最小包装,不用于生产、也不保证安全",生产中应当在执行模型生成的 SQL 之前叠加应用层校验。代码层面还有几处纵深防御:生成查询的 system prompt 明令"不得做任何 DML(INSERT/UPDATE/DELETE/DROP 等)"、默认 LIMIT 结果条数、只取相关列;sql_db_schema 工具会先校验表名是否真实存在再查。这些都是"约束模型自由度 + 数据库侧收权 + 应用侧校验"三层叠加,而不是依赖单点。
术语 least privilege(数据库连接权限收窄到最小); DML 禁止(prompt 层禁止 INSERT/UPDATE/DELETE/DROP); application-specific validation(执行前的应用层校验); narrowly scoped permissions(窄作用域权限)
📖 "Make sure that your database connection permissions are always scoped as narrowly as possible for your agent's needs." — Build a custom SQL agent
📖 "This will mitigate, though not eliminate, the risks of building a model-driven system." — Build a custom SQL agent
📖 "Use narrowly scoped database permissions and add application-specific validation before executing model-generated SQL." — Build a custom SQL agent
🧪 实例 生成查询节点用 prompt 在模型侧就堵死写操作,这是纵深防御的第一层:
python
generate_query_system_prompt = """
You are an agent designed to interact with a SQL database.
...
DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.
"""
🔍 追问 prompt 里写了"不许 DML"就足够安全吗? → 不够。prompt 是软约束、可被绕过,所以文档强调它只能"缓解而非消除"风险,真正的兜底是数据库侧的窄权限(比如只读账号)加执行前的应用层校验。
📚 拓展阅读
Q如何在 SQL agent 执行查询前插入人工审核(human-in-the-loop)?为什么必须配 checkpointer?深挖·拓展中频
human-in-the-loop interrupt checkpointer
⏱️ 现行
思路是把 sql_db_query 工具包进一个会调用 interrupt 的节点:执行真正的查询前,先把待执行的 action 和参数抛出一个 interrupt 请求,暂停整张图等待人来处理,人可以选择 approve(接受并执行)、edit(改写参数后执行)或 response(用反馈直接回给 LLM)。之所以能"无限期暂停再恢复",依赖 LangGraph 的持久化层——恢复运行时必须带上 checkpointer,否则图无法在中断点保存状态并续跑。恢复的方式是用 Command(resume={"type": "accept"})(或 edit 带新 args)把人的决定送回中断点。这套机制的权衡是:牺牲了全自动的吞吐,换来在"执行破坏性/低效 SQL"这类高风险动作前的一道人工闸门,和前面 prompt/权限的纵深防御互补。
术语 interrupt(在工具内暂停图并请求人工输入的函数); accept/edit/response(三种人工响应类型); Command(resume=...)(把人工决定送回并恢复运行); checkpointer/InMemorySaver(暂停与恢复所需的持久化层)
📖 "It can be prudent to check the agent's SQL queries before they are executed for any unintended actions or inefficiencies." — Build a custom SQL agent
📖 "Below, we allow for input to approve the tool call, edit its arguments, or provide user feedback." — Build a custom SQL agent
📖 "this is required to pause and resume the run." — Build a custom SQL agent
🧪 实例 中断被触发时,stream 会带回待审核的工具调用请求,人工审核后再 resume:
python
stream = agent.stream_events(
    Command(resume={"type": "accept"}),
    # Command(resume={"type": "edit", "args": {"query": "..."}}),
    config,
    version="v3",
)
🔍 追问 interrupt 版工具节点替换掉了原来的 check_query 程序化检查,这说明什么? → 说明"程序化 query-checker"和"人工审核"是两种可替换的把关方式:前者用一个 SQL 专家 prompt 自动纠错,后者把最终裁量权交给人;重装图时用人工 review 直接取代了 check_query 节点。
📚 拓展阅读
Q这些 Agentic RAG / SQL agent 模式在真实公司里落地成了什么?案例页能说明什么趋势?深挖·拓展中频
case-studies text-to-sql production
⏱️ 现行
案例页是一份从公开来源整理的 LangGraph 生产落地清单,覆盖金融(BlackRock、J.P. Morgan、Klarna)、软件与技术(Cisco、GitLab、Replit、Uber、Qodo)、医疗、电商、政府等行业,用途归类为 copilot、customer support、code generation、search、data extraction、research & summarization 等。与本章 SQL/检索主题最直接对应的是 LinkedIn 的 "practical text-to-SQL for data analytics"——正是把自然语言问题翻译成 SQL 查询这一模式在真实数据分析场景的落地,和自定义 SQL agent 教程是同一类问题。它给面试的启示是:agentic 检索与 text-to-SQL 不是玩具 demo,而是被大厂用在客服、代码生成、数据分析等核心链路上的成熟范式;清单本身是社区共建、依据公开博客/发布信息补充的。
术语 text-to-SQL(自然语言到 SQL 查询,LinkedIn 数据分析用例); copilot(领域特定任务的助手,案例中最高频用途); code generation(代码生成,GitLab/Replit/Uber 等采用); public sources(案例清单基于公开来源整理)
📖 "This list of companies using LangGraph and their success stories is compiled from public sources." — Case studies
🧪 实例 案例表中 LinkedIn 一行同时标注了 "Code generation; Search & discovery",并引用其工程博客里的实践文章,把 text-to-SQL 落到数据分析场景。
text
LinkedIn | Social Media | Code generation; Search & discovery
→ Blog: practical-text-to-sql-for-data-analytics
🔍 追问 为什么案例页强调是"public sources"而非官方背书? → 因为这份清单是社区/公开信息汇编,任何用 LangGraph 的公司都可基于公开博客、新闻稿贡献或更新条目,它反映采用面而非经过审计的性能承诺。
📚 拓展阅读
Q什么是 LangSmith Studio?它如何连接到本地运行的 agent 并帮助调试?深挖·拓展中频
Studio 本地开发 调试
⏱️ 现行
LangSmith Studio 是一个免费的可视化界面,用于在本地机器上开发和测试 LangChain agent。它的核心机制是连接到你本地正在运行的 agent,把 agent 执行的每一步都展示出来——发送给模型的 prompt、tool call 及其返回结果、以及最终输出;你可以在无需额外代码或部署的情况下测试不同输入、检查中间状态、迭代 agent 行为。连接方式上,你需要先用 LangGraph CLI 启动本地开发服务器(langgraph dev),Studio 通过一个带 baseUrl 查询参数的 URL 指向本地服务器(如 http://127.0.0.1:2024)来建立连接;这也意味着必须提供 LangSmith API key 才能连上。权衡点在于:开发期把追踪打到 LangSmith 能拿到完整执行 trace、异常上下文和 token/延迟指标,但如果不希望数据外流,可以在 .env 里设置 LANGSMITH_TRACING=false,禁用后没有数据离开本地服务器。开发服务器还支持热重载,改动 prompt 或 tool 签名后 Studio 会立即反映,便于快速迭代。
术语 LangSmith Studio(本地 agent 可视化调试界面); baseUrl(Studio URL 中指向本地服务器地址的查询参数); hot-reloading(热重载,代码改动即时生效); LANGSMITH_TRACING(控制是否把追踪数据发往 LangSmith 的开关)
📖 "LangSmith Studio is a free visual interface for developing and testing your LangChain agents from your local machine." — LangSmith Studio
📖 "The development server supports hot-reloading—make changes to prompts or tool signatures in your code, and Studio reflects them immediately." — LangSmith Studio
🧪 实例 启动后 agent 同时可通过 API 和 Studio UI 访问:
bash
langgraph dev

访问地址形如 https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024。如果用 Safari,localhost 连接会被拦截,需要加 --tunnel 通过安全隧道访问。
🔍 追问 不想把数据发到 LangSmith 怎么办? → 在应用的 .env 文件中设置 LANGSMITH_TRACING=false,禁用追踪后没有数据离开本地服务器。
📚 拓展阅读
Q如何在本地跑起一个 LangGraph 应用?langgraph dev 启动的是什么模式,为什么不能直接用于生产?深挖·拓展中频
local-server langgraph-cli in-memory
⏱️ 现行
本地跑一个 LangGraph 应用的标准流程是:先安装 LangGraph CLI(pip install -U "langgraph-cli[inmem]",要求 Python >= 3.11),用 langgraph new 从模板创建应用,以 editable 模式安装依赖(pip install -e .)让本地改动被服务器采用,再在项目根建 .env 填入 LANGSMITH_API_KEY,最后 langgraph dev 启动。这条命令启动的是 in-memory 模式的 Agent Server,机制上把状态放在内存里,因此适合开发和测试;但正因为没有持久化存储,它不适合生产——生产环境需要部署一个能访问持久化存储后端的 Agent Server。启动后服务器同时暴露三个入口:API(http://127.0.0.1:2024)、Studio UI 和 API Docs(http://127.0.0.1:2024/docs)。权衡上,in-memory 模式的好处是零基础设施、启动即用、便于快速迭代,代价是重启即丢状态、无法承载有状态长时运行的生产负载。
术语 langgraph-cli[inmem](带内存后端的 CLI 安装项); in-memory mode(状态存内存的开发测试模式); editable install(pip install -e .,本地改动即时被采用); Agent Server(暴露 API 供调用的 LangGraph 服务器)
📖 "The langgraph dev command starts Agent Server in an in-memory mode. This mode is suitable for development and testing purposes. For production use, deploy Agent Server with access to a persistent storage backend." — Run a local server
🧪 实例 用 SDK 向本地 assistant 发一条 threadless run:
python
from langgraph_sdk import get_sync_client

client = get_sync_client(url="http://localhost:2024")

for chunk in client.runs.stream(
    None,  # Threadless run
    "agent", # Name of assistant. Defined in langgraph.json.
    input={
        "messages": [{
            "role": "human",
            "content": "What is LangGraph?",
        }],
    },
    stream_mode="messages-tuple",
):
    print(f"Receiving new event of type: {chunk.event}...")
    print(chunk.data)
    print("\n\n")
🔍 追问 为什么依赖要用 editable 模式安装? → 文档明确是为了让本地改动能被服务器采用("so your local changes are used by the server"),配合热重载迭代。
📚 拓展阅读
Q如何把 LangGraph agent 部署到 LangSmith Cloud?流程的关键前提和步骤是什么?深挖·拓展中频
部署 LangSmith-Cloud GitHub
⏱️ 现行
LangGraph agent 部署到生产时,可以选择适配自身技术栈的托管模型,其中 LangSmith Cloud 提供全托管的基础设施,面向有状态、长时运行的 agent,支持持久化状态和后台执行。部署到 Cloud 的流程走 GitHub 仓库:机制上要求应用代码必须放在 GitHub 仓库(公有私有都支持),且这个应用得先是 LangGraph 兼容的(按本地服务器搭建指南跑通),然后把代码推上去。之后在 LangSmith 左侧栏进入 Deployments,点 + New Deployment,首次或加私有仓库时需连接 GitHub 账户,选中仓库点 Submit 部署——这一步大约需要 15 分钟完成,可在 Deployment details 视图查看状态。部署好后可点右上角 Studio 按钮打开图查看,并从 Deployment details 里复制 API URL 来调用。核心价值/权衡:LangSmith 接管基础设施、扩缩容和运维事项,换取的是把代码托管到 GitHub 并接受托管平台约束;文档也提到 Cloud 之外还有 hybrid、standalone servers、self-hosted with control plane 等多种部署选项。
术语 LangSmith Cloud(全托管部署基础设施); stateful, long-running agents(有状态、长时运行的 agent); Deployment details(查看部署状态与 API URL 的视图); API URL(部署后用于调用的地址)
📖 "LangSmith Cloud provides fully managed infrastructure for stateful, long-running agents with persistent state and background execution." — Deployment
📖 "Select your application's repository. Click Submit to deploy. This may take about 15 minutes to complete. You can check the status in the Deployment details view." — Deployment
🧪 实例 部署后用 SDK 带上部署 URL 和 API key 调用:
python
from langgraph_sdk import get_sync_client # or get_client for async

client = get_sync_client(url="your-deployment-url", api_key="your-langsmith-api-key")

for chunk in client.runs.stream(
    None,    # Threadless run
    "agent", # Name of agent. Defined in langgraph.json.
    input={
        "messages": [{
            "role": "human",
            "content": "What is LangGraph?",
        }],
    },
    stream_mode="updates",
):
    print(f"Receiving new event of type: {chunk.event}...")
    print(chunk.data)
    print("\n\n")
🔍 追问 部署前对代码有什么硬性要求? → 代码必须位于 GitHub 仓库(公私皆可),且应用要先是 LangGraph 兼容的,按本地服务器搭建指南跑通后再推送。
📚 拓展阅读
QLangSmith 的 trace 是什么?如何为应用开启追踪、并把 trace 打到指定 project?深挖·拓展中频
可观测性 tracing LangSmith
⏱️ 现行
trace(追踪)是应用从输入走到输出所经历的一系列步骤,每个单独步骤由一个 run 表示;用 LangSmith 可以把这些执行步骤可视化,从而调试本地运行的应用、评估应用性能、以及监控应用。开启追踪的机制是设置环境变量 LANGSMITH_TRACING=trueLANGSMITH_API_KEY。默认情况下 trace 会被记录到名为 default 的 project;要自定义 project 名,可以静态地设 LANGSMITH_PROJECT 环境变量,或动态地用 ls.tracing_context(project_name=...) 针对特定操作设置。除了全局开关,还能选择性追踪——用 tracing_context context manager 只追踪特定调用或应用的一部分:在 with ls.tracing_context(enabled=True): 块内的调用会被追踪,块外(且未设 LANGSMITH_TRACING)的调用则不会。权衡上,全量追踪给出完整可观测性但把所有输入输出都发往 LangSmith;选择性追踪 + 自定义 project 让你按调用粒度控制哪些数据被记录、以及归入哪个 project,便于隔离测试与生产数据。此外还可以给 trace 附加自定义 metadata 和 tags 做细粒度归类。
术语 trace(从输入到输出的一系列步骤); run(trace 中的单个步骤); tracing_context(按调用粒度控制追踪的 context manager); LANGSMITH_PROJECT(指定 trace 归属 project 的环境变量)
📖 "Traces are a series of steps that your application takes to go from input to output. Each of these individual steps is represented by a run." — LangSmith Observability
📖 "By default, the trace will be logged to the project with the name default. To configure a custom project name, see [Log to a project](#log-to-a-project)." — LangSmith Observability
🧪 实例 只追踪特定调用,并动态指定 project:
python
import langsmith as ls

with ls.tracing_context(project_name="email-agent-test", enabled=True):
    response = agent.invoke({
        "messages": [{"role": "user", "content": "Send a welcome email"}]
    })
🔍 追问 如何给某次调用打上标签和元数据? → 在 agent.invokeconfig 里传 tags(如 ["production", "email-assistant", "v1.0"])和 metadata(如 user_idsession_idenvironment),它们会被附加到 LangSmith 中的 trace 上。
📚 拓展阅读
Q追踪时如何防止敏感数据被记录到 LangSmith?anonymizer 的机制是什么?深挖·拓展低频
可观测性 数据脱敏 anonymizer
⏱️ 现行
当你希望屏蔽敏感数据、避免它被记录到 LangSmith 时,可以创建 anonymizer(匿名器)并通过配置应用到你的图上。机制是:用 create_anonymizer 定义一组基于正则的规则(每条规则给出匹配 pattern 和替换文本 replace),把它传给 Client(anonymizer=...) 构造一个带匿名器的 tracer client,再用这个 client 建 LangChainTracer,最后通过 .with_config({'callbacks': [tracer]}) 挂到编译后的图上。这样匹配到规则的内容会在发往 LangSmith 之前被替换掉——文档给的例子就是把符合社保号(SSN)格式 XXX-XX-XXXX 的内容从发往 LangSmith 的 trace 中脱敏。权衡上,这是一种基于规则的输入/输出屏蔽:好处是保留了追踪带来的可观测性,同时把 PII 之类敏感字段挡在记录之外;代价是它依赖正则规则的覆盖面,规则没覆盖到的敏感模式仍可能被记录,需要针对性地设计 pattern。
术语 anonymizer(基于规则屏蔽敏感数据的匿名器); create_anonymizer(用正则规则构造匿名器的函数); pattern / replace(规则中的匹配正则与替换文本); LangChainTracer(挂载匿名 client 的追踪器)
📖 "You may want to mask sensitive data to prevent it from being logged to LangSmith." — LangSmith Observability
📖 "This example will redact anything matching the Social Security Number format XXX-XX-XXXX from traces sent to LangSmith." — LangSmith Observability
🧪 实例 用正则规则对 SSN 脱敏,并挂到图上:
python
from langchain_core.tracers.langchain import LangChainTracer
from langgraph.graph import StateGraph, MessagesState
from langsmith import Client
from langsmith.anonymizer import create_anonymizer

anonymizer = create_anonymizer([
    # Matches SSNs
    { "pattern": r"\b\d{3}-?\d{2}-?\d{4}\b", "replace": "<ssn>" }
])

tracer_client = Client(anonymizer=anonymizer)
tracer = LangChainTracer(client=tracer_client)
# Define the graph
graph = (
    StateGraph(MessagesState)
    ...
    .compile()
    .with_config({'callbacks': [tracer]})
)
🔍 追问 anonymizer 是怎么挂到图上生效的? → 通过 create_anonymizer 建规则 → 传入 Client → 用该 client 建 LangChainTracer → 再用 .with_config({'callbacks': [tracer]}) 把 tracer 作为 callback 配置到编译后的图上。
📚 拓展阅读

第三部分 · LangSmith

第1章 可观测

🔥高频

可观测概念与上手

Observability concepts Tracing quickstart
QLangSmith 是如何组织可观测数据的?project、trace、run、thread 之间是什么关系?深挖·拓展🔥高频
LangSmith 数据模型 Trace
⏱️ 现行
LangSmith 的数据模型是一个自上而下的层级容器结构,用来把一次次 LLM 调用的原始步骤组织成可检索、可分析的单位。最外层是 *project*,它是"单个应用或服务"相关全部 trace 的容器;一个 *trace* 记录应用为单次操作(single operation)所执行的完整步骤序列,而这个序列又由一个个 *run* 构成——每个 run 就是一步离散工作(如一次 LLM 调用、一次 prompt 格式化、一次检索)。当应用是多轮对话时,多个 trace 又可以通过共享标识被串成一个 *thread*。这样分层的好处是:project 提供了按应用维度隔离与聚合的边界,trace 把一次请求内散落的调用绑定成一个可追溯整体(靠唯一 trace ID 绑定),run 提供了最细粒度的 span 供调试,thread 则跨请求还原对话上下文。权衡点在于 trace 是有硬上限的(单 trace 最多 25,000 runs),所以设计追踪粒度时不能把一次超长循环无限塞进同一个 trace。

graph TB
    Project -->|contains| Trace1[Trace]
    Project -->|contains| Trace2[Trace]
    Trace1 -->|contains| Run1[Run]
    Trace1 -->|contains| Run2[Run]
    Trace1 -->|part of| Thread
    Trace2 -->|part of| Thread
术语 project(项目,单个应用/服务全部 trace 的容器); trace(追踪,单次操作的 run 集合); run(运行,一个工作单元的 span); thread(线程,代表一段对话的一串 trace); trace ID(把 run 绑定到 trace 的唯一标识)
📖 "LangSmith groups multiple [*traces*](#traces) within a [*project*](#projects). Each trace records the sequence of steps your application takes for a single operation, which are made up of individual [*runs*](#runs)." — Observability concepts
📖 "A *project* is a container for all the traces related to a single application or service." — Observability concepts
🧪 实例 一个用户请求触发了一条 chain,它先调用 LLM,再调用 output parser。这些调用都属于同一个 trace,被同一个 trace ID 绑在一起,落在同一个 project 下面,方便在 UI 里作为一个整体查看。
🔍 追问 单个 trace 能包含无限多 run 吗? → 不能,每个 trace 最多 25,000 个 run,超过后 LangSmith 会拒收该 trace 后续发来的 run。
📚 拓展阅读
Qtrace 和 run 分别是什么?为什么文档反复拿 OpenTelemetry 的 span 来类比?深挖·拓展🔥高频
Trace Run Span OpenTelemetry
⏱️ 现行
trace 是"单次操作的一组 run",run 则是应用里一个工作单元的 span——可以是一次 LLM 调用、一次 prompt 格式化步骤、一次检索调用或任何其他离散操作。文档之所以反复用 OpenTelemetry 类比,是因为 LangSmith 的追踪模型本质上就是通用可观测领域的 tracing/span 模型在 LLM 场景下的落地:一个 LangSmith trace 相当于一组 spans,一个 run 就相当于一个 span。这个类比对面试很关键,因为它说明 LangSmith 不是一套全新概念,而是把成熟的分布式追踪范式套用到 LLM pipeline 上,让熟悉 OTel 的工程师能零成本迁移心智模型。权衡在于 trace 有 25,000 run 的硬上限:一旦到顶,LangSmith 会直接拒绝为该 trace 发送的额外 run,因此对于长循环 agent,需要在追踪结构上做拆分而不是无限累积到同一 trace。
术语 trace(单次操作的 run 集合,≈ 一组 span); run(单个工作单元的 span); span(OpenTelemetry 中的追踪基本单元); run limit(单 trace 上限 25,000)
📖 "A *trace* is a collection of runs for a single operation." — Observability concepts
📖 "A *run* is a span representing a single unit of work within your LLM application: a call to an LLM, a prompt formatting step, a retrieval call, or any other discrete operation." — Observability concepts
📖 "Each trace is limited to a maximum of 25,000 runs. Once the trace reaches this limit, LangSmith will reject any additional runs that you send for that trace." — Observability concepts
🧪 实例 在 quickstart 里,assistant 函数是外层 run(span),它内部嵌套了 get_context 工具 run 和一次 OpenAI 调用 run,三者共同组成一个 trace 的 run tree。
🔍 追问 如果我熟悉 OpenTelemetry,该怎么快速理解 run? → 直接把它当成一个 span 即可,文档明确说 "you can think of a run as a span"。
📚 拓展阅读
Q向 LangSmith 发送 trace 有哪几种方式?integrations 和 manual instrumentation 该怎么选?深挖·拓展🔥高频
发送Trace Instrumentation 集成
⏱️ 现行
LangSmith 提供两条发送 trace 的路径。第一条是 *integrations*,为常见 LLM provider 和 agent 框架提供自动追踪,相当于通用可观测里的 auto-instrumentation——当你用 LangChain、LangGraph、OpenAI、Anthropic 或 CrewAI 等受支持框架时,集成会自动捕获输入、输出和 metadata,不需要改动代码。第二条是 *manual instrumentation*,让你给任意代码加追踪,无论用什么框架;当你没有用受支持的集成、或者需要对追什么做细粒度控制时用它。手动方式提供三种机制:@traceable/traceable 装饰器(追踪任意函数)、trace context manager(Python,用于包裹特定代码块)、以及 RunTree API(底层、显式构造 trace)。选择逻辑是:能用集成就优先用集成,因为零代码改动、维护成本低;当框架不受支持或需要精细控制追踪范围时,退回到手动埋点,其中装饰器最轻量、context manager 适合局部代码块、RunTree 给最大控制权但也最繁琐。
术语 integrations(集成,自动追踪 ≈ auto-instrumentation); manual instrumentation(手动埋点); @traceable/traceable(追踪任意函数的装饰器); trace context manager(包裹代码块,Python); RunTree API(底层显式构造 trace)
📖 "LangSmith *integrations* provide automatic tracing for popular LLM providers and agent frameworks (the equivalent of auto-instrumentation in general observability)." — Observability concepts
📖 "*Manual instrumentation* lets you add tracing to any code, regardless of the framework. Use it when you're not using a supported integration or when you need granular control over what gets traced." — Observability concepts
🧪 实例 用受支持框架时,一个环境变量即可开启追踪(如 LangChain/LangGraph);不受支持的 provider 则用 @traceable 装饰器手动包住函数。
python
from langsmith import traceable

@traceable(run_type="tool")  # 手动埋点:把该函数追踪为一个 tool span
def get_context(question: str) -> str:
    return "LangSmith traces are stored for 14 days on the Developer plan."
🔍 追问 手动埋点的三种机制里,哪个最适合只想追踪一整个函数? → @traceable/traceable 装饰器,一行注解即可包住整个函数的输入输出与嵌套 span。
📚 拓展阅读
Q用 quickstart 给一个 LLM 应用加追踪,最少需要哪几步?OpenAI wrapper 和 traceable wrapper 各干什么?深挖·拓展中频
Quickstart 上手 wrap_openai
⏱️ 现行
quickstart 的核心是两件"包裹"工具加上环境变量配置。OpenAI wrapper 包裹 OpenAI client,让每一次 LLM 调用都自动作为一个嵌套 span 被记录;Traceable wrapper 包裹一个函数,让它的输入、输出以及所有嵌套 span 在 LangSmith 里合并显示为单个 trace(Python 用 @traceable,TypeScript/Kotlin 用 traceable,Java 用 Tracing.traceFunction)。示例里 assistant 函数先调用 get_context 工具取回上下文,再把上下文传给模型,两个函数都套上 traceable wrapper,就能把完整 pipeline 捕获进一个 trace,工具调用和 LLM 调用作为嵌套 span。开启追踪只需三个环境变量:LANGSMITH_TRACING=trueLANGSMITH_API_KEY 和 provider 的 key。要注意区域问题——如果账号不在默认的 US 区域,还必须设 LANGSMITH_ENDPOINT 指向所在区域的 API URL,否则 API key 不被识别、请求认证会失败。
术语 OpenAI wrapper(包裹 OpenAI client,自动记录每次 LLM 调用为嵌套 span); Traceable wrapper(包裹函数,把输入输出和嵌套 span 合成一个 trace); LANGSMITH_TRACING(开启追踪的环境变量); LANGSMITH_ENDPOINT(非 US 区域必须设置的 API URL)
📖 "OpenAI wrapper: wraps the OpenAI client so every LLM call is automatically logged as a nested span." — Tracing quickstart
📖 "Traceable wrapper: wraps a function so its inputs, outputs, and any nested spans appear as a single trace in LangSmith." — Tracing quickstart
📖 "If your account is in a region other than US (the default), also set LANGSMITH_ENDPOINT to the API URL for your region. Without this, your API key won't be recognized and requests will fail to authenticate." — Tracing quickstart
🧪 实例 开启追踪只需在 shell 里导出这几个环境变量。
bash
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="<your-langsmith-api-key>"
export OPENAI_API_KEY="<your-openai-api-key>"
🔍 追问 没设 LANGSMITH_PROJECT 会怎样? → LangSmith 会在 trace 首次写入时自动创建一个默认 tracing project。
📚 拓展阅读
Q多轮对话怎么在 LangSmith 里关联起来?thread 是怎么形成的?深挖·拓展中频
Thread 多轮对话 session_id
⏱️ 现行
LangSmith 用 *thread* 来表示"一段对话"。关键机制是:多轮对话里每一轮各自是一个独立 trace,但这些 trace 通过一个共享标识被链接在一起。要把多个 trace 归组成一个 thread,你需要传一个特殊的 metadata key——session_idthread_id——并给它一个唯一值,值相同的 trace 就会被认为属于同一个 thread。这个设计的合理性在于:单轮请求天然对应单个 trace(保持追踪粒度清晰),而对话上下文这种跨请求的关系不该硬塞进 trace 层级,用一个共享 metadata 键在更高维度做关联既保留了每轮的独立可调试性,又能在 UI 上还原完整对话流。归组后可以用 Chat 直接分析 thread,理解 agent 表现、调试问题、从对话中获得洞察,而不必手工翻数据。
术语 thread(线程,代表一段对话的一串 trace); session_id / thread_id(用于归组 thread 的特殊 metadata key); shared identifier(把各轮 trace 链接起来的共享标识)
📖 "A *thread* is a sequence of traces representing a single conversation. Each turn in a multi-turn conversation is its own trace, but traces are linked by a shared identifier. To group traces into threads, pass a special metadata key (session_id or thread_id) with a unique value." — Observability concepts
🧪 实例 一个客服对话 5 个来回就产生 5 个 trace,只要每个 trace 都带上相同的 thread_id(例如某个会话 UUID),LangSmith 就把它们显示为同一个 thread。
🔍 追问 归组成 thread 之后有什么现成工具可以分析? → 可以用 Chat 分析 trace、run 和 thread,理解 agent 表现并调试,无需手动翻数据。
📚 拓展阅读
Q什么是 trace enrichment(feedback / tags / metadata)?trace 数据会保留多久?深挖·拓展低频
Feedback Metadata 数据保留
⏱️ 现行
trace enrichment 是给原始 run 附加评价与上下文信息的一组能力。*Feedback* 让你按某些标准给单个 run 打分,每条 feedback 由一个 tag 和一个 score 组成,通过唯一 run ID 绑定到 run,可以是连续值或离散(分类)值。*Tags* 是附加到 run 上的字符串,用于在 UI 里分类、过滤和分组。*Metadata* 是附加到 run 上的键值对(如应用版本、环境等上下文),同样能用来过滤和分组。在数据保留方面,LangSmith(SaaS)从 ingestion 起保留 trace 数据 400 天,之后 trace 被永久删除,仅保留有限的 metadata 用于用量统计。关键权衡:如果需要长期留存数据,应把它加入 dataset——dataset 会无限期保存,即便源 trace 已被删除也不受影响。这套机制让你既能对追踪数据做质量标注和精细过滤,又能在成本可控的保留策略下把真正有价值的数据沉淀为长期资产。
术语 feedback(反馈,tag+score 给 run 打分,绑定 run ID); tags(附加到 run 的字符串,用于分类/过滤/分组); metadata(附加到 run 的键值对上下文); data retention(SaaS 保留 400 天); dataset(无限期保存的数据集)
📖 "*Feedback* allows you to score an individual run based on certain criteria. Each feedback entry consists of a tag and a score, and is bound to a run by a unique run ID." — Observability concepts
📖 "LangSmith (SaaS) retains trace data for 400 days from ingestion. After that, traces are permanently deleted, with limited metadata retained for usage statistics." — Observability concepts
🧪 实例 给一条 run 打上 environment=production 的 metadata 和一条 helpfulness 的 feedback score,之后就能在 UI 里筛出生产环境里低分的 run 集中排查。
🔍 追问 想让数据超过 400 天还能保留怎么办? → 把它加入 dataset,dataset 会无限期持久化,即使源 trace 被删除也保留。
📚 拓展阅读

第2章 评估

QLangSmith 的 offline 与 online 评估有什么区别?各自解决什么问题、为什么要分开?深挖·拓展🔥高频
offline-eval online-eval evaluation-lifecycle
⏱️ 现行
LLM 输出是非确定性的,因此响应质量很难评估,evals 的作用就是把"什么是好"拆解成可度量的指标。LangSmith 按"何时、在哪里运行"把评估分成两类:offline 评估面向 dataset 里的 example(input/reference output 对),用于 pre-deployment 阶段的 benchmarking、regression testing、unit testing、backtesting——因为有 reference output,可以直接把实际结果和期望答案做正确性对比;online 评估面向生产 tracing 中的 run 和 thread,没有 reference output,因此只能聚焦质量模式、安全性和真实行为,用于 real-time monitoring、anomaly detection 和 production feedback。二者不是互斥而是构成迭代反馈闭环:online 暴露的问题变成 offline 的测试用例,offline 验证修复,online 再确认生产改善。核心权衡在于——有无 reference output 决定了你能评什么:reference-free evaluator 在离线和在线都能用、保证一致性,reference-based evaluator 只能离线用但能做更精确的正确性检查。
flowchart LR
    A[Development] --> B[Testing] --> C[Deployment] --> D[Monitoring] --> E[Iteration]
    A -.Offline.-> F[Dataset/Examples]
    C -.Online.-> G[Runs/Threads]
    E -.Both.-> H[Feedback loop]
术语 offline evaluation(离线评估,跑在带 reference output 的 dataset 上); online evaluation(在线评估,跑在生产 run/thread 上,无 reference); reference output(参考输出/ground truth,决定能否做正确性对比)
📖 "This difference in targets determines what you can evaluate: offline evaluations can check correctness against expected answers, while online evaluations focus on quality patterns, safety, and real-world behavior." — Evaluation concepts
📖 "As applications mature, both evaluation types work together in an iterative feedback loop to improve quality continuously." — Evaluation concepts
🧪 实例 offline/online 的关键差异对照(源自 Quick reference 表):

| 维度 | Offline | Online |
| --- | --- | --- |
| Runs on | Dataset (Examples) | Tracing Project (Runs/Threads) |
| Data access | Inputs, Outputs, Reference Outputs | Inputs, Outputs only |
| When to use | Pre-deployment, during development | Production, post-deployment |
| Data requirements | Requires dataset curation | No dataset needed, evaluates live traces |
🔍 追问 online 评估为什么不能做"正确性(correctness)"检查? → 因为生产 run 不包含 reference output,评估器无从知道"正确答案"该是什么,只能依赖 quality heuristics、safety checks 和 reference-free 技术。
📚 拓展阅读
QLangSmith 支持哪几种评估技术(evaluation techniques)?Human / Code / LLM-as-judge / Pairwise 各自的机制和适用场景是什么?深挖·拓展🔥高频
evaluators llm-as-judge code-evaluator pairwise
⏱️ 现行
LangSmith 提供四种评估技术。Human evaluation 是人工审查输出和执行 trace,常作为评估的有效起点,通过 annotation queues 做结构化收集,分 single-run(逐条按 rubric 打分)和 pairwise(两个 run 并排比较)两种队列。Code evaluator 是确定性的、基于规则的函数,适合"响应结构非空、生成代码能编译、分类精确匹配"这类可判定检查。LLM-as-judge 用 LLM 给输出打分,评分规则和标准编码在 prompt 里,可以是 reference-free(检查是否有冒犯内容或是否符合特定标准)或 reference-based(相对参考做事实准确性对比);它需要仔细审查分数并调 prompt,few-shot evaluator(在 grader prompt 里放入 input/output/expected grade 示例)往往能提升表现。Pairwise evaluator 用启发式、LLM 或人工比较两个版本的输出——当"直接给单个输出打绝对分"困难、但"比较两个输出谁更好"容易时特别有效,典型如摘要任务里选更有信息量的那一个比给单个摘要打绝对分更简单。选择的核心权衡是:Code 快且确定但只覆盖可规则化的检查;LLM-as-judge 能评主观质量但要防评分漂移;Pairwise 回避了绝对打分难题但只给相对结论。
术语 Human evaluation(人工评估,用 annotation queues 组织); Code evaluator(确定性规则函数); LLM-as-judge(用 LLM 按 prompt 里的标准打分); Pairwise evaluator(比较两版本输出的相对优劣); few-shot evaluator(在 grader prompt 中加入示例以提升表现)
📖 "*Code evaluators* are deterministic, rule-based functions." — Evaluation concepts
📖 "Pairwise evaluation works well when directly scoring an output is difficult but comparing two outputs is straightforward." — Evaluation concepts
📖 "LLM-as-judge evaluators require careful review of scores and prompt tuning. Few-shot evaluators, which include examples of inputs, outputs, and expected grades in the grader prompt, often improve performance." — Evaluation concepts
🧪 实例 evaluator 返回的 feedback 结构——一个字典或字典列表,字段包括:
json
{
  "key": "correctness",
  "score": 1,
  "comment": "Answer is semantically equivalent to the reference."
}

其中数值型指标用 score、类别型指标用 valuecomment 可选,用于附加打分理由。
🔍 追问 为什么摘要任务更适合 pairwise 而不是绝对打分? → 因为摘要是创造性任务、存在很多正确答案,"选出两个摘要里更有信息量/更清晰的那个"通常比"给单个摘要分配绝对分数"简单可靠。
📚 拓展阅读
Qreference-free 和 reference-based 评估器有什么本质区别?为什么这个区别决定了它能用在哪里?深挖·拓展🔥高频
reference-free reference-based evaluator-inputs
⏱️ 现行
理解一个 evaluator 是否需要 reference output,是判断它能在何处使用的关键。Reference-free evaluator 不与期望输出比较就能评估质量,因此离线和在线都能用,典型包括 safety checks(毒性检测、PII 检测、内容策略违规)、format validation(JSON 结构、必填字段、schema 合规)、quality heuristics(响应长度、延迟、特定关键词)以及 reference-free LLM-as-judge(清晰度、连贯性、有用性、语气)。Reference-based evaluator 需要 reference output,因此只能用于离线评估,典型包括 correctness(与参考答案的语义相似度)、factual accuracy(对照 ground truth 做事实核查)、exact match(已知标签的分类任务)以及 reference-based LLM-as-judge(对照参考比较输出质量)。这背后的机制是评估器输入不同:offline evaluator 同时收到 example(含 inputs、reference outputs、metadata)和 run(实际输出与中间步骤),而 online evaluator 只收到 production trace 的 run(无 reference output)。设计评估策略时的权衡是——reference-free 在离线测试和在线监控之间提供一致性,reference-based 则能在开发阶段做更精确的正确性检查。
术语 reference-free evaluator(无参考评估器,离线在线通用); reference-based evaluator(有参考评估器,仅离线可用); safety checks(安全检查,如毒性/PII 检测); format validation(格式校验,如 JSON/schema); evaluator inputs(评估器输入,决定能否拿到 reference)
📖 "Reference-free evaluators assess quality without comparing to expected outputs. These work for both offline and online evaluation:" — Evaluation concepts
📖 "Reference-based evaluators require reference outputs and only work for offline evaluation:" — Evaluation concepts
🧪 实例 同一个"事实准确性"需求在两种模式下的落法——离线用 reference-based,对照 ground truth 答案做 fact-checking;在线用 reference-free LLM-as-judge,只检查生产响应里是否有毒性内容。以 RAG 为例:answer correctness(是否与参考答案一致)需要 reference output,只能离线;document relevance、answer faithfulness、answer helpfulness 都不需要参考,可在线做自一致性检查。
🔍 追问 一个 offline 评估器和 online 评估器在"输入"上具体差在哪? → offline 评估器同时拿到 example(含 reference outputs)和 run;online 评估器只拿到 production trace 的 run,没有 reference outputs。
📚 拓展阅读
Qoffline 评估的核心目标(targets)有哪些?Dataset、Example、Experiment、Run、Thread 分别是什么?深挖·拓展🔥高频
dataset example experiment run thread
⏱️ 现行
评估跑在不同 target 上,取决于它是离线还是在线。离线评估跑在 dataset 和 example 上——dataset 是一组 example 的集合,一个 example 就是一对 test input + reference output;每个 example 由 inputs(传给应用的输入变量字典)、reference outputs(可选,不传给应用、只在 evaluator 里用)、metadata(可选,用于做过滤视图)构成。experiment 表示"在某个 dataset 上评估某个特定应用版本"的结果,会为 dataset 里每个 example 捕获输出、evaluator 分数和执行 trace;通常一个 dataset 上会跑多个 experiment 来测不同配置(不同 prompt 或 LLM),LangSmith 支持并排对比。在线评估跑在 run 和 thread 上——run 是部署应用的一次执行 trace,含 inputs、outputs、intermediate steps(所有子 run,如 tool call、LLM call)、metadata;与 dataset 里的 example 不同,run 不含 reference output,所以在线评估器必须在不知道"正确答案"的情况下评质量。thread 是相关 run 的集合,代表多轮对话,可在 thread 层面评估跨轮连贯性、话题保持、用户满意度等对话级属性。
术语 dataset(example 的集合); example(一对 test input + reference output); experiment(某应用版本在 dataset 上的评估结果); run(部署应用的一次执行 trace); thread(相关 run 的集合,代表多轮对话)
📖 "A dataset is a *collection of examples* used for evaluating an application. An example is a test input, reference output pair." — Evaluation concepts
📖 "An *experiment* represents the results of evaluating a specific application version on a dataset. Each experiment captures outputs, evaluator scores, and execution traces for every example in the dataset." — Evaluation concepts
📖 "Unlike examples in datasets, runs do not include reference outputs." — Evaluation concepts
🧪 实例 一个 example 的组成(inputs / reference outputs / metadata):
json
{
  "inputs": {"question": "What is the capital of France?"},
  "reference_outputs": {"answer": "Paris"},
  "metadata": {"category": "geography", "split": "test"}
}

reference outputs 不会传给应用,仅供 evaluator 使用。
🔍 追问 dataset 的 split 和 metadata 有什么区别? → split 是命名子集,用于评估层面的高层组织分组(如 train/validation/test、按类别分、分阶段 rollout);metadata 是逐 example 的信息(如 tag、来源)。LangSmith 允许一个 example 属于多个 split。
📚 拓展阅读
Qoffline 评估有哪几种类型?benchmarking、unit test、regression test、backtesting 分别在什么阶段用?深挖·拓展中频
benchmarking unit-test regression-test backtesting
⏱️ 现行
离线评估在 pre-deployment 阶段跑在 curated dataset 上,因为 example 带 reference output,团队能比较版本、验证功能、在改动暴露给用户前建立信心。Benchmarking 在一个 curated dataset 上比较多个应用版本以找出最优者,需要有 gold-standard reference output 和精心设计的对比指标(如 RAG Q&A bot 用 LLM-as-judge 检查语义等价,ReAct agent 用启发式验证所有期望 tool call 都被调用)。Unit tests 验证单个组件的正确性,在 LLM 场景下常是对输入输出的规则断言(如验证生成代码能编译、JSON 能加载),通常期望稳定通过、适合放进 CI pipeline,此时应配置 caching 以减少 LLM API 调用和成本。Regression tests 度量版本间随时间的性能一致性,确保新版本不在当前版本能处理的用例上退化、并最好优于 baseline,通常在做预期影响用户体验的更新(如模型或架构变更)时运行,LangSmith 的对比视图用红色标退化、绿色标改进。Backtesting 用历史生产数据评估新版本——把生产日志转成 dataset,让新版本处理这些过去真实的用户输入,常用于评估新模型发布。
术语 benchmarking(基准比较,选出最优版本); unit test(单元测试,规则断言,适合 CI); regression test(回归测试,防止版本退化); backtesting(回测,用历史生产数据评估新版本)
📖 "*Benchmarking* compares multiple application versions on a curated dataset to identify the best performer." — Evaluation types
📖 "*Regression tests* measure performance consistency across application versions over time." — Evaluation types
📖 "*Backtesting* evaluates new application versions against historical production data." — Evaluation types
🧪 实例 把 unit test 放进 CI 时的典型配置——用 pytest 跑规则断言并开启 caching 降低成本:
bash
# 运行 LangSmith 评估型单元测试(CI 中开启 caching 以减少 LLM API 调用)
pytest tests/ --langsmith-output
🔍 追问 为什么 unit test 在 CI 里要开 caching? → unit test 期望稳定通过、会反复运行,caching 能最小化 LLM API 调用及相关成本。
📚 拓展阅读
Q如何评估一个 agent?final response、single step、trajectory 三种方式的取舍是什么?深挖·拓展中频
agent-eval trajectory tool-calling single-step
⏱️ 现行
LLM 驱动的自主 agent 组合了 tool calling、memory、planning 三部分,这引出三类常见的 agent 评估,且它们不互斥、往往要做多个甚至全部。Final response 把 agent 当黑盒、只评它是否把活干成了:输入是用户输入(可选带工具列表),输出是最终响应,常用 LLM-as-judge,但缺点是跑得慢、看不到 agent 内部、失败时难调试、指标难定义。Single step 评估单一步骤——通常是 agent 决定做什么的那次 LLM 调用:输入是该步的输入(可能含此前完成的步骤),输出是该步的 LLM 响应(常含 tool call),evaluator 通常是"是否选对了 tool"的二值分加上"tool 输入是否正确"的启发式;优点是能定位失败点、快(只一次 LLM 调用),缺点是只覆盖单步、且给靠后步骤造数据集很难。Trajectory 评估 agent 走过的所有步骤:输出是一列 tool call,可以是"精确"轨迹或"任意顺序的期望集合",evaluator 是对步骤序列的某个函数——精确匹配简单但有缺陷(可能有多条正确路径、无法区分"差一步"和"完全错"),改进方向是计"错误步数"或"是否任意顺序调用了全部期望工具",或把完整轨迹连同参考轨迹作为 messages 交给 LLM-as-judge。
术语 Final Response(黑盒评估最终响应); Single step(评估单一步骤,如是否选对 tool); Trajectory(评估走过的完整步骤序列); tool calling(模型生成"调哪个工具+输入参数")
📖 "One way to evaluate an agent is to assess its overall performance on a task. This basically involves treating the agent as a black box and simply evaluating whether or not it gets the job done." — Application-specific evaluation approaches
📖 "Evaluating an agent's trajectory involves evaluating all the steps an agent took." — Application-specific evaluation approaches
🧪 实例 LangGraph 里的 tool-calling agent 循环——assistant node 是决定是否调工具的 LLM,tool condition 判断是否选了工具并路由到 tool nodetool node 执行工具并把输出作为 tool message 返回 assistant node,只要 assistant node 继续选工具就循环,不选工具则直接返回 LLM 响应:
flowchart LR
    U[User input] --> A[assistant node/LLM]
    A --> C{tool condition}
    C -- tool selected --> T[tool node]
    T --> A
    C -- no tool --> R[Return LLM response]
🔍 追问 为什么"精确轨迹匹配"这种 evaluator 有缺陷? → 因为有时存在多条正确路径,而且它无法区分"轨迹只差一步"和"完全错";改进方式是关注错误步数或是否任意顺序调用了全部期望工具。
📚 拓展阅读
Q"评估(evaluation)"和"测试(testing)"是一回事吗?两者如何配合?深挖·拓展低频
evaluation-vs-testing metrics regression
⏱️ 现行
测试和评估相似但是不同的概念。Evaluation 按指标度量性能,指标可以是模糊或主观的,在相对意义上更有用,通常用来把多个系统互相比较。Testing 断言正确性,一个系统只有通过全部测试才能部署。二者的机制差异带来配合方式:评估指标可以转化成测试——例如 regression test 可以断言"新版本必须在相关指标上优于 baseline 版本";当系统运行代价高时,把 test 和 evaluation 一起跑更高效。评估本身可以用标准测试工具(如 pytest 或 Vitest/Jest)来写,这样就能把"度量质量"和"断言正确性"统一到同一套工程流程里。理解这个区别的价值在于:不要指望用 pass/fail 的测试思维去衡量本质上模糊、主观的质量维度,也不要把"相对更好"的评估结论直接当成部署门禁——而是让评估指标经过阈值化后转成可断言的回归测试。
术语 evaluation(按指标度量性能,偏相对/主观); testing(断言正确性,pass 才能部署); metrics(指标,可模糊/主观); regression test(可由评估指标转化而成的门禁测试)
📖 "Evaluation measures performance according to metrics. Metrics can be fuzzy or subjective, and prove more useful in relative terms. They typically compare systems against each other." — Evaluation concepts
📖 "Testing asserts correctness. A system can only be deployed if it passes all tests." — Evaluation concepts
🧪 实例 把评估指标转成回归测试的思路——用 pytest 断言新版本在关键指标上不低于 baseline,即把"相对更好"的评估结论固化成 pass/fail 的部署门禁;文档明确指出评估可以用标准测试工具(pytest 或 Vitest/Jest)来编写。
🔍 追问 什么时候适合把 test 和 evaluation 放在一起跑? → 当系统运行代价高(expensive to run)时,一起跑能复用一次执行、提升效率。
📚 拓展阅读
中频

评估实操与实验配置

How to evaluate agents Experiment configuration
Q用 LangSmith 的 evaluate() 跑一次评估,需要哪几个核心参数?各自作用是什么?深挖·拓展🔥高频
evaluate target-function dataset evaluators
⏱️ 现行
一次评估的本质是"把数据集里每条 example 的输入喂给被测应用,再用一组 evaluator 给输出打分"。因此 evaluate() 的核心参数围绕这三件事:第一个位置参数是 target 函数,它接收输入字典、返回输出字典——数据集里每条 Exampleexample.inputs 字段就是传给它的内容;data 指定要评估的 LangSmith 数据集(可以是名字、UUID,或一个 example 迭代器);evaluators 是给函数输出打分的评估器列表,并且绑定在数据集上的 UI evaluator 也会自动被触发;metadata 是可选的实验附加信息,其中 models/prompts/tools 三个保留键会自动填充实验表里对应的列。权衡上,官方建议 Python 大作业改用异步的 aevaluate()(接口相同),并务必配置 max_concurrency 把数据集拆分到多线程并行,否则大规模评估会很慢。
术语 target function(被测函数,吃 example.inputs、吐输出); data(数据集名/UUID/example 迭代器); evaluators(打分函数列表); metadata(实验元数据,models/prompts/tools 为保留键); experiment_prefix(实验名前缀,可选)
📖 "a target function that takes an input dictionary and returns an output dictionary. The example.inputs field of each Example is what gets passed to the target function." — How to evaluate agents
📖 "metadata - an optional object to attach to the experiment. Pass models, prompts, and tools keys to populate the corresponding columns in the experiment table view." — How to evaluate agents
🧪 实例
python
results = ls_client.evaluate(
    toxicity_classifier,
    data=dataset.name,
    evaluators=[correct],
    experiment_prefix="gpt-5.4-mini, baseline",  # optional, experiment name prefix
    description="Testing the baseline system.",  # optional, experiment description
    max_concurrency=4,  # optional, add concurrency
    metadata=EXPERIMENT_METADATA,  # optional, used to populate model/prompt/tool columns in UI
)
🔍 追问 evaluate()ls_client.evaluate() 有区别吗? → 没有,官方注释写明 "Client.evaluate() and evaluate() behave the same.",可以用 from langsmith import evaluate 直接调用等价函数。
📚 拓展阅读
QLangSmith 里 evaluator 是什么?它接收哪些参数,有哪几种定义方式?深挖·拓展🔥高频
evaluator scoring reference-outputs llm-as-judge
⏱️ 现行
evaluator 就是给应用输出打分的函数,它接收三类信息:example 的输入(inputs)、应用实际产生的输出(actual outputs),以及在有标注时的参考输出(reference outputs)。对于有标签的任务,evaluator 可以直接比较实际输出是否等于参考输出——比如毒性分类里就是 outputs["class"] == reference_outputs["label"] 返回布尔值。定义方式有两种主要途径:一是本地代码里写函数(Python 需 langsmith>=0.3.13、TS 需 langsmith>=0.2.9),二是在 LangSmith UI 的 Evaluators 标签页创建;UI 里创建的 evaluator 会随每个新实验自动触发(即绑定在数据集上)。权衡在于:本地代码 evaluator 灵活、可版本化、适合确定性规则;UI/LLM-as-judge evaluator 免代码、可自动挂到数据集上,适合主观或语义类打分。官方还提示可以直接用开源 openevals 包里的预置 evaluator,省去重复造轮子。
术语 evaluator(打分函数); reference outputs(参考/标注输出,可选); outputs(应用实际输出); llm-as-judge(用 LLM 当评判员的 UI evaluator); openevals(LangChain 开源预置 evaluator 包)
📖 "Evaluators are functions for scoring your application's outputs. They take in the example inputs, actual outputs, and, when present, the reference outputs." — How to evaluate agents
📖 "These evaluators will be automatically triggered with every new experiment." — How to evaluate agents
🧪 实例
python
def correct(inputs: dict, outputs: dict, reference_outputs: dict) -> bool:
    return outputs["class"] == reference_outputs["label"]
🔍 追问 想省事不自己写 evaluator 有什么选项? → 官方推荐 LangChain 开源评估包 openevals,里面有常见的 prebuilt evaluator 可直接用。
📚 拓展阅读
Q为什么评估要用 repetitions(重复次数)?配置它会带来什么开销?深挖·拓展中频
repetitions num_repetitions non-determinism
⏱️ 现行
repetitions 的动机是 LLM 输出的非确定性——同一输入多次运行结果可能不同,单次评估容易被随机波动误导。通过把一个实验重复运行多次,就能得到对性能更准确的估计(相当于对随机噪声做平均)。配置方式是给 evaluate/aevaluatenum_repetitions 参数。关键的成本权衡在于:每次重复会把 target 函数和所有 evaluator 都重跑一遍,所以重复 N 次意味着模型调用和评估成本都大致乘以 N。因此 repetitions 是"用更多算力/花费换更稳定的度量",适合对指标可信度要求高、或输出方差大的场景,而不该无脑调高。
术语 repetitions(重复运行实验以对抗输出波动); num_repetitions(配置重复次数的参数); non-deterministic(LLM 输出非确定性)
📖 "Since LLM outputs are non-deterministic, multiple repetitions provide a more accurate performance estimate." — Experiment configuration
📖 "Each repetition re-runs both the target function and all evaluators." — Experiment configuration
🧪 实例
python
# 让实验重复运行 3 次,以平滑 LLM 输出波动
results = ls_client.evaluate(
    toxicity_classifier,
    data=dataset.name,
    evaluators=[correct],
    num_repetitions=3,
)
🔍 追问 repetitions 会影响成本吗? → 会,因为每次重复都会重跑 target 函数和全部 evaluator,总调用量与开销随重复次数成倍增长。
📚 拓展阅读
Qmax_concurrencyevaluateaevaluate 里语义有什么不同?深挖·拓展中频
concurrency max_concurrency semaphore async
⏱️ 现行
concurrency 控制一次实验里同时运行多少条 example,靠给 evaluate/aevaluatemax_concurrency 配置,但两个函数的语义不一样。同步的 evaluate 里,max_concurrency 指的是运行 target 函数和 evaluator 的最大并发线程数——即线程池大小。异步的 aevaluate 里,max_concurrency 用一个信号量(semaphore)来限制并发任务:aevaluate 为每条 example 创建一个 task,每个 task 负责跑该 example 的 target 函数和全部 evaluator,于是 max_concurrency 表示的是同时处理的 example 的最大数量。理解这个区别很重要:同步版本是"线程"粒度,异步版本是"每个 example 一个协程任务"粒度,后者用信号量做背压,更适合大规模 IO 密集(大量模型 API 调用)的评估作业。官方也正是据此建议大作业优先用 aevaluate 并显式设置该参数,因为它"通过把数据集拆分到多线程/任务上"来并行化评估。
术语 concurrency(同时运行的 example 数); max_concurrency(并发上限参数); semaphore(aevaluate 用来限流的信号量); task(aevaluate 为每条 example 创建的异步任务)
📖 "The max_concurrency argument specifies the maximum number of concurrent threads for running both the target function and evaluators." — Experiment configuration
📖 "aevaluate creates a task for each example, where each task runs the target function and all evaluators for that example. The max_concurrency argument specifies the maximum number of concurrent examples to process." — Experiment configuration
🧪 实例
python
# 同步 evaluate:max_concurrency 是线程数上限
ls_client.evaluate(target, data="Toxic Queries", evaluators=[correct], max_concurrency=4)

# 异步 aevaluate:max_concurrency 是同时处理的 example 数(信号量限流)
# await aevaluate(target, data="Toxic Queries", evaluators=[correct], max_concurrency=4)
🔍 追问 为什么大评估作业官方推荐用 aevaluate? → 因为它是 evaluate 的异步版本、接口完全相同,并且能用 max_concurrency 通过信号量把数据集拆分到多个任务并行,从而更高效地处理大规模 IO 密集的评估。
📚 拓展阅读
QLangSmith 评估的 caching 是怎么工作的?怎么开启,能带来什么收益?深挖·拓展低频
caching LANGSMITH_TEST_CACHE env-var
⏱️ 现行
caching 的作用是把 API 调用结果缓存到磁盘,用来加速将来的实验。开启方式很简单:把环境变量 LANGSMITH_TEST_CACHE 设为一个有写权限的有效文件夹路径即可。之后凡是发起相同 API 调用的实验,就会复用缓存里的结果,而不是重新发请求。收益上,这对反复迭代评估流程(例如只改 evaluator 或改 prompt、但底层模型调用不变)特别有价值:既省钱(少打真实 API)又提速。权衡在于缓存是按"相同 API 调用"命中的,一旦请求内容变化(换模型、改输入)就不会命中;而且缓存写在磁盘,需要保证目标目录可写。
术语 caching(把 API 结果落盘复用); LANGSMITH_TEST_CACHE(指定缓存目录的环境变量); identical API calls(命中缓存的条件——完全相同的调用)
📖 "*Caching* stores API call results to disk to speed up future experiments. Set the LANGSMITH_TEST_CACHE environment variable to a valid folder path with write access." — Experiment configuration
📖 "Future experiments that make identical API calls will reuse cached results instead of making new requests." — Experiment configuration
🧪 实例
bash
# 指向一个可写目录即可开启评估缓存
export LANGSMITH_TEST_CACHE=./.langsmith_cache
🔍 追问 什么情况下缓存不会命中? → 只有发起"相同 API 调用"的实验才复用缓存;一旦调用内容变化(换模型、改输入、改参数),就会当作新请求重新发起。
📚 拓展阅读

第3章 监控·提示·部署

QLangSmith 的 Alerts 支持哪些指标类型?告警条件是怎么定义的?为什么它是 project-scoped 的?深挖·拓展🔥高频
LangSmith Alerts Observability
⏱️ 现行
LangSmith 的告警是 threshold-based(阈值触发),覆盖五类指标:Run Count(时间窗内 run 总数,用于监控管线产量骤降)、Cost(时间窗内总成本,需先配置好 cost tracking)、Errors(错误 run 数或错误率)、Feedback Score(平均反馈分,用于监控用户体验或 online evaluation 的回归)、Latency(平均执行时长)。一条告警条件由几部分组成:聚合方式(Average/Percentage/Count)、比较运算符(>=<= 或 exceeds threshold)、阈值、聚合窗口(只能选 5 或 15 分钟),Feedback Score 类还需指定 Feedback Key。设计上告警是 project-scoped 的——每个被监控的 project 都要单独配置,这样不同项目可以用不同的敏感度阈值互不干扰;文档也建议从较宽的阈值起步,再根据观察到的模式收紧。对 Errors 和 Latency 还能用 filter builder 叠加 Status、Run Type、Tag、Error 等条件做精细化 scope。一个实用能力是可以在历史时间窗上 preview,可视化在某阈值下哪些数据点会触发,便于校准而不是盲调。
术语 threshold-based alerting(基于阈值的告警); Aggregation Window(聚合窗口,仅 5 或 15 分钟); project-scoped(按项目隔离配置); Feedback Key(反馈分告警要监控的具体反馈键)
📖 "Alerts in LangSmith are project-scoped, requiring separate configuration for each monitored project." — Alerts in LangSmith
📖 "LangSmith provides threshold-based alerting on the following metrics:" — Alerts in LangSmith
📖 "The configuration in the screenshot would generate an alert when more than 5% of runs within the past 5 minutes result in errors." — Alerts in LangSmith
🧪 实例 给 support agent 配一条错误率告警——scope 到 llm 类型且带 support_agent tag、错误匹配 RateLimitExceeded 的 run,聚合方式选 Percentage,窗口 5 分钟,阈值 5%。触发时 Slack 会收到一行结构化明细:
text
Total Cost: $12.50 ≥ $5.00 · avg · 30 min
🔍 追问 Cost 告警能直接开箱即用吗? → 不能;Cost 指标要求先配置好 cost tracking,否则没有成本数据可聚合。
📚 拓展阅读
QLangSmith 怎么做成本追踪?自动与手动两种方式各适用什么场景?自动计算需要哪三样东西?深挖·拓展🔥高频
LangSmith Cost Tracking Tokens
⏱️ 现行
LangSmith 对主流 provider 自动记录 LLM 的 token 用量和成本,同时允许你为任何额外组件提交自定义成本数据,从而在整个应用层面得到一个统一的花费视图。追踪有两条路径:自动(从 token 数和模型价格推导 LLM 调用成本)和手动(直接在任意 run 上——包括非 LLM 类型如 tool call、retrieval——指定成本)。自动计算需要三样东西:token counts、model and provider、model price。落地时:先通过 usage_metadata(可挂在 run metadata 上,也可直接放进 traced 函数的输出让 LangSmith 抽取)上报 input_tokens/output_tokens 等;再用 ls_providerls_model_name 标注模型;最后 LangSmith 用 model pricing table 把模型名映射到单位 token 价格算出成本。权衡在于:用 LangChain、或用 OpenAI/Anthropic 的 @traceable/wrapper 时这套是自动的,不用手动上报;但若成本是非线性的(自定义成本函数),就该走手动、client 端算好再作为 usage_metadata 发送。一个重要限制是定价表更新不会回填已记录的 trace 成本。
术语 usage_metadata(承载 token 数/成本的 run 元数据字段); ls_provider / ls_model_name(标识模型 provider 与名称的元数据键); model pricing table(模型名→单位 token 价格的映射表); total_cost(非 LLM run 直接上报成本的字段)
📖 "LangSmith automatically records LLM token usage and costs for major providers, and also allows you to submit custom cost data for any additional components." — Cost tracking
📖 "To compute cost automatically from token usage, you need to provide token counts, the model and provider, and the model price." — Cost tracking
🧪 实例 给一个非 LLM 的 tool run 直接挂成本:
python
from langsmith import traceable, get_current_run_tree

@traceable(run_type="tool", name="get_weather")
def get_weather(city: str):
    result = {"temperature_f": 68, "condition": "sunny", "city": city}
    tool_cost = 0.0015
    run = get_current_run_tree()
    run.set(usage_metadata={"total_cost": tool_cost})
    return result
🔍 追问 一个 run 里既有 cache_read 明细价又有默认 input 价,成本怎么算? → 按 token 类型从最具体到最不具体贪心计算——先扣掉 cache_read 部分,剩余 input tokens 再套默认 input 价("The cost for a run is computed greedily from most-to-least specific token type.")。
📚 拓展阅读
QLangSmith 的 prebuilt 和 custom dashboards 有什么区别?custom dashboard 里怎么把一个指标拆成多条曲线?深挖·拓展中频
LangSmith Dashboards Monitoring
⏱️ 现行
LangSmith 提供两类看板:prebuilt(为每个 tracing project 自动生成,覆盖 trace count、error rate、token usage 等基础指标,含 Traces、LLM Calls、Cost & Tokens、Tools、Run Types、Feedback Scores 六个分区)和 custom(完全可配置的图表集合)。二者定位不同:prebuilt 开箱即用、用于快速看健康度趋势;custom 用于跟踪对你应用最重要的指标。在 custom 图表里把单一指标拆成多条曲线(多 series)有两种方式:Group by(按 run tag/metadata、run name 或 run type 自动拆分,默认取频率 top 5、最多可配到 20),或 Data series(手动定义多条各带自己 filter 的序列,适合在同一指标内对比更细粒度的数据)。要注意 group by 的一个坑:metadata 和 tag 不会在父子 run 间传播,所以若想同时看 trace 图和 LLM call 图都按某 metadata key 分组,就得在 root run 和 LLM run 上都挂这个 metadata。
术语 prebuilt dashboard(每个项目自动生成的预置看板); Group by(按属性自动拆分为多 series,默认 top 5,可配到 20); Data series(手动定义带独立 filter 的序列); Chart filters(作用于图内全部 series 的过滤)
📖 "Prebuilt dashboards are created automatically for each project and cover essential metrics, such as trace count, error rates, token usage, and more." — Monitor projects with dashboards
📖 "Group by defaults to the top 5 elements by frequency, configurable up to 20." — Monitor projects with dashboards
🧪 实例 用户旅程监控——追踪 email assistant 在 triage_input 节点做的决策分布:
flowchart LR
    A[triage_input node] --> B{triage.response}
    B -->|no| C[No response needed]
    B -->|email| D[Send email back]
    B -->|notify| E[Notify user]

选指标 Run count,加树过滤 name=triage_inputIs Root=true(避免被节点数灌大计数),再按 no/email/notify 各建一条 data series。
🔍 追问 为什么 Tool 和 Run Type 图不受全局 group by 影响? → 这两个图已经自带了 group by,所以全局 group by 对它们不生效,只作用于其他图。
📚 拓展阅读
QAnnotation queues 是干什么的?single-run 和 pairwise(PAQ)两种队列有何区别?怎么控制多人评审的完成状态?深挖·拓展中频
LangSmith Annotation Queues Human Feedback
⏱️ 现行
Annotation queues 给人类评审者一个聚焦的工作流,把 run 归组、规定 rubric、追踪评审进度,比在 trace 里逐条 inline 标注更成体系。两种队列:single-run 每次呈现一个 run,评审者按你配置的 rubric 提交反馈,还支持 assertions 来捕获用于 offline evaluation 的验收标准;pairwise(PAQ)把两个 run 并排展示,让评审者针对每个 rubric 项快速判断哪个输出更好(或等价),专为两个 experiment 的 A/B 对比设计,只能从 Datasets & Experiments 页创建且必须恰好选中两个 experiment。多人评审的完成控制上有两套机制:用基于数量的 "Number of reviewers per run" 阈值,或打开 "Use assigned reviewers" 指定具体成员——此时队列项走 Needs Review → Needs Others' Review → Completed 三态,只有每个被指派的评审者都提交后才算 Completed,非指派成员仍可标注但不计入完成。还可开启 reservations 锁定 run 防止并发评审。因为这些设置,每个评审者能看到的 run 数可能和队列总量不同。
术语 single-run queue(逐个呈现 run 并按 rubric 打分); pairwise annotation queue / PAQ(两 run 并排做 A/B 对比); assigned reviewers(指派特定成员,全部提交才 Completed); reservation(锁定 run 一段时间防并发评审)
📖 "Pairwise annotation queues (PAQs) present two runs side-by-side so reviewers can quickly decide which output is better (or if they are equivalent) against the rubric items you define." — Use annotation queues
📖 "A run is marked Completed only when every assigned reviewer has submitted their review. Queue items progress through three states: Needs ReviewNeeds Others' ReviewCompleted." — Use annotation queues
🧪 实例 文档建议的分诊策略——把已带用户负反馈(如 thumbs-down)的 run 路由进 single-run queue 做 triage,同时进 pairwise queue 与更强 baseline 做正面对比,以快速识别回归。PAQ 里评审用热键 A/B/E 分别锁定 A better、B better、Equal。
🔍 追问 PAQ 里的两个 experiment 的 run 是怎么配对的? → 创建 PAQ 时 LangSmith 按 chronological order(时间顺序)自动配对两个 experiment 的 run 并填充队列;后续追加会保留历史对比并把新对追加到队尾。
📚 拓展阅读
QLangSmith 告警能路由到哪些通知渠道?用 Webhook 时 payload 里会附带哪些字段,为什么占位符不会被自动替换?深挖·拓展低频
LangSmith Alerts Webhook Notifications
⏱️ 现行
告警可路由到 Slack、PagerDuty、Dynatrace 或任意 HTTP 端点(via webhook),其中原生 Slack 通知仅在 LangSmith Cloud 可用,自托管需改用 webhook 的 Slack recipe。Webhook 的工作机制是:告警触发时向你的端点发 HTTP POST(JSON),LangSmith 会把一批自动填充字段合并进 payload 作为顶层 key,包括 alert_rule_id(可用作去重键的 UUID)、alert_rule_namealert_rule_typealert_rule_attribute(error_count/feedback_score/latency/cost 之一)、triggered_metric_valuetriggered_thresholdtimestamp 等。关键的坑在于:LangSmith 不对 request body 做模板替换,像 {alert_rule_name} 这样的占位符会被原样发出,只有接收方自己能从 incoming JSON 抽字段(如 Power Automate、AWS Lambda、自定义 handler)时才会解析成真值。这就是为什么文档推荐 Teams 用 Workflows(Power Automate)方案——它能从 payload 里读字段并正确渲染。最佳实践还包括:根据应用关键度调敏感度、从宽阈值起步、确保告警路由到合适的 on-call。
术语 webhook(告警触发时向自定义端点发 HTTP POST); alert_rule_id(可作去重键的告警 UUID); alert_rule_attribute(触发指标属性:error_count/feedback_score/latency/cost); template substitution(LangSmith 不做的模板替换)
📖 "Alerts can [route](#step-4-configure-notification-channel) to Slack, PagerDuty, Dynatrace, or any HTTP endpoint via webhook." — Alerts in LangSmith
📖 "LangSmith does not perform template substitution on the request body. The auto-populated fields above are merged into the outgoing JSON as top-level keys, alongside the body you configure." — Alerts in LangSmith
🧪 实例 通过 webhook 发到 Slack chat.postMessage 的 header 配置(把 token 换成你的 Bot User OAuth Token):
json
{
  "Content-Type": "application/json",
  "Authorization": "Bearer xoxb-your-token-here"
}
🔍 追问 为什么 PagerDuty 集成里同一告警一小时内可能收不到第二次? → 因为要在一小时内再次收到该告警,必须先在 PagerDuty 里 resolve 掉这条告警创建的 active incident,否则重复触发会被抑制。
📚 拓展阅读
中频

提示管理·Agent Server·部署

How to sync prompts with GitHub Agent Server LangSmith Deployment
Q部署一个 Agent Server 时,你到底部署了哪几部分?这些组件各自承担什么职责?深挖·拓展🔥高频
Agent Server LangGraph 持久化
⏱️ 现行
Agent Server 是围绕 assistants 概念构建的 API,一次部署本质上由三大件组成:一个或多个 graph、一个用于持久化的数据库,以及一个 task queue。graph 是 Assistant 的"蓝图",最常见是实现一个 agent,但也可以只是简单来回对话的 chatbot,复杂应用里往往是多个 agent 协同的复杂流程;而且 graph 不必用 LangGraph 写,可通过 Functional API 部署 Strands、Google ADK 等其他框架的 agent。持久化层默认全部由 PostgreSQL 支撑,分三类数据:核心资源数据(assistants、threads、runs、cron jobs,始终存 Postgres)、checkpoint 短期记忆(每步的执行状态快照,让 run 可从最近 checkpoint 而非从头恢复)、store 长期记忆(跨 thread 保留信息)。这里的关键权衡是 durability mode 控制 checkpoint 写入频率——默认 async 每步后写入(更强的容错但更多写开销),exit 只存最终状态(更省但中断即丢中间进度)。checkpoint 可切到 MongoDB 或自定义实现,而 task queue 的信令/取消/流式则交给 Redis,但 Redis 只存临时数据,run 数据永远读写 Postgres——这种"状态归 Postgres、信令归 Redis"的分工保证了容器无状态化和水平扩展。
术语 graph(可部署的执行图,是 Assistant 的蓝图); assistant(为特定任务配置的 agent); checkpoint(每步执行状态快照,即短期记忆); store(跨 thread 的长期记忆); durability mode(控制 checkpoint 写入频率的模式)
📖 "When you deploy Agent Server, you are deploying one or more [graphs](#graphs), a database for persistence, and a [task queue](#task-queue)." — Agent Server
📖 "Agent Server persists three types of data, all backed by PostgreSQL by default:" — Agent Server
📖 "Durability mode controls checkpoint frequency—async (default) writes after each step; exit stores only the final state." — Agent Server
🧪 实例 一个部署的三层数据落点:
flowchart LR
    G[Graph 蓝图] --> AS[Agent Server]
    AS -->|assistants/threads/runs/cron| PG[(PostgreSQL)]
    AS -->|checkpoints 短期记忆| PG
    AS -->|store 长期记忆| PG
    AS -->|信令/取消/流式 pub-sub| R[(Redis 仅临时数据)]
🔍 追问 为什么 Redis 里不存 run 数据? → 因为 Redis 只承担 API server 与 queue worker 之间的信令、取消和流式 pub/sub,存的是 ephemeral 数据;run 数据始终从 Postgres 读写,这样 Redis 挂掉不丢业务状态。
📚 拓展阅读
  • Agent Server — 本卡片的官方原文页
  • PostgreSQL — 默认持久化后端
  • Redis — 处理信令与流式 pub/sub 的组件
  • MongoDB — checkpoint 可切换的备选后端
QAgent Server 的容器架构中 API server 和 queue worker 有什么分工?一次 run 是怎样执行的?深挖·拓展🔥高频
容器架构 task-queue run 生命周期
⏱️ 现行
一个典型部署由两类长期运行的容器组成,它们由同一个 Docker 镜像构建,但职责严格分离。API server 负责处理客户端请求(创建 run、读取 thread 状态、流式返回结果),但自己不执行 agent 代码;queue worker 才是执行引擎,监听持久化 task queue、执行你的 graph 代码并写 checkpoint。二者是独立的容器池、独立扩缩容——API server 按请求量扩,queue worker 按待处理 run 数扩。run 的生命周期是:客户端请求打到 API server,后者在持久化队列里创建一个 pending run;某个 worker 领取该 run、拿到 lease、加载对应 graph 开始执行,队列强制约束同一个 thread 同一时刻最多只能执行 1 个 run(避免状态竞争);执行中 worker 按 durability mode 写 checkpoint 并通过 pub/sub 广播流式事件,若客户端开了 /stream 连接,API server 就订阅该 channel 并以 SSE 实时转发;执行完 worker 更新 run 状态、释放槽位。容量上每个 worker 默认并发处理最多 N_JOBS_PER_WORKER(默认 10)个 run,所以单个 worker 容器能并行服务很多 run。核心权衡是:容器无状态但持久,任何时刻至少要有 1 个 queue worker 在监听队列,否则 run 会成孤儿。
术语 API server(处理请求但不执行 agent 代码的容器); queue worker(执行 graph 代码、写 checkpoint 的执行引擎); lease(worker 领取 run 时获得的租约); N_JOBS_PER_WORKER(单 worker 并发 run 上限,默认 10); SSE(server-sent events,流式返回机制)
📖 "API servers handle client requests (creating runs, reading thread state, streaming results) but do not execute agent code themselves." — Agent Server
📖 "Queue workers are the execution engine. They listen to the durable task queue, execute your graph code, and write checkpoints." — Agent Server
📖 "The queue enforces that at most 1 run can be executed for a given thread at one time." — Agent Server
📖 "Each worker handles up to N_JOBS_PER_WORKER runs concurrently (default: 10), so a single worker container serves many runs in parallel." — Agent Server
🧪 实例 一次带流式的 run 请求流转:
flowchart TB
    User -->|request| API[API Server]
    API -->|create pending run| DB[(Postgres)]
    API -->|notify| Redis[(Redis)]
    Redis -->|wake| QL[Queue Loop]
    QL -->|claim next run| DB
    QL --> W[Worker 执行 graph]
    W -->|checkpoints/status| DB
    W -->|publish events| Redis
    Redis -->|stream events| API
    API -->|SSE response| User
🔍 追问 为什么 compiled graph 通常比 factory function 更推荐? → compiled graph 在容器启动时加载一次并复用,没有每次请求的编译开销;factory function 每次调用都会跑,只有需要按 assistant config 做 per-run 定制(如切换模型/工具)时才用,且要保持轻量。
📚 拓展阅读
QAgent Server 支持哪几种运行时部署模式?各适合什么场景?深挖·拓展中频
部署模式 扩展 自托管
⏱️ 现行
Agent Server 支持三种运行时配置,本质区别在于 API 与队列执行是否分离、以及编排与执行是否再拆分。Single host 模式下 API server 直接管理 task queue,没有独立的 queue worker,这是自托管部署的默认值,适合开发和低流量场景——简单但没有独立扩缩容能力。Split API and queue 模式用专门的 queue worker 在与 API server 分离的主机上执行 run,自托管时通过配置 queue.enabled: true 开启,好处是每层独立扩缩容:API server 按请求量、queue worker 按 pending run 数各自伸缩。Distributed runtime 进一步把 API 与队列进程分开的基础上,让编排(orchestration)与执行(execution)也用不同进程承担,用于高并发的大规模部署。权衡很清晰:从 single host 到 split 再到 distributed,运维复杂度递增、但可扩展性和隔离性也递增;上文描述的容器架构与 run 生命周期适用于 single host 和 split API and queue 两种配置。
术语 single host(API 直管队列、无独立 worker 的默认自托管模式); split API and queue(API 与 queue worker 分主机、独立扩缩容); distributed runtime(编排与执行再拆分,面向高并发大规模); queue.enabled(自托管开启 split 模式的配置项)
📖 "Agent Server supports three runtime configurations:" — Agent Server
📖 "The API server manages the task queue directly with no separate queue workers. This is the default for self-hosted deployments and is suitable for development and low-traffic use cases." — Agent Server
📖 "Use this for large-scale deployments with high concurrency requirements." — Agent Server
🧪 实例 自托管从开发到高并发的演进配置:
yaml
# 开发/低流量:single host(默认,无需额外配置)
# 生产扩展:split API and queue
queue:
  enabled: true   # 开启独立 queue worker,API 与执行分主机
# 大规模高并发:distributed runtime(编排进程 + 执行进程分离)
🔍 追问 split 模式为什么能提升吞吐? → 因为 API server 和 queue worker 成为独立容器池,可分别按请求量和待处理 run 数扩缩容,互不争抢资源,避免请求处理阻塞 run 执行。
📚 拓展阅读
Q如何把 LangSmith 的 prompt 同步到 GitHub?其 webhook 机制有什么要注意的?深挖·拓展中频
提示管理 webhook 版本控制
⏱️ 现行
LangSmith 提供协作界面来创建、测试和迭代 prompt。虽然运行时可以动态从 LangSmith 拉取 prompt,但你可能更想把 prompt 同步到自己的数据库或版本控制系统,为此 LangSmith 支持通过 webhook 接收 prompt 更新通知。机制上,当你在 LangSmith 保存对 prompt 的修改时,实际上是创建了一个新版本即 "Prompt Commit",这些 commit 就能触发 webhook。要注意 LangSmith 的 webhook 不直接和 GitHub 交互——它调用你自己搭建的中间服务器(指南里用一个简单的 FastAPI 应用),该服务器接收 HTTP POST、解析 JSON 格式的 prompt manifest,再用 GitHub PAT(需 repo scope)以编程方式提交到指定仓库。同步到 GitHub 的价值在于版本控制(prompt 与应用代码在熟悉的系统里一起版本化)和 CI/CD 集成(关键 prompt 变更时触发自动化部署)。一个关键陷阱是:prompt commit 的 webhook 通常在 workspace 级别触发,意味着 workspace 内任意 prompt 被修改并保存 commit,webhook 都会 fire 并发送该 prompt 的 manifest,payload 靠 prompt id 区分,所以你的接收服务器必须按此设计。提交时服务器还需先 GET 现有文件的 SHA 才能更新已存在的文件。
术语 Prompt Commit(保存 prompt 修改产生的新版本); webhook(prompt 更新通知机制); prompt manifest(webhook payload 里的 prompt 配置 JSON); PAT(GitHub Personal Access Token,需 repo scope); workspace 级别(webhook 触发粒度)
📖 "In LangSmith, when you save changes to a prompt, you're essentially creating a new version or a "Prompt Commit." These commits are what can trigger webhooks." — How to sync prompts with GitHub
📖 "LangSmith webhooks for prompt commits are generally triggered at the workspace level." — How to sync prompts with GitHub
📖 "* Version Control: Keep your prompts versioned alongside your application code in a familiar system." — How to sync prompts with GitHub
🧪 实例 webhook 接收端点的核心思路(FastAPI):
python
@app.post("/webhook/commit", status_code=201, tags=["GitHub Webhooks"])
async def handle_webhook_direct_commit(payload: WebhookPayload = Body(...)):
    # payload 内含 prompt_id / prompt_name / commit_hash / manifest
    # 先 GET 现有文件 SHA,再 PUT 提交 manifest 到指定分支
    github_response = await commit_manifest_to_github(payload)
    return {"message": "Webhook received and manifest committed directly to GitHub successfully."}
🔍 追问 除了直接 commit,这个中间服务器还能扩展做什么? → 可以做粒度化提交(拆分单个 prompt 文件)、触发 CI/CD 流水线、直接更新数据库/缓存、向 Slack/邮件发通知,或按 payload 元数据做选择性处理。
📚 拓展阅读
QLangSmith Deployment 有哪几种部署环境?如何根据数据驻留需求选型?深挖·拓展中频
LangSmith Deployment control-plane data-plane
⏱️ 现行
LangSmith Deployment 是专为 agent 工作负载打造的工作流编排运行时,提供 durable 执行、实时流式和水平扩展,覆盖从本地开发到生产部署的完整生命周期。它是框架无关的(framework-agnostic),可以部署 LangGraph/LangChain、Google ADK、Claude Agent SDK、Strands、CrewAI、AutoGen 等框架的 agent。选型的核心维度是:你希望 control plane 和 data plane(Agent Server 及其数据库)跑在哪里——所有基础设施类型都用同一个 Agent Server 运行时。四种环境:Cloud 由 LangChain 在 AWS/GCP 全托管,需 Plus 及以上计划;Self-hosted with control plane 把 control plane 和 Agent Server 都跑在你自己的 Kubernetes 集群里,需 Enterprise 计划,适合完全数据驻留或气隙(air-gapped)场景;Hybrid 由 LangChain 托管 control plane,而 Agent Server 及数据平面在你的基础设施里,适合"agent 在你 VPC、控制面托管";Standalone server 则用 Docker/Compose/Kubernetes 部署 Agent Server,自带 PostgreSQL、Redis 和 license,无 control plane,适合只要 agent 运行时的场景。权衡本质是数据驻留/合规 vs 运维负担:越靠 Cloud 越省心但数据出域,越靠 self-hosted/standalone 越可控但要自己运维数据库和集群。
术语 control plane(控制平面,管理部署); data plane(数据平面,即 Agent Server 及其数据库); framework-agnostic(框架无关,可部署多种 agent 框架); air-gapped(气隙/完全数据驻留场景); standalone server(自带依赖、无控制面的 Agent Server 部署)
📖 "Pick a environment based on where you want the control plane and data plane (Agent Servers and their databases) to run. All infrastructure types use the same Agent Server runtime." — LangSmith Deployment
📖 "LangSmith Deployment is framework-agnostic which means you can deploy agents built with:" — LangSmith Deployment
🧪 实例 按需求映射到环境的决策:
text
需求 → 选型
- 托管省心、agent 全托管         → Cloud(Plus+)
- agent 在自己 VPC、控制面托管   → Hybrid
- 完全数据驻留 / air-gapped      → Self-hosted with control plane(Enterprise)
- 只要 agent 运行时、无控制面     → Standalone Agent Server(自带 PG/Redis/license)
🔍 追问 Standalone server 与 Self-hosted with control plane 最大区别是什么? → Standalone 用 Docker/Compose/K8s 直接跑 Agent Server 容器、自带 PostgreSQL/Redis/license 且没有 control plane;而 Self-hosted with control plane 会在你自己的 Kubernetes 里同时运行 control plane 和 Agent Server,配套自托管 LangSmith,管理能力更完整。
📚 拓展阅读