MCP(Model Context Protocol)这几年在 agent 开发里出现频率很高,尤其是把工具能力通过 MCP server 暴露给大模型之后,很多自动化流程都开始尝试用 agent 来串联。真正落地时你会发现,工具能调通只是起点,业务能不能闭环才是关键。就拿 payment flow 来说,agent 可能已经完成了选单、算价、发起支付,但最后一步需要 signer 签名确认,而 signer 不可达。很多人的第一反应是调大超时、重试几次,结果可能越试越乱。这篇文章从 MCP 工具设计、状态管理、异步审批、重试幂等和排查链路几个角度,把这类问题拆开讲清楚。如果你正在做 MCP server、agent 自动化流程,或者马上要接支付类业务,这篇应该能帮你少走几段弯路。
1. 先判断:agent 能发起支付,不代表能完成支付
1.1 三个角色先分清:agent、MCP server、signer
在 MCP payment flow 里,至少要分清三个角色。
第一个是 agent。agent 是决策方,它根据用户指令、上下文以及 MCP 工具描述决定下一步调用什么。它不直接操作支付系统,也不直接拿签名私钥。
第二个是 MCP server。MCP server 是能力提供方,把支付创建、状态查询、签名确认等能力封装成标准工具,暴露给 agent。它负责连接底层业务系统,也是排查问题的第一现场。
第三个是 signer。signer 表示最终给支付单签字确认的角色。在实际系统里,它可能是一个人,比如财务审批人;也可能是一个签名服务,比如持有签名私钥的后端节点;还可能是多签流程里的一个环节。不管哪种形态,只要 signer 不可达,支付单就只能在中间状态卡住。
很多刚接触 MCP 的人会把这三者混在一起看。看到 agent 调了工具、MCP server 也返回了结果,就以为流程走通了。实际上,MCP server 返回“请求已提交”,和 signer 真正完成签名,是两件完全不同的事。
1.2 signer 不可达是哪一种不可达
不要笼统地说“signer 不可达”。不可达有很多种,处理方式完全不同。
如果 signer 是一个远程服务,可能是网络不通、服务超时、HTTPS 证书异常、签名机离线、接口返回 5xx。这种情况一般可以通过重试恢复。
如果 signer 是一个人,那“不可达”可能是审批人不在电脑前、没有登录审批后台、消息推送被忽略、审批链上串了一个离职账号。这种情况靠 MCP server 重试意义不大,需要的是通知、升级和等待机制。
如果 signer 是一个内部审批流,那“不可达”可能是审批单没有正确投递、回调地址没有配置、权限组设置错误、审批表单填错导致无法提交。
我建议在 MCP 工具返回结果里,把不可达原因区分开。不要把“网络超时”和“审批人未处理”混成一个错误码。否则 agent 即使重试,也不知道该改成什么策略。
下面是一个简单的分类参考:
| 不可达类型 | 典型表现 | 是否建议立即重试 |
|---|---|---|
| 网络不可达 | 连接超时、DNS 解析失败 | 可短时重试 |
| 签名服务超时 | 请求发出但超过响应时间 | 需看幂等策略 |
| 审批人未处理 | 审批单 pending,无人动作 | 不要密集重试 |
| 审批链路配置错误 | 审批人不存在、角色缺失 | 先修复配置 |
| 签名密钥不可用 | HSM 离线、私钥被锁定 | 等运维恢复 |
判断清楚类型之后,MCP 工具的设计才有依据。
2. 把支付流当成状态机,别当成一次函数调用
2.1 为什么 MCP 工具不适合直接做长事务
一个常见的错误设计,是把“创建支付并等待签名”塞进同一个 MCP 工具里,让 agent 调用一次,然后 MCP server 同步等待 signer 返回。
这个方案看起来很直接,但会带来几个问题。
第一,MCP 工具调用通常是有超时限制的。signer 如果是人工审批,可能几分钟甚至几小时都不处理,你不可能让一次 MCP 调用一直挂着。
第二,大模型在等待期间会产生额外的上下文压力。如果工具调用迟迟不返回,agent 可能在一次对话里反复触发相同请求,产生重复支付单。
第三,长事务会让日志和错误信息变得很难判断。某次失败到底是因为签名失败、审批拒绝,还是仅仅因为等待超时,很难从单个返回结果里看出来。
所以,我一般会把 payment flow 设计成多状态流转,而不是单个工具函数。MCP server 负责创建支付单并记录当前状态,signer 的处理结果通过异步方式回写,agent 再通过状态查询接口拿到最终结果。
2.2 最小状态集合怎么设计
支付单的状态不需要设计得很复杂,但至少要覆盖整个生命周期。
可以这样定义:
| 状态 | 含义 | 谁负责推动 |
|---|---|---|
| CREATED | 支付单已创建,等待提交给 signer | MCP server |
| PENDING_SIGNER | 已提交给 signer,等待签名/审批 | signer |
| SIGNED | signer 已完成签名/审批 | signer 回调 |
| EXECUTING | 正在执行打款或入账 | 支付系统 |
| SUCCEEDED | 支付完成 | 支付系统 |
| FAILED | 支付失败 | 支付系统 |
| CANCELLED | 支付单作废 | 管理员或 agent |
这里最重要的是 PENDING_SIGNER。它不是一个错误状态,而是一个合法的中间状态。agent 拿到这个状态时,不应该立刻报错,也不应该盲目重试,而是应该把任务挂起来,等待 signer 状态更新。
2.3 幂等键是支付流的第一条安全线
支付类操作最怕重复执行。MCP agent 在遇到超时、连接重置、上下文被截断时,很可能对同一个操作发起多次调用。如果工具没有幂等保护,就会生成多笔重复支付。
所以,在 MCP payment flow 里,我建议每个创建支付的请求都带一个 idempotency_key。这个 key 可以由上游业务单号生成,也可以由 agent 在第一次创建时生成。
一个简化的工具输入结构可以是:
{ "tool": "create_payment", "input": { "idempotency_key": "po-2025-0128-001", "amount_cents": 1999, "currency": "CNY", "payee": "supplier-001", "description": "第一批测试采购付款" } }MCP server 收到这个请求后,先查幂等键。如果 key 已经存在,直接返回已有支付单信息,不再创建新记录。这样才能保证,即使 agent 因为 signer 不可达而重试,也不会产生重复支付单。
注意:幂等键不仅要在数据库里做唯一索引,MCP 工具本身也要能识别幂等键冲突。返回结果里要带上原有支付单 ID,方便 agent 继续处理,而不是把已存在的单子当作新单子覆盖掉。
3. signer 不可达时的接口设计:返回明确状态,而不是一直等
3.1 MCP 工具返回什么最合适
当 agent 发起支付创建,而 signer 不可达时,MCP server 不要返回“空”“超时”或者含糊的“失败”。
一个更清晰的返回结果应该包含这些信息:
- 是否成功
- 错误码
- 当前支付单状态
- 是否可以重试
- 后续可用于查询的支付单 ID
- 如果是因为等待人工审批,给出提示
比如这样:
{ "success": true, "payment_id": "pay_20250128_001", "status": "PENDING_SIGNER", "code": "SIGNER_UNREACHABLE", "message": "支付单已创建,但 signer 当前不可达,已进入等待状态", "retryable": true, "next_action": "check_payment_status" }这里的关键是,把“支付单创建成功”和“signer 尚未签名”分开表达。agent 拿到这个结果后,不需要反复调用 create_payment,而是可以转去调用 check_payment_status,或者把任务交给用户等待后续通知。
3.2 不要让 agent 用“等等再看”来处理支付
如果把 signer 不可达当成普通错误,agent 很可能会根据 prompt 自动重试。问题在于,人工审批不可达时,重试频率越高,越容易产生干扰。
我建议在 MCP 工具描述里就写清楚行为:
- create_payment 只负责创建支付单并提交给 signer。
- check_payment_status 负责查询支付单当前状态。
- signer 不可达不意味着支付失败,而是进入 pending 状态。
- 只有在状态为 FAILED 且错误可重试时,才考虑重新创建支付。
这样 agent 在决策时就不会把“PENDING_SIGNER”误判成“需要马上重试”。
3.3 异步审批流程落地顺序
真正适合生产环境的 MCP payment flow,建议按下面这样的顺序跑。
第一步,agent 调用 create_payment,MCP server 创建支付单,持久化状态为 CREATED。
第二步,MCP server 把签名请求投递给 signer 对应的通道。如果 signer 是人工审批人,投递到审批任务队列;如果 signer 是远程签名服务,调用签名接口。
第三步,不管投递是否成功,MCP server 都立即返回一个支付单 ID 和当前状态。如果投递失败,状态置为 PENDING_SIGNER 并标记不可达原因。
第四步,signer 后续通过外部系统完成处理。处理结果以 webhook、消息队列或状态表更新的方式回到 MCP server。
第五步,agent 或用户通过 check_payment_status 查询到最终状态。
这种设计的核心价值,是把“发起动作”和“完成动作”解耦。agent 不需要一直占住一次工具调用,signer 也不需要为了配合 MCP 而调整自己的审批节奏。
4. 超时、重试、并发:三个最容易把支付流打崩的参数
4.1 超时时间怎么设
在 MCP payment flow 里,超时不是一个单一值,而是一组值。
MCP server 到 signer 服务的网络超时,要短一点,比如 2 到 5 秒。如果 signer 接口在超时时间内没响应,server 端应该立即落库并返回 pending 状态,而不是继续阻塞。
agent 调用 MCP 工具的总体超时,要明显大于单次网络超时,避免 agent 那边先报错,而底层请求还在继续执行。
审批任务在队列里的等待时间,则不应该用超时表达。你没法给一个“审批人今天不在”设置固定超时,这种状态更适合用“截止时间”或“升级时间”来处理。
一个比较稳妥的设置思路是分层:
| 层次 | 建议事项 |
|---|---|
| MCP server 到 signer | 网络超时短一些,失败后落库 |
| agent 到 MCP server | 总超时留足余量,避免请求被提前切断 |
| 审批等待 | 不设固定超时,支持升级和催办 |
| 支付单整体有效期 | 设置业务有效期,过期自动取消 |
不要为了“让 agent 更容易成功”就把所有超时调得很大。超时调大的结果往往是,服务线程被占住,后续请求排队,最后整体响应都变慢。
4.2 重试必须带幂等键
signer 不可达时,很多系统会启用重试。但支付流里的重试和其他普通查询不一样。
普通查询重试几次无所谓,因为查询不会改变数据。支付创建和签名请求则不同,每次重试都可能产生副作用。如果重试时没有带同一个幂等键,就可能创建出多张支付单;如果带了幂等键,MCP server 就能识别出这是同一笔请求,从而只更新状态,不重复创建。
我见过一个真实案例:agent 调用创建支付工具,第一次请求因为网络原因没有收到响应。平台自动重试了一次,第二次请求没有复用原来的幂等键,结果同一笔业务生成了两笔支付单。这不是 MCP 的问题,而是接口设计的问题。
重试策略建议这样设计:
- 对网络超时和连接重置,可以按指数退避重试。
- 对 signer 明确返回“拒绝”或“校验失败”的状态,不要盲目重试。
- 对 PENDING_SIGNER 状态,不做自动重试,改为定时查询或等待回调。
- 每次重试都在日志里带上 payment_id 和 idempotency_key。
4.3 并发控制:同一支付单不能同时多个 agent 操作
MCP server 可以同时服务多个 agent,也可能同一个 agent 因为上下文恢复而重复调用。如果两个请求同时操作同一张支付单,就可能出现状态覆盖。
比如,一个请求把状态改为 PENDING_SIGNER,另一个请求又把它改回 CREATED,最终状态就乱了。
处理方法不难,但容易被忽略。支付单表可以加一个 version 字段,更新状态时带上条件 version = old_version。谁先更新成功,谁获得操作权;后到的请求直接提示状态已变化,让 agent 重新拉取最新状态。
如果系统是分布式的,可以用 Redis 分布式锁来锁定 payment_id。锁的粒度要尽量小,只在 signer 状态回写的关键步骤里持有,不要锁住整个支付流程。
注意:并发控制不是为了阻止多笔支付同时发生,而是为了保证同一笔支付单的状态只有一个写入方向。否则 signer 不可达的数据去排查时,你会发现日志里状态跳来跳去,根本看不出真实顺序。
5. 排查链路:先看日志,再改参数,别上来就怀疑 agent
5.1 一套通用排查顺序
当 MCP payment flow 出现“signer 不可达”相关问题时,我的排查顺序基本是固定的。
第一步,先看支付单当前状态。去状态表里查一下 payment_id,看它到底是在 CREATED、PENDING_SIGNER、SIGNED 还是 FAILED。先确认数据实际走到哪一步。
第二步,再看 MCP server 日志。重点看 create_payment 的入参、幂等键、返回结果,以及是否成功调用 signer 通道。MCP server 日志里如果连请求都没收到,那问题可能出在 agent 或工具路由配置上。
第三步,检查 signer 本身。如果是人工审批,看审批任务是否已生成、审批人是否有效、通知是否发送成功。如果是远程签名服务,直接手动调用一次接口,看网络连通性和响应时间。
第四步,再回头检查 agent 的 tool call 记录。看 agent 是否在 signer 不可达后错误地重复创建支付单,或者错误地把 pending 状态当成了失败。
不要在刚开始就调参数。很多问题根源是状态丢失、回调地址写错、幂等键没传,而不是超时时间不够。
5.2 常见报错和对应处理
MCP 系统里经常出现一些看起来很吓人的报错,但实际原因可能很小。
| 现象 | 常见原因 | 优先处理方式 |
|---|---|---|
| SIGNER_UNREACHABLE | signer 服务或审批人不可达 | 查 Signer 通道状态,确认 pending 单不重复创建 |
| agent execution terminated due to error | agent 调用链中断,模型主动终止任务 | 拉取 MCP server 日志,确认工具返回是否可读 |
| The agent execution provider did not respond in time | agent 侧超时 | 检查 MCP server 是否阻塞,降低单次工具耗时 |
| context too large / 上下文超出限制 | 工具返回内容太多,或长流程累积过多历史 | 控制工具返回字段,不要把大对象直接返回给模型 |
| payment_id 不存在 | 幂等键冲突或状态表被清空 | 查创建请求日志,确认事务是否提交 |
你会发现,这些报错并不一定都是 signer 的问题。尤其是“context too large”,很多时候不是模型容量不够,而是 MCP 工具返回了太多无关字段,把支付单详情、签名字段、日志原文都塞给了 agent。我建议 MCP 工具返回给 agent 的数据尽量精简,详细审计信息放在独立查询接口里。
6. 从 Demo 到生产的落地顺序和一点经验
6.1 不要一上来就打通全流程
如果你想快速验证 MCP payment flow,不要直接搭一个完整生产链。建议拆成三步。
第一步,先跑通 create_payment 单工具。用一个测试支付单,固定传 idempotency_key,确认 MCP server 能创建记录并返回 payment_id。
第二步,模拟 signer 不可达。把 signer 服务关掉,再调用一次支付创建,看返回结果是否符合预期。这时候最需要确认的是:MCP server 是否落库,状态是否为 PENDING_SIGNER,agent 是否理解这个 pending 状态而不是反复发创建请求。
第三步,再引入异步回调。让 signer 通过 webhook 更新支付单状态,agent 通过 check_payment_status 读到 SIGNED,然后走支付执行。
低配置环境也能试,但不建议在 Demo 阶段就开高并发。先把单条链路的日志、状态和错误码看清楚。
6.2 生产环境至少还要补四件事
如果这个 MCP payment flow 要真正用于业务,只靠状态机和幂等键还不够。我建议至少补上四件事。
第一,审计日志。每一次 MCP 工具调用、每一次 signer 不可达、每一次重试,都应该记录操作人、时间、请求参数和结果。支付类业务如果出事,没有审计日志几乎没法定位。
第二,手工干预入口。MCP agent 不是万能的,当 signer 长期不可达时,需要管理员有入口把支付单取消、重推或指定新的 signer。不要等到支付单卡住后只能靠数据库改状态。
第三,监控告警。重点关注 pending_signer 时间超过阈值的支付单,以及 signer 不可达错误数量的突增。一个人审批慢可能没事,但如果大量支付单同时卡住,往往是配置或服务出了问题。
第四,权限边界。MCP 工具暴露给 agent 的是支付创建和状态查询能力,通常不应该把“无限次重试”“绕过审批”“直接修改 signer”这样的能力暴露给 agent。权限最小化这个原则,在 agent 场景里反而更重要。
6.3 我个人保留的检查清单
最后留一份我排查这类问题时会过一遍的清单。
- 支付单当前状态是什么,是不是已经进入 PENDING_SIGNER。
- 创建请求有没有传 idempotency_key,重试是不是复用了同一个 key。
- MCP server 日志里,真实返回给 agent 的 code 是什么。
- signer 通道是从哪个环节开始断的,是网络、服务、还是人工审批。
- agent 是否把 pending 状态误判成需要重试。
- 支付单是否能通过后台接口手动取消或重新投递。
- 如果上下文过大,是不是因为工具返回字段太多。
说实话,很多被当成“AI 不智能”的问题,底层都是流程设计没做好。agent 拿到的信息不够清晰,MCP 工具返回的状态不够一致,业务流程中间状态没有被妥善管理,这些问题都会让 agent 表现得很笨。所以,与其不断调 prompt,不如先把 payment flow 的状态机、幂等键、异步回调和排查日志理顺。signer 不可达不可怕,可怕的是不可达之后,系统连一张支付单处于什么状态都说不清楚。