跳到主要内容

AI 原生 API 设计:当后端开始概率性思维,REST 为何失效

· 阅读需 12 分钟
Tian Pan
Software Engineer

大多数后端工程师能够背诵 REST 契约:客户端发送请求,服务器处理请求,服务器返回状态码和响应体。200 表示成功,4xx 表示客户端出了问题,5xx 表示服务器出了故障。响应是确定性的,超时是可预测的,幂等键保证了安全重试。

而 LLM 后端违背了上述所有假设。一个返回 200 OK 的请求,可能意味着模型对整个响应产生了幻觉。一次成功的请求可能需要十二分钟,而不是十二毫秒。两次参数完全相同的请求会返回不同的结果。如果服务器在推理过程中超时,你根本不知道模型究竟是否已完成。

把 LLM 硬塞进传统 REST API 的团队,最终往往面对一堆补丁:超时杀死了正在运行的 Agent 任务,客户端把带幻觉的 200 当成成功,重试逻辑因为幂等键没有针对概率性操作设计而三次扣了用户的信用卡。本文将梳理这些不匹配最致命的地方,以及真正在生产环境中能站得住脚的接口模式。

同步请求-响应模型是为速度而生的

REST 是为快速、无状态操作而设计的。数据库查询在毫秒内完成,文件上传最多几秒。HTTP 默认 30 秒的超时对于确定性工作负载来说绰绰有余。

LLM 推理从两个维度都不符合这一模型。首先,即使是简单的文本生成,按传统标准来看也很慢——以每秒 30 个 token 的速度生成 500 个 token 的响应需要 16 秒。其次,链式调用工具的 Agent 任务可能持续数分钟乃至数小时。一个需要搜索资料、编写代码、运行测试并对失败进行迭代的 Agent,可能需要 20 分钟的实际运行时间。

当客户端的 HTTP 超时在 30 秒触发时,它不知道任务究竟是被中止了,还是仍在服务器端运行。连接断开了,但模型还在继续。客户端重试,现在同一个 Agent 任务的两个实例在同时运行,可能同时向同一个数据库写入、调用同样的外部 API,并发送重复的邮件。

解决方案是异步任务模式,各大 LLM API 提供商已独立收敛到了这种模式:初始请求立即返回 202 Accepted 和一个任务 ID,客户端随后轮询状态端点或建立流式连接获取更新,任务无论客户端是否在线都会运行至完成。这种解耦是传统 REST API 与面向长时运行 AI 工作负载的 API 之间最重要的结构性差异。

状态码无法捕获语义失败

HTTP 状态码传递的是基础设施结果,而非语义结果。服务器返回 200 OK 意味着请求在传输层面处理成功,与内容是否正确无关。

对于确定性后端,这个区别无关紧要——API 返回用户数据,数据要么存在(200),要么不存在(404)。但 LLM 后端可能返回语法正确、却在语义上存在问题的响应,而 HTTP 对此没有任何表达能力。

以下所有失败模式都会返回 200 OK

  • 幻觉:模型虚构了 API 参数、方法名或并不存在的事实。JSON 解析正常,Schema 验证通过,但数据完全是捏造的。
  • 拒绝回答:模型拒绝作答,返回"我无法帮助处理这个问题"之类的礼貌提示,而不是应用程序期望的结构化输出。
  • Schema 漂移:模型返回了合法的 JSON,但使用了下划线命名而你的 Schema 期望驼峰命名,或者省略了它认为不重要的必填字段。
  • 截断:模型在响应中途耗尽了 token,你得到的是截断点之前的合法 JSON,然后是乱码或突然结束。

只检查 HTTP 状态码的客户端会错过所有这些问题。其下游影响是:应用程序将幻觉响应当作真相,尝试解析被截断的 JSON,并在字段缺失时崩溃——而这一切都不会在 API 错误统计中体现,因为每个请求都返回了 200。

