过去写 API 的好习惯,可能正在把我们的 Agent 搞糊涂
把后台接口一对一翻译成 MCP 工具,Agent 拿到的往往是一堵没有界面的按钮墙。从界面约束、有状态的业务 Action 到 MCP 2026-07 新规范,探讨面向 Agent 的工具应该怎样设计。
我们可能从一开始就把 MCP 接错了。
当行业真正开始把 Agent 接进核心业务时,一个反直觉的现象越来越明显:困住 Agent 的,常常不在模型智商,而在过去十年给前端写接口留下的肌肉记忆。
工程团队太擅长把每一步流程写死,却很少给 AI 留出一个表达“我想完成什么”的入口。把后台接口一对一翻译成 MCP 工具,Agent 最后拿到的,往往是一堵没有界面的按钮墙。
MCP 刚出来时,大家的第一反应都顺理成章:家里现成有几百个给网页和 App 用的后台接口,干脆写个脚本,一对一翻译成几百个 MCP 工具,全部挂上去。以前给前端供接口,讲究拆得细、职责单一,两百个接口,就整整齐齐摆出两百个开关。
但在订单、退款、审批这类业务里,大模型更需要的,是几个基于场景触发的业务入口。
当本地 Agent 真正开始接管工作流时,这套沿用多年的“好习惯”暴露了问题:没接后台之前,模型的对话和推理都正常;挂上两百个细粒度工具后,连查单、改状态这样的基础操作都开始频繁出错、反复重试。
模型并没有突然退化。我们只是忘了:大模型不是前端。
为什么原本在 App 里运行顺畅的接口,原样交给 AI 就出了问题?因为中间少了一层看不见的约束:界面。
界面一直在替我们做状态过滤
拿最常见的售后来说。顺着页面逻辑,后端会自然拆出:
- 未发货仅退款
- 已收货退货退款
- 申请换货
- 补退运费
- 修改收货地址
- 查询物流
真实用户在手机上很少点错,因为当订单处于“已发货”状态时,App 前端早就把“未发货仅退款”的按钮隐藏或置灰了。
前端界面,在无形中承担了状态过滤与防呆校验的职责。
一旦把这七八个接口直接翻译成 MCP 工具,藏按钮的界面就消失了。命名近似、职责相邻的接口同时摆在上下文里,模型要在没有任何状态引导的前提下,自己判断先调哪个、后调哪个。
大模型本质上是概率模型。以前前端替业务过滤掉了非法状态,现在这些隐性规则全部交给模型的推理去兜底。上下文干净时,它多半能选对,让人以为系统已经跑通;一旦对话变长、噪声增多,它就可能拿着一笔在途订单,去调用“未发货仅退款”。
演示项目里,这是一次偶发的幻觉;真实业务里,这是一笔资损。
这不只是个人体感。一项针对 116 个官方 MCP Server 的实证研究(From REST to MCP)发现,88.6% 的服务完全或部分依赖 REST API,92% 的服务把工具实现为对原有接口的简单包装。另一项 LongFuncEval 实验显示,随着可用工具数量增加,长上下文模型的函数调用表现会下降,在部分模型上降幅高达 85%(观察到的区间为 7% 到 85%)。
问题当然不在 REST 本身,细粒度接口在微服务架构里仍有不可替代的价值。错位发生在抽象层:细粒度接口适合交给前端,由确定性的页面代码组合出完整流程;而在多步骤的核心业务里,面向 Agent 的工具更应该直接表达当前可用的业务能力、状态约束和下一步动作。
这一点在业界正逐渐成为共识。Anthropic 的工程博客建议把常被连续调用的工具合并成一个,让单个工具在内部完成多次 API 调用;Block 基于 60 多个 MCP Server 的实践,也主张从工作流自顶向下设计,不要直接暴露细粒度接口。
但“合并工具”只回答了一半问题。对于下单、退款、审批这类有状态、有副作用的流程,合并之后的工具应该长什么样?流程中途缺信息怎么办?谁来拦住扣款?超时之后怎么恢复?这是本文想继续探讨的部分。
第一层:先别把整张接口表摆出来
工程界已经开始为“把所有接口平铺给 Agent”这件事做减法。
OpenAI 的 Tool Search 让模型先拿到轻量的工具目录,需要时再把相关工具的完整定义动态加载进上下文,并建议把延迟加载的函数按 Namespace 或 MCP Server 分组。
Grok Bot 和 Muse 的公开方案则更侧重执行环境。Grok Bot 运行在持久化的云端计算机上,通过基于 MCP 的连接器访问外部服务,在本地命令、团队账号、支付或身份验证等环节交还给用户批准或接管;Muse 运行在独立的云端安全环境中,凭据存放在安全存储里、模型不可见,支付使用一次性卡号,购买前会先询问用户。
这些方案切入点各不相同,但指向同一个共识:Agent 不应该在一开始就看到整张后台接口表。
不过,Tool Search 主要解决工具的发现与加载,连接器与受控运行时主要解决操作的执行边界。对于下单、退款、审批、付款这类有状态、有副作用的核心业务,还可以再往前走一步:重新定义 Agent 看到的业务最小单元。
第二层:把 API 串烧变成有状态的 Action
给前端写接口,和给 AI 设计工具,面对的是两种不同的契约。前者把动作拆碎,让页面去编排流程;后者需要把散落在页面里的状态机收回来,围绕一次完整的业务意图,封装成可以推进、暂停、确认和恢复的业务动作(Action)。
以“下订单”为例。在 REST 包装模式下,Agent 需要自己在上下文里串起整条链路:
查询商品 → 选择商品 → 查询地址 → 选择地址 → 查询库存 → 计算运费 → 查询优惠券 → 选择支付方式 → 创建订单 → 创建支付 → 提交支付 → 查询订单
这条链路很脆弱,任何一环的参数偏差或状态不一致,都会让整条链路中断。
如果把它收敛为一个有状态的 CheckoutAction,模型启动动作后,拿到的是一个自包含的业务上下文,其中包括整个流程需要从用户那里获取的信息清单:
{
"action_id": "checkout-123",
"entity": "Order",
"node": "ITEMS",
"items": [
{ "id": "item-1", "name": "商品 A", "quantity": 1 }
],
"addresses": [
{ "id": "address-1", "label": "公司" },
{ "id": "address-2", "label": "家" }
],
"payment_methods": [
{ "type": "balance", "label": "余额" },
{ "type": "card", "label": "银行卡" }
],
"inputs": [
{ "field": "items[].quantity", "node": "ITEMS", "required": true },
{ "field": "address_id", "node": "ADDRESS", "required": true },
{ "field": "coupon_id", "node": "COUPON", "required": false, "depends_on": ["items", "address_id"] },
{ "field": "payment_type", "node": "PAYMENT", "required": true }
],
"allowed_patch": ["items[].quantity", "address_id", "coupon_id", "payment_type"],
"requires_confirmation": false,
"next_actions": [
{ "tool": "advance_action", "description": "补齐或修改 allowed_patch 中的下单信息" }
],
"version": 3
}
这里有一条贯穿全文的约定:每一个响应都必须带上 next_actions,无论是搜索、启动、推进、确认、验证还是恢复。它明确告诉模型下一步可以调用哪些工具、各自是做什么的。next_actions 为空,表示流程已经结束;不在列表里的调用,服务端直接拒绝。模型不需要去工具列表里猜下一步,只需要在服务端给出的选项里做选择。
inputs 描述的是整条工作流的上下游:每个节点需要什么信息、哪些必填、哪些依赖上游的计算结果。Agent 不需要知道背后有多少个微服务,也不需要像网页表单那样一页问一个字段。用户说一句“买两件,寄到公司,用余额付”,模型就能一次抽出所有字段,一次提交:
{
"action_id": "checkout-123",
"version": 3,
"patch": {
"items": [{ "id": "item-1", "quantity": 2 }],
"address_id": "address-1",
"payment_type": "balance"
}
}
服务端收到 Patch 后,按节点顺序依次处理:先改数量、检查库存,再校验地址、计算运费,然后根据新的金额匹配可用优惠券,最后校验支付方式。每一步都由确定性代码完成,能推进多远就推进多远:
{
"action_id": "checkout-123",
"node": "CONFIRM_ORDER",
"applied": ["items", "address_id", "payment_type"],
"missing": [],
"available_coupons": [
{ "id": "coupon-1", "label": "满 300 减 30" }
],
"requires_confirmation": true,
"summary": {
"amount": 398,
"address": "公司",
"payment_type": "balance",
"coupon": null
},
"next_actions": [
{ "tool": "advance_action", "description": "使用优惠券或修改下单信息" },
{ "tool": "confirm_action", "description": "提交订单并扣款", "requires_user_approval": true }
],
"version": 4
}
一次调用,就从起点走到了真正产生资金副作用的边界。available_coupons 是只有算出金额之后才能知道的下游信息,Agent 可以在确认前顺带问用户要不要用;如果用户要用,再提交一次 coupon_id,服务端重新计算后依然停在确认节点。
如果某个字段校验失败,比如库存不足,服务端就停在出错的节点:applied 列出已经生效的字段,validation_errors 说明原因,下游字段保留为待校验,等上游修正后再重新计算。此时 next_actions 里只有 advance_action,confirm_action 不会出现。
这里有两条边界不能破:
- 依赖上游的字段,选项由服务端给出。 运费方式、可用优惠券这类信息依赖商品、数量和地址,模型可以提前提交意向,但最终是否有效,以服务端计算结果为准。
- 无论一次提交多少字段,都不会越过确认节点。
requires_confirmation是硬边界,不会因为信息齐全就被自动跳过。
模型此时还没有触发扣款,它只是把订单推进到了必须由人确认的节点。用户在界面上确认后,Agent 才调用 confirm_action,携带幂等键完成最终提交:
{
"action_id": "checkout-123",
"approval": "approved",
"idempotency_key": "req-20261026-001"
}
提交之后,响应同样告诉模型下一步该做什么:
{
"action_id": "checkout-123",
"node": "PAYMENT_SUBMITTED",
"external_id": "payment-123",
"next_actions": [
{ "tool": "verify_action", "description": "核实支付是否真实到账" }
]
}
关键在于:服务端知道整个工作流的上下游,所以能提前告诉模型总共需要从用户那里拿到什么;模型擅长从自然语言里一次抽出多个字段,所以没有必要照搬网页表单的分步节奏。逐节点推进是服务端内部的校验顺序,而不是模型与用户的对话节奏。
Agent 不必记住流程顺序,只需根据 inputs 一次问齐信息、一次提交,再从每个响应的 next_actions 里选择下一步。整个交互周期可以抽象为:
Search → Start Action → Read Context → Submit Patch → Advance → Confirm → Verify → Recover
这里有一个值得留意的边界:next_actions 告诉模型可以调用哪些平台原语,但底层微服务的 handler 不宜直接暴露给模型。下面这种结构适合留在服务端,作为内部流程定义:
{
"handler": "CONFIRM_ORDER",
"params": ["address", "payment_type", "coupon"]
}
面向模型的接口,则收敛为纯粹的业务状态:
{
"node": "CONFIRM_ORDER",
"missing": ["payment_type"],
"allowed_patch": ["payment_type"],
"next_actions": [
{ "tool": "advance_action", "description": "补齐支付方式" }
]
}
handler 是内部实现细节;node、missing、allowed_patch 和 next_actions 才是模型决策需要的路标。把 handler 透传出去,只是给 Endpoint 换了个名字,模型依然在猜后端该怎么调。
与 MCP 协议演进的关系
写到这里,熟悉 MCP 的读者可能已经注意到:协议本身也在往这个方向走。
2026-07-28 版 MCP 规范是协议发布以来最大的一次修订,其中几项改动和上面的设计直接相关:
- 无状态核心:协议层取消了会话,每个请求自带版本、身份和能力声明。需要跨请求保留的状态,由工具自己生成显式句柄来承载,这正是
action_id和version的角色。 - 多轮请求(Multi Round-Trip Requests):工具调用可以返回
input_required,告诉客户端还需要哪些输入,客户端补齐后携带inputResponses重试。这可以看作missing与patch在协议层的通用版本。 - Tasks 扩展:长时间运行的工具调用可以作为任务执行,客户端通过
tasks/get轮询状态、通过tasks/update推进。支付网关超时、外部审批等待这类场景,天然适合放在这里。 - MCP Apps:服务端可以向客户端提供自己渲染的界面。前文说“界面替人做了状态过滤”,在需要人确认的节点,这层界面现在可以由业务方自己提供。
所以,有状态的业务 Action 并不是要绕开协议另起炉灶,更合理的分工是:
- 协议负责传输、暂停与恢复、向用户索取输入、承载长任务;
- 业务 Action 负责业务语义:当前节点在这笔订单的状态下允许修改哪些字段、哪一步会产生资金副作用、校验失败的原因是什么。
协议能告诉模型“这里还需要输入”,但无法替业务回答“这笔已发货的订单,现在合法的操作有哪些”。后者依然需要业务系统用确定性代码给出答案。
重新设计 MCP 工具的四条原则
1. 从意图找到流程的发起点
Agent 想完成“下单”,不应该在几百个 Endpoint 里猜哪个叫 create_order。平台需要为每条业务流程提供一个触发入口,让 Agent 能从用户的原话出发,找到这条流程从哪里开始。
搜索入口接收的是自然语言意图,而不是 Agent 事先已经知道的能力名:
{
"query": "把购物车里那件商品买两件,寄到公司"
}
平台的能力目录为每条流程维护一段触发描述:什么情况下该用它、适用于哪些业务对象、不适用于哪些情况。匹配可以用 Embedding 检索,也可以结合关键词和模型重排。命中之后,返回的不只是一个名字,还有这条流程的上下游概览:
{
"capability": "checkout",
"trigger": "用户想购买商品并完成支付",
"prerequisites": ["已登录", "购物车或指定商品不为空"],
"inputs": ["items[].quantity", "address_id", "coupon_id", "payment_type"],
"side_effects": ["扣款", "锁定库存"],
"requires_approval": true,
"next_actions": [
{ "tool": "start_action", "args": { "capability": "checkout" }, "description": "启动下单流程" }
]
}
Agent 由此知道:这条流程从哪里发起、需要向用户要哪些信息、哪一步会产生副作用。后面的节点流转,交给 start_action 启动的状态机。
这个结构和 Agent Skill 的渐进加载很像:平时只把一段简短的描述放在模型视野里,命中之后才展开完整内容。换句话说,是把 Skill 的发现逻辑内置进了工具设计。
区别在于展开之后由谁执行。Skill 展开的是写给模型看的操作说明,由模型照着做;业务能力展开的是流程概览,具体的节点推进、校验和副作用由服务端的状态机执行。模型只需要知道全貌,不需要亲手走每一步。
2. 一次返回完整的业务上下文
读取一笔订单时,只返回主表的几行字段往往不够。商品、支付、履约进度、售后资格、当前可执行的操作以及不可操作的原因,更适合在一次读取中交代清楚。
这不是把整张数据库表塞给模型,而是把单次业务判断所需的关系与约束,组装成一个内聚的视图:
{
"entity": "Order",
"status": "SHIPPED",
"refund": { "eligible": false, "reason": "order_has_shipped" },
"requires_approval": false,
"next_actions": [
{ "tool": "start_action", "args": { "capability": "return_refund" }, "description": "申请退货退款" },
{ "tool": "start_action", "args": { "capability": "exchange" }, "description": "申请换货" }
]
}
注意这里的 next_actions 直接指向另外两条流程的启动入口:读取一笔订单的结果,本身就告诉了模型可以从这里进入哪些业务流程。业务系统内部已经有明确的规则,就应该把确定性的判断结果直接交给模型,而不是让它自己关联订单表、支付表和履约表,再靠概率推断能否退款。
3. 围绕意图收敛入口
无论是退钱、退货还是改资料,围绕同一个业务对象,对外可以尽量保留一个表达意图的入口。Agent 只需要说清楚:“为这笔订单申请售后。”
订单是否符合条件、走仅退款还是退货退款、是否需要凭证、是否触发人工审批、后面编排哪些微服务,都由底层的 RefundAction 状态机通过确定性代码分发。
4. 把确认、验证和恢复放进同一个动作生命周期
生产环境里风险最高的操作,集中在扣款、退款、删除数据、变更权限、提交订单、外发文件。这类操作不能止步于“请求已发送”,需要返回可验证的闭环状态:
{
"status": "submitted",
"external_id": "payment-123",
"verification": { "status": "paid", "amount": 199 },
"next_actions": []
}
遇到网关超时这类未决状态,也不该让模型盲目重试,而应给出明确的恢复指引:
{
"status": "unknown",
"recoverable": true,
"action_id": "checkout-123",
"next_actions": [
{ "tool": "verify_action", "description": "先向支付网关核实结果,不要重新提交" },
{ "tool": "recover_action", "description": "核实确认未支付后,按原幂等键重试" }
]
}
一个面向生产环境的业务 Action,通常至少要维护这些字段:action_id、current_node、version、inputs、missing、allowed_patch、applied、next_actions、requires_confirmation、verification、recoverable_errors。它本质上是一个轻量级状态机:业务系统掌握状态流转的控制权,Agent 负责当前节点的信息补全与交互决策。
给 Agent 换一副骨架
底层按这套思路重构后,平台向 Agent 暴露的工具集可以收敛为少数稳定的原语:
search_capabilities:定位业务能力入口start_action:初始化业务动作实例get_context:读取业务实体视图,或在中途恢复会话时重新拉取当前节点状态与约束advance_action:提交 Patch,推动节点流转confirm_action:拦住产生副作用的审批关口verify_action:核实外部系统是否真实完成recover_action:处理超时、悬挂与幂等重试
后台依然可以保留几百个 REST、RPC 和微服务接口,只是不再直接出现在模型的决策视野里。模型看到的是业务动作,水下运行的是确定性的流程代码。
这里有一个绕不开的质疑:advance_action(action_id, patch) 本身也是一个通用接口,模型要到运行时读 allowed_patch 才知道能填什么,这和把两百个工具塞进一个万能的 execute() 有什么区别?
区别在于约束从哪里来。execute() 把参数拼装和流程编排都留给模型,接口本身不提供“现在能做什么”的任何信息;advance_action 的每一步都由服务端返回当前节点、缺失字段、允许修改的范围和下一步可调用的动作,非法的 Patch 会被确定性代码拒绝,并附带原因。
代价也确实存在:工具定义本身失去了静态的类型化 Schema,从工具列表里看不出每个 Action 需要哪些字段。一个折中做法是让 get_context 同时返回当前节点 Patch 的 JSON Schema,把类型约束从工具定义挪到运行时上下文里。
真正需要压缩的,是模型在每一步需要理解的决策空间。
回看近期的探索,几条脉络正在交汇:
- Tool Search 让工具定义按需加载;
- Connector 与受控运行环境 让外部操作在安全边界内执行;
- MCP 协议本身开始提供多轮输入、长任务和服务端界面;
- 有状态的业务 Action 把交易、审批变成可推进、可暂停的状态机。
这几层叠加起来,或许更接近一套生产级的 Agent 接入架构。
如果只看“Agent 怎样从一句话走到一笔完成的订单”,这几层其实是一条递进的链路,每一层回答一个问题:
- 找到工具:Tool Search 让模型在大量工具里按需发现、按需加载;
- 找到流程,看清上下游:触发入口让模型从用户意图出发,定位流程的发起点,并提前知道需要哪些信息、哪一步有副作用;
- 把流程走完:状态机负责节点推进、校验、确认、验证和恢复。
第一层是工具层面的检索,第三层是业务层面的执行。中间这一层最容易被忽略,它决定了模型拿到工具之后,是在猜流程,还是在看着流程图做事。
顺着这条线,还有一个我没有完全想清楚、但值得讨论的问题:流程知识应该放在哪一侧?
一种做法是放在模型侧,写成 Skill:用自然语言描述“下单时先确认地址、再选支付方式”,由模型照着调用工具。另一种是放在服务端,做成业务能力:流程定义和状态机都在平台内部,模型只读概览、提交信息。
两者各有适用的地方。跨多个系统、没有单一业务方负责、步骤以读取和整理为主的流程,写成 Skill 更灵活,改起来也快。单一业务系统内部、有资金或数据副作用、规则需要强制执行的流程,放在服务端更稳妥,因为 Skill 里的规则只是建议,状态机里的规则是约束。
现实中可能两者并存:Skill 负责跨系统的编排和对话策略,业务能力负责每个系统内部不能出错的那一段。两者的边界划在哪里,或许会是 MCP 走入核心业务之后,工程团队需要反复权衡的问题。
当模型能力持续提升,接口数量不再是壁垒。真正的工程壁垒,在于能否把复杂业务抽象成少量稳定的动作原语,同时在底层兜住状态、权限、审批、验证和异常恢复。
不妨用这个问题重新审视现有的代码库:如果把所有依赖前端界面遮挡才不会出错的碎接口收回平台内部,真正需要暴露给 Agent 的业务开关,还剩几个?
也许最后留在模型视野里的,不再是两百个 Endpoint,而是七个清晰的阶段:
查清能力、发起动作、补齐信息、推进节点、确认提交、验证结果、恢复异常。
回头看,这个方向其实有点回到了 GraphQL 的思路:不再围绕一个个 Endpoint 组织能力,而是围绕业务实体和它们之间的关联建模,让调用方——不管是前端还是 LLM——按自己的意图,在服务端划定的边界里自由组合。不同的是,GraphQL 主要回答“要哪些数据”,而 Agent 还需要服务端告诉它“现在能做什么、下一步做什么”。实体视图之上,还要叠一层状态与动作的约束。
少给 Agent 塞一点碎接口,让它直接看到业务本身。这也许是 MCP 走入深水区之后,值得继续探索的方向。
参考资料
- OpenAI, Tool search: https://developers.openai.com/api/docs/guides/tools-tool-search
- Anthropic, Writing effective tools for agents — with agents: https://www.anthropic.com/engineering/writing-tools-for-agents
- Block, Block's Playbook for Designing MCP Servers: https://engineering.block.xyz/blog/blocks-playbook-for-designing-mcp-servers
- Model Context Protocol, The 2026-07-28 Specification: https://blog.modelcontextprotocol.io/posts/2026-07-28/
- xAI, Grok Bot: Computer and Apps: https://docs.x.ai/grok-bot/computer-and-apps
- Meta, Introducing Muse: https://about.fb.com/news/2026/09/introducing-muse-personal-ai-agent/
- Mastouri et al., From REST to MCP: An Empirical Study of API Wrapping and Automated Server Generation for LLM Agents: https://arxiv.org/abs/2507.16044
- Kate et al., LongFuncEval: Measuring the Effectiveness of Long Context Models for Function Calling: https://arxiv.org/abs/2505.10570
(完)