第32期 | OpenAI API接入实战

🎯 今天你将学会

  • 从零搭建 OpenAI API 调用环境(SDK 安装 + Key 管理)
  • 实现完整的后端代理接口(非流式 + 流式两种模式)
  • 射装可复用的 API 调用层——不是写一次就丢的临时代码
  • 处理所有常见错误:网络超时、token 超限、格式异常、rate limit

📖 核心知识

从零开始:环境搭建

Step 1:注册 OpenAI API

  1. 访问 https://platform.openai.com/signup 注册账号
  2. 进入 https://platform.openai.com/api-keys 创建 API Key
  3. 复制 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;
  },
};

我的审查:

  1. ✅ 超时处理合理——非流式 30s,流式不限
  2. ✅ Token 追踪实用——方便监控成本
  3. ❌ 成本估算硬编码了 gpt-4o-mini 的价格——应该做成可配置的,因为可能切换模型
  4. ✅ 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 使用量统计

📌 本期要点

  1. API Key 安全: 环境变量存储,后端代理转发,绝不暴露在前端代码
  2. 官方 SDK > 自己拼 HTTP: 类型完整、自动重试、流式支持,省大量边界处理
  3. 流式响应实现: SSE 格式 + ReadableStream 解析 + 逐 token 推送到前端
  4. 成本控制: 模型降级(gpt-4o-mini 日常用)+ max_tokens 限制 + 缓存 + token 追踪
  5. 错误处理全覆盖: 401(Key无效) + 429(限流) + 500(服务不可用) + context_length_exceeded(太长) + 网络超时

🔗 下期预告

下一期我们进入聊天界面开发——消息列表、流式打字效果、Markdown 渲染。你将用 shadcn/ui + react-markdown 实现一个完整的 ChatGPT 风格聊天界面。
如果你没有苹果电脑,需要上传ios到APPStore可以访问以下网站
iPA上传工具 - IPA解析与AppStore提交

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