给 AI Agent 造锤子:复杂系统的 Tool 设计框架
一台智能巡检设备、一条产线、一个外卖系统------它们向 AI Agent 开放能力时,面临的是同一道题。
引子:当复杂系统对 AI Agent 开放时
复杂系统在设计之初,能力通过 API 对外暴露。这些 API 默认了一个前提:调用者是人------读文档、理解参数、处理返回值、写 try-catch、调试。人会猜,会查 StackOverflow。
但当调用者从人变成 AI Agent,这套 API 就不够用了:
- 人靠经验填补接口漏洞。 文档没写的边界情况,人凭常识兜底。Agent 不会------漏一个状态就缺一个分支。
- 人能忍受模糊的返回值。
{ status: "failed" }不足以写程序,但人会翻日志、查上下文、试着重试。Agent 拿不到足够信息,很难选对下一步。 - 人能理解隐式约束。 "这两个接口不能同时调",人看一眼就懂。Agent 需要它被显式写出来。
本质不是"API 好不好用",而是系统能不能被 Agent 安全、可靠地编排。智能巡检设备要编排巡检-识别-取证-回基站,外卖平台要编排下单-支付-分仓-配送,产线要编排排产-投料-加工-质检------底层各不相同,但暴露的问题是同一套:状态怎么建模?失败怎么表达?安全边界谁兜底?调用方需要知道多少底层细节?
一个复杂系统如何为 AI Agent 重新设计能力接口:Agent 应该看到什么、不该看到什么、每次调用之后能确认什么。
下面从三种可选路径讲起,再看设计原则、落地方法、常见反模式和验证方式。
一、为什么 API 直接丢给 Agent 不行
1.1 旧方案的典型做法
目前常见的做法是:把所有 API 的 JSON Schema 和文档都交给大模型,让它自己理解、自己编排。
ini
tools = [route_upload, route_start, route_pause, move_to,
sensor_aim, media_capture, detect_objects, speaker_play,
// ... 还有几十个]
system_prompt = "你是一个智能巡检设备控制系统。完成本次巡检任务。"
Agent 需要自己把这些原子 API 串成:上传路线→启动巡检→识别目标→暂停路线→移动到目标附近→调整观测→拍照取证→播报→恢复路线→回基站。每一步出错还要自己处理恢复、释放资源、跳过目标和安全收尾。
换成电商系统,API 列表变成 inventory_lock、payment_charge、warehouse_notify,Agent 同样需要自己串下单-支付-履约-通知。载体不同,麻烦相通。
这个方案有三个独立的问题。
1.2 问题一:步骤数 n 指数级吞噬成功率
每一步都可能出错。Agent 每多编排一个步骤,出错概率就乘一次:
css
总成功率 = P(步骤1) × P(步骤2) × ... × P(步骤n)
假设每步成功率 99%(对线上系统已经很高),编排 50 步后 0.99^50 ≈ 60%。每步 95%(链路波动、目标追踪不稳定时更真实的估计),30 步后直接跌到 21%。
步骤越多,整体成功率越低。这不是 prompt 优化能解决的,问题出在步骤数本身。
1.3 问题二:原始数据侵占上下文,Agent 被迫自己做信息压缩
概率公式只说"会不会出错"。还有一个更隐蔽的问题:原子 API 返回的原始数据占据大量上下文,Agent 必须自己从里面提取有效信号。
以"判断巡检画面里是否存在需要处理的静止目标"为例。原子 API 的返回:
json
vision.read_raw_detections(area_id="A-03") →
{ "frames": [
{ "frame_id": 48291, "detections": [
{ "class": "vehicle", "bbox": [0.42,0.31,0.18,0.12], "confidence": 0.87,
"track_id": 3, "velocity_m_s": 0.03 },
{ "class": "person", "confidence": 0.65, "track_id": 9, ... }
] },
// ... 152 帧
] }
Agent 需要遍历 152 帧,过滤 class、按 track_id 去重、判断静止时长、排除边缘数据。这些逻辑全跑在 Agent 的上下文里。 152 帧原始数据加十多个字段,都在吃 tokens。
而一个被正确抽象的 Tool:
json
target_recognition.list_unprocessed(only_when="stationary_confirmed") →
{ "count": 2, "targets": [
{ "id": "T-003", "type": "vehicle", "stationary_duration_s": 73,
"can_drive_movement": true },
{ "id": "T-007", "type": "vehicle", "stationary_duration_s": 91,
"can_drive_movement": true }
] }
Tool 内部完成了检测→多帧确认→去重→静止判断→质量过滤。Agent 不需要知道 bbox、confidence、track_id。Tool 用少量语义化目标,替代了 152 帧原始数据。
同样的压缩发生任何场景:目标检测的检测框坐标由 Tool 内部处理完,Agent 只拿 has_target: true;PLC 传感器时间序列由 Tool 内部聚合为"设备健康度"。
这就是 Tool 抽象的核心价值:信息压缩。 原始数据先在 Tool 内部变成语义信号,Agent 的上下文才不会被塞满。
1.4 问题三:隐式约束和状态依赖全部压在 Agent 身上
原子 API 之间不是独立调用的------它们有隐式的依赖、时序和互斥关系:
- "先暂停路线,确认暂停成功,才能接管移动控制"------但
route_pause和move_to是两个独立 API,没有东西阻止 Agent 在路线仍运行时发移动指令 - "观测模块未锁定时拍照可能没有目标"------Agent 可能还没锁定就拍照
- "同一目标处理需要幂等"------Agent 可能在网络超时后重试,导致重复取证
人类开发者凭经验能感知这些约束。Agent 不会------除非约束被显式写在接口描述里。原子 API 的 JSON Schema 不会写这些东西。
一个典型的编排片段:
scssroute_pause() → 返回 ok move_to(target) → 超时,Agent 重试 move_to(target) → 返回 ok(设备可能已偏离原路线) sensor_aim(target_id) → 返回 ok(但目标已丢失) media_capture() → 返回 ok(拍到的不一定是目标) speaker_play("请驶离") → 返回 ok route_resume() → 返回 failed(断点不可恢复) // Agent 以为一切正常,实际取证错位+重复移动+路线无法恢复
每一步单看都"成功",但整个流程并没有成功。
1.5 小结:同一个根因的三个面孔
这三个问题------成功率的指数衰减、原始数据侵占上下文、隐式约束不可见------指向同一个根因:
原子 API 把系统内部的复杂性转嫁给了 Agent。Agent 被迫自己管理状态、压缩信息、推断约束。
这不是 prompt 工程能解决的,是接口设计层面的问题。
二、三种可选路径
将复杂系统开放给 Agent,大体三条路。
路径 A:全原子 API + 更强 prompt(继续旧方案)
做法:不改造 API,靠优化 prompt、增加 few-shot 示例让 Agent 更好地编排。
优点 :零适配成本。 缺点 :第一章三个问题一个没解决。 适用:API 幂等、无状态依赖、容错性高的场景(查数据库、发消息、调 SaaS)。
路径 B:预定义工作流 + Agent 只填充参数
做法:人预先定义完整工作流模板。Agent 不参与编排,只在特定判断点介入(如"这张图的目标是否异常")。
优点 :可靠性最高,不可能编排出不安全的动作。 缺点 :每加一个新场景就要写一个新工作流。只能应对"一个场景跑一万次"。 适用:场景固定、流程稳定、容错要求极低(产线、固定巡检路线)。
路径 C:高阶有状态 Tool + Agent 自由编排
做法:系统暴露的不再是原子 API,而是高阶、有状态的 Tool。Tool 内部封装状态机、信息压缩、安全门和资源互斥。Agent 在 Tool 的能力边界内自由编排。
优点 :自由度与安全性的折中。常规任务可用路径 B 的工作流跑,全新任务用高层 Tool 让 Agent 自己组合。 缺点:Tool 设计投入最大。
scss
三条路径对照:
自由度 ↑
│
路径 A │ 路径 C
(全原子API) │ (高阶有状态Tool)
│
│ 路径 B
│ (预定义工作流)
│
└──────────────────→ 可靠性
低 高
适用场景
这个框架不绑定某一种设备载体:
| 领域 | Agent 编排什么 |
|---|---|
| 智能巡检/机器人 | 巡检→识别→靠近→取证→处置→回基站 |
| 微服务系统 | 下单→支付→分仓→拣货→配送→签收 |
| 工业自动化 | 排产→投料→加工→质检→包装→入库 |
| 金融系统 | 核验→风控→复审→放款 |
| 医疗设备 | 扫描→增强→标注→生成报告→推送 |
| 运维/DevOps | 健康检查→摘流→更新→验证→回滚/引流 |
底层都在处理同一组东西:状态、资源、时序、失败、安全。
三、Tool 设计的核心原则
总原则:职责分界------"这件事该归谁"
状态管理、安全校验、信息压缩、业务判断、流程编排------这些事分别该由谁负责?答案是按"谁最有能力处理"来分配:
| 复杂度类型 | 归谁 | 为什么 |
|---|---|---|
| 资源互斥、操作时序 | Tool 内部 | 确定性逻辑,让不可靠的调用方来管就是灾难 |
| 原始数据→有语义的信号 | Tool 内部 | 不要把 Agent 上下文浪费在原始数据上 |
| 安全底线(安全区域、电量阈值、幂等) | Tool 内部 / 系统边界 | 不能依赖任何人"记得检查" |
| 业务判断(是否可疑、优先级、是否人工介入) | Agent / 上层编排逻辑 | 随场景变化,Tool 不该理解 |
| 流程编排 | Agent / 工作流 / 策略引擎 | 跨 Tool 的编排,Tool 不该知道 |
| 环境状态(负载、并发数、降级开关) | 系统只读视图 | 服务于编排决策,但不触发动作 |
一条总则:确定性逻辑下沉到 Tool,不确定性判断留给 Agent 或上层编排逻辑,安全底线由系统边界或 Tool 契约强制执行。层次放错了,系统要么不可靠,要么不可复用。
下面四条原则是这条总则的展开。
原则 1:状态优先于动作
当一条巡检路线状态是"已完成",Agent 调了 pause()。该不该让它暂停?
答案是:取决于当前状态。不定义状态机,Agent 就只能猜------"已完成"的路线调暂停,到底是拒绝、重新进入运行状态、还是静默忽略?只列方法不定义状态,Agent 就只能猜。
正例:
lua
route.pause() / route.resume() 的状态迁移:
idle → pause_rejected("当前没有运行中的路线")
running → paused(暂停成功,释放路线控制权)
pausing → already_pausing(正在暂停,等待稳定)
paused → already_paused(已暂停,可继续处理目标)
completed → already_completed(路线已完成,进入收尾)
failed → resume_rejected(需回基站或人工接管)
Tool 返回值不仅含状态码,还告诉 Agent "接下来可以做什么":
json
// pause() 在 completed 状态下返回:
{ "status": "already_completed",
"suggestion": "cleanup_and_return_to_base" }
Agent 拿到就知道:不能重试暂停,该进入收尾流程。
原则:所有动作都必须绑定状态迁移。没有状态契约的动作接口是不完整的。
原则 2:可预测接口
人类遇到模糊的返回值会查日志、看上下文、试着重试。Agent 没有这些手段------它只能根据你给的接口描述做分支判断。
Tool 的接口必须满足:输入字段稳定、类型严格、单位明确;合法范围明确;输出状态可穷举;每个状态有稳定的 data 结构;错误不靠隐式 exception 表达;降级不伪装成成功。
反例:
json
{ "status": "failed", "message": "Something went wrong" }
Agent 不知道是位置越界、传感器降级、控制权未释放还是链路超时。四种情况处理方式完全不同。
正例:
json
{ "status": "rejected", "reason": "out_of_safe_zone",
"recoverable": false,
"suggestion": "skip_target_and_resume_route",
"system_state": "route_paused" }
Agent 不需要猜------recoverable: false 告诉它不能重试,suggestion 告诉它下一步该跳过目标。
原则:接口的目标不是"容易调用",而是"没有意外"。
原则 3:信息压缩
第一章 1.3 节已经展开讲过。这里只强化一条设计准则:
Tool 的返回值应该是 Agent 可以直接用于决策的语义信号,而不是需要 Agent 二次处理的原始数据。
判断标准很简单:Agent 拿到返回值之后,是直接可以写 if-else,还是需要先自己做一遍过滤、聚合、去重? 如果是后者,这个 Tool 就没有完成信息压缩。
原则:不要把原始数据直接扔过 Tool 边界。
原则 4:安全第一
Agent 有时候会给出超出安全边界的指令------不一定是 Agent "错了",可能是它缺少信息,也可能是模型幻觉。
无论原因是什么,安全边界不能依赖 Agent 自觉。它必须是在 Tool 框架或 Tool 内部契约中强制执行的、Agent 无法绕过的硬约束。
正例:
bash
Agent 调用:move_to(lat, lon, alt)
安全约束自动执行:
├── 安全区域检查 → 目标在安全区域外 → 拒绝,返回 out_of_safe_zone
├── 高度/距离检查 → 超出设备能力 → 拒绝,返回 movement_limit_exceeded
├── 电量检查 → 不足以完成动作并返回 → 拒绝,返回 low_power
├── 资源互斥检查 → 路线控制权未释放 → 拒绝,返回 route_still_running
├── 幂等检查 → 同一目标正在处理 → 拒绝,返回 target_already_processing
└── 全部通过 → 放行
# Agent 拿到的拒绝结果是结构化的:
{ "status": "rejected", "reason": "out_of_safe_zone",
"suggestion": "skip_target_and_resume_route" }
这些检查不能被绕过,也不能靠 prompt 提醒 Agent "记得先校验"。
尤其要避免三种假安全:
- 静默修正:擅自把不合法参数改成合法值但不告诉 Agent
- 静默忽略:返回成功但实际没执行
- 静默降级:用看似成功的状态掩盖实际失败
原则:当 Agent 的指令和安全规则冲突,以安全规则为准。Agent 可以请求,系统必须校验。
四条原则的层次关系
markdown
总原则:职责分界
│
├── 原则 1:状态优先 ──→ 状态管理归谁
├── 原则 2:可预测接口 ──→ 边界契约怎么定
├── 原则 3:信息压缩 ──→ 数据怎么过边界
└── 原则 4:安全第一 ──→ 什么绝对不能过边界
四、从业务到 Tool:设计方法
第三章给了原则。这一章回答"怎么用"------从业务出发反向推导出 Tool。
以下用一台具备移动、观测、识别能力的智能巡检设备为例。它有物理状态、资源互斥和安全边界,这些问题在很多复杂系统里都会出现。
4.1 出发点:业务场景,不是 API 列表
不要打开现有 API 文档问"怎么包装"。反过来------看着业务场景问"Agent 需要什么能力"。
而且不是所有 Tool 都必须是高度复合的。一个 Tool 只返回布尔值------object_detector.has_target(type) → true------如果这个粒度正好匹配 Agent 的决策需要,就完全合理。目标不是造最复杂的锤子,是造业务刚好需要的锤子。
4.2 列出全部场景的能力需求
把当前 + 可预见的新增场景全部摊开,每个场景下 Agent 需要"能做什么"------用语义描述,不涉及具体实现。
markdown
场景 A:标准巡检
- 按预设路线移动、中途暂停和恢复
- 沿途拍摄、检测指定类型目标
- 结束后返回出发点
场景 B:定点核查
- 移动到指定位置、锁定并持续观测目标
- 判断目标是否长时间静止(≥N 秒)
- 多角度留存影像,完成后回到主路线
场景 C:告警响应(未来规划)
- 接收外部告警,中断当前任务,导航到告警位置
- 扫描周围目标,记录现场画面并回传
- 处理完毕后恢复原任务
三个场景已覆盖"常规任务""子任务干预""紧急打断"三种编排模式。
4.3 能力归并:聚合成正交的 Tool
把所有能力按职责聚类。同一类归一个 Tool,不同类拆开。
首要约束:新增场景时只新增 Tool 或参数,不拆散重组已有。
arduino
原始能力池 →
RoutePlanner → 路线生命周期(执行、暂停、恢复、停止)
MoveController → 一次性空间移动(移动到指定点、偏移移动)
DeviceControl → 全局设备控制(启动、回基站、急停)
MediaStorage → 所有"记录"能力(拍照、录像、媒体查询)
TargetRecognition → 所有"看到什么"及目标生命周期
TargetObservation → 锁定并观察指定目标
AlertResponder → 告警响应的事件入口和状态管理
正交性检验 :RoutePlanner 管路线生命周期,MoveController 管单次空间移动------不重叠。未来加"定点持续监控"场景不需要新 Tool,编排现有 Tool 即可。
反面示范 :把"中断当前任务→导航到告警位置→恢复原任务"塞进 RoutePlanner,理由是"都是路线相关的"。这在当前三个场景能跑通,但多设备协同巡检时------告警来了该谁去?协调器无法独立控制"谁去响应告警",因为告警响应逻辑焊在了路线管理里。
归并的铁律:如果两个能力在未来可能被不同的调用方独立控制,它们就不该在同一个 Tool 里。
4.4 扩展性检验
用可预见的未来场景反向拷问:
markdown
假设新增"多设备协同巡检"场景(两台设备分工覆盖不同区域):
- 需要改 RoutePlanner 吗?→ 不需要
- 需要改 TargetRecognition 吗?→ 可能需要新增 area_id 参数
- 需要新增 Tool 吗?→ 需要一个 CoordinationManager
- 现有 Tool 需要拆散重组吗?→ 不需要
如果答案是"需要拆散重组"------说明当前粒度有问题。
理想状态:新增场景 = 扩展参数 + 少量新 Tool,而不是推翻已有契约。
4.5 用第三章原则校验每个 Tool
以 RoutePlanner 为例逐一过四原则:
原则 1(状态优先)--- 状态机完整吗?
scss
RoutePlanner 状态迁移:
idle ──start(route)──→ running
running ──pause()────→ paused
paused ──resume()───→ running
running ──complete───→ completed
running ──stop()─────→ stopped
running ──error──────→ failed(携带原因:obstacle/link_lost/hardware)
paused ──stop()─────→ stopped
any ──emergency──→ aborted(安全抢占,不可恢复)
每个动作在每种状态下都有明确定义。
原则 2(可预测接口)--- 返回值能直接写 if-else 吗?
json
route.start(route_id) →
{ "status": "running", "handle": { "route_id": "R-042",
"current_waypoint": 3, "remaining": 15 } }
route.pause() →
{ "status": "paused", "resumable": true,
"current_position": { "x": 12.3, "y": 45.6 } }
// 或
{ "status": "already_completed", "resumable": false,
"suggestion": "release_route_and_return" }
原则 3(信息压缩)--- 返回值是语义信号吗?
RoutePlanner.status() 返回 { "status": "running", "waypoint": "3/18" },不把电机转速、IMU 数据全抛出来。Agent 需要的是"跑到哪了",不是"每个轮子转多快"。
原则 4(安全第一)--- 安全门在哪?
scss
安全门检查:
├── 电量 < 安全阈值 → 拒绝 start(),返回 low_power_abort
├── 正在执行另一条路线 → 拒绝 start(),返回 route_already_active
├── 目标超出安全边界 → MoveController.move_to() 返回 out_of_safe_zone
└── 通信中断 → 自动安全停止,系统状态 → aborted
TargetRecognition 是轻量 Tool,但不意味着可以跳过原则校验------只意味着每个维度的答案更短:
原则 1(状态优先):
scss
stopped ──start(types)────────→ running
running ──mark_processed(id)──→ running (更新目标生命周期)
running ──stop()──────────────→ stopped
状态不复杂,但必须明确:start() 在 running 下返回 already_running;mark_processed(id) 只改变目标生命周期,不偷偷释放其他资源。
原则 3(信息压缩)--- 原子 Tool 很容易在这里翻车:
json
// 初版(违反原则 3)
recognition.scan() →
{ "detections": [
{ "class": "vehicle", "bbox": [100,200,300,400], "confidence": 0.87, "track_id": 3 },
{ "class": "vehicle", "bbox": [500,150,700,350], "confidence": 0.92, "track_id": 7 },
// ...
] }
校验发现 Agent 需要自己过滤 class、判断置信度、理解 track_id → 违反原则 3。回退修改。
修正后:
json
recognition.has_target("vehicle") → true
recognition.list_unprocessed() → { "count": 2, "ids": [3, 7] }
recognition.mark_processed(3, result) → { "status": "ok" }
每个返回值都是语义信号。
原则 4(安全第一) :scan() 调用频率超限时拒绝,返回 rate_limited;设备不在安全状态时返回 device_not_ready。
原子 Tool 的校验清单和复杂 Tool 完全一样,只是每项答案更短。但任何一项答不出------就是设计缺口。
4.6 场景覆盖验证
回到 4.2 的三个场景,用设计出的 Tool 走一遍。
场景 A(标准巡检):
scss
Agent 调用 RoutePlanner.start(route_a)
→ waypoint_reached 事件 → MediaStorage.photo()
→ TargetRecognition.has_target("vehicle") → false,继续
→ 循环直到 route complete → DeviceControl.return_to_base()
✅ 跑通。
场景 C(告警响应):
scss
Agent 收到 AlertResponder.on_alert(alert)
→ RoutePlanner.pause() → MoveController.move_to(alert.location)
→ TargetRecognition.scan() → MediaStorage.photo(tags=["alert", alert.id])
→ RoutePlanner.resume()
✅ 跑通,不需要改任何已有 Tool。
场景 B(定点核查)--- 初版跑不通的情况:
假设归并时把路线、移动、观测、拍摄全压进一个大 Tool MegaPatrol.inspect_target(target_id)。跑是跑通了,但这个 Tool 把路线控制、移动、观测、媒体记录焊在一起。未来想多角度取证或让告警响应复用"移动到目标"能力,都必须改这个大 Tool。
回退修正:把能力拆回正交 Tool,上层编排逻辑自行组合:
scss
Agent / 上层编排逻辑:
→ TargetRecognition.on_detect(...) 收到 stationary_confirmed 目标
→ RoutePlanner.pause()
→ MoveController.move_to(target_location)
→ TargetObservation.observe(target_id)
→ MediaStorage.photo(tags=["evidence", target_id, "front"])
→ 需要多角度留证:
MoveController.move_to(angle_2) → TargetObservation.observe(target_id) → MediaStorage.photo()
MoveController.move_to(angle_3) → TargetObservation.observe(target_id) → MediaStorage.photo()
→ RoutePlanner.resume()
✅ 每个 Tool 职责单一,复用边界清楚。多角度取证如果经常出现,可在上层沉淀为可复用 workflow,但不该塞进媒体存储 Tool。
跑不通 → Tool 有缺口 → 回到 4.2 补能力、4.3 重归并。能闭环才算设计完成。
先看业务要什么,再把能力归并成正交 Tool,用原则校验,用场景验证,修缺口,再验证。
五、验证:怎么知道 Tool 设计对了
抽象最终要回到真实工作流里验证:
1. 端到端闭环测试
用一个完整工作流跑一遍:启动→主循环→接收事件→处理中间失败→记录结果→恢复主流程→收尾→上报。重点检查:中间失败后能不能恢复?中断后能不能接续?
2. 场景切换测试
换一个业务场景(如从"标准巡检"换成"定点核查"),Tool 要不要改?需要大改说明 Tool 里有业务语义泄漏。只需换上层编排逻辑或新增少量 Tool,设计合格。
3. 失败路径完备性
随机选一个 Tool、一个状态、一个动作,问:"如果这一步失败,Agent 的下一步是什么?" Agent 能直接从返回结果拿到答案而不是靠猜------设计合格。
总结
当一个复杂系统要向 AI Agent 开放能力时,不应该把原子 API 文档丢给 AI 让它自己编排,也不该把所有流程写成死工作流。正确的方式是设计一套高阶、有状态的 Tool,让 Agent 在 Tool 的能力边界内自由编排,而 Tool 或系统边界封装状态机、安全门、信息压缩和资源互斥。