AI 应用上线后最让人肉疼的不是性能瓶颈,而是月底收到的那张 API 账单。
我见过一个 AI 写作工具,月活不到 2000 人,月均 API 费用却高达 52——降幅 89%,而且输出质量几乎没有下降。
这篇文章把完整的优化方法论和代码实现全盘托出,涉及 Prompt 设计、缓存架构、模型路由、流式计费四大维度,并全部基于 Next.js 16 实现。
1. Token 消耗的 7 大黑洞
在动手优化之前,你得先知道钱烧在哪里。
黑洞 1:冗余 System Prompt
很多开发者喜欢写超长的 System Prompt:
你是一个专业的 AI 写作助手,拥有 10 年经验,你擅长 SEO 优化、内容策略、转化率提升...(省略 500 tokens)
每次请求都附带这 500 tokens,日请求 10000 次就是 500 万 tokens 白白消耗。
黑洞 2:重复上下文传递
多轮对话中,每次新请求都把完整历史对话塞进去。一个 10 轮对话,用户可能只提了一个新问题,但每次请求的上下文翻倍增长。
黑洞 3:无缓存的全量调用
常见问题如"什么是 SEO?""Next.js 路由怎么配置?"被反复请求,每次都调用 LLM 重新生成。
黑洞 4:小任务用大模型
格式校验、拼写检查、语气调整,明明 GPT-4o-mini 或 Claude Haiku 就够用,却每次都调用 Claude Opus 或 GPT-4o。
黑洞 5:无效重试与错误处理缺失
网络抖动或 API 限流后盲目重试 3-5 次,每次重试都是真金白银。
黑洞 6:不合理的输出长度
"给我写一篇关于 AI 的文章"——没有字数限制,模型可能输出 2000 tokens,而实际只需要 500 tokens。
黑洞 7:忽略免费/廉价替代方案
对于文本摘要、关键词提取、情感分析这类任务,开源模型(如 Llama 4、Qwen 3)在本地或边缘推理成本趋近于零。
2. 策略一:Prompt 压缩与工程化
2.1 动态 System Prompt
不要写万能 System Prompt。根据实际任务动态构建最小化上下文。
// src/lib/prompts/factory.ts
const BASE_SYSTEM_PROMPTS = {
writing: "你是写作助手。输出简洁专业。",
coding: "你是代码专家。提供可运行的 TypeScript 代码。",
analysis: "你是数据分析师。用数据说话。",
default: "你是一个有用的助手。"
} as const;
export function buildSystemPrompt(
task: keyof typeof BASE_SYSTEM_PROMPTS,
extraContext?: string
): string {
let prompt = BASE_SYSTEM_PROMPTS[task];
if (extraContext) {
// 只追加任务相关的上下文,不是整套知识库
prompt += `\n相关背景: ${extraContext.slice(0, 200)}`;
}
return prompt;
}
对比:万能 Prompt 500 tokens → 动态 Prompt 30-80 tokens,单次节省 85%。
2.2 输出约束前置
把字数、格式、JSON Schema 等约束写进 Prompt,而不是后处理。
const CONSTRAINTS = {
short: "回复不超过 100 字。",
json: "仅返回合法 JSON,不要包含 markdown 代码块。",
bullet: "使用无序列表,每条不超过 20 字。"
};
// 组合使用
const prompt = `${basePrompt}\n${CONSTRAINTS.short}\n${CONSTRAINTS.json}`;
实测:不加约束平均输出 650 tokens,加约束后平均 180 tokens,节省 72%。
2.3 Prompt Minification
在生产环境中,去除 Prompt 中的所有多余空白、换行、注释。
// 开发环境:可读性优先
const DEV_PROMPT = `
你是一个代码审查专家。
请检查以下代码的问题:
1. 安全漏洞
2. 性能瓶颈
3. 代码风格
`;
// 生产环境:体积优先
const PROD_PROMPT = DEV_PROMPT.replace(/\s+/g, ' ').trim();
// 结果: "你是一个代码审查专家。请检查以下代码的问题:1. 安全漏洞 2. 性能瓶颈 3. 代码风格"
这看似微小的优化,在日请求量 10 万+ 时,每月可节省数十万 tokens。
3. 策略二:语义缓存体系
缓存是 Token 优化的核武器。但传统精确匹配缓存(如 Redis key-value)命中率太低。语义缓存才是王道。
3.1 LRU 内存缓存(高频热点)
// src/lib/cache/lru-cache.ts
import { LRUCache } from 'lru-cache';
interface CacheEntry {
result: string;
tokens: number;
model: string;
}
export const aiCache = new LRUCache<string, CacheEntry>({
max: 1000,
ttl: 1000 * 60 * 30, // 30 分钟过期
allowStale: false,
});
export function getCacheKey(model: string, messages: unknown[]): string {
const normalized = JSON.stringify({
model,
messages: messages.map((m: any) => ({
role: m.role,
content: typeof m.content === 'string'
? m.content.toLowerCase().trim()
: m.content,
})),
});
// 简单的哈希
let hash = 0;
for (let i = 0; i < normalized.length; i++) {
const char = normalized.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return `${model}:${Math.abs(hash)}`;
}
3.2 语义相似度缓存
对于"类似但不完全相同"的请求,使用 embedding 计算相似度。
// src/lib/cache/semantic-cache.ts
import { aiCache } from './lru-cache';
interface SemanticCacheEntry {
query: string;
embedding: number[];
result: string;
tokens: number;
createdAt: number;
}
const semanticStore = new Map<string, SemanticCacheEntry>();
const SIMILARITY_THRESHOLD = 0.92;
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
export async function semanticCacheLookup(
query: string,
embedding: number[]
): Promise<string | null> {
for (const [_, entry] of semanticStore) {
const sim = cosineSimilarity(embedding, entry.embedding);
if (sim >= SIMILARITY_THRESHOLD) {
console.log(`[Cache Hit] Similarity: ${(sim * 100).toFixed(1)}%`);
return entry.result;
}
}
return null;
}
3.3 Next.js 16 use cache 服务端缓存
// src/app/actions/ai.ts
"use server";
import { cache } from "react";
export const generateArticleSummary = cache(
async (content: string, maxLength: number = 100) => {
// 这个函数的结果会被 Next.js 自动缓存
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: { /* ... */ },
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{
role: "user",
content: `用不超过 ${maxLength} 字总结:\n${content}`
}],
max_tokens: maxLength * 2
})
});
return response.json();
}
);
Next.js 16 的 use cache 可以让你声明式地缓存整个服务端函数的结果,零额外代码实现请求去重。
4. 策略三:多模型分层路由
不是所有任务都值得用最贵的大模型。建立任务难度 → 模型选择的映射表。
4.1 智能路由架构
// src/lib/models/router.ts
export type ModelTier = 'budget' | 'standard' | 'premium';
interface TaskClassifier {
tier: ModelTier;
task: string;
expectedTokens: number;
}
const MODEL_MAP: Record<ModelTier, { model: string; costPer1K: number }> = {
budget: { model: 'gpt-4o-mini', costPer1K: 0.00015 },
standard:{ model: 'gpt-4o', costPer1K: 0.0025 },
premium: { model: 'gpt-4o', costPer1K: 0.0025 }, // 或 Claude Opus
};
export function classifyTask(input: string, userIntent: string): ModelTier {
// 简单任务 → budget
if (/格式化|翻译|纠错|拼写|摘要|分类/.test(userIntent)) {
return 'budget';
}
// 中等任务 → standard
if (/写作|生成|解释|分析|对比/.test(userIntent)) {
return input.length > 2000 ? 'standard' : 'budget';
}
// 复杂任务 → premium
if (/架构|设计|审查|调试|安全|优化/.test(userIntent)) {
return 'premium';
}
return 'standard';
}
export async function routeAIRequest(
messages: any[],
userIntent: string
) {
const lastMessage = messages[messages.length - 1]?.content || '';
const tier = classifyTask(lastMessage, userIntent);
const modelConfig = MODEL_MAP[tier];
console.log(`[Router] ${userIntent} → ${tier} (${modelConfig.model})`);
return callModel(modelConfig.model, messages);
}
4.2 成本对比
| 场景 | 全用 GPT-4o | 智能路由 | 节省 | |------|------------|---------|------| | 文本摘要 (100次/天) | 0.015/天 | 94% | | 代码生成 (50次/天) | 0.40/天 | 20% | | 文章写作 (20次/天) | 0.55/天 | 8% | | 月总计 | 28.95 | 29% |
5. 策略四:流式响应与重试优化
5.1 合理使用 max_tokens 截断
不要信任模型的自我约束。用 max_tokens 参数从 API 层面硬限制。
function getMaxTokens(intent: string): number {
const limits: Record<string, number> = {
summary: 200,
translation: 500,
chat: 1000,
article: 3000,
code: 2000,
};
for (const [key, limit] of Object.entries(limits)) {
if (intent.includes(key)) return limit;
}
return 1000; // 默认
}
5.2 指数退避重试
不要盲目重试。区分可重试错误(429/503)和不可重试错误(401/400)。
async function callWithRetry(
fn: () => Promise<Response>,
maxRetries = 3
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fn();
// 不可重试的错误
if (response.status === 401 || response.status === 400) {
throw new Error(`Non-retryable: ${response.status}`);
}
// 可重试的错误
if (response.status === 429 || response.status >= 500) {
if (attempt === maxRetries) throw new Error('Max retries exceeded');
const delay = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 30000);
console.log(`[Retry] Attempt ${attempt + 1}, waiting ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
continue;
}
return response;
} catch (error) {
if (attempt === maxRetries) throw error;
}
}
throw new Error('Unreachable');
}
5.3 流式响应的 Token 计数
流式响应不会一次性返回所有 tokens,需要逐个 SSE 块累加计数。
// src/lib/token-counter.ts
export async function* countStreamTokens(
stream: ReadableStream<Uint8Array>
): AsyncGenerator<{ text: string; cumulativeTokens: number }> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let totalTokens = 0;
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// 解析 SSE 格式
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
try {
const parsed = JSON.parse(line.slice(6));
const text = parsed.choices?.[0]?.delta?.content || '';
totalTokens += estimateTokens(text);
yield { text, cumulativeTokens: totalTokens };
} catch { /* skip malformed */ }
}
}
}
} finally {
reader.releaseLock();
}
}
// 粗略估算:中文约 1.5 字符/token,英文约 4 字符/token
function estimateTokens(text: string): number {
const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
const otherChars = text.length - chineseChars;
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
}
6. 策略五:监控与告警
没有监控的优化是盲目的。建立实时 Token 消耗仪表盘。
6.1 Token 使用量追踪
// src/lib/monitoring/token-tracker.ts
interface TokenRecord {
timestamp: number;
model: string;
promptTokens: number;
completionTokens: number;
cost: number;
endpoint: string;
userId?: string;
}
const COST_PER_1K: Record<string, { input: number; output: number }> = {
'gpt-4o': { input: 0.0025, output: 0.01 },
'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
'claude-opus-4': { input: 0.015, output: 0.075 },
'claude-sonnet-4':{ input: 0.003, output: 0.015 },
};
export function calculateCost(
model: string,
promptTokens: number,
completionTokens: number
): number {
const rates = COST_PER_1K[model];
if (!rates) return 0;
return (promptTokens / 1000) * rates.input +
(completionTokens / 1000) * rates.output;
}
export class TokenTracker {
private records: TokenRecord[] = [];
record(record: TokenRecord) {
this.records.push(record);
// 超过阈值告警
const hourlyCost = this.getHourlyCost();
if (hourlyCost > 5) {
console.warn(`[ALERT] Hourly cost $${hourlyCost.toFixed(2)} exceeds threshold`);
}
}
getHourlyCost(): number {
const now = Date.now();
const hourAgo = now - 3600000;
return this.records
.filter(r => r.timestamp > hourAgo)
.reduce((sum, r) => sum + r.cost, 0);
}
getDailyReport() {
const today = this.records.filter(
r => new Date(r.timestamp).toDateString() === new Date().toDateString()
);
const byModel: Record<string, { tokens: number; cost: number }> = {};
for (const r of today) {
if (!byModel[r.model]) byModel[r.model] = { tokens: 0, cost: 0 };
byModel[r.model].tokens += r.promptTokens + r.completionTokens;
byModel[r.model].cost += r.cost;
}
return {
date: new Date().toISOString().split('T')[0],
totalCost: today.reduce((s, r) => s + r.cost, 0),
totalTokens: today.reduce((s, r) => s + r.promptTokens + r.completionTokens, 0),
byModel,
};
}
}
export const tokenTracker = new TokenTracker();
6.2 Next.js 16 中间件级拦截
// src/middleware.ts (token tracking part)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/ai')) {
// 注入请求级 Token 计数器
const requestId = crypto.randomUUID();
const response = NextResponse.next();
response.headers.set('X-Request-ID', requestId);
response.headers.set('X-Token-Cost-Monitor', 'enabled');
return response;
}
}
7. 优化效果汇总
以下是我们对一个真实 AI 写作 SaaS 进行全链路优化后的数据:
| 优化项 | 优化前 | 优化后 | 节省 | |--------|--------|--------|------| | System Prompt | 500 tokens/次 | 50 tokens/次 | 90% | | 输出长度限制 | 无限制 (~650 tokens) | max_tokens=200 | 69% | | 语义缓存命中率 | 0% | 34% | 34% | | 简单任务路由 | 全部 GPT-4o | 85% mini, 15% 4o | 70% | | 无效重试 | 平均 2.3 次 | 1.1 次 | 52% | | 上下文历史 | 全量传递 | 摘要+最近3轮 | 60% | | 月总成本 | 52 | 89% |
8. 最佳实践清单
在发布任何 AI 功能前,跑一遍这个检查清单:
- [ ] System Prompt 是否精简到 50 tokens 以内?
- [ ] 是否根据任务难度分配合适的模型?
- [ ] 高频请求是否启用了语义缓存?
- [ ]
max_tokens是否为每个端点设置了合理上限? - [ ] 重试逻辑是否使用指数退避并区分错误类型?
- [ ] 是否记录了每次调用的 Token 消耗和成本?
- [ ] 多轮对话是否使用摘要替代完整历史?
- [ ] 是否有成本监控和异常告警?
9. 总结
AI API 成本优化不是单点技术,而是一个系统性工程。核心思路可以归结为四点:
- 少传:压缩 Prompt、限制输出、摘要历史——减少每次请求的 Token 量
- 少调:语义缓存、请求去重、客户端预判——减少不必要的 API 调用
- 调便宜的:任务分类、模型分层路由——根据实际需求选择性价比最高的模型
- 看得见:Token 追踪、成本监控、异常告警——没有数据就没有优化
把这四点做到位,绝大多数 AI 应用的 API 成本可以降低 70%-90%,而且用户体验几乎没有影响。
你是否也在为 AI API 账单头疼?我们提供完整的 AI 应用 Token 审计服务,帮你精准定位成本黑洞并提供可落地的优化方案。联系 WebMaster 获取免费成本评估。
相关阅读: