第 25 节 · 工具的设计哲学:4 个关键选择
一句话回答
工具的设计不是"实现功能"就完了,而是"帮 LLM 不犯错"。 今天讲 4 个最关键的设计选择,每一个都对应一类典型的 LLM 翻车场景。
选择 ① edit 用 old_content / new_content,不用行号

翻车场景
假设 edit 接收 line_number 参数:
# 第 1 步:在第 5 行前面插入新函数(3 行)
edit_at_line("main.py", line=5, mode="insert_before", content="...")
# → 此时第 12 行 = 原文件的第 9 行
# 第 2 步:修改"原本的第 12 行"
edit_at_line("main.py", line=12, mode="replace", content="...")
# → 但 LLM 不知道行号偏移了,改错了位置解法
接收 old_content / new_content,按内容定位:
edit("main.py",
old_content="def greet():\n print('hi')",
new_content="def greet():\n print('hello')")不管前面增删多少行,目标文本永远精确。
唯一性约束
old_content 必须在文件中只出现一次。这是为什么?
old_content = "return"文件里 30 个 return——LLM 不知道改哪个。强制要求唯一就能逼模型加足够的上下文。
old_content = "def add(a, b):\n return a + b"这就唯一了。
这个约束是 Cursor / Claude Code 的
edit工具几乎一致的设计。
old/new 不是唯一答案——2026 年三种主流编辑设计
| old/new(Cursor / Claude Code / 我们) | apply_patch V4A diff(OpenAI Codex) | hashline / anchor(实验性) | |
|---|---|---|---|
| 怎么定位 | 按文本片段(要求唯一) | 按差异块的前后文(contextual diff) | 按「行号 + 内容哈希」双重校验 |
| 优点 | 实现最简、模型最熟、错误最直观 | 一次能改多处、token 紧凑 | 抗「模型记错内容」、抗「文件被改过」 |
| 缺点 | 多处一样的代码要 LLM 自己加上下文 | patch 解析器要写得稳,错位时报错难懂 | 要求 read 输出带哈希,工具更复杂 |
学完 Day5 的 old/new,再去看 OpenAI 的 apply_patch 文档 就能秒懂——本质都是「按内容定位、不按行号定位」。
选择 ② bash 工具的四道安全闸门

bash 是最强大也最危险的工具——能跑任意命令。LLM 一不小心就能把你仓库删了。
四道闸门是底线:
| 闸门 | 长什么样 | 为什么 |
|---|---|---|
| 超时 | timeout=30 | LLM 偶尔会写 while True,没超时直接挂死 |
| 输出截断 | max_output=50000 | 一次 find / 几百万行返回会爆 messages |
| 工作目录限定 | cwd=workdir | 防止跑到 ~ 或 /etc 那种地方动东西 |
| 危险命令拦截 | 黑名单 regex | rm -rf /、sudo、远程脚本下载、fork 炸弹 |
黑名单不是终极方案
我们的 bash.py 里有这样一段:
DANGER_PATTERNS = [
r"\brm\s+-rf\s+/",
r"\bsudo\b",
r"\b(curl|wget)\s+.*\|\s*(sh|bash)",
...
]这是最低限度。真正生产级的方案是:
- 容器化(Docker / Firejail)—— Agent 跑在隔离的小盒子里
- 命令白名单(只允许
python/pytest/git等明确命令) - 文件系统 read-only 挂载(除了 workdir)
今天我们做最低版,让你看到"边界在哪"。
2026 年的工业沙箱实践
2025-2026 年发生过几起标志性事件,让「黑名单不够用」成为业界共识:Claude Code 多次被报出 rm -rf 误删用户文件、有研究员演示了通过 /proc/self/root/... 路径绕过 denylist。主流方案演进成三层防御:
- 细粒度权限:Claude Code 的
settings.json把每个工具拆成allow/deny/ask三档(例如Bash(npm run *)自动放行、Bash(rm -rf *)直接拒绝、Bash(git push *)必须人工确认),比黑名单 regex 精细得多 - 沙箱隔离:Cursor 3 的 Background Agents 跑在 isolated VM 里、Claude Code 推荐 Docker 容器(2026.01 上线 Docker Sandboxes,每个用独立 microVM)、OpenAI Codex 直接跑在云端
- 行为分类器:Claude Code 2026.03 的 「Auto Mode」用一个独立模型实时判断「动作越权 / 提示注入 / 触碰未知系统」,比静态黑名单灵活
底线:纯黑名单只能挡住「教科书级」翻车,生产环境一定要加沙箱。我们今天做的是教学版黑名单,目的是让你看到「边界在哪、为什么不够」。
选择 ③ 读类工具大胆调,写类工具严防死守

