# 三行速读

  1. 这章重点不是 “工具变多了”,而是 “主循环不变、能力靠路由扩展”。
  2. TOOL_HANDLERS 是扩展核心, normalize_messages 是稳定性核心。
  3. 真正工程价值在于:新增工具不用改 loop 主干。

# 先修知识

  • 读过 s01 并理解 tool_use/tool_result 回路。
  • 知道 schema 与 handler 的区别。

# 读完后你应该能做到(可检验清单)

# 本篇要解决什么

s01 解决了 “循环会转”,s02 解决 “循环能做更多事”。重点不是改主循环,而是建立可扩展的工具路由层。

一句话:能力增加,loop 不改。

# 用一个类比先理解

把主循环当 “前台”,工具系统当 “总机”:

  • 前台接到请求后,不亲自干所有事。
  • 总机根据工具名把请求转到正确部门(读文件、写文件、改文件、执行命令)。

总机设计得好,新增部门就不会影响前台流程。

# 为什么这一步必须现在做

如果每新增一个工具都改主循环,你很快会得到一个难维护的大 if-else。s02 先把 “工具声明” 和 “工具执行” 分离,后续扩展成本会低很多。

# 关键代码怎么读

# 1) 路径越界保护

1
2
3
4
5
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path

这是安全底线。哪怕是教学版,也先做边界控制。

# 2) 分发表是扩展核心

1
2
3
4
5
6
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"]),
}

以后加工具,只要 “新增 schema + 新增 handler + 注册映射” 三步。

# 3) 发送前做消息归一化

1
2
3
4
5
def normalize_messages(messages: list) -> list:
# 清理内部字段
# 补齐 orphan tool_result
# 合并连续同角色消息
...

这个函数的价值在于 “减少协议层脏数据”。很多报错不是模型问题,而是消息格式问题。

# 4) 主循环结构保持不变

1
2
3
4
5
6
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=normalize_messages(messages),
tools=TOOLS,
)

这段证明了 s02 的核心思想:新增工具,不改 loop 骨架。

# 你可以怎么复现

  1. 在现有 4 个工具基础上新增一个只读工具(例如 list_dir )。
  2. 只动 TOOL_HANDLERSTOOLS ,不动 loop。
  3. 用一个多步骤任务验证新增工具是否被稳定调用。

# 常见误区

  • 误区 1:把 TOOLS 当执行逻辑。它只是给模型看的说明书。
  • 误区 2:忽略 normalize_messages ,导致偶发协议错误。
  • 误区 3:工具太多但没有清晰边界,模型容易误用。

# 一句话总结

s02 教会我们的不是 “多几个工具”,而是 “用路由层把能力扩展做成工程化动作”。

# 补充解读:s02 里容易被忽略的工程点

如果说 s01 是 “闭环能跑”,那 s02 就是 “闭环能扩容”。扩容不难,难的是扩容后不混乱。

# A. 并发安全标记虽然还简单,但意义很大

1
2
CONCURRENCY_SAFE = {"read_file"}
CONCURRENCY_UNSAFE = {"write_file", "edit_file"}

很多人会问:这不是还没真正并发执行吗?

对,但这两个集合是在提前声明 “哪些工具语义上可并发,哪些必须串行”。先把语义边界写出来,后面做并发调度才不会拍脑袋。

# B. normalize_messages 的三个动作要分别理解

1
2
3
# 1. Strip internal metadata
# 2. Ensure every tool_use has matching tool_result
# 3. Merge consecutive same-role messages

这三步分别在解决:

  1. 协议兼容问题(内部字段 API 不认)。
  2. 事务完整性问题(工具调用必须有收据)。
  3. 会话合法性问题(角色交替要求)。

尤其第 2 点,经常被忽视。只要你做过中断恢复,就会知道 “孤儿 tool_use” 有多难排查。

# C. orphan tool_use 的补偿机制

1
2
3
4
if block.get("type") == "tool_use" and block.get("id") not in existing_results:
cleaned.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": block["id"], "content": "(cancelled)"}
]})

这一段等于在说:就算工具没跑完,也给模型一个明确状态,而不是静默丢失。这样模型后续更容易做出 “重试 / 跳过” 的稳定决策。

# D. 主循环中 “路由 + 执行 + 回写” 的固定模板

1
2
3
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})

这其实是后面所有章节都会复用的模板。你可以把它当作 Agent 执行层最核心的三行。

# 实操建议:怎样判断你真的学会了 s02

  1. 新增一个只读工具(如 file_exists ),不改 loop,验证能稳定调用。
  2. 人为制造一条 orphan tool_use,确认 normalize_messages 能补齐。
  3. 连续发几条同角色消息,确认合并逻辑生效。

# 你会在后续章节反复用到的结论

  • 工具是能力面,loop 是控制面。
  • 控制面稳定,能力面可扩。
  • 消息清洗做在入口,问题会少一半。

# 统一术语口径(本章)

  • Tool Spec :给模型看的工具说明(名字、参数、描述)。
  • Dispatch Map :工具名到处理函数的映射表。
  • Message Normalization :发送给模型前的消息清洗与修正过程。

# 章节衔接(从易到难)

  • 本章解决 “能力扩展怎么做才不破坏 loop”。
  • 下一章 s03 进入 “多步骤任务如何保持方向和节奏”。