第7章 让Agent给自己造工具
本章你将学到:
- 让Agent在发现能力缺口时,自动生成新的工具代码
- Agent调用自己的
write_file工具保存代码,然后注册到ToolManager- 理解"Agent自我扩展"的完整流程和安全边界
- 亲眼见证Agent从"工具使用者"变成"能力扩展者"
本章你将产出 :一个具备自建工具能力的Agent,以及至少一个由Agent自己创建并成功调用的新工具
全部章节 :收录在专栏《AI应用工程化实战教程》之【智能体工具使用实战】
7.1 一个尴尬的时刻
第5章结束时,你的Agent完成了数据分析闭环:读文件 → 执行计算 → 写报告。第6章你给它搭建了评测体系,知道它哪里做得好、哪里容易出错。
现在设想这样一个场景。
你帮老师处理一份学生成绩数据。除了常规的统计------平均分、最高分、排名------老师还提了一个特别的要求:"帮我找出成绩波动最大的学生,也就是四科成绩标准差最大的那个。"
你把任务交给Agent:
请读取 scores.csv,找出四科成绩波动最大的学生(标准差最大),生成一份简短报告保存为 volatility_report.md。
Agent开始工作。它调用了 read_file,拿到了数据。然后它调用 execute_python,在代码里写了:
python
import pandas as pd
df = pd.read_csv('scores.csv')
# 计算每个学生的标准差
df['std'] = df[['高数', '线代', 'Python', '英语']].std(axis=1)
# 找到标准差最大的学生
max_std_row = df.loc[df['std'].idxmax()]
print(f"成绩波动最大的学生:{max_std_row['姓名']},标准差:{max_std_row['std']:.2f}")
沙盒执行成功。Agent拿到了结果,然后调用 write_file 生成了报告。
一切顺利。但这里有一个值得注意的细节:Agent在 execute_python 中写的代码,是一次性的。 它写完、执行完、沙盒销毁临时文件,这段代码就消失了。下次你再让它做类似的任务(比如"找出另一个班级成绩波动最大的学生"),它需要重新生成一遍这段代码。
如果Agent能把这段"计算波动率"的逻辑持久化为一个工具,情况就不同了。第一次遇到需求时它创建工具,以后每次需要时直接调用。工具箱会随着使用不断增长,Agent的能力边界也会随之扩展。
这就是本章要做的事:让Agent给自己造工具。
7.2 Agent自我扩展的完整流程
回顾一下你的项目架构。Agent有三层能力:
| 层次 | 当前状态 | 由谁管理 |
|---|---|---|
| 工具调用循环 | agent.py 中的 run_agent 函数 |
固定逻辑,不需要改动 |
| 工具管理器 | tool_manager.py 中的 ToolManager 类 |
支持动态注册新工具(register 方法) |
| 工具箱 | 三个预置工具 + 可能的自建工具 | 初始有三个,可以不断增加 |
关键洞察是:ToolManager的 register 方法可以在运行时被调用。 不需要修改 agent.py 的源代码,不需要重启程序。只要Agent能生成正确的工具代码,它就可以通过以下流程把它变成工具箱的永久成员:
Agent 发现能力缺口
│
▼
Agent 生成新工具的 Python 代码
│
▼
Agent 调用 write_file 保存代码到 tools/ 目录
│
▼
Agent 在自己的运行环境中 import 新代码
│
▼
Agent 调用 tool_manager.register(工具定义, 实现函数)
│
▼
新工具永久加入工具箱,后续任务可以直接使用
这六个步骤中,前三步由Agent通过调用自身工具完成(生成代码是它的推理能力,保存文件是调用 write_file),后两步需要 agent.py 提供一个支持动态注册的接口。
7.3 设计"造工具"的触发机制
Agent不会无缘无故开始造工具。它需要知道自己有这个能力,以及什么时候应该用。
我们在系统提示词中增加一段"造工具"指令。在Trae对话面板中输入:
请修改 agent.py 的系统提示词,在工具使用说明之后增加以下内容:
## 自建工具能力
当现有工具无法直接满足用户需求时,你可以创建新的工具来扩展自己的能力。
### 何时创建工具
- 某个计算逻辑需要反复使用(如"计算波动率""计算同比增长率")
- 现有工具需要组合多次才能完成,而封装为一个工具更高效
- 用户明确要求"以后这种任务都帮我自动处理"
### 如何创建工具
1. 写一个 Python 函数,包含:
- 清晰的函数名(calc_开头、check_开头、convert_开头)
- 完整的 docstring 说明功能、参数、返回值
- 类型注解
- 异常处理
2. 调用 write_file,将函数代码保存到 tools/ 目录下(文件名与函数名相同,如 tools/calc_volatility.py)
3. 告诉用户你创建了新工具,说明它的名称和功能
### 约束
- 新工具只能使用沙盒允许的模块(pandas, numpy, math, statistics, json, csv, collections, itertools, datetime, re, string, decimal, fractions)
- 新工具的函数签名必须简单明确
- 不要创建与现有工具功能重复的工具
- 单次会话最多创建3个新工具
- 创建工具前,简要向用户说明你打算创建什么工具、为什么需要它
修改 run_agent 函数以支持动态注册
有了"造工具"的指令,Agent理论上会尝试调用 write_file 来保存工具代码。但这还不够------保存完代码后,Agent需要能真正把这个新工具注册到 ToolManager 中。
目前 run_agent 函数的流程是:初始化时注册三个预置工具,然后进入工具调用循环。ToolManager是在函数外部初始化的(或者函数内部初始化但无法被Agent的工具调用触及)。
我们需要增加一个特殊的工具:register_tool。它不是给用户任务用的,而是给Agent自己用的------当Agent写好了工具代码并保存后,它调用 register_tool 来动态注册。
在Trae对话面板中输入:
请修改 agent.py,增加一个特殊工具 register_tool。
## 工具定义
- 名称:register_tool
- 用途描述:动态注册一个新的工具到工具箱中。当你创建了一个新工具的Python文件后,调用此工具将其注册。注册后,该工具将在后续任务中可用。
- 参数:
- tool_name(必填,字符串):工具名称,应与Python文件名(不含.py)一致,如 "calc_volatility"
- tool_file_path(必填,字符串):工具代码文件路径,如 "tools/calc_volatility.py"
- tool_description(必填,字符串):工具的功能描述,用于让AI理解何时使用该工具
- function_name(必填,字符串):文件中要注册的函数名
- parameters_schema(必填,字符串):参数的JSON Schema描述(简化版,只描述参数名、类型、说明)
## 工具实现
- 使用 importlib 动态导入模块:
import importlib.util
spec = importlib.util.spec_from_file_location(tool_name, tool_file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
func = getattr(module, function_name)
- 构建工具定义字典(type: "function", function: {name, description, parameters})
- 调用 tool_manager.register(tool_def, func) 注册
- 返回:成功时返回 "工具 [tool_name] 注册成功";失败时返回错误信息
## 安全约束
- tool_file_path 必须以 "tools/" 开头(只允许从 tools 目录加载)
- 注册前打印工具代码供人工审查(在日志中输出)
- 不覆盖已存在的同名工具
## 注意
register_tool 本身也需要加入 tools 列表中(它也是工具),但不需要加入初始的三个工具中------
在创建 ToolManager 之后、进入循环之前,单独注册它。
审查 register_tool 的实现
打开 agent.py,检查以下要点:
工具定义检查:
-
register_tool的描述中是否清晰说明了它的用途和约束? -
parameters_schema参数是否说明了格式要求?
工具实现检查:
- 是否使用了
importlib.util进行动态导入? - 是否有路径安全检查(必须
tools/开头)? - 是否构建了正确的工具定义字典(符合DeepSeek API格式)?
- 是否调用了
tool_manager.register? - 是否处理了各种异常(文件不存在、函数名错误、重复注册)?
安全约束检查:
- 注册前是否打印了工具代码?
- 是否阻止了
tools/以外的路径?
7.5 准备 tools 目录
在项目根目录下创建一个 tools/ 文件夹。这是Agent自建工具的存放位置。
在Trae终端中:
bash
mkdir tools
在 tools/ 目录下创建一个 __init__.py 文件(空文件即可),让它成为一个Python包:
bash
touch tools/__init__.py
7.6 实例演示:Agent给自己造一个"波动率计算"工具
现在所有基础设施就绪。我们来实际运行一次,看看Agent如何给自己造工具。
7.6.1 发起任务
修改 agent.py 底部的测试入口:
python
if __name__ == "__main__":
request = """
我需要你帮我分析 scores.csv 中的数据。具体任务是:
找出四科成绩(高数、线代、Python、英语)波动最大的学生,也就是标准差最大的那个。
但注意:我以后可能还会反复让你做类似的计算(不同班级、不同科目)。如果现有工具无法直接完成,你可以考虑创建一个可复用的工具。
请完成分析,并将结果保存到 volatility_report.md。
"""
output, log = run_agent(request)
print("\n" + "=" * 60)
print("执行日志:")
print(log)
7.6.2 运行并观察
在Trae终端中:
bash
python agent.py
观察终端日志。你期望看到的执行轨迹可能是这样的:
[用户请求] 我需要你帮我分析 scores.csv...
[第1轮] 模型调用工具: read_file({"path": "scores.csv"})
[工具返回] 学号,姓名,高数,线代,Python,英语...
[第2轮] 模型调用工具: execute_python({"code": "import pandas..."})
[工具返回] 成绩波动最大的学生:孙七,标准差:12.36
[第3轮] 模型:我已完成分析。但我注意到"计算成绩波动"是一个可复用的需求。
我建议创建一个 calc_volatility 工具,以后可以直接调用。
现在我先完成当前任务。
[第3轮] 模型调用工具: write_file({"path": "volatility_report.md", ...})
[工具返回] 成功写入文件
[第4轮] 模型:报告已保存。接下来我为"计算成绩波动"创建一个可复用工具。
[第4轮] 模型调用工具: write_file({"path": "tools/calc_volatility.py", "content": "def calc_volatility(...)..."})
[工具返回] 成功写入文件
[第5轮] 模型调用工具: register_tool({"tool_name": "calc_volatility", ...})
[工具返回] 工具 calc_volatility 注册成功
[第6轮] 模型最终回答: 分析完成!波动最大的学生是孙七(标准差12.36)。
报告已保存到 volatility_report.md。
我还为你创建了一个 calc_volatility 工具,以后你可以直接让我用它来分析任何成绩数据。
如果Agent没有主动创建工具,而是直接完成了任务就结束------这很正常。不是每次任务都需要造工具。你可以明确要求它:
在分析完成后,请为"计算成绩波动"这个功能创建一个可复用的工具。
7.6.3 验证新工具是否真的被注册了
修改测试入口,在任务完成后列出所有工具:
python
if __name__ == "__main__":
request = """..."""
output, log = run_agent(request)
print("\n" + "=" * 60)
print("当前工具箱:")
for name in tool_manager.list_tools():
print(f" - {name}")
再次运行。你应该看到 calc_volatility 出现在工具列表中。
7.6.4 测试新工具是否真的能用
再写一段测试,直接使用新工具:
python
if __name__ == "__main__":
# 第一次任务:创建工具(如果还没创建)
request1 = "请为计算成绩波动创建一个可复用的工具 calc_volatility"
run_agent(request1)
# 第二次任务:使用新工具
request2 = """
请读取 scores.csv,使用 calc_volatility 工具计算每个学生的成绩波动,
然后调用 write_file 将结果保存为 all_volatility.csv。
"""
output, log = run_agent(request2)
print(output)
如果一切正常,Agent在第二次任务中会直接调用 calc_volatility 工具,而不是重新写一段 execute_python 的代码。它用上了自己造的工具。
7.7 工具箱的持续生长
一旦Agent有了自建工具的能力,一个有趣的长期效应就出现了:工具箱会随着使用不断生长。
你可以想象这样一个演化路径:
- 第1次任务 :Agent创建了
calc_volatility - 第3次任务 :Agent发现需要做数据归一化,创建了
normalize_data - 第5次任务 :Agent发现每次都要读取CSV然后做描述性统计,创建了
quick_summary - 第10次任务:Agent已经积累了6个自建工具,新任务中80%的工具调用都是自建工具
这就是"AI自我扩展"的雏形。Agent不再是一个固定功能的程序,而是一个能力可以随时间积累的系统。
当然,在教学中,我们的规模不大。但这个概念本身,是本科生理解"AI Agent的长期价值"的关键。
7.8 安全边界:不要让Agent的自我扩展失控
工具自建能力给了Agent很大的自主权。必须再次强调安全边界。
第一,所有新工具仍在沙盒内。 Agent自建的工具,内部逻辑如果涉及代码执行,仍然需要通过 execute_python,仍然受沙盒的模块白名单和超时限制。自建工具不能绕过沙盒。
第二,注册前必须打印代码。 register_tool 的实现中有一个关键步骤:在真正注册之前,把工具代码打印到终端日志中。这是给人看的------你可以随时按 Ctrl+C 中断执行。
第三,路径限制。 register_tool 只允许加载 tools/ 目录下的文件。Agent不能通过路径穿越加载系统文件。
第四,数量限制。 系统提示词中明确写了"单次会话最多创建3个新工具"。这防止了Agent陷入"疯狂造工具"的死循环。
第五,不覆盖已有工具。 register_tool 的代码检查工具是否已存在。Agent不能替换 read_file 或 execute_python 这些基础工具。
7.9 本章小结
- Agent从"工具使用者"跃迁到"能力扩展者"。它不再受限于你预先提供的工具集。
- 自我扩展的完整流程 :发现缺口 → 生成代码 → 写文件 → 动态导入 → 注册 → 调用。ToolManager的
register方法支持运行时注册是关键。 register_tool是元工具------它不直接完成任务,而是扩展完成任务的能力。- 安全边界依然在:路径限制、数量限制、代码打印审查、沙盒约束。Agent的能力扩展是有护栏的。
- 工具箱会持续生长 。每个自建工具都留在
tools/目录下,下次启动Agent时可以被重新加载。
到现在为止,你的Agent已经拥有了三个预置工具,以及至少一个由它自己创建的工具。它读取数据、执行计算、保存报告、扩展自身------一个真正能"干活"的AI Agent的骨架已经完整。
下一章,你将运用第二部全部所学,完成一个结业实战项目:代码仓库健康度分析Agent。它将扫描项目文件夹,分析代码质量,生成报告------并且在这个过程中可能还会自己创建新工具。
课后练习
- 运行7.6节的演示,确认Agent成功创建了
calc_volatility工具。打开tools/calc_volatility.py,阅读Agent生成的代码。它的代码质量如何?有没有遵循你的指令(类型注解、docstring、异常处理)? - 要求Agent创建第二个工具:
check_pass_fail------输入一个学生的各科成绩,返回该学生是否需要补考(任何一科低于60分即为需要补考)。观察Agent是否能基于第一个工具的经验,更高效地完成创建。 - 关掉终端,重新启动Python,重新运行
agent.py。Agent还能调用之前创建的工具吗?如果不能,你觉得需要在代码中增加什么功能(提示:在初始化时扫描tools/目录)? - (进阶)修改
register_tool的实现,增加一个功能:注册新工具时,自动将工具信息追加到一个tools/registry.json文件中。下次Agent启动时,从registry.json中读取所有自建工具并自动注册。