AI 应用的成本优化终极指南——从架构到调用的全链路
AI 应用的成本优化终极指南——从架构到调用的全链路
适读人群:正在跑AI应用或者准备上生产的工程师 | 阅读时长:约18分钟 | 核心价值:系统性的成本优化框架,有数据,有代码,有判断什么时候不值得优化
今年二月,我帮一个朋友看他的AI应用账单。
他的用户数不多,大概每天500-800个活跃用户,但每月的模型API费用快到两万人民币了,把他吓到了。他以为自己的调用逻辑有问题,请我帮看。
排查之后,他的代码逻辑本身没什么大问题,但有几个可以优化的点,改完之后当月成本降到了大概6000元,节省了70%。
这篇我把那次优化的思路,加上我自己这两年在不同项目上积累的成本优化经验,系统整理出来。
先说结论:AI应用的成本优化不是一件需要特殊技巧的事,80%的优化空间来自几个标准操作。关键是知道在哪里找、怎么量化、以及什么时候停止优化。
先建立成本基线
在谈任何优化之前,必须先知道钱花在哪里。
我见过太多人在没有测量的情况下优化,结果优化了一个只占总成本5%的地方,洋洋得意,但另外95%完全没动。
最基本的成本分析:
// 在每次API调用后记录token使用
public class CostTracker {
public void recordApiCall(String feature, TokenUsage usage, String model) {
// 当前价格(需要根据实际模型调整)
double inputCostPer1k = getInputCostPer1k(model);
double outputCostPer1k = getOutputCostPer1k(model);
double cost = (usage.inputTokenCount() / 1000.0 * inputCostPer1k)
+ (usage.outputTokenCount() / 1000.0 * outputCostPer1k);
// 记录到你的监控系统
metricsRegistry.counter("ai.cost.usd",
"feature", feature,
"model", model
).increment(cost);
metricsRegistry.summary("ai.tokens.input",
"feature", feature
).record(usage.inputTokenCount());
metricsRegistry.summary("ai.tokens.output",
"feature", feature
).record(usage.outputTokenCount());
}
}跑一周,看报表。你会清楚地看到:哪个功能消耗了最多token,输入token和输出token的比例是多少,哪些调用的token数量是离群值。
有了这个,再谈优化。
架构层优化(省钱最多的地方)
优化1:模型分级策略
这是我朋友账单里最大的浪费来源——所有功能都在用同一个最贵的模型。
事实是:不同的任务对模型能力的要求差距很大。
任务分级参考:
需要强模型(GPT-4级别):
- 复杂推理、多步骤分析
- 需要深度理解上下文的对话
- 高风险决策支持(给重要结论)
中等模型(GPT-3.5/Claude Haiku级别)可以胜任:
- 文本分类、意图识别
- 简单摘要、格式转换
- 已有明确步骤的任务
便宜小模型可以胜任:
- 情感分析
- 关键词提取
- 简单的是/否判断实现分级路由:
public class ModelRouter {
public ChatLanguageModel selectModel(TaskType taskType, TaskComplexity complexity) {
return switch (taskType) {
case COMPLEX_REASONING, STRATEGIC_ANALYSIS ->
highCapabilityModel; // GPT-4o等
case SUMMARIZATION, TRANSLATION ->
complexity == TaskComplexity.HIGH ?
highCapabilityModel : midCapabilityModel;
case CLASSIFICATION, EXTRACTION ->
lowCapabilityModel; // GPT-3.5-turbo等
default -> midCapabilityModel;
};
}
}我朋友的应用里,有一个功能是"给用户消息分类,判断是咨询、投诉还是普通对话"——这个用GPT-4是杀鸡用牛刀,换成mini版本,准确率基本没变,成本降了约85%。
典型节省比例:30-60%的总成本,取决于任务分布。
优化2:语义缓存
相同或者相近的请求,不要重复调用模型。
基础缓存(精确匹配)每个人都想到了,但这个命中率很低,因为用户很少问完全一样的问题。
语义缓存的思路是:对用户输入做Embedding,如果向量相似度超过阈值,就认为是"相同的问题",返回缓存结果。
public class SemanticCache {
private final EmbeddingModel embeddingModel;
private final EmbeddingStore<CachedResponse> cacheStore;
private final double similarityThreshold;
public Optional<String> get(String query) {
Embedding queryEmbedding = embeddingModel.embed(query).content();
List<EmbeddingMatch<CachedResponse>> matches =
cacheStore.findRelevant(queryEmbedding, 1, similarityThreshold);
if (!matches.isEmpty()) {
EmbeddingMatch<CachedResponse> match = matches.get(0);
// 检查是否还在有效期内
if (!match.embedded().isExpired()) {
return Optional.of(match.embedded().getResponse());
}
}
return Optional.empty();
}
public void put(String query, String response, Duration ttl) {
Embedding queryEmbedding = embeddingModel.embed(query).content();
CachedResponse cached = new CachedResponse(response, Instant.now().plus(ttl));
cacheStore.add(queryEmbedding, cached);
}
}注意事项:
- 相似度阈值要认真调,太低会返回不相关的缓存结果,太高命中率很低。0.92-0.95通常是比较合理的起点,但要在你的真实数据上验证。
- 缓存的TTL要根据内容的新鲜度要求设置。知识库问答可以缓存较长时间,依赖实时数据的场景不能缓存。
典型节省比例:10-30%,取决于用户问题的重复度。
优化3:批处理代替逐条处理
如果你的应用有"批量处理"的场景(比如每天处理一批新文档、给一批内容打标签),不要逐条调用API。
批量调用的好处:
- 很多模型API支持Batch API,价格有折扣(通常50%)
- 减少网络往返次数
- 可以在非高峰期运行,避开限速
public class BatchProcessor {
private static final int BATCH_SIZE = 20;
public List<String> processInBatch(List<String> inputs) {
List<String> results = new ArrayList<>();
// 分批处理
List<List<String>> batches = Lists.partition(inputs, BATCH_SIZE);
for (List<String> batch : batches) {
// 构建批量Prompt,让一次调用处理多个输入
String batchPrompt = buildBatchPrompt(batch);
String batchResponse = callModel(batchPrompt);
// 解析批量响应,提取每个输入对应的结果
List<String> batchResults = parseBatchResponse(batchResponse, batch.size());
results.addAll(batchResults);
}
return results;
}
private String buildBatchPrompt(List<String> items) {
StringBuilder sb = new StringBuilder();
sb.append("请对以下每一条文本执行分类,返回JSON数组格式。\n\n");
for (int i = 0; i < items.size(); i++) {
sb.append(String.format("[%d] %s\n", i + 1, items.get(i)));
}
sb.append("\n返回格式:[{\"index\": 1, \"category\": \"...\", \"confidence\": 0.9}, ...]");
return sb.toString();
}
}典型节省比例:30-50%(利用batch折扣)+ 速度提升。
调用层优化(操作简单,效果明显)
优化4:Prompt精简
这是最容易被忽视的优化,因为大家在写Prompt的时候不自觉地会越写越长,加各种例子、重复说明、强调语气。
但每多100个input token,就是多一点钱,乘以调用次数,积少成多。
Prompt精简的几个操作:
移除冗余的语气词和重复声明:
优化前(87 tokens):
请你认真仔细地分析以下文本内容,你需要理解文本的主要意思,
然后提取其中最重要的关键信息点,返回一个简洁的摘要,
摘要不要太长,控制在100字以内,需要覆盖核心内容。
优化后(31 tokens):
提取以下文本的核心内容,输出100字以内的摘要。把示例移到系统Prompt,而不是每次调用都附上:
// 只在系统Prompt里放一次示例,不要每次用户消息里都附带
ChatLanguageModel model = OpenAiChatModel.builder()
.systemPrompt(SYSTEM_PROMPT_WITH_EXAMPLES) // 一次性设置
.build();按需附加上下文,而不是每次都附加所有上下文:
public String buildDynamicPrompt(String userQuery, ConversationContext context) {
StringBuilder prompt = new StringBuilder();
prompt.append(CORE_INSTRUCTIONS);
// 只附加相关的上下文部分,而不是全部
if (context.hasRelevantHistory()) {
prompt.append("\n相关历史:").append(context.getRelevantHistory(userQuery));
}
if (context.hasRelevantDocuments()) {
// RAG检索到的相关内容,控制数量
prompt.append("\n参考资料:")
.append(context.getTopDocuments(3)); // 不是全部,只取最相关的3条
}
return prompt.toString();
}典型节省比例:15-25%的输入token。
优化5:max_tokens的精确控制
很多人设置max_tokens的时候设的是一个保守的大值,"反正便宜,多点没关系"。
但output token的价格通常是input token的2-3倍,对输出长度的控制是非常有价值的。
关键是要根据每个功能的真实输出需求,设置合理的max_tokens:
// 不同功能使用不同的max_tokens
Map<String, Integer> maxTokensByFeature = Map.of(
"INTENT_CLASSIFICATION", 20, // 分类结果不需要很长
"KEYWORD_EXTRACTION", 100,
"SHORT_SUMMARY", 200,
"DETAILED_ANALYSIS", 800,
"FULL_REPORT", 2000
);同时,在Prompt里明确说明期望输出长度:
输出格式:JSON对象,不超过50个字。
不要解释你的判断过程,直接给结果。这比只设max_tokens更有效,因为模型会主动控制输出长度,而不是等到被截断。
典型节省比例:10-30%的输出token,取决于当前你的max_tokens设置有多保守。
优化6:流式输出的合理使用
流式输出(Streaming)不省钱,但它影响用户体验,间接影响产品的成本效率。
有一种反直觉的情况:非流式输出+缓存,比流式输出+无缓存更省钱。
如果你的某个功能是"生成报告"类的,用户可以等几秒钟,那么考虑非流式+缓存的组合,而不是追求流式体验却失去缓存机会。
RAG专属优化
如果你的应用有RAG功能,这里有几个专门针对RAG的成本优化点。
优化7:Rerank代替大topK
常见做法是向量检索topK=10,然后把10个文档全部放进context。
但每次多放7-8个文档,就是多几百个input token,乘以调用次数,成本不小。
更好的做法:先用向量检索topK=20(粗排),再用轻量Rerank模型精排,最终只放top3进context。
Rerank模型的调用成本远低于大LLM,但显著提升了放进context的内容质量,让你可以用更少的文档数量达到更好的回答质量。
public List<TextSegment> retrieveWithRerank(String query, int finalTopK) {
// 粗排:向量检索,取较多候选
List<EmbeddingMatch<TextSegment>> candidates =
embeddingStore.findRelevant(
embeddingModel.embed(query).content(),
finalTopK * 5 // 粗排多取
);
// 精排:Rerank模型对候选进行重排
List<TextSegment> reranked = rerankModel.rerank(
query,
candidates.stream()
.map(EmbeddingMatch::embedded)
.collect(Collectors.toList())
);
// 只返回精排后的topK
return reranked.subList(0, Math.min(finalTopK, reranked.size()));
}典型节省比例:对RAG功能的context token节省20-40%。
优化8:查询路由——不是每个问题都需要RAG
有些用户问题不需要检索就能回答(比如"你能做什么"这种元问题),有些问题只需要部分检索,有些问题不需要任何文档就可以回答。
在调用RAG流程之前,先做一个轻量的"是否需要检索"判断:
public enum RetrievalDecision {
FULL_RETRIEVAL, // 需要检索
SKIP_RETRIEVAL, // 不需要检索,直接回答
CLARIFY_FIRST // 问题不清楚,需要先澄清
}
public RetrievalDecision decideRetrieval(String query) {
// 用小模型或者规则判断
// 这里的判断成本远低于一次完整RAG流程
if (isMetaQuestion(query)) return RetrievalDecision.SKIP_RETRIEVAL;
if (isAmbiguous(query)) return RetrievalDecision.CLARIFY_FIRST;
return RetrievalDecision.FULL_RETRIEVAL;
}什么时候不值得优化
说了这么多节省的方法,我也要说清楚什么时候不要优化。
成本占总成本比例低于5%的功能,先不碰。 优化时间本身有成本,除非有批量优化的机会,否则优化小头不如把精力放在大头。
准确率已经在边界的功能,不要为了省钱降低模型质量。 如果一个功能当前用GPT-4的准确率是92%,换便宜模型之后可能降到80%,而这个功能的错误会直接影响用户,那不值得。
用户体验关键路径上的功能,响应速度优先于成本。 有些优化(比如多次检索+精排)会增加延迟,在用户等待的关键路径上,这个代价通常不值得。
还在快速迭代的功能,推迟优化。 功能设计还没稳定就去优化,大概率白做,因为需求一变,你的优化实现可能完全作废。稳定之后再优化是正确的节奏。
成本优化的优先级顺序
综合来看,操作的优先级:
优先级1(先做,ROI最高):
- 建立成本监控,看清楚钱花在哪里
- 模型分级,不同任务用不同模型
优先级2(效果显著,实现不复杂):
- Prompt精简
- max_tokens精确控制
- 批处理非实时任务
优先级3(需要一定工程投入,但长期有价值):
- 语义缓存
- RAG的Rerank优化
- 查询路由
优先级4(进阶优化,适合成熟产品):
- 自定义本地部署小模型替换云API
- Fine-tuning减少Prompt长度
- 混合推理架构我朋友的账单从两万降到六千,用的是优先级1和优先级2的操作,没有什么复杂的工程。
大部分AI应用的成本问题,不需要高级手段,需要的是基本功的系统执行。
