乐于分享
好东西不私藏

ADK Go 源码深度解读 05:LLM Flow 执行循环(系列最核心)

ADK Go 源码深度解读 05:LLM Flow 执行循环(系列最核心)

ADK 2.x-Go (05). LLM Flow 执行循环深度解析

系列第五篇,也是整个系列最核心的一篇 ⭐。internal/llminternal/base_flow.go 是 ADK 的「心脏」——所有 LLM Agent 的执行都汇聚到这里。本篇拆解 Flow.Run 的 step-loop 主循环、runOneStep 的五阶段、12 个 Request Processor 各做什么、callLLM 的回调三段式、handleFunctionCalls 的并发执行与响应合并、以及 transfer_to_agent 的后处理。第 04 篇讲 Runner 把控制权交给 agent 后,agent 内部跑的就是这套 Flow。理解了它,ADK Go 就理解了 80%。


一、Flow 结构体:执行引擎的全貌

第 02 篇讲过,llmAgent.run 会实例化一个 llminternal.Flow 并把所有配置(回调、工具、处理器链)喂给它。Flow 就是真正驱动 step-loop 的执行引擎。先看它的结构体:

type Flow struct {
    Model model.LLM                        // LLM 抽象(OpenAI/Gemini/Apigee)

    Tools             []tool.Tool            // 工具列表(来自 agent 的 State.Tools)
    RequestProcessors  []func(...) iter.Seq2[...]  // ⭐ 12 个请求处理器(有序链)
    ResponseProcessors []func(...) error          // 2 个响应处理器

    // 6 组回调(来自 llmagent.Config,第 02 篇讲过)
    BeforeModelCallbacks  []BeforeModelCallback
    AfterModelCallbacks   []AfterModelCallback
    OnModelErrorCallbacks []OnModelErrorCallback
    BeforeToolCallbacks   []BeforeToolCallback
    AfterToolCallbacks    []AfterToolCallback
    OnToolErrorCallbacks  []OnToolErrorCallback
}

关键认知:Flow 是一个纯执行器——它自己不持有 agent 配置,所有配置在构造时由 llmAgent.run 传入。这使得 Flow 可以专注于一件事:驱动 step-loop。两条处理器链(Request/Response Processors)是硬编码的默认值

// DefaultRequestProcessors:有序的请求处理器链,依次转换 LLMRequest
DefaultRequestProcessors = []func(ctx agent.InvocationContext, req *model.LLMRequest, f *Flow) iter.Seq2[*session.Event, error]{
    basicRequestProcessor,                // 1. 基础配置
    toolProcessor,                        // 2. 注入工具(含 HITL/认证工具)
    authPreprocessor,                     // 3. 认证
    RequestConfirmationRequestProcessor,  // 4. HITL 确认
    instructionsRequestProcessor,         // 5. 指令模板插值
    identityRequestProcessor,             // 6. agent 身份/全局指令
    ContentsRequestProcessor,             // 7. ⭐ 按 Branch/IsolationScope 过滤历史
    nlPlanningRequestProcessor,           // 8. NL Planning(必须在 contents 之后)
    codeExecutionRequestProcessor,        // 9. 代码执行(必须 contents 之后)
    outputSchemaRequestProcessor,         // 10. 结构化输出 schema
    AgentTransferRequestProcessor,        // 11. 注入 transfer_to_agent 工具
    removeDisplayNameIfExists,            // 12. 清理 displayName
}

DefaultResponseProcessors = []func(...) error{
    nlPlanningResponseProcessor,          // 1. NL Planning 后处理
    codeExecutionResponseProcessor,       // 2. 代码执行后处理
}

⚠️ 顺序至关重要

源码注释多次强调顺序依赖:nlPlanningRequestProcessor 必须在 ContentsRequestProcessor 之后(因为 NL Planning 会把 planning content 标记为 thought,需要 contents 先就位);codeExecutionRequestProcessor 也必须在 contents 之后(它会修改 contents 来优化数据文件)。不要随便调整这个顺序,会破坏历史隔离和 planning 机制。


二、Flow.Run:step-loop 主循环 ⭐

这是 ADK 最核心的循环。源码出奇地简洁——它的职责就是「反复跑 step,直到产生最终回复」:

