Eino 工具开发避坑指南:小白也能看懂的概念拆解 + 实操教程

305 阅读19分钟

一文吃透 Eino 工具的核心原理!从 BaseTool 接口、ToolInfo 说明书到 InferTool 实战,手把手教你写可运行的 PDF 解析、简历评分工具,附带通用开发模板直接套用~

这篇文章能让你:

  1. 搞懂Eino工具的核心概念
  2. 掌握创建Eino工具的4种方式
  3. 拆解项目中工具的实现逻辑
  4. 从零写出可运行的Eino工具

1. 核心概念通俗讲(先懂原理再看代码)

Eino的「工具」本质是让AI Agent能调用的「外部功能模块」,比如PDF解析、搜索、计算等。先搞懂这几个核心概念,后面看代码就不懵了:

1.1 三个核心接口(工具的「身份证」)

Eino用接口定义工具的规范,就像「必须满足这几个条件才能当工具」

  • BaseTool:所有工具的「基础要求」,必须能返回自己的「说明书」(ToolInfo)
  • InvokableTool:「同步工具」,调用后等待结果返回(比如PDF解析,调用后等文本输出)
  • StreamableTool:「流式工具」,调用后持续返回结果(比如实时聊天、视频流,项目中用得少)
  • 后两者二选一

代码对应:

// 基础工具接口(必须实现)
type BaseTool interface {
    Info(ctx context.Context) (*schema.ToolInfo, error) // 返回工具说明书
}

// 同步工具接口(项目中最常用)
type InvokableTool interface {
    BaseTool // 继承基础要求
    // 同步执行工具,入参是JSON字符串,返回结果字符串
    InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error)
}

1.2 ToolInfo:工具的「说明书」

AI Agent要知道「这个工具能干嘛、要传什么参数」,全靠ToolInfo。比如PDF解析工具的说明书:

  • 名称:pdf_to_text(AI调用时用的标识)
  • 描述:将本地PDF转为纯文本,支持可复制的PDF(告诉AI什么时候用)
  • 参数:需要传入PDF的绝对路径(告诉AI要传什么)

1.3 ToolsNode:工具的「组合器」

一个Agent可能需要多个工具(比如简历分析Agent需要PDF解析+简历评分工具),ToolsNode就是把多个工具打包成一个「工具包」,让Agent能统一调用,不用单独管理每个工具。

1.4 Option:工具的「动态配置」

比如调用工具时想设置超时时间、重试次数,就用Option传递(类似给工具传「额外参数」),项目中偶尔用到,后面讲示例。

2. ToolInfo的两种表示方式(工具的「说明书」)

ToolInfo的核心是「告诉AI参数规则」,Eino提供两种方式,项目中用的是第二种(结构体+Tag),重点掌握:

2.1 方式1:手动写参数规则(适合简单场景)

直接用map定义参数名、类型、是否必填,比如「添加用户」工具:

// 手动构建参数规则
params := map[string]*schema.ParameterInfo{
    "name": &schema.ParameterInfo{
        Type:     schema.String, // 参数类型:字符串
        Required: true,          // 必须传
        Desc:     "用户姓名",     // 描述
    },
    "age": &schema.ParameterInfo{
        Type: schema.Integer, // 参数类型:整数
        Desc: "用户年龄",
    },
}

// 构建ToolInfo
toolInfo := &schema.ToolInfo{
    Name:        "add_user",       // 工具名称(AI调用时用)
    Desc:        "添加新用户到系统", // 工具功能描述
    ParamsOneOf: schema.NewParamsOneOfByParams(params), // 绑定参数规则
}

2.2 方式2:结构体+Tag(项目中常用,推荐)

用Golang结构体定义参数,通过Tag标注规则(不用手动写map),Eino会自动转换成ToolInfo,小白只需记住Tag的含义:

  • json:"参数名":AI传递参数时的key
  • jsonschema:"required":该参数必须传
  • jsonschema:"description=xxx":参数描述(告诉AI)
  • jsonschema:"enum=xxx,enum=yyy":参数只能选枚举值

示例(项目中PDF解析工具的参数定义):

