Token成本控制实战:语义缓存、上下文压缩与工具缓存

Token成本控制实战:语义缓存、上下文压缩与工具缓存

LLM 按 Token 计费,一个中等规模的 AI 应用每天可能产生数百万 Token 消耗。以 GPT-4o 为例,input 2.50/1M tokens、output 10.00/1M tokens,一次包含 30 轮对话的练习会话可能消耗 15K+ tokens,月活 1000 用户的场景下月成本轻松过万。

Token 消耗的三大来源:上下文(System Prompt + 历史消息)占 60-70%,工具调用结果注入占 15-20%,输出生成占 10-15%。控制成本的核心就是从这三个来源下手。

我在 nvc-guide里做了四层成本控制,今天详细聊聊。

第一层:语义缓存

传统缓存的困境

传统缓存基于精确匹配(hash(query) == hash(cached_query)),但 LLM 场景中,用户提问存在大量语义等价但字面不同的情况:

  • "什么是 NVC?" vs "NVC 是什么意思?" vs "介绍一下 NVC"
  • 传统缓存:3 次 LLM 调用
  • 语义缓存:1 次 LLM 调用 + 2 次缓存命中

语义缓存原理

复制代码
用户问题 → Embedding 向量化 → 与缓存库做相似度搜索
    ├── 相似度 ≥ 0.95 → 命中缓存,直接返回
    └── 相似度 < 0.95 → 调用 LLM → 将结果存入缓存

关键技术参数:

参数 说明 推荐值
相似度阈值 控制缓存命中精度 0.93-0.97
TTL 缓存过期时间 7-30 天
Embedding 模型 向量化模型 text-embedding-3-small

阈值选择的权衡

0.95 会不会太高?命中率只有 30-40% 值得吗?

阈值选择是命中率和准确率的权衡。阈值太高(>0.98)命中率极低;阈值太低(<0.90)容易误命中------"NVC 的四个步骤"和"NVC 的四个要素"向量相似度可能达到 0.90,但答案完全不同。

选择 0.95 的原因:知识问答类场景中,回答错误的代价远高于多调用一次 LLM。宁可少命中,也不能答错。

30-40% 的命中率意味着每天减少 300-400 次 LLM 调用,按每次 0.05 计算,每天节省 15-20,一个月 $450-600。

实现细节

项目用 pgvector 存储向量,直接用 SQL 做余弦相似度搜索:

sql 复制代码
SELECT *, 1 - (query_embedding <=> CAST(:embedding AS vector)) AS score
FROM nvc_semantic_cache
WHERE 1 - (query_embedding <=> CAST(:embedding AS vector)) > :threshold
  AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY query_embedding <=> CAST(:embedding AS vector)
LIMIT :limit

几个设计决策:

场景白名单。只缓存知识问答类场景(7 种 Coach),不缓存评估和反思------这些任务的输出高度依赖用户的具体表现,缓存会导致评估结果失真。

异常降级。所有缓存操作都 try-catch 包裹,任何异常都降级为"缓存未命中",不影响主流程。

定时清理。每天凌晨 3 点清理过期条目,防止缓存表无限膨胀。

第二层:上下文压缩

Token 增长问题

多轮对话中,Token 数量持续增长。以 30 轮对话为例:System Prompt 1500 + 用户画像 300 + RAG 文档 2000 + 历史消息 30×400 = 15800 tokens。加上工具调用记录,实际消耗可能达到 20K+。

三层压缩方案

复制代码
消息数 ≤ 20 → 直接返回(不压缩)
消息数 > 20 → 触发压缩
  ├── 早期消息(第 1 到 N-10 条)→ LLM 摘要(不超过 500 字)
  ├── 最近消息(最后 10 条)→ 保持不变
  └── 摘要注入 System Prompt

为什么是 20 轮? 20 轮对话约消耗 6000-8000 tokens,加上 System Prompt 和用户画像,总计约 10K tokens,留出充足输出空间。

为什么保留 10 轮? NVC 练习需要足够的近期上下文来理解用户当前状态。10 轮能覆盖用户最近 2-3 个 NVC 步骤的讨论。

摘要注入位置:注入 System Prompt 而不是消息列表。System Prompt 在 LLM 的注意力机制中权重最高,保证摘要被优先关注。

降级截断

LLM 摘要调用失败时,降级为截断模式------取前 5 条消息,每条截断到 200 字符。保证即使 LLM 服务不可用,对话也能继续。

压缩效果