func (f *Flow) Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
    return func(yield func(*session.Event, error) bool) {
        for {                                    // ⭐ 外层 step 循环
            var lastEvent *session.Event
            for ev, err := range f.runOneStep(ctx) {
                if err != nil { yield(nil, err); return }
                if !yield(ev, nil) { return }  // 先把事件透传出去
                lastEvent = ev
            }
            // 终止条件:最后事件是"最终回复"且不是纯 thinking turn
            if lastEvent == nil ||
               (lastEvent.IsFinalResponse() && !isThoughtOnlyTurn(lastEvent)) {
                return
            }
            if lastEvent.LLMResponse.Partial {
                yield(nil, fmt.Errorf("TODO: last event is not final"))  // 流式未完,待处理
                return
            }
        }
    }
}
  step-loop 的终止逻辑(最关键的判断)

  runOneStep 产出一批事件,记下 lastEvent
       │
       ▼
  ┌─────────────────────────────────────────────────────────┐
  │  lastEvent == nil?                                       │
  │    是 → return(空 step,结束)                          │
  │                                                         │
  │  lastEvent.IsFinalResponse() && !isThoughtOnlyTurn?     │
  │    是 → return(有最终回复,结束)⭐ 正常出口             │
  │                                                         │
  │  lastEvent.LLMResponse.Partial?                          │
  │    是 → 报错(流式中途断,TODO 待处理)                 │
  │                                                         │
  │  否则 → 继续 for 循环,跑下一个 step                     │
  └─────────────────────────────────────────────────────────┘

🔑 三个关键概念

① step = 1 次 LLM 调用 + 0~N 次工具调用。一个 step 里,LLM 被调用一次,如果它返回 function_call,则并发执行这些工具,把 function_response 拼回去,然后这个 step 结束。

② IsFinalResponse 决定循环是否结束。如果 LLM 没有调用工具、直接给了文本回复,这个事件就是「最终回复」,step-loop 结束。如果 LLM 调了工具,事件不是 final,循环继续——下一个 step 会带上工具结果再调一次 LLM。

③ isThoughtOnlyTurn 的特殊性。有些模型支持 thinking(Gemini 的 thought),一个 turn 可能全是思考没有答案——它报告 IsFinalResponse=true,但其实没有真正回答。!isThoughtOnlyTurn 确保不在纯思考 turn 上停下,而是再调一次模型要真正的答案。


三、runOneStep:一个 step 的五阶段 ⭐

runOneStep 是 step-loop 的内部细节。一个 step 分五个阶段:

  runOneStep 的五阶段流水线
  ──────────────────────────────────────────────────────────────────────────

  ① preprocess(ctx, req)
     跑 12 个 Request Processor + 工具预处理,可能产 HITL 事件
     req 被逐步填充:工具声明、指令、过滤后的历史、输出 schema...
       │
  ② callLLM(ctx, req, stateDelta, artifactDelta)
     Plugin.BeforeModel → agent.BeforeModelCallbacks → generateContent →
     OnModelError → AfterModelCallbacks(任何非 nil 回调可短路/替换)
     流式产出 responseWithEventID
       │
  ③ postprocess(ctx, req, resp)
     跑 2 个 Response Processor(NL Planning / 代码执行)
       │
  ④ finalizeModelResponseEvent(ctx, resp, tools, stateDelta)
     把 resp 包成 Event,填 Author/Branch/StateDelta/LongRunningToolIDs
     yield 模型回复事件
       │
  ⑤ handleFunctionCalls(ctx, tools, resp)
     如含 FunctionCall:并发执行工具,合并 FunctionResponse 成一个事件
     若 ev.Actions.TransferToAgent 非空 → 处理转移(见第八节)

精简后的源码:

