Eino-Middleware 接口

ChatModelAgentMiddleware 是 Agent 的中间件接口:

type ChatModelAgentMiddleware interface {
    // BeforeAgent:在每次Agent任务开始运行前触发,
    // 可修改Agent的系统指令(instruction)和工具配置。
    BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)

    // BeforeModelRewriteState:**每一轮调用大模型之前**触发。
    // 返回的状态会保存到Agent内部状态,并传给大模型。
    BeforeModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)

    // AfterModelRewriteState:**每一轮大模型调用完成之后**触发。
    // 入参state里面已经包含模型本次返回的消息作为最后一条message。
    AfterModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)

    // WrapInvokableToolCall:包装【普通同步工具】的执行逻辑,用于自定义行为。
    // 仅对实现了 InvokableTool 接口的工具生效。
    WrapInvokableToolCall(ctx context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error)

    // WrapStreamableToolCall:包装【流式工具】的执行逻辑,用于自定义行为。
    // 仅对实现了 StreamableTool 接口的工具生效。
    WrapStreamableToolCall(ctx context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error)

    // WrapEnhancedInvokableToolCall:包装【增强型同步工具】的执行逻辑。
    // 仅对实现了 EnhancedInvokableTool 接口的工具生效(支持Interrupt中断/人机审批)。
    WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error)

    // WrapEnhancedStreamableToolCall:包装【增强型流式工具】的执行逻辑。
    // 仅对实现了 EnhancedStreamableTool 接口的工具生效。
    WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error)

    // WrapModel:包装大模型实例,自定义模型调用行为。
    // 在准备发起模型请求时触发。
    WrapModel(ctx context.Context, m model.BaseChatModel, mc *ModelContext) (model.BaseChatModel, error)
}

按照 Go 原生语法的要求,如果一个接口包含多个方法,那么结构体必须完整实现接口的全部方法,才能够赋值给该接口类型的变量ChatModelAgentMiddleware 。而在 Eino 框架中提供了便捷方案:在自定义中间件结构体里嵌入*adk.BaseChatModelAgentMiddleware,该基类已经预先完成了接口全部方法的默认空实现。开发时只需覆写业务需要的少量方法,其余方法直接复用基类自带的默认逻辑,无需手动编写全部接口方法,减少样板代码。

type safeToolMiddleware struct {
    *adk.BaseChatModelAgentMiddleware
}

func (m *safeToolMiddleware) WrapInvokableToolCall(
    _ context.Context,
    endpoint adk.InvokableToolCallEndpoint,
    _ *adk.ToolContext,
) (adk.InvokableToolCallEndpoint, error) {
    return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
        result, err := endpoint(ctx, args, opts...)
        if err != nil {
            if _, ok := compose.IsInterruptRerunError(err); ok {
                return "", err
            }
            // 将错误转换为字符串,而不是返回错误
            return fmt.Sprintf("[tool error] %v", err), nil
        }
        return result, nil
    }, nil
}

如上代码,只重写你想要自定义的这一个方法,其余7个里剩下的复用基类。

在 Agent 初始化阶段,框架只会调用一次WrapInvokableToolCall,入参endpoint代表原始的工具执行函数。我们在这个方法内部定义并返回一个新的闭包函数,随后WrapInvokableToolCall执行结束,不会再次运行;框架会保存返回的闭包,替换掉原始的endpoint。在后续对话流程里,当大模型选择调用该工具时,框架执行的是包装后的闭包,不再直接调用原始endpoint。闭包内部主动调用原始endpoint来执行业务逻辑,同时可以在闭包中实现错误捕获、返回值包装、调用日志记录等通用横切能力。

其中:

// 将错误转换为字符串,而不是返回错误
return fmt.Sprintf("[tool error] %v", err), nil

相对于普通业务错误相比,这里采用了特殊处理逻辑,不再向外返回 Go 的 error,而是将错误信息包装为普通字符串返回给 LLM。Go 常规写法是出错时直接返回result, err,但此处工具执行遇到业务报错(例如用户不存在、参数非法)时,不会把 error 抛给 Agent 框架,而是返回形如[tool error] xxx的文本,同时将 err 置为 nil。这样大模型可以读取到这段错误文本,识别出工具调用失败,并自主组织自然语言回复用户。倘若直接向框架返回 error,TurnLoop 会判定为系统异常,直接终止整个 Agent 任务。

二、配置 Agent 使用 Middleware

	cfg := &deep.TypedConfig[M]{
		Name:           "Ch05MiddlewareAgent",
		Description:    "ChatWithDoc agent with safe tool middleware and retry.",
		ChatModel:      cm,
		Instruction:    agentInstruction,
		Backend:        backend,
		StreamingShell: backend,
		MaxIteration:   50,
		Handlers: []adk.TypedChatModelAgentMiddleware[M]{
			helpers.NewSafeToolMiddleware[M](),
		},
	}
	helpers.ApplyMessageModelRetry(cfg)
	agent, err := deep.NewTyped[M](ctx, cfg)
	if err != nil {
		_, _ = fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

上面代码:构造deep.TypedConfig[M]配置结构体,依次设置 Agent 名称、描述、大模型、系统提示词、Backend、最大迭代次数以及中间件列表,在 Handlers 中注册 SafeToolMiddleware,也就是前面介绍的工具安全错误处理中间件;接着调用helpers.ApplyMessageModelRetry(cfg),为该 Agent 配置追加模型调用重试能力;最后通过deep.NewTyped[M]依据配置创建 Agent 实例,创建失败时打印错误信息并退出程序。

其中,Handlers 是中间件切片(slice),可以放多个中间件实例,多个 helpers.NewXXXMiddleware() 放进去。

Handlers: []adk.TypedChatModelAgentMiddleware[M]{
    helpers.NewSafeToolMiddleware[M](),
    middleware.NewAuthMiddleware[M](),   // 你自己写的鉴权中间件
    middleware.NewTraceMiddleware[M](),  // 链路追踪中间件
},

执行链路: M1 → M2 → M3 → 原始Agent逻辑 → M3返回 → M2返回 → M1返回