// PDF解析工具的入参结构体
type PDFToTextRequest struct {
    // 参数名:file_path,必须传,描述是PDF的绝对路径
    FilePath string `json:"file_path" jsonschema:"required,description=本地PDF文件的绝对路径"`
    // 参数名:split_page,可选,描述是是否按页分割,默认false
    SplitPage bool `json:"split_page" jsonschema:"description=是否按页面分割文本,默认false"`
}

然后用Eino提供的工具函数,自动生成ToolInfo(不用自己写map),后面创建工具时会用到。

3. 创建Eino工具的4种方式(从简单到复杂)

重点掌握「方式2」(InferTool),因为项目中所有工具都用的这个!其他方式了解即可。

3.1 方式1:直接实现接口(最基础,手动处理序列化)

适合想深入理解的场景,步骤:

  1. 定义工具结构体(空结构体就行,因为接口只要求方法)
  2. 实现Info()方法(返回ToolInfo)
  3. 实现InvokableRun()方法(工具核心逻辑,处理入参和返回结果)

示例:简单的「加法工具」

package main

import (
    "context"
    "encoding/json"
    "log"
    "github.com/cloudwego/eino/components/tool"
    "github.com/cloudwego/eino/components/tool/utils"
    "github.com/cloudwego/eino/schema"
)

// 1. 定义工具结构体(空的,只是为了实现接口)
type AddTool struct{}

// 2. 实现Info()方法:返回工具说明书
func (t *AddTool) Info(_ context.Context) (*schema.ToolInfo, error) {
    // 定义参数规则(方式1:手动写map)
    params := map[string]*schema.ParameterInfo{
        "a": &schema.ParameterInfo{Type: schema.Integer, Required: true, Desc: "第一个数字"},
        "b": &schema.ParameterInfo{Type: schema.Integer, Required: true, Desc: "第二个数字"},
    }
    return &schema.ToolInfo{
        Name:        "add",
        Desc:        "计算两个整数的和",
        ParamsOneOf: schema.NewParamsOneOfByParams(params),
    }, nil
}

// 3. 实现InvokableRun()方法:工具核心逻辑
func (t *AddTool) InvokableRun(_ context.Context, argsJSON string, _ ...tool.Option) (string, error) {
    // 第一步:解析入参(AI传的JSON字符串转成结构体)
    type Input struct {
        A int `json:"a"`
        B int `json:"b"`
    }
    var input Input
    err := json.Unmarshal([]byte(argsJSON), &input)
    if err != nil {
        return "", err // 参数解析失败返回错误
    }

    // 第二步:核心业务逻辑(计算和)
    sum := input.A + input.B

    // 第三步:返回结果(结构体转JSON字符串)
    type Output struct {
        Sum int `json:"sum"`
        Msg string `json:"msg"`
    }
    output := Output{Sum: sum, Msg: "计算成功"}
    outputJSON, _ := json.Marshal(output)
    return string(outputJSON), nil
}

// 测试工具
func main() {
    ctx := context.Background()
    addTool := &AddTool{}
    // 调用工具:传入{"a":1,"b":2}
    result, err := addTool.InvokableRun(ctx, `{"a":1,"b":2}`)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("结果:", result) // 输出:{"sum":3,"msg":"计算成功"}
}

解析:这种方式需要手动处理「JSON转结构体」和「结构体转JSON」,项目中不用,因为有更简单的方式。

3.2 方式2:用InferTool(项目常用,自动处理序列化)

Eino提供utils.InferTool函数,能自动帮你做3件事:

  1. 从入参结构体的Tag生成ToolInfo(不用手动写参数规则)
  2. 自动解析入参JSON(不用写json.Unmarshal)
  3. 自动序列化返回结果(不用写json.Marshal)

步骤:

  1. 定义入参结构体(带jsonschema Tag)
  2. 定义出参结构体(返回给AI的结果)
  3. 写工具核心逻辑函数(入参是结构体,出参是结构体+error)
  4. 用InferTool包装成Eino工具

示例:用InferTool实现「加法工具」(对比方式1,简洁太多)

package main

