第一章:环境搭建与基础调用

1.1 初始化项目并安装依赖

npm init -y
npm install openai express dotenv cors morgan express-rate-limit

openai 为官方 SDK(v4+),express 构建 API,其余是生产级中间件。

1.2 环境变量配置

创建 .env 文件:

OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
MODEL_NAME=gpt-4o-mini

⚠️ .env 禁止提交到 Git,建议提供 .env.example 模板。生产环境使用密钥管理服务。

1.3 最小可用示例

import OpenAI from 'openai';
import 'dotenv/config';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function chat() {
  const completion = await openai.chat.completions.create({
    model: process.env.MODEL_NAME,
    messages: [{ role: 'user', content: '你好,请用一句话介绍你自己。' }],
  });
  console.log(completion.choices[0].message.content);
}

chat();

运行 node src/index.js,成功输出回答即表示调用成功。

第二章:构建核心 API 服务

将 GPT 封装为 RESTful API,便于前端或其他服务调用。

2.1 Express 路由与请求校验

import { Router } from 'express';
import { chatCompletion } from '../services/openaiService.js';
import { validateBody } from '../middleware/validation.js';

const router = Router();

router.post('/chat', validateBody, async (req, res, next) => {
  try {
    const result = await chatCompletion(req.body.messages);
    res.json({ success: true, data: result });
  } catch (err) {
    next(err);
  }
});

export default router;

使用 joi 校验 messages 数组的 rolecontent 字段。

2.2 直接返回 vs 流式返回

直接返回适合短文本;流式返回(SSE)设置 stream: true,逐字推送,适合聊天机器人。

流式实现关键代码:

export async function streamChat(messages, res) {
  const stream = await openai.chat.completions.create({
    model: process.env.MODEL_NAME, messages, stream: true,
  });
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content || '';
    if (content) res.write(`data: ${JSON.stringify({ content })}\n\n`);
  }
  res.write('data: [DONE]\n\n');
  res.end();
}

可在路由上增加 ?stream=true 参数动态切换。

2.3 生产级中间件

import cors from 'cors';
import rateLimit from 'express-rate-limit';
import morgan from 'morgan';

app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(rateLimit({ windowMs: 60 * 1000, max: 30 }));
app.use(morgan('combined'));

配置 CORS 白名单、频率限制、访问日志,让服务从“能跑”升级到“能上线”。

第三章:多轮对话与上下文管理

聊天机器人需要维护对话历史。

3.1 消息结构与 Token 压缩

每条消息包含 role(user/assistant/system)和 content,整个会话用数组保存。使用 tiktoken 实现压缩函数,超出最大 token 时移除最早的非 system 消息:

import { getEncoding } from 'tiktoken';
const encoding = getEncoding('cl100k_base');

export function trimConversation(messages, maxTokens = 4096) {
  let total = 0;
  const reversed = messages.slice().reverse();
  const keep = [];
  for (const msg of reversed) {
    const tokens = encoding.encode(msg.content).length;
    if (total + tokens > maxTokens) break;
    total += tokens;
    keep.unshift(msg);
  }
  return keep;
}

也可将早期消息摘要后替换,但会额外消耗 token。

3.2 Redis 持久化

使用 sessionId 将会话存入 Redis,支持多实例部署:

import { createClient } from 'redis';
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();

export async function getSession(sessionId) {
  const data = await client.get(`chat:${sessionId}`);
  return data ? JSON.parse(data) : [];
}

export async function saveSession(sessionId, messages) {
  await client.set(`chat:${sessionId}`, JSON.stringify(messages), { EX: 3600 });
}

第四章:函数调用与动态模型选择

让 GPT 执行外部操作(如查数据库、调用第三方 API)。

4.1 业务场景:查询订单状态

定义 functions 参数描述接口,GPT 返回 tool_calls 后由服务端执行并回传结果。

const functions = [
  {
    name: 'get_order_status',
    description: '根据订单ID查询订单状态',
    parameters: {
      type: 'object',
      properties: { orderId: { type: 'string', description: '订单编号' } },
      required: ['orderId'],
    },
  },
];

执行失败时返回错误描述给 GPT,让其调整回答。

4.2 自动化模型选择与 Token 监控

根据请求特征动态选择模型:

export function selectModel(userMessage, ip) {
  if (userMessage.length < 50) return 'gpt-4o-mini';
  if (userMessage.length > 500) return 'gpt-4o';
  return process.env.DEFAULT_MODEL;
}

从响应获取 usage 数据,累计成本并与阈值比较,通过 webhook 通知。

第五章:错误处理与生产部署

5.1 通用错误处理与重试

export function errorHandler(err, req, res, next) {
  console.error(err.stack);
  if (err.status === 429) return res.status(429).json({ error: '请求过于频繁,请稍后重试' });
  if (err.status === 401) return res.status(401).json({ error: 'API Key 无效' });
  res.status(500).json({ error: '服务内部错误' });
}

使用 async-retry 实现指数退避,区分可重试与不可重试错误:

import retry from 'async-retry';

export async function callWithRetry(fn, maxRetries = 3) {
  return retry(async (bail, attempt) => {
    try {
      return await fn();
    } catch (err) {
      if (err.status === 401) bail(err);
      throw err;
    }
  }, { retries: maxRetries, minTimeout: 1000, factor: 2 });
}

5.2 Docker 部署

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "src/index.js"]

配合 docker-compose.yml 携带 Redis。生产建议使用 PM2 管理进程,Nginx 反向代理并关闭缓冲(proxy_buffering off)以支持 SSE 流式传输。

附录:常见问题 FAQ

Q:用官方 SDK 还是手动请求? A:官方 SDK 封装更完善(流式、重试、类型提示),推荐优先使用。

Q:如何估算成本? A:查看模型定价(每千 token),结合响应 usage 数据计算,建议添加日预算告警。

Q:函数调用返回参数格式不对怎么办? A:使用严格 JSON Schema 约束参数,解析失败时让 GPT 重新生成,循环调用限制 3 次内。

Q:流式传输中客户端断开怎么办? A:服务端监听 req.on('close') 主动释放 GPT 流,客户端实现指数退避重连。