工具设计规范:一个好的 Tool 接口长什么样
一只用 AI Agent 搭副业产线的程序员上篇我们实现了 Function Calling。但那个实现有个问题——每加一个 Tool你都要写一个新的executeXxxTool函数然后在switch里加一行。当你有 3 个 Tool 时还行。当你有 10 个、20 个的时候这就变成了意大利面。这篇我们设计一个统一的 Tool 接口。参照 Go 标准库的设计哲学——接口要小实现要专注错误要干净。先看反例没有统一接口时有多痛// 每个 Tool 有不同的函数签名funcexecuteFileReader(pathstring)(string,error){...}funcexecuteWebSearch(querystring,maxResultsint)([]Result,error){...}funcexecuteCodeRunner(codestring,languagestring,timeoutint)(string,error){...}// 调用方要写巨大的 switchswitchtoolName{casefile_reader:result,errexecuteFileReader(args.Path)caseweb_search:result,errexecuteWebSearch(args.Query,args.MaxResults)casecode_runner:result,errexecuteCodeRunner(args.Code,args.Language,args.Timeout)// ... 20 个 case}新增一个 Tool 要改 3 个地方定义结构体、写执行函数、在 switch 里加 case。这叫「散弹式修改」——一个变更要打多个文件。统一的 Tool 接口packagetoolimport(contextencoding/jsonfmttime)// Schema 工具的参数定义JSON Schema 格式typeSchemastruct{Typestringjson:typePropertiesmap[string]Propertyjson:propertiesRequired[]stringjson:required}// Property 参数属性typePropertystruct{Typestringjson:typeDescriptionstringjson:descriptionEnum[]stringjson:enum,omitemptyDefault anyjson:default,omitempty}// Result 统一返回结构typeResultstruct{Successbooljson:successDatastringjson:dataErrorstringjson:error,omitempty}// Tool 统一接口// 任何工具只要实现这 3 个方法就能被 Agent 使用typeToolinterface{// Name 工具名称唯一标识Name()string// Description 工具描述AI 用来判断什么时候用这个工具Description()string// Parameters 参数 SchemaJSON Schema 格式Parameters()Schema// Execute 执行工具// ctx超时控制// argsAI 传来的参数JSON 格式Execute(ctx context.Context,argsstring)Result}这 4 个方法对应 Tool 的 4 个核心问题方法回答的问题Name()这个工具叫什么Description()AI 什么时候用这个Parameters()AI 要传什么参数Execute()怎么执行三个真实的 Tool 实现Tool 1文件读取器packagetoolimport(contextfmtos)typeFileReaderstruct{// 安全限制只允许访问的基础路径BasePathstring// 最大读取文件大小防止 AI 要求读取 10GB 日志文件MaxSizeint64}func(f*FileReader)Name()string{returnread_file}func(f*FileReader)Description()string{return读取指定路径的文件内容。返回文件的完整文本内容。}func(f*FileReader)Parameters()Schema{returnSchema{Type:object,Properties:map[string]Property{path:{Type:string,Description:文件路径相对于项目根目录,},},Required:[]string{path},}}func(f*FileReader)Execute(ctx context.Context,argsJSONstring)Result{varargsstruct{Pathstringjson:path}iferr:json.Unmarshal([]byte(argsJSON),args);err!nil{returnResult{Success:false,Error:fmt.Sprintf(参数解析失败: %v,err)}}// 安全检查防止路径穿越fullPath:f.BasePath/args.Path// 实际项目用 filepath.Clean 和路径前缀检查这里简化// 检查文件大小info,err:os.Stat(fullPath)iferr!nil{returnResult{Success:false,Error:fmt.Sprintf(文件不存在: %v,err)}}ifinfo.Size()f.MaxSize{returnResult{Success:false,Error:fmt.Sprintf(文件过大: %d bytes (限制 %d bytes),info.Size(),f.MaxSize)}}// 带超时的读取done:make(chanstruct{})varcontent[]bytevarreadErrerrorgofunc(){content,readErros.ReadFile(fullPath)close(done)}()select{case-ctx.Done():returnResult{Success:false,Error:读取超时}case-done:ifreadErr!nil{returnResult{Success:false,Error:fmt.Sprintf(读取失败: %v,readErr)}}returnResult{Success:true,Data:string(content)}}}Tool 2Web 搜索typeWebSearchstruct{APIKeystringClient*http.Client}func(w*WebSearch)Name()string{returnweb_search}func(w*WebSearch)Description()string{return在互联网上搜索信息。返回前 5 条结果的标题、链接和摘要。}func(w*WebSearch)Parameters()Schema{returnSchema{Type:object,Properties:map[string]Property{query:{Type:string,Description:搜索关键词,},},Required:[]string{query},}}func(w*WebSearch)Execute(ctx context.Context,argsJSONstring)Result{varargsstruct{Querystringjson:query}json.Unmarshal([]byte(argsJSON),args)// 构建请求req,_:http.NewRequestWithContext(ctx,GET,fmt.Sprintf(https://api.search.example.com/v1/search?q%slimit5,args.Query),nil)req.Header.Set(Authorization,Bearer w.APIKey)resp,err:w.Client.Do(req)iferr!nil{ifctx.Err()!nil{returnResult{Success:false,Error:搜索超时}}returnResult{Success:false,Error:fmt.Sprintf(搜索失败: %v,err)}}deferresp.Body.Close()body,_:io.ReadAll(resp.Body)returnResult{Success:true,Data:string(body)}}Tool 3代码执行器typeCodeRunnerstruct{Timeout time.Duration}func(c*CodeRunner)Name()string{returnrun_code}func(c*CodeRunner)Description()string{return在安全沙箱中执行代码片段。支持 Go, Python, Bash。返回执行结果stdoutstderr。}func(c*CodeRunner)Parameters()Schema{returnSchema{Type:object,Properties:map[string]Property{code:{Type:string,Description:要执行的代码内容,},language:{Type:string,Enum:[]string{go,python,bash},Description:编程语言,},},Required:[]string{code,language},}}func(c*CodeRunner)Execute(ctx context.Context,argsJSONstring)Result{varargsstruct{Codestringjson:codeLanguagestringjson:language}json.Unmarshal([]byte(argsJSON),args)// 带超时的代码执行execCtx,cancel:context.WithTimeout(ctx,c.Timeout)defercancel()varcmd*exec.Cmdswitchargs.Language{casego:// 实际项目用临时文件 go run这里简化cmdexec.CommandContext(execCtx,go,run,-)casepython:cmdexec.CommandContext(execCtx,python3,-c,args.Code)casebash:cmdexec.CommandContext(execCtx,bash,-c,args.Code)default:returnResult{Success:false,Error:不支持的语言}}output,err:cmd.CombinedOutput()iferr!nil{returnResult{Success:false,Error:fmt.Sprintf(执行失败: %v\n输出: %s,err,string(output))}}returnResult{Success:true,Data:string(output)}}Tool 注册中心有了统一接口注册就变得很简单typeToolRegistrystruct{toolsmap[string]Tool}funcNewToolRegistry()*ToolRegistry{returnToolRegistry{tools:make(map[string]Tool)}}func(r*ToolRegistry)Register(t Tool){r.tools[t.Name()]t}func(r*ToolRegistry)Execute(ctx context.Context,namestring,argsstring)Result{t,ok:r.tools[name]if!ok{returnResult{Success:false,Error:fmt.Sprintf(未知工具: %s,name)}}returnt.Execute(ctx,args)}func(r*ToolRegistry)ListTools()[]Tool{vartools[]Toolfor_,t:ranger.tools{toolsappend(tools,t)}returntools}// 使用示例funcmain(){reg:NewToolRegistry()reg.Register(FileReader{BasePath:./project,MaxSize:1024*1024})// 1MBreg.Register(WebSearch{APIKey:xxx,Client:http.Client{}})reg.Register(CodeRunner{Timeout:5*time.Second})}新增工具只需要实现Tool接口然后reg.Register(NewTool)。一行代码不用改任何现有代码。这是 Go 接口的威力。错误处理规范三个原则1. 永远不要 panic。Tool 是给 Agent 循环调用的panic 会炸掉整个循环。所有错误都通过Result.Error返回。// 烂func(f*FileReader)Execute(...)Result{content,err:os.ReadFile(path)iferr!nil{panic(err)// 炸了}}// 好func(f*FileReader)Execute(...)Result{content,err:os.ReadFile(path)iferr!nil{returnResult{Success:false,Error:fmt.Sprintf(文件读取失败: %v,err)}}}2. 每次都检查 context。超时是最常见的 Tool 失败原因。每次 I/O 操作前检查ctx.Err()。3. 错误信息要对 AI 友好。Tool 的错误信息会发给 AIAI 需要根据错误信息决定下一步。写清楚「哪里出错了 可能的原因」。// 烂AI 看不懂returnResult{Error:EPERM}// 好AI 能理解可能自己纠正returnResult{Error:权限不足无法读取 /etc/shadow。请确认你有读取该文件的权限或换一个文件。}总结一个好的 Tool 接口特征说明接口统一所有 Tool 实现同一个Tool接口注册即用新 Tool 只需Register不改现有代码Context 超时每个 Tool 都接受context.Context不 panic所有错误通过Result.Error返回错误可读错误信息 AI 能理解能据此调整行为下一个问题是当 Agent 面对复杂任务时光有 Tool 还不够——它需要推理和行动交替进行。这就是 ReAct 模式。关注我别错过。 一只用 AI Agent 搭副业产线的程序员全平台同名虾哥不加班需要定制 AI 工具来聊聊 → lob_ai源码GitHub - lobster-bujiaban