import (
    "context"
    "log"
    "github.com/cloudwego/eino/components/tool"
    "github.com/cloudwego/eino/components/tool/utils"
)

// 1. 入参结构体(带Tag,告诉Eino参数规则)
type AddInput struct {
    A int `json:"a" jsonschema:"required,description=第一个数字"`
    B int `json:"b" jsonschema:"required,description=第二个数字"`
}

// 2. 出参结构体(工具返回的结果)
type AddOutput struct {
    Sum int    `json:"sum" description="两个数的和"`
    Msg string `json:"msg" description="执行状态"`
}

// 3. 工具核心逻辑函数(普通Golang函数)
func addFunc(ctx context.Context, input *AddInput) (*AddOutput, error) {
    sum := input.A + input.B
    return &AddOutput{
        Sum: sum,
        Msg: "计算成功",
    }, nil
}

// 4. 用InferTool包装成Eino工具
func CreateAddTool() tool.InvokableTool {
    // 函数参数:工具名称、工具描述、核心逻辑函数
    addTool, err := utils.InferTool("add", "计算两个整数的和", addFunc)
    if err != nil {
        log.Fatalf("创建工具失败:%v", err)
    }
    return addTool
}

// 测试工具
func main() {
    ctx := context.Background()
    addTool := CreateAddTool()
    // 调用工具:传入JSON字符串
    result, err := addTool.InvokableRun(ctx, `{"a":3,"b":5}`)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("结果:", result) // 输出:{"sum":8,"msg":"计算成功"}
}

解析:这就是项目中工具的实现方式!比如pdfParserTool.go中的CreatePDFToTextTool,完全遵循这个逻辑,后面会拆解。

3.3 方式3:带Option的工具(动态配置)

如果工具需要「动态参数」(比如超时时间、重试次数),用InferOptionableTool,步骤和方式2类似,多了Option定义:

示例:带超时配置的加法工具

// 1. 定义Option结构体(动态配置项)
type AddToolOptions struct {
    Timeout int `json:"timeout"` // 超时时间(秒)
}

// 2. 定义Option函数(给外部设置配置用)
func WithTimeout(timeout int) tool.Option {
    return tool.WrapImplSpecificOptFn(func(o *AddToolOptions) {
        o.Timeout = timeout
    })
}

// 3. 核心逻辑函数(多了opts参数)
func addWithOptionFunc(ctx context.Context, input *AddInput, opts ...tool.Option) (*AddOutput, error) {
    // 默认配置
    defaultOpts := &AddToolOptions{Timeout: 5}
    // 合并外部传入的配置
    tool.GetImplSpecificOptions(defaultOpts, opts...)
    
    // 这里可以用defaultOpts.Timeout做超时处理
    sum := input.A + input.B
    return &AddOutput{Sum: sum, Msg: fmt.Sprintf("超时时间:%d秒", defaultOpts.Timeout)}, nil
}

// 4. 用InferOptionableTool包装
func CreateAddWithOptionTool() tool.InvokableTool {
    tool, err := utils.InferOptionableTool("add_with_option", "带超时配置的加法工具", addWithOptionFunc)
    if err != nil {
        log.Fatal(err)
    }
    return tool
}

// 测试:传入超时时间3秒
func main() {
    ctx := context.Background()
    addTool := CreateAddWithOptionTool()
    result, _ := addTool.InvokableRun(ctx, `{"a":2,"b":4}`, WithTimeout(3))
    log.Println(result) // 输出:{"sum":6,"msg":"超时时间:3秒"}
}

3.4 方式4:使用Eino-ext现成工具(开箱即用)

Eino有现成的工具库(比如搜索、维基百科、HTTP请求),不用自己写,直接导入使用,示例:

import (
    "github.com/cloudwego/eino-ext/components/tool/duckduckgosearch"
)

// 创建 duckduckgo 搜索工具
func CreateSearchTool() tool.InvokableTool {
    searchTool, err := duckduckgosearch.NewTool()
    if err != nil {
        log.Fatal(err)
    }
    return searchTool
}

4. ToolsNode:工具的「组合器」(项目中怎么用)

