目录 · 第 10 / 28 章
EinoPart II · ADK 的设计

10中间件与 Handler 的设计

六个钩子、Middlewares 先于 Handlers 的执行序、两类职责与内置件分类学。

adk/handler.go

从”会用”到”看懂设计”

第 6 章你已经会叠中间件了。这一章我们回到设计层面,回答三个”为什么”:为什么 ADK 有两套中间件类型?九个钩子为什么这样切?十几个内置件如何归类?看懂这些,你就能判断”该用哪套、该实现哪个钩子、该往哪层塞逻辑”。

两套中间件:struct vs interface 的世代之争

翻开源码你会发现 ADK 里并存两种中间件:老的 AgentMiddleware(adk/chatmodel.go:239)是一个结构体,新的 TypedChatModelAgentMiddleware[M](adk/handler.go:139)是一个接口。这不是重复造轮子,而是一次有意的世代升级。

// Deprecated: Use ChatModelAgentMiddleware (interface-based Handlers) instead.
// AgentMiddleware will be removed in a future release.
//
// AgentMiddleware provides hooks to customize agent behavior at various stages of execution.
type AgentMiddleware struct {
// AdditionalInstruction adds supplementary text to the agent's system instruction.
// This instruction is concatenated with the base instruction before each chat model call.
AdditionalInstruction string
// AdditionalTools adds supplementary tools to the agent's available toolset.
// These tools are combined with the tools configured for the agent.
AdditionalTools []tool.BaseTool
// BeforeChatModel is called before each ChatModel invocation, allowing modification of the agent state.
BeforeChatModel func(context.Context, *ChatModelAgentState) error
// AfterChatModel is called after each ChatModel invocation, allowing modification of the agent state.
AfterChatModel func(context.Context, *ChatModelAgentState) error
// WrapToolCall wraps tool calls with custom middleware logic.
// Each middleware contains Invokable and/or Streamable functions for tool calls.
WrapToolCall compose.ToolMiddleware
}
// TypedChatModelAgentMiddleware defines the interface for customizing TypedChatModelAgent behavior.
//
// IMPORTANT: This interface is specifically designed for TypedChatModelAgent and agents built
// on top of it (e.g., DeepAgent).
//
// Why TypedChatModelAgentMiddleware instead of AgentMiddleware?
//
// AgentMiddleware is a struct type, which has inherent limitations:
// - Struct types are closed: users cannot add new methods to extend functionality
// - The framework only recognizes AgentMiddleware's fixed fields, so even if users
// embed AgentMiddleware in a custom struct and add methods, the framework cannot
// call those methods (config.Middlewares is []AgentMiddleware, not a user type)
// - Callbacks in AgentMiddleware only return error, cannot return modified context
//
// TypedChatModelAgentMiddleware is an interface type, which is open for extension:
// - Users can implement custom handlers with arbitrary internal state and methods
// - Hook methods return (context.Context, ..., error) for direct context propagation
// - Wrapper methods (WrapToolCall, WrapModel) enable context propagation through the
// wrapped endpoint chain: wrappers can pass modified context to the next wrapper
// - Configuration is centralized in struct fields rather than scattered in closures
//
// TypedChatModelAgentMiddleware vs AgentMiddleware:
// - Use AgentMiddleware for simple, static additions (extra instruction/tools)
// - Use TypedChatModelAgentMiddleware for dynamic behavior, context modification, or call wrapping
// - AgentMiddleware is kept for backward compatibility with existing users
// - Both can be used together; see AgentMiddleware documentation for execution order
//
// Use *TypedBaseChatModelAgentMiddleware as an embedded struct to provide default no-op
// implementations for all methods.
type TypedChatModelAgentMiddleware[M MessageType] interface {
// BeforeAgent is called before each agent run, allowing modification of
// the agent's instruction and tools configuration.
BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)
// AfterAgent is called after the agent run reaches a successful terminal state.
// Successful terminal states are: final answer (model response with no tool calls),
// and return-directly tool result.
//
// AfterAgent is NOT called when the agent terminates with an error (e.g.,
// ErrExceedMaxIterations, context cancellation, model errors).
//
// The state parameter contains the final conversation state, including all messages
// from the completed run.
//
// AfterAgent handlers are called in the same order as BeforeAgent handlers
// (first registered = first called). Consistent with all other middleware hooks,
// if any handler returns an error, subsequent handlers are NOT called (fail-fast)
// and the error is sent to the event stream.
// … 省略 98 行;完整声明 L110–255,点击上方「浏览完整文件」

