03给 Agent 装上工具
ToolInfo / InvokableTool、WithTools 不可变绑定、自动注入的 exit 工具与 returnDirectly。
components/tool/interface.gocomponents/model/interface.go:95adk/chatmodel.go工具:让 Agent 能「做事」而不只是「说话」
没有工具的 Agent 只会说话。一旦装上工具,它就能查天气、读文件、调 API——模型负责「决定调用哪个工具、传什么参数」,框架负责「真正执行并把结果喂回去」。这一章拆开这套契约。
flowchart LR M["模型<br/>决定调哪个工具 + 参数(JSON)"] -->|"ToolCall"| F["框架 · ToolsNode<br/>按名字找工具并执行"] F -->|"argumentsInJSON"| T["工具实现<br/>InvokableRun / StreamableRun"] T -->|"结果字符串"| F F -->|"作为 Tool 消息喂回"| M
模型出主意,框架干活
先看给 Agent 加工具的样子:
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: "assistant", Instruction: "你可以使用工具来完成任务。", Model: chatModel, ToolsConfig: adk.ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{weatherTool, searchTool}, }, },})注意工具是通过 ToolsConfig 声明式地配置的——ADK 没有 agent.AddTool(...) 这类可变方法。为什么?下一节的「不可变绑定」会解释。
工具的三层接口
工具接口定义在 components/tool/interface.go,是一个渐进式的三层设计:
/* * Copyright 2024 CloudWeGo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
package tool
import ( "context"
"github.com/cloudwego/eino/schema")
// BaseTool provides the metadata that a ChatModel uses to decide whether and// how to call a tool. Info returns a [schema.ToolInfo] containing the tool// name, description, and parameter JSON schema.//// BaseTool alone is sufficient when passing tool definitions to a ChatModel// via WithTools — the model only needs the schema to generate tool calls.// To also execute the tool, implement [InvokableTool] or [StreamableTool].type BaseTool interface { Info(ctx context.Context) (*schema.ToolInfo, error)}
// InvokableTool is a tool that can be executed by ToolsNode.//// InvokableRun receives the model's tool call arguments as a JSON-encoded// string and returns a plain string result that is sent back to the model as// a tool message. The framework handles JSON decoding automatically when using// … 这只是文件开头 40 行,并非完整声明;点击上方「浏览完整文件」// 第一层:只提供「我是什么」type BaseTool interface { Info(ctx context.Context) (*schema.ToolInfo, error)}
// 第二层:可被同步调用type InvokableTool interface { BaseTool InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error)}
// 第三层:可流式调用type StreamableTool interface { BaseTool StreamableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (*schema.StreamReader[string], error)}这个分层很讲究:
BaseTool已经够用来「告诉模型有这个工具」。模型选工具只需要 schema(名字、描述、参数),不需要能执行。所以只实现BaseTool的工具也能被WithTools传给模型。- 要真正执行,才需要
InvokableTool(同步返回字符串)或StreamableTool(流式返回)。 - 参数以 JSON 字符串 的形式传入(
argumentsInJSON),返回也是字符串。这是模型世界与 Go 世界的边界:模型吐 JSON,工具收 JSON。
📝 还有增强版接口
源码里还有
EnhancedInvokableTool/EnhancedStreamableTool(components/tool/interface.go:67),它们用结构化的*schema.ToolArgument/*schema.ToolResult取代裸字符串,支持多模态。当一个工具同时实现了标准版和增强版,ToolsNode 会优先用增强版。入门阶段用标准版即可。// EnhancedInvokableTool is a tool that returns structured multimodal results.//// Unlike [InvokableTool], arguments arrive as a [schema.ToolArgument] (not a// raw JSON string) and the result is a [schema.ToolResult] which can carry// text, images, audio, video, and file content.//// When a tool implements both a standard and an enhanced interface, ToolsNode// prioritises the enhanced interface.type EnhancedInvokableTool interface {BaseToolInvokableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...Option) (*schema.ToolResult, error)}
ToolInfo:模型「看到」的工具样子
模型并不知道你的 Go 函数长什么样,它只看到 ToolInfo(定义在 schema/tool.go):
type ToolInfo struct { Name string // 唯一名字 Desc string // 何时/为何用它——写给模型看的 Extra map[string]any *ParamsOneOf // 参数 schema;nil 表示无参数}ParamsOneOf 有两种写法(components/tool/interface.go 对应的 schema/tool.go):要么用 map[string]*ParameterInfo 手写参数,要么直接给一个 JSON Schema。每个 ParameterInfo 带 Type / Desc / Required / Enum 等,这些字段最终会拼成发给模型的工具定义。
Desc 是工具能不能被正确调用的关键:它是模型判断「什么时候该用这个工具」的唯一依据。写「查询天气」和写「根据城市名和日期查询未来天气预报,输入城市中文名」,模型的调用准确率天差地别。
为什么工具绑定是不可变的
模型侧绑定工具的接口在 components/model/interface.go:95 附近——ToolCallingChatModel:
type ToolCallingChatModel interface { BaseChatModel WithTools(tools []*schema.ToolInfo) (ToolCallingChatModel, error)}关键在返回值:WithTools 返回一个新的模型实例,而不是修改原来的。文档明确说这是为了并发安全——你可以把一个基础模型跨 goroutine 共享,每次 WithTools 派生出一个带不同工具集的新实例,互不干扰。
对比一下已废弃的老接口 ChatModel.BindTools(...)error:它就地修改 receiver,因此有数据竞争风险,已被标注废弃。
🔑 本章的设计钥匙
「不可变绑定返回新实例」是贯穿 Eino 的一条纪律。它换来的是可共享、可组合、无数据竞争:同一个基础模型能被多个 Agent 安全复用。这也解释了为什么
ToolsConfig是声明式的——工具集在 Agent 构造时就固定下来,而不是运行中可变的状态。可变性是并发的敌人,Eino 用「派生新实例」把它挡在门外。
运行时,ChatModelAgent 正是通过这个选项把工具喂给模型的(adk/chatmodel.go):它收集所有工具的 Info,在调用模型时 model.WithTools(toolInfos) 传进去。你在 Config 里写 Tools,框架在运行时翻译成 WithTools——这就是「ChatModelAgent 用 model.WithTools 配置工具」的完整链路。
/* * Copyright 2025 CloudWeGo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
package adk
import ( "bytes" "context" "encoding/gob" "errors" "fmt" "math" "runtime/debug" "strings" "sync" "sync/atomic"
"github.com/bytedance/sonic"
"github.com/cloudwego/eino/adk/internal" "github.com/cloudwego/eino/components/model" "github.com/cloudwego/eino/components/prompt" "github.com/cloudwego/eino/components/tool" "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/internal/safe" "github.com/cloudwego/eino/schema")// … 这只是文件开头 40 行,并非完整声明;点击上方「浏览完整文件」两个「特殊工具」:Exit 与 returnDirectly
有两个和工具相关、但容易混淆的机制:
Exit 工具不是自动注入的。只有你在 Config 里设置了 Exit(adk/chatmodel.go:295),框架才会把它加入工具集并生成 Exit 动作;不设就没有。ADK 提供了现成的 ExitTool(adk/chatmodel.go:597),它接受一个 final_result 参数,被调用时发出 NewExitAction() 结束运行。
// TypedChatModelAgentConfig is the generic configuration for ChatModelAgent.type TypedChatModelAgentConfig[M MessageType] struct { // Name of the agent. Better be unique across all agents. // Optional. If empty, the agent can still run standalone but cannot be used as // a sub-agent tool via NewAgentTool (which requires a non-empty Name). Name string // Description of the agent's capabilities. // Helps other agents determine whether to transfer tasks to this agent. // Optional. If empty, the agent can still run standalone but cannot be used as // a sub-agent tool via NewAgentTool (which requires a non-empty Description). Description string // Instruction used as the system prompt for this agent. // Optional. If empty, no system prompt will be used. // Supports f-string placeholders for session values in default GenModelInput, for example: // "You are a helpful assistant. The current time is {Time}. The current user is {User}." // These placeholders will be replaced with session values for "Time" and "User". Instruction string
// Model is the chat model used by the agent. // If your ChatModelAgent uses any tools, this model must support the model.WithTools // call option, as that's how ChatModelAgent configures the model with tool information. Model model.BaseModel[M]
ToolsConfig ToolsConfig
// GenModelInput transforms instructions and input messages into the model's input format. // Optional. Defaults to defaultGenModelInput which combines instruction and messages. GenModelInput TypedGenModelInput[M]
// Exit defines the tool used to terminate the agent process. // Optional. If nil, no Exit Action will be generated. // You can use the provided 'ExitTool' implementation directly. // // NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven // to be more effective empirically. Consider using ChatModelAgent with AgentTool // or DeepAgent instead for most multi-agent scenarios. Exit tool.BaseTool
// OutputKey stores the agent's response in the session. // Optional. When set, stores output via AddSessionValue(ctx, outputKey, msg.Content). // // NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven // to be more effective empirically. Consider using ChatModelAgent with AgentTool // or DeepAgent instead for most multi-agent scenarios. OutputKey string
// MaxIterations defines the upper limit of ChatModel generation cycles. // The agent will terminate with an error if this limit is exceeded.// … 省略 109 行;完整声明 L259–415,点击上方「浏览完整文件」type ExitTool struct{}returnDirectly 是「调用即返回」:正常情况下工具结果会被喂回模型继续循环,但如果某工具名列在 ToolsConfig.ReturnDirectly(adk/chatmodel.go:141)里,它一被调用,agent 就直接把结果作为最终输出返回,不再进模型。这对「最后一步就是执行某个动作」的工具很有用。
type ToolsConfig struct { compose.ToolsNodeConfig
// ReturnDirectly specifies tools that cause the agent to return immediately when called. // The map keys are tool names indicate whether the tool should trigger immediate return. ReturnDirectly map[string]bool
// EmitInternalEvents indicates whether internal events from agentTool should be emitted // to the parent agent's AsyncGenerator, allowing real-time streaming of nested agent output // to the end-user via Runner. // // Note that these forwarded events are NOT recorded in the parent agent's runSession. // They are only emitted to the end-user and have no effect on the parent agent's state // or checkpoint. // // Action Scoping: // Actions emitted by the inner agent are scoped to the agent tool boundary: // - Interrupted: Propagated via CompositeInterrupt to allow proper interrupt/resume // - Exit, TransferToAgent, BreakLoop: Ignored outside the agent tool EmitInternalEvents bool}值得一提:框架会自动往 returnDirectly 里加两项——transfer 工具和 Exit 工具(adk/chatmodel.go:141对应的运行时逻辑)。因为这两个天然就是「调用即终结当前流程」。
本章小结
- 工具让 Agent 能「做事」;通过
ToolsConfig声明式配置,没有可变的 AddTool。 - 工具接口三层:
BaseTool(仅描述)→InvokableTool(同步)→StreamableTool(流式);参数走 JSON 字符串边界。 ToolInfo是模型看到的工具样子,Desc直接决定调用准确率。- 工具绑定
WithTools返回新实例(不可变),换来并发安全与可组合性。 Exit工具需显式配置;returnDirectly让工具「调用即返回」,transfer/exit 会被自动加入。
下一章,我们让 Agent 拥有记忆——看清 session 如何跨步骤传值、历史如何维护,以及事件流怎么变成 SSE。