一个Agent可能需要多个工具,比如「简历分析Agent」需要:

  • PDF解析工具(提取简历文本)
  • 简历评分工具(给简历打分)

ToolsNode就是把这两个工具打包,让Agent统一调用。

4.1 创建ToolsNode的步骤

package main

import (
    "context"
    "log"
    "github.com/cloudwego/eino/compose"
    "github.com/cloudwego/eino/components/tool"
)

// 假设已经创建了两个工具
func CreatePDFTool() tool.InvokableTool { /* 省略实现,参考3.2 */ }
func CreateScoreTool() tool.InvokableTool { /* 省略实现 */ }

func main() {
    ctx := context.Background()
    
    // 1. 创建单个工具
    pdfTool := CreatePDFTool()
    scoreTool := CreateScoreTool()
    
    // 2. 用ToolsNode组合工具
    toolsNode, err := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{
        Tools: []tool.BaseTool{pdfTool, scoreTool}, // 放入工具包
    })
    if err != nil {
        log.Fatal(err)
    }
    
    // 3. 给Agent配置这个ToolsNode(项目中核心用法)
    // 后面Agent章节会讲,这里知道ToolsNode是给Agent用的就行
}

4.2 ToolsNode在项目中的应用

项目中agent/resumAgent.goNewResumAnalysisAgent函数,就是给简历分析Agent配置ToolsNode:

// 简历分析Agent的创建
func NewResumAnalysisAgent() adk.Agent {
    ctx := context.Background()

    a, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        Name:        "ResumAnalysisAgent",
        Description: "解析简历并分析",
        Instruction: "你的任务是分析简历...",
        Model: chat.CreatOpenAiChatModel(ctx),
        // 配置ToolsNode:给Agent绑定PDF解析工具
        ToolsConfig: adk.ToolsConfig{
            ToolsNodeConfig: compose.ToolsNodeConfig{
                Tools: []tool.BaseTool{tool2.CreatePDFToTextTool()}, // 这里就是ToolsNode
            },
        },
    })
    return a
}

解析:项目中没有单独创建ToolsNode变量,而是直接在Agent配置中传入ToolsNodeConfig,本质是一样的——给Agent绑定工具包。

5. 拆解项目中的工具实现(理论对接实战)

看看tool文件夹下的文件是怎么对应前面讲的理论的,以核心文件为例:

5.1 项目工具结构回顾

tool/
├── pdfParserTool.go    # PDF解析工具(核心)
├── gen_question.go     # 面试问题生成工具
├── gen_AnswerEvalTool.go # 回答评估工具
└── ask_for_clarification.go # 交互工具

5.2 拆解pdfParserTool.go(PDF解析工具)

这个文件完全遵循「方式2:InferTool」,逐行解析:

package tool2

import (
    "context"
    "log"
    "github.com/cloudwego/eino/components/tool"
    "github.com/cloudwego/eino/components/tool/utils"
)

// 1. 入参结构体(带Tag,工具的参数规则)
type PDFToTextRequest struct {
    FilePath string `json:"file_path" jsonschema:"required,description=本地PDF文件的绝对路径"`
    SplitPage bool `json:"split_page" jsonschema:"description=是否按页面分割文本,默认false"`
}

// 2. 出参结构体(工具返回结果)
type PDFToTextResult struct {
    Content string `json:"content" description="PDF转换后的文本"`
    Pages   []string `json:"pages" description="按页分割的文本(SplitPage为true时返回)"`
    Msg     string `json:"msg" description="执行状态"`
}

// 3. 核心逻辑函数(PDF转文本的实际操作)
func ConvertPDFToText(ctx context.Context, req *PDFToTextRequest) (*PDFToTextResult, error) {
    // 这里是真正的PDF解析逻辑:打开文件→读取内容→转换文本
    // 省略具体实现(和Eino工具规范无关,是业务逻辑)
    var content string
    var pages []string
    
    if req.SplitPage {
        return &PDFToTextResult{
            Content: "",
            Pages:   pages,
            Msg:     "转换成功(按页分割)",
        }, nil
    }
    return &PDFToTextResult{
        Content: content,
        Pages:   nil,
        Msg:     "转换成功",
    }, nil
}

