AI 内容检测 API 接入实战——异步任务、幂等键与轮询的正确写法

做内容平台的朋友最近问得最多的一个问题是:AI 检测接口怎么接。看起来只是「发个请求拿个结果」,但真正上线之后,90% 的坑都集中在三件事上:提交是扣费动作、结果是异步的、失败分两层。

这篇把这三个问题拆开讲,代码可以直接跑。

一、为什么这类接口一定是异步的

一次文本检测的实际耗时在 1--3 秒,长文本更久。同步接口意味着你的 HTTP 连接要挂在那里等上游推理,连接池很快被打满,上游一抖动你的服务跟着雪崩。

所以标准形态是:

bash 复制代码
POST /v1/text/classify   →  { task_id }        # 立即返回,此时已扣费
GET  /v1/tasks/{task_id} →  { status, result } # 轮询直到终态

status 的取值:pending / running 是中间态,succeeded / failed 是终态。

这个设计本身没问题,问题在于很多人把它当同步接口写:while True: 查一下; sleep(0.1)。这会撞上查询接口的限流(一般 10 次/秒),而且密集轮询并不会让上游算得更快。

二、完整实现

下面是最小可用版本,包含限流重试、幂等和超时处理。

python 复制代码
import os
import time
import uuid

import requests

BASE = "https://sbnew.top"
KEY = os.environ["AIGC_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}


def submit_text(text: str, is_merge: bool = False, idem: str | None = None) -> str:
    """提交检测任务,返回 task_id。提交即扣费。"""
    idem = idem or str(uuid.uuid4())

    while True:
        resp = requests.post(
            f"{BASE}/v1/text/classify",
            headers={**HEADERS, "Idempotency-Key": idem},
            json={"text": text, "is_merge": is_merge},
            timeout=30,
        )

        if resp.status_code == 429:
            # 未被受理,不产生任务也不扣费;复用同一个幂等键重试是安全的
            wait = int(resp.headers.get("Retry-After", "1"))
            time.sleep(wait)
            continue

        resp.raise_for_status()
        return resp.json()["data"]["task_id"]


def wait_for(task_id: str, timeout: int = 180) -> dict:
    """轮询到终态。超时不代表任务失败,任务仍在服务端。"""
    deadline = time.time() + timeout
    interval = 1.5

    while time.time() < deadline:
        resp = requests.get(f"{BASE}/v1/tasks/{task_id}", headers=HEADERS, timeout=30)
        resp.raise_for_status()
        task = resp.json()["data"]

        if task["status"] in ("succeeded", "failed"):
            return task

        time.sleep(interval)
        interval = min(interval * 1.5, 5.0)   # 连续 pending 时退避,避免空转

    raise TimeoutError(f"任务仍在执行,请稍后用 get_task('{task_id}') 继续查询,勿重新提交")

调用:

python 复制代码
task_id = submit_text("把待检测的文本放在这里", is_merge=False)
task = wait_for(task_id)

if task["status"] == "failed":
    print("任务失败:", task["error_code"], task["error_msg"])   # 是否退款看 error_code
else:
    result = task["result"]
    print("整体置信度:", result["softmax_confidence"])
    for seg in result["segment_labels"]:
        start, length = seg["position"]          # 注意:第二项是段长,不是结束下标
        print(seg["label"], seg["conf"], seg["text"])

三、幂等键的三个陷阱

陷阱一:以为重试就是重新提交。 提交接口一被调用就扣费。网络抖动导致客户端超时,服务端可能已经建了任务------这时如果你换一个新幂等键重试,就是实打实的重复扣费。正确做法是:同一个逻辑请求,自始至终用同一个键,把键和业务 ID 一起落库。

陷阱二:改了参数还复用旧键。 幂等是按「账号 + 键」去重的,服务端不校验请求内容 。你改了 is_merge 或换了文本,却复用旧键,拿回来的是上一次的任务------结果对不上,还很难排查。规则很简单:

场景 幂等键
网络超时、429 重试 复用同一个
改了文本 / 改了 is_merge 必须换新的
用户手动点「重新检测」 必须换新的

陷阱三:键不落库。 进程崩了、消息重投,键就丢了。建议直接用业务主键派生:f"{article_id}:{revision}:{attempt}",可复现、可追溯。

四、失败分两层,别混在一起处理

这是最容易写错的地方。HTTP 层的错误和任务层的错误语义完全不同:

HTTP 层(请求没被受理,不产生任务,不扣费)

状态 code 含义 处理
400 invalid_param 参数非法,如 text 为空或超长 修代码,别重试
400 missing_idempotency_key 缺幂等键 补上
401 invalid_api_key 密钥无效 检查配置
402 insufficient_balance 余额不足 充值后重提
429 rate_limited 超出每秒上限 按 Retry-After 等待
429 quota_exceeded 每日配额用尽 等次日重置,等待无用
413 payload_too_large 请求体超限 压缩或分片

任务层(HTTP 200,看 data.status 和 data.error_code)

error_code 含义 退款
bad_request 输入未被受理 全额退
rate_limited 检测服务繁忙 全额退
service_unavailable 服务暂不可用 全额退
service_error 检测服务异常 不退
detect_failed 结果未取回 不退
timeout 检测超时 不退

实践建议:不要穷举 error_code 做分支。这个枚举会随上游演进,认不出的码一律按失败处理,退款以资金流水为准------这是唯一权威的账。

五、并发怎么设

两个限流是独立的:提交 2 次/秒,查询 10 次/秒,另有每日提交配额。

这意味着提交侧的 2 QPS 就是你的吞吐上限,开 20 个线程也不会更快,只会在提交口排队撞 429。合理的配置是:4--6 个 worker,每个 worker 提交后以 1.5--2 秒间隔轮询,整体查询压力约 3 QPS,安全。

如果单账号不够用,优先考虑多账号分片或申请提额,而不是加线程。

六、小结

接入这类接口,真正要写对的只有四行逻辑:

  1. 幂等键跟着「逻辑请求」走,不跟着「HTTP 请求」走;
  2. 429 按 Retry-After 退避,且复用幂等键;
  3. 轮询超时继续查,永不重新提交;
  4. 失败分 HTTP 层和任务层两套处理,退款只看流水。

其余都是工程细节。


示例基于文识云(sbnew.top)的 AIGC 检测接口,底层为朱雀模型。接口形态与错误分层设计在同类的异步检测服务里比较通用,换成别家也能对上大半。

相关推荐
FRing034 小时前
vue2干货总结7(自定义指令、插槽、单页应用程序、路由的使用(VueRouter))
开发语言·前端·vue.js·vue2
ruanno_top8 小时前
点全选,已选的被清空了:Transfer 三态复选框的三个 bug
vue.js
时间的情敌9 小时前
Cesium 加载 3D Tiles 倾斜摄影、压平、样式设置(Vue3 实战)
vue.js·3d·cesium
usdoc文档预览1 天前
USDOC系列之PPT文件在线编辑-VUE前端实现在线浏览器在线编辑PPT文档
前端·vue.js·编辑器·office365·ppt在线编辑·office文件在线编辑·文件在线编辑
Java小卷1 天前
面向 schema 编程,轻松搞定 Element 虚拟化表格复杂应用场景
vue.js·低代码·element
仨孩子的爹1 天前
Lodop、hiprint 与 vue-print-designer:打印控件选型对比
vue.js
ruanno_top1 天前
四个 canvas 组件,在 2 倍屏上一直是糊的
vue.js
ruanno_top1 天前
一个脏数据能让整条折线消失:图表组件的四种数据契约
vue.js
FlyWIHTSKY2 天前
vue2和vue3有什么区别,用法上有哪些区别
前端·javascript·vue.js
ruanno_top2 天前
能点,但 Tab 不到:两个组件把「可操作」做了一半
vue.js