func (f *Flow) runOneStep(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
    return func(yield ...) {
        req := &model.LLMRequest{Model: f.Model.Name()}

        // ① 预处理:12 个 Request Processor + 工具/toolset 预处理
        for ev, err := range f.preprocess(ctx, req) { ... }
        if ctx.Ended() { return }

        stateDelta := make(map[string]any)

        // ②~⑤ 对每个 LLM response:
        for resp, err := range f.callLLM(ctx, req, stateDelta, artifactDelta) {
            if err != nil { yield(nil, err); return }

            // ③ 后处理(NL Planning / 代码执行)
            if err := f.postprocess(ctx, req, resp); err != nil { ... }

            // 跳过空内容、无错误码、未中断的响应(代码执行器的特殊触发)
            if resp.Content == nil && resp.ErrorCode == "" && !resp.Interrupted { continue }

            // ④ 构造模型回复事件并 yield
            modelResponseEvent := f.finalizeModelResponseEvent(ctx, resp, tools, stateDelta)
            if !yield(modelResponseEvent, nil) { return }

            if resp.Partial { continue }  // 流式片段,继续等完整响应

            // ⑤ 处理工具调用
            ev, err := f.handleFunctionCalls(ctx, tools, resp.LLMResponse, nilnil)
            if ev == nil { continue }

            // yield 工具响应事件 + 可能的确认请求事件
            yield(ev, nil)
            if toolConfirmationEvent != nil { yield(toolConfirmationEvent, nil) }

            // 结构化输出:set_model_response 工具的结果作为最终回复
            if outputSchemaResponse != "" { yield(createFinalModelResponseEvent(...)) }

            // ⑤' transfer_to_agent 后处理(见第八节)
            if ev.Actions.TransferToAgent == "" { return }
            // ... 处理 transfer ...
        }
    }
}

四、12 个 Request Processor:请求预处理流水线

这是 preprocess 阶段的核心。12 个处理器依次转换 LLMRequest,每个都能修改 req 或产出事件(如 HITL 事件)。精简后的 preprocess:

func (f *Flow) preprocess(ctx agent.InvocationContext, req *model.LLMRequest) iter.Seq2[*session.Event, error] {
    return func(yield ...) {
        // 按配置顺序跑每个 Request Processor
        for _, processor := range f.RequestProcessors {
            for ev, err := range processor(ctx, req, f) {
                if err != nil { yield(nil, err); return }
                if ev != nil { yield(ev, nil) }
            }
        }
        // 然后跑工具/toolset 预处理(把工具声明注入 req.Tools)
        toolPreprocess(ctx, req, f.Tools)
        toolsetPreprocess(ctx, req)
    }
}

4.1 12 个处理器逐一解读

# 处理器 做什么
1 basicRequestProcessor 基础配置:把 agent 的 GenerateContentConfig(温度等)合并进 req。处理 OutputSchema 与 Mode 的兼容性
2 toolProcessor 注入工具:遍历 agent 的 Tools,把需要认证/HITL 的工具做标记。第 03 篇的工具装配最终在这里生效
3 authPreprocessor 认证:处理需要 OAuth/API key 的工具,可能产 HITL 事件让用户授权
4 RequestConfirmationRequestProcessor HITL 确认:检查上一轮是否有 pending 的确认请求,决定本轮是否需要注入确认工具
5 instructionsRequestProcessor 指令插值:把 {var} 模板用 session state 填充(第 02 篇讲的 Instruction 模板机制)
6 identityRequestProcessor agent 身份:拼接 GlobalInstruction(仅 root 生效,第 02 篇讲过),注入 agent 名字等身份信息
7 ContentsRequestProcessor 历史过滤:按 Branch/IsolationScope 过滤会话历史,是 multi-agent 历史隔离的根本机制。Task 模式还要处理结构化输入
8 nlPlanningRequestProcessor NL Planning:自然语言规划,把 planning content 标记为 thought。必须在 contents 之后
9 codeExecutionRequestProcessor 代码执行:修改 contents 来优化数据文件(如内联大文件)。必须在 contents 之后
10 outputSchemaRequestProcessor 结构化输出:若设了 OutputSchema 且非 Task 模式,注入 set_model_response 工具(第 03 篇提过)
11 AgentTransferRequestProcessor 注入 transfer_to_agent 工具:根据子 agent 列表生成可转移目标(第 08 篇详讲)
12 removeDisplayNameIfExists 清理:移除 displayName 字段(兼容性处理)

🔑 第 7 个处理器 ContentsRequestProcessor 是 multi-agent 的灵魂

