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
  }'

看起来正常,但问题藏在细节里:

  1. 文档说 max_tokens 默认是 100,实际是 150,导致响应被意外截断
  2. 错误码只有数字,没有对应的人类可读解释
  3. 超时时间文档写的是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。

graph LR A[开发者] --> B[在线调试工具] B --> C[构建请求] C --> D[调用API] D --> E[显示响应] E --> F[生成curl代码] F --> A

这个工具的价值不是替代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/) 转载或引用必须申明原指尖魔法屋来源及源地址!