Python 复刻:agents/s02_tool_use.py
你跟 Claude Code 说一件事,它自己挑该用哪个工具——读文件用 Read、跑 Stata 用 mcp__stata-mcp__stata_do、跨文件搜词用 Grep、写审计报告用 Write。代码这边只是按工具名字找到对应的执行函数,调用一下,把结果回传。挑哪个工具完全是模型的判断。
2.1 工具是什么
工具是一份给模型看的清单。清单上每一条工具都有三样字段:名字、用途描述、参数格式。名字比如 Read 或 Grep。用途描述一句话写明这个工具能做什么。参数格式说明调用时该传什么。这份清单随每次模型调用一起发给模型,模型由此知道当前任务可以用哪些工具。
代码这边维护一份与清单对应的对照表:工具名字 → 执行函数。模型说"调用 Read,文件路径是 02_变量字典/测算方法说明.md",代码查对照表找到 Read 对应的函数,按文件路径参数运行,把读出来的内容作为结果回传给模型。
整个调度不做任何"该用什么工具"的判断。判断完全在模型那一头,依赖工具清单里"用途描述"那一句写得清不清楚。
2.2 一次工具调用的完整过程
模型每轮发回的响应里可能含工具调用请求。请求里有三个字段:调用的工具名、一个用来配对结果的标识号、一份参数对象。参数按工具规定的格式填好。
代码侧处理这次调用就是一行:在对照表里按工具名找到执行函数,把参数解包传进去,运行,得到一个字符串结果。执行函数内部读文件、调 stata-mcp、跑 Grep、调外部 API 都可以,对调度系统的唯一约束是返回字符串。
结果回到对话历史时带上原请求的标识号,让模型在下一轮看到结果时知道这是它哪次调用的回报。模型在下一轮根据结果决定下一步动作——继续调下一个工具、向你回报、或者结束本轮。
新工具进来的代价是一行对照表加一个执行函数。工具清单里"用途描述"写好之后,模型在合适的场景自己学会调用,循环本身不用改一行。
2.3 一次真实运行里模型挑了哪些工具
下面这段记录的是上面那次"PC 字段一致性核对"实际跑出来的工具序列。同一个任务里 Claude Code 自己挑了四类不同工具,按顺序串起来,没人事先指定该用哪个。
| 轮 | 模型决定 | 调用工具 | 参数 | 工具结果摘要 |
|---|---|---|---|---|
| 1 | 先看口径定义 | Read | 02_变量字典/测算方法说明.md | PC(A2 主)= 稳定型股权 2(持股标准差 ln(1+x) 软压缩)+ 关系型债权 2(银行长贷 /(银行长贷+应付债券+应付票据)),z-score 标准化后相加 |
| 2 | 看数据集里实际怎么落的字段 | mcp__stata-mcp__get_data_info | data_path=04_中间数据/main_panel.dta | 含字段 PC、PC_A1、PC_B2、PC_C、SD_A2_ln、Bank、z_SD_A2、z_Bank;PC 的 label 是"耐心资本(A2 主口径,z(SD_股权)+z(Bank_债权))" |
| 3 | 核 do4 拼面板时的构造代码 | Read | .statamcp/stata-mcp-dofile/do4_panel_assemble.do | 第 85-87 行:egen z_SD_A2 = std(SD_A2_ln)、egen z_Bank = std(Bank)、gen PC = z_SD_A2 + z_Bank。与 02 字典一致 |
| 4 | 核 do6 基准回归用的 PC 名字 | Grep | pattern="reghdfe.*PC " path=.statamcp/stata-mcp-dofile/do6_baseline.do | 7 处命中:第 54 行 PC、第 60 行 PC_A1、第 66 行 PC_C、第 89 行 PC(三分项 LHS=esg_score)、第 94/99/104 行(E/S/G 三分项) |
| 5 | 核 do10 稳健性用的口径替换 | Grep | pattern="reghdfe.*PC" path=.statamcp/stata-mcp-dofile/do10_robust.do | A2/A1/B2/C/仅股权/仅 Bank/仅 Rdebt 七套口径都在;命名一致 |
| 6 | 写核对报告 | Write | path=06_结果输出/pc_consistency_check.md | 报告含口径定义、字段名映射、构造代码摘录、回归引用位置;判定"02 字典 ↔ do4 构造 ↔ do6/do10 引用三方一致" |
四类工具完成了一次跨字典、数据集、do-file、报告的核对:Read 读 markdown 与 do-file 文本,stata-mcp 工具读 .dta 元数据,Grep 在跨文件正则定位,Write 落核对报告。代码侧的调度逻辑没有任何"该用 Read 还是 stata-mcp"的 if-else——模型每轮看到当前对话历史,自己决定该挑哪个工具继续推进。
2.4 为什么这样设计
代码层无法穷举"什么场景用什么工具"的组合。"核字段定义就用 Read"这条规则容易写,"核字段定义但还要去 Stata 看实际 label、再到 .do 里 Grep 看代码用法、最后写报告"这条组合规则就开始难。组合的数量呈指数增长,写不完。模型在训练时见过千万种类似的"看见这种场景该用哪几个工具组合"的例子,泛化判断是它的强项。
调度逻辑保持一行,让 Claude Code 的扩展成本极低。装一个 skill 新增的工具自动进入对照表,模型自动学会调它。任何外部能力都能通过这一行接入。实证项目里你装 stata-mcp 工具读 .dta、装 PDF 抽取工具读 01_文献/ 下面 20 篇耐心资本论文的 PDF、装 docx 工具读 07_论文写作/01_主表/PC_ESG_主表汇总.docx,调度代码一行不改。
2.5 容易踩的坑
模型有时会调一个清单上没有的工具名。这通常是用户的指令暗示某个工具但 Claude Code 实际未装。处理方式是调度入口检测到"对照表里没这个工具名"时返回一个明确的错误信息,让模型在下一轮重新选。
模型偶尔会传错参数——比如 stata-mcp 要求 data_path 是绝对路径但模型传了相对路径,或者 Grep 漏了 pattern 字段。执行函数入口需要做参数校验并返回明确错误,让模型在下一轮修正。上面那次第 2 轮调用 stata-mcp 时模型先试过 data_path="main_panel.dta"(相对路径),stata-mcp 返回路径不存在,模型在第 3 轮改成绝对路径才成功——这种自纠错完全在模型那一头,调度代码不参与。
工具用途描述写得模糊时,模型可能在错误的场景调用错误的工具。用途描述是模型做触发判断的唯一依据,描述质量直接决定调度准确性。装一个 skill 时如果它的用途描述含糊,效果会很差。
2.6 知识地图
| 关键词 | 含义 | 容易误会的点 |
|---|---|---|
| 工具清单 | 名字、用途、参数格式的列表 | 模型只看得到清单里列的,未列的工具它根本不会调 |
| 工具名 | 模型在调用时指定执行哪个工具的字符串 | 由模型显式写出;代码按字段查对照表,不靠上下文猜 |
| 参数格式 | 每个工具规定的输入结构 | 决定模型生成参数时填哪些字段;松散的格式容易让模型传错参数 |
| 工具对照表 | 代码侧"名字 → 执行函数"的查找结构 | 没有 if-else 决定执行什么,纯查表 |