上一篇 下一篇 分享链接 返回 返回顶部

别再只会换便宜模型了:ChatGPT API 成本优化实战与源码分享

发布人:慈云数据-客服中心 发布时间:17小时前 阅读量:6

ChatGPT 如何降低成本|附源码

在很多团队开始接入 ChatGPT 或其他大模型 API 之后,最先感受到的往往不是技术难度,而是成本压力。尤其是当产品从 Demo 进入真实用户场景后,请求量上升、上下文变长、重复问题增多、模型调用链复杂化,账单很容易快速增长。

很多人降低成本的第一反应是:“换更便宜的模型”。这当然是一种方法,但并不是唯一方法,也不一定是最优方法。真正有效的成本优化,通常来自一整套系统设计:减少不必要调用、减少 Token 消耗、选择合适模型、缓存高频结果、控制上下文长度、异步处理复杂任务、建立监控和限流机制

本文将系统介绍 ChatGPT 成本的组成、常见浪费点、实际可落地的优化方案,并附带一套 Node.js 示例源码,帮助你快速搭建一个“低成本调用 ChatGPT API”的基础框架。


一、ChatGPT 成本主要来自哪里?

如果你使用的是 OpenAI API 或类似的大模型 API,成本通常和以下因素有关:

  1. 输入 Token 数量
  2. 输出 Token 数量
  3. 模型单价
  4. 请求次数
  5. 是否使用高级功能
  6. 上下文长度
  7. 重试次数和失败请求
  8. 多轮对话历史

简单来说,大模型调用的成本可以粗略理解为:

总成本 ≈ 输入 Token 成本 + 输出 Token 成本 + 额外功能成本

其中,输入 Token 包括:

  • 系统提示词
  • 用户问题
  • 历史对话
  • 检索出来的知识库内容
  • 工具调用参数
  • 额外业务上下文

输出 Token 包括:

  • 模型生成的回答
  • JSON 结构化结果
  • 推理说明
  • 冗余解释文本

很多时候,真正导致成本失控的不是单次调用价格,而是每次请求携带了过多无效上下文,或者本来可以缓存的问题被重复调用了模型


二、常见的成本浪费场景

1. 每次都携带完整历史对话

很多应用在实现多轮对话时,会直接把用户和助手的全部历史消息都塞进 API 请求中。这种做法简单,但成本会随着对话轮数线性甚至指数增长。

例如用户进行了 20 轮对话,每次请求都携带前面 19 轮内容,那么后期单次调用的输入 Token 会非常高。

更合理的方式是:

  • 保留最近几轮关键对话
  • 对更早的内容做摘要
  • 只传递和当前问题相关的信息
  • 对长期记忆做结构化存储

2. System Prompt 写得过长

很多产品会写一个非常复杂的系统提示词:

你是一个非常专业、严谨、热情、善于沟通、懂法律、懂金融、懂心理学、懂产品设计、懂技术架构的超级助手……

然后还会附带大量规则、示例和格式要求。

系统提示词当然重要,但它每次都会作为输入 Token 被计费。如果系统提示词非常长,而请求量又很大,那么长期成本非常可观。

优化方式包括:

  • 删除重复描述
  • 将规则模块化
  • 只在需要时注入对应规则
  • 用更短的格式约束替代长文本解释
  • 把固定知识放入数据库,通过检索按需注入

3. 对简单任务使用了昂贵模型

并不是所有任务都需要最强模型。

比如:

  • 文本分类
  • 意图识别
  • 简单摘要
  • 关键词提取
  • FAQ 匹配
  • 格式转换

这些任务往往可以使用更便宜、更快的小模型完成。只有在复杂推理、长文分析、高质量创作、代码生成等场景下,才需要更强模型。

一个成熟系统通常会设计成:

简单任务 → 小模型
复杂任务 → 大模型
低价值请求 → 规则或缓存
高价值请求 → 高级模型

4. 没有做缓存

用户的问题经常是重复的。例如:

  • “你们的退款政策是什么?”
  • “如何修改密码?”
  • “怎么联系客服?”
  • “这个功能怎么使用?”

