Reduction

Reduction 中间件

adk/middlewares/reduction

💡 本中间件在 v0.8.0 版本引入。

概述

reduction 中间件用来控制工具结果占用的 token 数量,提供两种策略:

  1. 截断 (Truncation):工具返回时立即截断过长的输出,将完整内容保存到 Backend
  2. 清理 (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 中处理:

  1. 工具返回结果
  2. 调用 TruncHandler 判断是否需要截断
  3. 如需截断,将完整内容存到 Backend
  4. 返回截断后的内容,包含提示文字告知 agent 完整内容的位置

Clear(清理)

BeforeModelRewriteState 中处理:

  1. 用 TokenCounter 计算总 token
  2. 超过 MaxTokensForClear 才处理
  3. 从旧消息开始遍历,跳过已处理的和最近 ClearRetentionSuffixLimit 轮
  4. 对范围内的每个工具调用,调用 ClearHandler
  5. 需要清理的,写入 Backend,把消息里的结果替换成文件路径
  6. 调用 ClearPostProcess 回调

多语言支持

截断和清理的提示文字支持中英文,通过 adk.SetLanguage() 切换:

adk.SetLanguage(adk.LanguageChinese)  // 中文
adk.SetLanguage(adk.LanguageEnglish)  // 英文(默认)

注意事项

  • SkipTruncation 为 false 时,Backend 必须设置
  • 默认 TokenCounter 用 字符数 / 4 估算,对于中文不精准,建议使用 github.com/tiktoken-go/tokenizer 替换
  • 已处理过的消息会打标记,不会重复处理
  • ToolConfig 中的配置优先级高于全局配置