系列文章

本系列系统性地讲解 AI Agent 的核心概念、架构设计与生产实战,从理论到工程全方位覆盖:

  1. AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
  2. 理解 AI Agent 的大脑:ReAct 模式从入门到实战
  3. 从零理解 RAG:检索增强生成完整指南
  4. MCP 模型上下文协议:AI 的万能接口与 MCP Server 实战
  5. 让 AI 学会”说人话”——Spring AI 结构化输出实战
  6. AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
  7. AI Agent 的规划大脑:从任务分解到自适应执行策略
  8. AI Agent 的灵魂对话:Prompt Engineering 系统提示词设计的艺术与工程
  9. AI Agent 评估与优化:从基准测试到生产环境的质量守护实战
  10. Embedding 向量化的魔法:从文本到向量的数学之旅与 Java 实战
  11. 当 RAG 遇上知识图谱:GraphRAG 原理与 Java 实战
  12. 当 RAG 遇到 Agent:Agentic RAG 的架构设计与 Java 实战
  13. AI Agent 的安全防线:Prompt 注入防御与生产级安全防护实战
  14. AI Agent 的推理引擎:从 Chain-of-Thought 到推理模型的深度解析与 Java 实战
  15. Spring AI 核心架构全解析:从 ChatModel 到 Advisor Chain 的设计哲学
  16. AI Agent 的知识检索引擎:从向量搜索到智能检索策略的 Java 实战
  17. AI Agent 的工作流编排:从顺序链到自适应 DAG 的 Java 实战
  18. AI Agent 的流式响应与实时交互:从 SSE 到 WebSocket 的 Java 实战
  19. AI Agent 的可观测性:从链路追踪到成本监控的 Java 实战
  20. AI Agent 的多模态感知:从图片理解到语音交互的 Java 实战
  21. AI Agent 的自我反思与经验学习:从错误中进化的 Java 实战
  22. Agent 间如何对话:A2A 协议深度解析与 Java 实战
  23. AI Agent 的人机协作:从 Human-in-the-Loop 到渐进式自治的 Java 实战
  24. AI Agent 的上下文工程与 Token 预算管理:从窗口压缩到成本优化的 Java 实战
  25. AI Agent 的容错与韧性:从错误处理到生产级可靠性保障的 Java 实战
  26. AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战
  27. 从理论到生产:AI Agent 全景知识图谱与 Java 开发者成长路线
  28. AI Agent 的成本优化:从模型路由到缓存策略的 Java 实战(本文)

引子:一个真实的成本崩溃故事

2026 年初,某电商平台上线了基于 AI Agent 的智能客服系统。上线第一周,日均调用量 5 万次,每次调用平均消耗 2000 Token,使用 GPT-4o 模型。财务部门月底看到账单时惊呆了——单月 AI 调用费用超过 12 万元

更可怕的是,随着用户量增长,这个数字还在以每月 30% 的速度膨胀。

技术团队紧急复盘,发现问题出在三个地方:

  1. 所有请求都用最贵的模型——简单的”查订单状态”和复杂的”退换货方案制定”用的是同一个 GPT-4o
  2. 相同问题重复调用——用户问”发货时间”,每个会话都重新调用 LLM,没有任何缓存
  3. 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; // gpt-4o-mini
private final ChatModel standardModel; // gpt-4o
private final ChatModel powerfulModel; // claude-4-opus

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));
}
}

/**
* 复杂度评估器
* 综合考虑多个维度,返回 0-1 的复杂度分数
*/
private double assessComplexity(List<Message> messages) {
double score = 0.0;

// 维度1:消息长度(越长越复杂)
int totalTokens = messages.stream()
.mapToInt(m -> estimateTokens(m.getContent()))
.sum();
score += Math.min(totalTokens / 2000.0, 0.3); // 最高 0.3

// 维度2:对话轮次(轮次越多越复杂)
long turnCount = messages.stream()
.filter(m -> m instanceof UserMessage)
.count();
score += Math.min(turnCount / 10.0, 0.2); // 最高 0.2

// 维度3:是否包含工具调用结果
boolean hasToolResults = messages.stream()
.anyMatch(m -> m instanceof ToolResponseMessage);
if (hasToolResults) score += 0.15;

// 维度4:是否包含代码或数学内容
String lastUserMsg = messages.stream()
.filter(m -> m instanceof UserMessage)
.reduce((a, b) -> b) // 取最后一条
.map(Message::getContent)
.orElse("");
if (containsCodeOrMath(lastUserMsg)) score += 0.2;

// 维度5:是否要求多步推理
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) {
// 粗略估算:中文 1 字 ≈ 2 Token,英文 1 词 ≈ 1.3 Token
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) {
// 负面反馈超过 10%,降低阈值(更保守地使用小模型)
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 → 存入缓存

关键设计决策:

  1. 相似度阈值:太高会误命中不相关问题,太低会漏掉语义相同的问题。经验值:0.92-0.95
  2. 缓存失效策略:时间敏感的问题(天气、股价)需要短 TTL,知识性问题可以长 TTL
  3. 缓存粒度:是缓存整个回答,还是缓存中间结果(如工具调用结果)?

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
/**
* 语义缓存服务
* 核心思想:用 Embedding 向量做相似度匹配,语义相同的问题命中缓存
*/
@Service
public class SemanticCacheService {

private final EmbeddingModel embeddingModel;
private final VectorStore vectorStore;
private final ChatModel chatModel;

// 相似度阈值:0.92 表示只有非常相似的问题才命中缓存
private static final double SIMILARITY_THRESHOLD = 0.92;

// 缓存 TTL(秒)
private static final long CACHE_TTL_SECONDS = 3600; // 1 小时

public SemanticCacheService(EmbeddingModel embeddingModel,
VectorStore vectorStore,
ChatModel chatModel) {
this.embeddingModel = embeddingModel;
this.vectorStore = vectorStore;
this.chatModel = chatModel;
}

/**
* 带语义缓存的 LLM 调用
*/
public CachedResponse callWithCache(String userMessage, String sessionId) {
// 第一步:生成问题的 Embedding
float[] queryEmbedding = embeddingModel.embed(userMessage);

// 第二步:在向量库中搜索相似问题
SearchRequest searchRequest = SearchRequest.builder()
.query(userMessage)
.topK(1) // 只取最相似的 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 // 相似度分数
);
}
}

