跳到主要内容

34 篇博文 含有标签「api-design」

查看所有标签

你的智能体读不懂的弃用通知

· 阅读需 10 分钟
Tian Pan
Software Engineer

当你为人类开发人员弃用一个 API 时,你会为此举行一整套“仪式”。你提升版本号,在 OpenAPI 规范中添加 deprecated: true,发送 Sunset HTTP 响应头,向开发人员邮件列表发送邮件,发布变更日志,并给人们六个月的时间进行迁移。信号到达阅读它的开发人员,他们提交工单,并在旧路径消失之前更新他们的客户端。

现在,将同样的弃用通知指向一个智能体。调用你工具的模型不会阅读你的变更日志,它不会订阅你的邮件列表。它永远看不到 Sunset 响应头,除非你刻意将其放在模型会查看的地方,即便如此,它也没有可靠的习惯去据此行动。你精心编写的弃用通知落入了一个没有读者的信箱。智能体会一直调用工具的旧形式,直到该形式彻底消失,然后它就会失败——通常是静默失败,通常是在生产环境中,通常是在凌晨 2 点。

这就是为智能体而非人类构建工具时所存在的隐性不对称。我们在 API 演进的二十年里建立的每一项准则,都假设在弃用和迁移之间坐着一个人类。把人类拿掉,整个机制就会失效。

对你的工具接收的内容保持严格:波斯塔尔法则在智能体系统中失效

· 阅读需 12 分钟
Tian Pan
Software Engineer

