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 字段是用于描述工具參數(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í)行。

image.png

在 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):

  1. 定義PostRequestTool類型:
type PostRequestTool struct {
    config *Config // 工具的name , dese, 及自定義headers信息
    client *http.Client
}
  1. 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)
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容