这是 12 个处理器里最复杂、也最重要的一个。它决定「这个 agent 在这次 step 里能看到哪些历史事件」。过滤逻辑基于两个维度:

  • Branch:事件用 a1.a2.a3 这样的分支路径标记。agent 只看到自己分支及祖先分支的事件——这就是 transfer 后历史隔离的原理(第 08 篇详讲)。
  • IsolationScope:Task 模式的隔离作用域。同一 isolation scope 的事件才可见,保证 task 子 agent 的多轮收集不被其他 task 污染。

没有这个处理器,所有 agent 都会看到完整历史,multi-agent 系统就会乱套。


五、callLLM:回调三段式 ⭐

callLLM 是 step 内部最复杂的部分——它围绕真正的模型调用织入了 Before/OnError/After 三段回调。核心逻辑:

func (f *Flow) callLLM(ctx, req, stateDelta, artifactDelta) iter.Seq2[*responseWithEventID, error] {
    return func(yield ...) {
        pluginManager := pluginManagerFromContext(ctx)

        // ① Plugin.BeforeModel(全局插件优先)
        if pluginManager != nil {
            callbackResponse, callbackErr := pluginManager.RunBeforeModelCallback(cctx, req)
            if callbackResponse != nil || callbackErr != nil {
                yield(..., callbackErr); return  // ⭐ 短路:跳过真正的模型调用
            }
        }

        // ② agent.BeforeModelCallbacks(agent 自己的回调)
        for _, callback := range f.BeforeModelCallbacks {
            callbackResponse, callbackErr := callback(cctx, req)
            if callbackResponse != nil || callbackErr != nil {
                yield(..., callbackErr); return  // ⭐ 短路
            }
        }

        // ③ 真正调用模型(流式或非流式)
        useStream := runconfig.FromContext(ctx).StreamingMode == runconfig.StreamingModeSSE
        for resp, err := range generateContent(ctx, f.Model, req, useStream) {
            if err != nil {
                // ③' OnModelError:模型出错时的兜底(重试/降级)
                cbResp, cbErr := f.runOnModelErrorCallbacks(ctx, req, ..., err)
                if cbResp == nil { yield(nil, err); return }
                resp = ...  // 用兜底响应替换
            }

            // 给 function_call 补 ID(有些模型不填)
            utils.PopulateClientFunctionCallID(ctx, resp.Content)

            // ④ AfterModelCallbacks:响应后处理(日志/metrics/改写)
            callbackResp, callbackErr := f.runAfterModelCallbacks(ctx, resp.LLMResponse, ..., err)
            if callbackResp != nil { yield(替换后的响应); continue }

            yield(resp, nil)
        }
    }
}
  callLLM 的回调三段式(短路语义)

  Plugin.BeforeModel ─┐ 非nil → 短路,跳过模型调用,用回调返回值
                       │
  agent.BeforeModelCallbacks ─┤ 非nil → 短路
                       │
                       ▼ 全部返回 nil
  generateContent(真正调模型)
                       │
            ┌──────────┴──────────┐
            ▼ 出错                  ▼ 成功
  OnModelErrorCallbacks     AfterModelCallbacks
   非nil → 替换错误             非nil → 替换响应
   nil → 透传错误               nil → 透传响应

🔑 回调三段式的两个铁律

铁律一:plugin 先于 agent callback 执行。看源码顺序——先 pluginManager.RunBeforeModelCallback,后 f.BeforeModelCallbacks。这意味着 plugin 有「全局拦截至高优先级」(第 11 篇详讲)。

铁律二:任何 Before 回调返回非 nil 即短路callbackResponse != nil || callbackErr != nil 一旦成立,立即 yield 并 return——真正的 generateContent 根本不会执行。这是实现「LLM 缓存」(BeforeModel 返回缓存响应)、「请求审计/改写」的标准钩子。

5.1 BeforeModel 短路的实际应用:LLM 缓存

func cacheCallback(ctx agent.Context, req *model.LLMRequest) (*model.LLMResponse, error) {
    key := hash(req)  // 根据请求内容算缓存 key
    if cached := cache.Get(key); cached != nil {
        return cached, nil  // ⭐ 返回非 nil,跳过真正的模型调用(省钱!)
    }
    return nilnil      // 返回 nil,继续正常调模型
}