// 4. 用InferTool包装成Eino工具(对外提供)
func CreatePDFToTextTool() tool.InvokableTool {
    // 工具名称:pdf_to_text,描述:PDF转纯文本,核心函数:ConvertPDFToText
    pdfTool, err := utils.InferTool(
        "pdf_to_text", 
        "将本地PDF文件转换为纯文本,仅支持文本型PDF,不支持扫描件、加密PDF",
        ConvertPDFToText,
    )
    if err != nil {
        log.Fatalf("创建PDF工具失败:%v", err)
    }
    fmt.Println("✅ PDF工具初始化完成")
    return pdfTool
}

对应理论

  • PDFToTextRequest:入参结构体+Tag → 对应「ToolInfo的方式2」
  • ConvertPDFToText:核心逻辑函数 → 对应「方式2的核心函数」
  • CreatePDFToTextTool:用InferTool包装 → 对应「方式2创建工具」
  • 最终返回tool.InvokableTool → 对应「同步工具接口」

5.3 拆解gen_question.go(问题生成工具)

和PDF工具逻辑完全一致,只是业务不同:

  1. 入参结构体:QuestionGenRequest(接收简历文本、岗位信息)
  2. 核心函数:GenerateQuestion(根据简历生成面试问题)
  3. 包装函数:CreateQuestionGenTool(用InferTool包装)

5.4 工具在Agent中的调用流程(项目核心)

以「简历分析Agent」为例,工具调用的完整流程:

  1. 用户传入PDF简历路径
  2. Agent的Instruction告诉AI:「先调用pdf_to_text工具提取文本,再分析」
  3. AI生成工具调用指令(比如{"name":"pdf_to_text","arguments":{"file_path":"/Users/xxx/简历.pdf"}}
  4. Eino框架解析这个指令,调用ToolsNode中的PDF工具
  5. PDF工具返回转换后的文本
  6. Agent接收文本,继续执行分析逻辑

6. 从零实现自己的工具(实操演练)

跟着做,你会创建一个「简历评分工具」,并集成到项目中:

6.1 步骤1:创建工具文件tool/gen_resume_score.go

package tool2

import (
    "context"
    "log"
    "github.com/cloudwego/eino/components/tool"
    "github.com/cloudwego/eino/components/tool/utils"
)

// 1. 入参结构体:接收简历文本
type ResumeScoreRequest struct {
    ResumeText string `json:"resume_text" jsonschema:"required,description=简历转换后的纯文本"`
    Position   string `json:"position" jsonschema:"required,description=目标岗位(如:后端开发)"`
}

// 2. 出参结构体:返回评分和建议
type ResumeScoreResponse struct {
    Score    int    `json:"score" description="简历评分(0-100分)"`
    Strength string `json:"strength" description="简历优势"`
    Suggest  string `json:"suggest" description="改进建议"`
    Msg      string `json:"msg" description="执行状态"`
}

// 3. 核心逻辑:给简历评分
func ScoreResume(ctx context.Context, req *ResumeScoreRequest) (*ResumeScoreResponse, error) {
    // 简单的评分逻辑(实际项目中可以用AI或更复杂的规则)
    resumeLen := len(req.ResumeText)
    var score int
    
    // 规则:文本长度≥500字得80+,≥300字得60+,否则低于60
    if resumeLen >= 500 {
        score = 85
    } else if resumeLen >= 300 {
        score = 65
    } else {
        score = 50
    }
    
    // 生成优势和建议
    strength := "文本完整度达标"
    suggest := "建议补充量化成果(如:负责XX项目,提升XX效率)"
    
    return &ResumeScoreResponse{
        Score:    score,
        Strength: strength,
        Suggest:  suggest,
        Msg:      "评分成功",
    }, nil
}

// 4. 包装成Eino工具
func CreateResumeScoreTool() tool.InvokableTool {
    scoreTool, err := utils.InferTool(
        "resume_score", // 工具名称
        "根据简历文本和目标岗位,给简历打分并提供改进建议", // 工具描述
        ScoreResume, // 核心逻辑函数
    )
    if err != nil {
        log.Fatalf("创建简历评分工具失败:%v", err)
    }
    log.Println("✅ 简历评分工具初始化完成")
    return scoreTool
}

6.2 步骤2:将工具添加到Agent中(修改agent/resum``e``Agent.go

给简历分析Agent添加「评分工具」:

func NewResumAnalysisAgent() adk.Agent {
    ctx := context.Background()

    a, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        Name:        "ResumAnalysisAgent",
        Description: "一个可以解析简历pdf分析简历的智能体",
        Instruction: `你是一名资深的简历分析专家,负责对用户的简历进行分析:
1. 先使用pdf_to_text工具提取文本(如果用户提供PDF路径);
2. 再使用resume_score工具给简历打分;
3. 最终输出分析结果、评分和改进建议。`,
        Model: chat.CreatOpenAiChatModel(ctx),
        ToolsConfig: adk.ToolsConfig{
            ToolsNodeConfig: compose.ToolsNodeConfig{
                // 新增评分工具
                Tools: []tool.BaseTool{tool2.CreatePDFToTextTool(), tool2.CreateResumeScoreTool()},
            },
        },
        MaxIterations: 10,
    })
    if err != nil {
        log.Fatal(fmt.Errorf("failed to create chatmodel: %w", err))
    }

    return a
}