解决这个问题的模式是:在 HTTP 状态码之外返回一个结构化错误信封。响应体携带一个语义状态字段,而不是仅依赖状态码:

{
"status": "partial_success",
"result": { ... },
"errors": [
{
"type": "schema_violation",
"message": "Field 'unit_price' missing from line_items[2]",
"severity": "warning",
"recovery_suggested": true
}
]
}

HTTP 200 表示请求已处理,响应体中的 status 字段告诉你输出是否可用。这种模式让客户端能做出细粒度的决策:对轻微漂移记录警告,对 Schema 违规进行重试,对幻觉信号上报给人工处理。它也让你的 API 对自己能真正保证什么保持诚实:基础设施层面的交付,而非语义正确性。

流式传输不是可选项——但协议选择至关重要

对于文本生成,流式传输决定了应用程序是感觉流畅还是看起来卡住了。用户可以在 200 毫秒内开始阅读,而不是等待 16 秒拿到完整响应。对于长时 Agent 任务,流式状态更新是让用户了解进展的唯一方式,无需反复轮询。

业界已收敛到两种协议,各自适用于不同场景。

Server-Sent Events(SSE) 是 token 流式传输的默认选择。所有主要 LLM 提供商——OpenAI、Anthropic、Google——都使用 SSE 作为流式 API 协议。它基于标准 HTTP 运行,无需特殊配置即可穿越负载均衡器和代理,浏览器的 EventSource API 会自动处理重连。每个数据块作为结构化事件到达:

data: {"delta": {"type": "text_delta", "text": "答"}}
data: {"delta": {"type": "text_delta", "text": "案"}}
data: [DONE]

SSE 是单向的——只能由服务器推送到客户端。这对只读场景已经足够:聊天、摘要、代码生成、搜索结果。

WebSocket 在更复杂的连接状态管理代价下增加了双向通信能力。当客户端需要中断或修改进行中的生成时使用它——例如语音界面中用户在响应中途开始说话,或者多人协作编辑器中多个用户可以调整模型输出方向。大多数团队默认使用 SSE,只有在真正需要客户端向服务器发送实时消息时才引入 WebSocket。

SSE 在故障场景下的运营风险在于连接管理。会缓冲响应的代理和 CDN 会吞掉你的流。在慢速推理阶段对空闲连接激进超时的负载均衡器会截断流。没有实现重连的客户端会悄无声息地断开连接,并展示空白界面。这些都是工程问题,而非协议问题——但它们会在每个团队首次上线流式功能时如期出现。

幂等键是为错误的威胁模型设计的

经典的幂等性保证:如果你用幂等键 K 发送请求 X,服务器只处理一次。如果 K 再次到达,服务器返回第一次执行的缓存结果。这是支付 API 防止网络故障导致重试时重复扣款的方式。

这一保证依赖于确定性:同样的请求必须每次产生同样的结果,这样缓存结果才与新鲜执行无法区分。LLM 打破了这一点。temperature > 0 意味着两次相同的 prompt 会产生不同的文本。temperature = 0 加固定 seed 能让你接近,但 GPU 浮点非确定性和负载均衡效应意味着你仍然无法保证跨次运行字节级完全一致的输出。

这为执行有状态工具调用的 Agent 系统制造了真实的麻烦。一个调用"发送邮件"工具三次(初始调用加两次网络故障后的重试)的 Agent,如果工具本身没有单独防护,可能真的会发出三封邮件。LLM 请求上的幂等键并不能阻止工具调用被多次执行。

有效的模式是在工具层而非 LLM 层应用幂等性。每次执行写操作的工具调用都拥有自己的幂等键,从 Agent 任务 ID 和调用位置哈希派生而来。工具执行器在接触外部系统之前在执行层去重。LLM 层可以非确定性地生成并自由重试;工具层为有状态操作吸收幂等性保证。

对于只读工具调用,幂等性不那么关键——随意重试即可。对于写操作(数据库变更、邮件、支付、有副作用的 API 调用),无论 LLM 请求了多少次相同的调用,都要在工具层严格执行"只执行一次"的保证。

