Agent Mentor Learn
Agent 工具调用基础:让 Agent 真正动手做事 · 第 6 / 6 节

第 6 课:实战:给 Agent 接上三个工具

学习目标:

  • 写出一个完整的工具执行循环,让 Agent 真正跑起来
  • 用一张表同时注册工具的接口定义和实现,避免两边对不上
  • 给循环装上安全阀,并能从日志里判断"工具接错了"

前置要求:读完第 1-5 课,能读懂基本的 JavaScript / Node.js | 上一课 第 5 课 <<

先看效果:一次完整的运行

这是本课最后要跑出来的东西。终端里输入一句话,Agent 自己决定调用哪些工具、调几次:

$ node agent.js "我们项目里是不是用了 lodash?查一下它在 GitHub 上的情况"
[轮次 1] 调用 search_files { pattern: 'lodash', dir: '.' }[轮次 2] 调用 read_file { path: 'package.json' }[轮次 3] 调用 github_repo_info { owner: 'lodash', repo: 'lodash' }
最终回答:是的,项目用了 lodash,package.json 里锁定的版本是 ^4.17.21,src/utils/format.js 里也直接 require 了它。GitHub 上 lodash/lodash目前 star 数超过 6 万,最后一次 push 是几周前——仓库还在维护。要确认 ^4.17.21 是不是最新发布,还需要再查一次它的发布列表。

三轮,三个工具,每一轮的参数都建立在上一轮结果之上:先搜出 lodash 出现在哪些文件里,再读 package.json 确认版本号,最后拿着这个名字去问 GitHub。这不是写死的脚本,是模型自己决定"接下来调哪个工具、传什么参数"。

这一课把它从零搭出来:三个工具、一张注册表、一个执行循环、几道安全阀。

这背后发生了什么:一次一次的 API 往返

上面看到的每一"轮",背后都是一次完整的 HTTP 请求。第 2 课讲过单次工具调用的往返长什么样;这里只是把它接成了循环——模型返回 stop_reason: "tool_use",你的代码执行工具、把结果拼回对话,再发一次请求,直到模型不再要求调用工具为止1

三轮工具调用的背后其实是四次 messages.create:前三次模型都在要工具,第四次它拿到了 GitHub 返回的数据,觉得信息够了,直接给出文字回答,循环结束。这个"要不要继续要工具"的判断完全在模型那边,你的代码只负责执行、回传。

第一步:给工具写契约

第 4 课讲过工具接口的三个核心字段:namedescriptioninput_schema2。这里直接落成代码。三个工具分别对应第 3 课讲过的五类工具里的三类:搜索、读、调用——写和执行这两类留给你在练习里自己接。

github_repo_info 带了 github_ 前缀——官方建议工具涉及外部服务时用服务名做命名空间前缀,模型选错工具的概率会低很多3search_filesread_file 操作本地文件系统,不存在"哪个服务"的歧义,不需要前缀。

三段 description 都写了"找不到时返回什么样的文字",不是废话。第 4 课提过好的描述要消除输入输出的歧义4;这里的歧义不在参数上,而在"工具没找到东西时该怎么表达"——这个坑会在"安全阀"一节炸出来。

第二步:把契约和实现注册在同一张表里

一个容易踩的坑:schema 列表和执行时用的 handler 查找表如果分开写成两份,迟早会对不上。你把 search_files 改名成 find_in_files,却忘了同步改 handler 表里的 key,模型照着新 schema 发起调用,handler 表里查不到,直接抛错。

解决办法是只维护一张表,namedescriptioninput_schema、真正执行的函数全挤在同一个对象里,API 需要的 schema 列表和执行时需要的 handler 查找表都从这张表派生:

toolSchemastoolHandlers 永远同步,因为它们是同一份数据算出来的两个视图,不是两份手写的数据。改一个工具的名字、加一个参数,只需要改 TOOLS 这一处。

第三步:实现三个工具,带上边界

searchFiles 自己写目录遍历,不借 shell 的 grep——避免把用户输入拼进命令行触发命令注入。命中数量封顶,避免一次搜索把几千行塞进上下文:

readFile 只做一件事:确认目标路径没有跑出项目根目录。第 5 课讲过的边界思路在这里就是一行带分隔符的前缀检查。注意不是裸的 startsWith(PROJECT_ROOT):假如项目根目录是 /Users/me/proj,模型传一个 ../proj-backup/x 进来,resolve 之后得到 /Users/me/proj-backup/x,裸前缀匹配照样通过——拼上 path.sep 之后,边界才真正落在目录分隔符上:

githubRepoInfo 是唯一会把数据发到项目之外的工具——本地文件内容经模型提炼成 ownerrepo 两个字符串,再发到公网。这正好是"读了私有数据 + 对外通信"两个高风险条件凑在一起的场景5,所以加一条明确的权限规则:参数必须匹配 GitHub 合法命名格式,不许是别的:

GITHUB_TOKEN 从环境变量读,不出现在代码里;不设置也能跑,只是匿名请求的速率限制更低。这跟第 5 课讲的权限规则是一个思路的两种写法:那一课讲 Claude Code 配置文件里 allow/deny/ask 那种声明式规则6,这里是写进工具代码里的命令式版本——核心都是"给高风险操作画一条不能越过的线"7

