Tool Use
循环不用动,新工具注册进 dispatch map 就行。
这是 harness 可扩展性的第一个关键模式。
s01 只有一个 bash 工具,有什么问题?
s01 只用 bash 解决一切 —— 读文件用 cat,写文件用 echo > file,编辑用 sed。能跑,但很粗糙:
cat 截断不可控
bash 输出被一刀切 50000 字符。读到哪断了都不知道。结构化工具可以精确控制 offset/limit。
sed 遇到特殊字符就崩
文件里有 / * . 这些字符时,sed 的正则需要疯狂转义。LLM 经常写出有 bug 的 sed 命令。
每次 bash 都是不受约束的安全面
cat /etc/passwd、rm -rf ../other-project——bash 能访问整个文件系统。没有路径边界。
核心洞察:加工具不需要改循环
这是 s02 最关键的认知。对比 s01 和 s02 的 agent_loop:
for block in response.content: if block.type == "tool_use": output = run_bash(block.input["command"]) # ↑ 只有一个工具,直接写死
for block in response.content: if block.type == "tool_use": handler = TOOL_HANDLERS.get(block.name) output = handler(**block.input) # ↑ 字典查一次,N个工具都一样
变的只是 for 循环里面的 3 行代码。
如果加一个工具就要改一次循环逻辑,那有 20 个工具时循环就是一团 if/elif 面条。s02 的模式让工具数量从 1 到 100,循环复杂度不变。
🧪 案例:假如你想加第 5 个工具 "grep_file"
用它来在文件里搜索指定文本。我们看看在 s01 风格和 s02 风格下分别要怎么做:
# 你要钻进 agent_loop 的 while 循环里面,加一个 elif: for block in response.content: if block.type == "tool_use": if block.name == "bash": output = run_bash(...) elif block.name == "read_file": # 每次加工具 output = run_read(...) # 都要来这里 elif block.name == "write_file": # 加一个新的 output = run_write(...) # elif 分支 elif block.name == "edit_file": output = run_edit(...) elif block.name == "grep_file": # ← 新加的! output = run_grep(...) # 循环越来越胖
# ① 写一个 handler 函数(独立的,不碰循环) def run_grep(path, pattern): text = safe_path(path).read_text() matches = [l for l in text.splitlines() if pattern in l] return "\n".join(matches) # ② 在 TOOL_HANDLERS 字典加一行(不动循环) TOOL_HANDLERS = { "bash": ..., "read_file": ..., "write_file": ..., "grep_file": lambda **kw: run_grep(kw["path"], kw["pattern"]), # ← 就这! } # ③ 在 TOOLS 列表加一个 schema(不动循环) {"name": "grep_file", "description": "Search text in file", ...} # Agent Loop?一个字都不用改。
👆 这就是 dispatch map 模式的核心价值:循环是封闭的,工具是开放的。加工具不需要理解循环逻辑,循环逻辑不依赖具体工具。
Dispatch Map:一个字典解决工具路由
整个模式就三个部分:工具 schema(给 LLM 看)+ 处理函数(给 harness 用)+ 映射表(连接两者)。
① 工具 schema 定义(TOOLS 列表)
告诉 LLM "有哪些工具、每个工具接受什么参数"。这是 Anthropic API 的 tool use 协议的一部分。
② 处理函数(handler functions)
每个工具一个函数,接收 LLM 传的参数,执行实际操作,返回字符串结果。
③ 映射表(TOOL_HANDLERS 字典)
一个字典,key 是工具名(与 schema 的 name 严格一致),value 是处理函数。这就是 dispatch map。
{"path": "a.py", "limit": 20} 这种字典。lambda 的作用是把字典"拆开"传给函数。用 lambda(代码里用的):
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit"))不用 lambda(等价写法,更易读但更啰嗦):
def handle_read(kw): return run_read(kw["path"], kw.get("limit"))TOOL_HANDLERS = {"read_file": handle_read, ...}两者完全等价。lambda 只是一行写完。关键理解:TOOL_HANDLERS 字典的 value 是一个"函数",不管这个函数是 lambda 还是 def 定义的。运行时用
handler(**block.input) 调用它,block.input 就是 LLM 传来的参数字典。
🧪 跟读数据流:一次 "read_file" 调用从头到尾发生了什么
假设用户在 s02 里输入 "看看 requirements.txt 里面有什么"。我们跟踪整个过程:
"type": "tool_use",
"name": "read_file", ← LLM 决定调用哪个工具
"id": "toolu_001",
"input": {"path": "requirements.txt"} ← LLM 填的参数
}
↓ 从字典里取出 key="read_file" 对应的 value
handler = lambda **kw: run_read(kw["path"], kw.get("limit"))
↓ lambda 把字典拆开,等价于调用:
output = run_read(path="requirements.txt", limit=None)
② 读文件内容 → "anthropic>=0.25.0\npython-dotenv>=1.0.0\npyyaml>=6.0"
③ 返回这段文本
"content": "anthropic>=0.25.0\npython-dotenv>=1.0.0\npyyaml>=6.0"}
👆 整个过程的关键:循环代码不需要知道"read_file"是什么。它只知道:从 TOOL_HANDLERS 字典里取出一个函数,用 LLM 给的参数调用它,把返回值塞回 messages。不管未来加 10 个还是 100 个工具,这 3 行代码始终不变。
三个新工具,各自解决了 bash 的一个硬伤
bash: echo 'print("hello")' > a.py # 问题:引号嵌套容易出错 # 问题:不能 mkdir -p 父目录 # 问题:内容含特殊字符就崩
write_file: path="a.py" content='print("hello")' # 自动创建父目录 # 内容原样写入,不需要转义 # 返回写入字节数供验证
bash: sed -i 's/old_text/new_text/' file.py # 问题:old_text 含 / 就崩 # 问题:特殊字符需要疯狂转义 # 问题:LLM 经常写出有 bug 的 sed
edit_file: path="file.py" old_text="exact string" new_text="replacement" # Python 字符串精确匹配+替换 # 不涉及任何 shell 转义 # 找不到 old_text 会报错
bash: cat long_file.py # 输出在 50000 字符处被一刀切 # 不知道切在哪一行 # 想读后半段?没法指定 offset
read_file: path="long_file.py" limit=50 # 按行限制,不丢完整行 # 返回 "(N more lines)" 提示 # 可精确控制读取范围
代码拆解:s02 的四个组成部分
初始化:多了安全路径的基准
s02 新增了 WORKDIR 和 safe_path()。所有文件操作都基于 WORKDIR,作为路径沙箱的锚点。
工具实现:四个独立的处理函数
每个函数内聚一个工具的全部逻辑。路径安全、输出截断、错误处理全在函数内部。
Dispatch:字典映射 + 循环中调用
TOOL_HANDLERS 字典(上一节已展示)是静态的映射表。循环中的调用只需要一行查找:handler = TOOL_HANDLERS.get(block.name)。
外层:完全不变
REPL 循环、history 管理、输入输出处理 —— 和 s01 一模一样。这就是关键:工具层面的改动不影响外层架构。
safe_path:路径沙箱是最小但最关键的安全机制
给 Agent 文件读写能力后,第一个安全问题就是路径逃逸。Agent 可能被诱导读写工作区外的文件:
path = "../../../.ssh/id_rsa" open(path).read() # 读到了!
path = "../../../.ssh/id_rsa" safe = (WORKDIR / path).resolve() # → /Users/xxx/.ssh/id_rsa if not safe.is_relative_to(WORKDIR): raise ValueError("Path escapes!")
safe_path 做了什么
① 拼接:WORKDIR / user_path — 把用户给的相对路径拼到工作目录下。
② resolve:解析所有 .. 和符号链接,得到真实的绝对路径。
③ 检查:is_relative_to(WORKDIR) — 真实路径是否还在工作目录内?不在就报错。
注意:safe_path 不止是安全检查。它让 Agent 可以用相对路径操作("a.py" 而不是 "/full/path/a.py"),LLM 不需要知道绝对路径。
s01 vs s02:什么变了,什么没变
| 组件 | s01 | s02 |
|---|---|---|
| 工具数量 | 1(仅 bash) | 4(bash, read_file, write_file, edit_file) |
| Dispatch 方式 | 硬编码:直接调用 run_bash | 字典映射:TOOL_HANDLERS.get(block.name) |
| 路径安全 | 无 | safe_path() 沙箱 |
| Agent Loop | 完全不变(while True、stop_reason、messages 管理都一样) | |
| 外层 REPL | 完全不变 | |
| System Prompt | 微小调整:从 "Use bash" 变成 "Use tools" | |
从 s02 带走的三个核心认识
循环是稳定的,工具是可扩展的
Agent Loop 不会因为加工具而改变。好的架构让核心稳定,让扩展点开放。dispatch map 就是这个扩展点。
专用工具 > 通用工具
bash 能做任何事,但 read_file/write_file/edit_file 在各自领域更好。精确的参数 schema = LLM 调用更准确。工具层面的安全检查 = bash 做不到的沙箱。
加工具 = 加 handler + 加 schema
以后你要加新工具(grep、git、web),只需要三步:写一个 handler 函数 → 定义一个 schema → 在 TOOL_HANDLERS 字典里加一行。循环碰都不碰。
落地案例:Dispatch Map 在真实 AI 产品中的应用
学完 Tool Use 之后,你会注意到所有成熟的 AI Agent 产品都有一套"工具注册系统"。以下是三个真实的 Agent 工具系统案例。
🔌 场景一:MCP(Model Context Protocol)— Anthropic 的工具标准
Anthropic 发布的 MCP 协议 就是 s02 的 dispatch map 工业化版本。它定义了一个标准:AI 应用如何发现、注册、调用外部工具。
MCP Server = 一个工具包(比如 GitHub MCP Server 提供 create_issue、list_prs、search_code 等工具)。
MCP Client(Claude Code)= 自动发现 Server 提供的工具 → 注册到 TOOLS 列表 → LLM 调用时通过 dispatch 分发。
你正在用的 Claude Code 支持 MCP——你在设置里加一个 MCP Server,就相当于往 TOOL_HANDLERS 字典里批量注册了一组工具。不需要改 Claude Code 的核心逻辑。这就是 s02 的 dispatch map 模式在工业级产品中的样子。
🔍 场景二:OpenAI Function Calling — ChatGPT 的插件生态
ChatGPT 的"插件"和"Function Calling"功能就是 s02 的 TOOLS 列表 + dispatch map:
TOOLS 列表:每个插件声明自己的 functions(名称 + 参数 schema)→ 注册到 ChatGPT 的工具列表。
Dispatch:用户说"帮我订一张明天去上海的机票"→ LLM 判断需要调用"携程插件"的 search_flights function → dispatch 到这个 function → 返回航班列表。
OpenAI 的开发者文档里教你定义 function schema 的方式,和 s02 里定义 TOOLS 的方式完全一致:name、description、parameters(就是 input_schema)。你学完 s02 之后,可以直接看懂 OpenAI 的 function calling 文档。
🛠️ 场景三:LangChain 的 Tool 抽象
LangChain 是 Python 生态里最流行的 Agent 框架。它的 Tool 系统本质就是 s02 的 dispatch map:
LangChain 用装饰器(@tool)自动把 Python 函数转成工具 schema + handler。但底层原理和 s02 完全一样:一个函数 = 一个工具,一个名字 = dispatch key,加工具不改循环。你理解了 s02 的 3 行 dispatch 代码,就理解了 LangChain 几百行 Tool 抽象背后的核心思想。