如果每次都调用模型生成答案,就是明显浪费。

对于高频问题,可以使用:

  • 精确缓存
  • 语义缓存
  • FAQ 规则匹配
  • 向量相似度检索
  • Redis 缓存

缓存是降低成本最有效的手段之一。


5. 输出过长

很多提示词会要求模型“详细解释”“一步一步说明”“尽可能全面”。这会导致输出 Token 急剧增加。

如果用户只是需要一个简短答案,就不应该返回长篇大论。

可以在 Prompt 中加入:

请用不超过 150 字回答。

或者:

只返回 JSON,不要额外解释。

对于结构化任务,应明确要求模型输出最小必要内容。


三、降低成本的核心策略

下面我们从架构和代码两个角度看如何降低 ChatGPT 成本。


1. 模型分层:不要所有任务都用大模型

模型分层是最基础的成本控制方法。

可以将任务分为三类:

任务类型 示例 推荐策略
简单任务 分类、改写、提取关键词 小模型
中等任务 摘要、客服回答、普通写作 中等模型
复杂任务 代码生成、复杂分析、长文推理 强模型

伪代码如下:

function chooseModel(taskType) {
  const modelMap = {
    classify: "gpt-4o-mini",
    extract: "gpt-4o-mini",
    summarize: "gpt-4o-mini",
    chat: "gpt-4o-mini",
    reasoning: "gpt-4o",
    coding: "gpt-4o",
  };

  return modelMap[taskType] || "gpt-4o-mini";
}

实际项目中,你也可以根据用户等级选择模型:

function chooseModelByUser(user) {
  if (user.plan === "enterprise") {
    return "gpt-4o";
  }

  if (user.plan === "pro") {
    return "gpt-4o-mini";
  }

  return "gpt-4o-mini";
}

2. Prompt 精简:减少无效 Token

很多成本浪费都藏在 Prompt 里。一个好的 Prompt 不一定长,而是要清晰、准确、可执行。

优化前

你是一个非常优秀的客服专家,你需要非常认真地阅读用户的问题,然后结合我们的业务背景,用一种自然、友好、专业、温暖、可靠、可信任的方式来回答用户。请注意不要胡编乱造,如果你不知道答案,就要告诉用户你不知道……

优化后

你是客服助手。要求:
1. 只根据给定资料回答;
2. 不知道就说“不确定”;
3. 回答简洁、友好;
4. 不超过 150 字。

优化后语义更清楚,Token 更少,模型执行效果也更稳定。


3. 控制输出长度

大模型输出越长,成本越高。因此必须限制输出长度。

常见方法:

const response = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages,
  max_tokens: 300,
});

同时在提示词中加入:

请用不超过 150 字回答。

注意:max_tokens 是硬限制,Prompt 中的字数要求是软约束。两者可以结合使用。


4. 缓存:减少重复调用

缓存可以分为两种:

精确缓存

如果用户输入完全相同,就直接返回缓存结果。

优点:

  • 实现简单
  • 速度快
  • 成本几乎为零

缺点:

  • 只能命中完全相同的问题

语义缓存

如果用户问题语义相似,也可以返回缓存结果。

例如:

如何退款?
怎么申请退款?
退款流程是什么?

这些问题虽然字面不同,但语义相近,可以共用答案。

语义缓存通常需要结合 Embedding 和向量数据库。


5. 历史对话压缩

多轮对话中,不要无限保留历史消息。

推荐方案:

系统提示词 + 最近 3 轮对话 + 历史摘要 + 当前问题

当历史对话超过一定长度时,自动调用小模型生成摘要,然后用摘要替代原始历史。

示例:

async function summarizeHistory(client, history) {
  const content = history
    .map(item => `${item.role}: ${item.content}`)
    .join("\n");

  const res = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: "请将以下对话压缩为不超过 200 字的摘要,保留用户偏好、关键事实和未解决问题。",
      },
      {
        role: "user",
        content,
      },
    ],
    max_tokens: 250,
  });

  return res.choices[0].message.content;
}

