Skip to content
Go
综合实战

综合实战

本章用同一个需求分别以「原始组件(Component)」和「ADK 封装」两种方式实现,直观对比两者的差异,逐个结合前述所有知识点。

场景:智能客服 Agent

  • 用户提问 → Agent 判断能否直接回答
  • 不能回答时,调用 search_knowledge 工具检索内部知识库
  • 涉及退款等敏感操作 → 触发人工审批中断
  • 调用可能瞬时失败 → 启用模型重试
  • 全套操作通过 Callback 记录日志和耗时
用户:"我想退款订单 #12345"
  │
  ▼
Agent 推理 → 判断需要查知识库 + 人工审批
  │
  ├── 调用 search_knowledge("退款政策")
  ├── 触发 Interrupt(需要人工审批)
  └── 人工确认后 → 返回最终回复

Part 1:Component 原始实现

以下代码展示用 Eino 最底层的 compose.Graphcomponents.Tool 手写 ReAct 循环——这些正是 ADK 的 ChatModelAgent 帮你自动化掉的部分。

🔬 学这份代码的目的:不是在项目里这样写(应该用 ADK),而是理解 ADK 下面发生了什么。

1.1 定义工具

// ── 知识库搜索工具 ──
type KnowledgeSearchTool struct{}

func (t *KnowledgeSearchTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
    return &schema.ToolInfo{
        Name: "search_knowledge",
        Desc: "搜索内部知识库。用于查询退款政策、产品说明、操作流程等。",
        ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
            "query": {Type: schema.String, Desc: "搜索关键词", Required: true},
        }),
    }, nil
}

func (t *KnowledgeSearchTool) InvokableRun(ctx context.Context, argsJSON string, opts ...tool.Option) (string, error) {
    var args struct{ Query string }
    json.Unmarshal([]byte(argsJSON), &args)

    results := t.search(args.Query)
    if len(results) == 0 {
        return "未找到相关信息", nil
    }
    return strings.Join(results, "\n"), nil
}

func (t *KnowledgeSearchTool) search(query string) []string {
    // 模拟知识库
    kb := map[string][]string{
        "退款": {
            "退款政策:购买后 7 天内可申请退款,退款将在 3-5 个工作日内原路返回",
            "退款流程:进入订单详情 → 点击申请退款 → 填写原因 → 等待审核",
            "退款审核:金额 > 500 元需要主管审批",
        },
    }
    return kb[query]
}

1.2 构建 ReAct Graph(手写编排)

下面是 Component 方式的核心:用 Graph 手动搭出 ReAct 循环。每一步的注释说明了它的作用,以及 ADK 用什么替代它。

// reactState 图内共享状态(跨节点、跨迭代保持)
// 对应 ADK 的 ChatModelAgentState
type reactState struct {
    iteration int              // 当前循环次数
    maxIter   int              // 最大迭代次数
    msgs      []*schema.Message // 对话历史
}

