9  工具链:Agent 够得着什么

前三章讲的是 Agent 知道什么,这一章讲它能做什么

这是四块环境里唯一一块光靠约束长不出来的。目录可以规定、依赖方向可以拦、 规则的载体可以安排 —— 但”Agent 够不够得着”只能靠有没有人一件一件 把工具造出来。

9.1 环境完美的仓库,可能还是瞎的

设想一个仓库:目录结构完美、依赖方向清晰、规则都在正确的载体上、 检查严密到没有一条坏代码能进主干。** Agent 只能改文本文件、跑一下构建。**

那么它看不见自己写的界面长什么样,出不了上架截图,读不到线上日志, 不知道自己刚加的那个按钮在真机上是不是被键盘挡住了。这条边界不是靠规范 划出来的,是靠有没有人去把工具造出来。

投入比例比我自己预期的大得多:工具链目录下有 225,192 行 Rust, 占全仓 310 万行的 7.4%,编译出 38 个二进制。作为对照, 前面整整一章讲的那个检查器运行时是 67,113 行 —— 工具链是它的三倍多。

这个比例值得记住,因为它和大部分人的直觉相反:“给 Agent 建规矩” 听起来是主要工作量,实际上”给 Agent 造手”才是。

9.2 让 Agent 直接操作真机

第一件工具解决的是”看不见”。它是一个 MCP 服务,让 Agent 能像人一样操作 真机或模拟器上正在跑的 App,而架构是两侧对接的。

flowchart LR
    subgraph APP["App 内侧(DEBUG 构建)"]
        direction TB
        WS["WebSocket 服务<br/>mDNS 广播自己"]
        CAP["触摸模拟 · 元素定位<br/>窗口管理 · 输入 · 截图"]
        WS --- CAP
    end
    subgraph HOST["宿主机侧"]
        direction TB
        D[("全机单例 daemon<br/>独占设备连接")]
        SH["shim(每个客户端一个)<br/>list_tools 静态,零 IPC"]
        SH -->|"首个 call_tool<br/>才按需连接"| D
    end
    AG["Agent"] -->|"MCP"| SH
    D <-->|"局域网"| WS
    style D fill:#fdf3e3
    style SH fill:#e8f4e8

一共 20 个工具,分四类:操作(点击、滑动、缩放、旋转、长按、长按拖拽、 输入文本、跳路由)、观察(截图、视图层级快照、找元素、找全部元素、 录屏开始停止)、读状态(列目录、取文件、查日志、设备信息)、 连接(连接、列连接、装组件、诊断)。

这组工具闭上的是”人盯屏幕”那个环。 在此之前,一个 Agent 改完界面 只能靠单元测试和自己脑补;现在它可以装上、点进去、截个图、查一下视图层级、 读一眼沙盒里写了什么文件、翻一遍日志,然后判断自己改对没有。

9.2.1 两个设计决定值得学

代码里有两处注释,各说明一个不显然的决定。

第一处:工具目录必须能在不碰设备的情况下列出来。注释写的是 “不预连 daemon:list_tools 是静态的,首个 call_tool 才按需连接或拉起”。 这个决定的理由是:Agent 问”我能做什么”的频率,远高于它真的去做什么。 如果列出工具需要先连上设备,那么每一个会话都要付一次设备连接的延迟 —— 而且在没有设备接着的时候会直接失败,于是 Agent 会以为自己没有这些能力。

工具的存在性,不该依赖工具的可用性。 这条可以推广:任何”我能做什么” 的查询,都应该比”我去做”便宜一个数量级,而且不应该有前置条件。

第二处:设备连接收敛到全机单例。 多个进程不能各自持有到同一台设备的连接, 会互相踩,所以每个客户端进程只是一个转发壳,真正的连接归一个单例守着 —— 又是单一 writer,这次用在了设备句柄上。而这一条在几十个 Agent 并行的 场景下不是优化,是必需品:260 个工作区如果各自去连同一台测试机, 得到的不是”慢”,是”全都连不上”。

9.2.2 第三处:所有可调阈值集中在一个地方

