AI Agent 与耗时 API 调用:轮询还是 Webhook

耗时API常让AI Agent崩溃。本文教你如何优化异步API设计,通过外部轮询和合理使用Webhook规范Agent行为,解决其过早声明成功或高频轮询的难题,让异步任务处理更稳定。

用 Apifox,节省研发团队的每一分钟

AI Agent 与耗时 API 调用:轮询还是 Webhook

免费使用 Apifox

相关推荐

最新文章

API

一体化协作平台

API 设计

API 文档

API 调试

自动化测试

API Mock

API Hub

立即体验 Apifox
目录

Agent 调用了您的视频转码接口。该接口返回了 202 Accepted 和一个任务 ID。但 Agent 根本不知道 202 在您的系统里意味着什么,它直接汇报转码已完成,并继续执行下一步——读取一个甚至还不存在的文件。

耗时操作会以一种特定的方式使 Agent 崩溃。同步调用有一个显式的契约:发送请求、等待、获取响应。而异步调用则将这一过程拆分为“开始”和“结束”,而两者的间隔正是 Agent 容易混淆的地方。它们会过早声明成功,在紧密循环中轮询上千次,或者在长达六分钟的时间里一直保持阻塞状态,占用着会话轮次。

本指南将介绍如何设计异步契约以便 Agent 能够遵循,何时该轮询、何时该移交,如何编写工具来规范模型的行为,以及如何测试包括慢响应和失败案例在内的整个路径。我们关于 Agent 错误恢复的博文涵盖了 API 调用失败的情况;而本文则重点关注那些响应缓慢但最终成功的调用。

当您需要验证 Agent 如何处理一个耗时四分钟然后失败的任务时,Apifox 就能派上用场——您绝对不想在生产环境中才发现这类问题。

AI Coding 交流群

如果你也在用 AI 写代码,或者正在研究 Cursor、Claude Code 这些工具,欢迎加入以下交流群。群里平时会聊一些 AI 编程的实际用法、开发工作流,还有各种新工具和新玩法。

为什么 Agent 无法妥善处理异步操作

以下三个习惯是导致大部分问题的原因。

模型会将 2xx 视为已完成。202 表示请求已被接受并等待处理,而 HTTP 语义规范 明确指出处理可能尚未完成。在普通请求/响应流量上训练出来的模型,往往倾向于将任何 2xx 状态码都解读为已完成,除非响应内容中以文字形式明确说明并非如此。

循环的成本非常高。如果 Agent 在其推理循环内部进行轮询,每一次检查都会消耗一个模型轮次,加上之前对话的 Token。对于一个耗时四分钟的任务,每两秒轮询一次就是 120 个轮次,这要么会耗尽上下文窗口,要么会超出预算。我们关于如何避免将工具响应写入上下文窗口的文章,解释了为什么这部分的消耗累积速度会远超人们的预期。

Agent 会跟丢任务。启动任务并返回任务 ID 的工具创建了 Agent 必须维护的状态。如果该 ID 夹在冗长对话的中间,可能会因为上下文压缩而被遗漏,导致 Agent 忘记自己还有一个正在进行的任务。

设计响应,避免模型产生误判

最有效的解决方案是措辞,而不是架构。无论您的状态码是什么,都要在 body 中明确说明发生了什么以及接下来该怎么做。

{
  "status": "processing",
  "job_id": "job_7f21c",
  "message": "The transcode has STARTED and is NOT complete. Do not report success. Check status with getJobStatus(job_id) after at least 30 seconds.",
  "poll_after_seconds": 30,
  "estimated_duration_seconds": 240,
  "status_url": "/v1/jobs/job_7f21c"
}


对于人类 API 消费者来说,这显得有些累赘。但它的目标是模型,而相比于让模型从状态码中推断含义,模型遵循 response body 中明确指令的可靠性要高得多。起作用的有三个细节:短语“not complete”(未完成)、指定的下一个工具以及最小等待时间。