// buildReActGraph 构建含 Tool Calling 的 ReAct 循环图
//
// 图拓扑:
//   START → [build_messages] → [call_model]
//                                   │
//                     ┌─────────────┼─────────────┐
//               有 ToolCall    无 ToolCall → [format] → END
//                     │
//               [call_tools]
//                     │
//               (回到 build_messages → call_model —— ReAct 环)
//
func buildReActGraph(
    ctx context.Context,
    chatModel model.BaseChatModel,
    tools []tool.BaseTool,
) (compose.Runnable[[]*schema.Message, *schema.Message], error) {

    // 创建工具节点(将 []BaseTool 聚合为一个可执行节点)
    toolsNode, err := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{Tools: tools})
    if err != nil {
        return nil, err
    }

    graph := compose.NewGraph[[]*schema.Message, *schema.Message](
        // WithGenLocalState:每次 Run/Invoke 调用时创建一个新的 state 实例
        // 同一个 Run 内的所有节点共享这个 state
        compose.WithGenLocalState(func(ctx context.Context) *reactState {
            return &reactState{maxIter: 15}
        }),
    )

    // ── 节点 1:消息预处理(首次运行时插入 System Prompt)──
    // Lambda 节点 = 纯 Go 函数作为图节点
    // 注意:Lambda 内无法直接访问 graph state,消息的拼装和迭代管理需要
    // 在下游节点的 StatePreHandler 中完成
    graph.AddLambdaNode("build_messages", compose.InvokableLambda(
        func(ctx context.Context, msgs []*schema.Message) ([]*schema.Message, error) {
            return msgs, nil
        },
    ))

    // ── 节点 2:调用模型 —— ReAct 的核心决策点 ──
    // StatePreHandler 在 ChatModel 执行前被调用,可以读写 state
    // 这是 Component 方式管理对话历史的唯一正确位置
    graph.AddChatModelNode("call_model", chatModel,
        compose.WithStatePreHandler(
            func(ctx context.Context, in []*schema.Message, state *reactState) ([]*schema.Message, error) {
                state.iteration++
                if state.iteration > state.maxIter {
                    return nil, fmt.Errorf("超过最大迭代次数 %d", state.maxIter)
                }

                // 首次迭代:在消息列表前面插入 System Prompt
                if state.iteration == 1 {
                    sysMsg := schema.SystemMessage(`你是智能客服助手。规则:
1. 先查询知识库确认信息后再回答
2. 涉及退款金额 > 500 元时,必须告知用户需要人工审批
3. 用中文回复`)
                    state.msgs = append([]*schema.Message{sysMsg}, in...)
                } else {
                    // 后续迭代:将新消息(如工具结果)拼入历史
                    state.msgs = append(state.msgs, in...)
                }

                // 返回完整消息历史给 ChatModel——这样模型能看到历史上下文
                return state.msgs, nil
            },
        ),
    )

    // ── 节点 3:执行工具 ──
    // 当模型返回 ToolCall 时,框架自动匹配 Tool 并调用 InvokableRun
    graph.AddToolsNode("call_tools", toolsNode,
        compose.WithStatePreHandler(
            func(ctx context.Context, in *schema.Message, state *reactState) (*schema.Message, error) {
                // in:包含 ToolCall 的 assistant 消息
                // 将其加入历史,这样下次模型调用能看到"我调用了哪个工具"
                state.msgs = append(state.msgs, in)
                return in, nil
            },
        ),
    )

    // ── 节点 4:透传最终回复 ──
    graph.AddLambdaNode("format", compose.InvokableLambda(
        func(ctx context.Context, msg *schema.Message) (*schema.Message, error) {
            return msg, nil
        },
    ))

    // ── 构建边(控制流)──
    graph.AddEdge(compose.START, "build_messages")
    graph.AddEdge("build_messages", "call_model")

    // ── 关键:运行时分支 —— 这是 ReAct 的"判断逻辑"──
    // NewGraphBranch 的签名:(判断函数, 合法目标节点集合)
    // 判断函数返回目标节点名,框架自动路由
    graph.AddBranch("call_model", compose.NewGraphBranch(
        func(ctx context.Context, msg *schema.Message) (string, error) {
            if len(msg.ToolCalls) > 0 {
                return "call_tools", nil // 模型要调工具
            }
            return "format", nil // 模型直接回复
        },
        map[string]bool{"call_tools": true, "format": true},
    ))

    // 工具执行完后回到 call_model(通过 build_messages),形成循环
    graph.AddEdge("call_tools", "build_messages")
    graph.AddEdge("format", compose.END)

    // 编译:将声明式的图转为可执行体
    return graph.Compile(ctx,
        compose.WithGraphName("custom_react_agent"),
        compose.WithMaxRunSteps(100), // 安全阀:防止图无限循环
    )
}

1.3 Component 方式的运行入口