还有一处值得单独看,因为它把 小节 13.2 那条纪律 用在了工具自己身上。这个工具的策略模块开头写着一句: “所有可调阈值集中在此,不散落在通路里”,紧接着列出了四个注入点 —— 重连、健康检查、发现、编排,各自注入到哪个组件。

其中重连那一组长这样:

pub struct Reconnect {
    pub max_attempts: u32,   // 同一 Endpoint 内的最大重试次数
    pub base_delay: Duration,
    pub max_delay: Duration, // 指数退避上限
    pub factor: f64,
    pub cooldown: Duration,  // 期满后 reconciler tick 自醒
}

impl Reconnect {
    pub fn delay_for(&self, attempt: u32) -> Duration {
        let raw = self.base_delay.as_millis() as f64
            * self.factor.powi(attempt as i32);
        Duration::from_millis(raw.min(self.max_delay.as_millis() as f64) as u64)
    }
}

三件事值得注意。max_delay 那个上限是形状 F 的解药—— 一个没有上限的指数退避,在设备长时间不在的时候会退避到几小时, 而那时候即使设备回来了它也不会重连。 max_attemptscooldown 是抗积分饱和: 达到上限之后不是无限重试下去,是进入一个有期限的冷却, 期满自醒 —— 这正是 小节 8.7 讲的那个模式, 只不过这次出现在设备连接上。

第三件最要紧:这五个数全都在一个结构体里, 而不是散在重连逻辑的各处。它带来的差别是, 当有人问”我们的重连策略是什么”时,答案是一个可以被完整读完的类型, 不是一次跨五个文件的搜索。对 Agent 来说这个差别更大—— 它不会去跨五个文件拼一个策略,它只会看它当下读到的那一处。

9.3 但工具会制造它自己的假象

这一节是这一章最重要的一节。

给了 Agent 一只手之后,会出现一类新问题:它开始能观察到东西了, 而它观察到的东西可能是假的。

说明文档里记着两条真实的。第一条是模拟器默认被判为非生产环境, 于是埋点上报短路、页面浏览事件不发 —— 结果是所有依赖页面浏览的功能 都永不触发,而 Agent 会看到一个”功能没生效”的现象,然后去修那个功能。 第二条是调试浮层恒在最上层,把提示和奖励横幅盖住, 现象是”逻辑跑了、界面没出来”。

第二条的处理方式很有代表性,原话是:

改取窗逻辑一律无效 —— 浮层按设计恒在最上层,主窗口本来就是正确的宿主。 把它当选窗 bug 去修会改坏正常路径。

注意最后半句。它没有停在”这是个假象”,而是明确写出了如果不知道这是假象、 去修它会造成什么后果 —— 一个正确的窗口选择逻辑会被改坏, 而且改坏之后不会立刻报错。

所以:给 Agent 工具的同时,必须把工具的假象一起给它。否则它会拿着 一只能操作的手,去修一个根本不存在的 bug,而且修完之后会真的坏掉一个东西。

这是形状 E(边界处的静默降级)在工具链里的形态 —— 工具是一层边界,穿过它之后语义变了,而两侧看起来都正常。

9.4 能操作,还得能复现

只让 Agent 会点还不够。同一串操作,这次进去是登录态、下次是新用户, 得到的结论就不一样 —— 这时候”能操作”反而制造了不确定性, 因为 Agent 会观察到两次不同的结果,然后开始寻找一个并不存在的原因。

解法是一份声明式的自动化 profile,取代所有散落的环境变量约定, 换成 App bundle 里的一份 JSON。一份 profile 声明用户状态、 数据层引用哪套 fixture、界面初始状态(含首屏落在哪条路由)。 schema 与产品无关,每个产品实现自己的种子器把它翻译成具体的 App 内效果。 实测有 45 份 profile、11 个产品的种子器,命名一看就知道用途。

9.4.1 两条契约让它成为确定性的来源

一份配置如果没有这两条,它只是又一处可变状态,而不是确定性的来源。

第一条是必须幂等 —— 同一份 profile 跑两次,得到同一个状态。 第二条是合并遵循标准的 merge-patch 语义 —— 整体状态用一份共享 profile, 每页只用一个编码过的补丁覆盖起始路由,所以”同一个场景的 20 个页面” 不需要 20 份互相抄的配置。

