第五章:tRPC-Agent-Go StateGraph 图引擎
本章目标:深入理解 tRPC-Agent-Go 中的 StateGraph 图引擎——一个 6192 行代码实现的 Go 版 LangGraph,掌握其核心 API、编译执行流程,以及它与 LangGraph 的关键差异。
5.1 StateGraph 是什么?
日常类比:城市地铁线路图
想象你在设计一个城市的地铁线路图。你需要:
- 站点(Node):每个站都有自己的功能——换乘站、始发站、终点站
- 轨道(Edge):连接站点之间的路线,决定列车从哪到哪
- 调度中心(Compiler):把图纸变成真正能跑列车的系统
- 列车状态(State):列车当前位置、载客量、运行方向等信息
StateGraph 就是这样一个”地铁调度系统”——你定义站点(节点)和轨道(边),编译后得到一个可执行的图,数据像列车一样在节点间流动,每个节点处理后更新状态,传给下一个节点。
技术定义
StateGraph 是 tRPC-Agent-Go 中的有向图执行引擎,允许开发者以声明式方式定义节点和边,将复杂的 AI 工作流编排为一个可编译、可执行、可中断的状态机。它的核心代码约 6192 行(含测试),是 Go 语言对 Python LangGraph 概念的移植与增强。
与第四章的 Team 模式(Coordinator/Swarm)不同,StateGraph 给你的是底层的图编程能力——你可以精确控制每一步的执行逻辑、条件分支、循环路径,而不是依赖预设的协作模式。
Team 模式:高层抽象(像用框架的脚手架)
StateGraph:底层引擎(像自己画电路板)
5.2 核心数据结构:StateSchema
在理解 API 之前,必须先理解 State——它是图中流动的”血液”。
StateSchema 的设计
StateSchema 定义了图的状态结构——图中所有节点共享和修改的数据。在 tRPC-Agent-Go 中,StateSchema 是一个 Go struct,通过 tag 标注字段类型和合并策略:
// 定义状态结构
type ContentCreationState struct {
// 基础字段:每次写入直接覆盖
Topic string `json:"topic"`
Draft string `json:"draft"`
FinalOutput string `json:"final_output"`
// 列表字段:使用 append 策略合并
Feedback []string `json:"feedback" reducer:"append"`
// 消息字段:LLM 对话历史
Messages []Message `json:"messages" reducer:"append"`
// 控制字段:决定路由走向
ReviewPass bool `json:"review_pass"`
Iteration int `json:"iteration"`
}
字段类型与 Reducer
StateGraph 中的字段有两种基本更新策略:
- 覆盖型(LastValue):新值直接替换旧值,适合表示”当前状态”的字段
- 追加型(Append/Reducer):新值合并到已有值上,适合表示”历史记录”的字段
// 覆盖型 —— 像黑板上擦掉重写
state.Draft = "新的草稿内容" // 旧内容直接消失
// 追加型 —— 像笔记本上接着写
state.Feedback = append(state.Feedback, "需要加更多例子") // 保留历史
这个设计直接借鉴了 LangGraph 的 Channel 系统,但做了简化——tRPC-Agent 用 struct tag 代替了 LangGraph 复杂的 Annotated 类型系统。
State 在图中的流动
┌──────────┐ State ┌──────────┐ State ┌──────────┐
│ Node A │ ──────────→ │ Node B │ ──────────→ │ Node C │
│ (写作) │ {draft:x} │ (审核) │ {pass:y} │ (发布) │
└──────────┘ └──────────┘ └──────────┘
每个节点接收当前 State,执行逻辑,返回 State 的部分更新(不需要返回完整 State),引擎负责合并更新到全局 State 中。
5.3 核心 API 详解
5.3.1 NewStateGraph —— 创建图
func NewStateGraph[S any](schema S, opts ...GraphOption) *StateGraph[S]
这是一切的起点。传入一个 StateSchema 实例(通常是零值),得到一个空白的图对象。
// 创建一个内容创作工作流图
graph := NewStateGraph(ContentCreationState{})
类比:买了一张白纸准备画地铁线路图。
5.3.2 AddNode —— 添加通用节点
func (g *StateGraph[S]) AddNode(name string, fn NodeFunc[S], opts ...NodeOption) *StateGraph[S]
添加一个执行任意逻辑的节点。NodeFunc 是核心签名:
type NodeFunc[S any] func(ctx context.Context, state S) (S, error)
接收当前状态,返回更新后的状态(或部分更新)。
graph.AddNode("generate_outline", func(ctx context.Context, state ContentCreationState) (ContentCreationState, error) {
// 调用 LLM 生成大纲
outline, err := llm.Generate(ctx, fmt.Sprintf("为主题'%s'生成文章大纲", state.Topic))
if err != nil {
return state, err
}
state.Draft = outline
return state, nil
})
5.3.3 AddLLMNode —— 添加 LLM 调用节点
func (g *StateGraph[S]) AddLLMNode(name string, opts ...LLMNodeOption) *StateGraph[S]
这是 tRPC-Agent 相比 LangGraph 的独有便捷方法。在 LangGraph 中,你需要手动写一个函数来调用 LLM、处理消息、返回结果。tRPC-Agent 把这些样板代码封装了:
// tRPC-Agent:一行搞定 LLM 调用
graph.AddLLMNode("writer",
WithModel("gpt-4"),
WithSystemPrompt("你是一个专业的内容创作者"),
WithTemperature(0.7),
)
// LangGraph 等价代码(需要手写):
// def writer_node(state):
// messages = state["messages"]
// response = model.invoke(messages)
// return {"messages": [response]}
AddLLMNode 自动处理了:
- 从 State 中读取 Messages 字段
- 调用配置的 LLM
- 将响应追加回 Messages
- 处理流式输出
- 处理 token 限制
5.3.4 AddToolsNode —— 添加工具执行节点
func (g *StateGraph[S]) AddToolsNode(name string, tools []Tool, opts ...ToolsNodeOption) *StateGraph[S]
另一个便捷方法。当 LLM 返回 tool_calls 时,需要一个节点来实际执行这些工具调用并将结果放回 State。
// 定义工具
searchTool := NewTool("web_search", "搜索互联网", searchFunc)
calcTool := NewTool("calculator", "数学计算", calcFunc)
// 添加工具执行节点
graph.AddToolsNode("execute_tools", []Tool{searchTool, calcTool})
在 LangGraph 中,你需要用 ToolNode 类或手动遍历 tool_calls 来实现同样的功能。tRPC-Agent 把这些封装为一行调用。
5.3.5 AddAgentNode —— 添加完整 Agent 节点
func (g *StateGraph[S]) AddAgentNode(name string, agent Agent, opts ...AgentNodeOption) *StateGraph[S]
最强大的便捷方法——把一个完整的 Agent(含 LLM + Tools + System Prompt + ReAct Loop)作为图中的一个节点。这意味着你可以在 StateGraph 中嵌套完整的 Agent 执行。
// 创建一个 research Agent
researcher := NewAgent(
WithName("researcher"),
WithModel("gpt-4"),
WithTools(searchTool, readTool),
WithSystemPrompt("你是一个研究助手,负责搜集资料"),
WithMaxIterations(5),
)
// 作为图中的一个节点
graph.AddAgentNode("research_phase", researcher)
这实现了图中嵌套 Agent 的模式——图控制宏观流程,Agent 处理微观任务。LangGraph 中要实现类似效果,需要手动创建子图(subgraph)或在节点函数中实例化 Agent。
5.3.6 AddEdge —— 添加固定边
func (g *StateGraph[S]) AddEdge(from, to string) *StateGraph[S]
最简单的边——从节点 A 无条件跳转到节点 B。
graph.AddEdge("generate_outline", "write_draft")
graph.AddEdge("write_draft", "review")
类比:地铁直达线路,中间不停。
5.3.7 AddJoinEdge —— 添加汇聚边
func (g *StateGraph[S]) AddJoinEdge(from []string, to string) *StateGraph[S]
多个节点并行执行后,需要等待所有节点完成才能进入下一步。JoinEdge 就是这个”等待所有人到齐”的机制。
// research 和 competitor_analysis 并行执行
// 两者都完成后才进入 synthesize
graph.AddJoinEdge([]string{"research", "competitor_analysis"}, "synthesize")
类比:几条地铁线在换乘站汇合,所有方向的列车都到了才能发出下一班。
这在 LangGraph 中通过 Channel 的 reducer 机制隐式实现——当多个节点写入同一个 Channel 时,只有所有写入者都完成后,下游节点才能读取。tRPC-Agent 把这个语义显式化了。
5.3.8 AddConditionalEdges —— 添加条件边
func (g *StateGraph[S]) AddConditionalEdges(from string, condition ConditionFunc[S], mapping map[string]string) *StateGraph[S]
这是构建循环和分支的核心 API。根据当前 State 决定下一步去哪。
// 定义条件函数
reviewCondition := func(ctx context.Context, state ContentCreationState) string {
if state.ReviewPass {
return "pass"
}
if state.Iteration >= 3 {
return "max_retries"
}
return "revise"
}
// 添加条件边
graph.AddConditionalEdges("review", reviewCondition, map[string]string{
"pass": "publish", // 审核通过 → 发布
"revise": "write_draft", // 需要修改 → 回到写作(形成循环!)
"max_retries": "publish", // 超过最大重试 → 强制发布
})
这就是 StateGraph 能表达循环的秘密——条件边可以指回之前的节点。
5.3.9 AddMultiConditionalEdges —— 添加多目标条件边
func (g *StateGraph[S]) AddMultiConditionalEdges(from string, condition MultiConditionFunc[S]) *StateGraph[S]
与 AddConditionalEdges 不同,这个方法允许条件函数返回多个目标节点——即一个节点可以同时触发多个下游节点并行执行。
// 根据内容类型,同时触发多个处理节点
multiRoute := func(ctx context.Context, state ContentCreationState) []string {
targets := []string{"grammar_check"} // 语法检查总是要做
if len(state.Draft) > 5000 {
targets = append(targets, "summarize") // 长文需要摘要
}
if containsCode(state.Draft) {
targets = append(targets, "code_review") // 含代码需要代码审查
}
return targets
}
graph.AddMultiConditionalEdges("write_draft", multiRoute)
5.3.10 AddToolsConditionalEdges —— 工具调用条件边
func (g *StateGraph[S]) AddToolsConditionalEdges(from string, opts ...ToolsCondEdgeOption) *StateGraph[S]
这是一个高度特化的便捷方法,专门处理 ReAct 模式中最常见的条件判断:”LLM 返回了 tool_calls 还是普通文本?”
// 经典 ReAct 循环
graph.AddLLMNode("agent")
graph.AddToolsNode("tools", myTools)
graph.AddToolsConditionalEdges("agent",
WithToolsTarget("tools"), // 有 tool_calls → 去执行工具
WithEndTarget("output"), // 无 tool_calls → 去输出节点
)
graph.AddEdge("tools", "agent") // 工具执行完回到 agent → 形成循环
这三行代码就实现了完整的 ReAct 循环!在 LangGraph 中,等价代码需要:
# LangGraph 等价实现
def should_continue(state):
messages = state["messages"]
last_message = messages[-1]
if last_message.tool_calls:
return "tools"
return "end"
graph.add_conditional_edges("agent", should_continue, {
"tools": "tools",
"end": END,
})
5.3.11 SetEntryPoint / SetFinishPoint —— 设置起终点
func (g *StateGraph[S]) SetEntryPoint(name string) *StateGraph[S]
func (g *StateGraph[S]) SetFinishPoint(name string) *StateGraph[S]
定义图的入口和出口。
graph.SetEntryPoint("generate_outline") // 从生成大纲开始
graph.SetFinishPoint("publish") // 到发布结束
一个图必须有且只有一个 EntryPoint,但可以有多个 FinishPoint(不同的条件分支可能导向不同的结束节点)。
5.3.12 Compile —— 编译图
func (g *StateGraph[S]) Compile(opts ...CompileOption) (*CompiledGraph[S], error)
编译是从”图纸”到”可执行引擎”的关键步骤。编译阶段会做:
- 结构校验:检查是否有孤立节点、是否设置了入口点
- 可达性检查:从入口点出发能否到达所有节点
- 类型检查:确保节点函数的输入输出与 StateSchema 兼容
- 拓扑排序:确定节点的潜在执行顺序
- 生成执行计划:构建运行时需要的数据结构
compiled, err := graph.Compile(
WithCheckpointStore(memoryStore), // 可选:持久化检查点
)
if err != nil {
// 编译失败说明图结构有问题
log.Fatal("图编译失败:", err)
}
// 执行
finalState, err := compiled.Invoke(ctx, ContentCreationState{
Topic: "Go 并发编程入门",
})
类比:代码写完后的 go build——语法错误在编译时就能发现,不需要等到运行时。
5.3.13 WithInterruptBeforeNodes / WithInterruptAfterNodes
func WithInterruptBeforeNodes(nodes ...string) CompileOption
func WithInterruptAfterNodes(nodes ...string) CompileOption
这是实现 Human-in-the-Loop(人机协作) 的核心机制。在指定节点执行前/后暂停图的执行,等待外部输入。
compiled, err := graph.Compile(
WithInterruptBeforeNodes("publish"), // 发布前暂停,等人工确认
WithInterruptAfterNodes("review"), // 审核后暂停,展示结果给用户
)
当执行到被中断的节点时,引擎会:
- 保存当前 State 到 Checkpoint
- 返回一个特殊的中断信号
- 外部系统收到信号后可以展示 UI、等待用户操作
- 用户操作完成后,调用
Resume方法继续执行
// 第一次执行,到 publish 前会暂停
result, err := compiled.Invoke(ctx, initialState)
// result.Interrupted == true
// 用户确认后,恢复执行
finalResult, err := compiled.Resume(ctx, result.CheckpointID, map[string]any{
"user_approved": true,
})
5.4 图的编译与执行流程
5.4.1 编译阶段详解
编译不仅仅是校验——它还生成了高效的运行时结构:
源码定义 编译产物
┌──────────────┐ ┌──────────────────────┐
│ AddNode(...) │ │ nodeRegistry: map │
│ AddEdge(...) │ ──Compile→ │ edgeTable: [][]int │
│ AddConditional│ │ entryIdx: int │
│ SetEntry(...)│ │ finishSet: set │
│ SetFinish(...)│ │ interruptSet: set │
└──────────────┘ │ topologicalOrder: [] │
└──────────────────────┘
编译时的关键检查:
// 伪代码:编译器的校验逻辑
func (g *StateGraph[S]) Compile() (*CompiledGraph[S], error) {
// 1. 入口点必须存在
if g.entryPoint == "" {
return nil, errors.New("未设置入口点")
}
// 2. 入口点必须是已注册的节点
if _, ok := g.nodes[g.entryPoint]; !ok {
return nil, fmt.Errorf("入口点 %s 不是已注册节点", g.entryPoint)
}
// 3. 所有边的目标必须是已注册节点
for _, edge := range g.edges {
if _, ok := g.nodes[edge.To]; !ok {
return nil, fmt.Errorf("边的目标 %s 不是已注册节点", edge.To)
}
}
// 4. 从入口点可达性检查(BFS/DFS)
reachable := g.computeReachable(g.entryPoint)
for name := range g.nodes {
if !reachable[name] {
return nil, fmt.Errorf("节点 %s 从入口点不可达", name)
}
}
// 5. 构建运行时数据结构
return &CompiledGraph[S]{...}, nil
}
5.4.2 执行阶段详解
编译后的图通过 Invoke 方法执行:
Invoke(initialState)
│
▼
┌─────────────────┐
│ 1. 初始化 State │ ← 用户传入的初始状态
└────────┬────────┘
│
▼
┌─────────────────┐
│ 2. 定位当前节点 │ ← 从 entryPoint 开始
└────────┬────────┘
│
▼
┌─────────────────────────────────────────┐
│ 3. 执行当前节点 │
│ node.Func(ctx, currentState) │ ← 核心执行
│ → 得到 partialUpdate │
└────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 4. 合并更新到 State │
│ currentState = merge(state, update) │ ← 根据 reducer 策略
└────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 5. 确定下一个节点 │
│ - 固定边 → 直接跳转 │
│ - 条件边 → 执行条件函数 │
│ - 到达 FinishPoint → 结束 │
└────────┬────────────────────────────────┘
│
▼ (如果未结束)
回到步骤 3
5.4.3 并行执行
当多个节点可以同时执行时(通过 MultiConditionalEdges 或 JoinEdge 的上游),StateGraph 会启动 goroutine 并行执行它们:
// 伪代码:并行节点执行
func (cg *CompiledGraph[S]) executeParallel(ctx context.Context, nodes []string, state S) (S, error) {
var wg sync.WaitGroup
results := make([]S, len(nodes))
errs := make([]error, len(nodes))
for i, nodeName := range nodes {
wg.Add(1)
go func(idx int, name string) {
defer wg.Done()
results[idx], errs[idx] = cg.nodes[name].Func(ctx, state)
}(i, nodeName)
}
wg.Wait()
// 按 reducer 策略合并所有结果
merged := state
for _, result := range results {
merged = mergeState(merged, result)
}
return merged, nil
}
5.5 与 LangGraph 的异同
5.5.1 共同点
| 特性 | tRPC-Agent StateGraph | LangGraph |
|---|---|---|
| 有向图抽象 | ✓ | ✓ |
| 条件边/循环 | ✓ | ✓ |
| State 贯穿全图 | ✓ | ✓ |
| Checkpoint 持久化 | ✓ | ✓ |
| Human-in-the-Loop | ✓(Interrupt) | ✓(Interrupt) |
| 编译后执行 | ✓ | ✓ |
| 并行执行 | ✓(goroutine) | ✓(asyncio) |
5.5.2 tRPC-Agent 独有的便捷方法
这是最显著的差异——tRPC-Agent 提供了一系列开箱即用的高级节点类型:
tRPC-Agent 独有 LangGraph 需要手写
─────────────────────────────────────────────────────────
AddLLMNode → 需要手写 node function 调用 LLM
AddToolsNode → 需要手写 ToolNode 或遍历 tool_calls
AddAgentNode → 需要创建 subgraph 或在 node 中实例化
AddToolsConditional → 需要手写 should_continue 条件函数
AddJoinEdge → 通过 Channel reducer 隐式实现
这些便捷方法的意义在于降低使用门槛——在企业级应用中,80% 的场景都是 LLM 调用 + 工具执行 + 条件路由,tRPC-Agent 把这些模式固化为一行 API 调用。
5.5.3 LangGraph 独有的 Channel Reducer 系统
LangGraph 的 Channel 系统比 tRPC-Agent 的 struct tag 更灵活:
# LangGraph: 精细的 Channel 控制
from langgraph.graph import StateGraph
from typing import Annotated
import operator
class AgentState(TypedDict):
# 用 Annotated + operator.add 表示追加合并
messages: Annotated[list, operator.add]
# 自定义 reducer:只保留最新 3 条
recent_actions: Annotated[list, lambda old, new: (old + new)[-3:]]
# 默认 LastValue:直接覆盖
current_plan: str
tRPC-Agent 目前只支持两种 reducer(覆盖和追加),而 LangGraph 允许任意自定义 reducer 函数。这在需要复杂状态合并逻辑(如去重、限长、加权平均)时差异明显。
5.5.4 类型安全
Go 的泛型给了 tRPC-Agent 一个优势——编译时类型检查:
// Go: 编译时就能发现类型错误
graph := NewStateGraph(ContentCreationState{})
graph.AddNode("writer", func(ctx context.Context, state ContentCreationState) (ContentCreationState, error) {
state.Draft = 123 // 编译错误!Draft 是 string 类型
return state, nil
})
而 LangGraph(Python)的类型检查依赖运行时或可选的 mypy/pyright:
# Python: 类型错误只在运行时暴露(除非用严格 type checker)
def writer(state: AgentState) -> dict:
return {"draft": 123} # 运行时才发现类型不匹配
5.5.5 执行模型差异
| 维度 | tRPC-Agent | LangGraph |
|---|---|---|
| 并发模型 | goroutine + channel | asyncio + await |
| 序列化 | JSON/protobuf | JSON + pickle |
| 流式输出 | gRPC stream | AsyncIterator |
| 部署 | tRPC 微服务原生集成 | LangGraph Platform |
5.6 双引擎架构的意义
tRPC-Agent-Go 同时提供了 Team(第四章)和 StateGraph 两种编排引擎。这不是冗余,而是不同抽象层级的互补。
什么时候用 Team?
- 需求明确是”多个专家协作”的模式
- 每个 Agent 的职责清晰、工具集独立
- 不需要精确控制执行流程的每一步
- 想快速搭建原型
// Team: 5 行代码搞定多 Agent 协作
team := NewTeam(
WithMode(ModeCoordinator),
WithAgents(researcher, writer, reviewer),
)
result, _ := team.Run(ctx, "写一篇关于 Go 并发的技术博客")
什么时候用 StateGraph?
- 工作流有复杂的条件分支和循环
- 需要精确控制每个步骤的输入输出
- 需要 Human-in-the-Loop 在特定节点暂停
- 工作流不是简单的”分配任务”模式
- 需要并行执行后汇聚
// StateGraph: 精确控制每一步
graph := NewStateGraph(MyState{})
graph.AddNode("step1", ...)
graph.AddConditionalEdges("step1", routeFunc, ...)
// ... 更多精细控制
混用模式
最强大的用法是两者混用——在 StateGraph 中嵌入 Team 作为节点,或在 Team 的 Agent 中使用 StateGraph 定义内部逻辑。
// StateGraph 中嵌入 Team
researchTeam := NewTeam(
WithMode(ModeCoordinator),
WithAgents(webSearcher, paperReader, summarizer),
)
graph := NewStateGraph(ProjectState{})
graph.AddNode("research_phase", func(ctx context.Context, state ProjectState) (ProjectState, error) {
// 用 Team 完成研究阶段
result, _ := researchTeam.Run(ctx, state.ResearchQuery)
state.ResearchResults = result
return state, nil
})
graph.AddNode("implementation_phase", ...)
graph.AddConditionalEdges("research_phase", ...)
5.7 实战示例:带循环的内容创作工作流
现在让我们把所有知识点串起来,构建一个完整的内容创作工作流。这个工作流模拟了一个真实的写作过程:
选题 → 研究 → 写初稿 → 审核 → (不通过)回到写初稿 → 审核通过 → 润色 → 发布
5.7.1 定义 State
package main
import (
"context"
"fmt"
sg "github.com/example/trpc-agent-go/stategraph"
)
// 内容创作状态
type ArticleState struct {
// 输入
Topic string `json:"topic"`
Audience string `json:"audience"` // 目标读者
// 中间产物
Research string `json:"research"` // 研究结果
Outline string `json:"outline"` // 大纲
Draft string `json:"draft"` // 草稿
// 审核相关
Feedback []string `json:"feedback" reducer:"append"` // 历次反馈
ReviewPass bool `json:"review_pass"`
Iteration int `json:"iteration"`
// 输出
FinalArticle string `json:"final_article"`
// LLM 对话(供 LLMNode 使用)
Messages []Message `json:"messages" reducer:"append"`
}
5.7.2 定义节点
func main() {
graph := sg.NewStateGraph(ArticleState{})
// 节点 1:研究阶段(用完整 Agent)
researcher := sg.NewAgent(
sg.WithName("researcher"),
sg.WithModel("gpt-4"),
sg.WithTools(webSearchTool, academicSearchTool),
sg.WithSystemPrompt("你是研究助手,搜集主题相关的关键信息、数据和案例"),
sg.WithMaxIterations(3),
)
graph.AddAgentNode("research", researcher)
// 节点 2:生成大纲
graph.AddNode("outline", func(ctx context.Context, state ArticleState) (ArticleState, error) {
prompt := fmt.Sprintf(
"基于以下研究结果,为面向%s的文章'%s'生成结构化大纲:\n%s",
state.Audience, state.Topic, state.Research,
)
outline, err := llm.Generate(ctx, prompt)
if err != nil {
return state, err
}
state.Outline = outline
return state, nil
})
// 节点 3:写草稿
graph.AddNode("write_draft", func(ctx context.Context, state ArticleState) (ArticleState, error) {
var prompt string
if state.Iteration == 0 {
prompt = fmt.Sprintf(
"按照以下大纲写一篇完整文章:\n%s\n\n研究资料:\n%s",
state.Outline, state.Research,
)
} else {
prompt = fmt.Sprintf(
"根据审核反馈修改文章。\n\n当前草稿:\n%s\n\n反馈:\n%s",
state.Draft, state.Feedback[len(state.Feedback)-1],
)
}
draft, err := llm.Generate(ctx, prompt)
if err != nil {
return state, err
}
state.Draft = draft
state.Iteration++
return state, nil
})
// 节点 4:审核
graph.AddNode("review", func(ctx context.Context, state ArticleState) (ArticleState, error) {
prompt := fmt.Sprintf(
"作为编辑审核以下文章。判断是否可以发布。\n"+
"如果可以发布,只回复'APPROVED'。\n"+
"如果需要修改,列出具体问题。\n\n文章:\n%s",
state.Draft,
)
review, err := llm.Generate(ctx, prompt)
if err != nil {
return state, err
}
if review == "APPROVED" {
state.ReviewPass = true
} else {
state.ReviewPass = false
state.Feedback = append(state.Feedback, review)
}
return state, nil
})
// 节点 5:润色
graph.AddNode("polish", func(ctx context.Context, state ArticleState) (ArticleState, error) {
prompt := fmt.Sprintf(
"对以下文章进行最后润色(修正语法、优化措辞、统一风格):\n%s",
state.Draft,
)
polished, err := llm.Generate(ctx, prompt)
if err != nil {
return state, err
}
state.FinalArticle = polished
return state, nil
})
// 节点 6:发布
graph.AddNode("publish", func(ctx context.Context, state ArticleState) (ArticleState, error) {
fmt.Printf("发布文章:%s\n字数:%d\n迭代次数:%d\n",
state.Topic, len(state.FinalArticle), state.Iteration)
return state, nil
})
5.7.3 定义边和条件
// 设置流程
graph.SetEntryPoint("research")
// 固定边
graph.AddEdge("research", "outline")
graph.AddEdge("outline", "write_draft")
graph.AddEdge("write_draft", "review")
graph.AddEdge("polish", "publish")
// 条件边:审核后的路由(核心循环)
graph.AddConditionalEdges("review", func(ctx context.Context, state ArticleState) string {
if state.ReviewPass {
return "approved"
}
if state.Iteration >= 3 {
return "max_retries" // 防止无限循环
}
return "needs_revision"
}, map[string]string{
"approved": "polish", // 通过 → 润色
"needs_revision": "write_draft", // 不通过 → 重写(循环!)
"max_retries": "polish", // 达到上限 → 强制进入润色
})
graph.SetFinishPoint("publish")
5.7.4 编译与执行
// 编译(可加中断点)
compiled, err := graph.Compile(
sg.WithInterruptBeforeNodes("publish"), // 发布前让人确认
)
if err != nil {
log.Fatal("编译失败:", err)
}
// 执行
result, err := compiled.Invoke(ctx, ArticleState{
Topic: "Go 语言并发编程实战",
Audience: "有 1 年编程经验的后端开发者",
})
if result.Interrupted {
fmt.Println("文章已准备好,等待确认发布...")
fmt.Println("最终文章预览:", result.State.FinalArticle[:200])
// 人工确认后恢复
final, _ := compiled.Resume(ctx, result.CheckpointID, nil)
fmt.Println("文章已发布!")
}
}
5.7.5 完整流程图
┌──────────┐
│ research │ ← 入口(AgentNode,含 ReAct 循环)
└────┬─────┘
│
▼
┌──────────┐
│ outline │ ← 生成大纲
└────┬─────┘
│
▼
┌─→ ┌──────────────┐
│ │ write_draft │ ← 写/改草稿
│ └──────┬───────┘
│ │
│ ▼
│ ┌──────────┐
│ │ review │ ← 审核
│ └──┬───┬──┘
│ │ │
│ │ │ approved / max_retries
│ │ ▼
│ │ ┌──────────┐
│ │ │ polish │ ← 润色
│ │ └────┬─────┘
│ │ │
needs_ │ │ ▼
revision │ │ ┌──────────┐
│ │ │ publish │ ← 发布(FinishPoint + Interrupt)
└───────┘ └──────────┘
5.8 进阶话题:StateGraph 的设计权衡
5.8.1 为什么需要编译步骤?
你可能会问:为什么不直接执行定义好的图,还要多一步 Compile?
原因有三:
- 尽早发现错误:结构问题在编译时暴露,不要等到运行了一半才崩溃
- 性能优化:编译时可以预计算路由表、预分配内存
- 不可变性保证:编译后的图是不可变的,多个请求可以安全共享同一个 CompiledGraph
这和 Go 语言本身的哲学一致——如果编译通过了,运行时大概率没问题。
5.8.2 循环的安全性
StateGraph 允许循环,但循环可能导致无限执行。保护机制:
- MaxIterations 配置:在 Compile 时设置最大执行步数
- 条件边中的计数器:像我们示例中的
state.Iteration >= 3 - 超时控制:通过
context.WithTimeout实现
compiled, _ := graph.Compile(
sg.WithMaxSteps(50), // 最多执行 50 步
)
// 加超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
result, err := compiled.Invoke(ctx, initialState)
5.8.3 错误处理策略
节点执行失败时怎么办?StateGraph 提供几种策略:
graph.AddNode("risky_operation", riskyFunc,
sg.WithRetry(3, time.Second), // 重试 3 次,间隔 1 秒
sg.WithFallback("safe_fallback"), // 失败后跳转到 fallback 节点
sg.WithErrorHandler(func(err error) {
// 自定义错误处理
log.Warn("操作失败,使用默认值", err)
}),
)
5.9 本章总结
StateGraph 是 tRPC-Agent-Go 中的底层图执行引擎,它给予开发者对工作流的完全控制权:
| 维度 | Team 模式 | StateGraph |
|---|---|---|
| 抽象层级 | 高(声明式协作) | 低(命令式编排) |
| 灵活性 | 受限于 Coordinator/Swarm | 任意图结构 |
| 循环支持 | 有限 | 原生支持 |
| Human-in-the-Loop | 较弱 | 原生 Interrupt |
| 学习成本 | 低 | 中等 |
| 适用场景 | 多专家协作 | 复杂工作流 |
| 混用 | 可作为 StateGraph 的节点 | 可包含 Team |
核心记忆点:
- StateGraph = 声明节点 + 声明边 + 编译 + 执行
- 便捷方法(AddLLMNode/AddToolsNode/AddAgentNode)是 tRPC-Agent 的差异化优势
- 条件边 + 循环 = 能表达任意复杂逻辑
- 编译步骤 = 尽早发现错误 + 性能优化
- 双引擎(Team + StateGraph)= 不同抽象层级的互补
5.10 思考题
-
设计题:假设你要构建一个”客服工单处理系统”,工单需要经过分类→分配→处理→回复→满意度调查,其中”处理”阶段可能需要升级到人工(Human-in-the-Loop)。请用 StateGraph 的 API 设计这个图的结构(写出 AddNode/AddEdge/AddConditionalEdges 的调用代码)。
-
对比分析:为什么 tRPC-Agent 选择用 Go struct tag(
reducer:"append")而不是像 LangGraph 那样用Annotated[list, operator.add]?这个设计选择有什么优缺点?提示:从类型安全、灵活性、可读性三个角度分析。 -
调试思考:一个 StateGraph 在执行时陷入了无限循环(审核节点永远返回 “needs_revision”)。除了设置 MaxSteps 限制外,你能想到哪些方法来诊断和修复这个问题?至少列出 3 种方法。
-
架构选择:一个电商平台需要实现”智能客服”功能:用户提问→理解意图→查询知识库→如果是退款请求则走审批流程→回复用户。你会选择 Team 模式还是 StateGraph?还是混用?请给出理由和具体方案。
-
深度思考:AddAgentNode 允许在图的一个节点中运行完整的 Agent(含多次 LLM 调用和工具使用)。这意味着图的一个”步骤”可能包含不确定数量的内部迭代。这对 Checkpoint 机制有什么影响?如果 Agent 执行到一半系统崩溃了,恢复时会发生什么?
下一章:第六章 LangGraph Channel 与 Pregel 引擎精读
返回目录:Agent 框架深度导读系列