func runWithComponent(ctx context.Context, chatModel model.BaseChatModel, userInput string) error {
    tools := []tool.BaseTool{&KnowledgeSearchTool{}}
    // 注意:Component 方式下没有中断/恢复机制。
    // 如果 Tool 返回 compose.NewInterruptAndRerunErr,Invoke 会直接返回该 error。

    runnable, err := buildReActGraph(ctx, chatModel, tools)
    if err != nil {
        return fmt.Errorf("构建图失败: %w", err)
    }

    // ── 注入 Callback(日志 + 计时)──
    // 在调用时通过 compose.WithCallbacks 注入,仅本次 Invoke 生效
    timerHandler := buildTimerCallback()

    msgs := []*schema.Message{schema.UserMessage(userInput)}
    result, err := runnable.Invoke(ctx, msgs,
        compose.WithCallbacks(timerHandler),
    )
    if err != nil {
        return fmt.Errorf("执行失败: %w", err)
    }

    fmt.Printf("最终回复: %s\n", result.Content)
    return nil
}

// buildTimerCallback 创建记录耗时的 Callback Handler
//
// Callback 的 5 个时机:
//   OnStart / OnEnd / OnError
//   OnStartWithStreamInput / OnEndWithStreamOutput
//
// 通过 OnStart 返回的 context 可以向下传递数据(此处传递计时起点)
func buildTimerCallback() callbacks.Handler {
    type timerKey struct{} // context key,不导出以避免冲突

    return callbacks.NewHandlerHelper().
        OnStart(func(ctx context.Context, info *callbacks.RunInfo, _ callbacks.CallbackInput) context.Context {
            // 将开始时间存入 context → OnEnd/OnError 中可以取出计算耗时
            return context.WithValue(ctx, timerKey{}, time.Now())
        }).
        OnEnd(func(ctx context.Context, info *callbacks.RunInfo, _ callbacks.CallbackOutput) context.Context {
            if start, ok := ctx.Value(timerKey{}).(time.Time); ok {
                fmt.Printf("[trace] %s/%s 耗时: %v\n", info.Component, info.Name, time.Since(start))
            }
            return ctx
        }).
        OnError(func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
            if start, ok := ctx.Value(timerKey{}).(time.Time); ok {
                fmt.Printf("[trace] %s/%s 错误(耗时 %v): %v\n", info.Component, info.Name, time.Since(start), err)
            }
            return ctx
        }).
        Handler()
}

Component 方式要点

你必须手动做的事(ADK 替你做了):
  ├── 管理对话历史 ── state.msgs 手动 append(ADK:ChatModelAgentState.Messages)
  ├── 构建 System Prompt ── 在 StatePreHandler 中拼接(ADK:Instruction 字段)
  ├── 手写 ReAct 循环 ── Branch + Edge 形成环(ADK:ChatModelAgent 内置)
  ├── 迭代上限 ── state.iteration vs maxIter(ADK:MaxIterations 配置项)
  ├── 处理 ToolCall 路由 ── compose.NewGraphBranch(ADK:自动)
  ├── 注入 Callback ── compose.WithCallbacks(ADK:自动 + Middleware 钩子)
  └── 没有:事件流、Middleware、中断/恢复、ModelRetry/Failover……

Part 2:ADK 封装实现

现在用 ADK 方式实现完全相同的功能。对比 Part 1,你会看到 ADK 帮你省掉了多少代码。

2.1 注册自定义类型(gob)

ADK 的中断/恢复依赖 gob 序列化。所有存入 InterruptInfo.Data 的自定义类型必须注册:

func init() {
    // 注册自定义类型 → 中断时框架可以序列化/反序列化
    // RegisterName 第二个参数是注册名,用于 gob 查找类型
    schema.RegisterName[*ApprovalRequest]("customerservice.ApprovalRequest")
    schema.RegisterName[*ApprovalDecision]("customerservice.ApprovalDecision")
}

