2. 给 Agent 添加工具:从“猜答案”到“先查再答”
Claude Code 这类编码 Agent 真正好用的分水岭,不是模型多聪明,而是它能不能调用工具去读真实文件。第 1 天我们跑通了最小对话循环,但模型只能靠提示词“猜”目录里有什么,问它“列出所有 Go 文件”,它可能一本正经地编出几个不存在的文件名。第 2 天要解决的就是这件事:给 Agent 接上第一个外部工具 Glob,让它先查文件系统再回答。同时我会把多工具调用的 Key 和 API 通道统一交给 TaoToken 管理,避免后面工具越加越多、每个工具一套配置把自己绕晕。这篇适合已经跑通 Day 1 对话、准备让 Agent 真正碰文件系统的同学,全程 Go 代码可直接复制。
3. 为什么 Agent 必须要有 Tool
LLM 本身是个纯文本函数,它访问不了你的本地文件系统、进程和网络。你问它“当前目录有哪些 Go 文件”,它只能基于训练数据里的常见项目结构去猜,结果就是两个老问题:一是幻觉,凭空编出utils.go、config.go这种听起来很合理的文件名;二是不可验证,用户没法确认回答到底基于真实数据还是模型脑补。
Tool 的价值在于把流程改成“先查再答”:用户提问 → LLM 决策要不要调工具 → 调用工具拿到真实数据 → LLM 基于真实数据组织回答。这样回答可验证、可复现、可调试。在 Agent 里,Tool 本质就是 LLM 可以调用的函数,模型看到的是工具名、描述和参数 Schema,真正执行的是你写的 handler。下面这张流程是核心心智模型:
User: "列出所有 Go 文件" │ ▼ LLM 决策: 我需要 glob 工具 │ 调用 glob("*.go") ▼ Glob Tool → ["main.go", "tools.go"] │ 返回真实结果 ▼ LLM 组织回答: "找到了 2 个 Go 文件..."4. TaoToken 前置:把多工具的 Key 和通道统一掉
工具一多,最烦的不是写 handler,而是每个模型调用点都要配一遍 base_url 和 api_key。我的做法是统一走 TaoToken:官网入口 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址固定用 https://taotoken.net/api 。这样 Agent 里所有模型请求都指向同一个通道,换模型、加工具都不用动业务代码。
先拿 Key:进控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 创建一个 Key。建议按项目建独立 Key,方便后面排查是哪个 Agent 在烧 token。拿到后写进环境变量,别硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"如果你用的是 Claude Code 本体或 Anthropic 风格客户端,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,Claude Code 专用说明在 https://taotoken.net/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=ClaudeCodeAnthropic&utm_campaign=rewrite 。想先验证模型通不通,直接开模型对话页 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 发一句测试即可。长期跑编码 Agent、工具调用频繁的,可以看 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,比按次调用更省心。
5. 可复制配置:config.toml 与 settings.json 骨架
在写 Go 代码前,先把配置骨架搭好。很多同学工具调不通,最后发现是配置里 base_url 写错或 Key 没生效。下面两份骨架可以直接抄。
config.toml,放在项目根目录,给 Agent 读取模型和通道信息:
# config.toml [provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model = "claude-sonnet-4-20250514" [agent] system_prompt_file = "prompts/system.md" max_tool_rounds = 8 [tools.glob] enabled = true description = "Find files matching a glob pattern in the current directory"settings.json,给 Claude Code 或兼容客户端用,重点是 env 和权限:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "${TAOTOKEN_API_KEY}" }, "permissions": { "allow": ["Glob", "Read"], "deny": ["Bash(rm:*)"] }, "model": "claude-sonnet-4-20250514" }注意两点:base_url结尾不要多加/v1,TaoToken 的 API 根就是https://taotoken.net/api;api_key_env这种写法是让程序从环境变量读,不要把明文 Key 提交到仓库。工具权限里先只放 Glob 和 Read,等验证稳定再逐步放开,这是踩过坑之后的保守做法。
6. 工具注册配置片段:Glob 工具完整实现
现在写代码。项目结构在 Day 1 基础上新增tools.go:
MiniCode/ ├── main.go # 主程序 ├── tools.go # 工具定义(新增) ├── config.toml # 通道配置 └── go.modtools.go里定义参数结构和 handler。参数用 Go struct + tag,框架会自动生成 JSON Schema 给模型看:
package main import ( "context" "fmt" "path/filepath" "strings" "charm.land/fantasy" ) // GlobParams 定义 glob 工具的参数 type GlobParams struct { Pattern string `json:"pattern" jsonschema:"required,description=The glob pattern to match files in the current directory (e.g., *.go)"` } // NewGlobTool 创建 glob 工具 func NewGlobTool() fantasy.AgentTool { return fantasy.NewAgentTool( "glob", "Find files matching a glob pattern in the current directory. Example: '*.go'.", handleGlob, ) } // handleGlob 是 glob 工具的处理函数 func handleGlob(ctx context.Context, params GlobParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) { // 1. 参数校验,Schema 之外再兜一层 if params.Pattern == "" { return fantasy.NewTextErrorResponse("pattern is required"), nil } // 2. 执行 glob 匹配 matches, err := filepath.Glob(params.Pattern) if err != nil { return fantasy.NewTextErrorResponse(fmt.Sprintf("invalid pattern: %v", err)), nil } // 3. 格式化结果 if len(matches) == 0 { return fantasy.NewTextResponse("No files found matching the pattern"), nil } var result strings.Builder result.WriteString(fmt.Sprintf("Found %d file(s):\n", len(matches))) for _, match := range matches { result.WriteString(fmt.Sprintf("- %s\n", match)) } return fantasy.NewTextResponse(result.String()), nil }这里有个关键区分:工具执行错误(比如 pattern 非法)要返回ToolResponse带错误信息,让模型知道发生了什么并调整;系统级错误(内存不足之类)才返回error。搞混了模型会收到一个它无法理解的失败,直接卡死。
main.go里注册工具并挂到 Agent 上:
var systemPrompt = `You are a helpful coding assistant. You have access to tools that help you interact with the file system. When the user asks about files, use the appropriate tool to find information. Always respond in the same language as the user.` func main() { // 1. 读取 config.toml,创建模型(base_url 指向 TaoToken) // 2. 创建工具列表 tools := []fantasy.AgentTool{ NewGlobTool(), } // 3. 创建带工具的 Agent agent := fantasy.NewAgent( model, fantasy.WithSystemPrompt(systemPrompt), fantasy.WithTools(tools...), ) // 4. 发送消息 messages := []fantasy.Message{ fantasy.NewUserTextMessage(prompt), } result, err := agent.Generate(context.Background(), messages) if err != nil { // 错误处理 } // 5. 打印响应与 token 统计 fmt.Println(result.Text()) fmt.Printf("--- Tokens: %d ---\n", result.Usage().TotalTokens) }工具列表会随消息一起发给模型,模型根据 name 和 description 决定调不调、怎么填参数。所以描述写得越清楚,调用越准,别偷懒写“查找文件”四个字。
7. 验证请求:跑通第一次工具调用
配置和代码就位,跑三条测试。第一条查 Go 文件:
go run . "列出当前目录所有 Go 文件"预期输出:
让我帮你查找当前目录的 Go 文件。 找到了 2 个 Go 文件: - main.go - tools.go --- Tokens: 234 ---第二条查不存在的类型,验证空结果处理:
go run . "有没有 Python 文件?"预期输出:
让我检查一下是否有 Python 文件。 当前目录没有找到 Python 文件(.py)。 --- Tokens: 198 ---第三条复杂查询,看模型会不会组合工具结果做解释:
go run . "这个项目有哪些源代码文件?"预期输出:
让我查看一下项目中的源代码文件。 找到了以下源代码文件: - main.go - 主程序入口 - tools.go - 工具定义 这是一个 Go 项目,目前有 2 个源文件。 --- Tokens: 312 ---三条都过,说明工具调用链路完整:模型决策 → 参数解析 → handler 执行 → 结果回传 → 模型组织回答。整个过程中模型请求都走 TaoToken 统一通道,token 统计也能在控制台对上。
8. 本篇常见错排查
工具调不通,八成是下面几个坑。
报错pattern is required但明明传了参数。检查 struct tag 是不是写成了json:"Pattern"大写开头,JSON 字段名大小写敏感,模型按 Schema 传的是小写pattern,对不上就解析成空。
模型死活不调工具,直接编答案。先看工具 description 是不是太模糊,再确认工具列表真的传进WithTools了。还有一种情况是 system prompt 没告诉模型“有工具可用”,补一句“When the user asks about files, use the appropriate tool”通常就好了。
filepath.Glob匹配不到子目录文件。这是标准库的已知限制,它不支持**递归。要递归匹配得换doublestar库,或者自己写目录遍历。别以为是工具没生效。
请求 401 或连接失败。优先查ANTHROPIC_BASE_URL是不是写成了https://taotoken.net/api/带尾斜杠,或者误加了/v1。Key 没生效就确认环境变量在当前 shell 里echo $TAOTOKEN_API_KEY有值。接入细节对照文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 逐项核。
工具返回了结果但模型回答里没体现。大概率是 handler 返回了error而不是ToolResponse,模型收到的是系统错误,只能忽略。记住:业务错误走 ToolResponse,系统错误才走 error。
9. 下一步:把工具链交给统一通道
Day 2 跑通 Glob 之后,你会发现加工具本身不难,难的是工具一多,模型调用点、Key、通道散落各处。我的建议是趁现在就把所有模型请求收敛到 TaoToken 一个通道:新工具接入时只写 handler,不用再碰 base_url 和 Key。需要新建或轮换 Key 去 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,接入配置参考 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,Claude Code 场景看 https://taotoken.net/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=ClaudeCodeAnthropic&utm_campaign=rewrite 。想先手动验证模型对工具描述的理解,开模型对话 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 试几轮;准备长期跑编码 Agent、工具调用密集的,直接上 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。明天 Day 3 我们加 Read 工具,让 Agent 能读文件内容,到时候你会庆幸 Key 已经统一好了。