6.3 步骤3:运行测试

  1. 确保项目依赖已安装:go mod tidy
  2. 运行main.go,传入PDF路径,Agent会自动调用「PDF解析工具」和「评分工具」,输出结果。

7. 常见问题与排查(小白避坑)

7.1 依赖安装失败

报错:cannot find module providing package github.com/cloudwego/eino/xxx

解决:

go mod tidy # 自动整理依赖
go get github.com/cloudwego/eino@latest # 安装最新版Eino

7.2 工具调用时参数解析失败

报错:json: cannot unmarshal object into Go value of type xxx

原因:AI传入的参数JSON和工具的入参结构体不匹配

排查:

  • 检查入参结构体的json:"参数名"是否和AI调用的key一致
  • 确保必填参数都传了(没传会报错)

7.3 工具不被Agent调用

原因:Agent的Instruction没说清楚「什么时候用工具」

解决:在Agent的Instruction中明确工具调用逻辑,比如:

Instruction: `当用户提供PDF路径时,必须先调用pdf_to_text工具提取文本;
提取完成后,调用resume_score工具打分;
最后根据两个工具的结果生成分析报告。`,

7.4 序列化失败

报错:unsupported type for JSON marshaling

原因:出参结构体中有不能转JSON的类型(比如函数、指针)

解决:出参结构体只保留基础类型(string、int、bool、slice、map)

Eino 工具开发通用模板

这份模板是基于 Eino 框架「utils.InferTool 方式」(项目中最常用、最简洁)打造的通用模板,所有工具都能套用这个结构,只需修改「入参、出参、核心逻辑」3个部分,复制粘贴即可使用。

模板文件结构