a, _ := llmagent.New(llmagent.Config{
    Model: openaiModel,
    BeforeModelCallbacks: []llmagent.BeforeModelCallback{cacheCallback},
})

六、finalizeModelResponseEvent:构造模型回复事件

拿到模型响应后,要把它包装成一个 session.Event。这个函数很简短但字段语义重要:

func (f *Flow) finalizeModelResponseEvent(ctx, resp, tools, stateDelta) *session.Event {
    // 补全 function_call 的 ID(有些模型不返回 ID)
    utils.PopulateClientFunctionCallID(ctx, resp.Content)

    ev := session.NewEvent(ctx, ctx.InvocationID())
    ev.ID = resp.eventID
    ev.Author = ctx.Agent().Name()       // 谁产的就标谁的名(findAgentToRun 用)
    ev.Branch = ctx.Branch()             // 分支标记(ContentsRequestProcessor 过滤用)
    ev.LLMResponse = *resp.LLMResponse
    ev.Actions.StateDelta = stateDelta  // 回调期间累积的状态变更

    // 识别 long-running 工具调用(第 03 篇讲过 IsLongRunning)
    ev.LongRunningToolIDs = findLongRunningFunctionCallIDs(resp.Content, tools)

    return ev
}

💡 每个字段都对应后续某个机制

这个事件结构的每个字段都不是摆设:

  • Author → 第 04 篇 findAgentToRun 按它找该谁接管
  • Branch → 第 7 号 ContentsRequestProcessor 按它过滤历史
  • StateDelta → 回调里 ctx.State().Set() 的变更都累积在这里,最终由 Runner 持久化
  • LongRunningToolIDs → 第 04 篇 HITL 恢复的 openLongRunningCallIDs 按它找挂起的调用
  • TransferToAgent(在 Actions 里,由工具调用阶段填)→ 第八节的转移逻辑

一个事件结构串起了 Runner(04 篇)、Flow(本篇)、多 Agent(08 篇)、HITL(10 篇)的所有机制。


七、handleFunctionCalls:并发工具调用与响应合并 ⭐

当 LLM 返回的 Content 里含 FunctionCall 时,进入这个阶段。ADK 会并发执行所有 function call,再把结果合并成一个事件。核心逻辑:

func (f *Flow) handleFunctionCalls(ctx, toolsDict, resp, toolConfirmations, liveSess) (*session.Event, error) {
    fnCalls := utils.FunctionCalls(resp.Content)  // 取出所有 function_call

    fnResponseEvents := make([]*session.Event, len(fnCalls))

    // ① 为每个 function_call 构造一个 task
    tasks := make([]func(context.Context)len(fnCalls))
    for i, fnCall := range fnCalls {
        tasks[i] = func(taskCtx context.Context) {
            toolCtx := agent.NewToolContext(...)  // 每个工具独立上下文

            // 按工具类型分发(见 7.1)
            if streamTool, ok := curTool.(StreamingFunctionTool); ok {
                // 流式工具:边算边吐
            } else if funcTool, ok := curTool.(FunctionTool); ok {
                result = f.callTool(toolCtx, funcTool, fnCall.Args)  // 普通工具
            }

            // 构造 function_response 事件
            ev := session.NewEvent(ctx, ctx.InvocationID())
            ev.LLMResponse.Content.Parts = [{FunctionResponse: {ID: fnCall.ID, Name: fnCall.Name, Response: result}}]
            ev.Actions = *toolCtx.Actions()  // 工具里的状态变更
            fnResponseEvents[i] = ev
        }
    }

    // ② ⭐ 并发执行所有 task(platform.RunTasks)
    platform.RunTasks(ctx, tasks)

    // ③ 合并所有 function_response 成一个事件
    mergedEvent, err := mergeParallelFunctionResponseEvents(fnResponseEvents)
    return mergedEvent, err
}

7.1 工具分发的三种情况

handleFunctionCalls 内部对每个 function_call,按工具实现的接口分三种情况:

工具类型 处理方式
StreamingFunctionTool Live 模式下异步执行,边算边通过 liveSess.Send 吐结果;非 Live 模式同步收集所有 chunk 拼成结果
FunctionTool(普通) f.callTool(内部走 BeforeTool 回调 → tool.Run → OnToolError → AfterTool 回调链,第 03 篇讲过)
找不到工具 / 非可执行工具 构造 tool not found 错误,走 runOnToolErrorCallbacks 兜底