6. RAG 按需注入知识,而不是把所有资料都塞进去

很多知识库问答系统会把大段文档直接放入 Prompt,这会导致输入 Token 很高。

更合理的方式是 RAG:

  1. 用户提问
  2. 对问题做 Embedding
  3. 从向量数据库检索相关片段
  4. 只把最相关的几段放入 Prompt
  5. 模型基于资料回答

这样可以显著减少输入 Token。


7. 限流与配额

如果产品开放给大量用户,一定要做限流。

常见限制:

  • 每个用户每天最多请求次数
  • 每分钟最大请求次数
  • 免费用户最大输出长度
  • 高成本功能仅对付费用户开放
  • 同一 IP 频率限制
  • 异常请求拦截

限流不仅能降低成本,也能防止恶意刷接口。


四、低成本 ChatGPT 调用示例源码

下面给出一个完整的 Node.js 示例,实现以下功能:

  • 模型自动选择
  • 精确缓存
  • 输出长度控制
  • 简单限流
  • 历史对话压缩
  • 成本估算日志

1. 安装依赖

npm init -y
npm install express openai dotenv crypto-js

创建 .env 文件:

OPENAI_API_KEY=你的_API_Key
PORT=3000

2. 项目结构

chatgpt-cost-demo/
├── .env
├── package.json
└── server.js

3. server.js 完整源码

import express from "express";
import OpenAI from "openai";
import dotenv from "dotenv";
import CryptoJS from "crypto-js";

dotenv.config();

const app = express();
app.use(express.json());

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

/**
 * 简易内存缓存
 * 生产环境建议使用 Redis
 */
const cache = new Map();

/**
 * 简易限流表
 * 生产环境建议使用 Redis + 滑动窗口算法
 */
const rateLimitMap = new Map();

/**
 * 模型价格示例,实际价格请以服务商官方价格为准
 * 单位:美元 / 100万 tokens
 */
const MODEL_PRICES = {
  "gpt-4o-mini": {
    input: 0.15,
    output: 0.6,
  },
  "gpt-4o": {
    input: 5,
    output: 15,
  },
};

/**
 * 生成缓存 Key
 */
function createCacheKey(payload) {
  const raw = JSON.stringify(payload);
  return CryptoJS.SHA256(raw).toString();
}

/**
 * 简单限流:每个用户每分钟最多 20 次请求
 */
function checkRateLimit(userId) {
  const now = Date.now();
  const windowMs = 60 * 1000;
  const maxRequests = 20;

  const record = rateLimitMap.get(userId) || {
    count: 0,
    startTime: now,
  };

  if (now - record.startTime > windowMs) {
    rateLimitMap.set(userId, {
      count: 1,
      startTime: now,
    });
    return true;
  }

  if (record.count >= maxRequests) {
    return false;
  }

  record.count += 1;
  rateLimitMap.set(userId, record);
  return true;
}

/**
 * 根据任务类型选择模型
 */
function chooseModel(taskType, userPlan = "free") {
  if (userPlan === "enterprise") {
    if (taskType === "reasoning" || taskType === "coding") {
      return "gpt-4o";
    }
  }

  const cheapTasks = ["classify", "extract", "summarize", "chat", "faq"];
  if (cheapTasks.includes(taskType)) {
    return "gpt-4o-mini";
  }

  return "gpt-4o-mini";
}

/**
 * 根据业务类型生成更短的 System Prompt
 */
function buildSystemPrompt(taskType) {
  const prompts = {
    chat: "你是客服助手。只根据已知信息回答;不知道就说“不确定”;回答简洁友好,不超过150字。",
    faq: "你是FAQ助手。请用简短中文回答用户问题,不超过120字。",
    summarize: "你是摘要助手。请提取重点,输出简洁摘要。",
    classify: "你是分类器。只输出分类名称,不要解释。",
    coding: "你是编程助手。给出准确代码和必要说明。",
    reasoning: "你是分析助手。请严谨推理,结论清晰。",
  };

  return prompts[taskType] || prompts.chat;
}