值得注意的是:这两条契约在测试里是被直接断言的,不是只写在文档里。 测试文件里能看到这些用例名:

test_is_idempotent
test_mergePatch_replaces_scalars
test_mergePatch_deep_merges_objects
test_mergePatch_null_deletes_key

最后一条尤其说明问题。merge-patch 语义里,一个字段被设成 null 表示 “删除这个键”,而不是”把它设成空” —— 这是个容易实现错的细节, 而它有一条专门的测试守着。

一个”应该幂等”的东西和一个”有测试证明它幂等”的东西,在 Agent 手里 是两个东西。前者会在某次改动后悄悄不幂等,而所有依赖它的截图任务 会开始产出随机结果 —— 那时候没有人会怀疑到这里。

这正是前面几章反复出现的那条纪律,只是这次用在了 App 的启动状态上: 每份可变状态收敛到单一 owner,而这个 owner 的契约要被断言。

9.5 发布大脑:一张声明式的表

第三件工具解决的是”发布是不可逆的”。

发布逻辑集中在一个约三十万行的 Go 服务里,它的核心是一张声明式的 工作流注册表 —— 注释写着它是”每一条业务流水线的唯一事实源”, 变更检测、任务注册、回调注册全都读这张表,而不是各自维护一份。

28 条工作流,六种触发模式。签名那一组是每天两次定时加每周一次全量; 崩溃那一组是每 5 分钟扫描、扫描结果驱动后续;导入那一组是 webhook 加定时兜底。

发版那一组是这一章的正例。 打包、上传、改元数据、报符号表、提审 —— 这五个动作没有 webhook 入口、没有定时触发、没有裸 API, 只能由发布流程本身驱动 —— 小节 7.4 那条”连触发方式都要被收窄”, 在工具链这一层的形态就是它。

9.5.1 这张表实际跑成了什么样

每一次执行都被记下来,所以这些不是设计意图,是能查的账。近四个月 181,570 次执行,日均约 1,441 次,整体成功率 98.3%。

流水线那一侧的分布更能说明问题。取样的 13 天、1,200 条流水线里, 改动事件触发的占 47.0%,发布大脑自己调起的占 26.7%,push 占 26.2%, 而人在网页上点的只有 1 条,占 0.1%

也就是说:每四条流水线就有一条不是人推出来的,而十三天里人手动点的只有一次。

9.6 一个反例:签名自动修复挂了四个月

现在讲这一章的反例,也是全书最重要的一节之一。

签名这一组本来是我最想拿出来讲的。证书会过期,过期了构建就红, 常规做法是等它红了人去后台点一遍。这里有三条定时任务顶着 —— 每天体检、每天自动刷新、每周全量重签,仓库里躺着 46 份配置文件、 4 个团队的证书。听起来这类问题应该被彻底消灭掉。

翻账的时候才发现不是:

工作流 执行 失败 失败率
体检 126 3 2.4%
自动刷新 126 46 37%
全量重签 18 16 89%

失败原因很具体。自动刷新的 46 次失败里,29 次是同一个空指针崩溃, 首末两次跨了四个月。至于体检,它只在最早的三天失败过, 之后 123 天零失败。

探测是可靠的,修复是坏的。

9.6.1 为什么四个月没人发现

因为它坏了不产生任何可见后果。

我抽查了 35 次构建与发布的脚本失败,只有 1 次跟签名有关, 而且不是证书过期,是权限声明与配置文件对不上。构建照样在绿, 没有人被这个崩溃挡住过路,于是它就一直挂在那里。

这正是全书那句话的反面:一个不产生判定的动作,坏了你不会知道。 检查失败会返回退出码、CI 会变红、Agent 会被挡住;而一条定时任务失败, 只是记录表里多了一行”失败”,没有任何人或机器把它当成需要行动的信号。

9.6.2 但归因还要再深一层

上面那个诊断是对的,但它不完整。真正该追问的是: 自动刷新挂了 37%、全量重签挂了 89%,构建为什么一直是绿的?