// 第四步:缓存未命中,调用 LLM
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
/**
* 智能缓存失效策略
* 根据问题类型动态调整 TTL
*/
@Component
public class CacheInvalidationStrategy {

/**
* 根据问题内容判断缓存 TTL
*/
public long determineTTL(String question) {
String lower = question.toLowerCase();

// 实时性问题:短 TTL(5 分钟)
if (containsAny(lower, "天气", "股价", "汇率", "今天", "现在", "最新")) {
return 300; // 5 分钟
}

// 知识性问题:长 TTL(24 小时)
if (containsAny(lower, "什么是", "如何", "为什么", "介绍", "原理")) {
return 86400; // 24 小时
}

// 个性化问题:不缓存(每个用户答案不同)
if (containsAny(lower, "我的", "我", "个人", "订单", "账户")) {
return 0; // 不缓存
}

// 默认:1 小时
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
/**
* System Prompt 优化器
* 核心思想:根据当前上下文动态裁剪 System Prompt
*/
@Component
public class SystemPromptOptimizer {

// 完整的 System Prompt(用于复杂任务)
private static final String FULL_SYSTEM_PROMPT = """
你是一个智能客服助手。你的职责包括:
1. 回答用户关于商品的咨询
2. 处理订单相关的问题
3. 解决售后和退换货问题
4. 收集用户反馈和建议
5. 推荐相关商品
6. 处理投诉和升级问题

回答规则:
- 保持友好、专业的语气
- 如果不确定,诚实地说"我不确定"
- 涉及金额时要精确到分
- 退换货政策要在 7 天内
- 优惠券使用规则要详细说明
- 物流信息要提供单号和预计到达时间

工具使用规则:
- 查询订单时使用 order_query 工具
- 搜索商品时使用 product_search 工具
- 处理退款时使用 refund_process 工具
- 查询物流时使用 logistics_track 工具

安全规则:
- 不要泄露用户个人信息
- 不要承诺超出权限的优惠
- 不要绕过公司政策
- 敏感操作需要二次确认
""";

/**
* 根据意图裁剪 System Prompt
*/
public String optimize(String intent) {
return switch (intent) {
case "order_query" -> """
你是订单查询助手。使用 order_query 工具查询订单状态。
回答要简洁,包含订单号、状态、物流信息。
""";

case "product_search" -> """
你是商品搜索助手。使用 product_search 工具搜索商品。
推荐时要说明推荐理由,对比价格和评价。
""";

case "complaint" -> FULL_SYSTEM_PROMPT; // 投诉处理需要完整 Prompt

default -> """
你是智能客服助手。友好、专业地回答用户问题。
不确定时诚实说"我不确定"。不泄露用户个人信息。
""";
};
}

/**
* 计算节省的 Token 数
*/
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
/**
* 上下文压缩器
* 核心思想:保留最近 N 轮对话,更早的历史压缩为摘要
*/
@Component
public class ContextCompressor {

private final ChatModel summaryModel; // 用于生成摘要的小模型

// 保留最近 5 轮对话的原始内容
private static final int KEEP_RECENT_TURNS = 5;

// 摘要的最大 Token 数
private static final int MAX_SUMMARY_TOKENS = 200;

/**
* 压缩对话历史
*/
public List<Message> compress(List<Message> history) {
if (history.size() <= KEEP_RECENT_TURNS * 2) {
return history; // 不需要压缩
}

// 分割:旧历史 vs 最近对话
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 {

/**
* 裁剪工具调用结果
* @param toolResult 原始工具结果
* @param userQuestion 用户问题(用于判断哪些信息相关)
* @param maxTokens 最大 Token 数
*/
public String trim(String toolResult, String userQuestion, int maxTokens) {
// 如果结果已经很短,直接返回
if (estimateTokens(toolResult) <= maxTokens) {
return toolResult;
}

// 尝试解析 JSON 并提取关键字段
if (toolResult.trim().startsWith("{") || toolResult.trim().startsWith("[")) {
return trimJsonResult(toolResult, userQuestion, maxTokens);
}

// 文本结果:截断 + 添加省略标记
return truncateText(toolResult, maxTokens);
}

/**
* JSON 结果裁剪
* 根据用户问题推断需要的字段,只保留这些字段
*/
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()) {
// 数组只保留前 3 个元素
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) {
// JSON 解析失败,走文本截断
}

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
/**
* 成本优化的 Agent 服务
* 整合模型路由 + 语义缓存 + Token 优化
*/
@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);

// 第三步:System Prompt 优化
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() {
// 注册 Prometheus 指标
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() {
// 1. 所有请求强制使用最便宜的模型
// 2. 提高缓存相似度阈值(更容易命中缓存)
// 3. 降低上下文窗口大小
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 渐进式上线

不要一次性开启所有优化策略。推荐上线顺序:

  1. 第一周:开启 Prompt 瘦身(风险最低,效果立竿见影)
  2. 第二周:开启模型路由(需要监控路由准确率)
  3. 第三周:开启上下文压缩(需要验证对话质量不下降)
  4. 第四周:开启语义缓存(需要调优相似度阈值)

6.2 A/B 测试

每次开启新策略前,先对 10% 的流量做 A/B 测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 成本优化的 A/B 测试框架
*/
@Component
public class CostOptimizationABTest {

/**
* 判断当前请求是否在实验组
*/
public boolean isInExperimentGroup(String sessionId) {
// 用 sessionId 的 hash 值做分流,保证同一用户始终在同一组
int hash = Math.abs(sessionId.hashCode());
return hash % 100 < 10; // 10% 流量进入实验组
}
}

6.3 监控指标

必须监控的核心指标:

  • 每次请求平均成本:优化前后的直接对比
  • 缓存命中率:低于 15% 说明阈值太严格
  • 模型路由准确率:小模型回答失败需要回退的比例
  • 用户满意度:优化不能以牺牲质量为代价
  • P99 延迟:缓存和路由不能增加太多延迟

七、边界与局限

7.1 不适合优化的场景

  • 创意生成任务:写文章、写代码不能用缓存,每次都需要原始输出
  • 实时数据分析:股票、天气等实时数据不能缓存
  • 个性化推荐:每个用户的结果不同,缓存命中率极低

7.2 优化的代价

  • 维护复杂度:多了一套缓存和路由系统需要运维
  • 延迟开销:Embedding 计算 + 向量搜索约增加 50-100ms
  • 质量风险:小模型可能回答质量不如大模型,需要持续监控

7.3 未来方向

2026 年的趋势是模型成本持续下降。GPT-4o 的价格已经比 2024 年降了 60%。未来看点:

  1. 模型厂商的价格战会继续,小模型能力会越来越强
  2. 推测解码(Speculative Decoding)等推理优化技术会进一步降低成本
  3. 本地部署小模型(如 Qwen2.5-7B)在特定场景下可以实现零 API 成本
  4. 混合云架构:简单任务走本地模型,复杂任务走云端大模型

总结

AI Agent 的成本优化不是一次性工程,而是一个持续迭代的过程。核心策略三板斧:

  1. 模型路由——70% 的请求用小模型就够了,这是最大的节省来源
  2. 语义缓存——相同问题只问一次,命中率通常 20%-40%
  3. Token 优化——瘦身 Prompt、压缩上下文、裁剪工具结果

实施时记住三个原则:

  • 渐进式上线:一次只开一个策略,观察一周再开下一个
  • 质量优先:成本优化不能以牺牲用户体验为代价
  • 持续监控:建立成本仪表盘,设置预算告警

回到开头那个电商客服的故事——技术团队用了三周时间实施上述策略,最终将月成本从 12 万元降到了 3.5 万元,**降幅 71%**,而用户满意度反而提升了 5%(因为路由策略让简单问题响应更快了)。

成本优化,本质上是在质量效率之间找到最佳平衡点。这个平衡点不是静态的——随着模型能力提升和价格下降,你需要持续调整策略。但只要掌握了模型路由、语义缓存、Token 优化这三个核心工具,你就能在任何模型价格环境下,让 Agent 跑在成本效率的最前沿。


本文是 AI Agent 生产实战系列的第 28 篇。系列文章系统性地覆盖了 Agent 的记忆、推理、工具、安全、可观测性等核心维度。完整系列目录见文章顶部。