接口定義:
// BaseTool get tool info for ChatModel intent recognition.
type BaseTool interface {
Info(ctx context.Context) (*schema.ToolInfo, error)
}
// InvokableTool the tool for ChatModel intent recognition and ToolsNode execution.
// nolint: byted_s_interface_name
type InvokableTool interface {
BaseTool
// InvokableRun call function with arguments in JSON format
InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error)
}
Info方法: 返回工具的信息, 如Name, Desc及ParamsOneOf , 其中ParamsOneOf 字段是用于描述工具參數(shù)的結(jié)構(gòu)體字段,它定義了工具調(diào)用時所需的參數(shù)規(guī)范,供大模型理解和使用, 這里需要遵從openApI 3規(guī)范, 詳情參見規(guī)范中的schema定義(https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object) 。
InvokableRun方法: 方法調(diào)用, 參數(shù)為json字符串
接口實現(xiàn):
type invokableTool[T, D any] struct {
info *schema.ToolInfo
um UnmarshalArguments
m MarshalOutput
Fn OptionableInvokeFunc[T, D]
}
func (i *invokableTool[T, D]) Info(ctx context.Context) (*schema.ToolInfo, error) {
return i.info, nil
}
// InvokableRun invokes the tool with the given arguments.
func (i *invokableTool[T, D]) InvokableRun(ctx context.Context, arguments string, opts ...tool.Option) (output string, err error) {
var inst T
if i.um != nil {
var val interface{}
val, err = i.um(ctx, arguments)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to unmarshal arguments, toolName=%s, err=%w", i.getToolName(), err)
}
gt, ok := val.(T)
if !ok {
return "", fmt.Errorf("[LocalFunc] invalid type, toolName=%s, expected=%T, given=%T", i.getToolName(), inst, val)
}
inst = gt
} else {
inst = generic.NewInstance[T]()
err = sonic.UnmarshalString(arguments, &inst)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to unmarshal arguments in json, toolName=%s, err=%w", i.getToolName(), err)
}
}
resp, err := i.Fn(ctx, inst, opts...)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to invoke tool, toolName=%s, err=%w", i.getToolName(), err)
}
if i.m != nil {
output, err = i.m(ctx, resp)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to marshal output, toolName=%s, err=%w", i.getToolName(), err)
}
} else {
output, err = sonic.MarshalString(resp)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to marshal output in json, toolName=%s, err=%w", i.getToolName(), err)
}
}
return output, nil
}
invokableTool 類型包含四個屬性:
- info , ToolInfo類型, 工具的描述信息
- um UnmarshalArguments, 自定義反系列化函數(shù), 用于將json字符串反序列化為golang的結(jié)構(gòu)體, 對應(yīng)調(diào)用函數(shù)Fn 的input
type UnmarshalArguments func(ctx context.Context, arguments string) (interface{}, error)
- m MarshalOutput 自定義序列化函數(shù), 用于將工具調(diào)用輸出的golang結(jié)構(gòu)體(對應(yīng)調(diào)用函數(shù)Fn 的output)序列化化為json字符串
type MarshalOutput func(ctx context.Context, output interface{}) (string, error)
- Fn OptionableInvokeFunc[T, D]
Eino 中的工具調(diào)用
要不要調(diào)工具、調(diào)哪一個工具”完全由大模型自己決定,Eino 只是把「有哪些工具、每個工具能干什么」提前告訴模型,然后把模型的調(diào)用意圖解析出來并執(zhí)行。