老的结构体版长这样:

type AgentMiddleware struct {
AdditionalInstruction string
AdditionalTools []tool.BaseTool
BeforeChatModel func(context.Context, *ChatModelAgentState) error
AfterChatModel func(context.Context, *ChatModelAgentState) error
WrapToolCall compose.ToolMiddleware
}

它简单,但源码在 adk/handler.go:117 直白地列出了它的天花板:

// TypedChatModelAgentMiddleware defines the interface for customizing TypedChatModelAgent behavior.
//
// IMPORTANT: This interface is specifically designed for TypedChatModelAgent and agents built
// on top of it (e.g., DeepAgent).
//
// Why TypedChatModelAgentMiddleware instead of AgentMiddleware?
//
// AgentMiddleware is a struct type, which has inherent limitations:
// - Struct types are closed: users cannot add new methods to extend functionality
// - The framework only recognizes AgentMiddleware's fixed fields, so even if users
// embed AgentMiddleware in a custom struct and add methods, the framework cannot
// call those methods (config.Middlewares is []AgentMiddleware, not a user type)
// - Callbacks in AgentMiddleware only return error, cannot return modified context
//
// TypedChatModelAgentMiddleware is an interface type, which is open for extension:
// - Users can implement custom handlers with arbitrary internal state and methods
// - Hook methods return (context.Context, ..., error) for direct context propagation
// - Wrapper methods (WrapToolCall, WrapModel) enable context propagation through the
// wrapped endpoint chain: wrappers can pass modified context to the next wrapper
// - Configuration is centralized in struct fields rather than scattered in closures
//
// TypedChatModelAgentMiddleware vs AgentMiddleware:
// - Use AgentMiddleware for simple, static additions (extra instruction/tools)
// - Use TypedChatModelAgentMiddleware for dynamic behavior, context modification, or call wrapping
// - AgentMiddleware is kept for backward compatibility with existing users
// - Both can be used together; see AgentMiddleware documentation for execution order
//
// Use *TypedBaseChatModelAgentMiddleware as an embedded struct to provide default no-op
// … 这是 L110–137 的片段(该行不在任何顶层声明内);点击上方「浏览完整文件」
  • 结构体是封闭的:用户无法给它加新方法来扩展能力。
  • 框架只认它的固定字段:哪怕你内嵌它、加了自己的方法,框架也调不到——因为 config.Middlewares 的类型是 []AgentMiddleware,不是你的类型。
  • 回调只能返回 error:改不了 context,没法把修改后的上下文往下传。

🔑 本章的设计钥匙

从 struct 升级到 interface,本质是把中间件从”填空题”变成”开放实现”。结构体版让你填几个预定义的字段(封闭、有限);接口版让你实现一个类型——可以带任意内部状态和方法,钩子还能返回修改后的 context 往下传播。这正是”面向接口而非实现”在框架扩展点上的体现:框架只依赖接口,把”能扩展成什么样”的权力完全交给你。结构体版被保留只为向后兼容,新代码一律用接口版。

为什么钩子返回 context 如此重要

接口版每个钩子都返回 (context.Context, ..., error),而不是只返回 error。这个细节是整套设计的枢纽。

想象一个链路追踪中间件:它在 BeforeModelRewriteState 里往 ctx 里塞一个 span,后续的模型调用、工具执行都该看到这个 span。如果钩子不能返回 ctx,你塞进去的东西就断在原地,传不下去。接口版通过”钩子返回新 ctxWrap 包裹链传递 ctx”(adk/handler.go:124),让上下文能在整条调用链上连续流动。这是结构体版的 func(...) error 永远做不到的。

