Agent 工具注册中心落地:让每个工具都有版本和回滚路径

一、当 Agent 的工具一夜之间全部不可用

Agent 的核心能力来自工具调用。
给大模型挂载搜索、计算、数据库查询等能力。
但工具变更往往成为事故的温床。
上周,搜索工具的 API 参数从 query 改成了 search_text
Agent 发出的所有搜索请求全部失败。
排查耗时两小时,最终只是改了一行代码。

更可怕的是,没人知道哪个版本的工具有效。
回滚?回到哪个版本?
测试环境用的是旧版 API 吗?
这些问题在缺少工具注册中心时,答案都是"不知道"。

工具管理有三层痛点。
第一,版本混乱:同一个工具存在多个签名版本。
第二,依赖不可见:工具之间的调用关系是隐式的。
第三,回滚无门:出问题时只能靠记忆恢复配置。

二、工具注册与版本管理的核心抽象

工具注册中心借鉴了微服务的服务注册思想。
核心是对工具进行三层抽象。

第一层是工具定义。
每个工具用一个 JSON Schema 描述其签名。
包含名称、描述、参数列表、返回值类型。

第二层是版本管理。
每次工具变更生成新版本。
版本号采用语义化规范(MAJOR.MINOR.PATCH)。
参数增减或类型变更视为 MAJOR 变更。

第三层是运行时绑定。
Agent 不直接引用工具代码。
而是通过注册中心,按版本号或标签获取工具实例。

工作流程如下:

flowchart TB
    A[开发者注册工具 v1.2.0] --> B[注册中心存储 Schema]
    B --> C[Agent 启动时获取最新版本]
    C --> D{工具调用成功?}
    D -->|是| E[记录调用日志]
    D -->|否| F[注册中心触发回滚]
    F --> G[切换至上一稳定版本]
    G --> H[发送告警通知]
    E --> I[更新调用统计]

版本策略的关键是标签系统。
每个版本可打上 lateststabledeprecated 等标签。
Agent 默认使用 stable 标签。
新版本需经过灰度验证,才能晋升为 stable

三、Go 实现的工具注册中心

以下是基于 Go 实现的核心组件。

package toolregistry

import (
    "context"
    "encoding/json"
    "fmt"
    "sync"
    "time"
)

// ToolSchema 描述一个工具的参数签名
type ToolSchema struct {
    Name        string          `json:"name"`
    Description string          `json:"description"`
    Version     string          `json:"version"`
    Parameters  json.RawMessage `json:"parameters"`
    Returns     json.RawMessage `json:"returns"`
    CreatedAt   time.Time       `json:"created_at"`
    Tags        []string        `json:"tags"`
}

// Registry 工具注册中心
type Registry struct {
    mu      sync.RWMutex
    tools   map[string][]ToolSchema // key: 工具名, value: 版本列表
    aliases map[string]string       // 标签 -> 版本号 映射
}

// NewRegistry 创建注册中心实例
func NewRegistry() *Registry {
    return &Registry{
        tools:   make(map[string][]ToolSchema),
        aliases: make(map[string]string),
    }
}

// Register 注册工具新版本
func (r *Registry) Register(schema ToolSchema) error {
    r.mu.Lock()
    defer r.mu.Unlock()

    if schema.Name == "" || schema.Version == "" {
        return fmt.Errorf("工具名和版本号不能为空")
    }

    // 检查版本是否已存在
    for _, v := range r.tools[schema.Name] {
        if v.Version == schema.Version {
            return fmt.Errorf("版本 %s 已存在", schema.Version)
        }
    }

    r.tools[schema.Name] = append(r.tools[schema.Name], schema)
    return nil
}

// Resolve 按标签解析工具版本
// tag 可以是具体版本号(v1.2.0)、标签(stable)、或 "latest"
func (r *Registry) Resolve(name string, tag string) (*ToolSchema, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()

    versions, ok := r.tools[name]
    if !ok || len(versions) == 0 {
        return nil, fmt.Errorf("工具 %s 未注册", name)
    }

    // 优先按标签查别名
    aliasKey := name + ":" + tag
    if v, ok := r.aliases[aliasKey]; ok {
        for i := range versions {
            if versions[i].Version == v {
                return &versions[i], nil
            }
        }
    }

    // 按标签匹配版本
    for i := len(versions) - 1; i >= 0; i-- {
        for _, t := range versions[i].Tags {
            if t == tag {
                return &versions[i], nil
            }
        }
    }

    // 兜底:返回最新版本
    return &versions[len(versions)-1], nil
}

// Rollback 回滚到指定版本并更新 stable 标签
func (r *Registry) Rollback(name string, targetVersion string) error {
    r.mu.Lock()
    defer r.mu.Unlock()

    versions, ok := r.tools[name]
    if !ok {
        return fmt.Errorf("工具 %s 未注册", name)
    }

    found := false
    for _, v := range versions {
        if v.Version == targetVersion {
            found = true
            break
        }
    }
    if !found {
        return fmt.Errorf("目标版本 %s 不存在", targetVersion)
    }

    // 将 stable 标签指向回滚版本
    r.aliases[name+":stable"] = targetVersion
    return nil
}

// ListVersions 列出工具的所有版本
func (r *Registry) ListVersions(name string) ([]ToolSchema, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()

    versions, ok := r.tools[name]
    if !ok {
        return nil, fmt.Errorf("工具 %s 未注册", name)
    }
    return versions, nil
}

工具变更通知机制同样关键。
当工具版本发生变更时,需要通知 Agent 热更新。

// Watch 监听工具版本变更
func (r *Registry) Watch(ctx context.Context, toolName string) <-chan ToolSchema {
    ch := make(chan ToolSchema, 10)
    go func() {
        ticker := time.NewTicker(5 * time.Second)
        defer ticker.Stop()
        var lastVersion string

        for {
            select {
            case <-ctx.Done():
                close(ch)
                return
            case <-ticker.C:
                schema, err := r.Resolve(toolName, "stable")
                if err != nil {
                    continue
                }
                if schema.Version != lastVersion {
                    lastVersion = schema.Version
                    ch <- *schema
                }
            }
        }
    }()
    return ch
}

四、适用边界与不适用场景

工具注册中心不是银弹。

首先,它增加了运维复杂度。
需要维护注册中心进程、持久化存储、健康检查。
对于只有 3 到 5 个工具的小型 Agent,引入注册中心得不偿失。

第二,注册中心本身是单点风险。
如果注册中心宕机,所有 Agent 无法获取工具。
必须配合本地缓存和降级策略。
Agent 应缓存最近一次获取的工具 Schema。

第三,Schema 漂移问题。
工具实现可能与 Schema 声明不一致。
需要配合自动化兼容性测试。
每次工具发布前,用 Schema 对实现做一轮参数校验。

禁用场景包括:
原型验证阶段,工具还在快速迭代时;
单个 Agent 独立运行的场景;
工具变更频率低于每月一次的项目。

对于生产环境的多 Agent 系统,注册中心是必需品而非可选项。
它把"改一行代码"的风险,从不可控变成了可控。

五、总结

工具注册中心解决了 Agent 系统中工具管理的三个核心问题。
版本管理让每次变更都有迹可循。
标签系统让灰度发布和快速回滚成为可能。
运行时绑定解耦了 Agent 和工具实现。

实现上需要注意线程安全、本地缓存降级、变更通知机制。
对于小规模场景,先保证工具函数签名不变更即可。
对于生产级多 Agent 系统,注册中心应当作为基础设施优先建设。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