Google 关于长期运行操作的 AIP-151 为此描述了一种清晰的资源形态,即通过一个包含 doneerrorresponse 字段的单 Operation object。借鉴该结构可以让你在每个慢速接口中保持一致的表现,这非常重要,因为 Agent 一旦学会了一种轮询模式,就能处理所有类似的接口。

保持状态响应同样简单直接:

{
  "job_id": "job_7f21c",
  "status": "processing",
  "done": false,
  "progress_percent": 45,
  "elapsed_seconds": 108,
  "poll_after_seconds": 45,
  "message": "Still processing. Do not proceed to the next step."
}

在任务完成时,如果结果较小,则以内联(inline)方式直接返回结果,这样 Agent 就不需要进行第三次调用:

{
  "job_id": "job_7f21c",
  "status": "succeeded",
  "done": true,
  "result": { "output_url": "https://cdn.example.com/out/7f21c.mp4", "duration_seconds": 372 }
}

在模型外部进行轮询,而不是在内部

最核心的实现选择是:将等待过程放在你的工具包装器(wrapper)中,而不是放在 Agent 的推理循环中。

import time

def start_and_await_transcode(client, source_url, max_wait=600):
    job = client.post("/v1/transcode", json={"source_url": source_url}).json()
    job_id = job["job_id"]
    delay = job.get("poll_after_seconds", 5)
    waited = 0

    while waited < max_wait:
        time.sleep(delay)
        waited += delay
        status = client.get(f"/v1/jobs/{job_id}").json()

        if status.get("done"):
            if status["status"] == "succeeded":
                return {"status": "succeeded", "result": status["result"]}
            return {"status": "failed", "error": status.get("error")}

        delay = min(int(delay * 1.5), 60)

    return {
        "status": "timed_out",
        "job_id": job_id,
        "message": f"Still running after {max_wait}s. Job {job_id} continues in the background.",
    }

从模型侧来看,这只是一个耗时较长并返回最终答案的工具调用。上下文中不会出现轮询循环,不会遗忘任务 ID,也不会产生 120 次交互轮数。退避(backoff)机制使请求数量保持在合理范围内,而上限设置则可以防止卡住的任务使运行永久挂起。在调整这些参数之前,亚马逊关于超时、重试和抖动退避的专文是非常值得阅读的参考资料。

遵循两个规则可以确保其安全性。一是始终设定等待时间上限,二是超时时始终返回 job ID,以便 agent 或人工后续检查。切勿返回含糊不清的结果:succeededfailedtimed_out 是三种截然不同的结果,模型应该看到这三个不同的词。

对于执行时间以小时而非分钟计的任务,在包装器内进行轮询就失去了意义。此时,更合适的模式是使用两个工具:一个用于启动任务,另一个用于检查状态,并结合在对话之外对未完成任务的持久化记录,以防因上下文压缩而丢失信息。存储 job_id、其所属的任务以及启动时间,并让 agent 在每次运行开始时读取该列表。

何时 Webhook 是更好的选择

轮询简单且通用。回调效率更高,但运行起来更费功夫。我们的 Webhook 与轮询对比文章中详细介绍了这种权衡,而针对 agent 的具体场景,选择范围则会更窄。

当任务耗时在数秒到数分钟之间、agent 需要等待结果才能继续,或者你无法托管公共接口时,请使用轮询。大多数 agent 的工作负载都属于这一类。

当任务耗时数小时、agent 触发任务后即可继续其他工作,或者有大量并发任务且逐个轮询非常浪费资源时,请使用 Webhook。这需要付出实际的成本:你需要一个公共接收端、签名验证、重试处理,以及在回调到达时唤醒 agent 的方法。我们关于设计可靠 Webhook 和 Webhook 签名验证的指南涵盖了这些基础工作。

还有一种折中方案值得了解。通过 server-sent events(SSE)流式传输任务进度可以提供推送语义,且无需公共接口,因为客户端保持着连接。它适用于有人工监督的交互式 agent,我们关于使用 SSE 流式传输 API 响应的指南涵盖了具体实现。

