目录 · 第 3 / 28 章
EinoPart I · ADK 的使用

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 {
BaseTool
InvokableRun(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。每个 ParameterInfoType / 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。

源码

正在读取完整文件…