长时任务需要不同的 API 形态

异步任务模式将提交与执行解耦,但面向客户端的 API 在长任务生命周期的三个关键时间点仍需要连贯的形态:提交、状态查询和完成。

提交立即返回。POST /tasks 接受任务描述和所有配置,然后返回 202 Accepted 以及任务 ID 和可用时的预计耗时。客户端现在拿到了一个句柄,可以断开连接——任务会运行到完成。

状态查询有三种可行选项,取决于使用场景:

  • 轮询GET /tasks/{id})最简单。客户端按计划轮询,收到 {"status":"running","progress":0.4} 直到任务结束。添加 Retry-After 头来建议轮询间隔并减少不必要的请求。适合不需要实时更新的批量场景。
  • Webhook 让服务器在状态变化时主动通知客户端,消除轮询开销。客户端在提交时提供回调 URL;服务器在每次重要状态转换时调用它。适合客户端可以暴露公开端点的服务器间集成。
  • SSE 状态流GET /tasks/{id}/stream)为交互式客户端提供最佳体验。客户端在提交后建立持久连接,随着 Agent 推进各步骤实时接收更新。这结合了 Webhook 的即时性和客户端主动发起连接的简洁性。

完成应包含完整结果、最终状态以及足够诊断失败的元数据:哪些步骤运行了、调用了哪些工具、各步骤耗时多少,以及发生了什么错误。一个"失败"的任务在响应中需要足够的上下文,以便调用系统决定是重试、上报,还是向用户展示错误。

输出分布变了时如何做版本控制

对确定性 API 做版本控制很简单:修改接口,升级版本号,保持向后兼容性。客户端固定到某个版本,直到准备好迁移为止。

LLM API 带来了一个新的版本控制问题:即使接口保持不变,给定模型的输出分布也会随时间改变。提供商在不总是发布新版本标识符的情况下重新训练和更新模型。你针对特定模型行为调优的 prompt,现在运行在一个行为上微妙不同的模型之上。

实际影响是版本固定必须比 API 版本更深入。在可用时固定到特定的模型检查点(Anthropic 和 OpenAI 等提供商为此暴露了带日期的模型标识符)。在迁移到较新检查点之前,对固定检查点运行行为回归测试。这些回归测试不是二元的通过/失败——它们是统计阈值:JSON 有效率、Schema 遵从率、语义质量分数。一个 2% 的时候产生幻觉的模型和一个 8% 的时候产生幻觉的模型不是同一回事,这种差异不会在单元测试中显现。

对于 SLA 承诺,这意味着从"API 返回有效 JSON"转变为"API 在 24 小时测量窗口内 ≥97% 的时间返回有效 JSON"。这让习惯了确定性保证的工程师感到不适,但它是诚实的。统计性 SLA 是概率性后端能真正兑现的唯一承诺。

从第一个接口就为不确定性而设计

把 AI 原生 API 设计做对的团队,不会把它当成对 REST 惯例的补丁。他们从失败模式出发,反向推导出接口。

长时任务从第一天就需要异步提交,因为把轮询改造到同步端点上是一次破坏性变更。语义错误信封需要在初始 Schema 中就存在,因为没有检查它们的客户端在上线后也不会开始检查。流式传输需要基础设施支持(兼容 SSE 的负载均衡器、连接超时配置),部署后再补救要困难得多。工具层的幂等性需要在第一次写操作投入生产之前就构建好。

底层的心智模型转变是这样的:REST API 是围绕服务器知道自己能交付什么来设计的。AI 原生 API 是围绕服务器无法保证什么来设计的——并在接口的每个层面将这种不确定性显式化。状态信封、置信度元数据、概率性 SLA 和异步模式,不是为了复杂而复杂,而是概率性后端真正能够兑现的契约。

References:Let's stay in touch and Follow me for more thoughts and updates