LLM
golang/eino eino框架的基础使用 在ChatModel中使用工具
字节跳动 Eino 框架工具调用教程,介绍 Tool 接口定义、ToolInfo 构造方法、JSON Schema 参数定义、限流重试处理以及完整的交互式工具调用示例。
Tool
- 参考
- 定义
// 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) } - 大模型如何调用工具
- 简单来说,就是将工具描述成字符串,告诉大模型这个工具是做什么的,调用这个工具需要哪些参数,然后大模型返回一条消息,告诉我们要调用这个工具,参数也发过来。然后本地代码执行工具完成之后将结果返回给大模型。
创建工具
- 在eino中,有几种基本的tool类型
增强型工具接口,支持多模态
type InvokableTool interface { BaseTool // InvokableRun call function with arguments in JSON format 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) }// EnhancedInvokableTool 是支持返回结构化多模态结果的工具接口 // 与返回字符串的 InvokableTool 不同,此接口返回 *schema.ToolResult // 可以包含文本、图片、音频、视频和文件 type EnhancedInvokableTool interface { BaseTool InvokableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...Option) (*schema.ToolResult, error) } // EnhancedStreamableTool 是支持返回结构化多模态结果的流式工具接口 type EnhancedStreamableTool interface { BaseTool StreamableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...Option) (*schema.StreamReader[*schema.ToolResult], error) } - 了解接口定义之后,除了工具本身的逻辑实现,最麻烦的是如何构造
schema.ToolInfo
构造ToolInfo
- 官方文档描述了两种实现方式,这里主要使用第二种,即使用
JSON Schema - eino提供了
func GoStruct2ParamsOneOf[T any](opts ...Option) (*schema.ParamsOneOf, error)来封装这个过程,只需要将工具的参数定义用结构体表示,然后调用这个函数即可。 - 在定义结构体时,有几个tag需要注意
jsonschema_description:"xxx"jsonschema:"enum=xxx,enum=yyy,enum=zzz"jsonschema:"required"jsonschema:"xxx,omitempty"=> 代表非 required
- 贴个官方的示例
type User struct { Name string `json:"name" jsonschema_description:"the name of the user" jsonschema:"required"` Age int `json:"age" jsonschema_description:"the age of the user"` Gender string `json:"gender" jsonschema:"enum=male,enum=female"` }
限流处理
- 在使用第三方的大模型时,可能会遇到限流错误,即429错误。可以添加重试机制来处理。
// streamWithRetry calls cm.Stream with exponential backoff on 429 errors. func streamWithRetry(ctx context.Context, cm model.ToolCallingChatModel, messages []*schema.Message) (*schema.StreamReader[*schema.Message], error) { const maxRetries = 5 const baseDelay = time.Second for attempt := range maxRetries { stream, err := cm.Stream(ctx, messages) if err == nil { return stream, nil } if !strings.Contains(err.Error(), "429") || attempt == maxRetries-1 { return nil, err } delay := time.Duration(math.Pow(2, float64(attempt))) * baseDelay fmt.Fprintf(os.Stderr, "\n[rate limited, retrying in %s]\n[Assistant] ", delay) select { case <-time.After(delay): case <-ctx.Done(): return nil, ctx.Err() } } panic("unreachable") }
交互式完整示例
package main
import (
"bufio"
"cc/tools"
"context"
"errors"
"flag"
"fmt"
"io"
"math"
"os"
"strings"
"time"
"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
)
func main() {
var instruction string
flag.StringVar(&instruction, "instruction", "You are a helpful assistant.", "")
flag.Parse()
ctx := context.Background()
globTool := tools.GlobTool{}
toolInfo, err := globTool.Info(ctx)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// tool registry: name → InvokableRun
toolRegistry := map[string]func(context.Context, string) (string, error){
globTool.Name(): func(c context.Context, args string) (string, error) {
return globTool.InvokableRun(c, args)
},
}
cm, err := newChatModel(ctx)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
cm, _ = cm.WithTools([]*schema.ToolInfo{toolInfo})
fmt.Println("Chat session started. Type 'exit' or 'quit' to end.")
scanner := bufio.NewScanner(os.Stdin)
messages := []*schema.Message{
schema.SystemMessage(instruction),
}
for {
fmt.Print("\n[User] ")
if !scanner.Scan() {
break
}
query := strings.TrimSpace(scanner.Text())
if query == "" {
continue
}
if query == "exit" || query == "quit" {
break
}
messages = append(messages, schema.UserMessage(query))
fmt.Print("[Assistant] ")
// Agentic loop: keep calling the model until it stops issuing tool calls.
for {
stream, err := streamWithRetry(ctx, cm, messages)
if err != nil {
fmt.Fprintf(os.Stderr, "\nError: %v\n", err)
break
}
var textContent string
var toolCallFrames []*schema.Message
for {
frame, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
fmt.Fprintf(os.Stderr, "\nStream error: %v\n", err)
break
}
if frame == nil {
continue
}
if frame.Content != "" {
textContent += frame.Content
fmt.Print(frame.Content)
}
if len(frame.ToolCalls) > 0 {
toolCallFrames = append(toolCallFrames, frame)
}
}
stream.Close()
if len(toolCallFrames) == 0 {
// No tool calls — we have the final answer.
messages = append(messages, schema.AssistantMessage(textContent, nil))
break
}
// Concatenate streaming tool-call chunks into a single message.
assistantMsg, err := schema.ConcatMessages(toolCallFrames)
if err != nil {
fmt.Fprintf(os.Stderr, "\nError concatenating tool calls: %v\n", err)
break
}
messages = append(messages, assistantMsg)
// Execute each tool call and append its result.
for _, tc := range assistantMsg.ToolCalls {
fn, ok := toolRegistry[tc.Function.Name]
if !ok {
fmt.Fprintf(os.Stderr, "\nUnknown tool: %s\n", tc.Function.Name)
messages = append(messages, schema.ToolMessage(
fmt.Sprintf("error: unknown tool %q", tc.Function.Name), tc.ID,
))
continue
}
result, err := fn(ctx, tc.Function.Arguments)
if err != nil {
result = fmt.Sprintf("error: %v", err)
}
fmt.Printf("\n[Tool: %s] %s\n[Assistant] ", tc.Function.Name, result)
messages = append(messages, schema.ToolMessage(result, tc.ID))
}
// Continue loop — ask model for next response.
}
fmt.Println()
}
}
// streamWithRetry calls cm.Stream with exponential backoff on 429 errors.
func streamWithRetry(ctx context.Context, cm model.ToolCallingChatModel, messages []*schema.Message) (*schema.StreamReader[*schema.Message], error) {
const maxRetries = 5
const baseDelay = time.Second
for attempt := range maxRetries {
stream, err := cm.Stream(ctx, messages)
if err == nil {
return stream, nil
}
if !strings.Contains(err.Error(), "429") || attempt == maxRetries-1 {
return nil, err
}
delay := time.Duration(math.Pow(2, float64(attempt))) * baseDelay
fmt.Fprintf(os.Stderr, "\n[rate limited, retrying in %s]\n[Assistant] ", delay)
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, ctx.Err()
}
}
panic("unreachable")
}
func newChatModel(ctx context.Context) (model.ToolCallingChatModel, error) {
return openai.NewChatModel(ctx, &openai.ChatModelConfig{
APIKey: "",
Model: "qwen/qwen3.6-plus:free",
BaseURL: "https://openrouter.ai/api/v1",
})
}
Chat session started. Type 'exit' or 'quit' to end.
[User] 帮我分析下当前目录的结构
[Assistant]
[rate limited, retrying in 1s]
[Assistant]
[Tool: Glob] go.sum
go.mod
docs/01.md
docs/02.md
tools/glob.go
main.go
[Assistant] 当前目录结构如下:
```
.
├── main.go # 主入口文件
├── go.mod # Go 模块依赖文件
├── go.sum # Go 依赖校验文件(自动生成)
├── docs/ # 文档目录
│ ├── 01.md
│ └── 02.md
└── tools/ # 工具包目录
└── glob.go # glob 工具源码
```
**说明**:
- **Go 项目**:这是一个标准的 Go 项目,包含 `go.mod` 和 `go.sum` 依赖管理文件,以及一个可执行的入口文件 `main.go`
- **docs/**:存放了一些文档文件(01.md, 02.md),可能是使用说明或笔记
- **tools/glob.go**:一个工具包,从文件名来看,似乎是与文件 glob 匹配功能相关的代码
需要我查看某个具体文件的内容吗?
[User]