早期消息约 4000-6000 tokens → 摘要约 700 tokens,节省约 40% Token。

第三层:工具结果缓存

Agent 调用的工具(RAG 检索、用户档案查询等)结果在短时间内不会变化。在 Hook 链中拦截重复的工具调用,直接返回缓存结果。

分层 TTL

工具 TTL 理由
dashboard_query 5 分钟 仪表盘数据变化频繁
profile_query 10 分钟 用户档案较稳定
rag_search 30 分钟 知识库不常更新
wiki_search 30 分钟 Wiki 内容不常变化

缓存 Key 策略

两种模式:

  • 用户级缓存{toolName}:{userId},同一用户短时间内结果相同(dashboard、profile)
  • 参数级缓存{toolName}:{userId}:{paramHash},不同查询参数结果不同(rag、wiki)

为什么用 JVM 内存而不是 Redis?

工具缓存在 LLM 推理的关键路径上。JVM 内存访问 0.001ms vs Redis 网络往返 1-2ms,差距 1000 倍。且工具缓存是短 TTL、高频访问、允许重启后丢失的数据,完全适合内存缓存。

实现方式

在自建的 Hook 链中,CacheToolHook 位于 @Order(3)

复制代码
beforeToolCall: 检查缓存
  ├── 命中 → 设置 skipReason → 返回 SKIP(跳过工具执行)
  └── 未命中 → 返回 PROCEED(继续执行工具)
afterToolCall: 缓存结果
  ├── 成功结果 → 存入缓存
  └── 错误结果 → 不缓存(避免缓存错误导致后续请求持续返回错误)

第四层:模型路由

不同任务对模型能力的要求不同。简单任务(如意图识别、格式化输出)用小模型处理,复杂任务(如 NVC 评估、反思分析)才用大模型。

项目通过 LlmProviderRegistry 支持多 Provider 动态管理:

场景 ChatClient 变体 特点
Agent 对话 getDefaultChatClient() 带工具 + 带记忆
出题/评分 getPlainChatClient() 无工具、无记忆
语音练习 getVoiceChatClient() 带工具、无记忆

评估和反思用 Plain ChatClient(无工具、无记忆),减少无关 Token 注入。

成本监控

通过 MetricsCollector 采集四项核心指标:

指标类型 采集内容 用途
TOKEN inputTokens, outputTokens, model 成本核算
COMPRESSION beforeTokens, afterTokens, reductionPercent 压缩效果分析
TOOL_CALL toolName, success, latencyMs 工具效率分析
LATENCY latencyMs, phase 性能监控

指标通过 Redis Stream 异步写入 PostgreSQL,不影响主流程性能。

整体效果

复制代码
用户消息
  │
  ▼
[语义缓存] --命中--> 直接返回(省 100% Token)
  │ 未命中
  ▼
[上下文压缩] --超过 20 轮--> LLM 摘要(省 40%)
  │
  ▼
[AgentLoop + 工具缓存] --缓存命中--> 跳过工具执行
  │
  ▼
[LLM 调用 + 响应返回 + 语义缓存写入]

四层策略配合使用,Token 消耗降低约 60%,同时保证用户体验不受影响。

小结

Token 成本控制不是单一手段能解决的,需要多层配合。语义缓存解决"重复调用"问题,上下文压缩解决"上下文膨胀"问题,工具缓存解决"重复查询"问题,模型路由解决"大材小用"问题。四层叠加,才能在保证质量的前提下把成本压下来。

相关推荐
乐观勇敢坚强的老彭1 小时前
C++ STL 常用容器的速查表
java·c++·算法
wwwzhouzy1 小时前
SpringBoot 响应式编程
java·spring boot·后端·响应式编程
Bonnie_12151 小时前
10-深入理解ConcurrentHashMap(JDK1.8)
java·开发语言
莫得感情 o1 小时前
设计模式 22 · 三个冷门模式:中介者、访问者、解释器
java·设计模式
evans在进步2 小时前
LeetCode 74:搜索二维矩阵——Java 虚拟一维数组与二分查找详解
java·leetcode·矩阵
andongni2032 小时前
Spring Boot基础应用开发与部署
java·spring boot·后端
lupai2 小时前
增值税发票 OCR 识别 API 新手接入指南
java·前端·ocr
TELL5212 小时前
Sonar质量门禁
java
java修仙传2 小时前
从网页禅道到 AI 能调用的工具:我的禅道 MCP 实现思路分享
java·人工智能·python·ai应用·mcp开发