打开网易新闻 查看精彩图片

凌晨3点,你的支付系统突然收到47次相同的扣款请求。不是攻击,是一个AI代理(AI Agent,自主执行任务的智能程序)在循环重试。它没看明白你的文档,以为"创建订单"就是"扣款"。

这不是科幻。LangChain、OpenAI Function Calling这些框架里的代理,正在以每秒20次的速度调用API。它们不读你的落地页,直接解析OpenAPI规范(一种机器可读的API描述格式)。描述写错一个字,代理就走错一条路。

你的API是给前端开发者设计的——人类会犹豫、会确认、会在报错时停下来想。AI不会。它们把模糊描述当真理,把临时故障当永久失败,把重试机制当无限循环的许可证。

补丁1:把"显然"写进代码注释里

补丁1:把"显然"写进代码注释里

人类开发者看文档能猜意图。AI代理只看OpenAPI规范里的description字段,而且照字面执行。

下面这个例子来自原文,展示了什么叫"差一个字,错一条路":

```python class OrderCreate(BaseModel): """Create a new order. Does NOT charge the customer. Use POST /orders/{order_id}/confirm to finalize and charge.""" customer_id: str = Field( description="Unique customer identifier. Format: cust_xxxxxxxxxxxx" ) items: list[str] = Field( description="List of SKU strings. Each SKU must exist in the product catalog." ) amount_cents: int = Field( description="Total order amount in USD cents. Must match sum of item prices.", ge=1, ) ```

注意那个大写的"Does NOT charge the customer"。没有这句话,代理可能直接调用创建订单然后告诉用户"已完成支付"。30分钟后草稿过期,用户的钱没扣,货也没发,客服工单爆炸。

三个代理需要、但人类自己脑补的东西:

第一,把规范暴露在标准端点,比如/openapi.json。代理发现API靠这个,不是你的营销页面。

第二,字段描述要包含格式约束。"cust_xxxxxxxxxxxx"比"customer ID"让代理少打一次试错电话。

第三,端点summary必须说明副作用。"Create a draft order (does not charge customer)"——括号里的否定句,是防止凌晨3点灾难的保险。

补丁2:幂等键不是可选项,是生存必需品

补丁2:幂等键不是可选项,是生存必需品

每个代理框架都内置重试逻辑。LangChain遇到异常就重试。OpenAI的函数调用遇到格式错误就重试。你自己的代理循环遇到超时也重试。

没有幂等键(Idempotency Key,保证同一操作多次执行结果相同的唯一标识),每次重试都是一笔新订单。一个"创建订单"的意图,变成三个订单、三笔扣款、三个物流包裹。

原文给了一个最小可行实现:

```python @app.post("/payments") async def create_payment( amount_cents: int, customer_id: str, response: Response, idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"), ): if idempotency_key is None: raise HTTPException(status_code=400, detail="Idempotency-Key required") # 检查是否处理过 if idempotency_key in idempotency_store: return idempotency_store[idempotency_key]["response"] # 处理支付,存储结果 result = process_payment(amount_cents, customer_id) idempotency_store[idempotency_key] = { "response": result, "timestamp": time.time() } return result ```

生产环境用Redis或数据库替代那个内存字典。TTL设24小时足够覆盖大多数代理的超时窗口。

打开网易新闻 查看精彩图片

关键细节:代理不会自己生成幂等键。你的API要在首次返回时明确告诉它"下次带这个键来",或者在规范里写明键的生成规则。否则代理会傻乎乎地重试,然后被你返回的"重复键错误"搞懵。

补丁3:把"部分成功"设计成一等公民

补丁3:把"部分成功"设计成一等公民

人类看到"3个成功,2个失败"会手动处理。代理看到200状态码就认为全成功了,或者直接崩溃。

批量操作要返回详细结果数组,每个元素有自己的状态码和错误信息。不要用一个总状态码掩盖局部失败。

错误信息要机器可读。"Invalid SKU"对代理没用。"SKU 'ABC-123' not found in catalog. Valid format: XXX-NNNN. Did you mean 'ABC-124'?"——这种错误代理能自己修,不用叫醒你。

原文没提但值得补充:考虑给代理专用的" dry-run"端点。让它们在真正执行前先模拟一遍,返回"我会调用A、B、C三个端点,预计产生X、Y、Z副作用"。人类不看这个,但代理能用它做自我检查。

补丁4:速率限制要分人类和代理两本账

补丁4:速率限制要分人类和代理两本账

你的API可能已经有速率限制。但那是给人类点的按钮设计的——每秒5次够用了。

代理的循环可能在一秒内触发20次调用。不是恶意,是它们在链式推理:查库存→算价格→应用优惠→创建草稿→确认支付→通知仓库。六步流程,人类要6分钟,代理要6秒。

分桶限速:人类令牌桶宽松,代理令牌桶更宽松但要求强制身份标识。让代理在Header里声明"我是Claude-3-Sonnet-20240229-v1",你就能区分是用户在测试还是代理在暴走。

更激进的方案:给代理专用端点。/v1/agent/orders 和 /v1/orders 共享业务逻辑,但前者接受更复杂的查询参数,返回更结构化的错误,允许更高的并发。人类开发者不会用这些端点——太啰嗦。代理不会用普通端点——太慢太脆。

补丁5:把"不确定"设计成可恢复的状态

补丁5:把"不确定"设计成可恢复的状态

代理遇到未预期响应时的默认行为是重试。如果你的API返回500,它会等两秒再试。再500,等四秒。再500,等八秒。指数退避,直到成功或超时。

但有些失败不该重试。信用卡余额不足,重试100次也是不足。库存为零,重试100次也是零。这些要返回明确的4xx状态码,让代理知道"这不是临时故障,是业务规则阻止"。

更微妙的是"我不确定"状态。支付网关超时,你不知道钱扣了没。对人类,你显示"处理中,请稍后查询"。对代理,你要返回一个"pending"状态和一个查询端点,让它能轮询而不是重试创建。

原文强调的最后一个细节:代理会记住你的错误模式。如果一个端点经常500,代理框架的日志里会积累失败记录。下次规划任务时,它可能绕开你——不是恶意,是优化。你的API从"不太好用"变成"尽量不用",只需要三次超时。

凌晨3点的那47次重试,最后怎么解决的?

原文没讲。但你可以检查自己的日志:有没有代理在循环调用同一个端点?有没有创建后从未确认的草稿订单?有没有支付网关超时后代理直接放弃,留下悬而未决的交易?

这些问题现在不解决,等代理用户占比从1%涨到50%时,你的 on-call 工程师会先崩溃。