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你该看懂的几点
- Tool Schema 的 description 是给模型看的”使用说明书”。 模型看不到你的函数体,它只凭
name+description+parameters判断”什么时候该用这个工具、怎么填参数”。描述写不清,模型就调错——这就是为什么 Part 2.2 说”描述写得好坏直接决定调用准确率”。 - 错误处理是免疫系统。 看 Demo B:
calc("x", 2)在 Python 里会抛TypeError,但Tool.run把它包成了TOOL_ERROR: ...字符串回流。模型在下一步看到了错误,自己改成calc(3, 2)重试。循环没崩,任务完成了。对比后端:这就像你在 service 层 catch 异常返回错误码,而不是直接让进程挂掉。 - 安全边界是”请求”与”执行”的物理隔离。
delete_file永远不会裸删——白名单之外的路径直接返回SAFETY_DENY。模型以为自己”下令删了”,其实它只发出了请求,真正的执行权在你手里。这是 Part 2.4 讲的幂等、确认、沙箱的最小实现。
思考题
【思考题】
- Demo B 里,如果 mock 剧本在 step2 不返回”重试 calc”,而是直接 text_resp 说”算不出来”,循环会怎样结束?这跟真实模型遇到工具报错的行为有何不同?
delete_file用”白名单”做安全。真实生产里,白名单往往不够(路径可被../绕过、符号链接逃逸)。你觉得还该加哪几道防线?(提示:Part 2.4 提到的沙箱、人类确认)- 把 CRUD 系统暴露成 agent 工具时,为什么”只读查询”和”生成草稿”通常可以放开,而”直接 DELETE / 无审计的写”绝对不能暴露?用你后端经验解释。
【参考答案】
- 若 step2 直接 text_resp,循环会因为”模型不再要求调工具”而正常结束,只是答案变成了”算不出来”。区别在于:真实模型不会主动放弃——它看到
TOOL_ERROR后大概率会重试或换参数,因为 system prompt 里通常要求”出错要尝试修复”。mock 是我们人为写死的,真实模型有”继续尝试”的倾向,所以你必须设计好 max_steps 和错误次数上限,否则它会在一个坏工具上反复重试烧 token。 - 至少三道额外防线:(a) 路径规范化 + 前缀校验——先把
../解析掉,再确认解析后的绝对路径确实落在允许的目录树内;(b) 人类确认(HITL)——删除类动作在真正执行前弹确认,甚至要求二次签名;(c) 执行沙箱——在容器/专用用户下运行,即使逃逸也伤不到宿主。Part 5.5 会把这三道防线系统化。 - 后端经验告诉我们:读操作(SELECT)是幂等的、可重复的、不改变的;写操作改变状态。但”直接 DELETE”比普通写更危险——它不可逆、且往往没有业务上下文校验。生成草稿(INSERT 到一个 draft 表、状态=未发布)是可回滚的;无审计的写(直接 UPDATE 生产行)出了事查不到谁改的。所以暴露给 agent 的工具要遵循”CQRS 思路”:查询和”安全写”(带审计、带状态机)放开,裸 DELETE / 跨聚合的破坏性写必须走人类确认或根本不暴露。Part 2.5 的 CRUD 案例就是这套取舍。
下一步
工具会出错了、会自救了、也拦住危险动作了。但单个工具解决不了”先查再改再验证”的多步任务——那是 lab02 规划 的事。