工具分两类,对它们的设计心态完全不同:
| 读类(read / list_dir / glob / grep) | 写类(write / edit / bash) | |
|---|---|---|
| 失败成本 | 低(重试就行) | 高(可能写坏文件 / 删错东西) |
| 副作用 | 无 | 有(文件系统改了) |
| 模型策略 | LLM 可以"试错" | 每次必须有明确意图 |
| 设计心态 | 友好 / 容错 / 信息丰富 | 严格 / 校验 / 可回滚 |
读类工具的设计要点
- 路径不存在 → 友好报错(不要 raise,让模型看到"文件不存在: xxx" 自己处理)
- 大文件 → 自动截断(默认 100KB,避免一次塞满 messages)
- 输出带行号(让 LLM 后续能精确引用)
写类工具的设计要点
- write: 默认拒绝覆盖已有文件(要
overwrite=True才能覆盖) - edit: 强制 old_content 唯一
- bash: 4 道闸门
- 所有写类工具: 失败时 不要 raise,把错误转成字符串返回 —— 让 LLM 能看到错误自己重试
选择 ④ 工具失败 → 字符串返回,不抛异常
def edit(path, old_content, new_content):
if not os.path.isfile(path):
return f"❌ 文件不存在: {path}" # ← 不 raise
if count == 0:
return f"❌ 未找到匹配内容..." # ← 不 raise
...为什么?
如果 raise,整个 Agent Loop 就崩了。但如果返回字符串:
LLM 调 edit("a.py", "old", "new")
↓
工具返回:"❌ 未找到匹配内容: a.py"
↓
LLM 看到错误 → "啊我搞错了,先 read 看看再来" → 自己纠正让模型自己看到错误,自己决定下一步。这是 Day2 第 8 节就讲过的"工具异常兜底"原则——Day5 的所有工具一律遵守。
错误消息的「三段式」格式
光「返回字符串」还不够——错误消息要让模型有足够信息恢复。Anthropic、OpenAI 工程团队总结的最佳实践是 三段式:
❌ <出了什么问题> ← what went wrong
<预期是什么> ← what was expected
<一个修复示例 / 下一步建议> ← example of correct usage对照看 edit 的两个错误消息:
| 场景 | 不及格 | 及格 |
|---|---|---|
| 0 处匹配 | Error: not found | ❌ 未找到匹配内容。请用 read 重新看一下当前内容,再调一次 edit。 |
| 多处匹配 | Error: ambiguous | ❌ old_content 匹配了 3 处,请加更多上下文使其唯一(如包含函数签名、前后空行)。 |
一句话:错误消息是给 LLM 的「恢复说明书」。
把这 4 条总结成一句话
工具的输出是给 LLM 看的 UI——参数怎么命名、输出什么格式、怎么报错,全部都是「给 LLM 看」,不是给人看。
Anthropic 工程团队把这件事提到了「投入和 HCI 一样多的设计精力」的高度,并给出 3 条最有杠杆的建议(Writing effective tools for AI agents,2025):
- 参数名要无歧义:
user改成user_id、time改成start_iso8601——名字本身就是文档 - 零上下文心态:写 description 时假装「给一个新来的实习生看」,不要默认 LLM 知道你的内部约定
- 工具是契约:deterministic 程序和 non-deterministic agent 之间的契约——工具自己要 robust to error,不要把校验责任甩给 LLM
一些常见的"误用工具"案例
| 误用 | 现象 | 怎么防 |
|---|---|---|
| LLM 用 write 覆盖已有文件 | 整个文件被替换 | write 默认拒绝覆盖 |
| LLM 用 edit 改文件,但 old_content 写错 | 静默失败 | 唯一性约束 + 友好报错 |
LLM 用 bash 跑 find / | 几百万行返回爆 messages | 输出截断 + 工作目录限定 |
| LLM 反复同样调用同一工具 | 死循环 | Day3 已经讲了:max_iters + 重复检测 |
| 工具 description 写得太简略 | LLM 不知道何时调、参数怎么填 | 按「零上下文」心态写 description + 给 1-2 个示例 |
设计思路总结
写好 Coding Agent 的工具,3 件事:
- 默认安全:写类工具默认拒绝危险操作(覆盖、删除、远程执行)
- 失败可读:错误转字符串返回,含具体原因 + 修复建议
- 输出友好:带行号 / 含状态码 / 含元数据,让下一个工具的 LLM 调用方便
跟 full-agent 的工具集对照看,examples/full-agent/tools/impl/ 全部 9 个工具都遵守这 3 条。
动手试试
cd labs/05-coding-agent-tools
python demo_25_design_philosophy.py会跑 3 个对比演示:edit 唯一性约束、行号偏移问题、bash 危险命令拦截。
小结
| 设计选择 | 防什么翻车 |
|---|---|
| edit 用 old/new | 防多步编辑后行号偏移 |
| edit 唯一性约束 | 防一次修改改了不该改的多处 |
| bash 4 道闸门 + 沙箱 | 防仓库被删 / 输出爆 / 死循环 |
| 失败返回字符串 | 防 Agent Loop 崩溃,让模型自己纠错 |
| 错误消息三段式 | 让 LLM 能从错误中恢复 |
| 参数无歧义命名 | 让模型不用猜、少出错 |
理论结束。下面 lab:
- Part 1(60min):实现
read/list_dir(读类,失败成本低,grep选做) - Part 2(60min):实现
write/edit/bash(写类,严防死守) - 末尾 5min:Git 速通 5 个命令把代码推到 CNB