Lab 01 · 工具机制(对应 Part 2 工具机制)

墨叔:上节模型只会调一个 stub。这节让工具”长齐五官”——有 schema、有免疫系统、有安全边界。这三个东西,是从”玩具”到”工具”的分水岭。


这个 lab 在练什么

三个递进的 demo,对应 Part 2 的三个核心:

demo对应 Part 2 小节练什么
A 基础调用2.1 工具的本质 / 2.2 Schema 设计模型靠描述选工具、靠参数结构填参数
B 错误恢复2.3 失败与重试(免疫系统)工具抛异常不崩循环,回流给模型自救
C 危险拦截2.4 副作用与安全模型只有”请求权”,删除由安全层把关

怎么跑

python main.py          # 三个 demo 依次跑,全程 mock,无需 key

你该看懂的几点

  1. Tool Schema 的 description 是给模型看的”使用说明书”。 模型看不到你的函数体,它只凭 name + description + parameters 判断”什么时候该用这个工具、怎么填参数”。描述写不清,模型就调错——这就是为什么 Part 2.2 说”描述写得好坏直接决定调用准确率”。
  2. 错误处理是免疫系统。 看 Demo B:calc("x", 2) 在 Python 里会抛 TypeError,但 Tool.run 把它包成了 TOOL_ERROR: ... 字符串回流。模型在下一步看到了错误,自己改成 calc(3, 2) 重试。循环没崩,任务完成了。对比后端:这就像你在 service 层 catch 异常返回错误码,而不是直接让进程挂掉。
  3. 安全边界是”请求”与”执行”的物理隔离。 delete_file 永远不会裸删——白名单之外的路径直接返回 SAFETY_DENY。模型以为自己”下令删了”,其实它只发出了请求,真正的执行权在你手里。这是 Part 2.4 讲的幂等、确认、沙箱的最小实现。

思考题

【思考题】

  1. Demo B 里,如果 mock 剧本在 step2 不返回”重试 calc”,而是直接 text_resp 说”算不出来”,循环会怎样结束?这跟真实模型遇到工具报错的行为有何不同?
  2. delete_file 用”白名单”做安全。真实生产里,白名单往往不够(路径可被 ../ 绕过、符号链接逃逸)。你觉得还该加哪几道防线?(提示:Part 2.4 提到的沙箱、人类确认)
  3. 把 CRUD 系统暴露成 agent 工具时,为什么”只读查询”和”生成草稿”通常可以放开,而”直接 DELETE / 无审计的写”绝对不能暴露?用你后端经验解释。

【参考答案】

  1. 若 step2 直接 text_resp,循环会因为”模型不再要求调工具”而正常结束,只是答案变成了”算不出来”。区别在于:真实模型不会主动放弃——它看到 TOOL_ERROR 后大概率会重试或换参数,因为 system prompt 里通常要求”出错要尝试修复”。mock 是我们人为写死的,真实模型有”继续尝试”的倾向,所以你必须设计好 max_steps 和错误次数上限,否则它会在一个坏工具上反复重试烧 token。
  2. 至少三道额外防线:(a) 路径规范化 + 前缀校验——先把 ../ 解析掉,再确认解析后的绝对路径确实落在允许的目录树内;(b) 人类确认(HITL)——删除类动作在真正执行前弹确认,甚至要求二次签名;(c) 执行沙箱——在容器/专用用户下运行,即使逃逸也伤不到宿主。Part 5.5 会把这三道防线系统化。
  3. 后端经验告诉我们:读操作(SELECT)是幂等的、可重复的、不改变的;写操作改变状态。但”直接 DELETE”比普通写更危险——它不可逆、且往往没有业务上下文校验。生成草稿(INSERT 到一个 draft 表、状态=未发布)是可回滚的;无审计的写(直接 UPDATE 生产行)出了事查不到谁改的。所以暴露给 agent 的工具要遵循”CQRS 思路”:查询和”安全写”(带审计、带状态机)放开,裸 DELETE / 跨聚合的破坏性写必须走人类确认或根本不暴露。Part 2.5 的 CRUD 案例就是这套取舍。

下一步

工具会出错了、会自救了、也拦住危险动作了。但单个工具解决不了”先查再改再验证”的多步任务——那是 lab02 规划 的事。