系列文章
本系列系统性地讲解 AI Agent 的核心概念、架构设计与生产实战,从理论到工程全方位覆盖:
- AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
- 理解 AI Agent 的大脑:ReAct 模式从入门到实战
- 从零理解 RAG:检索增强生成完整指南
- MCP 模型上下文协议:AI 的万能接口与 MCP Server 实战
- 让 AI 学会”说人话”——Spring AI 结构化输出实战
- AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
- AI Agent 的规划大脑:从任务分解到自适应执行策略
- AI Agent 的灵魂对话:Prompt Engineering 系统提示词设计的艺术与工程
- AI Agent 评估与优化:从基准测试到生产环境的质量守护实战
- Embedding 向量化的魔法:从文本到向量的数学之旅与 Java 实战
- 当 RAG 遇上知识图谱:GraphRAG 原理与 Java 实战
- 当 RAG 遇到 Agent:Agentic RAG 的架构设计与 Java 实战
- AI Agent 的安全防线:Prompt 注入防御与生产级安全防护实战
- AI Agent 的推理引擎:从 Chain-of-Thought 到推理模型的深度解析与 Java 实战
- Spring AI 核心架构全解析:从 ChatModel 到 Advisor Chain 的设计哲学
- AI Agent 的知识检索引擎:从向量搜索到智能检索策略的 Java 实战
- AI Agent 的工作流编排:从顺序链到自适应 DAG 的 Java 实战
- AI Agent 的流式响应与实时交互:从 SSE 到 WebSocket 的 Java 实战
- AI Agent 的可观测性:从链路追踪到成本监控的 Java 实战
- AI Agent 的多模态感知:从图片理解到语音交互的 Java 实战
- AI Agent 的自我反思与经验学习:从错误中进化的 Java 实战
- Agent 间如何对话:A2A 协议深度解析与 Java 实战
- AI Agent 的人机协作:从 Human-in-the-Loop 到渐进式自治的 Java 实战
- AI Agent 的上下文工程与 Token 预算管理:从窗口压缩到成本优化的 Java 实战
- AI Agent 的容错与韧性:从错误处理到生产级可靠性保障的 Java 实战
- AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战
- 从理论到生产:AI Agent 全景知识图谱与 Java 开发者成长路线
- AI Agent 的成本优化:从模型路由到缓存策略的 Java 实战(本文)
引子:一个真实的成本崩溃故事
2026 年初,某电商平台上线了基于 AI Agent 的智能客服系统。上线第一周,日均调用量 5 万次,每次调用平均消耗 2000 Token,使用 GPT-4o 模型。财务部门月底看到账单时惊呆了——单月 AI 调用费用超过 12 万元。
更可怕的是,随着用户量增长,这个数字还在以每月 30% 的速度膨胀。
技术团队紧急复盘,发现问题出在三个地方:
- 所有请求都用最贵的模型——简单的”查订单状态”和复杂的”退换货方案制定”用的是同一个 GPT-4o
- 相同问题重复调用——用户问”发货时间”,每个会话都重新调用 LLM,没有任何缓存
- Prompt 冗余严重——System Prompt 长达 3000 Token,其中 60% 是历史遗留的无用指令
这三类问题,正是 AI Agent 生产环境中最常见的成本陷阱。本文将从模型路由、语义缓存、Token 优化三个核心维度,结合 Spring AI 和 LangChain4j 的实战代码,系统性地讲解如何将 Agent 运营成本降低 60% 以上。
一、模型路由:用合适的模型做合适的事
1.1 为什么需要模型路由?
想象你开了一家餐厅。你会让米其林三星主厨去切菜、洗碗吗?显然不会——切菜用帮厨,洗碗用洗碗机,只有真正需要创意和技艺的菜品才交给主厨。
AI Agent 的模型选择也是同样的道理。不同的任务对模型能力的要求天差地别:
| 任务类型 |
示例 |
所需能力 |
推荐模型 |
| 简单查询 |
“查订单状态”、”获取天气” |
工具调用、格式化 |
GPT-4o-mini / Qwen-turbo |
| 中等推理 |
“推荐相似商品”、”解释政策” |
逻辑推理、上下文理解 |
GPT-4o / Claude-3.5-sonnet |
| 复杂分析 |
“制定退换货方案”、”代码审查” |
深度推理、多步规划 |
o3 / Claude-4-opus |
核心原则:用最便宜的能完成任务的模型。80% 的请求其实只需要小模型。
1.2 模型路由的三种策略
策略一:基于意图的静态路由
最简单的路由方式——根据用户意图分类,直接映射到不同模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
@Component public class IntentBasedModelRouter {
private final Map<String, String> intentModelMapping = Map.of( "order_query", "gpt-4o-mini", "product_search", "gpt-4o-mini", "complaint", "gpt-4o", "complex_return", "claude-4-opus" );
private static final String DEFAULT_MODEL = "gpt-4o-mini";
public String route(String userMessage, ChatContext context) { String intent = classifyIntent(userMessage, context);
String model = intentModelMapping.getOrDefault(intent, DEFAULT_MODEL);
if (context.getTurnCount() > 5 && context.hasToolCalls()) { model = "gpt-4o"; }
return model; }
private String classifyIntent(String message, ChatContext context) { String lower = message.toLowerCase();
if (containsAny(lower, "订单", "物流", "发货", "快递")) { return "order_query"; } if (containsAny(lower, "推荐", "相似", "搜索", "找")) { return "product_search"; } if (containsAny(lower, "投诉", "不满", "差评", "退款") || context.getSentimentScore() < -0.5) { return "complaint"; } if (containsAny(lower, "退货", "换货", "售后") && context.getTurnCount() > 3) { return "complex_return"; }
return "general"; }
private boolean containsAny(String text, String... keywords) { for (String keyword : keywords) { if (text.contains(keyword)) return true; } return false; } }
|
优点:实现简单,延迟低(意图分类可以用规则或小模型)。
缺点:需要人工维护意图-模型映射表,新增意图时容易遗漏。
策略二:基于复杂度的动态路由
不预定义意图,而是根据消息本身的复杂度动态选择模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
|
@Component public class ComplexityBasedRouter {
private final ChatModel lightweightModel; private final ChatModel standardModel; private final ChatModel powerfulModel;
public ComplexityBasedRouter( @Qualifier("lightweight") ChatModel lightweightModel, @Qualifier("standard") ChatModel standardModel, @Qualifier("powerful") ChatModel powerfulModel) { this.lightweightModel = lightweightModel; this.standardModel = standardModel; this.powerfulModel = powerfulModel; }
public ChatResponse routeAndCall(List<Message> messages) { double complexity = assessComplexity(messages);
if (complexity < 0.3) { return lightweightModel.call(new Prompt(messages)); } else if (complexity < 0.7) { return standardModel.call(new Prompt(messages)); } else { return powerfulModel.call(new Prompt(messages)); } }
private double assessComplexity(List<Message> messages) { double score = 0.0;
int totalTokens = messages.stream() .mapToInt(m -> estimateTokens(m.getContent())) .sum(); score += Math.min(totalTokens / 2000.0, 0.3);
long turnCount = messages.stream() .filter(m -> m instanceof UserMessage) .count(); score += Math.min(turnCount / 10.0, 0.2);
boolean hasToolResults = messages.stream() .anyMatch(m -> m instanceof ToolResponseMessage); if (hasToolResults) score += 0.15;
String lastUserMsg = messages.stream() .filter(m -> m instanceof UserMessage) .reduce((a, b) -> b) .map(Message::getContent) .orElse(""); if (containsCodeOrMath(lastUserMsg)) score += 0.2;
if (containsMultiStepIndicators(lastUserMsg)) score += 0.15;
return Math.min(score, 1.0); }
private boolean containsCodeOrMath(String text) { return text.contains("```") || text.matches(".*\\d+\\s*[+\\-*/]\\s*\\d+.*") || text.contains("代码") || text.contains("bug"); }
private boolean containsMultiStepIndicators(String text) { return text.contains("首先") || text.contains("然后") || text.contains("步骤") || text.contains("分析") || text.contains("对比") || text.contains("评估"); }
private int estimateTokens(String text) { int chineseChars = (int) text.chars() .filter(c -> c >= 0x4e00 && c <= 0x9fff).count(); int otherChars = text.length() - chineseChars; return chineseChars * 2 + (int) (otherChars * 0.4); } }
|
优点:无需人工维护映射表,自适应能力强。
缺点:复杂度评估本身有误差,可能选错模型。需要配合回退机制。
策略三:基于反馈的自适应路由
最高级的路由策略——根据用户反馈和任务完成质量,动态调整路由策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
@Component public class AdaptiveModelRouter {
private final JdbcTemplate jdbc; private final ComplexityBasedRouter baseRouter;
private volatile double complexityThresholdLow = 0.3; private volatile double complexityThresholdHigh = 0.7;
public RouteResult routeAndTrack(List<Message> messages, String sessionId) { double complexity = baseRouter.assessComplexity(messages); String selectedModel = selectModel(complexity);
String requestId = UUID.randomUUID().toString(); recordRoutingDecision(requestId, sessionId, complexity, selectedModel);
return new RouteResult(requestId, selectedModel, complexity); }
public void recordFeedback(String requestId, FeedbackType type) { jdbc.update(""" UPDATE model_routing_log SET feedback_type = ?, feedback_at = NOW() WHERE request_id = ? """, type.name(), requestId); }
@Scheduled(fixedRate = 3600000) public void optimizeThresholds() { List<RouteFeedback> lowComplexityFeedback = jdbc.query(""" SELECT complexity_score, model_used, feedback_type FROM model_routing_log WHERE complexity_score < ? AND feedback_at > NOW() - INTERVAL '1 day' """, (rs, i) -> new RouteFeedback( rs.getDouble("complexity_score"), rs.getString("model_used"), FeedbackType.valueOf(rs.getString("feedback_type")) ), complexityThresholdLow);
long negativeCount = lowComplexityFeedback.stream() .filter(f -> f.feedbackType() == FeedbackType.NEGATIVE) .count(); double negativeRate = (double) negativeCount / lowComplexityFeedback.size();
if (negativeRate > 0.1) { complexityThresholdLow = Math.max(0.1, complexityThresholdLow - 0.05); log.info("调整低复杂度阈值至 {}", complexityThresholdLow); } }
private String selectModel(double complexity) { if (complexity < complexityThresholdLow) return "gpt-4o-mini"; if (complexity < complexityThresholdHigh) return "gpt-4o"; return "claude-4-opus"; }
private void recordRoutingDecision(String requestId, String sessionId, double complexity, String model) { jdbc.update(""" INSERT INTO model_routing_log (request_id, session_id, complexity_score, model_used, created_at) VALUES (?, ?, ?, ?, NOW()) """, requestId, sessionId, complexity, model); } }
|
成本对比:假设日均 5 万次调用,路由前全部使用 GPT-4o(输入 $2.5/1M Token,输出 $10/1M Token),路由后分布为:
| 模型 |
占比 |
单次成本 |
日成本 |
| GPT-4o-mini |
70% |
$0.0003 |
$10.5 |
| GPT-4o |
25% |
$0.003 |
$37.5 |
| Claude-4-opus |
5% |
$0.015 |
$37.5 |
| 总计 |
100% |
- |
$85.5 |
路由前:$0.003 × 50000 = $150/天。路由后节省 43%,月省约 $1935(约 1.4 万元)。
二、语义缓存:让相同问题只问一次
2.1 为什么传统缓存不够用?
传统缓存是精确匹配——key = "北京天气" 只能命中完全相同的 key。但用户问同样的问题,措辞可能完全不同:
- “北京今天天气怎么样?”
- “北京天气如何”
- “帝都今天热不热?”
这三句话意思完全一样,但精确缓存会认为是三个不同的请求,分别调用 LLM。
语义缓存的核心思想:用 Embedding 向量表示用户的问题,语义相似的问题命中同一份缓存。
2.2 语义缓存的实现架构
1 2 3
| 用户问题 → Embedding → 向量相似度搜索 → 命中? ├─ 命中 → 直接返回缓存结果 └─ 未命中 → 调用 LLM → 存入缓存
|
关键设计决策:
- 相似度阈值:太高会误命中不相关问题,太低会漏掉语义相同的问题。经验值:0.92-0.95
- 缓存失效策略:时间敏感的问题(天气、股价)需要短 TTL,知识性问题可以长 TTL
- 缓存粒度:是缓存整个回答,还是缓存中间结果(如工具调用结果)?
2.3 Spring AI 实现语义缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
|
@Service public class SemanticCacheService {
private final EmbeddingModel embeddingModel; private final VectorStore vectorStore; private final ChatModel chatModel;
private static final double SIMILARITY_THRESHOLD = 0.92;
private static final long CACHE_TTL_SECONDS = 3600;
public SemanticCacheService(EmbeddingModel embeddingModel, VectorStore vectorStore, ChatModel chatModel) { this.embeddingModel = embeddingModel; this.vectorStore = vectorStore; this.chatModel = chatModel; }
public CachedResponse callWithCache(String userMessage, String sessionId) { float[] queryEmbedding = embeddingModel.embed(userMessage);
SearchRequest searchRequest = SearchRequest.builder() .query(userMessage) .topK(1) .similarityThreshold(SIMILARITY_THRESHOLD) .build();
List<Document> results = vectorStore.similaritySearch(searchRequest);
if (!results.isEmpty()) { Document cached = results.get(0); double similarity = cached.getMetadata().get("similarity", Double.class);
if (isValidCache(cached)) { log.info("语义缓存命中! 相似度: {}, 原问题: {}, 当前问题: {}", similarity, cached.getMetadata().get("original_question"), userMessage);
return new CachedResponse( cached.getContent(), true, similarity ); } }
log.info("语义缓存未命中,调用 LLM: {}", userMessage); ChatResponse response = chatModel.call(new Prompt(userMessage)); String answer = response.getResult().getOutput().getContent();
storeInCache(userMessage, answer, sessionId);
return new CachedResponse(answer, false, 0.0); }
private void storeInCache(String question, String answer, String sessionId) { Map<String, Object> metadata = Map.of( "original_question", question, "session_id", sessionId, "cached_at", System.currentTimeMillis(), "ttl_seconds", CACHE_TTL_SECONDS, "type", "semantic_cache" );
String content = String.format("问题: %s\n答案: %s", question, answer); Document doc = new Document(content, metadata);
vectorStore.add(List.of(doc)); log.info("已缓存问答对: {}", question); }
private boolean isValidCache(Document cached) { Long cachedAt = cached.getMetadata().get("cached_at", Long.class); Long ttl = cached.getMetadata().get("ttl_seconds", Long.class);
if (cachedAt == null || ttl == null) return false;
long elapsed = (System.currentTimeMillis() - cachedAt) / 1000; return elapsed < ttl; } }
|
2.4 缓存失效策略
缓存最怕的是”过期数据”。用户问”今天天气”,如果返回昨天缓存的答案,那就闹笑话了。
分场景的失效策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
@Component public class CacheInvalidationStrategy {
public long determineTTL(String question) { String lower = question.toLowerCase();
if (containsAny(lower, "天气", "股价", "汇率", "今天", "现在", "最新")) { return 300; }
if (containsAny(lower, "什么是", "如何", "为什么", "介绍", "原理")) { return 86400; }
if (containsAny(lower, "我的", "我", "个人", "订单", "账户")) { return 0; }
return 3600; }
public boolean shouldCache(String question, String answer) { if (answer.length() < 20) return false;
if (answer.contains("抱歉") || answer.contains("无法回答")) return false;
return true; } }
|
成本影响:语义缓存的命中率通常在 20%-40%(取决于业务场景)。以 30% 命中率计算,日均 5 万次调用可以减少 1.5 万次 LLM 调用,月省约 $1350(约 1 万元)。
三、Token 优化:把每一 Token 都用在刀刃上
3.1 System Prompt 瘦身
很多 Agent 的 System Prompt 长达 3000-5000 Token,其中大量内容是”以防万一”加上去的。但每轮对话都要发送 System Prompt,5 万次调用就是 1.5 亿 Token 的浪费。
瘦身策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
|
@Component public class SystemPromptOptimizer {
private static final String FULL_SYSTEM_PROMPT = """ 你是一个智能客服助手。你的职责包括: 1. 回答用户关于商品的咨询 2. 处理订单相关的问题 3. 解决售后和退换货问题 4. 收集用户反馈和建议 5. 推荐相关商品 6. 处理投诉和升级问题
回答规则: - 保持友好、专业的语气 - 如果不确定,诚实地说"我不确定" - 涉及金额时要精确到分 - 退换货政策要在 7 天内 - 优惠券使用规则要详细说明 - 物流信息要提供单号和预计到达时间
工具使用规则: - 查询订单时使用 order_query 工具 - 搜索商品时使用 product_search 工具 - 处理退款时使用 refund_process 工具 - 查询物流时使用 logistics_track 工具
安全规则: - 不要泄露用户个人信息 - 不要承诺超出权限的优惠 - 不要绕过公司政策 - 敏感操作需要二次确认 """;
public String optimize(String intent) { return switch (intent) { case "order_query" -> """ 你是订单查询助手。使用 order_query 工具查询订单状态。 回答要简洁,包含订单号、状态、物流信息。 """;
case "product_search" -> """ 你是商品搜索助手。使用 product_search 工具搜索商品。 推荐时要说明推荐理由,对比价格和评价。 """;
case "complaint" -> FULL_SYSTEM_PROMPT;
default -> """ 你是智能客服助手。友好、专业地回答用户问题。 不确定时诚实说"我不确定"。不泄露用户个人信息。 """; }; }
public int calculateSavings(String intent) { int fullTokens = estimateTokens(FULL_SYSTEM_PROMPT); int optimizedTokens = estimateTokens(optimize(intent)); return fullTokens - optimizedTokens; } }
|
效果:简单查询的 System Prompt 从 300 Token 降到 50 Token,节省 83%。按 70% 的简单查询占比计算,日均节省约 875 万 Token,月省约 $650(约 4700 元)。
3.2 上下文窗口压缩
多轮对话时,上下文会越来越长。如果把所有历史消息都发给 LLM,Token 消耗会爆炸式增长。
滑动窗口 + 摘要压缩:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
@Component public class ContextCompressor {
private final ChatModel summaryModel;
private static final int KEEP_RECENT_TURNS = 5;
private static final int MAX_SUMMARY_TOKENS = 200;
public List<Message> compress(List<Message> history) { if (history.size() <= KEEP_RECENT_TURNS * 2) { return history; }
int splitIndex = history.size() - KEEP_RECENT_TURNS * 2; List<Message> oldHistory = history.subList(0, splitIndex); List<Message> recentHistory = history.subList(splitIndex, history.size());
String summary = generateSummary(oldHistory);
List<Message> compressed = new ArrayList<>(); compressed.add(new SystemMessage("以下是之前对话的摘要:\n" + summary)); compressed.addAll(recentHistory);
return compressed; }
private String generateSummary(List<Message> history) { StringBuilder conversation = new StringBuilder(); for (Message msg : history) { String role = msg instanceof UserMessage ? "用户" : "助手"; conversation.append(role).append(": ").append(msg.getContent()).append("\n"); }
String prompt = String.format(""" 请将以下对话压缩为 %d 字以内的摘要,保留关键信息: - 用户的主要问题 - 已经讨论的要点 - 已经做出的决定或承诺
对话内容: %s """, MAX_SUMMARY_TOKENS, conversation);
ChatResponse response = summaryModel.call(new Prompt(prompt)); return response.getResult().getOutput().getContent(); } }
|
效果:10 轮对话的上下文从 4000 Token 压缩到 1500 Token,节省 62.5%。按日均 30% 的请求是多轮对话计算,月省约 $900(约 6500 元)。
3.3 工具调用结果裁剪
Agent 调用工具后,返回的结果往往很长(比如一次 API 调用返回 2000 Token 的 JSON),但 LLM 只需要其中的一小部分信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
|
@Component public class ToolResultTrimmer {
public String trim(String toolResult, String userQuestion, int maxTokens) { if (estimateTokens(toolResult) <= maxTokens) { return toolResult; }
if (toolResult.trim().startsWith("{") || toolResult.trim().startsWith("[")) { return trimJsonResult(toolResult, userQuestion, maxTokens); }
return truncateText(toolResult, maxTokens); }
private String trimJsonResult(String json, String question, int maxTokens) { try { ObjectMapper mapper = new ObjectMapper(); JsonNode root = mapper.readTree(json);
Set<String> relevantFields = inferRelevantFields(question);
ObjectNode trimmed = mapper.createObjectNode(); if (root.isObject()) { root.fields().forEachRemaining(entry -> { if (relevantFields.isEmpty() || relevantFields.contains(entry.getKey())) { trimmed.set(entry.getKey(), entry.getValue()); } }); } else if (root.isArray()) { ArrayNode trimmedArray = mapper.createArrayNode(); int count = 0; for (JsonNode item : root) { if (count++ >= 3) break; trimmedArray.add(item); } return mapper.writeValueAsString(trimmedArray); }
String result = mapper.writeValueAsString(trimmed); if (estimateTokens(result) <= maxTokens) { return result; } } catch (Exception e) { }
return truncateText(json, maxTokens); }
private Set<String> inferRelevantFields(String question) { Set<String> fields = new HashSet<>(); if (question.contains("状态") || question.contains("进度")) { fields.add("status"); fields.add("state"); } if (question.contains("价格") || question.contains("多少钱")) { fields.add("price"); fields.add("amount"); } if (question.contains("时间") || question.contains("什么时候")) { fields.add("created_at"); fields.add("updated_at"); fields.add("estimated_delivery"); } return fields; }
private String truncateText(String text, int maxTokens) { int maxChars = maxTokens * 3; if (text.length() <= maxChars) return text; return text.substring(0, maxChars) + "\n...(结果已截断)"; } }
|
四、综合实战:构建成本优化的 Agent 服务
将上述三个策略整合到一个完整的 Agent 服务中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
@Service public class CostOptimizedAgentService {
private final IntentBasedModelRouter modelRouter; private final SemanticCacheService semanticCache; private final SystemPromptOptimizer promptOptimizer; private final ContextCompressor contextCompressor; private final ToolResultTrimmer toolResultTrimmer;
private final AtomicLong totalRequests = new AtomicLong(0); private final AtomicLong cacheHits = new AtomicLong(0); private final AtomicDouble totalCost = new AtomicDouble(0);
public AgentResponse process(String userMessage, String sessionId) { totalRequests.incrementAndGet();
CachedResponse cached = semanticCache.callWithCache(userMessage, sessionId); if (cached.isHit()) { cacheHits.incrementAndGet(); trackCost(sessionId, 0, cached.similarity(), true); return new AgentResponse(cached.content(), true, "cache_hit"); }
ChatContext context = loadContext(sessionId); String intent = modelRouter.classifyIntent(userMessage, context); String selectedModel = modelRouter.route(userMessage, context);
String systemPrompt = promptOptimizer.optimize(intent);
List<Message> history = contextCompressor.compress(context.getHistory());
List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage(systemPrompt)); messages.addAll(history); messages.add(new UserMessage(userMessage));
ChatModel model = resolveModel(selectedModel); ChatResponse response = model.call(new Prompt(messages)); String answer = response.getResult().getOutput().getContent();
int inputTokens = countTokens(messages); int outputTokens = estimateTokens(answer); double cost = calculateCost(selectedModel, inputTokens, outputTokens); trackCost(sessionId, cost, 0, false);
updateContext(sessionId, userMessage, answer);
return new AgentResponse(answer, false, selectedModel); }
public CostReport generateReport() { long total = totalRequests.get(); long hits = cacheHits.get(); double hitRate = total > 0 ? (double) hits / total : 0;
return new CostReport( total, hits, hitRate, totalCost.get(), totalCost.get() / Math.max(total, 1) ); } }
|
成本监控仪表盘
有了成本数据,还需要可视化和告警:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
|
@Component public class CostMonitor {
private final MeterRegistry meterRegistry;
private static final double DAILY_BUDGET = 50.0;
@PostConstruct public void initMetrics() { Gauge.builder("ai_agent_cost_total", this, CostMonitor::getTotalCost) .description("AI Agent 总成本") .register(meterRegistry);
Gauge.builder("ai_agent_cache_hit_rate", this, CostMonitor::getCacheHitRate) .description("语义缓存命中率") .register(meterRegistry);
Timer.builder("ai_agent_latency") .description("Agent 响应延迟") .register(meterRegistry); }
@Scheduled(fixedRate = 60000) public void checkBudget() { double todayCost = getTodayCost(); double ratio = todayCost / DAILY_BUDGET;
if (ratio > 0.8) { log.warn("⚠️ AI 成本已达预算的 {}%: ${}/{}", (int)(ratio * 100), todayCost, DAILY_BUDGET); }
if (ratio > 1.0) { log.error("🚨 AI 成本已超出日预算! 当前: ${}, 预算: ${}", todayCost, DAILY_BUDGET); triggerCostSavingMode(); } }
private void triggerCostSavingMode() { log.warn("进入成本节约模式:全部使用 mini 模型,缓存阈值降至 0.85"); } }
|
五、各策略的成本节省汇总
假设日均 5 万次调用,基线成本 $150/天(全部使用 GPT-4o):
| 优化策略 |
节省比例 |
月省金额 |
实施难度 |
| 模型路由 |
43% |
$1,935(约 1.4 万) |
⭐⭐ 中等 |
| 语义缓存 |
30% |
$1,350(约 1 万) |
⭐⭐⭐ 较高 |
| Prompt 瘦身 |
15% |
$675(约 4900 元) |
⭐ 简单 |
| 上下文压缩 |
10% |
$450(约 3300 元) |
⭐⭐ 中等 |
| 工具结果裁剪 |
5% |
$225(约 1600 元) |
⭐ 简单 |
| 综合优化 |
~70% |
$3,150(约 2.3 万) |
- |
注意:各策略有重叠,综合节省不是简单相加,实际约 60%-70%。
六、生产环境的最佳实践
6.1 渐进式上线
不要一次性开启所有优化策略。推荐上线顺序:
- 第一周:开启 Prompt 瘦身(风险最低,效果立竿见影)
- 第二周:开启模型路由(需要监控路由准确率)
- 第三周:开启上下文压缩(需要验证对话质量不下降)
- 第四周:开启语义缓存(需要调优相似度阈值)
6.2 A/B 测试
每次开启新策略前,先对 10% 的流量做 A/B 测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@Component public class CostOptimizationABTest {
public boolean isInExperimentGroup(String sessionId) { int hash = Math.abs(sessionId.hashCode()); return hash % 100 < 10; } }
|
6.3 监控指标
必须监控的核心指标:
- 每次请求平均成本:优化前后的直接对比
- 缓存命中率:低于 15% 说明阈值太严格
- 模型路由准确率:小模型回答失败需要回退的比例
- 用户满意度:优化不能以牺牲质量为代价
- P99 延迟:缓存和路由不能增加太多延迟
七、边界与局限
7.1 不适合优化的场景
- 创意生成任务:写文章、写代码不能用缓存,每次都需要原始输出
- 实时数据分析:股票、天气等实时数据不能缓存
- 个性化推荐:每个用户的结果不同,缓存命中率极低
7.2 优化的代价
- 维护复杂度:多了一套缓存和路由系统需要运维
- 延迟开销:Embedding 计算 + 向量搜索约增加 50-100ms
- 质量风险:小模型可能回答质量不如大模型,需要持续监控
7.3 未来方向
2026 年的趋势是模型成本持续下降。GPT-4o 的价格已经比 2024 年降了 60%。未来看点:
- 模型厂商的价格战会继续,小模型能力会越来越强
- 推测解码(Speculative Decoding)等推理优化技术会进一步降低成本
- 本地部署小模型(如 Qwen2.5-7B)在特定场景下可以实现零 API 成本
- 混合云架构:简单任务走本地模型,复杂任务走云端大模型
总结
AI Agent 的成本优化不是一次性工程,而是一个持续迭代的过程。核心策略三板斧:
- 模型路由——70% 的请求用小模型就够了,这是最大的节省来源
- 语义缓存——相同问题只问一次,命中率通常 20%-40%
- Token 优化——瘦身 Prompt、压缩上下文、裁剪工具结果
实施时记住三个原则:
- 渐进式上线:一次只开一个策略,观察一周再开下一个
- 质量优先:成本优化不能以牺牲用户体验为代价
- 持续监控:建立成本仪表盘,设置预算告警
回到开头那个电商客服的故事——技术团队用了三周时间实施上述策略,最终将月成本从 12 万元降到了 3.5 万元,**降幅 71%**,而用户满意度反而提升了 5%(因为路由策略让简单问题响应更快了)。
成本优化,本质上是在质量和效率之间找到最佳平衡点。这个平衡点不是静态的——随着模型能力提升和价格下降,你需要持续调整策略。但只要掌握了模型路由、语义缓存、Token 优化这三个核心工具,你就能在任何模型价格环境下,让 Agent 跑在成本效率的最前沿。
本文是 AI Agent 生产实战系列的第 28 篇。系列文章系统性地覆盖了 Agent 的记忆、推理、工具、安全、可观测性等核心维度。完整系列目录见文章顶部。