Eino Tool 开发:三种姿势,我踩过的坑
最近在用 Eino 做 Agent 开发,发现创建 Tool 有三种方式。踩了一些坑,记录一下。
为什么需要 Tool?
LLM 本身只能处理文本,想让它查数据库、调 API、操作文件?这些都需要 Tool。
简单说:Tool 就是给 LLM 装上的"手",让它能干实事。
Eino 里创建 Tool 有三种方式,各有优劣,选对了事半功倍。
先说结论
不想看长文的直接看这里:
- 赶时间/写 demo:
InferTool,一行搞定 - 要控制参数描述:
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), ¶ms); 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
一个文件一个工具,改谁都不影响别人。
总结
| 场景 | 推荐方式 |
|---|---|
| 快速验证 / demo | InferTool |
| 需要枚举 / 精确描述 | NewTool |
| 生产环境 | 实现接口 |
开发建议:
- 需求明确、直奔生产 → 直接实现接口,省得重构
- 方案不确定、先验证可行性 → 用 InferTool 快速试错,确定后再重构