type ApprovalRequest struct {
    OrderID string  `json:"order_id"`
    Amount  float64 `json:"amount"`
    Reason  string  `json:"reason"`
}

type ApprovalDecision struct {
    Approved bool   `json:"approved"`
    Comment  string `json:"comment"`
}

2.2 自定义 Middleware

// LoggingMiddleware 在模型调用的生命周期钩子上注入日志
//
// 嵌入 BaseChatModelAgentMiddleware → 只需覆盖你关心的钩子
type LoggingMiddleware struct {
    *adk.BaseChatModelAgentMiddleware
}

// BeforeModelRewriteState:每次模型调用前触发
// 返回的 state 被框架持久化 —— 这里的修改会跨迭代保持
func (m *LoggingMiddleware) BeforeModelRewriteState(
    ctx context.Context,
    state *adk.ChatModelAgentState,
    mc *adk.ModelContext,
) (context.Context, *adk.ChatModelAgentState, error) {

    log.Printf("[Agent] 第 N 轮模型调用,历史消息数: %d", len(state.Messages))
    return ctx, state, nil
}

// AfterModelRewriteState:每次模型调用后触发
func (m *LoggingMiddleware) AfterModelRewriteState(
    ctx context.Context,
    state *adk.ChatModelAgentState,
    mc *adk.ModelContext,
) (context.Context, *adk.ChatModelAgentState, error) {

    lastMsg := state.Messages[len(state.Messages)-1]
    if len(lastMsg.ToolCalls) > 0 {
        for _, tc := range lastMsg.ToolCalls {
            log.Printf("[Agent] → 调用工具: %s(%s)", tc.Function.Name, tc.Function.Arguments)
        }
    } else {
        log.Printf("[Agent] → 直接回复,长度: %d 字符", len(lastMsg.Content))
    }
    return ctx, state, nil
}

2.3 审批工具 —— 返回中断

type ApprovalToolV2 struct{}

func (t *ApprovalToolV2) Info(ctx context.Context) (*schema.ToolInfo, error) {
    return &schema.ToolInfo{
        Name: "request_approval",
        Desc: "需要人工审批时调用。用于退款金额 > 500 的场景。",
        ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
            "order_id": {Type: schema.String, Desc: "订单号", Required: true},
            "amount":   {Type: schema.Number, Desc: "金额(元)", Required: true},
            "reason":   {Type: schema.String, Desc: "审批原因", Required: true},
        }),
    }, nil
}

func (t *ApprovalToolV2) InvokableRun(ctx context.Context, argsJSON string, opts ...tool.Option) (string, error) {
    var req ApprovalRequest
    json.Unmarshal([]byte(argsJSON), &req)

    // NewInterruptAndRerunErr 告知 Runner:"我需要中断,请保存状态"
    // Runner 捕获后:1. 序列化当前状态 → CheckPointStore
    //               2. 将 Interrupted 事件发送给调用方
    //               3. 等待 ResumeWithParams
    return "", compose.NewInterruptAndRerunErr(&adk.InterruptInfo{
        Data: &req, // 结构化数据,Resume 时通过 event.Action.Interrupted.Data 取回
    })
}

2.4 创建 ADK Agent(完整配置)