如果这三条任务真的在维持签名有效性,那么它们失败到这个程度, 证书应该早就过期了,构建应该早就红了。最可能的解释是: 这三条任务从头到尾就没起过作用 —— 真正在维持签名有效性的是别的东西, 可能是构建工具的自动签名,可能是证书还没到期,可能是有人手动补过。

所以这不只是”监控缺失”,它同时是:

一个新建的自动化上线时,从来没有验收标准来证明它在做事。

这恰恰是 Agent 大量产出基建代码时最典型的失败模式 —— Agent 很擅长写出一个”看起来在做这件事”的定时任务,而如果没有人问 “怎么证明它真的做了”,这个任务会一直挂在那里,既不工作也不报错。

顺带还有一个同类的数字:这 28 条工作流里有 5 条从注册之后一次都没跑过。 注册表是唯一事实源,但”注册了”和”在跑”是两件事,中间同样缺一层判定。

章节 19 会讲这个缺口该怎么补,而且给出的答案不是”加监控”。

9.7 工具链内部也在守同一条纪律

最后一件事最能说明这套东西的一致性。

伪本地化审计流水线要做的事是:切语言 → 装包 → 逐页启动 → 等渲染完 → 截图 → 文字识别 → 分类。它的注释记着自己是怎么来的:

取代旧的 148 行脚本。旧脚本用固定的 sleep 6 + sleep 3, 在冷启动加重初始化的路径上会产生假阴性。现在改成盯日志流里的路由事件。

这和测试那章的 flake 教训是同一条(见 小节 12.5)—— 别拿时间赌,上界要用真正代表”这件事发生了”的信号。 只不过它这次发生在工具链里,而不是测试里。

它的分层也照搬了检查器的做法:模型层从构建图派生出数据模型(纯,不碰 I/O), 静态层做完整度与一致性分析(纯),运行时层才是那个要起模拟器、 跑文字识别的重家伙。注释里直接写着这是”镜像了架构检查引擎”。

9.7.1 但这条纪律还没走完

补一句。整个仓库 150 万行 Swift 生产代码里,违反”别拿时间赌”这条纪律的 轮询只剩 7 处,其中 4 处是系统 API 的契约要求(那些接口只能轮询, 没有回调),1 处是外部引入的代码。

真正的违规只有 1 处 —— 而它在最核心的那个运行时文件里, 是一个服务解析的等待循环,用毫秒级轮询加固定次数上界, 而且那条路径没有测试覆盖。

这一节想说的不是”我也会犯错”,是:

纪律的推进是由外向内的。工具链先改,最核心的运行时最后改。

这个顺序不是偶然。工具链的问题会立刻表现成”审计流水线出假阴性”, 而微内核那个轮询在正常情况下几乎不会触发 —— 它只在两个线程同时解析同一个未装配服务时才走到。 看得见的先修,看不见的留到最后 —— 即使后者更危险。

这和 小节 6.6.1 里磁盘满重复五次是同一件事: 判定覆盖到哪里,纪律就执行到哪里。

9.8 什么时候该买,什么时候该造

这一章讲的工具大部分是自己造的, 所以得说清楚什么时候不该造 —— 否则”7.4% 的代码量” 会被读成一个必须付的价钱。

判据不是”有没有现成的”,是这个工具的输出会不会被 Agent 当成事实。

如果答案是不会 —— 它只是在做一件事而不产生结论 (部署、上传、格式化)—— 那么买或者用现成的, 基本不会有问题。这类工具的失败是响的: 它没做成,你会知道。

如果答案是会 —— 它的输出会被 Agent 读进去、 当成”当前的状态是这样”然后继续推理 —— 那么现成的工具有一个很实际的问题: 你不知道它在什么情况下会给出一个看起来正常的错误答案。 一个截图工具在设备没连时返回一张空图, 一个日志查询工具在索引落后时返回一个不完整的结果 —— 这些行为在它自己的语境里都是合理的, 而在你的语境里它们是 小节 9.3 讲的那种假象。

所以现实的做法不是”全造”,是 小节 9.15.1 那条: 买或用现成的,然后在外面包一层薄的适配, 把它的假象和边界在那层里写清楚。 真正需要从头造的,是那些现成工具根本够不着的能力 —— 而在这个仓库里,那部分就是那个能操作真机的 MCP 服务, 因为没有任何现成的东西提供”让 Agent 像人一样点一个真机上的 App”。

