Eino Tool 开发:三种姿势,我踩过的坑

13 阅读4分钟

Eino Tool 开发:三种姿势,我踩过的坑

最近在用 Eino 做 Agent 开发,发现创建 Tool 有三种方式。踩了一些坑,记录一下。

为什么需要 Tool?

LLM 本身只能处理文本,想让它查数据库、调 API、操作文件?这些都需要 Tool。

简单说:Tool 就是给 LLM 装上的"手",让它能干实事。

Eino 里创建 Tool 有三种方式,各有优劣,选对了事半功倍。

先说结论

不想看长文的直接看这里:

  • 赶时间/写 demoInferTool,一行搞定
  • 要控制参数描述NewTool,手动定义
  • 正经项目:实现接口,别偷懒

下面详细说。


方式一:InferTool

这个最省事,从函数签名自动推断工具定义。

// 定义参数结构体,tag 里写描述
type UserUpdateParams struct {
    UserID   string  `json:"user_id" jsonschema_description:"用户ID"`
    Name     *string `json:"name,omitempty" jsonschema_description:"用户名"`
    Email    *string `json:"email,omitempty" jsonschema_description:"邮箱地址"`
}

// 处理函数
func UpdateUserFunc(_ context.Context, params *UserUpdateParams) (string, error) {
    // 业务逻辑
    return `{"msg": "ok"}`, nil
}

// 一行创建工具
updateTool, _ := utils.InferTool("update_user", "更新用户信息", UpdateUserFunc)

优缺点

优点缺点
代码最简洁,一行创建依赖 jsonschema_description tag,容易遗漏
自动生成 JSON Schema,类型安全参数描述分散在 struct 中,不便于集中管理
函数签名改了自动同步无法注入外部依赖(db、cache 等)
编译期检查参数类型不支持枚举值(Enum)

适用场景:写 demo、快速原型、参数结构简单且无外部依赖的工具。


方式二:NewTool

手动定义 ToolInfo,然后挂个函数上去。

// 参数结构体
type UserCreateParams struct {
    Name string `json:"name"`
    Role string `json:"role"`
}

// 处理函数
func CreateUserFunc(_ context.Context, params *UserCreateParams) (string, error) {
    // 业务逻辑
    return `{"msg": "创建成功", "user_id": "123"}`, nil
}

// 创建工具
func getCreateUserTool() tool.InvokableTool {
    info := &schema.ToolInfo{
        Name: "create_user",
        Desc: "创建新用户",
        ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
            "name": {
                Type:     schema.String,
                Desc:     "用户名",
                Required: true,
            },
            "role": {
                Type: schema.String,
                Desc: "用户角色",
                Enum: []string{"admin", "user", "guest"}, // 枚举,这个 InferTool 做不到
            },
        }),
    }
    return utils.NewTool(info, CreateUserFunc)
}

优缺点

优点缺点
支持枚举值(Enum),InferTool 做不到需要同时维护 struct 和 ToolInfo 两处定义
参数描述集中,便于管理代码量比 InferTool 多
不依赖 struct tag参数修改时容易遗漏同步
可精确控制每个参数的细节同样无法注入外部依赖

适用场景:需要枚举值、参数描述要求精确、不想用 tag 污染 struct。


方式三:实现接口(生产环境推荐)

老实说,一开始我觉得这个方式太繁琐。但写过几个项目后,真香。

type ListUserTool struct {
    db    *sql.DB       // 依赖注入,清爽
    cache *redis.Client
}

func NewListUserTool(db *sql.DB, cache *redis.Client) *ListUserTool {
    return &ListUserTool{db: db, cache: cache}
}

func (t *ListUserTool) Info(_ context.Context) (*schema.ToolInfo, error) {
    return &schema.ToolInfo{
        Name: "list_user",
        Desc: "搜索用户",
        ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
            "keyword": {Type: schema.String, Desc: "搜索关键词"},
        }),
    }, nil
}

func (t *ListUserTool) InvokableRun(ctx context.Context, args string, _ ...tool.Option) (string, error) {
    // 解析参数
    var params struct{ Keyword string }
    if err := json.Unmarshal([]byte(args), &params); err != nil {
        return "", err
    }

    // 直接用 t.db,不用全局变量
    rows, err := t.db.Query(ctx, "SELECT * FROM users WHERE name LIKE ?", "%"+params.Keyword+"%")
    if err != nil {
        return "", err
    }
    defer rows.Close()

    // 查询结果...
    return `{"users": [{"id": "1", "name": "张三"}]}`, nil
}

优缺点

优点缺点
支持依赖注入(db、cache、logger 等)代码量最大
便于单元测试,可 mock 接口需要手动解析 JSON 参数
职责内聚,一个工具一个文件实现成本较高
可动态生成 ToolInfo-
符合 Go 接口设计模式-

适用场景:生产环境、需要外部依赖注入、需要单元测试、团队协作项目。


ParameterInfo 小记

定义参数的时候会用到这个结构:

type ParameterInfo struct {
    Type       DataType                   // 类型
    ElemInfo   *ParameterInfo             // 数组元素类型
    SubParams  map[string]*ParameterInfo  // 对象嵌套参数
    Desc       string                     // 描述
    Enum       []string                   // 枚举值
    Required   bool                       // 是否必填
}

几个常见写法:

// 字符串
&schema.ParameterInfo{Type: schema.String, Desc: "用户名", Required: true}

// 枚举(限制可选值)
&schema.ParameterInfo{Type: schema.String, Enum: []string{"admin", "user"}}

// 数组
&schema.ParameterInfo{
    Type: schema.Array,
    Desc: "用户标签",
    ElemInfo: &schema.ParameterInfo{Type: schema.String},
}

// 嵌套对象
&schema.ParameterInfo{
    Type: schema.Object,
    Desc: "地址",
    SubParams: map[string]*schema.ParameterInfo{
        "city": {Type: schema.String, Desc: "城市"},
    },
}

项目结构建议

工具多了之后,建议这样组织:

tools/
├── list_user.go      # ListUserTool
├── create_user.go    # CreateUserTool
├── update_user.go    # UpdateUserTool
└── mock_tools.go     # 测试用 mock

一个文件一个工具,改谁都不影响别人。


总结

场景推荐方式
快速验证 / demoInferTool
需要枚举 / 精确描述NewTool
生产环境实现接口

开发建议

  • 需求明确、直奔生产 → 直接实现接口,省得重构
  • 方案不确定、先验证可行性 → 用 InferTool 快速试错,确定后再重构