【Eino 框架入门】Backend 是怎么变成工具的
上一篇我们这样创建 Agent:
go
backend, _ := localbk.NewBackend(ctx, &localbk.Config{})
agent, _ := deep.New(ctx, &deep.Config{
Backend: backend,
StreamingShell: backend,
})
传了个 Backend 进去,Agent 就有了 read_file、grep 等工具。
问题是:Backend 是怎么变成工具的?
这篇文章跟踪一个工具的诞生过程:从 Backend 配置到模型能调用的工具。
一个工具的诞生:read_file
我们以 read_file 工具为例,看它从无到有的完整过程。
css
你写的代码 框架做的事
─────────────────────────────────────────────────
Backend: backend → filesystem middleware
↓
toolSpec{createFunc: newReadFileTool}
↓
utils.InferTool(readFileArgs)
↓
ToolInfo{read_file, 参数schema}
↓
模型看到工具说明书
下面一步步拆解。
第一步:Backend 进入 middleware
deep.New() 收到你的配置后,会创建一个 filesystem middleware:
go
// adk/prebuilt/deep/deep.go
func buildBuiltinAgentMiddlewares(ctx context.Context, cfg *Config) {
if cfg.Backend != nil {
fm, _ := filesystem.New(ctx, &filesystem.MiddlewareConfig{
Backend: cfg.Backend, // 你的 Backend
StreamingShell: cfg.StreamingShell, // 你的 StreamingShell
})
// fm 就是一个 middleware,里面已经包含了工具
}
}
Backend 就这样被传进去了。接下来看 middleware 内部发生了什么。
第二步:toolSpec 模式创建工具
middleware 调用 getFilesystemTools() 生成工具列表。
这里用了一个设计模式:toolSpec(工具规格)。
go
// adk/middlewares/filesystem/filesystem.go
type toolSpec struct {
config *ToolConfig // 可选:自定义名称、描述、禁用
createFunc func(name, desc string) (tool.BaseTool, error)
}
每个工具用一个 toolSpec 描述。比如 read_file:
go
toolSpecs := []toolSpec{
{
config: cfg.ReadFileToolConfig, // 用户可配置
createFunc: func(name, desc string) (tool.BaseTool, error) {
if cfg.Backend != nil {
return newReadFileTool(cfg.Backend, name, desc)
}
return nil, nil
},
},
// ls, write_file, grep 等同理...
}
// 统一遍历创建
for _, spec := range toolSpecs {
t, _ := createToolFromSpec(cfg, spec)
if t != nil {
tools = append(tools, t)
}
}
为什么用 toolSpec?
- 统一处理:所有工具的创建逻辑一致
- 可扩展:用户可以通过 ToolConfig 覆盖名称、描述、甚至整个工具
第三步:Go 函数 → Tool + Schema
newReadFileTool() 是核心,它把一个 Go 函数变成模型能理解的工具:
go
// adk/middlewares/filesystem/filesystem.go
func newReadFileTool(fs filesystem.Backend, name, desc string) (tool.BaseTool, error) {
return utils.InferTool("read_file", desc, func(ctx context.Context, input readFileArgs) (string, error) {
// 这里调用 Backend 的 Read 方法
fileCt, err := fs.Read(ctx, &filesystem.ReadRequest{
FilePath: input.FilePath,
Offset: input.Offset,
Limit: input.Limit,
})
// ... 格式化返回
})
}
关键魔法 :utils.InferTool() 从结构体 tag 推断 JSON Schema:
go
type readFileArgs struct {
FilePath string `json:"file_path" jsonschema:"description=The path to the file to read"`
Offset int `json:"offset" jsonschema:"description=The line number to start reading from"`
Limit int `json:"limit" jsonschema:"description=The number of lines to read"`
}
反射读取 tag,自动生成模型能理解的 Schema:
json
{
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "The path to the file to read"},
"offset": {"type": "integer", "description": "..."},
"limit": {"type": "integer", "description": "..."}
},
"required": ["file_path"]
}
这就是模型看到的"工具说明书"。模型根据这个说明书决定:
- 要不要调用工具
- 调用时传什么参数
第四步:工具注入 Agent
工具创建好了,怎么给 Agent?
middleware 有个 BeforeAgent 钩子,在 Agent 运行前执行:
go
// adk/middlewares/filesystem/filesystem.go
func (m *filesystemMiddleware) BeforeAgent(ctx context.Context, runCtx *adk.ChatModelAgentContext) {
nRunCtx := *runCtx
// 把工具追加到 Agent 的工具列表
nRunCtx.Tools = append(nRunCtx.Tools, m.additionalTools...)
return ctx, &nRunCtx
}
然后 Agent 运行时,会:
- 调用每个工具的
Info()方法获取 ToolInfo - 通过
model.WithTools(toolInfos)把工具信息传给模型 - 模型根据工具信息决定是否调用
总结一张图
scss
你的代码 框架处理 模型看到
─────────────────────────────────────────────────────────────────────
deep.Config{
Backend: backend → filesystem.New()
} ↓
getFilesystemTools()
↓
toolSpec 模式
↓
newReadFileTool()
↓
utils.InferTool()
↓
ToolInfo{ → "read_file 工具,
Name: "read_file" 参数:file_path..."
Params: JSON Schema
}
↓
BeforeAgent() 注入
↓
model.WithTools()
核心:Backend 提供文件系统操作能力,框架把它封装成工具,生成 JSON Schema 说明书,模型根据说明书调用。
工具与 Backend 方法的对应
| 工具 | Backend 方法 | 用途 |
|---|---|---|
ls |
LsInfo() |
列目录 |
read_file |
Read() |
读文件 |
write_file |
Write() |
写文件 |
edit_file |
Edit() |
编辑文件 |
glob |
GlobInfo() |
文件模式匹配 |
grep |
GrepRaw() |
搜索内容 |
execute |
Execute() / ExecuteStreaming() |
执行命令 |
所以你传一个 Backend,框架就帮你生成这 7 个工具。