一句话:造的理由应该是”够不着”,不是”不够好用”。 不够好用可以包一层;够不着才必须造, 而够不着的东西通常比想象中少。

9.9 结论

给 Agent 造工具,难的不是让它能点。

难的是让它点完之后,你能确定刚才发生了什么 —— 所以才有幂等的启动状态、 有代替 sleep 的就绪信号、有把假象一起写下来的说明、有静态的工具目录、 有单例的设备连接。

但签名那个反例说明,这套纪律在工具链里还没走完。前三章讲的检查全都作用在 代码上,而工具链里跑的这一万多次定时任务,没有任何一层检查在管它们 —— 它们是给 Agent 生产确定性的东西,自己却处在确定性覆盖之外。

9.10 工具链投入的三个阶段

7.4% 这个数字容易吓退人。但这笔投入不是一次付清的,它有明确的阶段, 而且每个阶段的边际收益差别很大。

第一阶段是让 Agent 能看见它改了什么 —— 截图、日志、状态查询。 这一阶段的收益最陡,因为从”完全瞎”到”能看见”是一个质变,而不是量变。 大部分团队缺的是这一阶段,而它通常只需要几百行胶水代码。

第二阶段是让观察可复现 —— 幂等的启动状态、固定的种子数据、 就绪信号代替固定等待。这一阶段的收益不如第一阶段陡, 但它是第一阶段能否被信任的前提:一个不可复现的观察, 比没有观察更糟,因为它会让 Agent 追一个不存在的原因。

第三阶段是让不可逆的动作有唯一入口 —— 发布、签名、上架。 这一阶段的收益是避免灾难,而不是提高效率,所以它的价值 不体现在任何效率指标上。

三个阶段必须按顺序做。跳过第二阶段直接做第三阶段, 会得到一个”能自动发布但没人知道发出去的是什么”的系统。

9.10.1 可以直接抄的判据

判断该不该造一个工具,有一个比”这样会更方便”更硬的判据: Agent 现在是在观察,还是在猜?

如果它在猜 —— 猜界面长什么样、猜日志里有什么、猜这个改动在真机上生效没有 —— 那么这个工具的价值不是”提高效率”,是把一个猜测变成一个事实

猜测和事实的差别在 Agent 场景下被放大了,因为 Agent 的猜测会被它自己当成事实继续往下推。它不会说”我猜界面是这样”, 它会直接基于那个猜测做下一个决定。所以每一个”它只能猜”的地方, 都是一条会静默产生错误结论的路径。

9.10.2 被低估的那一类工具:只读的

这一章讲的大部分工具都涉及操作,而收益最高的那一批往往是只读的, 原因有三条。

只读工具的风险接近零。 一个查日志的工具最坏的情况是查错了, 而一个点击的工具最坏的情况是把生产数据点没了 —— 这个差别意味着只读工具可以被更早、更宽松地开放。

9.10.2.1 状态查询要比状态修改便宜得多

Agent 需要读的次数远多于写。 它没有”我刚才看过了”这个记忆, 所以它会反复地问”现在是什么状态”。

只读工具是写工具的前提。 你没法验证一个写操作的效果, 除非你能读到它的结果。

所以正确的建设顺序是先把所有的”看”补齐,再补”做”。实践中常见的顺序 是反的 —— 因为”让 Agent 能自动部署”听起来比”让 Agent 能查日志”更有价值。

9.10.2.2 只读工具清单

按通用程度排,大部分项目都适用的只读工具有六个:查最近的日志 (替代”它跑起来了吗、报了什么错”这个猜测)、查当前的配置与环境变量 (“这个值现在是什么”)、查数据库或存储的当前状态(“我刚写进去的东西在吗”)、 截一张界面的图(“它长什么样”)、查一个进程或服务的健康(“它还活着吗”)、 查依赖的版本(“我们用的是哪个版本”)。六个,每一个通常几十行, 而它们合起来能消掉 Agent 大部分的猜测。