func buildADKAgent(
    ctx context.Context,
    chatModel model.BaseChatModel,
    fallbackModel model.BaseChatModel,
) (adk.Agent, error) {

    searchTool := &KnowledgeSearchTool{}
    approvalTool := &ApprovalToolV2{}

    agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        // ── 基础身份 ──
        Name:        "customer_service_agent",
        Description: "智能客服助手,可查询知识库、处理退款审批",

        // ── System Prompt ──
        // {user_name} 占位符 → 运行时从 SessionValues 取值并渲染
        Instruction: `你是智能客服助手。当前用户: {user_name}。
规则:
1. 先调用 search_knowledge 查询相关信息,再回答用户
2. 退款金额 > 500 元时必须调用 request_approval
3. 审批通过后才能告知用户退款结果
4. 回复简洁、专业,用中文`,

        // ── 模型(必填)──
        Model: chatModel,

        // ── 工具 ──
        ToolsConfig: adk.ToolsConfig{
            ToolsNodeConfig: compose.ToolsNodeConfig{
                Tools: []tool.BaseTool{searchTool, approvalTool},
            },
            // request_approval 触发中断,执行后直接返回不回调模型
            ReturnDirectly: map[string]bool{"request_approval": true},
        },

        // ── Middleware ──
        Handlers: []adk.ChatModelAgentMiddleware{&LoggingMiddleware{}},

        // ── 模型重试(瞬时错误自动重试)──
        ModelRetryConfig: &adk.ModelRetryConfig{
            MaxRetries: 2,
            IsRetryAble: func(ctx context.Context, err error) bool {
                return !errors.Is(err, context.Canceled) &&
                    !errors.Is(err, context.DeadlineExceeded)
            },
        },

        // ── 模型故障切换(主模型不可用时切备机)──
        ModelFailoverConfig: &adk.ModelFailoverConfig{
            MaxRetries: 1,
            ShouldFailover: func(ctx context.Context, _ *schema.Message, err error) bool {
                return err != nil && !errors.Is(err, context.Canceled)
            },
            GetFailoverModel: func(ctx context.Context, fc *adk.FailoverContext) (
                model.BaseChatModel, []*schema.Message, error,
            ) {
                log.Printf("[Failover] 主模型失败,第 %d 次切换备机", fc.FailoverAttempt)
                return fallbackModel, nil, nil
            },
        },

        MaxIterations: 10,
    })
    if err != nil {
        return nil, fmt.Errorf("创建 Agent 失败: %w", err)
    }
    return agent, nil
}

2.5 ADK 方式的运行入口(含中断恢复)

func runWithADK(
    ctx context.Context,
    chatModel, fallbackModel model.BaseChatModel,
    userInput string,
) error {

    // ── 1. 创建 Agent ──
    agent, err := buildADKAgent(ctx, chatModel, fallbackModel)
    if err != nil {
        return err
    }

    // ── 2. 创建 Runner ──
    // Runner 是 Agent 的唯一正确调用方式。
    // 配置 CheckPointStore 才能启用中断/恢复。
    store := adk.NewInMemoryStore()
    runner := adk.NewRunner(ctx, adk.RunnerConfig{
        Agent:           agent,
        EnableStreaming: true,  // 启用流式输出(打字机效果)
        CheckPointStore: store, // 中断/恢复的关键
    })

    // ── 3. 注入 Callback(仅本次 Run 调用)──
    timerHandler := buildTimerCallback()

    // ── 4. 发起请求 ──
    checkpointID := "session-" + time.Now().Format("150405")
    iter := runner.Run(ctx, []*schema.Message{
        schema.UserMessage(userInput),
    },
        adk.WithCheckPointID(checkpointID),
        // SessionValues 中的值会渲染 Instruction 中的 {user_name}
        adk.WithSessionValues(map[string]any{"user_name": "张三"}),
        adk.WithCallbacks(timerHandler),
    )

    // ── 5. 消费事件流 ──
    // AgentEvent 可能包含三种内容:
    //   - Output(消息内容:流式 chunk / 完整消息 / 自定义数据)
    //   - Action(控制信号:Interrupted / Exit / BreakLoop)
    //   - Err(错误)
    for {
        event, ok := iter.Next()
        if !ok { break }

        // 5a. 错误
        if event.Err != nil {
            log.Printf("[Runner] 错误: %v", event.Err)
            continue
        }

        // 5b. 控制信号 → 中断
        if event.Action != nil && event.Action.Interrupted != nil {
            fmt.Println("\n⚠️  Agent 请求人工审批")

            // InterruptInfo.Data 是我们在 InvokableRun 中传入的 *ApprovalRequest
            req := event.Action.Interrupted.Data.(*ApprovalRequest)
            fmt.Printf("   订单: %s, 金额: ¥%.2f, 原因: %s\n",
                req.OrderID, req.Amount, req.Reason)

            // 模拟人工审批
            decision, _ := json.Marshal(&ApprovalDecision{
                Approved: true,
                Comment:  "审核通过",
            })

            // ── 从中断点恢复 ──
            // ResumeWithParams 的 Targets:key 是 Agent.Name(),value 是传给 Agent.Resume() 的数据
            resumeIter, err := runner.ResumeWithParams(ctx, checkpointID, &adk.ResumeParams{
                Targets: map[string]any{"customer_service_agent": decision},
            })
            if err != nil {
                return fmt.Errorf("恢复失败: %w", err)
            }
            fmt.Println("\n✅ 审批完成,Agent 继续:")
            consumeStreamEvents(resumeIter)
            return nil
        }

        // 5c. 消息输出
        if event.Output != nil && event.Output.MessageOutput != nil {
            msgOut := event.Output.MessageOutput
            if msgOut.IsStreaming {
                // 流式 → 逐 chunk 打印
                stream := msgOut.MessageStream
                fmt.Print("🤖 ")
                for {
                    chunk, err := stream.Recv()
                    if err == io.EOF { break }
                    if err != nil {
                        var willRetry *adk.WillRetryError
                        if errors.As(err, &willRetry) {
                            fmt.Printf("\n⏳ 重试中... (第 %d 次)\n", willRetry.RetryAttempt)
                            break
                        }
                        break
                    }
                    fmt.Print(chunk.Content)
                }
                stream.Close()
                fmt.Println()
            } else if msgOut.Role == schema.Tool {
                fmt.Printf("🔧 [工具结果] %s\n", truncate(msgOut.Message.Content, 100))
            }
        }
    }

    fmt.Println("\n✅ Agent 完成任务")
    return nil
}

