S02|工具使用:让 Agent 真正会干活,加工具不改循环的核心设计
在上一章 S01 里,我们搭好了 Agent 的最小循环,让模型能调用工具、把结果喂回自己继续推理。但当时只有一个bash工具,既不安全也不好扩展。
这一章 S02,我们要解决工具扩展、安全限制、统一分发三大问题,实现一句核心原则:加一个工具,只加一个 handler,循环完全不用改。
本章核心信息
- 核心闭环:把模型意图 → 路由成真实动作
- 工具数量:4 个(bash /read_file/write_file /edit_file)
- 核心思想:主循环不变,靠分发层扩展能力
先把本章所有名词讲明白
1. Handler(工具处理器)
每个工具真正执行的代码函数。比如读文件、写文件、执行命令,各自有独立的处理逻辑。
2. Dispatch Map / 工具路由表
一张 “对照表”:工具名字 → 对应的处理函数
比如,模型说要用read_file,程序就查表找到run_read执行。
3. Harness 层(工具调度层)
统一管理所有工具的中间层,负责:
接收模型调用 → 找到对应工具 → 执行 → 返回结果。
4. Path Sandbox 路径沙箱
安全限制:Agent 只能读写指定文件夹内的文件,不能越权访问系统文件,防止删库、偷文件。
5. Tool Schema(工具描述)
给模型看的 “工具说明书”,告诉它:这个工具叫什么、需要传什么参数、用来干什么。
6. normalize_messages(消息规范化)
把内部消息整理成模型 API 能接受的格式,保证:
- 工具调用和结果一一对应
- 消息角色不连续重复
- 不携带内部字段导致报错
7. turn(轮次)
一轮 = 模型思考 + 可能的工具调用 + 结果返回一轮结束进入下一轮,直到任务完成。
8. state(状态)
Agent 运行时的全部记忆:消息历史、轮次数、继续 / 停止原因。
为什么不能只用一个 bash 工具?
S01 我们只用了bash(命令行),看起来万能,其实隐患巨大:
cat 读文件可能被截断特殊字符、长文件会导致输出不完整,模型拿到错误信息。
sed 编辑极易崩溃遇到
$ & / \等符号,命令直接报错。完全没有安全边界模型可以执行任何命令:删文件、改系统配置、泄露信息。
所以必须拆成专用工具:read_filewrite_fileedit_file,并加上安全沙箱。
本章架构:一句话看懂
用户输入 → LLM 思考 → 工具分发(查表执行)→ 结果返回 → 写回消息 → 下一轮工具分发是关键:LLM 不需要知道代码怎么实现,它只说 “工具名 + 参数”,系统查表找到对应函数执行,循环完全不用改。
核心实现:工具路由表 + 安全路径
1. 路径安全校验(沙箱)
保证 Agent 只能在工作目录内操作:
def safe_path(p: str) -> Path: path = (WORKDIR / p).resolve() if not path.is_relative_to(WORKDIR): raise ValueError(f"路径超出工作区:{p}") return path2. 读文件工具(带长度限制)
def run_read(path: str, limit: int = None) -> str: text = safe_path(path).read_text() lines = text.splitlines() if limit and limit < len(lines): lines = lines[:limit] return "\n".join(lines)[:50000]3. 工具路由表(核心设计)
TOOL_HANDLERS = { "bash": lambda **kw: run_bash(kw["command"]), "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), "write_file": lambda **kw: run_write(kw["path"], kw["content"]), "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), }4. 循环中统一分发
和 S01 循环几乎一样,只是把硬编码改成查表:
for block in response.content: if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) output = handler(**block.input) if handler else f"未知工具:{block.name}" results.append({ "type": "tool_result", "tool_use_id": block.id, "content": output })关键洞察:加工具不用改循环
以后你想加新工具,比如:
- search_web
- run_python
- get_weather
只需要两步:
- 写一个 handler 函数
- 在
TOOL_HANDLERS里加一行映射
agent_loop 代码一行不动这就是工业级 Agent 的标准设计。
为什么要消息规范化?
随着工具变多,消息会越来越乱:
- 工具调用缺结果
- 连续两条 user 消息
- 带内部字段 API 不识别
所以必须在发给模型前规范化:
def normalize_messages(messages: list) -> list: # 1. 清理内部字段 # 2. 补齐缺失的 tool_result # 3. 合并连续同角色消息 ...调用模型时这样用:
response = client.messages.create( model=MODEL, system=SYSTEM, messages=normalize_messages(messages), # 先规范化 tools=TOOLS, max_tokens=8000 )S01 → S02 到底升级了什么?
| 模块 | S01 | S02 |
|---|---|---|
| 工具数量 | 1 个(bash) | 4 个 |
| 工具调用 | 硬编码 | 路由表分发 |
| 安全 | 无 | 路径沙箱 |
| 消息 | 直接发送 | 规范化后发送 |
| 扩展性 | 差 | 极强 |
| 主循环 | 不变 | 不变 |
初学者可以直接试的 Prompt
Read the file requirements.txt Create a file called greet.py with a greet(name) function Edit greet.py to add a docstring to the function Read greet.py to check the result你会看到:Agent 会自主选择工具,自动读写编辑,完全不用你干预。
本章教学边界(不搞复杂,只抓核心)
本章不讲权限、不讲缓存、不讲流式、不讲异常恢复。只牢牢抓住三件事:
- Tool Schema给模型看
- Handler Map给代码分发
- Tool Result回流到循环
只要懂这三点,你就掌握了所有 Agent 工具系统的底层结构。
一句话总结本章
Agent 的能力增长,不靠把循环写复杂,而靠一层清晰的工具分发层。加工具只加 handler,循环永远不动。