9.10.3 工具链的投入曲线为什么是反直觉的

大部分基础设施投入的收益曲线是递减的 —— 第一台 CI 机器的价值 远高于第十台。工具链的收益曲线在早期是递增的。

原因是 Agent 的每一项能力都在放大其它能力的价值。只能截图的时候, 它能看到界面但不知道点了之后会怎样;截图加点击,它能验证交互; 再加上读日志,它能诊断为什么交互不对;再加上可复现的初始状态, 前三项的结论才开始可信。每加一项,前面所有项的价值都上升了。

这解释了一个常见的困惑:“我们给 Agent 加了截图工具,好像也没什么用”—— 因为单独一项能力经常真的没什么用。这条曲线的膝点大概在三到四项能力之间。

工具链的投入之所以容易被中途放弃,也是这个道理:它的回报在膝点之后 才出现,而膝点在前面看不见。

9.11 工具的说明该写在哪

给了工具之后,紧接着的问题是 Agent 怎么知道它存在、 以及怎么知道它的假象。这是一个载体问题(章节 8), 而工具的说明有一个别的规则没有的特点:它必须和工具一起变。

一份工具说明如果住在一份独立的文档里, 它会在工具的第三次迭代之后开始失真 —— 而失真的方式是最坏的那种:参数还对,行为已经变了。 Agent 照着说明调用,得到一个不报错但不是它期望的结果, 然后基于那个结果继续。

所以工具说明的正确载体有一条硬要求: 它必须住在一个”改工具就必然会看到它”的位置。 实践中的三种形态,按可靠性排序: 最好的是工具自己的输出 —— --help、错误消息、 以及调用失败时返回的那句”你可能想要的是 X”; 其次是工具源码旁边的说明文件,因为改工具的人至少会路过它; 最差的是一份独立的工具目录文档, 因为改工具的人没有任何理由打开它。

“工具的假象”(小节 9.3)比工具的用法更需要这条纪律, 因为假象的描述是最容易过时的 —— 一个假象往往在某次修复之后就不存在了, 而一条描述已经不存在的假象的说明, 会让 Agent 绕开一条其实已经好了的路。 这一类失效尤其难发现,因为它的表现是”Agent 用了一个笨办法”, 而没有人会去追问它为什么不用那个更直接的办法。

9.12 工具链和判定层的交界

有一类东西既像工具又像判定,值得单独辨析, 因为放错位置会导致它两边都不管

典型的例子是一个”跑起来看看有没有崩”的脚本。 当工具用的时候,Agent 主动调用它来验证自己的改动,它属于工具链; 当判定用的时候,CI 自动跑它、失败就拦,它属于判定层。 同一个脚本,两种用法,而它们的要求不一样:

作为工具 作为判定
谁调用 Agent 主动 自动,每次
失败的含义 “再试试别的” “这次不算数”
输出要求 信息丰富 结论明确 + 证据
是否需要退出码三分 不必 必须
是否需要哨兵 不必 需要

最常见的错误是把一个当工具写的东西直接接进 CI 当判定用。 后果有三个:它的失败语义不清楚,Agent 不知道该改代码还是改环境; 它没有哨兵,坏了会静默通过;它的输出是给人看的,下游得去解析它。 三个后果单独看都不致命,合起来就是一道看起来在守、 实际上什么都没守的检查。

判据很简单:它的失败会不会挡住合并? 会,它就是判定, 按判定的标准要求它 —— 而这个标准比工具的标准高得多, 高到通常需要一次重写而不是一次包装。

9.13 工具链投资的优先级

如果预算有限,按这个顺序。

第一优先是让 Agent 能观察它改的东西的直接结果—— 截图、日志、状态查询,也就是 小节 9.10 的第一阶段。 第二优先是让那些观察可复现,也就是幂等的初始状态; 否则第一优先的投入会打对折,因为不可复现的观察 会让 Agent 追一个不存在的原因。 第三优先是把不可逆的动作收窄到唯一入口—— 这一项的收益是避免灾难,不体现在任何效率指标上, 所以它最容易被排到后面,而它应该在第三位,不是最后第四优先及以后是一切”让它更方便”的东西。