“发送时要保守,接收时要开放。”波斯塔尔法则(Postel's Law)无疑是网络历史上最成功的设计原则——正是依靠它,来自不同厂商的 TCP 实现才得以在 20 世纪 80 年代实现互操作,并塑造了四十多年来的协议和 API 设计。然而,根据 IETF 自身的判断,这一原则会随着时间的推移而变质:RFC 9413 源于一份标题直白为《稳健性原则的有害后果》(The Harmful Consequences of the Robustness Principle)的草案,认为开放式的接收在短期内有助于互操作性,但从长远来看,却在悄悄腐蚀整个生态系统。

智能体(Agent)系统将这种长期的腐蚀缩短到了几周。当“发送者”是一个发出工具调用(tool calls)的大语言模型时,每一次开放式的接收——将 "5" 强制转换为 5、丢弃未知字段、对枚举拼写错误进行模糊匹配——都会破坏维持系统健康所需的精确信号。在智能体架构中,工具边界是唯一一处“大声报错”即是可靠性特性的地方,而大多数团队却反其道而行之。

当你最大的客户端是 Prompt 时,如何弃用 API

· 阅读需 12 分钟
Tian Pan
Software Engineer

你完美地执行了弃用指南。提前六个月发布公告邮件。包含四种语言代码示例的迁移指南。在每个 v1 响应中添加了 Sunset 响应头。开发者仪表板上的横幅。两封提醒邮件。然后你关闭了 v1 —— 但结果并非指南所承诺的安静切换,你的错误率直线飙升并维持在高位。流量没有迁移。它只是不断涌入、失败并重试,且容量比以前更高,因为每一次失败都触发了另一次尝试。

指南之所以失败,并不是因为你执行得不好。它的失败是因为它假设另一端是一个人类 —— 一个会阅读邮件、浏览变更日志并在截止日期前提交 Jira 任务进行迁移的人。你很大一部分流量背后已经没有这样的人了。它来自智能体 (Agent),它们对你 API 的认知被冻结在系统提示词、工具 schema 以及散布在成千上万个你看不见也永远无法触达的代码库中的模型训练数据里。你的停用通知是写给读者看的。而你的最大客户端是一个 Prompt。

GraphQL 终于找到了它的客户端,而且它不是人类

· 阅读需 12 分钟
Tian Pan
Software Engineer

GraphQL 的核心赌注是客户端应该能够自主组合其数据需求。在过去的十年里,这个赌注大体上是输了——因为客户端是由人类团队组成的,而他们根本不想组合任何东西。前端工程师想要的是一个可以调用后就不用管的稳定端点。GraphQL 推销的灵活性是以解析器复杂度(resolver complexity)、缓存变通方案和安全审查为代价的,而换取的收益——按请求进行字段选择——对于已知的、变化缓慢的 Web 应用来说几乎毫无必要。到 2024 年,从业者的共识已明显降温:大多数内部 API 只服务于两三个已知客户端,一个设计良好的 REST 端点或 BFF 层就能很好地覆盖这些需求。

接着,一种新型的客户端出现了。AI 智能体没有固定的一套屏幕。它根据每个任务决定自己需要什么数据,并且它为响应的每一个字节付费——字面意义上是在为 Token 付费,而在认知意义上,随着上下文窗口被没人要求的字段填满,推理能力也会随之下降。真正能够自主组合数据需求的客户端终于出现了。只是它不是人类。

Postel 法则在工具边界是一种负担

· 阅读需 11 分钟
Tian Pan
Software Engineer

1980 年,Jon Postel 在 TCP 规范中写下了一句话,这成为了互联网的奠基原则:“对自己发送的内容要保守,对接收的内容要宽容。”四十年来,工程师们将其应用到各个角落——容忍尾随逗号的解析器、将 "10" 强制转换为 10 的 API、静默修复错误标记的 HTML 渲染器。可以说,万维网之所以存在,是因为浏览器宽容了所有人的错误。

然而,当调用者不再是人类时,这条建议就反转了。当一个 AI Agent 调用你的工具并传入字符串类型的数字、嵌套错误的 JSON 对象,或者一个 几乎 正确的枚举值时,那个原本能节省人类开发人员 20 分钟调试时间的宽容解析器,对 Agent 来说却造成了更糟糕的后果:它确认了这个草率的调用是正确的。Agent 在循环中唯一的训练信号就是你的工具返回的反馈。接受垃圾数据,你就在教导模型——就在此时此刻,在这个会话片段中——垃圾数据是行得通的。

你的错误信息现在成了 Prompt:为 AI Agent 编写失败输出

· 阅读需 12 分钟
Tian Pan
Software Engineer

统计一下你的堆栈跟踪(stack traces)的阅读者。对于大多数内部工具,过去的答案通常是“偶尔有一位疲惫的工程师”。如今,你的错误输出的最大阅读者几乎肯定是一个处于重试循环中的语言模型。编程智能体(Coding agents)每天成千上万次地阅读你的 linter 警告、CLI 使用说明字符串、API 错误主体以及测试失败信息——频率远高于任何人类。而且与人类不同,智能体会字面理解每一个词。

这改变了错误信息的“本质”。它不再仅仅是失败的文档,而是注入到下一次尝试的上下文窗口中的指令——这是你几个月前编写的提示词,现在正引导着一群你从未见过的智能体集群。一个精确的错误能让循环在一次重试中收敛。而一个模糊或误导性的错误则会让智能体陷入恶性循环:错误的修复、--no-verify 的权宜之计、幻觉出来的参数标识、消耗殆尽的 token。如果你维护着一个工具、一项服务或一个构建系统,你其实已经在进行提示词工程(prompt engineering)了。你只是在错误字符串中进行的,而且很可能是无意为之。

你的内部平台的新首要客户是 AI Agent

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的平台团队通过开发者采纳度来衡量成功。内部 API 的月活跃工程师数量。新服务的首次调用时间(Time-to-first-call)。季度 DX 调研中的净推荐值 (NPS)。每一个指标都假设请求的另一端是一个人类——一个阅读入门指南、复制 curl 示例、并在错误消息毫无帮助时在 Slack 中抱怨的人。

这种假设正悄然变得不再成立。你内部 API 增长最快的消费者不是人,而是一个智能体 (Agent):一个正在解决工单的编码助手,一个在夜间核对发票的工作流,或者一个为了回答一个问题而调用六个服务的支持机器人。这些调用者不会浏览你的文档——它们会将你的工具模式 (tool schemas) 摄取到上下文窗口中。当错误提示模糊不清时,它们不会提交 bug——它们会默默地重试、消耗 token,然后放弃。而且它们的数量即将超过人类。

供应商上调 max_tokens 默认值,导致你的尾部响应长度翻倍

· 阅读需 13 分钟
Tian Pan
Software Engineer

你的事故时间线显示没有部署。你的代码没有变。你的流量组合没有变。你的提示词也没有变。然而,你的 p99 输出长度在一周内翻了一番,下游渲染层开始截断响应,而且在流量没有请求更长答案的情况下,你的输出 Token 账单增长了 38%。这种变化是真实的,回归是可以衡量的,但你的版本控制系统中没有记录——因为发生变动的值是你的代码从未发送过的。

供应商提高了一个隐式默认值。发布说明将其归类为“改进的长内容表现”。有问题的参数是 max_tokens,你的应用程序从第一天起就忽略了它,因为文档中记录的默认值很慷慨,而且你的输出很少接近这个值。默认值从 4096 移动到 8192,以适应供应商新模型中更长的推理过程。无论你是否想要,你的应用程序都获得了新的默认值,因为缺少参数本身就是一种配置选择——而供应商拥有更改其背后值的权利。

这种故障模式下,供应商侧的“无操作”(no-op)发布会作为行为变化、成本变化和用户体验(UX)变化同时在你的系统中传播,而你团队唯一的诊断信号是月底寄来的账单。

导致你的智能体重试机制失效两周的工具 Schema 迁移

· 阅读需 12 分钟
Tian Pan
Software Engineer

弃用通知是在周二发出的。下游团队更改了其搜索工具的响应结构 —— results[].snippet 变成了 results[].excerpt,这是一个干净的重命名,有六周的窗口期,文档中有横幅提醒,还给工程邮件列表发了三封提醒邮件。每一个人类用户都迁移了。但 Agent 没有,因为 Agent 不读邮件。在 14 天的时间里,重试循环静默地解析新的有效负载,发现它正在寻找的字段缺失了,抛出了一个 KeyError,并将其计为可重试的故障。重试命中了相同的端点,得到了相同的新结构,抛出了相同的错误,在尝试三次后放弃,并向用户返回了一条致歉消息。重试预算仪表盘在那段时间里一直显示为绿色 —— 重试次数从未 耗尽,它们只是在 预算内永久失败。在该路径上,从工具层测得的成功率为零。但没有人察觉,因为没有触发报警。

这是 2026 年最让工程师头疼的失败形式:不是那种戏剧性的停机,而是隐蔽的契约漂移(Contract Drift)。在这种情况下,面向人类的迁移已经顺利完成,而面向 Agent 的迁移甚至从未开始,因为没有人意识到还需要进行迁移。弃用流程完全按照设计运行,服务于它所设计的使用方。而 Agent 却是一个不在名单上的使用方。

你的代码从未检查过的 Finish Reason

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的处理器(handler)做对了一切。HTTP 状态码是 200。Body 解析成功。文本字段里有内容。你增加了 responses_succeeded 的计数,将消息追加到对话中,把 JSON 返回给客户端,然后继续下一步。用户得到的是一个在句中戛然而止的句子,一个伪装成正常答案的经过编辑的回复,或者是一个被措辞为补全的礼貌拒绝。你的仪表盘对这一切一无所知。供应商已经告诉了你,但你没有读取那个字段。

每个主流的推理 API 都会在文本之外返回一个停止信号:OpenAI 称之为 finish_reason,Anthropic 称之为 stop_reason,Gemini 称之为 finishReason。这个字段很小,每个响应对应一个枚举值。它也是模型用来告诉你刚才发送的响应是一个完整答案还是一个碎片的唯一带外(out-of-band)通道。将其视为无关紧要的装饰,与忽略 HTTP 状态码属于同一种类型的 Bug —— 不同之处在于,你的监控系统在十年前就能捕捉到 HTTP 错误,但对这个错误却无动于衷。

你为人类设置的速率限制,AI 智能体三秒钟就会让其饱和

· 阅读需 11 分钟
Tian Pan
Software Engineer

速率限制从来就不是一种公平性原语。它只是一个逐渐“演化”而来的销售工程指标——是三年前某个解决方案工程师在客户接入期间随手写进文档、被复制到套餐定义中,且由于从未有人触发过而从未被重新审视的一个数字。这个限制写着“每分钟 100 次请求”,其真实含义是“超出了任何理性的集成方案的需求”,因为当时平台上的每一个集成都是由人类在键盘前驱动的后端服务,而人类每分钟敲不了 100 次字。

然后,一个付费租户将一个智能体(agent)指向了该端点。智能体不会打字。它不会为了阅读响应而停顿。它没有需要在请求之间渲染的 UI。它执行一个规划循环,每一个推理步骤调用一次 API,而模型制定一个推理步骤只需要大约 30 毫秒的实际时间。智能体在 3 秒内就触及了每分钟的限额,在 3 分钟内触及了每小时的限额,而在轮值工程师的咖啡还没变凉之前,它就已经耗尽了每日配额。在限流仪表盘更新之前,技术支持的升级请求就已经送达了。

你的智能体把指针当成了值:工具输出里的引用 vs 值

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个搜索工具返回了十个文档 ID。一个素材工具返回了一个 S3 预签名 URL。一个数据库工具返回了一行的 handle。一个文件工具返回了一条路径。这些返回值,从形式上看,全都是指针——一串简短的字符串,命名着智能体当前还没有真正拿到手的那个值。模型接下来怎么走,完全取决于它是否意识到这一点、并在推理之前先做一次解引用,还是说它把指针当成了对象本身。

这个失败模式在 trace 里是看不见的。工具调用成功了。返回结构正常。模型也输出了看上去合理的文本。日志里没有任何一行会说:"智能体在对一个文件名做推理,并把它当成了文档。"指针 vs 值的混淆,发生在可见行为底下的那一层——一个你的工具 schema 从未命名过的层。