无论选择哪种方式,完成路径都必须是幂等的。Webhook 会重试,轮询会产生竞态条件,而看到两次 “succeeded” 的 agent 不应该将下游步骤启动两次。我们关于 AI agent 幂等性的文章介绍了保障其安全的关键要素。

不仅要测试快速路径,还要测试慢速路径

异步 Bug 极易隐藏,因为测试环境运行速度很快。在生产环境中需要 4 分钟的任务,在本地存根(stub)上只需 200 毫秒即可完成,因此 agent 永远无法经历实际会遇到的状态。

有四个场景值得刻意构建。

真正缓慢的任务。 对状态接口进行 mock,使其在排在前面的几次调用中返回 processing,之后再返回 succeeded。这可以验证包装器是否进行了轮询、退避并最终返回。在 Apifox 中,你可以通过根据请求次数或控制参数而变化的 mock 来驱动此过程,从而使每次运行相同测试的结果都保持一致。

延迟失败的任务。 返回三次 processing,然后返回带有错误 body 的 failed。Agent 必须报告失败,而不是将完成的轮询误认为是已完成的任务。如果处理不当,这种情况会导致数据静默丢失。

超时。 让 mock 持续返回 processing 并超过封装器的上限时间,然后断言该工具返回 timed_out 且保留完整的任务 ID,而不是抛出异常或返回虚假的成功。

重复完成。 通过 webhook 重试或竞态轮询两次交付成功状态,并断言下游步骤只运行一次。

将这四种情况保存为测试场景,以便它们可以在 CI 中运行。它们的运行成本极低,并且能够捕获因有人缩短超时时间或吞掉错误而导致的回归问题。更广泛的方法可以在我们的 API 契约测试指南中找到。

暴露该问题的三种任务

报表生成。 一个财务 Agent 请求季度导出。这需要 90 秒。在使用原生工具时,Agent 只是获取一个任务 ID,宣布报表已准备就绪,然后向用户提供一个失效的下载链接。而使用阻塞式封装器时,它会等待 90 秒并返回真实的 URL。相同的 API,截然相反的结果,唯一的区别在于等待发生的地点。

批量导入。 一个运营 Agent 上传了 20,000 条记录。导入运行了 8 分钟,并在第 14,000 行处部分失败。这种情况是对简单成功校验机制的考验:任务已经结束,因此状态确实为 done(为 true),但结果中包含了一组被拒绝的行。应当明确返回部分结果及数量,并让 Agent 在继续下一步之前读取这些内容。

模型与构建流水线。 Agent 触发了一个需要 40 分钟的训练运行或 CI 构建。在这种情况下,封装器内轮询是错误的设计;该运行会导致当前轮次占用时间过长。应该启动任务,将 ID 记录在持久化存储中,结束当前轮次,并让定时检查或回调来唤醒后续流程。我们关于多 Agent 交接和上下文传递的文章中,详细介绍了如何在不同运行之间转移状态而不会丢失。

定义部分结果的结构

长期运行的任务通常结束于成功与失败之间的某种中间状态,而双状态模型会迫使你对此做出妥协。请明确定义第三种状态:

{
  "job_id": "job_a11f",
  "status": "completed_with_errors",
  "done": true,
  "summary": { "processed": 20000, "succeeded": 19860, "failed": 140 },
  "errors_url": "/v1/jobs/job_a11f/errors?limit=50",
  "message": "Import finished. 140 rows failed and were not written. Review errors before reporting success."
}

在该 payload 中,有两点至关重要。数量信息是内联的,因此 Agent 无需进行另一次调用即可做出决策。失败的行被置于带有限制(limit)的 URL 之后,这样 140 个错误 object 就不会在未经请求的情况下直接塞进上下文中。

必须有人关注停滞的任务

超时路径以返回任务 ID 和“工作仍在运行”的消息而结束。这是正确的返回值,且只有当它传递给人工时才是有用的。