建议新建文件命名规范:tool/[工具功能]_tool.go(如 pdf_parser_tool.goresume_score_tool.go

// 包名:根据项目实际的 tool 目录调整(比如你的项目用 tool2,就改成 package tool2)
package tool

import (
        "context"
        "fmt"
        "log"

        "github.com/cloudwego/eino/components/tool"
        "github.com/cloudwego/eino/components/tool/utils"
)

// ======================================
// 第一步:定义工具入参(必须修改!)
// 说明:
// 1. 结构体字段对应工具需要的参数
// 2. json:"参数名":AI 调用工具时传递参数的 key(必须小写,符合 JSON 规范)
// 3. jsonschema 标签:告诉 AI 参数规则(required 表示必填,description 是参数说明)
// ======================================
type [工具名称]Input struct {
        // 示例字段1:必填参数(根据实际需求修改字段名、类型、描述)
        Param1 string `json:"param1" jsonschema:"required,description=参数1的功能说明(比如:PDF文件绝对路径)"`
        // 示例字段2:可选参数(去掉 required 即可)
        Param2 int `json:"param2" jsonschema:"description=参数2的功能说明(比如:超时时间,默认3秒)"`
        // 示例字段3:枚举参数(限制只能选指定值)
        Param3 string `json:"param3" jsonschema:"description=参数3的功能说明(比如:输出格式),enum=json,enum=text"`
}

// ======================================
// 第二步:定义工具出参(必须修改!)
// 说明:
// 1. 结构体字段对应工具返回的结果
// 2. description 标签:说明字段含义(方便 AI 理解结果)
// 3. 只保留基础类型(string、int、bool、slice、map),避免序列化失败
// ======================================
type [工具名称]Output struct {
        // 示例字段1:核心结果(比如 PDF 转换后的文本、评分结果)
        Result string `json:"result" description="工具执行的核心结果"`
        // 示例字段2:状态信息(比如执行成功/失败说明)
        Msg string `json:"msg" description="执行状态说明"`
        // 示例字段3:附加信息(比如耗时、额外建议)
        Extra map[string]interface{} `json:"extra" description="附加信息(可选)"`
}

// ======================================
// 第三步:工具核心逻辑(必须修改!)
// 函数命名规范:[工具名称]Func(如 PDFToTextFunc、ResumeScoreFunc)
// 入参:context + 入参结构体指针
// 出参:出参结构体指针 + error(执行失败返回错误,成功返回 nil)
// ======================================
func [工具名称]Func(ctx context.Context, input *[工具名称]Input) (*[工具名称]Output, error) {
        // --------------------------
        // 可选:设置默认值(处理可选参数)
        // --------------------------
        if input.Param2 == 0 { // 如果用户没传 Param2,设置默认值 3
                input.Param2 = 3
        }
        if input.Param3 == "" { // 如果用户没传 Param3,设置默认值 json
                input.Param3 = "json"
        }

        // --------------------------
        // 核心业务逻辑(重点修改这里)
        // 示例:模拟一个简单的工具逻辑(替换成你的实际功能)
        // --------------------------
        log.Printf("工具开始执行:param1=%s, param2=%d, param3=%s", input.Param1, input.Param2, input.Param3)

        // 模拟业务处理(比如 PDF 解析、搜索、计算等)
        coreResult := fmt.Sprintf("处理成功!输入参数:%s(超时时间:%d秒,输出格式:%s)", input.Param1, input.Param2, input.Param3)

        // 构建附加信息(可选)
        extraInfo := map[string]interface{}{
                "cost_time": "200ms", // 模拟耗时
                "tip":       "这是附加提示信息",
        }

        // --------------------------
        // 返回结果(固定格式,不用改)
        // --------------------------
        return &[工具名称]Output{
                Result: coreResult,
                Msg:    "工具执行成功",
                Extra:  extraInfo,
        }, nil
}

// ======================================
// 第四步:包装成 Eino 工具(无需修改!)
// 函数命名规范:Create[工具名称]Tool(如 CreatePDFToTextTool)
// 作用:将核心逻辑函数包装成 Eino 可识别的 InvokableTool
// ======================================
func Create[工具名称]Tool() tool.InvokableTool {
        // 调用 Eino 工具函数,自动处理序列化、ToolInfo 生成
        toolInstance, err := utils.InferTool(
                "[工具名称小写下划线]", // 工具唯一标识(AI 调用时用,比如 "pdf_to_text")
                "[工具功能描述]",       // 告诉 AI 这个工具能干嘛(比如 "将本地 PDF 文件转换为纯文本")
                [工具名称]Func,        // 绑定第三步的核心逻辑函数
        )
        if err != nil {
                // 工具创建失败直接退出(避免后续报错)
                log.Fatalf("[%s] 工具创建失败:%v", "[工具名称]", err)
        }

        // 日志提示(可选,方便调试)
        log.Printf("✅ [%s] 工具初始化完成(AI 可调用标识:%s)", "[工具名称]", "[工具名称小写下划线]")
        return toolInstance
}

// ======================================
// 第五步:测试工具(可选,用于单独调试)
// 说明:单独运行这个文件,测试工具是否正常工作
// ======================================
func Test[工具名称]Tool() {
        // 1. 创建上下文
        ctx := context.Background()

        // 2. 创建工具实例
        testTool := Create[工具名称]Tool()

        // 3. 构造测试入参(JSON 格式,对应第一步的入参结构体)
        testInputJSON := `{
                "param1": "测试参数1",
                "param2": 5,
                "param3": "text"
        }`

        // 4. 调用工具
        result, err := testTool.InvokableRun(ctx, testInputJSON)
        if err != nil {
                log.Fatalf("工具测试失败:%v", err)
        }

        // 5. 输出结果
        log.Printf("\n工具测试成功!返回结果:\n%s", result)
}

// 测试入口(单独运行时执行)
// 命令:go run tool/[工具文件].go
func main() {
        Test[工具名称]Tool()
}

模板使用步骤

第一步:替换模板中的「占位符」

所有 [工具名称] 都要替换成你的实际工具名称(比如 PDFToTextResumeScore),示例:

  • 入参结构体:PDFToTextInput(原 [工具名称]Input
  • 出参结构体:PDFToTextOutput(原 [工具名称]Output
  • 核心函数:PDFToTextFunc(原 [工具名称]Func
  • 包装函数:CreatePDFToTextTool(原 Create[工具名称]Tool
  • 工具标识:"pdf_to_text"(原 "[工具名称小写下划线]"
  • 工具描述:"将本地 PDF 文件转换为纯文本,支持可复制 PDF,不支持扫描件"(原 "[工具功能描述]"

第二步:修改「入参/出参结构体」

根据你的工具需求,删除/新增字段,示例:

  • 如果是「简历评分工具」,入参可能是 ResumeText(简历文本)、Position(目标岗位)
  • 出参可能是 Score(评分)、Strength(优势)、Suggest(建议)

第三步:编写「核心业务逻辑」

[工具名称]Func 中的「模拟业务逻辑」替换成你的实际功能,比如:

  • PDF 解析:调用 PDF 处理库(如 github.com/unidoc/unipdf/v3)提取文本
  • 搜索工具:调用 HTTP 接口请求搜索服务
  • 计算工具:实现具体的计算逻辑(如简历评分规则)

第四步:测试工具(单独调试)

  1. 在工具文件末尾的 main 函数中调用 Test[工具名称]Tool()
  2. 运行命令:go run tool/[工具文件].go(如 go run tool/pdf_parser_tool.go
  3. 查看日志输出,确认工具是否正常返回结果

第五步:集成到 Agent 中

  1. 在 Agent 的创建函数中,导入你的工具
  2. ToolsConfig.ToolsNodeConfig.Tools 中添加工具实例,示例:
// 简历分析 Agent 中添加 PDF 解析工具
import "your-project/tool"

func NewResumAnalysisAgent() adk.Agent {
    ctx := context.Background()

    a, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        // ... 其他配置(名称、描述、模型等)
        ToolsConfig: adk.ToolsConfig{
            ToolsNodeConfig: compose.ToolsNodeConfig{
                // 加入你的工具
                Tools: []tool.BaseTool{tool.CreatePDFToTextTool()},
            },
        },
    })
    return a
}

注意事项

  1. 依赖安装:如果使用第三方库(如 PDF 解析库),需要先执行 go get 库地址(如 go get github.com/unidoc/unipdf/v3
  2. 参数命名:入参结构体的 json:"参数名" 必须小写,符合 JSON 规范(AI 调用时会用小写 key)
  3. 序列化 兼容:出参结构体不要用复杂类型(如函数、指针),只保留基础类型(string、int、bool、slice、map)
  4. 错误处理:核心逻辑中要捕获可能的错误(如文件不存在、解析失败),并返回具体的错误信息(方便调试)
  5. 工具标识唯一:每个工具的 工具名称小写下划线 必须唯一(比如不能有两个 pdf_to_text 工具)

和我们一起拥抱AI应用的开发

AI智能体,AI编程感兴趣的朋友可以在掘金私信我,或者直接加我微信:wangzhongyang1993。

后面我还会更新更多跟AI相关的文章,欢迎关注我一起学习