/**
 * 历史消息压缩
 */
async function summarizeHistory(history) {
  if (!history || history.length <= 6) {
    return null;
  }

  const oldMessages = history.slice(0, -6);
  const content = oldMessages
    .map(msg => `${msg.role}: ${msg.content}`)
    .join("\n");

  const res = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content:
          "请将以下对话压缩为不超过200字的中文摘要,保留用户目标、偏好、关键事实和未解决事项。",
      },
      {
        role: "user",
        content,
      },
    ],
    max_tokens: 250,
    temperature: 0.2,
  });

  return res.choices[0].message.content;
}

/**
 * 构造消息,避免携带完整历史
 */
async function buildMessages({ taskType, question, history }) {
  const systemPrompt = buildSystemPrompt(taskType);

  const messages = [
    {
      role: "system",
      content: systemPrompt,
    },
  ];

  if (history && history.length > 6) {
    const summary = await summarizeHistory(history);
    messages.push({
      role: "system",
      content: `历史摘要:${summary}`,
    });

    const recentMessages = history.slice(-6);
    messages.push(...recentMessages);
  } else if (history && history.length > 0) {
    messages.push(...history);
  }

  messages.push({
    role: "user",
    content: question,
  });

  return messages;
}

/**
 * 粗略估算成本
 * 注意:这里使用 API 返回的 usage 统计,比本地估算更可靠
 */
function estimateCost(model, usage) {
  const price = MODEL_PRICES[model];

  if (!price || !usage) {
    return null;
  }

  const inputTokens = usage.prompt_tokens || 0;
  const outputTokens = usage.completion_tokens || 0;

  const inputCost = (inputTokens / 1_000_000) * price.input;
  const outputCost = (outputTokens / 1_000_000) * price.output;

  return {
    inputTokens,
    outputTokens,
    totalTokens: usage.total_tokens,
    inputCost,
    outputCost,
    totalCost: inputCost + outputCost,
  };
}

/**
 * 主接口
 */
app.post("/api/chat", async (req, res) => {
  try {
    const {
      userId = "anonymous",
      userPlan = "free",
      taskType = "chat",
      question,
      history = [],
      useCache = true,
    } = req.body;

    if (!question || typeof question !== "string") {
      return res.status(400).json({
        error: "question 不能为空",
      });
    }

    /**
     * 限流,防止滥用
     */
    const allowed = checkRateLimit(userId);
    if (!allowed) {
      return res.status(429).json({
        error: "请求过于频繁,请稍后再试",
      });
    }

    /**
     * 选择模型
     */
    const model = chooseModel(taskType, userPlan);

    /**
     * 构造缓存 Key
     * 注意:不要把无关字段放进缓存 Key,否则命中率会下降
     */
    const cacheKey = createCacheKey({
      taskType,
      question: question.trim(),
      model,
    });

    if (useCache && cache.has(cacheKey)) {
      const cached = cache.get(cacheKey);

      return res.json({
        fromCache: true,
        model,
        answer: cached.answer,
        cost: {
          totalCost: 0,
          message: "命中缓存,本次未调用模型",
        },
      });
    }

    /**
     * 构造低 Token 消息
     */
    const messages = await buildMessages({
      taskType,
      question,
      history,
    });

    /**
     * 控制输出长度
     */
    const maxTokensMap = {
      classify: 30,
      extract: 120,
      summarize: 300,
      faq: 180,
      chat: 300,
      coding: 800,
      reasoning: 800,
    };

    const max_tokens = maxTokensMap[taskType] || 300;

    const completion = await client.chat.completions.create({
      model,
      messages,
      max_tokens,
      temperature: taskType === "classify" ? 0 : 0.3,
    });

    const answer = completion.choices[0].message.content;

    /**
     * 写入缓存
     * 适合 FAQ、摘要、分类等相对稳定的任务
     */
    if (useCache && ["faq", "classify", "extract", "summarize", "chat"].includes(taskType)) {
      cache.set(cacheKey, {
        answer,
        createdAt: Date.now(),
      });
    }

    const cost = estimateCost(model, completion.usage);

    console.log("[AI_COST_LOG]", {
      userId,
      taskType,
      model,
      usage: completion.usage,
      cost,
    });

    return res.json({
      fromCache: false,
      model,
      answer,
      cost,
    });
  } catch (error) {
    console.error(error);

    return res.status(500).json({
      error: "服务器错误",
      detail: error.message,
    });
  }
});