如果 agent 是你自己的服务,请将其路由到你团队已经在监控的任何队列中。如果 agent 是处理分配任务的编码运行时,运行它的平台通常会有地方来接收这些任务。在 HiFox 中,因受阻而结束的运行会保持在其“任务(Task)”中,并保留其执行状态和结果,而“收件箱(Inbox)”则会将需要人工回复或审核的项与普通更新区分开来。关键不在于具体的工具,而在于“仍在运行,稍后检查”需要有一个负责人,否则它就会变成“无人检查”。

简短的检查清单

  • 每个慢速接口都返回一个任务 ID、一个状态 URL 以及一条用通俗语言说明工作尚未完成的消息。
  • 状态响应包含一个布尔类型的 done 字段,而不仅仅是需要模型去解析的字符串。
  • 轮询逻辑存在于工具封装器中,并带有指数退避机制和硬上限。
  • 超时会返回任务 ID,以便工作可以恢复而不是丢失。
  • 成功、失败和超时是三个不同的返回值。
  • 对于任何耗时超过几分钟的任务,正在执行的任务都会在对话之外进行记录。
  • 无论信号是通过轮询还是通过回调到达,完成处理逻辑都是幂等的。
  • 针对慢速、后期失败、超时和重复完成的场景,都保存了相应的测试。

只要处理好响应的措辞和封装器,长时间运行的操作就不再是 Agent 的特例。它调用工具、等待并获取答案,这是它最擅长处理的约定。下载 Apifox,在编写测试的同时构建慢速任务的 mock。

常见问题解答

异步启动时,API 应该返回 202 还是 200? 202 Accepted 是更准确的状态码,它向标准客户端表明处理尚未完成。对于 Agent 而言,不要仅仅依赖状态码,因为模型最容易可靠读取的是 body。建议两者结合使用。

在放弃之前,工具封装器应该等待多久? 将上限设置得略高于接口实际可能的最坏情况,通常是二到十分钟。超过这个时间,封装器阻塞单轮对话的时间就太长了,此时采用“稍后检查”工具会是更好的设计。

我应该使用多大的轮询间隔? 如果服务端提供了 poll_after_seconds 提示,就以此为起点,然后以大约 1.5 的系数进行退避,并将上限设在 60 秒左右。固定的 1 秒轮询会浪费请求,并可能触发频率限制,这在我们关于“超出速率限制”的指南中有详细介绍。

Agent 在等待时可以做一些有用的事情吗? 只有当你的编排器支持并发工具调用时才可以。如果支持,可以先启动任务,执行其他独立工作,然后检查状态。如果不支持,阻塞式封装器比手动编写的调度器更简单、更不容易出错。

如何阻止 agent 过早宣布成功? 在响应 body 中以文字形式说明,暴露一个布尔类型的 done 字段,并使完成工具成为呈现结果的唯一位置。如果启动响应中不包含任何结果,模型就无需报告任何输出结果。

Webhook 适用于在笔记本电脑上运行的 agent 吗? 无法直接使用,因为没有公开的接口。开发时请使用隧道,如我们关于使用 Webhook 服务测试 localhost API 的指南中所示,或者在 agent 运行在可寻址的位置之前,坚持采用轮询方式。

开发必备:API 全流程管理神器 Apifox

介绍完上文的内容,我想额外介绍一个对开发者同样重要的效率工具 —— Apifox。作为一个集 API 文档、调试、设计、测试、Mock、自动化测试于一体的工具,Apifox 是目前提升研发效率的首选。

如果你正在开发项目,不妨试试其极其友好的界面设计,它完全兼容 Postman 和 Swagger 数据格式,导入数据非常方便,,即使是新手也能很快上手,点击这里即可注册使用

Apifox

值得一提的是,除了个人和常规团队使用,针对有高安全合规要求、或需要在内网环境协作的企业,Apifox 还提供了深度定制的私有化部署方案

获取专属报价与部署方案

icon 详细的私有化部署系统架构与安全白皮书
icon 针对您公司规模的专属报价单
icon 免费的 1v1 专属产品演示 (Demo) 机会
获取部署方案
* 提交后,我们的客户经理将在 1 个工作日内与您联系
林俊锋 企业微信
@Apifox 专属顾问
扫码备注: 私有化 + 公司名