注意前三项都不是”更方便”,它们分别回答的是: 能不能观察、观察可不可信、错了能不能挽回。 这三个问题的排序不是偏好问题 —— 后一个的答案 在前一个没有答案时没有意义。第四项之后的东西, 收益是线性的;前三项的收益是阶跃的, 而阶跃的东西必须整块建完才开始产生价值。

9.14 工具链腐化的信号

工具会坏,而工具坏掉的方式和代码不一样 —— 它通常不会报错,它会开始返回过时或错误的结果。 有三个可以观察的信号。

Agent 开始不用某个工具了。 如果一个工具存在但 Agent 不调用它, 有两种可能:它不知道有这个工具(载体问题), 或者它用过发现不好用。两种都值得查, 而且这两种的修法完全不同 —— 前者改的是常驻文件,后者改的是工具本身。

同一个工具的调用后面经常跟着”手工验证”。 这说明 Agent(或人)不信任它的输出, 而不信任通常来自几次具体的不一致 —— 找出那几次, 比追问”为什么不信任”有效得多。

工具的文档和它的行为对不上。 这是 小节 8.14.0.2 讲的那种漂移,而工具比 skill 更容易发生, 因为工具的实现会跟着依赖升级而变, 而依赖升级是一件持续发生、且没有人为它写变更说明的事。

三个信号的共同点是它们都不会让任何检查变红。 这就回到了这一章的结论:工具链自己处在判定覆盖之外, 所以它的腐化是静默的(小节 9.9)。

9.15 给 Agent 的工具,接口设计不一样

给 Agent 用的工具和给人用的工具,接口设计的取舍不一样。 列几条差别,因为它们不显然。

第一,返回结构化数据,不是给人看的文本。一个给人看的命令行工具, 输出通常是格式化的文本,而 Agent 消费这类输出需要解析 —— 解析是一个会出错的步骤,而且解析失败通常是静默的: 它会得到一个部分正确的结果,然后基于它继续。 这是形状 E 在工具接口上的形态。

第二,错误要可分类,不只是可读。 而且错误的类型应该在结构上 可区分,不是靠错误消息的文字 —— 因为消息文字会变(改一次措辞就变了), 而 Agent 如果靠匹配文字来分类,它会在某次无关的改动后静默失效。

第三,幂等优于回滚。 一个操作如果可以被安全地重复执行, Agent 就不需要知道上一次执行到哪了 —— 而”上一次执行到哪了” 是 Agent 最容易丢失的信息,它可能在中途被打断、上下文被截断, 或者干脆是一个新的会话。小节 9.4.1 里那个”必须幂等”的契约, 在这个视角下有了第二层理由:它不只是为了让观察可复现, 也是为了让操作可以被安全重试。

第四,前置条件必须是显式的。 一个截图工具在没有设备连接时该怎么表现? 返回一张空图是最坏的 —— Agent 会以为界面是空的;报一个泛泛的错误是中等 —— Agent 知道失败了但不知道为什么;报”没有设备连接”才是正确的。 这三种在实现上的差别只有几行代码,而在使用上的差别是巨大的。

第五,失败的语义要和判定层对齐。 一个工具失败了,它是”你用错了” 还是”环境不行”?这个区分就是退出码三分,只不过发生在工具这一层。 如果一个工具在设备没连时和在参数写错时返回同样的错误, 那么 Agent 无法区分”我该改参数”和”我该等设备”——它会去改参数,反复地改。

9.15.1 现实一点:先包装,再重写

上面那五条可能让人觉得要重写所有工具。不必。

正确的顺序是先包一层薄的适配,跑一段时间,再决定哪些值得重写。 包装层做四件事,每一件都是几十行:把文本输出解析成结构化数据; 把退出码和错误消息映射成可区分的类型;在前置条件不满足时明确报出来; 把已知的假象写在这个包装层旁边。

跑一段时间之后,你会知道哪个工具被调用得最频繁 —— 而那才是值得重写的那个。

这个顺序还有一个好处:包装层本身就是一份规格, 它记录了”Agent 需要这个工具的哪些能力”,而重写时这份规格是现成的。

9.16 ⚙️ 小规模怎么做

