阶段4(上):LangGraph、Function Calling、MCP(SDK 2.x)、Agent 评测与运维、Agent 设计专题;run_examples 设置 RUN_EXAMPLES=1
This commit is contained in:
1 parent
6650f7eb6d
commit
caaec830b3
38 files changed
+3515
-64
No files matched your search
@@ -0,0 +1,110 @@
|
||||
---
|
||||
id: ai.langgraph.state-graph
|
||||
title: LangGraph 状态图:状态、节点、边、条件路由和流式输出
|
||||
type: lesson
|
||||
domain: ai
|
||||
stack: [langgraph]
|
||||
tags: [LangGraph, StateGraph, reducer, 条件边, 流式输出, stream_mode]
|
||||
level: intermediate
|
||||
prerequisites: [ai.function-calling.tool-loop]
|
||||
est_hours: 3
|
||||
related: [ai.langgraph.persistence-hitl, ai.agent-design.react-planning, ai.langgraph.interview]
|
||||
week: [11]
|
||||
order: 1
|
||||
---
|
||||
|
||||
# LangGraph 状态图:状态、节点、边、条件路由和流式输出
|
||||
|
||||
> 基于 langgraph 1.2 编写,API 变化较快,以官方文档为准:https://docs.langchain.com/oss/python/langgraph/overview
|
||||
|
||||
## 解释说明
|
||||
|
||||
LangGraph 把一个 Agent / 工作流写成**有状态的图**:
|
||||
|
||||
| 概念 | 说明 |
|
||||
| --- | --- |
|
||||
| **State(状态)** | 一个 `TypedDict`(或 pydantic 模型),所有节点共享。节点返回“要更新的字段”,而不是整个状态 |
|
||||
| **Reducer(合并函数)** | 决定字段怎么更新。默认覆盖;`Annotated[list, operator.add]` 表示追加;消息列表常用 `add_messages`(按消息 id 追加或替换) |
|
||||
| **Node(节点)** | 普通函数:输入状态,返回部分更新。可以调用模型、工具、数据库 |
|
||||
| **Edge(边)** | 固定边 `add_edge(a, b)`;条件边 `add_conditional_edges(a, 路由函数)`,路由函数返回下一个节点名 |
|
||||
| **START / END** | 入口和出口 |
|
||||
| **compile** | 编译成可执行对象,支持 `invoke` / `stream` / `ainvoke` / `astream`;传入 `checkpointer` 后支持持久化(下一课) |
|
||||
|
||||
为什么不直接写 while 循环?手写循环(第 10 周)能帮你理解原理;但当流程有分支、并行、人工审批、失败续跑时,LangGraph 把**状态保存、恢复、流式事件、可视化**都提供好了。
|
||||
|
||||
### 流式输出的几种模式
|
||||
|
||||
| `stream_mode` | 输出内容 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `"values"` | 每一步之后的完整状态 | 调试 |
|
||||
| `"updates"` | 每个节点返回的更新 | 前端展示“当前在哪一步” |
|
||||
| `"messages"` | 大模型逐 token 输出 | 打字机效果 |
|
||||
| `"custom"` | 节点里用 `get_stream_writer()` 主动发送的任意数据 | 进度条、中间结果 |
|
||||
|
||||
可以同时订阅多种:`stream_mode=["updates", "custom"]`,返回 `(模式, 数据)` 元组。后端把这些事件转成 SSE 发给前端,就实现了“AI 原生”界面里的流式输出 + 任务进度。
|
||||
|
||||
## 案例
|
||||
|
||||
工单处理流程:分类 → 起草回复 → 金额大的退款需要人工审批 → 发送。分类结果用 pydantic 校验(结构化输出),不合法时走“转人工”分支;前端订阅 `custom` 流显示“正在分类 / 正在起草 / 等待审批”。
|
||||
|
||||
## 代码示例
|
||||
|
||||
最小的条件路由图(可直接运行):
|
||||
|
||||
```python
|
||||
import operator
|
||||
from typing import Annotated, TypedDict
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
question: str
|
||||
route: str
|
||||
answer: str
|
||||
steps: Annotated[list[str], operator.add] # 追加而不是覆盖
|
||||
|
||||
|
||||
def route(state: State):
|
||||
r = "billing" if "发票" in state["question"] else "general"
|
||||
return {"route": r, "steps": [f"route:{r}"]}
|
||||
|
||||
|
||||
def billing(state: State):
|
||||
return {"answer": "开票请在订单详情页申请。", "steps": ["billing"]}
|
||||
|
||||
|
||||
def general(state: State):
|
||||
return {"answer": "已转给客服。", "steps": ["general"]}
|
||||
|
||||
|
||||
g = StateGraph(State)
|
||||
g.add_node("route", route)
|
||||
g.add_node("billing", billing)
|
||||
g.add_node("general", general)
|
||||
g.add_edge(START, "route")
|
||||
g.add_conditional_edges("route", lambda s: s["route"], ["billing", "general"])
|
||||
g.add_edge("billing", END)
|
||||
g.add_edge("general", END)
|
||||
app = g.compile()
|
||||
|
||||
for update in app.stream({"question": "怎么开发票", "steps": []}, stream_mode="updates"):
|
||||
print(update)
|
||||
print(app.invoke({"question": "怎么开发票", "steps": []})["steps"]) # ['route:billing', 'billing']
|
||||
```
|
||||
|
||||
完整的工单图(结构化输出校验、自动重试、人工审批、流式进度、崩溃续跑)见 [examples/ticket-flow](examples/ticket-flow/)。
|
||||
|
||||
## 易错点
|
||||
|
||||
- 节点返回了整个状态并修改了列表:应该只返回要更新的字段;列表字段要用 reducer 追加,不要原地 `append`。
|
||||
- 条件边函数里修改状态:路由函数只负责返回下一个节点名,状态修改放在节点里。
|
||||
- 条件边没有声明可能的目标(第三个参数),图的可视化和校验不完整。
|
||||
- 图中有循环却没有退出条件:设置最大步数(`recursion_limit`,以及状态里自己的计数器)。
|
||||
- 混用同步和异步:节点是 `async def` 时要用 `ainvoke` / `astream`。
|
||||
|
||||
## 面试常问
|
||||
|
||||
- LangGraph 和 LangChain 的关系?为什么需要 LangGraph?
|
||||
- reducer 是什么?消息列表为什么用 `add_messages`?
|
||||
- 怎么把图的执行过程流式推给前端?
|
||||
Reference in new issue
Block a user