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 检测接口,底层为朱雀模型。接口形态与错误分层设计在同类的异步检测服务里比较通用,换成别家也能对上大半。

相关推荐
星光不问赶路人1 小时前
Vue 项目类型检查:深入理解 vue-tsc --build
前端·vue.js
WeiXin_DZbishe2 小时前
基于springboot装修风格选择系统的设计与开发-计算机毕设【课程设计】77964
java·vue.js·spring boot·后端·python·php·课程设计
雪碧聊技术2 小时前
Vue 一个页面中使用多个同类组件出现“串数据“现象?一个 key 就能解决
vue.js·v-show·一个页面使用多个相同组件·解决数据错乱·v-if、key
Java小卷5 小时前
Vue3 动态表单支持弹框组件
前端·vue.js·低代码
溪语流沙5 小时前
【Web全栈进阶】前端对接新契约:信封解包 + 统一错误处理
前端·vue.js·python·状态模式·fastapi
Q26433650235 小时前
【有源码】基于SpringBoot+Vue的居民健康体检与慢病干预管理系统-面向智慧医疗的体检预约与健康知识服务平台
java·vue.js·spring boot·mysql·毕业设计·javaweb·课程设计
雪芽蓝域zzs6 小时前
Vue导入方法三种方式对比
前端·javascript·vue.js
雪芽蓝域zzs7 小时前
从零搭建【pnpm+Vue3+TS+Setup+VueRouter4+Pinia+Axios+ElementPlus (unplugin 按需)】仿若依后台
前端·vue.js
weixin_BYSJ198710 小时前
【计算机毕设】基于Spring_Boot_Vue的马拉松报名与选手成绩追踪系统05445
java·vue.js·spring boot·学习·spring·django·课程设计