Reduction
Reduction 中间件
adk/middlewares/reduction
💡 本中间件在 v0.8.0 版本引入。
概述
reduction 中间件用来控制工具结果占用的 token 数量,提供两种策略:
- 截断 (Truncation):工具返回时立即截断过长的输出,将完整内容保存到 Backend
- 清理 (Clear):总 token 超过阈值时,把旧的工具结果存到文件系统
架构
Tool 调用返回结果
│
▼
┌─────────────────────────────────────────────────────────────┐
│ WrapInvokableToolCall / WrapStreamableToolCall │
│ │
│ Truncation 策略(可跳过) │
│ 结果长度 > MaxLengthForTrunc? │
│ 是 → 截断内容,完整内容存到 Backend │
│ 否 → 原样返回 │
└─────────────────────────────────────────────────────────────┘
│
▼
结果加入 Messages
│
▼
┌─────────────────────────────────────────────────────────────┐
│ BeforeModelRewriteState │
│ │
│ Clear 策略(可跳过) │
│ 总 token > MaxTokensForClear? │
│ 是 → 把旧的工具结果存到 Backend,替换成文件路径 │
│ 否 → 不处理 │
└─────────────────────────────────────────────────────────────┘
│
▼
调用 Model
配置
Config 主配置
type Config struct {
// Backend 存储后端,用于保存截断/清理的内容
// 当 SkipTruncation 为 false 时必填
Backend Backend
// SkipTruncation 跳过截断阶段
SkipTruncation bool
// SkipClear 跳过清理阶段
SkipClear bool
// ReadFileToolName 读取文件的工具名
// 内容卸载到文件后,agent 需要使用此工具读取
// 默认 "read_file"
ReadFileToolName string
// RootDir 保存内容的根目录
// 默认 "/tmp"
// 截断内容保存到 {RootDir}/trunc/{tool_call_id}
// 清理内容保存到 {RootDir}/clear/{tool_call_id}
RootDir string
// MaxLengthForTrunc 触发截断的最大长度
// 默认 50000
MaxLengthForTrunc int
// TokenCounter token 计数器
// 用于判断是否需要触发清理
// 默认使用 字符数/4 估算
TokenCounter func(ctx context.Context, msg []adk.Message, tools []*schema.ToolInfo) (int64, error)
// MaxTokensForClear 触发清理的 token 阈值
// 默认 30000
MaxTokensForClear int64
// ClearRetentionSuffixLimit 保留最近多少轮对话不清理
// 默认 1
ClearRetentionSuffixLimit int
// ClearPostProcess 清理完成后的回调
// 可用于保存或通知当前状态
ClearPostProcess func(ctx context.Context, state *adk.ChatModelAgentState) context.Context
// ToolConfig 针对特定工具的配置
// 优先级高于全局配置
ToolConfig map[string]*ToolReductionConfig
}
ToolReductionConfig 工具级配置
type ToolReductionConfig struct {
// Backend 此工具使用的存储后端
Backend Backend
// SkipTruncation 跳过此工具的截断
SkipTruncation bool
// TruncHandler 自定义截断处理器
// 不设置时使用默认处理器
TruncHandler func(ctx context.Context, detail *ToolDetail) (*TruncResult, error)
// SkipClear 跳过此工具的清理
SkipClear bool
// ClearHandler 自定义清理处理器
// 不设置时使用默认处理器
ClearHandler func(ctx context.Context, detail *ToolDetail) (*ClearResult, error)
}
ToolDetail 工具详情
type ToolDetail struct {
// ToolContext 工具元信息(工具名、调用 ID)
ToolContext *adk.ToolContext
// ToolArgument 输入参数
ToolArgument *schema.ToolArgument
// ToolResult 输出结果
ToolResult *schema.ToolResult
}
TruncResult 截断结果
type TruncResult struct {
// NeedTrunc 是否需要截断
NeedTrunc bool
// ToolResult 截断后的工具结果
// NeedTrunc 为 true 时必填
ToolResult *schema.ToolResult
// NeedOffload 是否需要卸载到存储
NeedOffload bool
// OffloadFilePath 卸载文件路径
// NeedOffload 为 true 时必填
OffloadFilePath string
// OffloadContent 卸载内容
// NeedOffload 为 true 时必填
OffloadContent string
}
ClearResult 清理结果
type ClearResult struct {
// NeedClear 是否需要清理
NeedClear bool
// ToolArgument 清理后的工具参数
// NeedClear 为 true 时必填
ToolArgument *schema.ToolArgument
// ToolResult 清理后的工具结果
// NeedClear 为 true 时必填
ToolResult *schema.ToolResult
// NeedOffload 是否需要卸载到存储
NeedOffload bool
// OffloadFilePath 卸载文件路径
// NeedOffload 为 true 时必填
OffloadFilePath string
// OffloadContent 卸载内容
// NeedOffload 为 true 时必填
OffloadContent string
}
创建中间件
基本用法
import (
"context"
"github.com/cloudwego/eino/adk/middlewares/reduction"
)
// 使用默认配置
middleware, err := reduction.New(ctx, &reduction.Config{
Backend: myBackend, // 必填:存储后端
})
// 与 ChatModelAgent 一起使用
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Model: yourChatModel,
Middlewares: []adk.ChatModelAgentMiddleware{middleware},
})
自定义配置
config := &reduction.Config{
Backend: myBackend,
RootDir: "/data/agent",
MaxLengthForTrunc: 30000,
MaxTokensForClear: 100000,
ClearRetentionSuffixLimit: 2,
TokenCounter: myTokenCounter,
ClearPostProcess: func(ctx context.Context, state *adk.ChatModelAgentState) context.Context {
log.Printf("Clear completed, messages: %d", len(state.Messages))
return ctx
},
ToolConfig: map[string]*reduction.ToolReductionConfig{
"grep": {
Backend: grepBackend,
SkipTruncation: false,
},
"read_file": {
Backend: readFileBackend,
SkipClear: true, // 读文件工具不需要清理
},
},
}
middleware, err := reduction.New(ctx, config)
仅使用截断策略
middleware, err := reduction.New(ctx, &reduction.Config{
Backend: myBackend,
SkipClear: true, // 跳过清理阶段
})
仅使用清理策略
middleware, err := reduction.New(ctx, &reduction.Config{
Backend: myBackend,
SkipTruncation: true, // 跳过截断阶段
})
工作原理
Truncation(截断)
在 WrapInvokableToolCall / WrapStreamableToolCall 中处理:
- 工具返回结果
- 调用 TruncHandler 判断是否需要截断
- 如需截断,将完整内容存到 Backend
- 返回截断后的内容,包含提示文字告知 agent 完整内容的位置
Clear(清理)
在 BeforeModelRewriteState 中处理:
- 用 TokenCounter 计算总 token
- 超过 MaxTokensForClear 才处理
- 从旧消息开始遍历,跳过已处理的和最近 ClearRetentionSuffixLimit 轮
- 对范围内的每个工具调用,调用 ClearHandler
- 需要清理的,写入 Backend,把消息里的结果替换成文件路径
- 调用 ClearPostProcess 回调
多语言支持
截断和清理的提示文字支持中英文,通过 adk.SetLanguage() 切换:
adk.SetLanguage(adk.LanguageChinese) // 中文
adk.SetLanguage(adk.LanguageEnglish) // 英文(默认)
注意事项
- 当
SkipTruncation为 false 时,Backend必须设置 - 默认 TokenCounter 用
字符数 / 4估算,对于中文不精准,建议使用github.com/tiktoken-go/tokenizer替换 - 已处理过的消息会打标记,不会重复处理
ToolConfig中的配置优先级高于全局配置