// TypedChatModelAgentMiddleware defines the interface for customizing TypedChatModelAgent behavior.
//
// IMPORTANT: This interface is specifically designed for TypedChatModelAgent and agents built
// on top of it (e.g., DeepAgent).
//
// Why TypedChatModelAgentMiddleware instead of AgentMiddleware?
//
// AgentMiddleware is a struct type, which has inherent limitations:
// - Struct types are closed: users cannot add new methods to extend functionality
// - The framework only recognizes AgentMiddleware's fixed fields, so even if users
// embed AgentMiddleware in a custom struct and add methods, the framework cannot
// call those methods (config.Middlewares is []AgentMiddleware, not a user type)
// - Callbacks in AgentMiddleware only return error, cannot return modified context
//
// TypedChatModelAgentMiddleware is an interface type, which is open for extension:
// - Users can implement custom handlers with arbitrary internal state and methods
// - Hook methods return (context.Context, ..., error) for direct context propagation
// - Wrapper methods (WrapToolCall, WrapModel) enable context propagation through the
// wrapped endpoint chain: wrappers can pass modified context to the next wrapper
// - Configuration is centralized in struct fields rather than scattered in closures
//
// TypedChatModelAgentMiddleware vs AgentMiddleware:
// - Use AgentMiddleware for simple, static additions (extra instruction/tools)
// - Use TypedChatModelAgentMiddleware for dynamic behavior, context modification, or call wrapping
// - AgentMiddleware is kept for backward compatibility with existing users
// - Both can be used together; see AgentMiddleware documentation for execution order
//
// Use *TypedBaseChatModelAgentMiddleware as an embedded struct to provide default no-op
// implementations for all methods.
type TypedChatModelAgentMiddleware[M MessageType] interface {
// BeforeAgent is called before each agent run, allowing modification of
// the agent's instruction and tools configuration.
BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)
// AfterAgent is called after the agent run reaches a successful terminal state.
// … 这是 L110–144 的片段(该行不在任何顶层声明内);点击上方「浏览完整文件」

九个钩子的分类学

第 6 章列过九个钩子,这里我们按”作用对象 × 时机”给它们建一张坐标图,理解切法背后的意图:

层级钩子类型用途
AgentBeforeAgent / AfterAgent观测·改写进入前改指令/工具集,成功收尾时观测
模型BeforeModelRewriteState / AfterModelRewriteState观测·改写(持久化)每次调模型前后改消息/工具,改动进 state
工具WrapInvokableToolCall 等四个包裹围绕一次工具执行插逻辑
模型调用WrapModel包裹围绕模型调用本身:重试/降级/发事件

切法的意图有三条线索:

  1. Before/After(钩子)vs Wrap(包裹)。前者是”在某个时刻被叫一下”,适合观测和改状态;后者给你原始端点、你返回一个新端点,适合”在调用前后都插手”的洋葱式逻辑。
  2. 持久化边界BeforeModelRewriteState 的改动会写进 state、影响后续迭代;而 WrapModel 的改动只作用于单次调用。这条边界决定了”改消息去哪改”——源码在 adk/handler.go:254 明确劝退在 WrapModel 里改输入,因为它不持久化、还打断 prompt 缓存。

3. 工具钩子为何有四个。标准/增强 × 同步/流式,共四个 Wrap*ToolCall。因为工具有两代接口(裸字符串 vs 结构化 ToolArgument)和两种执行模式(同步 vs 流式),框架为每种组合都留了精确的包裹点,而不是用一个笼统的钩子糊过去。

执行序:一张必须记住的时序图

多个中间件 + 框架内部逻辑叠在一起,顺序不能靠猜。源码在 adk/chatmodel.go:323 给出了权威的模型调用生命周期(从最外层到最内层):

