AI API设计踩坑记录
先复现,再谈优化。
“The art of programming is the art of organizing complexity.” — Edsger Dijkstra
做了这么多年API,才发现最痛苦的去年接手一个AI项目的API重构,前任同事留下的接口列表有几十页,文档写得像法律条文,但真正用起来全是坑。
最初的坑
接手那个项目时,第一个任务是接入第三方AI服务。对方文档齐全,有完整的中英文说明,还有在线测试工具。但真正对接时才发现:
# 对方推荐的调用方式
curl -X POST "https://api.example.com/v1/chat/completions" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "你好"}],
"max_tokens": 100,
"temperature": 0.7,
"stream": false
}'
看起来正常,但问题藏在细节里:
- 文档说
max_tokens默认是 100,实际是 150,导致响应被意外截断 - 错误码只有数字,没有对应的人类可读解释
- 超时时间文档写的是30秒,但实际长请求经常40秒还挂着
第一次上线就收到了一堆投诉:用户反馈"回答突然断掉"是因为超时,“不知道错在哪"是因为错误信息太简略,“参数没生效"是因为默认值和文档对不上。
从这些坑里爬出来后,开始系统性地整理设计原则
URL和资源命名
不要为了统一而统一,要为语义清晰服务。
# 不好的设计
GET /api/v1/ai-chat-completions-get
POST /api/v1/ai-chat-completions-create
# 好的设计
GET /api/v1/completions
POST /api/v1/completions
资源名用复数,动词用HTTP方法表达。例外场景再单独处理,比如批量操作:
# 批量删除不是每个资源单独DELETE
DELETE /api/v1/completions?ids=1,2,3
请求参数设计
去年遇到一个头疼的问题:同事把所有参数都塞进了query string,导致URL过长被截断。
# 容易出问题的方式
GET /api/v1/completions?model=gpt-4&messages=...&temperature=0.7&max_tokens=100&top_p=0.9&frequency_penalty=0.1&presence_penalty=0.1
# 正确的方式
POST /api/v1/completions
Content-Type: application/json
{
"model": "gpt-4",
"messages": [{"role": "user", "content": "..."}],
"temperature": 0.7,
"max_tokens": 100,
"top_p": 0.9,
"frequency_penalty": 0.1,
"presence_penalty": 0.1
}
简单原则:参数多、结构复杂时用请求体;参数少、简单时可以用query。
响应结构统一
响应结构要统一,但不要为了统一而做无意义的包装。
// 过度包装
{
"code": 0,
"message": "success",
"data": {
"result": "你好",
"usage": {"prompt_tokens": 10, "completion_tokens": 2}
},
"timestamp": 1699876543210,
"request_id": "req_xxx"
}
// 实用主义
{
"id": "cmpl_xxx",
"object": "chat.completion",
"created": 1699876543,
"model": "gpt-4",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "你好"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12}
}
第二种的优点是字段都有明确语义,不需要额外解析data里的内容。错误情况统一处理:
// 错误响应
{
"error": {
"message": "Invalid API key provided",
"type": "invalid_request_error",
"param": null,
"code": "invalid_api_key"
}
}
错误处理是开发体验的分水岭
接手那个项目时,最让我头疼的是错误处理。用户报错说"接口返回500”,但日志里只有一行空白的异常堆栈。
错误码设计
错误码要分层级,不能只是数字。
# 好的错误码设计
class ErrorCode:
# 系统级错误(1xxx)
INTERNAL_ERROR = "1001"
SERVICE_UNAVAILABLE = "1002"
TIMEOUT = "1003"
# 客户端错误(2xxx)
INVALID_REQUEST = "2001"
INVALID_API_KEY = "2002"
RATE_LIMIT_EXCEEDED = "2003"
QUOTA_EXCEEDED = "2004"
# 业务错误(3xxx)
MODEL_NOT_AVAILABLE = "3001"
CONTENT_FILTERED = "3002"
CONTEXT_LENGTH_EXCEEDED = "3003"
数字可以快速定位问题层级,字符串描述具体错误类型。
错误信息要可读且可机读
// 不可读的错误
{
"error": "ERR_INVALID_PARAM"
}
// 可读的版本
{
"error": {
"message": "The 'model' field is required and must be a non-empty string",
"type": "invalid_request_error",
"param": "model",
"code": "invalid_parameter",
"details": {
"expected_type": "string",
"received": null
}
}
}
这样既能快速定位问题,又能把错误信息直接展示给用户。
错误信息要帮助用户解决问题
去年接手一个项目时,用户报错说"Quota exceeded”,但不知道是次数超了还是金额超了,也不知道什么时候恢复。
// 帮助用户解决问题的错误信息
{
"error": {
"message": "You have exceeded your API quota. Your current limit is 1000 requests per day, and you have made 1005 requests. The quota will reset at 2026-07-18T00:00:00+08:00.",
"type": "rate_limit_error",
"code": "quota_exceeded",
"details": {
"limit": 1000,
"used": 1005,
"resets_at": "2026-07-18T00:00:00+08:00"
},
"documentation_url": "https://docs.example.com/rate-limits"
}
}
这样用户就知道具体是什么问题,什么时候能恢复,去哪查看更多信息。
文档不是装饰品
接手那个项目时,文档写得很详细,但问题是:没人愿意看,看了也记不住。
文档要实用,不要只是API列表
去年做了一个改动:在每个API的文档里加入实际可运行的curl示例。
# 文档中的示例
curl -X POST "https://api.example.com/v1/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "Say hello"}],
"max_tokens": 10
}' | jq
而且这个示例是真的可以运行的,开发者可以复制粘贴直接试。
文档要示例化,不要只列参数
# 传统的参数列表文档
## Parameters
- model: string, required. The ID of the model to use.
- messages: array, required. The messages to generate chat completions for.
- max_tokens: integer, optional. The maximum number of tokens to generate.
# 示例化的文档
## Quick Start
```bash
curl -X POST "https://api.example.com/v1/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "What is 2+2?"}]
}' | jq
# Common Patterns
### Single turn conversation
```bash
curl -X POST "https://api.example.com/v1/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "Your question"}]
}' | jq
### Multi-turn conversation
```bash
curl -X POST "https://api.example.com/v1/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "First question"},
{"role": "assistant", "content": "First answer"},
{"role": "user", "content": "Follow-up question"}
]
}' | jq
开发者看到的是真实场景,而不是参数的堆砌。
文档要有版本和变更记录
文档不是静态的,必须跟着API一起演进。
## Changelog
### 2026-07-17
- Added `stream` parameter to enable streaming responses
- Deprecated `stop_sequences` parameter, use `stop` instead
- Increased `max_tokens` limit from 4096 to 8192
### 2026-06-15
- Added `top_p` parameter for nucleus sampling
- Fixed bug where `temperature` was not being applied correctly
这样用户知道什么时候改了什么,什么时候需要检查自己的代码。
开发者体验是最终评判标准
做了这么多API设计,最终发现一个残酷的事实:技术选型再正确,代码写再好,如果用起来不顺手,别人就不会用。
提供多个SDK
不是所有人都喜欢用curl。
# Python SDK
from example_client import Client
client = Client(api_key="your-api-key")
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Hello"}]
)
print(response.choices[0].message.content)
# Node.js SDK
import { Client } from 'example-client';
const client = new Client({ apiKey: 'your-api-key' });
const response = await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }]
});
console.log(response.choices[0].message.content);
SDK要做的是降低接入成本,不是完全掩盖API。
提供调试工具
去年做了一个简单的在线调试工具,让开发者可以在页面上测试API。
这个工具的价值不是替代SDK,而是让开发者快速验证想法。
提供回调支持
不是所有请求都能同步完成。
# 同步调用
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Write a long story"}],
timeout=60
)
# 异步调用
job = client.chat.completions.create_async(
model="gpt-4",
messages=[{"role": "user", "content": "Write a long story"}],
webhook_url="https://your-domain.com/webhook"
)
# 轮询状态
status = client.jobs.get(job.id)
if status.status == "completed":
result = client.jobs.get_result(job.id)
长任务要支持异步,避免客户端超时。
踩过的几个具体坑
Content-Type的问题
去年遇到一个诡异的问题:同事的代码总是返回 Unsupported Media Type,但明明设置了 Content-Type: application/json。
排查了很久才发现,对方用的是 Content-Type: application/json; charset=utf-8,服务器只检查了 application/json。
# Nginx配置问题
location /api/v1/ {
if ($http_content_type != "application/json") {
return 415;
}
proxy_pass http://backend;
}
# 修复后
location /api/v1/ {
if ($http_content_type !~* "^application/json") {
return 415;
}
proxy_pass http://backend;
}
检查Content-Type要用正则,不要做精确匹配。
时间格式的问题
接口文档说日期格式是 2026-07-17T22:19:21+08:00,但实际测试时发现返回的是 2026-07-17 22:19:21。
# 文档中的定义
from datetime import datetime
from pydantic import BaseModel
class CompletionRequest(BaseModel):
created_at: datetime # 默认输出ISO格式
# 实际输出
{"created_at": "2026-07-17T22:19:21+08:00"}
# 如果数据库存的是datetime类型,直接转JSON可能变成
{"created_at": "2026-07-17 22:19:21"}
要明确时区处理逻辑,最好统一用UTC加时区偏移。
流式响应的坑
做流式响应时,发现客户端总是收不到完整的数据。
# 服务端代码
def stream_chat_completion(request):
def generate():
yield "data: {\"choices\": [{\"delta\": {\"content\": \"Hello\"}}]}\n\n"
yield "data: [DONE]\n\n"
return Response(generate(), content_type="text/event-stream")
# 问题在于没有设置合适的header
def stream_chat_completion(request):
def generate():
yield "data: {\"choices\": [{\"delta\": {\"content\": \"Hello\"}}]}\n\n"
yield "data: [DONE]\n\n"
response = Response(generate(), content_type="text/event-stream")
response.headers["Cache-Control"] = "no-cache"
response.headers["Connection"] = "keep-alive"
response.headers["X-Accel-Buffering"] = "no" # Nginx需要这个
return response
流式响应要禁用缓存和缓冲,否则数据会被积压在中间层。
最后几点实践判断
做了这么多API设计,有几个经验判断可以分享:
统一是手段,不是目的
不是所有东西都要统一,统一是为了降低认知负担。如果强制统一让代码变得更复杂,那就偏离了初衷。
文档要活在代码里
文档和代码不同步是常态。解决办法不是写更好的文档,而是让代码自己说话。
# 用代码作为文档
from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
app = FastAPI()
class Message(BaseModel):
role: str = Field(..., description="The role of the message author (user, assistant, system)")
content: str = Field(..., description="The content of the message")
class CompletionRequest(BaseModel):
model: str = Field(..., description="The ID of the model to use")
messages: list[Message] = Field(..., description="The messages to generate completions for")
max_tokens: int = Field(100, ge=1, le=4096, description="Maximum tokens to generate")
@app.post("/v1/completions")
async def create_completion(request: CompletionRequest):
"""
Create a chat completion.
- model: The model to use (e.g., "gpt-4")
- messages: Array of message objects
- max_tokens: Maximum number of tokens to generate (1-4096)
"""
# ...
FastAPI这类框架会自动生成文档,而且文档永远和代码同步。
错误处理要投入精力
错误处理往往被认为是"边际工作",但它的投入产出比其实很高。一个好的错误处理能让用户自己解决80%的问题,减少客服和研发压力。
用户体验比技术正确性重要
API也是产品,最终要由用户来评判。技术方案再优雅,如果用起来不顺手,那就是失败的设计。
这次折腾下来,最深的感受是:API设计不是技术问题,是沟通问题。你在和开发者对话,要让对方听懂、能用、愿意用。
技术再好,用不上也没用。
版权声明: 本文首发于 指尖魔法屋-AI API设计踩坑记录(https://blog.thinkmoon.cn/post/201-api-design-ai-experience/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。