这一章的投入门槛是全书最高的,但有三件事不需要任何基建。

列出你的 Agent 现在够不着的东西。 不是”应该有的功能”, 是”它现在只能靠猜的东西”—— 典型答案是它看不到界面、 读不到线上日志、跑不了真实的数据库查询。 这份清单通常十分钟就能列完,而它本身就是一份优先级排序, 因为猜得越频繁的地方,工具的收益越高。

每给一个工具,同时写下它的假象。 一句话就够, 但必须写”如果不知道这是假象、去修它会怎样”—— 因为假象的危害不在于它存在,在于有人会把它当成缺陷去修 (小节 9.3)。

给任何一个自动化定时任务,先想清楚验收标准。 也就是”怎么证明它真的做了事”—— 如果答不上来, 那它上线之后大概率会变成那个挂了四个月的崩溃 (小节 9.6)。这一条的成本是三分钟的思考, 而它防住的是一整类不可观测的缺口。

9.17 压成一句话

前三块环境降低 Agent 做错的概率, 这一块决定它能不能知道自己做对了。 “知道自己做对了”这件事,是自主性的全部前提(小节 7.11)。

一个够不着验证手段的 Agent,它的每一次”完成”都是一个猜测 —— 而猜测会被它自己当成事实继续往下推(小节 9.10.1)。 所以工具链的投入不是在提高效率,是在把猜测变成事实。 它的收益曲线在早期递增(小节 9.10.3),原因也在这里: 每多一项能力,就有一批原本只能猜的东西变成了可以查的。

9.18 工具链的一条反直觉建议

最后一条,它和”工具越多越好”的直觉相反: 不要把所有能力都做成工具,有些东西应该保持”人才能做”。 具体是哪些?那些不可逆、且判断依赖外部目标的。

小节 7.4 讲过 Agent 能把发布准备到完全就绪, 但最后按下去那一下始终是人的决定。 这不是”能力不够”,是刻意不给—— 因为那个动作对应的判断在环外(章节 20)。

理由比”安全”更具体:一个工具的存在会创造使用它的压力。 如果发布是一条命令,那么”要不要现在发”这个问题会更容易被跳过 —— 不是因为有人决定跳过,而是因为不跳过需要额外的动作。 所以在不可逆的动作上保留摩擦,是一个设计选择,不是一个遗留问题。

9.19 和第七章的一处矛盾

小节 7.4 讲交付期时说”触发权收敛到唯一入口”, 而这一章讲工具时说”给 Agent 更多能力”。 这两条看起来矛盾,但它们不是 —— 区分它们的是可逆性

动作 该给 Agent 吗
截图、查日志、读状态 —— 只读,零风险
装包、点击、跳路由 —— 在测试环境里可逆
改本地配置、跑迁移(测试库) 给,但要有回滚
打包、上架、改线上配置 不给 —— 不可逆

四行是一条连续的谱,而分界线是”错了能不能挽回”。 这条分界线和路径不变量那边的风险分级用的是同一个判据 (小节 14.2)—— 这不是巧合, 是同一条原则在两个地方的应用。

9.19.1 那”把发布准备到就绪”算哪一类

小节 7.4 说 Agent 能把发布准备到完全就绪的状态, 而”准备”和”执行”之间的那条线,正是这一章的答案: 所有的准备工作都是可逆的—— 生成的截图可以重新生成, 写好的元数据可以改,打好的包可以扔掉 —— 而”提交审核”那一下不可逆。

所以正确的设计不是”限制 Agent 的能力”, 是”在不可逆的那一步保留摩擦”(小节 9.18)。 这个区分让”给 Agent 更多能力”和”收窄不可逆动作”可以同时成立, 而且它们指向同一个方向:把 99% 的工作交出去,把那 1% 留住。

值得强调的是这个 1% 不会随着工具变好而缩小。 它的大小由”哪些动作不可逆”决定,而那是一个关于外部世界的事实 —— 应用商店的审核提交、一次已经跑过的迁移、一封已经发出的邮件, 这些的不可逆性和你的工具链多成熟没有关系。 所以这条边界是稳定的,值得一次性画清楚。