别再只会换便宜模型了:ChatGPT API 成本优化实战与源码分享
ChatGPT 如何降低成本|附源码
在很多团队开始接入 ChatGPT 或其他大模型 API 之后,最先感受到的往往不是技术难度,而是成本压力。尤其是当产品从 Demo 进入真实用户场景后,请求量上升、上下文变长、重复问题增多、模型调用链复杂化,账单很容易快速增长。
很多人降低成本的第一反应是:“换更便宜的模型”。这当然是一种方法,但并不是唯一方法,也不一定是最优方法。真正有效的成本优化,通常来自一整套系统设计:减少不必要调用、减少 Token 消耗、选择合适模型、缓存高频结果、控制上下文长度、异步处理复杂任务、建立监控和限流机制。
本文将系统介绍 ChatGPT 成本的组成、常见浪费点、实际可落地的优化方案,并附带一套 Node.js 示例源码,帮助你快速搭建一个“低成本调用 ChatGPT API”的基础框架。
一、ChatGPT 成本主要来自哪里?
如果你使用的是 OpenAI API 或类似的大模型 API,成本通常和以下因素有关:
- 输入 Token 数量
- 输出 Token 数量
- 模型单价
- 请求次数
- 是否使用高级功能
- 上下文长度
- 重试次数和失败请求
- 多轮对话历史
简单来说,大模型调用的成本可以粗略理解为:
总成本 ≈ 输入 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:
- 用户提问
- 对问题做 Embedding
- 从向量数据库检索相关片段
- 只把最相关的几段放入 Prompt
- 模型基于资料回答
这样可以显著减少输入 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": "命中缓存,本次未调用模型"
}
}
这就是缓存带来的直接成本节省。
六、进一步优化:语义缓存
精确缓存只能命中完全一样的问题。如果用户换一种说法,缓存就失效了。
例如:
如何申请退款?
我想退款怎么办?
退款流程是什么?
这三个问题语义相似,但字符串不同。
要解决这个问题,可以增加语义缓存:
- 给用户问题生成 embedding
- 在向量数据库中查询相似问题
- 如果相似度超过阈值,直接返回历史答案
- 如果没有命中,再调用模型
伪代码如下:
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 应用完全可以在保证体验的同时,把成本控制在一个可预测、可管理、可持续的范围内。