// TypedChatModelAgentConfig is the generic configuration for ChatModelAgent.
type TypedChatModelAgentConfig[M MessageType] struct {
// Name of the agent. Better be unique across all agents.
// Optional. If empty, the agent can still run standalone but cannot be used as
// a sub-agent tool via NewAgentTool (which requires a non-empty Name).
Name string
// Description of the agent's capabilities.
// Helps other agents determine whether to transfer tasks to this agent.
// Optional. If empty, the agent can still run standalone but cannot be used as
// a sub-agent tool via NewAgentTool (which requires a non-empty Description).
Description string
// Instruction used as the system prompt for this agent.
// Optional. If empty, no system prompt will be used.
// Supports f-string placeholders for session values in default GenModelInput, for example:
// "You are a helpful assistant. The current time is {Time}. The current user is {User}."
// These placeholders will be replaced with session values for "Time" and "User".
Instruction string
// Model is the chat model used by the agent.
// If your ChatModelAgent uses any tools, this model must support the model.WithTools
// call option, as that's how ChatModelAgent configures the model with tool information.
Model model.BaseModel[M]
ToolsConfig ToolsConfig
// GenModelInput transforms instructions and input messages into the model's input format.
// Optional. Defaults to defaultGenModelInput which combines instruction and messages.
GenModelInput TypedGenModelInput[M]
// Exit defines the tool used to terminate the agent process.
// Optional. If nil, no Exit Action will be generated.
// You can use the provided 'ExitTool' implementation directly.
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
Exit tool.BaseTool
// OutputKey stores the agent's response in the session.
// Optional. When set, stores output via AddSessionValue(ctx, outputKey, msg.Content).
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
OutputKey string
// MaxIterations defines the upper limit of ChatModel generation cycles.
// The agent will terminate with an error if this limit is exceeded.
// … 省略 109 行;完整声明 L259–415,点击上方「浏览完整文件」
1. AgentMiddleware.BeforeChatModel (老·钩子)
2. ChatModelAgentMiddleware.BeforeModelRewriteState (新·钩子,可改 state)
3. failoverModelWrapper (内部·多模型故障切换)
4. retryModelWrapper (内部·重试)
5. eventSenderModelWrapper (内部·发送模型事件)
6. ChatModelAgentMiddleware.WrapModel (新·包裹,先注册=最外层)
7. callbackInjectionModelWrapper (内部·注入回调)
8. Model.Generate/Stream ← 真正的模型调用
9. ChatModelAgentMiddleware.AfterModelRewriteState (新·钩子)
10. AgentMiddleware.AfterChatModel (老·钩子)
flowchart TB
  subgraph OLD["老 AgentMiddleware(最外层)"]
    B1["1 BeforeChatModel"]
    subgraph NEW["新 Handlers"]
      B2["2 BeforeModelRewriteState(改 state)"]
      subgraph INT["框架内部 wrapper(洋葱)"]
        W1["3 failover → 4 retry → 5 eventSender"]
        W2["6 WrapModel(先注册=最外)"]
        W3["7 callbackInjection"]
        CALL["8 Model.Generate/Stream ← 真正调用"]
        W1 --> W2 --> W3 --> CALL
      end
      A2["9 AfterModelRewriteState"]
    end
    A1["10 AfterChatModel"]
  end
  B1 --> B2 --> INT --> A2 --> A1

模型调用生命周期:老 Middlewares 包着新 Handlers

三条纪律从这张图里跳出来:

  • Middlewares 包在新 Handlers 外面BeforeChatModel(第 1 步)先于 BeforeModelRewriteState(第 2 步);对称地,AfterChatModel(第 10 步)最后收尾。文档在 adk/chatmodel.go:320 明确:Handlers 在 Middlewares 之后处理。

  • 先注册 = 最外层。多个 WrapModel 按注册顺序 [A, B, C] 组成 A(B(C(model)))(adk/chatmodel.go:349)。想让某件事最后看到原始输出,就把它注册在最内层——EventSenderModelWrapper 就是这么用的。

  • 框架内部件也在链上。重试、故障切换、事件发送不是散落的 if,而是作为一层层 wrapper 精确排在你的中间件之间。工具调用有一条对称的生命周期(adk/chatmodel.go:356)。

📝 跨件与跨中断传值

中间件之间、乃至跨中断续跑传数据,用 SetRunLocalValue(adk/handler.go:338)/ GetRunLocalValue(adk/handler.go:366)。值绑定到”当前这次 Run”,并会被序列化进 checkpoint——所以它天然兼容第 5 章的 HITL。要发自定义事件到事件流,用 SendEvent(adk/handler.go:425)。

// SetRunLocalValue sets a key-value pair that persists for the duration of the current agent Run() invocation.
// The value is scoped to this specific execution and is not shared across different Run() calls or agent instances.
//
// Values stored here are compatible with interrupt/resume cycles - they will be serialized and restored
// when the agent is resumed. For custom types, you must register them using schema.RegisterName[T]()
// in an init() function to ensure proper serialization.
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution.
// Returns an error if called outside of an agent execution context.
func SetRunLocalValue(ctx context.Context, key string, value any) error {
if err := checkGobEncodability(key, value); err != nil {
return err
}
err := processTypedState(ctx, func(extra map[string]any) map[string]any {
if extra == nil {
extra = make(map[string]any)
}
extra[key] = value
return extra
})
if err != nil {
return fmt.Errorf("SetRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err)
}
return nil
}
// GetRunLocalValue retrieves a value that was set during the current agent Run() invocation.
// The value is scoped to this specific execution and is not shared across different Run() calls or agent instances.
//
// Values stored via SetRunLocalValue are compatible with interrupt/resume cycles - they will be serialized
// and restored when the agent is resumed. For custom types, you must register them using schema.RegisterName[T]()
// in an init() function to ensure proper serialization.
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution.
// Returns the value and true if found, or nil and false if not found or if called outside of an agent execution context.
func GetRunLocalValue(ctx context.Context, key string) (any, bool, error) {
var val any
var found bool
err := processTypedState(ctx, func(extra map[string]any) map[string]any {
if extra != nil {
val, found = extra[key]
}
return extra
})
if err != nil {
return nil, false, fmt.Errorf("GetRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err)
}
return val, found, nil
}
// SendEvent sends a custom AgentEvent to the event stream during agent execution.
// This allows ChatModelAgentMiddleware implementations to emit custom events that will be
// received by the caller iterating over the agent's event stream.
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution.
// Returns an error if called outside of an agent execution context.
func SendEvent(ctx context.Context, event *AgentEvent) error {
return TypedSendEvent(ctx, event)
}

内置件的两类职责

adk/middlewares/ 下的内置件不是随意堆砌,按职责能清晰分成两类:

  • 给能力(注入工具):filesystem(adk/middlewares/filesystem/filesystem.go:395,文件读写/grep/glob)、plantask(任务清单)、skill(加载 SKILL.md)、toolsearch(动态工具检索)。它们的共性是往 Agent 里加工具,扩展”能做什么”。
// New constructs and returns the filesystem middleware as a ChatModelAgentMiddleware.
//
// This is the recommended constructor for new code. It returns a ChatModelAgentMiddleware which provides:
// - Better context propagation through WrapInvokableToolCall and WrapStreamableToolCall methods
// - BeforeAgent hook for modifying agent instruction and tools at runtime
// - More flexible extension points compared to the struct-based AgentMiddleware
//
// The middleware provides filesystem tools (ls, read_file, write_file, edit_file, glob, grep)
// and optionally an execute tool if the Backend implements ShellBackend or StreamingShellBackend.
//
// Example usage:
//
// middleware, err := filesystem.New(ctx, &filesystem.Config{
// Backend: myBackend,
// })
// agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
// // ...
// Handlers: []adk.ChatModelAgentMiddleware{middleware},
// })
func New(ctx context.Context, config *MiddlewareConfig) (adk.ChatModelAgentMiddleware, error) {
return NewTyped[*schema.Message](ctx, config)
}
  • 管上下文(改写状态):summarization(超长历史摘要压缩)、reduction(两阶段截断/清理工具输出)、agentsmd(临时注入 Agents.md)、patchtoolcalls(修补悬空的工具调用)。它们的共性是在 Before/AfterModelRewriteState改消息,管理”喂给模型什么”。

这个二分不是我们硬套的,而是钩子设计逼出来的结果:BeforeAgent 改工具集 → 天然承载第一类;BeforeModelRewriteState 改消息 → 天然承载第二类。能力从工具进,上下文从状态改——记住这条,你一眼就能判断一个新需求该做成哪类中间件、实现哪个钩子。

本章小结

  • ADK 有两套中间件:老 AgentMiddleware(struct,封闭)与新 TypedChatModelAgentMiddleware[M](interface,开放)。新代码用接口版,结构体版仅为兼容。
  • 升级的本质是”填空 → 开放实现”;接口版钩子返回 context,让上下文能沿整条调用链传播。
  • 九个钩子按”层级 × 时机”分:Agent / 模型(持久化)/ 工具 / 模型调用;Before/After 是钩子,Wrap* 是洋葱包裹。
  • 执行序有权威时序图:老 Middlewares 包在新 Handlers 外,先注册=最外层,框架内部件(重试/切换/事件)也在链上。
  • 内置件分两类:给能力(注入工具)与管上下文(改写状态),对应两组钩子。

理解了单个 Agent 的扩展模型,下一章我们上升到多智能体的组合范式:supervisor、plan-execute、DeepAgent 各代表一种把 Agent 拼起来的哲学。

源码

正在读取完整文件…