vault backup: 2026-04-29 18:54:02
This commit is contained in:
@@ -1,65 +1,332 @@
|
||||
---
|
||||
Description: ""
|
||||
date: "2026-03-16"
|
||||
lastmod: ""
|
||||
tags: []
|
||||
title: 第五章:Middleware(中间件模式)
|
||||
weight: 5
|
||||
tags: ["Eino", "Agent", "Middleware", "DeepAgent", "错误处理", "重试"]
|
||||
create time: "2026-04-29 16:00"
|
||||
---
|
||||
|
||||
本章目标:理解 Middleware 模式,实现 Tool 错误处理和 ChatModel 重试机制。
|
||||
# 第五章:Middleware(中间件模式)
|
||||
|
||||
## 概述
|
||||
|
||||
第四章为 Agent 加入了 Tool 能力后,Agent 已经可以「看见」和「触碰」真实世界了。但现实中的 API 会限流、文件会不存在、网络会超时——**直接暴露的错误会让 Agent 流程中断**。本章通过 Middleware 模式引入拦截器机制,让 Agent 具备错误自愈和自动重试的能力。
|
||||
|
||||
## 为什么需要 Middleware
|
||||
|
||||
第四章我们为 Agent 添加了 Tool 能力,让 Agent 能够访问文件系统。但在实际应用场景中,**Tool 报错或 ChatModel 报错是常见的现象**,例如:
|
||||
|
||||
- **Tool 报错**:文件不存在、参数错误、权限不足等
|
||||
- **ChatModel 报错**:API 限流(429)、网络超时、服务不可用等
|
||||
|
||||
### 问题一:Tool 错误会中断整个流程
|
||||
|
||||
当 Tool 执行失败时,错误会直接传播到 Agent,导致整个对话中断:
|
||||
第四章结束时,Tool 报错或 ChatModel 报错会直接中断整个对话流程:
|
||||
|
||||
```
|
||||
[tool call] read_file(file_path: "nonexistent.txt")
|
||||
Error: open nonexistent.txt: no such file or directory
|
||||
// 对话中断,用户需要重新开始
|
||||
// 💥 对话中断,用户需要重新开始
|
||||
```
|
||||
|
||||
### 问题二:模型调用可能因限流失败
|
||||
这类错误很常见:
|
||||
|
||||
当模型 API 返回 429(Too Many Requests)错误时,整个对话也会中断:
|
||||
| 场景 | 错误类型 | 常见原因 |
|
||||
|------|---------|---------|
|
||||
| **Tool 报错** | 业务错误 | 文件不存在、参数错误、权限不足 |
|
||||
| **ChatModel 报错** | 临时错误 | API 限流(429)、网络超时、服务不可用 |
|
||||
|
||||
```
|
||||
Error: rate limit exceeded (429)
|
||||
// 对话中断
|
||||
```
|
||||
> [!tip] 关键洞察
|
||||
>
|
||||
> 这些错误**不应该终止 Agent 流程**。更好的做法是把错误信息交给模型,让它自动调整策略继续执行:
|
||||
>
|
||||
> ```
|
||||
> [tool call] read_file(file_path: "nonexistent.txt")
|
||||
> [tool result] [tool error] open nonexistent.txt: no such file or directory
|
||||
> [assistant] 抱歉,文件不存在。让我先列出当前目录的文件...
|
||||
> [tool call] glob(pattern: "*")
|
||||
> // ✅ 对话继续,模型自行纠错
|
||||
> ```
|
||||
|
||||
### 期望的行为
|
||||
> [!question] 深入思考
|
||||
>
|
||||
> 既然可以直接把错误返回给模型,为什么不直接在每个 Tool 内部写 `if err != nil` 判断?
|
||||
> 提示:考虑开闭原则(OCP)——如果明天要加 10 个新 Tool,是不是每个都要改一遍?**Middleware 的本质是将横切关注点从业务代码中剥离**,这也是 AOP(面向切面编程)的核心思想。
|
||||
|
||||
这些报错信息往往**不希望直接终止 Agent 流程**,而是希望把报错信息给到模型,由模型自动纠错进行下一轮。例如:
|
||||
## 什么是 Middleware
|
||||
|
||||
```
|
||||
[tool call] read_file(file_path: "nonexistent.txt")
|
||||
[tool result] [tool error] open nonexistent.txt: no such file or directory
|
||||
[assistant] 抱歉,文件不存在。让我先列出当前目录的文件...
|
||||
[tool call] glob(pattern: "*")
|
||||
```
|
||||
**Middleware 是 Agent 的拦截器**,可以在调用前后插入自定义逻辑:
|
||||
|
||||
### Middleware 的定位
|
||||
|
||||
**Middleware 模式**可以扩展 Tool 和 ChatModel 的行为,非常适合解决这个问题:
|
||||
|
||||
- **Middleware 是 Agent 的拦截器**:在调用前后插入自定义逻辑
|
||||
- **Middleware 可处理错误**:将错误转换为模型可理解的格式
|
||||
- **Middleware 可实现重试**:自动重试失败的操作
|
||||
- **Middleware 可组合**:多个 Middleware 可以串联使用
|
||||
- **拦截调用**:在 Tool 或 ChatModel 执行前/后包装自定义行为
|
||||
- **错误转换**:将错误转为模型可理解的字符串,而非中断流程
|
||||
- **自动重试**:对临时错误(如限流)实现指数退避重试
|
||||
- **可组合**:多个 Middleware 串联形成责任链
|
||||
|
||||
**简单类比:**
|
||||
|
||||
- **Agent** = "业务逻辑"
|
||||
- **Middleware** = "AOP 切面"(日志、重试、错误处理等横切关注点)
|
||||
|
||||
> [!note] 装饰器模式
|
||||
>
|
||||
> Middleware 的本质是**装饰器模式**(Decorator Pattern)——每个 Middleware 包装原始调用,可以修改输入、输出或错误,而不改变被包装对象的接口。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### Middleware 接口
|
||||
|
||||
`ChatModelAgentMiddleware` 是 Agent 中间件的统一接口:
|
||||
|
||||
```go
|
||||
type ChatModelAgentMiddleware interface {
|
||||
BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)
|
||||
BeforeModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)
|
||||
AfterModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)
|
||||
WrapInvokableToolCall(ctx context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error)
|
||||
WrapStreamableToolCall(ctx context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error)
|
||||
WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error)
|
||||
WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error)
|
||||
WrapModel(ctx context.Context, m model.BaseChatModel, mc *ModelContext) (model.BaseChatModel, error)
|
||||
}
|
||||
```
|
||||
|
||||
**方法分组:**
|
||||
|
||||
| 分组 | 方法 | 作用时机 |
|
||||
|------|------|---------|
|
||||
| **Agent 生命周期** | `BeforeAgent` | 每次 Agent 运行前,可修改指令和工具配置 |
|
||||
| **状态处理** | `BeforeModelRewriteState` / `AfterModelRewriteState` | 每次模型调用前后的状态变换 |
|
||||
| **Tool 调用** | `WrapInvokableToolCall` / `WrapStreamableToolCall` | 包装同步/流式 Tool 的执行 |
|
||||
| **模型调用** | `WrapModel` | 包装底层 ChatModel 的调用 |
|
||||
|
||||
### 洋葱模型:Middleware 执行顺序
|
||||
|
||||
Handlers 按**数组正序**包装,形成洋葱模型:
|
||||
|
||||
```go
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
&middlewareA{}, // 最外层:最先 Wrap,最后生效
|
||||
&middlewareB{}, // 中间层
|
||||
&middlewareC{}, // 最内层:最后 Wrap,最先生效
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Request ["📥 请求方向 →"]
|
||||
A["Middleware A\n(最外层)"] --> B["Middleware B\n(中间层)"]
|
||||
B --> C["Middleware C\n(最内层)"]
|
||||
C --> T["实际 Tool/Model\n执行"]
|
||||
end
|
||||
|
||||
subgraph Response ["📤 响应方向 ←"]
|
||||
T --> CR["Middleware C\n返回"]
|
||||
CR --> CB["Middleware B\n返回"]
|
||||
CB --> CA["Middleware A\n返回"]
|
||||
end
|
||||
|
||||
style A fill:#fce4ec
|
||||
style B fill:#e8f5e9
|
||||
style C fill:#e3f2fd
|
||||
style T fill:#fff3e0
|
||||
```
|
||||
|
||||
> [!warning] 实用建议
|
||||
>
|
||||
> 将 `safeToolMiddleware`(错误捕获)放在最内层(数组末尾),确保其他 Middleware 抛出的中断错误能正确向外传播,不被吞掉。
|
||||
|
||||
### ModelRetryConfig:内置重试配置
|
||||
|
||||
`ModelRetryConfig` 提供了 ChatModel 级别的自动重试能力:
|
||||
|
||||
```go
|
||||
type ModelRetryConfig struct {
|
||||
MaxRetries int // 最大重试次数
|
||||
IsRetryAble func(ctx context.Context, err error) bool // 哪些错误可重试
|
||||
}
|
||||
```
|
||||
|
||||
**重试策略:**
|
||||
|
||||
| 策略 | 说明 |
|
||||
|------|------|
|
||||
| **指数退避** | 每次重试间隔递增,避免频繁请求加剧限流 |
|
||||
| **条件过滤** | 通过 `IsRetryAble` 精确控制哪些错误值得重试 |
|
||||
| **自动恢复** | 无需用户干预,模型调用失败后自动重试 |
|
||||
|
||||
## 实现细节
|
||||
|
||||
### SafeToolMiddleware:错误转换
|
||||
|
||||
`SafeToolMiddleware` 捕获 Tool 执行时的错误,将其转换为字符串返回给模型而非中断流程:
|
||||
|
||||
```go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
**设计要点:**
|
||||
|
||||
- **区分错误类型**:中断错误(如主动要求停止)必须传播,业务错误(如文件不存在)可以转换
|
||||
- **不吞错**:只转换预期的业务错误,真正的系统异常仍向上抛出
|
||||
- **格式化**:使用 `[tool error]` 前缀方便模型识别并回复时引用
|
||||
|
||||
流式 Tool 的错误处理同理,需将错误封装为单帧流:
|
||||
|
||||
```go
|
||||
func (m *safeToolMiddleware) WrapStreamableToolCall(
|
||||
_ context.Context,
|
||||
endpoint adk.StreamableToolCallEndpoint,
|
||||
_ *adk.ToolContext,
|
||||
) (adk.StreamableToolCallEndpoint, error) {
|
||||
return func(ctx context.Context, args string, opts ...tool.Option) (*schema.StreamReader[string], error) {
|
||||
sr, err := endpoint(ctx, args, opts...)
|
||||
if err != nil {
|
||||
if _, ok := compose.IsInterruptRerunError(err); ok {
|
||||
return nil, err
|
||||
}
|
||||
// 返回包含错误信息的单帧流
|
||||
return singleChunkReader(fmt.Sprintf("[tool error] %v", err)), nil
|
||||
}
|
||||
return safeWrapReader(sr), nil
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 注册 Middleware 与重试配置
|
||||
|
||||
将 Middleware 注入 DeepAgent 的配置中:
|
||||
|
||||
```go
|
||||
agent, err := deep.New(ctx, &deep.Config{
|
||||
Name: "Ch05MiddlewareAgent",
|
||||
Description: "ChatWithDoc agent with safe tool middleware and retry.",
|
||||
ChatModel: cm,
|
||||
Instruction: agentInstruction,
|
||||
Backend: backend,
|
||||
StreamingShell: backend,
|
||||
MaxIteration: 50,
|
||||
|
||||
// ⭐ 注册 Middleware
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
&safeToolMiddleware{}, // 将 Tool 错误转为字符串
|
||||
},
|
||||
|
||||
// ⭐ 注册模型重试配置
|
||||
ModelRetryConfig: &adk.ModelRetryConfig{
|
||||
MaxRetries: 5,
|
||||
IsRetryAble: func(_ context.Context, err error) bool {
|
||||
return strings.Contains(err.Error(), "429") ||
|
||||
strings.Contains(err.Error(), "Too Many Requests")
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
> [!note] Handlers vs Middlewares
|
||||
>
|
||||
> `Handlers` 字段(在 Config 中)和 "Middleware"(文档讨论的概念)是同一回事——`Handlers` 是配置字段名,而 `ChatModelAgentMiddleware` 是对接口的命名。
|
||||
|
||||
## 执行流程
|
||||
|
||||
结合 Middleware 后,一次 Tool 调用的完整生命周期如下:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U["用户:读取不存在的文件"] --> A{"Agent 分析意图"}
|
||||
A -->|"决定调用 Tool"| M["SafeToolMiddleware\n拦截 Tool 调用"]
|
||||
M --> T["执行 read_file\n返回错误"]
|
||||
T --> E["SafeToolMiddleware\n捕获错误"]
|
||||
E -->|"非中断错误"| S["转换为字符串\ntool error: no such file"]
|
||||
E -->|"中断错误"| EP["向上抛出中断"]
|
||||
S --> R["返回 Tool Result"]
|
||||
R --> AG{"Agent 整合信息"}
|
||||
AG -->|"生成解释性回复"| O["抱歉,文件不存在...\n尝试列出目录"]
|
||||
AG -->|"需要更多信息"| A
|
||||
|
||||
style M fill:#e8f5e9
|
||||
style E fill:#fff3e0
|
||||
style S fill:#e3f2fd
|
||||
style EP fill:#ffebee
|
||||
```
|
||||
|
||||
> [!example] 逐步拆解
|
||||
>
|
||||
> **Step 1 — 用户输入**
|
||||
> 用户请求读取一个不存在的文件。
|
||||
>
|
||||
> **Step 2 — 意图分析**
|
||||
> Agent 判断需要文件系统操作,决定调用 `read_file` Tool。
|
||||
>
|
||||
> **Step 3 — Middleware 拦截**
|
||||
> `SafeToolMiddleware.WrapInvokableToolCall` 在 Tool 执行前被触发,注册了自己的回调逻辑。
|
||||
>
|
||||
> **Step 4 — Tool 执行**
|
||||
> 实际文件读取操作失败,返回 `open nonexistent.txt: no such file` 错误。
|
||||
>
|
||||
> **Step 5 — 错误转换**
|
||||
> Middleware 发现这不是中断错误,将其包装为 `[tool error] open nonexistent.txt: ...` 字符串。
|
||||
>
|
||||
> **Step 6 — Agent 自愈**
|
||||
> Agent 收到带错误的 Tool Result,理解后回复用户并调整策略(如改用 `glob` 列出可用文件)。
|
||||
|
||||
## 扩展:Eino 内置 Middleware
|
||||
|
||||
Eino 生态还提供了以下开箱即用的中间件:
|
||||
|
||||
<table>
|
||||
<tr><th>Middleware</th><th>功能说明</th></tr>
|
||||
<tr><td><strong>reduction</strong></td><td>工具输出缩减——当工具返回过长时自动截断并存入文件系统,防止上下文溢出</td></tr>
|
||||
<tr><td><strong>summarization</strong></td><td>对话历史摘要——Token 超阈值时自动生成摘要压缩历史,节省上下文空间</td></tr>
|
||||
<tr><td><strong>skill</strong></td><td>技能加载——让 Agent 按需动态加载预定义的 SKILL.md 知识包</td></tr>
|
||||
</table>
|
||||
|
||||
### 多 Middleware 组合示例
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/cloudwego/eino/adk/middlewares/reduction"
|
||||
"github.com/cloudwego/eino/adk/middlewares/summarization"
|
||||
)
|
||||
|
||||
// 创建 reduction:管理工具输出长度
|
||||
reductionMW, _ := reduction.New(ctx, &reduction.Config{
|
||||
Backend: filesystemBackend,
|
||||
MaxLengthForTrunc: 50000,
|
||||
MaxTokensForClear: 30000,
|
||||
})
|
||||
|
||||
// 创建 summarization:自动压缩对话历史
|
||||
summarizationMW, _ := summarization.New(ctx, &summarization.Config{
|
||||
Model: chatModel,
|
||||
Trigger: &summarization.TriggerCondition{
|
||||
ContextTokens: 190000,
|
||||
},
|
||||
})
|
||||
|
||||
// 组合使用
|
||||
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
summarizationMW, // 外层:对话历史摘要
|
||||
reductionMW, // 内层:工具输出缩减
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
> [!question] 扩展思考
|
||||
>
|
||||
> 在这个例子中,`summarizationMW` 在外层、`reductionMW` 在内层。如果把顺序反过来,会有什么影响?试着根据洋葱模型的执行顺序推导一下。
|
||||
|
||||
## 代码位置
|
||||
|
||||
- 入口代码:[cmd/ch05/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch05/main.go)
|
||||
@@ -77,13 +344,11 @@ export PROJECT_ROOT=/path/to/eino # Eino 核心库根目录
|
||||
在 `examples/quickstart/chatwitheino` 目录下执行:
|
||||
|
||||
```bash
|
||||
# 设置项目根目录
|
||||
export PROJECT_ROOT=/path/to/your/project
|
||||
|
||||
go run ./cmd/ch05
|
||||
```
|
||||
|
||||
输出示例:
|
||||
**输出示例:**
|
||||
|
||||
```
|
||||
you> 列出当前目录的文件
|
||||
@@ -97,352 +362,19 @@ you> 读取一个不存在的文件
|
||||
[assistant] 抱歉,文件不存在...
|
||||
```
|
||||
|
||||
## 关键概念
|
||||
|
||||
### Middleware 接口
|
||||
|
||||
`ChatModelAgentMiddleware` 是 Agent 的中间件接口:
|
||||
|
||||
```go
|
||||
type ChatModelAgentMiddleware 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)
|
||||
|
||||
// BeforeModelRewriteState is called before each model invocation.
|
||||
// The returned state is persisted to the agent's internal state and passed to the model.
|
||||
BeforeModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)
|
||||
|
||||
// AfterModelRewriteState is called after each model invocation.
|
||||
// The input state includes the model's response as the last message.
|
||||
AfterModelRewriteState(ctx context.Context, state *ChatModelAgentState, mc *ModelContext) (context.Context, *ChatModelAgentState, error)
|
||||
|
||||
// WrapInvokableToolCall wraps a tool's synchronous execution with custom behavior.
|
||||
// This method is only called for tools that implement InvokableTool.
|
||||
WrapInvokableToolCall(ctx context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error)
|
||||
|
||||
// WrapStreamableToolCall wraps a tool's streaming execution with custom behavior.
|
||||
// This method is only called for tools that implement StreamableTool.
|
||||
WrapStreamableToolCall(ctx context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error)
|
||||
|
||||
// WrapEnhancedInvokableToolCall wraps an enhanced tool's synchronous execution.
|
||||
// This method is only called for tools that implement EnhancedInvokableTool.
|
||||
WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error)
|
||||
|
||||
// WrapEnhancedStreamableToolCall wraps an enhanced tool's streaming execution.
|
||||
// This method is only called for tools that implement EnhancedStreamableTool.
|
||||
WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error)
|
||||
|
||||
// WrapModel wraps a chat model with custom behavior.
|
||||
// This method is called at request time when the model is about to be invoked.
|
||||
WrapModel(ctx context.Context, m model.BaseChatModel, mc *ModelContext) (model.BaseChatModel, error)
|
||||
}
|
||||
```
|
||||
|
||||
**设计理念:**
|
||||
|
||||
- **装饰器模式**:每个 Middleware 包装原始调用,可以修改输入、输出或错误
|
||||
- **洋葱模型**:请求从外向内穿过 Middleware,响应从内向外返回
|
||||
- **可组合**:多个 Middleware 按顺序执行
|
||||
|
||||
### Middleware 执行顺序
|
||||
|
||||
`Handlers`(即 Middlewares)按**数组正序**包装,形成洋葱模型:
|
||||
|
||||
```go
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
&middlewareA{}, // 最外层:最先 Wrap,最先拦截请求,但 WrapModel 最后生效
|
||||
&middlewareB{}, // 中间层
|
||||
&middlewareC{}, // 最内层:最后 Wrap
|
||||
}
|
||||
```
|
||||
|
||||
**对于 Tool 调用的执行顺序:**
|
||||
|
||||
```
|
||||
请求 → A.Wrap → B.Wrap → C.Wrap → 实际 Tool 执行 → C返回 → B返回 → A返回 → 响应
|
||||
```
|
||||
|
||||
**实用建议:** 将 `safeToolMiddleware`(错误捕获)放在最内层(数组末尾),确保其他 Middleware 抛出的中断错误能正确向外传播。
|
||||
|
||||
### SafeToolMiddleware
|
||||
|
||||
`SafeToolMiddleware` 将 Tool 错误转换为字符串,让模型能够理解并处理:
|
||||
|
||||
```go
|
||||
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 {
|
||||
// 将错误转换为字符串,而不是返回错误
|
||||
return fmt.Sprintf("[tool error] %v", err), nil
|
||||
}
|
||||
return result, nil
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
**效果:**
|
||||
|
||||
```
|
||||
[tool call] read_file(file_path: "nonexistent.txt")
|
||||
[tool result] [tool error] open nonexistent.txt: no such file or directory
|
||||
[assistant] 抱歉,文件不存在,请检查文件路径...
|
||||
// 对话继续,模型可以根据错误信息调整策略
|
||||
```
|
||||
|
||||
### ModelRetryConfig
|
||||
|
||||
`ModelRetryConfig` 配置 ChatModel 的自动重试:
|
||||
|
||||
```go
|
||||
type ModelRetryConfig struct {
|
||||
MaxRetries int // 最大重试次数
|
||||
IsRetryAble func(ctx context.Context, err error) bool // 判断是否可重试
|
||||
}
|
||||
```
|
||||
|
||||
**使用方式(以 DeepAgent 为例):**
|
||||
|
||||
```go
|
||||
agent, err := deep.New(ctx, &deep.Config{
|
||||
// ...
|
||||
ModelRetryConfig: &adk.ModelRetryConfig{
|
||||
MaxRetries: 5,
|
||||
IsRetryAble: func(_ context.Context, err error) bool {
|
||||
// 429 限流错误可重试
|
||||
return strings.Contains(err.Error(), "429") ||
|
||||
strings.Contains(err.Error(), "Too Many Requests") ||
|
||||
strings.Contains(err.Error(), "qpm limit")
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**重试策略:**
|
||||
|
||||
- 指数退避:每次重试间隔递增
|
||||
- 可配置条件:通过 `IsRetryAble` 判断哪些错误可重试
|
||||
- 自动恢复:无需用户干预
|
||||
|
||||
## Middleware 的实现
|
||||
|
||||
### 1. 实现 SafeToolMiddleware
|
||||
|
||||
```go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实现流式 Tool 错误处理
|
||||
|
||||
```go
|
||||
func (m *safeToolMiddleware) WrapStreamableToolCall(
|
||||
_ context.Context,
|
||||
endpoint adk.StreamableToolCallEndpoint,
|
||||
_ *adk.ToolContext,
|
||||
) (adk.StreamableToolCallEndpoint, error) {
|
||||
return func(ctx context.Context, args string, opts ...tool.Option) (*schema.StreamReader[string], error) {
|
||||
sr, err := endpoint(ctx, args, opts...)
|
||||
if err != nil {
|
||||
if _, ok := compose.IsInterruptRerunError(err); ok {
|
||||
return nil, err
|
||||
}
|
||||
// 返回包含错误信息的单帧流
|
||||
return singleChunkReader(fmt.Sprintf("[tool error] %v", err)), nil
|
||||
}
|
||||
// 包装流,捕获流中的错误
|
||||
return safeWrapReader(sr), nil
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 配置 Agent 使用 Middleware
|
||||
|
||||
本章继续使用第四章引入的 `DeepAgent`,在其 `Handlers` 字段中注册 Middleware:
|
||||
|
||||
```go
|
||||
agent, err := deep.New(ctx, &deep.Config{
|
||||
Name: "Ch05MiddlewareAgent",
|
||||
Description: "ChatWithDoc agent with safe tool middleware and retry.",
|
||||
ChatModel: cm,
|
||||
Instruction: agentInstruction,
|
||||
Backend: backend,
|
||||
StreamingShell: backend,
|
||||
MaxIteration: 50,
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
&safeToolMiddleware{}, // 将 Tool 错误转换为字符串
|
||||
},
|
||||
ModelRetryConfig: &adk.ModelRetryConfig{
|
||||
MaxRetries: 5,
|
||||
IsRetryAble: func(_ context.Context, err error) bool {
|
||||
return strings.Contains(err.Error(), "429") ||
|
||||
strings.Contains(err.Error(), "Too Many Requests")
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**注意**:`Handlers` 字段(在配置中)和 "Middleware"(在文档中讨论的概念)是同一回事——`Handlers` 是配置字段名,而 `ChatModelAgentMiddleware` 是接口名。
|
||||
|
||||
```
|
||||
**关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [cmd/ch05/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch05/main.go)):
|
||||
|
||||
```go
|
||||
// SafeToolMiddleware 捕获 Tool 错误并转换为字符串
|
||||
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
|
||||
}
|
||||
|
||||
// 配置 DeepAgent(与第四章一样,新增 Handlers 和 ModelRetryConfig)
|
||||
agent, _ := deep.New(ctx, &deep.Config{
|
||||
ChatModel: cm,
|
||||
Backend: backend,
|
||||
StreamingShell: backend,
|
||||
MaxIteration: 50,
|
||||
Handlers: []adk.ChatModelAgentMiddleware{
|
||||
&safeToolMiddleware{},
|
||||
},
|
||||
ModelRetryConfig: &adk.ModelRetryConfig{
|
||||
MaxRetries: 5,
|
||||
IsRetryAble: func(_ context.Context, err error) bool {
|
||||
return strings.Contains(err.Error(), "429")
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Middleware 执行流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 用户:读取不存在的文件 │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Agent 分析意图 │
|
||||
│ 决定调用 read_file │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ SafeToolMiddleware │
|
||||
│ 拦截 Tool 调用 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 执行 read_file │
|
||||
│ 返回错误 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ SafeToolMiddleware │
|
||||
│ 将错误转换为字符串 │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ 返回 Tool Result │
|
||||
│ "[tool error] ..." │
|
||||
└──────────────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Agent 生成回复 │
|
||||
│ "抱歉,文件不存在..." │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 本章小结
|
||||
|
||||
- **Middleware**:Agent 的拦截器,可以在调用前后插入自定义逻辑
|
||||
- **SafeToolMiddleware**:将 Tool 错误转换为字符串,让模型能够理解并处理
|
||||
- **ModelRetryConfig**:配置 ChatModel 的自动重试,处理限流等临时错误
|
||||
- **装饰器模式**:Middleware 包装原始调用,可以修改输入、输出或错误
|
||||
- **洋葱模型**:请求从外向内穿过 Middleware,响应从内向外返回
|
||||
| 概念 | 一句话理解 |
|
||||
|------|-----------|
|
||||
| **Middleware** | Agent 的拦截器,在调用前后插入自定义逻辑 |
|
||||
| **SafeToolMiddleware** | 将 Tool 错误转为字符串交给模型,而非中断流程 |
|
||||
| **ModelRetryConfig** | 配置 ChatModel 的自动重试,处理限流等临时错误 |
|
||||
| **洋葱模型** | 请求从外向内穿过 Middleware,响应从内向外返回 |
|
||||
| **装饰器模式** | 每个 Middleware 包装原始调用,可修改输入、输出或错误 |
|
||||
| **中断错误不转换** | 只有业务错误才转字符串,中断错误继续传播 |
|
||||
|
||||
## 扩展思考
|
||||
## 关联笔记
|
||||
|
||||
**Eino 内置 Middleware:**
|
||||
|
||||
<table>
|
||||
<tr><td>Middleware</td><td>功能说明</td></tr>
|
||||
<tr><td><strong>reduction</strong></td><td>工具输出缩减,当工具返回内容过长时自动截断并卸载到文件系统,防止上下文溢出</td></tr>
|
||||
<tr><td><strong>summarization</strong></td><td>对话历史自动摘要,当 token 数量超过阈值时自动生成摘要压缩历史</td></tr>
|
||||
<tr><td><strong>skill</strong></td><td>技能加载中间件,让 Agent 能够动态加载和执行预定义的技能</td></tr>
|
||||
</table>
|
||||
|
||||
**Middleware 链示例:**
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/cloudwego/eino/adk/middlewares/reduction"
|
||||
"github.com/cloudwego/eino/adk/middlewares/summarization"
|
||||
"github.com/cloudwego/eino/adk/middlewares/skill"
|
||||
)
|
||||
|
||||
// 创建 reduction middleware:管理工具输出长度
|
||||
reductionMW, _ := reduction.New(ctx, &reduction.Config{
|
||||
Backend: filesystemBackend, // 存储后端
|
||||
MaxLengthForTrunc: 50000, // 单次工具输出最大长度
|
||||
MaxTokensForClear: 30000, // 触发清理的 token 阈值
|
||||
})
|
||||
|
||||
// 创建 summarization middleware:自动压缩对话历史
|
||||
summarizationMW, _ := summarization.New(ctx, &summarization.Config{
|
||||
Model: chatModel, // 用于生成摘要的模型
|
||||
Trigger: &summarization.TriggerCondition{
|
||||
ContextTokens: 190000, // 触发摘要的 token 阈值
|
||||
},
|
||||
})
|
||||
|
||||
// 组合多个 middleware(概念示例,使用 DeepAgent 时将 adk.NewChatModelAgent 替换为 deep.New)
|
||||
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
||||
Handlers: []adk.ChatModelAgentMiddleware{ // 注意:配置字段名为 Handlers,概念上与 Middlewares 等价
|
||||
summarizationMW, // 最外层:对话历史摘要
|
||||
reductionMW, // 中间层:工具输出缩减
|
||||
},
|
||||
})
|
||||
```
|
||||
- [[Eino/quick_start/_index]]
|
||||
- [[Eino/quick_start/chapter_04_tool_and_filesystem]] — Tool 与文件系统访问(上一章)
|
||||
- [[Eino/quick_start/chapter_06_callback_and_trace]] — Callback 与 Trace 可观测性(下一章)
|
||||
|
||||
Reference in New Issue
Block a user