app.listen(process.env.PORT || 3000, () => {
  console.log(`Server running on port ${process.env.PORT || 3000}`);
});

五、接口调用示例

启动服务:

node server.js

请求示例:

curl -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "u_10001",
    "userPlan": "free",
    "taskType": "faq",
    "question": "如何申请退款?",
    "history": [],
    "useCache": true
  }'

返回示例:

{
  "fromCache": false,
  "model": "gpt-4o-mini",
  "answer": "请进入订单详情页,点击“申请退款”,按页面提示提交原因和凭证。审核通过后,退款将原路返回。",
  "cost": {
    "inputTokens": 75,
    "outputTokens": 42,
    "totalTokens": 117,
    "inputCost": 0.00001125,
    "outputCost": 0.0000252,
    "totalCost": 0.00003645
  }
}

当你第二次请求同样问题时,可能返回:

{
  "fromCache": true,
  "model": "gpt-4o-mini",
  "answer": "请进入订单详情页,点击“申请退款”,按页面提示提交原因和凭证。审核通过后,退款将原路返回。",
  "cost": {
    "totalCost": 0,
    "message": "命中缓存,本次未调用模型"
  }
}

这就是缓存带来的直接成本节省。


六、进一步优化:语义缓存

精确缓存只能命中完全一样的问题。如果用户换一种说法,缓存就失效了。

例如:

如何申请退款?
我想退款怎么办?
退款流程是什么?

这三个问题语义相似,但字符串不同。

要解决这个问题,可以增加语义缓存:

  1. 给用户问题生成 embedding
  2. 在向量数据库中查询相似问题
  3. 如果相似度超过阈值,直接返回历史答案
  4. 如果没有命中,再调用模型

伪代码如下:

async function semanticCacheSearch(question) {
  const embedding = await createEmbedding(question);

  const similar = await vectorDB.search({
    vector: embedding,
    topK: 1,
  });

  if (similar && similar.score > 0.88) {
    return similar.answer;
  }

  return null;
}

语义缓存特别适合:

  • 客服系统
  • 内部知识库
  • 教育问答
  • 电商导购
  • SaaS 帮助中心

但要注意,语义缓存不能盲目使用。对于涉及用户个人信息、订单状态、实时数据的问题,不能简单复用别人的答案。


七、进一步优化:任务前置判断

很多请求并不需要调用大模型。

例如用户输入:

你好
谢谢
再见

这些可以直接用规则回复。

示例代码:

function ruleBasedReply(question) {
  const text = question.trim();

  if (["你好", "您好", "hello", "hi"].includes(text.toLowerCase())) {
    return "你好,有什么可以帮你?";
  }

  if (["谢谢", "感谢", "thank you", "thanks"].includes(text.toLowerCase())) {
    return "不客气,很高兴帮到你。";
  }

  if (["再见", "拜拜", "bye"].includes(text.toLowerCase())) {
    return "再见,祝你生活愉快。";
  }

  return null;
}

在调用模型前加上:

const ruleReply = ruleBasedReply(question);
if (ruleReply) {
  return res.json({
    fromRule: true,
    answer: ruleReply,
    cost: {
      totalCost: 0,
      message: "命中规则,本次未调用模型",
    },
  });
}

这类规则虽然简单,但在真实客服场景中能过滤掉不少无效请求。


八、进一步优化:流式输出不一定省钱

很多开发者以为使用流式输出就能降低成本。其实流式输出主要优化的是用户体验,让用户更快看到首字响应,但并不天然减少 Token 费用。