第四步:写执行循环

有了 toolSchemastoolHandlers,循环本身并不复杂。核心逻辑就四步:发请求、看 stop_reason、不是 tool_use 就返回文字、是就执行每一个工具调用块并把结果拼回去1

这里有个容易漏的细节:for (const block of response.content) 遍历的是这一轮返回的所有内容块,不是只取第一个。模型经常一次并行请求两三个工具,每一个都要执行、生成对应的 tool_resulttool_use_id 一一对应,一个都不能少8。Level 2 练习会让你亲手踩一次漏处理的坑。

安全阀,以及怎么看出工具接错了

上面这版循环能跑,但少了两道保险。加上它们:

保险一:工具执行失败要喂回去,不能让循环崩掉。 把裸调用包一层 try/catch,失败也生成一个 tool_result,只是标上 is_error: true——模型看到这个标记,通常会调整参数重试,而不是重复同一个错误9 8

保险二:同一个工具、同一组参数,连续调三次就该停了。 这不是靠猜,是靠记录最近几次调用的签名:

加上 MAX_TURNS 这道总闸,三道安全阀分工不同:MAX_TURNS 防"模型换着花样一直要工具,永远不停";重复调用检测防"模型卡在同一个参数上原地打转";工具内部的路径和格式校验(第三步写的那些)防"模型编了个越权参数,工具还老老实实执行了"。三层缺一,循环就有失控或越权的风险7

怎么从日志里看出"工具接错了"? 两个最常见的信号:

  • 模型反复调同一个工具,参数只在小范围内变化(大小写、加减一个词)。十有八九不是模型笨,是 tool_result 内容太模糊——"没找到"返回空字符串,模型分不清"确实没有"和"工具坏了",只能靠猜再试一次。
  • 模型把参数猜着填,比如给 read_file 传了一个不存在的路径。往回查通常有两种原因:description 没交代清楚参数该从哪来(呼应第 4 课),或前一步工具返回的内容里没给出精确路径,模型只能拍脑袋编一个。

小结

  • 工具的 schema 和 handler 放在同一张表(TOOLS)里注册,toolSchemastoolHandlers 都从这张表派生,改一处不会漏改另一处
  • 执行循环的核心是:发请求 → 看 stop_reason 是不是 tool_use → 是就遍历每一个工具调用块、执行、拼回 tool_result → 不是就返回文字,循环结束
  • 一轮里可能有多个并行的工具调用,每一个 tool_use 都要有唯一对应的 tool_result,漏一个下一轮请求就会报错
  • 三道安全阀各管一层:MAX_TURNS 防止模型无限索要工具,重复调用检测防止模型在同一组参数上打转,工具内部的路径和格式校验防止参数越权
  • tool_result 的内容要把"没找到"和"出错了"说清楚,含糊的空返回是模型反复重试、日志看起来像是"接错了"的头号原因

你已经走完这门课的六课,从"Agent 为什么需要工具"讲到自己写出一个能跑的工具执行循环。接下来最值得做的,不是再读一课,而是挑一个你项目里真实要做的小任务,拆成两三个工具,把这套循环骨架搬过去改一改——跑起来一次,比再读十遍解释都管用。调试时拿不准具体字段,回 sources.md 查 S4、S5 两篇官方文档,那是这套多轮循环最原始的规范文本。

Footnotes

  1. How tool use works — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works 2

  2. Define tools — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools

  3. How to implement tool use - Claude Platform Docs — https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use

  4. Writing effective tools for AI agents—using AI agents | Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents

  5. The lethal trifecta for AI agents - Simon Willison's Weblog — https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/

  6. Configure permissions - Claude Code Docs — https://code.claude.com/docs/en/permissions

  7. LLM06:2025 Excessive Agency - OWASP Gen AI Security Project — https://owasp.org/www-project-top-10-for-large-language-model-applications/2_0_vulns/LLM06_ExcessiveAgency.html 2

  8. Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2

  9. Tools - Model Context Protocol — https://modelcontextprotocol.io/docs/concepts/tools

练习

01

把本课代码抄到本地一个空目录,npm install @anthropic-ai/sdk,再执行 npm pkg set type=module(本课代码全部用 ESM 的 import 语法,Node 22.7 以下版本少了这一步会直接报 "Cannot use import statement outside a module"),设置好 ANTHROPIC_API_KEY(GITHUB_TOKEN 可选),跑一次 node agent.js "我们项目里是不是用了 lodash?查一下它在 GitHub 上的情况",确认能看到至少两轮不同的工具调用、最后给出文字回答。

Level 1:跑通它,再接上第四个工具

跑通之后,接上第四个工具 write_report(path, content):把检查结果写成 Markdown 文件,只允许写到项目下的 reports/ 目录,写到别处一律拒绝。改一句提示词,比如"把刚才的检查结果写成 reports/lodash-check.md",确认模型会主动调用这个新工具。

完成标准 · 本地勾选
02

下面这段循环代码有一个 bug,请先说明它在什么情况下会导致下一轮 API 请求报错,再给出修复后的代码。

Level 2:制造一个失败,再修好它
完成标准 · 本地勾选