第32期 | OpenAI API接入实战
第32期 | OpenAI API接入实战
🎯 今天你将学会
- 从零搭建 OpenAI API 调用环境(SDK 安装 + Key 管理)
- 实现完整的后端代理接口(非流式 + 流式两种模式)
- 射装可复用的 API 调用层——不是写一次就丢的临时代码
- 处理所有常见错误:网络超时、token 超限、格式异常、rate limit
📖 核心知识
从零开始:环境搭建
Step 1:注册 OpenAI API
- 访问 https://platform.openai.com/signup 注册账号
- 进入 https://platform.openai.com/api-keys 创建 API Key
- 复制 Key,存到环境变量(绝不写进代码)
Step 2:环境变量管理
# .env.local(前端项目,Next.js 会自动加载)
OPENAI_API_KEY=sk-xxxxxxxxxxxx
# .env(后端项目,Node.js 用 dotenv 加载)
OPENAI_API_KEY=sk-xxxxxxxxxxxx
⚠️ .env 文件绝不提交到 Git——在 .gitignore 中加上 .env*。
Step 3:安装 SDK
# 前端项目(Next.js)
pnpm add openai
# 纯后端项目(Express)
npm install openai dotenv
为什么用官方 SDK 而不是直接 fetch?
| 方式 | 优点 | 缺点 |
|---|---|---|
| 直接 fetch | 最简单,几行代码 | 要自己处理认证、超时、重试、类型 |
| openai SDK | 类型完整、自动重试、流式支持 | 多一个依赖 |
生产环境用 SDK——它帮你处理了 90% 的边界情况。
后端代理:完整实现
非流式接口(简单场景):
// app/api/ai/completion/route.ts (Next.js App Router)
import OpenAI from 'openai';
import { NextRequest, NextResponse } from 'next/server';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // 从环境变量读取
});
export async function POST(req: NextRequest) {
try {
const { prompt, systemPrompt } = await req.json();
// 参数校验
if (!prompt || prompt.trim().length === 0) {
return NextResponse.json(
{ error: 'Prompt 不能为空' },
{ status: 400 }
);
}
if (prompt.length > 4000) {
return NextResponse.json(
{ error: 'Prompt 太长,请缩减到 4000 字以内' },
{ status: 400 }
);
}
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini', // 用 mini 版降低成本
messages: [
{ role: 'system', content: systemPrompt || '你是一个技术助手。' },
{ role: 'user', content: prompt },
],
temperature: 0.7, // 0-2,越高越随机,技术回答建议 0.3-0.7
max_tokens: 2000, // 控制成本
});
return NextResponse.json({
content: completion.choices[0].message.content,
usage: completion.usage, // token 使用量,方便监控成本
});
} catch (error) {
// 统一错误处理
if (error instanceof OpenAI.APIError) {
return NextResponse.json(
{ error: `API Error: ${error.message}`, code: error.code },
{ status: error.status || 500 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
流式接口(聊天场景):
// app/api/ai/chat/route.ts
import OpenAI from 'openai';
import { NextRequest } from 'next/server';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export async function POST(req: NextRequest) {
const { messages, systemPrompt } = await req.json();
// 流式响应必须用 StreamTextResponse 或手动设置 SSE headers
const stream = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: systemPrompt || '你是一个技术助手。' },
...messages, // 多轮对话:传入完整消息历史
],
stream: true, // ← 开启流式
temperature: 0.7,
});
// 创建 SSE 流式响应
const encoder = new TextEncoder();
const readableStream = new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
// SSE 格式:data: {json}\n\n
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ content })}\n\n`)
);
}
}
// 流结束标记
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
} catch (error) {
controller.error(error);
}
},
});
return new Response(readableStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
前端调用层:封装可复用的 API 客户端
不要在每个组件里直接 fetch——封装一个统一的 API 客户端,处理认证、重试、错误。
// lib/ai-client.ts
interface CompletionRequest {
prompt: string;
systemPrompt?: string;
}
interface CompletionResponse {
content: string;
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
}
interface ChatRequest {
messages: { role: 'user' | 'assistant' | 'system'; content: string }[];
systemPrompt?: string;
}
class AIClient {
private baseUrl: string;
constructor(baseUrl = '/api/ai') {
this.baseUrl = baseUrl;
}
// 非流式调用
async completion(req: CompletionRequest): Promise<CompletionResponse> {
const response = await this.fetchWithRetry(`${this.baseUrl}/completion`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req),
});
if (!response.ok) {
const error = await response.json();
throw new AIError(error.error, response.status, error.code);
}
return response.json();
}
// 流式调用
async chatStream(req: ChatRequest): Promise<ReadableStream<string>> {
const response = await this.fetchWithRetry(`${this.baseUrl}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req),
});
if (!response.ok) {
const error = await response.json();
throw new AIError(error.error, response.status, error.code);
}
return this.parseSSEStream(response.body!);
}
// SSE 流解析
private parseSSEStream(rawStream: ReadableStream<Uint8Array>): ReadableStream<string> {
const decoder = new TextDecoder();
let buffer = '';
return new ReadableStream({
async start(controller) {
const reader = rawStream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
controller.close();
return;
}
try {
const parsed = JSON.parse(data);
if (parsed.content) {
controller.enqueue(parsed.content);
}
} catch {
// 忽略无法解析的行
}
}
}
}
} catch (error) {
controller.error(error);
}
},
});
}
// 带重试的 fetch
private async fetchWithRetry(url: string, options: RequestInit, maxRetries = 3): Promise<Response> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Rate limit — 等待后重试
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('retry-after') || '5');
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
return response;
} catch (error) {
if (attempt === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
}
}
throw new AIError('Max retries exceeded', 500);
}
}
// 自定义错误类
class AIError extends Error {
status: number;
code?: string;
constructor(message: string, status: number, code?: string) {
super(message);
this.status = status;
this.code = code;
}
}
export const aiClient = new AIClient();
export { AIError };
成本控制:你必须知道的事
OpenAI API 按 token 计费。不了解成本模型,你的应用可能一个月烧掉几千块。
GPT 模型定价(2026年参考):
| 模型 | 输入价格 | 输出价格 | 适用场景 |
|---|---|---|---|
| gpt-4o-mini | $0.15/1M tokens | $0.60/1M tokens | 聊天、摘要、分类 |
| gpt-4o | $2.50/1M tokens | $10/1M tokens | 复杂推理、代码生成 |
| gpt-4-turbo | $10/1M tokens | $30/1M tokens | 最复杂任务 |
token 是什么? 大约 1 个英文单词 = 1 token,1 个中文字 ≈ 2 tokens。一篇 500 字的中文文章约 1000 tokens。
成本控制策略:
| 策略 | 做法 | 节省效果 |
|---|---|---|
| 模型降级 | 日常用 gpt-4o-mini,复杂任务才用 gpt-4o | 80% 成本节省 |
| max_tokens 控制 | 设 max_tokens=500 防止 AI 写太长 | 50% 输出成本 |
| 缓存相似问题 | 相同/相似问题缓存答案,不重复调用 | 30-50% 节省 |
| 压缩 prompt | system prompt 精简,不要写长篇大论 | 20% 输入成本 |
| 批量请求 | 需要多个结果时用 batch API | 50% 成本节省 |
错误处理:完整的错误类型覆盖
// lib/ai-errors.ts
export function handleAIError(error: unknown): { message: string; action: string } {
if (error instanceof AIError) {
switch (error.status) {
case 401:
return { message: 'API Key 无效', action: '检查环境变量中的 OPENAI_API_KEY' };
case 429:
return { message: '请求过多,请稍后再试', action: '等待后重试或升级 API 限额' };
case 500:
return { message: 'OpenAI 服务暂时不可用', action: '稍后重试' };
case 400:
if (error.code === 'context_length_exceeded') {
return { message: '输入内容太长', action: '缩减 prompt 长度或切换到更大模型' };
}
return { message: '请求格式错误', action: '检查请求参数' };
default:
return { message: `未知错误: ${error.message}`, action: '联系管理员' };
}
}
if (error instanceof TypeError && error.message.includes('fetch')) {
return { message: '网络连接失败', action: '检查网络连接' };
}
return { message: '未知错误', action: '请稍后重试' };
}
常见误区
误区1:直接从前端调用 OpenAI API
API Key 暴露在前端 = 任何人都能盗用。必须后端代理。
误区2:不用 SDK,自己拼 HTTP 请求
SDK 帮你处理了类型、重试、流式解析。自己拼 HTTP 请求需要处理大量边界情况,容易出错。
误区3:不做成本控制
一个用户连续聊 30 分钟可能消耗 10 万 tokens = $0.06(gpt-4o-mini)。如果 1000 个用户每天聊 30 分钟 = $60/天 = $1800/月。必须设限。
🤖 AI协作实战
实战场景:封装完整的 AI 调用层
我给 AI 的 prompt:
@lib/ai-client.ts
帮我优化这个 AI 调用层,添加以下功能:
1. 请求超时处理(30秒非流式,流式不设超时)
2. Token 使用量追踪(每次调用记录 usage)
3. 请求取消(AbortController 支持)
4. 流式响应的中断处理(用户手动停止生成)
遵循项目 .cursorrules 规范。
AI 补充的关键代码:
// 超时处理
private async fetchWithTimeout(url: string, options: RequestInit, timeout = 30000): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof DOMException && error.name === 'AbortError') {
throw new AIError('Request timeout', 408);
}
throw error;
}
}
// Token 使用量追踪
interface TokenUsage {
totalRequests: number;
totalTokens: number;
totalCost: number;
lastRequestAt: string;
}
const usageTracker = {
data: { totalRequests: 0, totalTokens: 0, totalCost: 0, lastRequestAt: '' } as TokenUsage,
track(usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number }) {
this.data.totalRequests++;
this.data.totalTokens += usage.total_tokens;
// gpt-4o-mini 成本估算
this.data.totalCost += (usage.prompt_tokens * 0.00015 + usage.completion_tokens * 0.0006) / 1000;
this.data.lastRequestAt = new Date().toISOString();
// 持久化到 localStorage
localStorage.setItem('ai_usage', JSON.stringify(this.data));
},
getReport(): TokenUsage {
return this.data;
},
};
我的审查:
- ✅ 超时处理合理——非流式 30s,流式不限
- ✅ Token 追踪实用——方便监控成本
- ❌ 成本估算硬编码了 gpt-4o-mini 的价格——应该做成可配置的,因为可能切换模型
- ✅ AbortController 支持完整
学到了什么: AI 帮你补齐了你自己不容易想到的功能(超时、取消、追踪)。但模型价格硬编码需要你根据实际使用的模型来调整。
💻 动手练习
练习1(简单):搭建后端代理接口
用 Next.js API Routes 或 Express 实现一个最简单的 /api/ai/completion 接口:
- 接收 prompt 参数
- 调用 OpenAI API
- 返回结果
- API Key 存在环境变量中
练习2(中等):封装完整的 AI 客户端
根据本期的 AIClient 代码,实现一个完整的前端调用层,包含:
- 非流式 + 流式两种模式
- 重试机制(最多 3 次)
- 错误处理(覆盖 4 种常见错误)
- Token 使用量追踪
练习3(挑战):实现流式聊天接口 + 前端渲染
完整实现后端 SSE 流式 + 前端逐字渲染:
- 后端:流式转发 OpenAI 响应
- 前端:ReadableStream 读取 + 逐字追加到 UI
- 支持中断(用户点击停止按钮,中止流式传输)
- 显示 token 使用量统计
📌 本期要点
- API Key 安全: 环境变量存储,后端代理转发,绝不暴露在前端代码
- 官方 SDK > 自己拼 HTTP: 类型完整、自动重试、流式支持,省大量边界处理
- 流式响应实现: SSE 格式 + ReadableStream 解析 + 逐 token 推送到前端
- 成本控制: 模型降级(gpt-4o-mini 日常用)+ max_tokens 限制 + 缓存 + token 追踪
- 错误处理全覆盖: 401(Key无效) + 429(限流) + 500(服务不可用) + context_length_exceeded(太长) + 网络超时
🔗 下期预告
下一期我们进入聊天界面开发——消息列表、流式打字效果、Markdown 渲染。你将用 shadcn/ui + react-markdown 实现一个完整的 ChatGPT 风格聊天界面。
如果你没有苹果电脑,需要上传ios到APPStore可以访问以下网站
iPA上传工具 - IPA解析与AppStore提交
更多推荐


所有评论(0)