如果模型最终输出一样长,成本基本一样。

流式输出的价值是:

  • 降低用户等待感
  • 提升交互体验
  • 允许前端中途停止生成
  • 长回答场景更友好

如果配合“停止生成”按钮,用户提前终止输出,才可能减少部分输出 Token。


九、进一步优化:监控每个功能的成本

没有监控,就无法优化。

建议记录以下字段:

字段 说明
userId 用户 ID
taskType 任务类型
model 使用模型
prompt_tokens 输入 Token
completion_tokens 输出 Token
total_tokens 总 Token
totalCost 估算成本
latency 请求耗时
fromCache 是否命中缓存
createdAt 请求时间

有了这些数据,就可以分析:

  • 哪些功能最烧钱
  • 哪些用户调用最多
  • 哪些 Prompt 输入最长
  • 哪些任务适合换小模型
  • 缓存命中率是否足够高
  • 是否存在异常刷量

十、成本优化的推荐落地顺序

如果你正在维护一个已经上线的 ChatGPT 应用,可以按照下面的顺序优化:

第一步:加监控

先知道钱花在哪里。

第二步:限制输出长度

这是最快见效的方法。

第三步:精简 Prompt

删除无效规则和重复描述。

第四步:加缓存

优先缓存 FAQ、分类、摘要等稳定结果。

第五步:模型分层

简单任务换小模型,复杂任务再用强模型。

第六步:压缩历史对话

多轮对话必须做摘要,否则成本会持续增长。

第七步:接入 RAG

知识库场景不要把完整文档塞进 Prompt。

第八步:做限流和配额

防止免费用户、恶意用户或异常脚本造成成本失控。


十一、实际项目中的注意事项

1. 不要为了省钱牺牲核心体验

成本优化不是一味压缩输出、降低模型能力。如果用户因为答案质量下降而流失,反而得不偿失。

建议将任务分级:

  • 低价值任务:强力控成本
  • 核心业务任务:保证质量
  • 付费用户任务:适度使用高级模型
  • 免费用户任务:限制调用频率和输出长度

2. 缓存要考虑数据安全

缓存用户问题和模型回答时,要注意:

  • 是否包含个人隐私
  • 是否包含订单信息
  • 是否包含企业敏感数据
  • 是否允许跨用户复用
  • 缓存过期时间是否合理

对于通用 FAQ 可以长期缓存;对于用户订单状态这类数据,不建议复用缓存。


3. 不要过度依赖本地 Token 估算

本地可以用 tokenizer 粗略估算 Token,但最终计费还是应以 API 返回的 usage 为准。

生产环境建议把 usage 记录到数据库中。


4. 失败重试也会产生成本

如果请求已经被模型处理,只是在网络层返回失败,重试可能导致重复计费。

建议:

  • 设置合理超时时间
  • 对幂等请求使用请求 ID
  • 记录调用状态
  • 对失败重试设置最大次数
  • 避免无限重试

十二、总结

降低 ChatGPT 成本不是单点技巧,而是一套工程体系。

最重要的原则可以概括为:

能不用模型,就不用模型;
能用小模型,就不用大模型;
能少传上下文,就少传上下文;
能缓存结果,就不要重复生成;
能限制输出,就不要无限展开;
能监控成本,就不要盲目优化。

如果你的应用刚开始接入 ChatGPT,建议一开始就设计好:

  • 模型分层
  • Prompt 模板
  • 缓存机制
  • 历史压缩
  • 成本日志
  • 用户限流

这样后续用户量增长时,系统才不会因为成本问题被迫重构。

本文给出的 Node.js 源码只是一个基础版本,适合用于理解成本优化思路。在生产环境中,你可以进一步替换为 Redis 缓存、数据库日志、向量数据库语义缓存、用户套餐系统和完整的风控限流策略。

只要方法得当,ChatGPT 应用完全可以在保证体验的同时,把成本控制在一个可预测、可管理、可持续的范围内。

目录结构
全文