Function Tool 性能优化:超时、并发与资源管理

一个 Tool 响应慢,整个 Agent 就会变慢。这篇讲三个优化方向:超时控制、并发调用、连接池管理,让 Agent 在生产环境里跑得稳健。

问题:慢 Tool 会拖累整个 Agent

默认情况下,Tool 的 Call() 是同步执行的。如果一个 Tool 需要 10 秒返回,整个 Agent 就要等 10 秒。

用户请求 → Agent → Tool A(3秒)→ Tool B(10秒)→ 返回用户
                  整个 Agent 在这里卡住

一、超时控制:不让任何一个 Tool 把 Agent 卡死

基础超时实现

func (myTool) Call(ctx context.Context, input string) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    result := make(chan string, 1)
    errCh := make(chan error, 1)

    go func() {
        // 实际的工作逻辑
        r, err := doHeavyWork()
        if err != nil {
            errCh <- err
        } else {
            result <- r
        }
    }()

    select {
    case r := <-result:
        return r, nil
    case err := <-errCh:
        return "", err
    case <-ctx.Done():
        return "", fmt.Errorf("tool execution timeout after 5s")
    }
}

全局默认超时

可以在创建 Agent 时配置全局超时:

config := &launcher.Config{
    AgentLoader: agent.NewSingleLoader(myAgent),
    Timeout:     30 * time.Second,  // 全局超时 30 秒
}

Tool 级别的超时配置

把超时时间放到 Tool 的配置里:

type configurableTool struct {
    timeout time.Duration
}

func (t configurableTool) Call(ctx context.Context, input string) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, t.timeout)
    defer cancel()
    // ...
}

二、并发调用:让多个 Tool 同时跑

如果一个请求需要调用多个独立的 Tool,可以并发执行节省时间:

串行:Tool A(2秒)+ Tool B(3秒)+ Tool C(4秒)= 9秒
并行:[Tool A(2秒), Tool B(3秒), Tool C(4秒)] = 4秒

ADK Go 的并发 Tool 调用

ADK Go 本身支持同时调用多个 Tool。只需要在 Instruction 里说明可以并行调用:

Instruction: "你可以同时调用多个工具获取信息,不需要等一个完成再调用下一个。"

Model 会自动决定哪些 Tool 可以并行调用。

在 Tool 内部并发处理批量数据

如果一个 Tool 需要处理多个请求(比如批量查书):

func (batchTool) Call(ctx context.Context, input string) (string, error) {
    var args struct {
        Titles []string `json:"titles"`
    }
    json.Unmarshal([]byte(input), &args)

    // 并发查询
    var wg sync.WaitGroup
    results := make([]string, len(args.Titles))

    for i, title := range args.Titles {
        wg.Add(1)
        go func(idx int, t string) {
            defer wg.Done()
            results[idx] = queryBook(t)
        }(i, title)
    }

    wg.Wait()
    return strings.Join(results, "\n"), nil
}

三、连接池管理:减少 TCP 连接开销

Tool 往往需要调用外部 API。每次调用都新建连接会有建立握手的开销。连接池复用连接:

HTTP 客户端连接池

type httpTool struct {
    client *http.Client
}

func newHTTPTool() *httpTool {
    return &httpTool{
        client: &http.Client{
            Transport: &http.Transport{
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 10,
                IdleConnTimeout:     90 * time.Second,
            },
            Timeout: 10 * time.Second,
        },
    }
}

func (t *httpTool) Call(ctx context.Context, input string) (string, error) {
    req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.example.com/query", strings.NewReader(input))
    resp, err := t.client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    // ...
}

数据库连接池

如果 Tool 查数据库:

import "database/sql"
import _ "github.com/go-sql-driver/mysql"

type dbTool struct {
    db *sql.DB
}

func newDBTool() *dbTool {
    db, _ := sql.Open("mysql", "user:password@tcp(localhost:3306)/mydb?parseTime=true")
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(5 * time.Minute)
    return &dbTool{db: db}
}

四、资源限制:防止 Tool 耗尽系统资源

并发数限制

用信号量限制同时执行的并发数:

var (
    maxConcurrent = 10
    semaphore     = make(chan struct{}, maxConcurrent)
)

func (t *myTool) Call(ctx context.Context, input string) (string, error) {
    select {
    case semaphore <- struct{}{}:
        defer func() { <-semaphore }()
    case <-ctx.Done():
        return "", fmt.Errorf("too many concurrent requests")
    }
    // 执行实际逻辑
}

内存限制

Go 的 runtime 可以监控 goroutine 数量:

import "runtime"

func checkResources() error {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    if m.NumGoroutine > 10000 {
        return fmt.Errorf("too many goroutines: %d", m.NumGoroutine)
    }
    return nil
}

五、监控与调试:知道 Tool 跑得怎么样

日志 Tool 执行时间

func (t *myTool) Call(ctx context.Context, input string) (string, error) {
    start := time.Now()
    result, err := doWork()
    duration := time.Since(start)

    if err != nil {
        log.Printf("Tool %s failed after %v: %v", t.Name(), duration, err)
    } else {
        log.Printf("Tool %s succeeded in %v", t.Name(), duration)
    }
    return result, err
}

指标上报

接入 Prometheus:

import "github.com/prometheus/client_golang/prometheus"

var toolDuration = prometheus.NewHistogramVec(
    prometheus.NewHistogramOpts{
        Name:    "tool_execution_duration_seconds",
        Help:    "Tool execution duration",
        Buckets: []float64{0.1, 0.5, 1, 3, 5, 10},
    },
    []string{"tool_name", "status"},
)

func init() {
    prometheus.Register(toolDuration)
}

func (t *myTool) Call(ctx context.Context, input string) (string, error) {
    start := time.Now()
    result, err := doWork()

    toolDuration.WithLabelValues(t.Name(), statusFromError(err)).Observe(time.Since(start).Seconds())
    return result, err
}

下一步

性能优化做完了,接下来看 MCP——Model Context Protocol,这是 ADK Go 连接外部服务的主流方式,可以把任何暴露了 MCP 接口的服务接入 Agent。

Function Tool 编写基础 | MCP Server 接入实战 →


想跟着学更多 Go ADK 实战?关注「全栈之巅-梦兽编程」公众号,每周更新 Go / AI 编程实战干货。