在上一章 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_file write_file edit_file,并加上安全沙箱。
本章架构:一句话看懂
用户输入 → LLM 思考 → 工具分发(查表执行)→ 结果返回 → 写回消息 → 下一轮
工具分发是关键:LLM 不需要知道代码怎么实现,它只说 "工具名 + 参数",系统查表找到对应函数执行,循环完全不用改。
核心实现:工具路由表 + 安全路径
1. 路径安全校验(沙箱)
保证 Agent 只能在工作目录内操作:
python
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"路径超出工作区:{p}")
return path
2. 读文件工具(带长度限制)
python
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. 工具路由表(核心设计)
python
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 循环几乎一样,只是把硬编码改成查表:
python
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 不识别
所以必须在发给模型前规范化:
-
清理内部字段
-
补齐缺失的 tool_result
-
合并连续同角色消息
python
def normalize_messages(messages: list) -> list:
"""将内部消息列表规范化为 API 可接受的格式。"""
normalized = []
for msg in messages:
# Step 1: 剥离内部字段
clean = {"role": msg["role"]}
if isinstance(msg.get("content"), str):
clean["content"] = msg["content"]
elif isinstance(msg.get("content"), list):
clean["content"] = [
{k: v for k, v in block.items()
if k not in ("_internal", "_source", "_timestamp")}
for block in msg["content"]
]
normalized.append(clean)
# Step 2: tool_result 配对补齐
# 收集所有已有的 tool_result ID
existing_results = set()
for msg in normalized:
if isinstance(msg.get("content"), list):
for block in msg["content"]:
if block.get("type") == "tool_result":
existing_results.add(block.get("tool_use_id"))
# 找出缺失配对的 tool_use, 插入占位 result
for msg in normalized:
if msg["role"] == "assistant" and isinstance(msg.get("content"), list):
for block in msg["content"]:
if (block.get("type") == "tool_use"
and block.get("id") not in existing_results):
# 在下一条 user 消息中补齐
normalized.append({"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": block["id"],
"content": "(cancelled)",
}]})
# Step 3: 合并连续同角色消息
merged = [normalized[0]] if normalized else []
for msg in normalized[1:]:
if msg["role"] == merged[-1]["role"]:
# 合并内容
prev = merged[-1]
prev_content = prev["content"] if isinstance(prev["content"], list) \
else [{"type": "text", "text": prev["content"]}]
curr_content = msg["content"] if isinstance(msg["content"], list) \
else [{"type": "text", "text": msg["content"]}]
prev["content"] = prev_content + curr_content
else:
merged.append(msg)
return merged
调用模型时这样用:
python
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=normalize_messages(messages), # 先规范化
tools=TOOLS,
max_tokens=8000
)
重点区分:messages 列表是系统的内部表示, API 看到的是规范化后的副本,二者不等价。
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,循环永远不动。