// consumeStreamEvents 消费恢复后的事件流
func consumeStreamEvents(iter *adk.AsyncIterator[*adk.AgentEvent]) {
    for {
        event, ok := iter.Next()
        if !ok { break }
        if event.Output != nil && event.Output.MessageOutput != nil {
            msgOut := event.Output.MessageOutput
            if msgOut.IsStreaming {
                stream := msgOut.MessageStream
                fmt.Print("🤖 ")
                for {
                    chunk, err := stream.Recv()
                    if err == io.EOF { break }
                    if err != nil { break }
                    fmt.Print(chunk.Content)
                }
                stream.Close()
                fmt.Println()
            } else if msgOut.Message != nil {
                fmt.Printf("🤖 %s\n", msgOut.Message.Content)
            }
        }
    }
}

2.6 main() 入口

func main() {
    ctx := context.Background()

    primaryModel, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
        Model: "gpt-4o", APIKey: os.Getenv("OPENAI_API_KEY"),
    })
    fallbackModel, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
        Model: "gpt-4o-mini", APIKey: os.Getenv("OPENAI_API_KEY"),
    })

    // 方式一:Component 原始实现(需自行管理一切)
    // runWithComponent(ctx, primaryModel, "我想退款订单 #12345,金额 680 元")

    // 方式二:ADK 封装实现(推荐)
    runWithADK(ctx, primaryModel, fallbackModel, "我想退款订单 #12345,金额 680 元")
}

Component vs ADK 对照

