做AI应用和Agent平台的后端,最常被问的一句话是:“不就是写个接口吗,跟以前给App用有什么区别?” 真做起来才发现,区别大到能把前几周的设计全部推翻。
我最近刚好完成了一个为AI应用构建可扩展、智能体友好的API改造项目。所谓“智能体友好”,说白了就是让大模型和Agent拿到你的接口时,不用反复试错、不用靠猜就能正确调用;所谓“可扩展”,则是当几十个Agent并发循环调用时,服务不被打垮,延迟不失控。这篇博文把整个设计和落地过程完整的拆一遍。适合正在做LLM应用、Agent平台、AI工具链的后端开发者看;前端和全栈如果想把AI功能接到自家系统里,同样值得花十分钟读完。
1. 先想清楚:普通API和AI应用的API差在哪
1.1 消费方变了:人类看文档,模型看Schema
传统API的消费方是人。人可以打开文档,看到接口说明,然后自己理解哪些字段必填、哪些枚举可选、出错时看错误信息再改。人是有常识的,哪怕文档写得不那么清楚,也能猜个八九不离十。
但Agent和大模型调用API时,只能依靠两样东西:模型自身的世界知识,以及你提供给它的工具描述。模型没有“先点点试试”的习惯,它拿到一个OpenAPI描述后,会直接按描述生成参数去调用。如果字段名含糊、类型给得太宽、缺少示例,它会一本正经地生成错误参数,然后报错,再重试,再报错,直到耗尽上下文窗口。
所以“智能体友好”的第一个要求是:API描述本身要像岗位说明书一样精确,不能像用户手册一样需要读者自己发挥。这一条看着简单,却直接决定了模型对你的接口的理解成本。
1.2 三种典型接入模式,决定协议选型
AI应用调用后端接口,归纳起来基本跑不出三种模式。每一种对API的要求都不太一样,我直接说结论。
第一种是普通请求响应模式,适合不需要长连接的查询类操作,比如查订单、查余额、查配置。REST接口加JSON就够了,模型把参数拼好,发过去,拿回结果。
第二种是工具调用模式,也就是Function Calling。模型根据任务决定调哪个工具,传入结构化参数,后端返回结构化结果。这种模式对Schema的严谨程度要求最高,因为它本质上是在“让模型按表格填参数”。
第三种是流式交互模式,适合聊天、生成、长任务进度反馈这类场景。HTTP短连接根本扛不住动辄几百毫秒的首包延迟和长时间的生成过程,需要用到SSE或者WebSocket。
三种模式的选型依据,我列了一张表:
| 接入模式 | 典型场景 | 推荐协议 | 核心关注点 |
|---|---|---|---|
| 请求响应 | 查询类、更新类操作 | REST + JSON | 延迟、限流、幂等 |
| 工具调用 | Agent调用具体能力 | REST + JSON Schema | 参数描述、结构化输出、错误恢复 |
| 流式交互 | 聊天、生成、长任务推送 | SSE / WebSocket | 连接管理、断线重连、事件格式 |
很多团队一上来就全部用REST,等接到流式需求再补SSE,结果接口风格割裂,Agent维护成本高。比较好的做法是:从上表出发,把每一类接口明确归类,再决定要不要引入第二种协议。协议多一层,整体复杂度就上一层,不值得为所有接口都上SSE。
2. 可扩展性设计:看似老生常谈,其实都是新坑
“可扩展”这个词被说烂了,但放到AI应用场景里,新的问题比老问题更烦人。传统接口是用户点了才请求,高峰期有规律可循;Agent则可能在一个任务里循环调用同一个接口几十次,并发模型还会同时拉起多个Agent。突然的流量尖峰是常态,不是异常。
2.1 无状态服务:AI应用的第一道坎
可扩展性的前提是无状态。你的服务实例一旦在内存里存了Session、临时状态、进度信息,扩容就只能靠重启和粘性会话,这在Agent高频调用的场景下基本等于慢性死亡。Agent可能先请求A实例,重试时被负载均衡转到B实例,如果A和B不共享状态,要么数据丢了,要么重复执行。
我把所有会话状态、任务状态全部赶到了外部存储:Redis存临时状态和分布式锁,数据库存最终结果。服务实例变成一堆可以随意启停的无状态进程,扩容只是加副本的事。这里有个容易忽略的细节:外部存储本身也要扛住压力。Redis连接池别设太小,数据库连接数要留余量,否则服务扩容了,存储层反而成了瓶颈。
2.2 限流与配额:按Agent身份分桶
传统限流按IP和用户做,AI场景下这套不够。同一个用户可能通过多个Agent调用,同一个Agent可能用多个用户身份;如果只按用户限流,一个Agent就能把限额吃光。我采用的方案是“三桶限流”:用户维度、Agent维度、接口维度各一个独立的令牌桶。三层都放行才放行,任何一层超限都直接返回429。
令牌桶算法个人觉得最实用,比固定窗口平滑,也比滑动窗口好实现。关键是返回429的时候必须带上Retry-After头,并且响应体里写明是哪个维度触发了限流。模型拿到429后,只要你的错误信息够清楚,它就会等一会儿再试;如果只是笼统的“请求太频繁”,模型不知道该等多久,就会用更激进的频率重试,把限流变成雪崩放大器。
2.3 长任务别占HTTP:任务队列与流式响应
AI应用里常有这类需求:让Agent调一个接口去“分析一周的销售数据”,后端要查库、要跑统计模型、要汇总,可能需要几秒钟甚至更久。如果傻乎乎地在HTTP请求里同步等结果,网关超时、数据库连接被占、下游服务被拖垮,全来了。
我把超过800ms的操作一律改成异步:接口收到请求后立即返回task_id,把实际工作丢进消息队列,由Worker异步处理;Agent通过轮询任务状态或订阅SSE获取最终结果。这一步改造对延迟体验的提升是立竿见影的。同步等待时,一个接口只能同时服务几十个慢请求;改成异步后,同样的资源能扛几百个,而且每个请求都能及时拿到响应,用户体验反而更好。
2.4 缓存与预热:省成本、降延迟
Agent场景下,很多请求其实是重复的。比如多个Agent都问“昨天的订单总量”,查询条件完全一样,结果也一样。这种确定性查询,我用一个以请求参数哈希为Key的缓存来承接,命中率上去之后,接口的P95延迟能下降一大半,后端数据库压力也小很多。
但缓存有个坑:只能缓存和当前用户无关的全局数据。凡是带用户维度、账号维度、权限差异的结果,一律不缓存,否则会出现A用户的数据串给B用户的严重事故。在AI场景里尤其危险,因为模型会把缓存里的错误数据当成事实写进回答。更稳妥的做法是只缓存经过脱敏的汇总型数据,并且设置很短的TTL,宁可多查几次库,也不能给错数据。
3. 智能体友好:让模型“一次就会”的API设计细节
这章是全文的重点。可扩展性是地基,智能体友好是电梯——地基不牢电梯白装,电梯不通楼就废了。
3.1 工具描述:把API写成给模型的“岗位说明书”
模型决定调用哪个函数、填什么参数,依赖的是你提供的工具描述。这个描述的质量,直接决定了调用成功率。我见过太多接口的OpenAPI描述写成这样:参数名叫start_time,描述写“开始时间”,类型是string。模型看了完全不知道格式是时间戳还是YYYY-MM-DD,也不知道时区是什么,只能靠猜。
一个好的参数描述,至少要包含四件事:
| 要素 | 示例 |
|---|---|
| 参数含义 | 查询范围的起始日期 |
| 格式约束 | ISO 8601,如2024-06-01 |
| 边界限制 | 不得早于2020年,不得晚于今天 |
| 用途示例 | 用于按天聚合查询,结束日期需大于起始日期 |
描述语言不要太口语化,模型反而喜欢明确、克制的说明。我在实际项目里的经验是:把每个参数的description当作用户手册的一个小节来写,写清楚“是什么、什么格式、边界在哪、和谁配合”。这样看起来繁琐,但模型调用错误率可以从30%降到5%以内。
另外,OpenAPI里的examples字段非常重要。模型对示例的依赖远超想象,一个贴近真实业务的示例,比十句描述都管用。
3.2 结构化输出:锁死返回格式,减少解析成本
Agent拿到接口返回值后,会把它作为上下文继续推理。如果返回的是自由文本、嵌套极深的JSON、或者字段时有时无,模型读起来费力,解析时更容易出错。智能体友好的接口,返回体必须稳定、扁平、结构清晰。
我在设计返回结构时强制几条规则:
- 统一用
{ "code": 0, "message": "ok", "data": {...} }包裹,业务数据和状态分开; data内部的字段名用语义清晰的完整单词,不用缩写;- 关键枚举字段在返回里直接带可读的
label; - 数量类字段统一标注单位,避免模型把“元”猜成“万元”。
还有一个容易被忽略的点:优先返回summary字段。Agent不总是需要全量明细,它更常需要“结论”。我在列表接口里额外生成一个摘要对象,比如订单列表同时返回总金额、总单数、平均单价。模型拿到摘要直接就可以用来回答用户,不需要自己再遍历数组做计算。实测下来,Agent回答准确率和对上下文的消耗改善都非常明显。
3.3 幂等与重试:模型不按人类规矩来
人类不会对同一个下单接口连续点十次,模型会。Agent在遇到超时、网络抖动、收到不确定错误时,第一反应就是重试。如果你的创建类接口不满足幂等,一次点击变成十遍创建,后果是致命的。
解决方法是给写操作的接口支持幂等键。客户端在请求头里带Idempotency-Key,服务端对这个Key做去重,相同的Key在指定时间内返回第一次的结果,而不是重复执行业务逻辑。实现上我是用Redis里的SETNX加过期时间来实现;数据库层面再给业务表加唯一索引,双保险。
这个设计不需要给Agent额外的提示,模型天然会遵守幂等键的逻辑,因为大部分主流Agent框架生成的请求里本来就会带上请求ID。如果你的API不支持,框架就会生成一个却没用上,浪费了一次防错的机会。对Agent而言,一个接口“可安全重试”是比“响应快”更重要的品质。
3.4 错误信息设计:让Agent能自己“改正归邪”
传统API的错误信息是人看的,所以写“用户名或密码错误”就够了。Agent拿到这种提示,不知道自己该干什么。理想的错误响应,不仅要说明“错在哪”,还要说明“怎么改”。
我采用的错误结构是:
{ "code": 40001, "message": "参数 start_date 格式不正确", "hint": "请使用 ISO 8601 格式,例如 2024-06-01,且不晚于 end_date", "recoverable": true }recoverable这个字段尤其关键。它告诉模型这个错误是因为调用方参数问题还是服务端问题。模型识别到recoverable: true,就会尝试修正参数后重试;如果是false,它就知道重试无意义,转而向用户请求更多信息。这一招让Agent在业务场景里的“自愈率”大幅提升,也减少了大量无谓的重试请求。
所有非200状态码,都要在错误响应里附上可读的hint。不要在错误提示里写“详见文档”——模型找不着文档,只会继续瞎试。
4. 实操过程:一个真实项目的API改造实录
前面讲了一堆原则,这章说点实际的。用一个我近期经手的模拟项目X来做复盘:内部业务系统要接一个销售数据查询Agent,老接口是一堆给管理后台用的REST API,参数缩写多、返回嵌套深、没有限流、错误信息约等于没有。
4.1 项目背景与目标
项目目标很明确:让Agent能自主完成“查销量、做汇总、看趋势”三类任务。不是让人去调接口,是让模型自动调。这意味着老接口那套“给程序员看”的设计必须重来一遍。我给自己定了三条验收标准:Agent调用接口的参数错误率低于5%;接口能扛住10个Agent并发循环调用;P95延迟稳定在500ms以内。
4.2 梳理老接口,重写OpenAPI描述
拿到老接口,第一件事是把每个接口的数据字典梳理一遍。原接口里有start_dt、end_dt、cgy这种缩写字段,人看都得猜,模型更猜不动。我把它们全部重命名为完整语义字段,同时统一了时间格式、金额单位、分页参数。
然后给全部接口写了一份新的OpenAPI规范。重点在于每个参数的description和examples都按第3章的标准填满,返回结构全部改为标准的code/message/data包裹,并在列表接口里增加summary摘要块。这一步花在写描述上的时间占了三成,但事后证明每一分钟都值回票价。
4.3 鉴权、幂等与限流的落地
鉴权方面,考虑到Agent不像人有交互输入验证码的能力,直接采用静态API Key加作用域(Scope)的方式。Key绑定运行环境和允许调用的接口集合,比如只读Agent只能调查询接口,写操作接口不开放给这个Key。
幂等方面,写接口(如创建告警、生成报表)统一要求Idempotency-Key请求头,Redis里去重,过期时间设24小时。限流按前面说的用户、Agent、接口三桶方案配置。最开始的版本我踩了一个坑:给Agent的配额只按接口总额算,结果某个Agent疯狂刷一个低效接口,把整个集群的数据库连接池打满了。后来改成每个接口独立的令牌桶,才把问题解决。
4.4 压测结果与上线效果
改造完成后,我用一套模拟真实Agent行为的脚本做了压测:10个并发调用方,每个按“查询-汇总-创建报告”的任务循环执行300次。结果如下:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 参数错误率 | 约30% | 约3% |
| P95延迟 | 1200ms | 320ms |
| 单实例并发承载 | 50并发开始抖 | 200并发稳定 |
| 失败重试次数 | 任务级重复 | 幂等去重后基本为0 |
参数错误率从30%降到3%,核心原因就是OpenAPI描述重写和结构化返回。延迟下降则是因为无状态化后连接池利用率上去了,加上缓存分担了大量重复查询。这次改造最大的体会是:智能体友好不是给Agent“开后门”,而是从协议、语义、错误处理三个层面把API设计得让机器能顺畅使用。
5. 常见问题与排查技巧实录
再分享几个线上实际踩过的坑,都是那种不亲自跑一遍很难意识到的细节。
5.1 模型反复报参数错误:先查Schema设计
现象是Agent调用接口失败率特别高,日志里全是参数格式错误。90%的情况是因为Schema里类型定得太宽。比如金额字段写了number,但实际业务要求两位小数;字段枚举没列全,或者描述里没写默认值。排查方法很简单:拿一次真实请求的日志,把Agent传给接口的参数和Schema对照,看看哪个字段跟描述不符,然后修正描述,而不是去改Agent提示词。改Schema比改Prompt可靠得多。
5.2 并发一上来延迟就崩
服务单机跑着挺稳,一上并发P95就飙升。我遇到过一次,查了半天发现是数据库连接池设置太小,连接排队时间直接把延迟顶上去了。另一次是被外部依赖拖累:一个查询接口内部调第三方服务,第三方偶尔抖动,又没有超时和熔断,导致所有请求堵在那。这两个问题的共性都是“瓶颈在别处”。排查顺序建议是:网络层看实际建立连接的耗时,再看数据库连接池告警,再看外部依赖调用耗时。给所有外部调用加上超时和熔断,是必须做的基础操作。
5.3 返回结果被模型看懂但用错
接口返回200了,Agent也成功解析了,但回答里的数字和接口返回不一致,经常差一个数量级。这种大多是单位不明确引起的。比如接口返回金额是amount,没写单位,模型默认理解成元,实际业务用的是万元。修复方法是:字段名改成amount_in_yuan,同时描述里写清楚“单位:元,保留两位小数”,返回体加一个currency字段。用自描述字段替代隐含约定,是智能体友好API最实在的改进之一。
5.4 限流误伤正常用户
限流配置上线后,正常用户的请求也被429了。原因是Agent和用户走的同一个接口,配额只在Agent维度做了限制,用户维度却漏了。结果Agent的循环调用把共享的实例连接全部消耗掉,用户请求也跟着遭殃。后来把所有接口的限流维度统一改为“用户+Agent+接口”三层,并且给内部白名单接口单独开通高配额通道,才彻底解决。限流配置一定要在压测阶段就验证好,别等上线了才发现误伤。
踩过这一圈坑,我个人的体会是:为AI应用构建API,本质上是把接口从“给人看的文件”改造成“给机器看的协议”。人可以在模糊信息下做判断,模型不能;人可以容忍不良描述,Agent只会拿着描述一本正经地犯错。所以可扩展性解决的是“扛不扛得住”的问题,智能体友好解决的是“用不用得对”的问题。两件事都做扎实了,AI应用才算真正接上了地气。后续如果接入了多模态模型,接口的上传协议和流式返回还会再迭代一轮,但底层的无状态、幂等、结构化、可恢复错误这几个设计原则,放到哪个阶段都不会过时。