7.2 platform.RunTasks:并发执行的 seam

注意 platform.RunTasks(ctx, tasks)——这不是 go 关键字,而是一个可替换的 seam

💡 为什么用 platform.RunTasks 而非 go?

platform 包提供可注入的执行器。默认实现用 goroutine 并发跑所有 task。但测试时可以注入串行 task runnerplatform.WithTaskRunner),让并发变成可预测的顺序执行——这是 ADK 能为并发场景写确定性测试的关键(第 06 篇详讲)。

同理,platform 还提供 WithTimeProvider(fake clock)和 WithUUIDProvider(顺序 UUID),三者配合让无网络、无模型的离线测试成为可能。

7.3 mergeParallelFunctionResponseEvents:状态合并

并发执行的 N 个工具各自产出一个 event(含自己的 StateDelta、TransferToAgent 等),最后要合并成一个。合并规则:

  • FunctionResponse:简单拼接,所有工具的响应都放进一个 Content。
  • StateDeltadeepMergeMap 递归合并。两个工具都写了 {"user": {"name": "x"}}{"user": {"age": 20}},合并成 {"user": {"name":"x","age":20}}
  • TransferToAgent / Escalate:「最后一个赢」。这是「决策型」动作,不能合并——两个工具都要转移,只能听最后一个的。

⚠️ 并发写状态的注意事项

因为工具并发执行,每个工具在自己的 ToolContext 里写 Actions.StateDelta,互不干扰。合并时才汇总。但如果你在工具里用了共享的外部资源(如全局变量、外部数据库),仍然需要自己处理并发安全——ADK 只隔离了 state 写入,不隔离你的业务副作用。


八、transfer_to_agent 后处理:控制权移交

runOneStep 末尾有一段重要逻辑——如果工具调用阶段产生了 TransferToAgent 动作,Flow 会在这里处理控制权移交:

// Actually handle "transfer_to_agent" tool. The function call sets the
// ev.Actions.TransferToAgent field. We are following python's execution flow.
if ev.Actions.TransferToAgent == "" {
    return  // 没有转移,step 结束
}

// 找到目标 agent
nextAgent := f.agentToRun(ctx, ev.Actions.TransferToAgent)
if nextAgent == nil { yield(nil, fmt.Errorf("failed to find agent: %s", ...)); return }

