Eino中的组件-Tool

接口定义:

// 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 字段是用于描述工具参数的结构体字段,它定义了工具调用时所需的参数规范,供大模型理解和使用, 这里需要遵从openApI 3规范, 详情参见规范中的schema定义(https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object) 。

InvokableRun方法: 方法调用, 参数为json字符串

接口实现:

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, 自定义反系列化函数, 用于将json字符串反序列化为golang的结构体, 对应调用函数Fn 的input
type UnmarshalArguments func(ctx context.Context, arguments string) (interface{}, error)
  • m MarshalOutput 自定义序列化函数, 用于将工具调用输出的golang结构体(对应调用函数Fn 的output)序列化化为json字符串
type MarshalOutput func(ctx context.Context, output interface{}) (string, error)
  • Fn OptionableInvokeFunc[T, D]

Eino 中的工具调用

要不要调工具、调哪一个工具”完全由大模型自己决定,Eino 只是把「有哪些工具、每个工具能干什么」提前告诉模型,然后把模型的调用意图解析出来并执行。

image.png

在 Eino 中,工具最终会被包装进 ToolsNode,并通过编排(Chain/Graph)与模型串联, 那么模型是如何调用工具, 并给工具传参的?

一、模型侧:模型如何“知道”要调用哪个工具?

Eino 的 ChatModel 实现了 WithTools 方法,用于将工具信息注册到模型上下文:

chatModel = chatModel.WithTools([]*schema.ToolInfo{
    {
        Name:        "get_weather",
        Desc:        "获取城市天气",
        ParamsOneOf: schema.NewParamsOneOfByParams(...),
    },
})

作用:告诉模型有哪些工具可用,每个工具的名称、描述、参数结构。
机制:模型在生成回复时,会自动推理判断是否需要调用工具,如果需要会返回一个 ToolCall 结构,包含:

  • name:工具名
  • arguments:JSON 字符串形式的参数

二、框架侧: 触发工具调用, 并返回结果给大模型

在 Eino 中,工具最终会被包装进 ToolsNode, ToolsNode 会根据上游ChatModel节点返回的 msg.ToolCalls 提取工具名和参数, 并通过 genToolCallTasks函数,将工具名映射到实际的 InvokableTool 实现, 最后调用对应工具的 InvokableRun(ctx, argumentsInJSON) 方法, 将结果返回给大模型。

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 属性描述模型需要调用工具信息, genToolCallTasks通过Function Name 在已注册的Tools中找到需要调用的工具。

上述流程的伪代码实现:

// 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 // 继续对话循环
}

实现接口的方式:

  • 方式一: 直接实现接口, 定义tool类型, 实现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,参考上文中构建 params 约束的方式
        }),
    }, nil
}

func (t *AddUser) InvokableRun(_ context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
    // 1. 反序列化 argumentsInJSON,处理 option 等
    user, _ := json.Unmarshal([]byte(argumentsInJSON))
    // 2. 处理业务逻辑
    // 3. 把结果序列化为 string 并返回
    return `{"msg": "ok"}`, nil
}

  • 方式二:把本地函数转为 tool,Eino 中提供了 NewTool 的方法来把一个函数转成 tool,同时,针对为参数约束通过结构体的 tag 来表示的场景提供了 InferTool 的方法,让构建的过程更加简单。

当一个函数满足下面这种函数签名时,就可以用 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 中可以看出,构建一个 tool 的过程需要分别传入 ToolInfo 和 InvokeFunc ,其中,ToolInfo 中包含 ParamsOneOf 的部分,这代表着函数的入参约束, 需要手工构建并维护, 且需要和InvokeFunc 的 input 参数需要保持一致。更优雅的解决方法是 “参数约束直接维护在 input 参数类型定义中”, 同过GoStruct2ParamsOneOf 方法,自动从类型属性的Tag标签推导出约束信息:

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 实现:

  1. 定义PostRequestTool类型:
type PostRequestTool struct {
    config *Config // 工具的name , dese, 及自定义headers信息
    client *http.Client
}
  1. Post 请求方法实现:
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 函数转为 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)
}
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容