一文吃透 Eino 工具的核心原理!从 BaseTool 接口、ToolInfo 说明书到 InferTool 实战,手把手教你写可运行的 PDF 解析、简历评分工具,附带通用开发模板直接套用~
这篇文章能让你:
- 搞懂Eino工具的核心概念
- 掌握创建Eino工具的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传递参数时的keyjsonschema:"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:直接实现接口(最基础,手动处理序列化)
适合想深入理解的场景,步骤:
- 定义工具结构体(空结构体就行,因为接口只要求方法)
- 实现Info()方法(返回ToolInfo)
- 实现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件事:
- 从入参结构体的Tag生成ToolInfo(不用手动写参数规则)
- 自动解析入参JSON(不用写json.Unmarshal)
- 自动序列化返回结果(不用写json.Marshal)
步骤:
- 定义入参结构体(带jsonschema Tag)
- 定义出参结构体(返回给AI的结果)
- 写工具核心逻辑函数(入参是结构体,出参是结构体+error)
- 用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.go的NewResumAnalysisAgent函数,就是给简历分析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工具逻辑完全一致,只是业务不同:
- 入参结构体:
QuestionGenRequest(接收简历文本、岗位信息) - 核心函数:
GenerateQuestion(根据简历生成面试问题) - 包装函数:
CreateQuestionGenTool(用InferTool包装)
5.4 工具在Agent中的调用流程(项目核心)
以「简历分析Agent」为例,工具调用的完整流程:
- 用户传入PDF简历路径
- Agent的Instruction告诉AI:「先调用pdf_to_text工具提取文本,再分析」
- AI生成工具调用指令(比如
{"name":"pdf_to_text","arguments":{"file_path":"/Users/xxx/简历.pdf"}}) - Eino框架解析这个指令,调用ToolsNode中的PDF工具
- PDF工具返回转换后的文本
- 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:运行测试
- 确保项目依赖已安装:
go mod tidy - 运行
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.go、resume_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()
}
模板使用步骤
第一步:替换模板中的「占位符」
所有 [工具名称] 都要替换成你的实际工具名称(比如 PDFToText、ResumeScore),示例:
- 入参结构体:
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 接口请求搜索服务
- 计算工具:实现具体的计算逻辑(如简历评分规则)
第四步:测试工具(单独调试)
- 在工具文件末尾的
main函数中调用Test[工具名称]Tool() - 运行命令:
go run tool/[工具文件].go(如go run tool/pdf_parser_tool.go) - 查看日志输出,确认工具是否正常返回结果
第五步:集成到 Agent 中
- 在 Agent 的创建函数中,导入你的工具
- 在
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
}
注意事项
- 依赖安装:如果使用第三方库(如 PDF 解析库),需要先执行
go get 库地址(如go get github.com/unidoc/unipdf/v3) - 参数命名:入参结构体的
json:"参数名"必须小写,符合 JSON 规范(AI 调用时会用小写 key) - 序列化 兼容:出参结构体不要用复杂类型(如函数、指针),只保留基础类型(string、int、bool、slice、map)
- 错误处理:核心逻辑中要捕获可能的错误(如文件不存在、解析失败),并返回具体的错误信息(方便调试)
- 工具标识唯一:每个工具的
工具名称小写下划线必须唯一(比如不能有两个pdf_to_text工具)
和我们一起拥抱AI应用的开发
对AI智能体,AI编程感兴趣的朋友可以在掘金私信我,或者直接加我微信:wangzhongyang1993。
后面我还会更新更多跟AI相关的文章,欢迎关注我一起学习。