零基础入门 LangChain 与 LangGraph(九):LangGraph 收官——运行时上下文、流式输出、子图、与项目结构

文章目录

    • [零基础入门 LangChain 与 LangGraph(九):LangGraph 收官------运行时上下文、流式输出、子图与项目结构](#零基础入门 LangChain 与 LangGraph(九):LangGraph 收官——运行时上下文、流式输出、子图与项目结构)
    • 一、学到这里,还差什么?
      • [1.1 会建图,不等于会做系统](#1.1 会建图,不等于会做系统)
      • [1.2 LangGraph 的能力分层](#1.2 LangGraph 的能力分层)
    • [二、运行时上下文:不是所有数据都应该塞进 State](#二、运行时上下文:不是所有数据都应该塞进 State)
      • [2.1 State 装不下所有东西](#2.1 State 装不下所有东西)
      • [2.2 三类上下文](#2.2 三类上下文)
      • [2.3 一个特别直观的例子:智能旅行规划助手](#2.3 一个特别直观的例子:智能旅行规划助手)
    • [三、LangGraph 里的 Runtime context 到底怎么用?](#三、LangGraph 里的 Runtime context 到底怎么用?)
      • [3.1 `context_schema`:给图定义一份"运行期依赖结构"](#3.1 context_schema:给图定义一份"运行期依赖结构")
      • [3.2 在节点里访问 `runtime.context`](#3.2 在节点里访问 runtime.context)
      • [3.3 为什么要把"用户身份"和"系统配置"放在 context 里?](#3.3 为什么要把"用户身份"和"系统配置"放在 context 里?)
        • [1. 不污染业务状态](#1. 不污染业务状态)
        • [2. 更容易做多用户隔离](#2. 更容易做多用户隔离)
        • [3. 更容易切换运行策略](#3. 更容易切换运行策略)
    • [四、工具里的 Runtime:把上下文、状态、持久化、流式输出串到一起](#四、工具里的 Runtime:把上下文、状态、持久化、流式输出串到一起)
      • [4.1 工具是系统边界](#4.1 工具是系统边界)
      • [4.2 `ToolRuntime` 给工具注入了什么?](#4.2 ToolRuntime 给工具注入了什么?)
      • [4.3 一个搜索工具为什么需要用户上下文?](#4.3 一个搜索工具为什么需要用户上下文?)
    • [五、Streaming:把 `graph.stream()` 拆到最细](#五、Streaming:把 graph.stream() 拆到最细)
      • [5.1 先定好一个图,后面所有例子都用它](#5.1 先定好一个图,后面所有例子都用它)
      • [5.2 `invoke` 和 `stream` 的区别](#5.2 invokestream 的区别)
      • [5.3 参数逐个看](#5.3 参数逐个看)
      • [5.4 updates](#5.4 updates)
      • [5.5 values](#5.5 values)
      • [5.6 messages](#5.6 messages)
      • [5.7 custom](#5.7 custom)
      • [5.8 checkpoints 和 tasks](#5.8 checkpoints 和 tasks)
      • [5.9 debug](#5.9 debug)
      • [5.10 同步和异步](#5.10 同步和异步)
      • [5.11 模板](#5.11 模板)
    • 六、Subgraphs:复杂系统的拆分方式
    • 七、项目结构:当图开始变成应用
      • [7.1 LangGraph 应用有一套标准结构](#7.1 LangGraph 应用有一套标准结构)
      • [7.2 `langgraph.json`:只需搞清楚三件事](#7.2 langgraph.json:只需搞清楚三件事)
    • [八、CLI:`dev`、`up`、`build` 各自干什么](#八、CLI:devupbuild 各自干什么)
      • [8.1 CLI 是 Agent Server 的本地入口](#8.1 CLI 是 Agent Server 的本地入口)
      • [8.2 `dev` 和 `up` 不是一回事](#8.2 devup 不是一回事)
    • [九、Agent Server、Studio、自托管:把这些概念连起来](#九、Agent Server、Studio、自托管:把这些概念连起来)
      • [9.1 Agent Server 是什么](#9.1 Agent Server 是什么)
      • [9.2 Studio 不只是可视化界面](#9.2 Studio 不只是可视化界面)
      • [9.3 自托管:最轻量的方式是 Standalone Server](#9.3 自托管:最轻量的方式是 Standalone Server)
    • 十、本篇总结

零基础入门 LangChain 与 LangGraph(九):LangGraph 收官------运行时上下文、流式输出、子图与项目结构

💬 开篇 :前面两篇已经把 LangGraph 最核心的地基搭起来了。第一篇解决的是"图怎么建",也就是 StateNodeEdge、工作流、Agent 这些最基础的结构问题;第二篇解决的是"图怎么长期活着跑",也就是持久化、记忆、人机交互和时间旅行。

但真正到了想做项目的时候,还有最后一层能力必须补齐:

一个图,怎么把用户身份、配置信息、数据库连接 这种运行期依赖带进来?怎么把执行过程流式暴露 出来?怎么把复杂系统拆成子图来开发?最后又怎么把整个 LangGraph 应用组织成一个真正可运行、可调试、可部署的项目?

👍 这一篇要解决的问题

如果说前面两篇让我学会了"会写图",那这一篇要解决的就是:怎么让图变成真正的系统

🚀 这一篇的目标:LangGraph 剩下最重要的四个主题:

  • 运行时上下文(Runtime context)
  • 流式输出(Streaming)
  • 子图(Subgraphs)
  • 综合项目案例与部署思路

到这一步,LangGraph 对我们来说就不再只是"一个会画流程图的库",而真正变成了一个可以拿来组织复杂 AI 系统的底层运行时。官方文档现在对它的定位也很明确:它是一个低层的 agent orchestration framework,重点支持 durable execution、streaming、human-in-the-loop 和 persistence;而 LangChain 上层 agent 本身就是建立在 LangGraph 之上的。(LangChain 参考文档)


一、学到这里,还差什么?

1.1 会建图,不等于会做系统

前面其实已经能做很多事了:能定义 State,能拆 Node,能连 Edge,能做条件分支,能做循环,能加 checkpointer,能做人机交互,能做时间旅行。如果只写 Demo,到这里已经很强了。

但只要把它往真实项目靠,就会马上碰到几个新问题。

当前这次执行的"静态信息"放哪? 比如用户 ID、语言偏好、当前会员等级、数据库连接、默认系统提示词......这些东西明明很重要,但它们又不是 State 那种会在流程里不断变化的数据。

系统执行过程怎么让前端或调用方"看见"? 不能总是等整个图跑完了,才把一个最终结果塞回去。很多时候需要中间进度、节点状态、LLM token,甚至工具调用的详细阶段信息。

复杂逻辑怎么拆? 一旦系统开始变大,所有逻辑全堆在一个图里很快就会失控。这时候必须要有"图中套图"的能力,也就是子图。

最后怎么把它真的跑成服务? 本地写几个 .py 文件可以,真正变成可测试、可部署、可维护的 Agent Server,又是另一层问题。

所以如果说前面两篇解决的是"图怎么表达复杂任务",那这一篇解决的其实是"图怎么变成可运行、可观察、可模块化、可部署的系统"。


1.2 LangGraph 的能力分层

写到这里,对 LangGraph 的理解已经越来越像一个分层结构了:
图结构能力
状态与路由
持久化与记忆
运行时上下文
流式输出与调试
子图与模块化
项目化与部署

这几层不是并列的,而是一层层往上长出来的:图结构能力解决"怎么建图",持久化与记忆解决"怎么活着跑",运行时上下文解决"怎么把外部依赖带进来",流式输出解决"怎么把执行过程暴露出来",子图解决"怎么拆复杂系统",项目化与部署解决"怎么交付出去"。


二、运行时上下文:不是所有数据都应该塞进 State

2.1 State 装不下所有东西

一开始你可能会这么想:既然 LangGraph 是"共享状态图",那干脆把所有信息都塞进 State 不就行了?

这其实是个坏习惯。State 更适合存放的是在流程里会被节点不断读取和更新的数据------随着执行推进而变化、对当前任务结果有直接影响的中间状态。

但还有另一类东西,虽然也很重要,却不应该硬塞进 State:用户 ID、当前语言、会员等级、数据库连接、API 密钥、本次运行用哪个模型、系统级配置......这些东西要么在本次执行中基本不变,要么本质上属于"运行依赖",而不是"任务状态"。


2.2 三类上下文

类型 是否会变 生命周期 典型例子 更适合放哪
静态运行时上下文 基本不变 单次运行有效 用户 ID、语言、数据库连接、模型配置 context
动态运行时上下文 会变化 单次运行有效 消息历史、中间结果、路由判断 State
动态跨会话上下文 会变化 跨会话长期存在 用户预算偏好、历史订单、长期画像 Store

context 是本次运行的"静态依赖",State 是本次运行的"动态状态",Store 是跨会话长期保存的"动态记忆"。前两篇已经把 StateStore 讲得差不多了,这一篇要补上的正是这个"静态运行时上下文"。


2.3 一个特别直观的例子:智能旅行规划助手

如果要做一个智能旅行规划助手,它可能需要这些信息:根据用户语言返回中文或英文回答、根据会员等级决定是否提供高级推荐、查询旅游数据库、按当前季节推荐活动、记住用户历史偏好。

数据 应该放哪里 原因
数据库连接 context 本次运行要用,但不属于业务状态
用户语言 context 单次执行中基本固定
用户会员等级 context 单次执行中基本固定
当前季节 context 运行中固定配置
当前对话历史 State 会随着流程推进不断变化
用户长期偏好 Store 要跨会话保留

这样就不会再把"状态"和"依赖"混为一谈。


三、LangGraph 里的 Runtime context 到底怎么用?

3.1 context_schema:给图定义一份"运行期依赖结构"

在当前 LangGraph 里,StateGraph 本身就支持 context_schema 参数,用来定义 run-scoped context 的结构;而早期的 config_schema 已经被标记为废弃,官方明确建议改用 context_schema。(LangChain 参考文档)

把它理解成:给这张图声明一份"运行时依赖对象"的类型。

最常见的写法是 dataclass

python 复制代码
from dataclasses import dataclass

@dataclass
class AppContext:
    user_id: str
    language: str = "zh"
    model_provider: str = "openai"
    system_message: str = "你是一个乐于助人的助手。"

然后建图时传进去:

python 复制代码
from langgraph.graph import StateGraph

builder = StateGraph(
    AppState,
    context_schema=AppContext
)

就相当于告诉这张图:除了 State,这次执行还会收到一份额外的运行上下文,里面包含用户 ID、语言、模型提供商、系统消息这些信息。这个设计非常像依赖注入。


3.2 在节点里访问 runtime.context

一旦图声明了 context_schema,节点里就可以通过 runtime 参数访问:

python 复制代码
from langgraph.runtime import Runtime

def greet_user(state: AppState, runtime: Runtime[AppContext]):
    greeting = "Hello" if runtime.context.language == "en" else "你好"
    user_name = state.get("user_name", "Guest")
    return {"messages": [f"{greeting}, {user_name}!"]}

调用时传入:

python 复制代码
graph.invoke(
    {"user_name": "Alice"},
    context={"user_id": "123", "language": "en"}
)

特别要注意:context 不是图里自动累积的状态,它是你在一次 invoke() / stream() 调用时显式带进去的运行参数。这和 thread_id 加载历史状态完全不是一个概念。


3.3 为什么要把"用户身份"和"系统配置"放在 context 里?

1. 不污染业务状态

有些数据虽然重要,但不属于"任务状态"。把它们放到 State 里,会让图的状态结构越来越脏。

2. 更容易做多用户隔离

user_id 放进 context 之后,节点就可以用它来构造:

python 复制代码
namespace = (user_id, "preferences")

从而自然拿到当前用户自己的长期记忆。

3. 更容易切换运行策略

同一张图,可以在不同调用里传 language="zh"language="en",或者 model_provider="openai"model_provider="anthropic",而图本身逻辑不需要改。这就像配置驱动,而不是代码驱动。


四、工具里的 Runtime:把上下文、状态、持久化、流式输出串到一起

4.1 工具是系统边界

在 LangGraph 里,工具一旦开始接外部 API、数据库、检索系统、订单系统,它就变成了图和外部世界的边界。而一旦是边界,就必然需要上下文信息:这个用户是谁、有没有权限、当前图里的状态是什么、要不要把结果写进 Store、要不要把进度流式发出去。

工具真正重要的不是"会不会调",而是"调的时候知不知道自己在什么上下文里"。


4.2 ToolRuntime 给工具注入了什么?

官方文档对 ToolRuntime 的定位写得很清楚:在工具内部,可以通过 ToolRuntime 访问 contextstorestream_writer;在 LangGraph 的 ToolNode 参考里还额外包含 configstatetool_call_id 这些工具调用级信息。(LangChain 文档)

一个工具签名可以长这样:

python 复制代码
from langchain.tools import tool, ToolRuntime

@tool
def get_user_info(runtime: ToolRuntime[AppContext]) -> str:
    user_id = runtime.context.user_id
    user_name = runtime.state["user_name"]
    return f"user_id={user_id}, user_name={user_name}"

工具可以读 context,可以读当前 State,可以读写 Store,还能把自定义进度写到流里。这一下,工具就不再是"黑盒调用",而变成了图系统里一个真正有上下文感知能力的执行单元。


4.3 一个搜索工具为什么需要用户上下文?

搜索工具可能需要按用户权限过滤结果、记录是哪个用户发起了这次调用、基于用户偏好做个性化排序,或者结合当前图状态决定查询策略:

python 复制代码
@tool
def search_weather(runtime: ToolRuntime[Context]) -> str:
    user_id = runtime.context.user_id
    user_name = runtime.state["user_name"]
    print(f"日志:user_id={user_id}, user_name={user_name} 发起天气搜索")
    return "西安今天晴天,15-20度"

工具不是无状态公共函数,而是带运行语境的执行动作。


五、Streaming:把 graph.stream() 拆到最细

5.1 先定好一个图,后面所有例子都用它

python 复制代码
from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    topic: str
    joke: str

def refine_topic(state: State):
    return {"topic": state["topic"] + " and cats"}

def generate_joke(state: State):
    return {"joke": f"This is a joke about {state['topic']}"}

graph = (
    StateGraph(State)
    .add_node(refine_topic)
    .add_node(generate_joke)
    .add_edge(START, "refine_topic")
    .add_edge("refine_topic", "generate_joke")
    .add_edge("generate_joke", END)
    .compile()
)

执行顺序:

text 复制代码
START → refine_topic → generate_joke → END

初始输入:

python 复制代码
inputs = {"topic": "ice cream", "joke": ""}

refine_topic 把 topic 改成 "ice cream and cats"generate_joke 根据 topic 生成笑话。


5.2 invokestream 的区别

python 复制代码
result = graph.invoke(inputs)

图跑完才返回,只能看到最终结果:

python 复制代码
{"topic": "ice cream and cats", "joke": "This is a joke about ice cream and cats"}

stream 让图每跑一步就吐一条数据,执行过程全部可见:

python 复制代码
for chunk in graph.stream(inputs, stream_mode="updates", version="v2"):
    print(chunk)

5.3 参数逐个看

stream_mode

最核心的参数,控制你能看到什么:

python 复制代码
stream_mode="updates"                       # 单个
stream_mode=["updates", "messages"]         # 多个同时开

全部可选值:

模式 看什么
updates 每步节点返回的状态增量
values 每步后的完整状态快照
messages LLM token 流
custom 节点里手动发出的自定义数据
checkpoints 状态保存事件(需要 checkpointer)
tasks 节点开始 / 结束 / 报错事件(需要 checkpointer)
debug 所有调试信息

日常用到的主要是前四个。


version="v2"

强烈建议固定写上。v1 的输出格式会随 stream_mode 数量和是否开 subgraphs 而变化;v2 统一成 StreamPart 结构:

python 复制代码
{
    "type": "updates",  # 流类型
    "ns": (),           # 命名空间,子图用
    "data": {...}       # 实际数据
}

这样不管开几种模式,处理逻辑都一样:

python 复制代码
if chunk["type"] == "updates":
    print(chunk["data"])

subgraphs=True

用不到子图就不管。开了之后,chunk["ns"] 是空元组表示来自父图,有内容则来自子图:

python 复制代码
# 父图
{"type": "updates", "ns": (), "data": {...}}
# 子图
{"type": "updates", "ns": ("sub_graph:xxxx",), "data": {...}}

config

checkpointstasks 这两种模式需要传,用来标识当前会话线程:

python 复制代码
config = {"configurable": {"thread_id": "1"}}

其他模式不需要。


5.4 updates

最适合调试节点返回值。

python 复制代码
for chunk in graph.stream(inputs, stream_mode="updates", version="v2"):
    if chunk["type"] == "updates":
        print(chunk["data"])

输出:

python 复制代码
{"refine_topic": {"topic": "ice cream and cats"}}
{"generate_joke": {"joke": "This is a joke about ice cream and cats"}}

结构是 {节点名: {改动的字段: 新值}},只含这一步变化的部分,不含其他字段。


5.5 values

每步执行后的完整状态快照。

python 复制代码
for chunk in graph.stream(inputs, stream_mode="values", version="v2"):
    if chunk["type"] == "values":
        print(chunk["data"])

输出:

python 复制代码
{"topic": "ice cream", "joke": ""}
{"topic": "ice cream and cats", "joke": ""}
{"topic": "ice cream and cats", "joke": "This is a joke about ice cream and cats"}

updates 放在一起跑对照最清楚:

python 复制代码
for chunk in graph.stream(inputs, stream_mode=["updates", "values"], version="v2"):
    print(f"[{chunk['type']}]", chunk["data"])

输出:

python 复制代码
[values]  {"topic": "ice cream", "joke": ""}
[updates] {"refine_topic": {"topic": "ice cream and cats"}}
[values]  {"topic": "ice cream and cats", "joke": ""}
[updates] {"generate_joke": {"joke": "This is a joke about ice cream and cats"}}
[values]  {"topic": "ice cream and cats", "joke": "This is a joke about ice cream and cats"}

调试节点返回值用 updates,看全局状态变化用 values


5.6 messages

专门用来看 LLM 生成 token 的过程。改一下图加上模型调用:

python 复制代码
from langchain.chat_models import init_chat_model

model = init_chat_model(model="gpt-4o-mini")

def generate_joke(state: State):
    response = model.invoke([
        {"role": "user", "content": f"Write a short joke about {state['topic']}"}
    ])
    return {"joke": response.content}

流式接收:

python 复制代码
for chunk in graph.stream(inputs, stream_mode="messages", version="v2"):
    if chunk["type"] == "messages":
        msg, metadata = chunk["data"]
        print(msg.content, end="", flush=True)

输出(token 逐个流出):

text 复制代码
Why| did| the| ice| cream| bring| a| cat|?| Because| it| wanted| a| purr|-fect| scoop|!

chunk["data"] 是个二元组 (message_chunk, metadata)metadata 长这样:

python 复制代码
{
    "langgraph_node": "generate_joke",
    "tags": ["joke"],
    ...
}
按节点或标签过滤

一个图里有多个 LLM 节点时,可以只盯住某一个:

python 复制代码
if metadata.get("langgraph_node") == "generate_joke":
    print(msg.content, end="", flush=True)

也可以在初始化模型时加 tag,再按 tag 过滤:

python 复制代码
model = init_chat_model(model="gpt-4o-mini", tags=["joke"])

if "joke" in metadata.get("tags", []):
    print(msg.content, end="", flush=True)

并行图里同时跑多个模型、前端要分区域展示不同输出时,这两个过滤就是必须的。


5.7 custom

messages 是模型自己吐的 token,custom 是你在节点里主动往流里塞的数据。

python 复制代码
from langgraph.config import get_stream_writer

def search_node(state: State):
    writer = get_stream_writer()
    writer({"step": "start", "msg": "开始搜索"})
    # ... 实际搜索逻辑 ...
    writer({"step": "done", "msg": "搜索完成"})
    return {"result": "搜索结果"}

外面接收:

python 复制代码
for chunk in graph.stream(inputs, stream_mode="custom", version="v2"):
    if chunk["type"] == "custom":
        print(chunk["data"])

输出:

python 复制代码
{"step": "start", "msg": "开始搜索"}
{"step": "done", "msg": "搜索完成"}

你发什么它就流什么。进度条、工具调用日志、数据库查询状态都可以这样做,是前后端实时联调最直接的手段

get_stream_writer() 必须在节点执行期间调用,不能在图定义阶段用。


5.8 checkpoints 和 tasks

这两种模式都需要 checkpointer,普通图用不到。先给图加上:

python 复制代码
from langgraph.checkpoint.memory import MemorySaver

graph = (
    StateGraph(State)
    ...
    .compile(checkpointer=MemorySaver())
)

config = {"configurable": {"thread_id": "1"}}

checkpoints :每次状态被持久化保存时触发一条事件,格式和 get_state() 输出相同。多轮对话、断点恢复才会关心它,普通应用可以忽略。

tasks:节点生命周期日志。节点开始时:

python 复制代码
{"name": "refine_topic", "input": {"topic": "ice cream", "joke": ""}, ...}

节点结束时:

python 复制代码
{"name": "refine_topic", "result": {"topic": "ice cream and cats"}, "error": None}

报错时 error 字段有内容。适合做执行监控,能知道哪个节点挂了。


5.9 debug

debug 包含 tasks、checkpoints 以及额外的内部事件,是信息量最大的模式,同时也最乱。只在深度排错时开,平时别用。官方文档也建议:如果只需要部分信息,直接用 checkpointstasks 更清晰。


5.10 同步和异步

graph.stream() 是同步版,graph.astream() 是异步版,参数完全一样:

python 复制代码
# 同步
for chunk in graph.stream(inputs, stream_mode="updates", version="v2"):
    print(chunk)

# 异步(FastAPI 等异步场景用)
async for chunk in graph.astream(inputs, stream_mode="updates", version="v2"):
    print(chunk)

先把同步版搞熟,写异步服务时换成 astream 就行。


5.11 模板

python 复制代码
for chunk in graph.stream(
    inputs,
    stream_mode=["updates", "values", "messages", "custom"],
    version="v2",
):
    if chunk["type"] == "updates":
        print("[节点更新]", chunk["data"])

    elif chunk["type"] == "values":
        print("[完整状态]", chunk["data"])

    elif chunk["type"] == "messages":
        msg, metadata = chunk["data"]
        print("[token]", msg.content, end="", flush=True)

    elif chunk["type"] == "custom":
        print("[自定义]", chunk["data"])

逻辑只有一条:每个 chunk,用 chunk["type"] 判断类型,用 chunk["data"] 拿数据。

stream_mode 看什么
updates 节点返回了什么
values 当前完整 state
messages LLM token
custom 自己发的进度/日志
checkpoints 状态保存事件
tasks 节点开始/结束/报错
debug 所有调试信息

Streaming 的本质不是"让模型流式输出",而是"让整个图的执行过程对你可见"。


六、Subgraphs:复杂系统的拆分方式

6.1 先看一个具体的子图是什么样的

子图就是一个独立编译的图,可以被另一个图当作节点来使用。

先定义一个子图,专门负责"润色笑话":

python 复制代码
from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class SubState(TypedDict):
    joke: str

def polish_joke(state: SubState):
    return {"joke": state["joke"] + " (polished!)"}

def add_punchline(state: SubState):
    return {"joke": state["joke"] + " Ba dum tss!"}

subgraph = (
    StateGraph(SubState)
    .add_node(polish_joke)
    .add_node(add_punchline)
    .add_edge(START, "polish_joke")
    .add_edge("polish_joke", "add_punchline")
    .add_edge("add_punchline", END)
    .compile()
)

子图本身就是完整的图,可以单独跑:

python 复制代码
subgraph.invoke({"joke": "Why did the cat sit on the computer?"})
# {"joke": "Why did the cat sit on the computer? (polished!) Ba dum tss!"}

然后是父图,负责生成笑话,子图负责润色:

python 复制代码
class ParentState(TypedDict):
    topic: str
    joke: str

def generate_joke(state: ParentState):
    return {"joke": f"Why did the {state['topic']} sit on the computer?"}

parent_graph = (
    StateGraph(ParentState)
    .add_node(generate_joke)
    .add_node("polish", subgraph)
    .add_edge(START, "generate_joke")
    .add_edge("generate_joke", "polish")
    .add_edge("polish", END)
    .compile()
)

执行:

python 复制代码
parent_graph.invoke({"topic": "cat", "joke": ""})
# {"topic": "cat", "joke": "Why did the cat sit on the computer? (polished!) Ba dum tss!"}

执行顺序:

text 复制代码
START → generate_joke → [polish_joke → add_punchline] → END
                         ↑ 这两个节点在子图里 ↑

6.2 两种接入方式,看状态是否有共享字段

方式一:直接当节点加(共享状态字段)

上面那个例子就是这种。父图有 joke 字段,子图也有 joke 字段,两者共享这个 key。

python 复制代码
builder.add_node("polish", subgraph)

LangGraph 会自动把父图 state 里和子图 state 匹配的字段传进去,子图跑完再把结果合并回父图 state。条件只有一个:父子图之间必须有至少一个同名的状态字段。 官方文档明确说明,这种情况下可以直接把编译后的子图作为节点加入父图。(LangChain 文档)


方式二:包在节点函数里调用(状态结构不同)

如果父图和子图状态字段完全不同,就需要自己做转换:

python 复制代码
class ParentState(TypedDict):
    topic: str
    result: str

class SubState(TypedDict):
    joke: str
    polished: str

def run_subgraph(state: ParentState):
    sub_input = {"joke": f"A joke about {state['topic']}", "polished": ""}
    sub_output = subgraph.invoke(sub_input)
    return {"result": sub_output["polished"]}

这个包装节点对父图来说就是个普通函数,父图完全感知不到子图的存在。

场景 用哪种
父子图有共享字段 直接 add_node("name", subgraph)
父子图状态完全不同 包在节点函数里,手动做转换
需要对子图输出做加工再写回父图 包在节点函数里

6.3 子图流式输出:subgraphs=True

普通 stream 只能看到父图输出,子图内部节点更新是看不到的。加上 subgraphs=True 才能把子图的过程也流出来:

python 复制代码
for chunk in parent_graph.stream(
    {"topic": "cat", "joke": ""},
    stream_mode="updates",
    subgraphs=True,
    version="v2",
):
    print(chunk)

输出:

python 复制代码
{"type": "updates", "ns": (), "data": {"generate_joke": {"joke": "Why did the cat sit on the computer?"}}}
{"type": "updates", "ns": ("polish:xxxx",), "data": {"polish_joke": {"joke": "Why did the cat sit on the computer? (polished!)"}}}
{"type": "updates", "ns": ("polish:xxxx",), "data": {"add_punchline": {"joke": "Why did the cat sit on the computer? (polished!) Ba dum tss!"}}}
{"type": "updates", "ns": (), "data": {"polish": {"joke": "Why did the cat sit on the computer? (polished!) Ba dum tss!"}}}

区分来源只看 chunk["ns"]

python 复制代码
for chunk in parent_graph.stream(inputs, stream_mode="updates", subgraphs=True, version="v2"):
    if chunk["ns"] == ():
        print("[父图]", chunk["data"])
    else:
        print("[子图]", chunk["ns"], chunk["data"])

6.4 checkpointer 怎么传

子图的 compile()checkpointer 参数有三种写法:(LangChain 文档)

写法 行为
不传(默认) 子图状态被纳入父图的 checkpoint,跟着父图一起持久化
checkpointer=True 子图使用父图传入的 checkpointer,支持跨调用的子图状态恢复
checkpointer=False 子图完全不做检查点,当普通函数跑

大多数场景用默认就够了。子图本身需要跨多次调用保留状态才用 True,想完全跳过检查点开销才用 False


6.5 中断发生在子图里时要注意什么

父图配了 checkpointer,子图里的节点就也可以触发中断。中断后查看状态时,需要加 subgraphs=True 才能看到子图的当前状态:

python 复制代码
state = parent_graph.get_state(config, subgraphs=True)

恢复执行时有一个要注意的地方:被中断的节点恢复后会从头重新执行,不是接着中断前的进度跑。子图里触发中断,外层调用子图的那个父节点也可能跟着重跑。

实际影响是:如果节点在中断前做了有副作用的操作(比如写数据库、发通知),恢复一次就可能重复触发一次。处理方式:中断前的操作尽量幂等,或者把有副作用的步骤拆成独立节点放到中断之后。


七、项目结构:当图开始变成应用

7.1 LangGraph 应用有一套标准结构

当图逻辑复杂起来,随手堆 .py 文件就会越来越乱。官方对可部署应用的结构要求很明确,至少包含四个东西:图本身、langgraph.json 配置文件、依赖文件(pyproject.tomlrequirements.txt),以及可选的 .env 环境变量文件。(LangChain 文档)

一个典型的 Python 项目结构:

bash 复制代码
my-app/
├── .env
├── langgraph.json
├── pyproject.toml
└── src/
    └── agent/
        ├── __init__.py
        ├── graph.py        # 主图
        ├── state/          # 状态定义
        ├── nodes/          # 节点逻辑
        └── subgraphs/      # 各子图

state 放状态定义,nodes 放节点函数,子图单独拆文件,主图负责组装。这和普通软件工程的分层逻辑是一样的。


7.2 langgraph.json:只需搞清楚三件事

langgraph.json 是部署应用的核心配置入口。字段虽然不少,但初学阶段最关键的只有三个:(LangChain 文档)

依赖从哪来:

json 复制代码
"dependencies": ["."]

哪些图要暴露出来:

json 复制代码
"graphs": {
    "main_agent": "./src/agent/graph.py:graph"
}

格式是 图的名字: 文件路径:变量名,可以同时注册多个图。

环境变量怎么加载:

json 复制代码
"env": ".env"

其余字段(storecheckpointerauthwebhooks 等)等真正需要时再研究,不要一开始就全铺上。


八、CLI:devupbuild 各自干什么

8.1 CLI 是 Agent Server 的本地入口

LangGraph CLI 是本地构建和运行 Agent Server 的命令行工具,生成的服务会暴露 runs、threads、assistants 等 API,并内置 checkpointing 和 storage 支持。官方当前列出的核心命令是:langgraph devlanggraph uplanggraph buildlanggraph dockerfilelanggraph deploy。(LangChain 文档)

按用途分三层记:

阶段 命令 用途
开发 langgraph dev 轻量启动,不需要 Docker,适合频繁改代码
验证 langgraph up 起完整 Docker 栈,接近生产环境
交付 langgraph build / dockerfile / deploy 打镜像、导出 Dockerfile、部署到 LangSmith

8.2 devup 不是一回事

官方专门有一页对这两个命令做了对比,差异很明确:(LangChain 文档)

langgraph dev 不需要 Docker,默认端口 2024,启动快,适合日常开发迭代。

langgraph up 需要 Docker,默认端口 8123,会真正起 PostgreSQL 和 Redis,资源开销更高,适合阶段性验证"这套东西在 Docker 环境里能不能正常跑"。

推荐工作流:

text 复制代码
日常写代码 → langgraph dev
阶段验证   → langgraph up
正式发布   → langgraph deploy

九、Agent Server、Studio、自托管:把这些概念连起来

9.1 Agent Server 是什么

Agent Server 是 LangGraph 应用的服务化运行时,建立在 assistants、threads、runs 这些概念之上,并内置 persistence 和任务队列。官方说得很直白:部署 Agent Server 时,你部署的不只是图本身,还包括持久化数据库任务队列 。(LangChain 文档)

它实际上解决了一个图单独跑解决不了的问题:

text 复制代码
接请求 → 管线程 → 管状态 → 持久化 → 管执行 → 管队列

可以把它理解成"专为 agent 应用设计的一层服务运行时"。


9.2 Studio 不只是可视化界面

Studio 可以连接本地或已部署的 Agent Server,支持可视化执行图、检查每个 checkpoint 的状态、逐步调试、修改中间状态,并从历史 checkpoint 分支重放。(LangChain 文档)

如果前面的中断、持久化、时间旅行已经搞清楚了,Studio 就很好理解------它只是把这些能力做成了一个可交互的界面:调图逻辑时当可视化调试工具,查某次执行的 checkpoint 状态,做时间旅行和分支重放。


9.3 自托管:最轻量的方式是 Standalone Server

如果不想走 LangSmith 控制平面,最轻量的自托管方式是直接部署 Standalone Agent Server,可以用 Docker、Docker Compose 或 Kubernetes,不经过 LangSmith UI,适合少量 agent 的独立部署场景。(LangChain 文档)

这种方式对后端开发者来说很直观:一台服务器、起一个服务、自己管环境变量和基础设施,它就是一个独立微服务。

自托管时 PostgreSQL 和 Redis 经常一起出现,原因很明确:(LangChain 文档) PostgreSQL 负责持久化,存 checkpoints、threads、runs 等数据;Redis 负责任务队列和实时事件。这两个不是可选的点缀,而是支撑 checkpointing、streaming、任务调度这些运行时能力的基础设施。看到数据库连接字符串、Redis URI 出现在 .env 里,就知道它们在干什么了。


十、本篇总结

这一篇补上的是 LangGraph 最后一层能力,把前面所有东西串成了一个完整系统。

不是所有数据都应该放进 StateState 适合放动态业务状态,context 适合放本次运行的静态依赖,Store 适合放跨会话长期记忆。当前 StateGraph 已经明确支持 context_schema,而旧的 config_schema 已被废弃。(LangChain 参考文档)

ToolRuntime 让工具拿到 contextstatestorestream_writer 以及调用级信息,工具从此不再只是一个无状态函数,而是带运行语境的执行动作。(LangChain 文档)

Streaming 不只是 token 流,而是图执行过程的实时可观察性。最常用的是 valuesupdatesmessagescustom,显式使用 version="v2" 会让输出结构统一成 StreamPart,对子图和多模式输出尤其友好。(LangChain 文档)

子图是复杂系统走向模块化的关键。父子图状态不同时用"节点里调用子图",共享状态键时直接把子图当节点加进去。(LangChain 文档)

如果用一句话给整个 LangGraph 总结:

LangGraph 真正强的,不是"能让模型多说几句话",而是它把 AI 应用从一段调用模型的脚本,提升成了一个有状态、可恢复、可观察、可模块化、可部署的系统。

相关推荐
火车叼位1 小时前
planning-with-files 完全指南:从入门到高阶,规范你的 AI 编程智能体
agent·vibecoding
新知图书1 小时前
LangGraph 基础图创建思路
人工智能·agent·智能体·langgraph·langchian
倦王2 小时前
langchain 尚硅谷day4-5 记忆缓存部分!
langchain
Gauss松鼠会2 小时前
效率起飞!GaussDB 管理平台(TPOPS)升级指南
服务器·数据库·性能优化·gaussdb·经验总结
m0_740352422 小时前
Layui如何解决表单select下拉框在移动端点击没反应
jvm·数据库·python
qq_392690662 小时前
Scikit-learn怎么实现协同过滤推荐_利用NearestNeighbors找相似用户
jvm·数据库·python
dfdfadffa2 小时前
C#怎么使用TopLevel顶级语句 C#顶级语句怎么写如何省略Main方法简化控制台程序【语法】
jvm·数据库·python
qq_413502022 小时前
Workerman vs Swoole:2026高性能PHP框架怎么选?
jvm·数据库·python
zjy277772 小时前
PHP源码对声卡有依赖吗_音频硬件无关性说明【方法】
jvm·数据库·python