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

相关推荐
莫得感情 o6 小时前
Redis 05 · 持久化:RDB 与 AOF 怎么保证数据不丢
redis·缓存
zww89491117 小时前
匿名树洞系统开发实战:从需求分析到部署指南
java·eclipse
curd_boy7 小时前
【Redis】Redis从缓存到AI向量平台
人工智能·redis·缓存
kiracrimson8 小时前
从缓存的角度看链表与线性表的差异
数据结构·链表·缓存
黑马程序员毕设8 小时前
基于Java的医院药品管理系统的优化设计与实现
java·开发语言·spring boot·小程序·架构·课程设计·毕设
木井巳8 小时前
【BFS/DFS 解决 FloodFill 算法】太平洋大西洋水流问题
java·算法·leetcode·深度优先·广度优先·宽度优先·推荐算法
白山编程大哥9 小时前
Java 集合算法:从排序、查找到底层原理的实战指南
java·python·算法
devpotato9 小时前
缓存与数据库更新顺序不一致问题
java·数据库·redis
xcl09259 小时前
酒馆预约系统开发实战:从需求分析到上线全流程指南
java·spring boot·需求分析
Kyrie_kk9 小时前
Java--IO--Path文件访问
java·后端