MCP 2026-07-28 长任务改造:别再用 HTTP 超时判断任务失败

一个 Agent 调用工具生成报表,网关在 60 秒时返回超时。此时最危险的处理不是报错,而是客户端立刻重试:前一次报表可能仍在后台生成,第二次请求又启动了一份;如果工具带有扣款、发信或写状态副作用,重复执行会更麻烦。

据 MCP 官方数据,2026 年 7 月 28 日发布的新规格把协议核心改为无状态,并明确说明:取消协议层 session 不等于应用不能保存状态。需要跨调用延续的工作,应由工具签发显式 handle,再由模型或客户端在后续调用中传回。Tasks 也移入 io.modelcontextprotocol/tasks 扩展,提供轮询式 tasks/get 和新的 tasks/update

这次变化值得关注的不是"又多了一版协议",而是它迫使长任务把状态从隐蔽连接里拿出来。

HTTP 请求结束,不代表业务任务结束

长任务至少同时存在三个状态层:

状态层 典型状态 回答的问题
传输层 connected / timeout / closed 这次网络请求怎样结束
任务层 queued / running / input_required / completed / failed / cancelled 后台工作走到哪里
业务层 not_applied / applied / unknown / compensated 外部系统到底有没有改变

HTTP timeout 只说明客户端没有按时拿到响应,不能推出后台任务失败,更不能推出业务动作没有发生。把三层压成一个 success: boolean,重试策略一定会选错。

工具先返回显式任务句柄

一个适合长任务的初始响应可以只确认受理:

json 复制代码
{
  "task_id": "task_7f3a...",
  "status": "queued",
  "status_url": "/tasks/task_7f3a...",
  "cancel_supported": true,
  "expires_at": "<iso-time>",
  "idempotency_key": "report:month:account"
}

task_id 用于查询进度,idempotency_key 用于阻止相同业务请求创建两个任务。两者不能互相替代:前者标识已经存在的运行实例,后者描述哪些创建请求应被视为同一次业务意图。

服务端创建任务时,应先检查幂等记录,再写入任务:

typescript 复制代码
async function createTask(input: CreateInput) {
  const old = await taskStore.findByIdempotencyKey(input.idempotencyKey);
  if (old) return old;

  return taskStore.insert({
    taskId: crypto.randomUUID(),
    idempotencyKey: input.idempotencyKey,
    status: "queued",
    businessState: "not_applied",
    createdAt: new Date().toISOString(),
  });
}

如果你的 Agent 正在处理会产生副作用的长任务,可以先用回滚与补偿检查项核对幂等键、未知状态和人工接管是否闭合,再决定是否允许模型自动重试。

input_required 不能伪装成失败

MCP 新规格中的 Multi Round-Trip Requests 允许工具在执行中途返回 resultType: "input_required",客户端补齐确认或缺失参数后,再把答案附到原调用中。

这类状态经常被通用重试器误伤:它不是网络异常,也不是业务失败,而是"任务暂停,等待特定输入"。建议把等待内容写成结构化请求:

json 复制代码
{
  "task_id": "task_7f3a...",
  "status": "input_required",
  "requests": [
    {
      "request_id": "confirm_recipient",
      "kind": "confirmation",
      "prompt": "确认收件人和附件哈希后继续",
      "expires_at": "<iso-time>"
    }
  ]
}

客户端必须把 request_id 与回答绑定,不能只传一句"确认"。任务恢复前还要重新检查关键参数是否变化;如果附件已经换过,旧确认不能继续使用。

轮询要处理版本和终态

最小轮询器至少要避免三个坑:无限轮询、旧状态覆盖新状态、终态后继续更新。

typescript 复制代码
const TERMINAL = new Set(["completed", "failed", "cancelled"]);

async function waitForTask(taskId: string, deadlineMs: number) {
  let version = 0;

  while (Date.now() < deadlineMs) {
    const task = await getTask(taskId);

    if (task.version < version) continue;
    version = task.version;

    if (TERMINAL.has(task.status)) return task;
    if (task.status === "input_required") return task;

    await delay(task.retry_after_ms ?? 1500);
  }

  return { task_id: taskId, status: "unknown", last_version: version };
}

超出客户端等待期限时返回 unknown,不要擅自写成 failed。下一步应该保留任务句柄并继续查询,或交给人工处理。

取消请求也需要业务回执

cancelled 容易被误解为"所有副作用都撤销了"。实际系统里,取消可能只停止后续步骤,已经发出的邮件、已经写入的记录或已经提交的作业不会自动消失。

取消接口可以返回更明确的结果:

json 复制代码
{
  "task_id": "task_7f3a...",
  "task_status": "cancelled",
  "business_state": "partially_applied",
  "completed_steps": ["render_report"],
  "pending_compensations": ["delete_temp_artifact"],
  "operator_action": "review_required"
}

这让调用方知道"任务停了"和"业务恢复了"是两件事。补偿成功后再单独写回 compensated,不要覆盖原始执行轨迹。

用故障用例验收,不用正常演示验收

长任务上线前至少跑以下场景:

  1. 首次创建成功但响应丢失,使用同一幂等键重试只能得到原任务;
  2. 工具执行中要求确认,未提供 inputResponses 时不能继续;
  3. 客户端超时后任务仍完成,后续查询必须返回同一结果;
  4. 取消发生在副作用之后,结果必须显示 partially_applied
  5. 服务实例切换后,另一实例仍能按 task_id 查询;
  6. 任务过期后,审计记录仍能说明最终业务状态和处理人。

MCP 的无状态核心解决的是传输和部署问题,不会替应用自动设计业务状态。真正稳妥的改造,是让任务句柄、幂等键、等待输入、取消结果和业务回执都成为可查询的数据,而不是藏在一条长连接和一段模型上下文里。

相关推荐
LorryJovens1 小时前
【LAAP架构与安全伦理】LAAP框架与具身智能大脑:从认知架构到自主意识与人机共生社会--LAAP先导愿景片发布
人工智能·架构·agi
阳明山水2 小时前
销量预测2025—2026:从模型竞赛到系统融合的范式转型
人工智能·深度学习·算法·机器学习·架构
Dawson Zhu3 小时前
大语言模型的本质:从条件概率预测到能力涌现的技术剖析
人工智能·语言模型·架构·aigc·agi
deepseek233 小时前
GPT-6 Astra 发布拆解:Computer Use 产品化时刻、CoT 监控失守与 AGI 契约的终结
大模型·openai·ai agent
数字孪生视频孪生4 小时前
空间智能技术|跨镜轨迹全域可溯,无感定位无扰赋能落地
大数据·运维·人工智能·重构·架构
MetaLite4 小时前
微服务认证选型-SpringSecurity-SaToken与自研方案
微服务·云原生·架构
秋名RG4 小时前
2026/6/15 系统故障复盘与整改方案
java·架构
资深技术分享员4 小时前
技术赋能降本增效:Geejing WebBuilder 企业级低代码平台的专业化研发与运维便利价值解析
运维·低代码·架构
ting94520004 小时前
Hey Noah 主动式 AI 执行助理全栈技术深度剖析 —— 从被动对话 LLM 到 FSD 级自主 Agent 工程实现
人工智能·架构