// ⭐ 关键:根据目标 agent 是否实现 nodeRunner,走两条路径
type nodeRunner interface {
    RunNode(ctx agent.Context, nodeInput any) iter.Seq2[*session.Event, error]
}
var nextStream iter.Seq2[*session.Event, error]
if nr, ok := nextAgent.(nodeRunner); ok {
    nextStream = nr.RunNode(agent.Promote(ctx), nil)  // Node 路径(LlmAgent)
else {
    nextStream = nextAgent.Run(ctx)                       // Agent 路径(其他)
}

// forward 目标 agent 的所有事件
for ev, err := range nextStream {
    if !yield(ev, err) || err != nil { return }
}
  transfer_to_agent 的执行流

  当前 agent 的某次 step
    LLM 返回 transfer_to_agent("weather_agent")
    handleFunctionCalls 把它当工具调用,设置 ev.Actions.TransferToAgent
       │
       ▼
  f.agentToRun(ctx, "weather_agent") 找到目标 agent
       │
       ▼ 判断目标 agent 类型
  ┌────────────────────┬─────────────────────┐
  │ LlmAgent(nodeRunner)│ 其他 agent          │
  │ → nr.RunNode()      │ → nextAgent.Run()   │
  │   走 Node 路径       │   走 Agent 路径      │
  └────────────────────┴─────────────────────┘
       │
       ▼
  forward 目标 agent 的所有事件 给上层(Runner)
  目标 agent 接管后续对话——下一次 findAgentToRun 会选中它

🔑 transfer 的两个关键设计

① RunNode vs Run 的选择:如果目标 agent 实现了 nodeRunner 接口(LlmAgent 通过 RunNode 方法实现),走 RunNode(Node 路径,获得 HITL/并行能力);否则走 Run(Agent 路径)。这呼应第 04 篇 Runner 的两条路径——但这里是在 Flow 内部、跨 agent 的转移

② forward 所有事件:转移后,目标 agent 的事件流被原样 yield 给上层(Runner)。上层并不知道发生了转移,只看到一连串事件。下一次用户消息时,第 04 篇的 findAgentToRun 会因为历史里有目标 agent 的事件,而让目标 agent 接管。

这就是 multi-agent 协作的核心机制——第 08 篇会深入对比 transfer vs agent-as-tool。


九、小结与动手清单

核心要点回顾

🎯 本篇五个关键认知(系列最核心)

  1. step-loop 是 ADK 的心脏Flow.Run 反复跑 runOneStep,直到产生最终回复(IsFinalResponse 且非纯 thinking turn)。一个 step = 1 次 LLM 调用 + 0~N 次工具调用。
  2. runOneStep 五阶段:preprocess(12 个 Request Processor)→ callLLM(回调三段式)→ postprocess(2 个 Response Processor)→ finalizeModelResponseEvent(构造事件)→ handleFunctionCalls(并发工具 + 合并)。顺序固定,不可乱。
  3. 12 个 Request Processor 是有序流水线:尤其第 7 个 ContentsRequestProcessor 按 Branch/IsolationScope 过滤历史,是 multi-agent 历史隔离的根本。顺序有强依赖(nlPlanning/codeExecution 必须在 contents 之后)。
  4. callLLM 回调三段式有两个铁律:plugin 先于 agent callback;任何 Before 回调返回非 nil 即短路(可实现 LLM 缓存)。OnError 是出错兜底,After 可改写响应。
  5. handleFunctionCalls 并发执行 + 合并platform.RunTasks(可替换 seam,测试可注入串行 runner)并发跑所有 function call,mergeParallelFunctionResponseEvents 深度合并 StateDelta,决策型动作(TransferToAgent)「最后赢」。转移靠 forward 目标 agent 事件实现。

动手清单(建议 2-3 天,这是最烧脑的一篇)

  1. 追踪一次完整 step:在 quickstart 里,给 agent 挂一个 functiontool。问一个会触发工具调用的问题,用 BeforeModelCallbacksBeforeToolCallbacks 打印每一步,对照本篇的五阶段图,画出「preprocess → callLLM → finalize → handleFunctionCalls → 下一个 step(带工具结果)→ 最终回复」的完整链路。
  2. 体验 IsFinalResponse 的差别:观察「LLM 调了工具」和「LLM 直接回答」两种情况下,step-loop 的行为差异——前者继续循环,后者结束。
  3. 实现一个 LLM 缓存 callback:用 BeforeModelCallback 实现简单缓存(key 用请求内容的 hash),验证第二次相同请求会短路、不调模型。
  4. 观察并发工具调用:写两个会 sleep 的工具,让 LLM 一次调用它们两个,打印执行时间——验证它们是并发的(总时间≈max 而非 sum)。
  5. 追踪 transfer 的事件流:构造 root + Chat 子 agent,问一个属于子 agent 的问题。观察事件流的 Author 如何从 root 变成子 agent——这就是 transfer 的 forward 机制。
  6. (进阶)注入串行 task runner:用 platform.WithTaskRunner 注入串行执行器,写一个并发工具的测试,验证它变得可预测。

下一篇预告

第 **06 篇「并发工具调用与 platform seam」**将深入本篇提到的 platform.RunTasks 与状态合并细节——deepMergeMap 的递归合并规则、TransferToAgent「last wins」的决策合并、WithTimeProvider/WithUUIDProvider/WithTaskRunner 三大可注入点如何让并发场景的确定性测试成为可能。本篇打开了 handleFunctionCalls 的门,下一篇就走进去细看。


ADK 2.x-Go · 源码深度解读 · 05 · LLM Flow 执行循环(系列最核心 ⭐) 核心源码:internal/llminternal/base_flow.go(1422 行,ADK 的心脏) 配套:12 个 Processor 实现分布在 internal/llminternal/*_processor.go 本系列代码示例统一使用 OpenAI 模型 · 基于 google/adk-go v2 源码整理