在 Eino 中,工具最終會被包裝進 ToolsNode,并通過編排(Chain/Graph)與模型串聯(lián), 那么模型是如何調(diào)用工具, 并給工具傳參的?
一、模型側(cè):模型如何“知道”要調(diào)用哪個工具?
Eino 的 ChatModel 實現(xiàn)了 WithTools 方法,用于將工具信息注冊到模型上下文:
chatModel = chatModel.WithTools([]*schema.ToolInfo{
{
Name: "get_weather",
Desc: "獲取城市天氣",
ParamsOneOf: schema.NewParamsOneOfByParams(...),
},
})
作用:告訴模型有哪些工具可用,每個工具的名稱、描述、參數(shù)結(jié)構(gòu)。
機制:模型在生成回復(fù)時,會自動推理判斷是否需要調(diào)用工具,如果需要會返回一個 ToolCall 結(jié)構(gòu),包含:
- name:工具名
- arguments:JSON 字符串形式的參數(shù)
二、框架側(cè): 觸發(fā)工具調(diào)用, 并返回結(jié)果給大模型
在 Eino 中,工具最終會被包裝進 ToolsNode, ToolsNode 會根據(jù)上游ChatModel節(jié)點返回的 msg.ToolCalls 提取工具名和參數(shù), 并通過 genToolCallTasks函數(shù),將工具名映射到實際的 InvokableTool 實現(xiàn), 最后調(diào)用對應(yīng)工具的 InvokableRun(ctx, argumentsInJSON) 方法, 將結(jié)果返回給大模型。
type toolsTuple struct {
indexes map[string]int // 工具名稱和ID的映射
}
type Message struct {
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
}
func (tn *ToolsNode) genToolCallTasks(ctx context.Context, tuple *toolsTuple, input *schema.Message, executedTools map[string]string, isStream bool) ([]toolCallTask, error) {
index, ok := tuple.indexes[toolCall.Function.Name]
if !ok {
if tn.unknownToolHandler == nil {
return nil, fmt.Errorf("tool %s not found in toolsNode indexes", toolCall.Function.Name)
}
toolCallTasks[i] = newUnknownToolTask(toolCall.Function.Name, toolCall.Function.Arguments, toolCall.ID, tn.unknownToolHandler)
}
}
tuple : 已注冊的工具三元組,
input : 上游模型的輸入Message信息,ToolCalls 屬性描述模型需要調(diào)用工具信息, genToolCallTasks通過Function Name 在已注冊的Tools中找到需要調(diào)用的工具。
上述流程的偽代碼實現(xiàn):
// 1. 把工具塞進模型
model := openai.NewChatModel(...).
WithTools(toolInfos)
// 2. 模型返回
msg, _ := model.Generate(ctx, messages) //messages為用戶輸入
// 3. Eino 判斷
if len(msg.ToolCalls) > 0 {
results := toolsNode.Invoke(ctx, msg.ToolCalls)
msg.Content= msg.Content + results // observation
goto 2 // 繼續(xù)對話循環(huán)
}
實現(xiàn)接口的方式:
- 方式一: 直接實現(xiàn)接口, 定義tool類型, 實現(xiàn)InvokableTool 接口中的Info 和InvokableRun 方法:
type AddUser struct{}
func (t *AddUser) Info(_ context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "add_user",
Desc: "add user",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
// omit,參考上文中構(gòu)建 params 約束的方式
}),
}, nil
}
func (t *AddUser) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
// 1. 反序列化 argumentsInJSON,處理 option 等
user, _ := json.Unmarshal([]byte(argumentsInJSON))
// 2. 處理業(yè)務(wù)邏輯
// 3. 把結(jié)果序列化為 string 并返回
return `{"msg": "ok"}`, nil
}
- 方式二:把本地函數(shù)轉(zhuǎn)為 tool,Eino 中提供了 NewTool 的方法來把一個函數(shù)轉(zhuǎn)成 tool,同時,針對為參數(shù)約束通過結(jié)構(gòu)體的 tag 來表示的場景提供了 InferTool 的方法,讓構(gòu)建的過程更加簡單。
當一個函數(shù)滿足下面這種函數(shù)簽名時,就可以用 NewTool 把其變成一個 InvokableTool :
type InvokeFunc[T, D any] func(ctx context.Context, input T) (output D, err error)
NewTool 的方法如下:
// 代碼見: github.com/cloudwego/eino/components/tool/utils/invokable_func.go
func NewTool[T, D any](desc *schema.ToolInfo, i InvokeFunc[T, D], opts ...Option) tool.InvokableTool
從 NewTool 中可以看出,構(gòu)建一個 tool 的過程需要分別傳入 ToolInfo 和 InvokeFunc ,其中,ToolInfo 中包含 ParamsOneOf 的部分,這代表著函數(shù)的入?yún)⒓s束, 需要手工構(gòu)建并維護, 且需要和InvokeFunc 的 input 參數(shù)需要保持一致。更優(yōu)雅的解決方法是 “參數(shù)約束直接維護在 input 參數(shù)類型定義中”, 同過GoStruct2ParamsOneOf 方法,自動從類型屬性的Tag標簽推導(dǎo)出約束信息:
package main
import (
"fmt"
"github.com/cloudwego/eino/components/tool/utils"
)
func main() {
type PostRequest struct {
URL string `json:"url" jsonschema:"description=The URL to make the POST request"`
Body string `json:"body" jsonschema:"description=The body to send in the POST request"`
}
param, err := utils.GoStruct2ParamsOneOf[PostRequest]()
if err != nil {
panic(err)
}
fmt.Println(param)
}
http post 請求tool 實現(xiàn):
- 定義PostRequestTool類型:
type PostRequestTool struct {
config *Config // 工具的name , dese, 及自定義headers信息
client *http.Client
}
- Post 請求方法實現(xiàn):
type PostRequest struct {
URL string `json:"url" jsonschema:"description=The URL to make the POST request"`
Body string `json:"body" jsonschema:"description=The body to send in the POST request"`
}
func (r *PostRequestTool) Post(ctx context.Context, req *PostRequest) (string, error) {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, req.URL, strings.NewReader(req.Body))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
for key, value := range r.config.Headers {
httpReq.Header.Set(key, value)
}
resp, err := r.client.Do(httpReq)
if err != nil {
return "", fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
return string(body), nil
}
將PostRequestTool 的Post 函數(shù)轉(zhuǎn)為 tool :
func NewTool(ctx context.Context, config *Config) (tool.InvokableTool, error) {
reqTool, err := newRequestTool(config) // 通過配置實例化一個PostRequestTool 對象
if err != nil {
return nil, fmt.Errorf("failed to create request tool: %w", err)
}
invokableTool, err := utils.InferTool(config.ToolName, config.ToolDesc, reqTool.Post)
if err != nil {
return nil, fmt.Errorf("failed to infer the tool: %w", err)
}
return invokableTool, nil
}
http post 請求工具使用完整示例:
package main
import (
"context"
"fmt"
"log"
"github.com/bytedance/sonic"
post "github.com/cloudwego/eino-ext/components/tool/httprequest/post"
)
func main() {
config := &post.Config{
Headers: map[string]string{
"User-Agent": "MyCustomAgent",
"Content-Type": "application/json; charset=UTF-8",
},
}
ctx := context.Background()
tool, err := post.NewTool(ctx, config)
if err != nil {
log.Fatalf("Failed to create tool: %v", err)
}
request := &post.PostRequest{
URL: "https://jsonplaceholder.typicode.com/posts",
Body: `{"title": "my title","body": "my body","userId": 1}`,
}
jsonReq, err := sonic.Marshal(request)
if err != nil {
log.Fatalf("Error marshaling JSON: %v", err)
}
resp, err := tool.InvokableRun(ctx, string(jsonReq))
if err != nil {
log.Fatalf("Post failed: %v", err)
}
fmt.Println(resp)
}