┌─────────────────────────────────────────────────────────────────┐
│                      Component 方式                              │
├─────────────────────────────────────────────────────────────────┤
│  自己手写:                                                      │
│  ├── Graph 拓扑 + Branch + Edge(~80 行)                         │
│  ├── 对话历史管理(state.msgs 手动 append)                       │
│  ├── System Prompt 注入(在 StatePreHandler 中拼接)              │
│  ├── 迭代上限保护(state.iteration vs state.maxIter)             │
│  ├── compose.WithCallbacks 手动注入                               │
│  ├── 调用 runnable.Invoke / Stream(无事件流)                    │
│  └── 不支持:事件流、Middleware、Checkpoint/Resume                 │
│      ModelRetry/Failover、SessionValues 跨 Agent 共享              │
├─────────────────────────────────────────────────────────────────┤
│  适用:需要深度定制 ReAct 循环逻辑的极少数场景                      │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                        ADK 方式                                  │
├─────────────────────────────────────────────────────────────────┤
│  框架提供(你只需配置):                                         │
│  ├── ReAct 循环 → ChatModelAgent 内置                            │
│  ├── 对话历史 → ChatModelAgentState.Messages 自动管理            │
│  ├── System Prompt → Instruction 字段 + {key} 渲染               │
│  ├── 迭代上限 → MaxIterations 配置                               │
│  ├── Middleware 拦截器 → 7 个生命周期钩子                         │
│  ├── ModelRetry / ModelFailover → 配置项开箱即用                  │
│  ├── 中断/恢复 → Runner + CheckPointStore + ResumableAgent      │
│  ├── 事件流 → AgentEvent + AsyncIterator                         │
│  ├── Session 共享 → SessionValues + GetSessionValue              │
│  └── Callback 自动注入到所有组件                                  │
├─────────────────────────────────────────────────────────────────┤
│  适用:99% 的场景                                                 │
└─────────────────────────────────────────────────────────────────┘

运行输出示例

[trace] ChatModel/call_model 耗时: 1.23s
[Agent] 第 N 轮模型调用,历史消息数: 3
[Agent] → 调用工具: search_knowledge({"query":"退款"})
[trace] Tool/call_tools 耗时: 5ms
[trace] ChatModel/call_model 耗时: 0.89s
[Agent] 第 N 轮模型调用,历史消息数: 5
[Agent] → 调用工具: request_approval({"order_id":"#12345","amount":680,"reason":"退款审核"})

⚠️  Agent 请求人工审批
   订单: #12345, 金额: ¥680.00, 原因: 退款审核

✅ 审批完成,Agent 继续:
🤖 您好,您的退款申请(订单 #12345,金额 ¥680.00)已通过审核。
   退款将在 3-5 个工作日内原路返回到您的支付账户。如有其他问题请随时联系。

本示例覆盖的知识点

# 知识点 对应章节 Component 体现 ADK 体现
1 ChatModel Generate/Stream 01-组件接口 runnable.Invoke EnableStreaming + 流式消费
2 Tool 自定义实现 01-组件接口 KnowledgeSearchTool 同(Tool 定义通用)
3 Graph 编排(节点/边/分支/环) 03-编排框架 手写完整 ReAct Graph ChatModelAgent 内置
4 图内状态(StatePreHandler) 03-编排框架 reactState + PreHandler 内置于 ChatModelAgentState
5 Agent/AgentEvent/AsyncIterator 04-ADK核心 无(直接调 Invoke) Runner + 事件流
6 Runner / Run / Resume 04-ADK核心 runner.Run + ResumeWithParams
7 Middleware 钩子 05-ChatModelAgent LoggingMiddleware
8 ModelRetry / ModelFailover 05-ChatModelAgent ModelRetryConfig + ModelFailoverConfig
9 ReturnDirectly 05-ChatModelAgent ReturnDirectly: {"request_approval": true}
10 Interrupt/Resume/CheckPointStore 07-中断与恢复 compose.NewInterruptAndRerunErr(无恢复) Runner 自动管理
11 gob 类型注册 07-中断与恢复 不需要(无序列化) init()schema.RegisterName
12 Callback 可观测性 08-Callback与可观测性 HandlerHelper + WithCallbacks 同(通用机制)
13 SessionValues 跨节点共享 06-多Agent协作 无(靠 state 结构体) WithSessionValues 注入