犀牛鸟 2026 研究笔记 estelledc.github.io

第五章:tRPC-Agent-Go StateGraph 图引擎

本章目标:深入理解 tRPC-Agent-Go 中的 StateGraph 图引擎——一个 6192 行代码实现的 Go 版 LangGraph,掌握其核心 API、编译执行流程,以及它与 LangGraph 的关键差异。


5.1 StateGraph 是什么?

日常类比:城市地铁线路图

想象你在设计一个城市的地铁线路图。你需要:

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 中的字段有两种基本更新策略:

  1. 覆盖型(LastValue):新值直接替换旧值,适合表示”当前状态”的字段
  2. 追加型(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 自动处理了:

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)

编译是从”图纸”到”可执行引擎”的关键步骤。编译阶段会做:

  1. 结构校验:检查是否有孤立节点、是否设置了入口点
  2. 可达性检查:从入口点出发能否到达所有节点
  3. 类型检查:确保节点函数的输入输出与 StateSchema 兼容
  4. 拓扑排序:确定节点的潜在执行顺序
  5. 生成执行计划:构建运行时需要的数据结构
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"),    // 审核后暂停,展示结果给用户
)

当执行到被中断的节点时,引擎会:

  1. 保存当前 State 到 Checkpoint
  2. 返回一个特殊的中断信号
  3. 外部系统收到信号后可以展示 UI、等待用户操作
  4. 用户操作完成后,调用 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?

// Team: 5 行代码搞定多 Agent 协作
team := NewTeam(
    WithMode(ModeCoordinator),
    WithAgents(researcher, writer, reviewer),
)
result, _ := team.Run(ctx, "写一篇关于 Go 并发的技术博客")

什么时候用 StateGraph?

// 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?

原因有三:

  1. 尽早发现错误:结构问题在编译时暴露,不要等到运行了一半才崩溃
  2. 性能优化:编译时可以预计算路由表、预分配内存
  3. 不可变性保证:编译后的图是不可变的,多个请求可以安全共享同一个 CompiledGraph

这和 Go 语言本身的哲学一致——如果编译通过了,运行时大概率没问题。

5.8.2 循环的安全性

StateGraph 允许循环,但循环可能导致无限执行。保护机制:

  1. MaxIterations 配置:在 Compile 时设置最大执行步数
  2. 条件边中的计数器:像我们示例中的 state.Iteration >= 3
  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

核心记忆点:


5.10 思考题

  1. 设计题:假设你要构建一个”客服工单处理系统”,工单需要经过分类→分配→处理→回复→满意度调查,其中”处理”阶段可能需要升级到人工(Human-in-the-Loop)。请用 StateGraph 的 API 设计这个图的结构(写出 AddNode/AddEdge/AddConditionalEdges 的调用代码)。

  2. 对比分析:为什么 tRPC-Agent 选择用 Go struct tag(reducer:"append")而不是像 LangGraph 那样用 Annotated[list, operator.add]?这个设计选择有什么优缺点?提示:从类型安全、灵活性、可读性三个角度分析。

  3. 调试思考:一个 StateGraph 在执行时陷入了无限循环(审核节点永远返回 “needs_revision”)。除了设置 MaxSteps 限制外,你能想到哪些方法来诊断和修复这个问题?至少列出 3 种方法。

  4. 架构选择:一个电商平台需要实现”智能客服”功能:用户提问→理解意图→查询知识库→如果是退款请求则走审批流程→回复用户。你会选择 Team 模式还是 StateGraph?还是混用?请给出理由和具体方案。

  5. 深度思考:AddAgentNode 允许在图的一个节点中运行完整的 Agent(含多次 LLM 调用和工具使用)。这意味着图的一个”步骤”可能包含不确定数量的内部迭代。这对 Checkpoint 机制有什么影响?如果 Agent 执行到一半系统崩溃了,恢复时会发生什么?


上一章:第四章 Team 多 Agent 协作

下一章:第六章 LangGraph Channel 与 Pregel 引擎精读

返回目录:Agent 框架深度导读系列