Token 消耗异常的技术根源:上下文累积机制剖析

现象描述

运行 Clawdbot 一段时间后,你可能会遇到这样的情况:

  • 第一轮对话进行了 400 次交互,触发了 API 配额限制
  • 清空界面重新开始,第二轮只发送了 20-30 条简单消息,配额又被耗尽
  • 第二轮的每条消息都很短(比如"查天气"),但消耗速度反而更快

这个现象的根源在于 LLM 的上下文处理机制。

技术原理:上下文是线性累积的

单轮对话的实际输入

当你和 AI 进行多轮对话时,每一轮的输入都包含完整的历史记录。

示例对话:

[第1轮]
用户: 帮我订明天下午 3 点的会议室
AI: 好的,已预订。需要我发邮件通知参会人吗?

[第2轮]
用户: 要,发给张三和李四
AI: 邮件已发送。还有其他安排吗?

[第3轮]
用户: 没了,谢谢
AI: 不客气,随时找我。

到第3轮时,AI 实际接收到的输入是:

[历史] 帮我订明天下午 3 点的会议室
[历史] 好的,已预订。需要我发邮件通知参会人吗?
[历史] 要,发给张三和李四
[历史] 邮件已发送。还有其他安排吗?
[当前] 没了,谢谢

AI 必须读取所有历史才能理解"没了"指的是什么。

Token 消耗的数学模型

假设每轮对话平均产生 500 token(输入+输出),进行 N 轮对话后:

第1轮消耗: 500 token
第2轮消耗: 500 + 500 = 1,000 token
第3轮消耗: 500 + 500 + 500 = 1,500 token
...
第N轮消耗: 500 × N token

总消耗是一个等差数列求和

总消耗 = 500 × (1 + 2 + 3 + ... + N)
       = 500 × N × (N+1) / 2
       = 250 × N²  + 250 × N

这是一个 O(N²) 的复杂度

当 N = 400 时,总消耗约为 4000 万 token。

为什么"清空界面"不起作用?

会话持久化机制

Clawdbot 使用 SQLite 数据库存储会话历史,数据结构类似:

CREATE TABLE messages (
  id INTEGER PRIMARY KEY,
  session_id TEXT,
  role TEXT,  -- 'user' or 'assistant'
  content TEXT,
  timestamp INTEGER
);

CREATE TABLE sessions (
  id TEXT PRIMARY KEY,
  channel TEXT,  -- 'whatsapp', 'telegram', etc.
  user_id TEXT,
  created_at INTEGER
);

当你"重新开始对话"时,UI 层面可能清空了显示,但数据库中的记录依然存在。

上下文加载逻辑

每次处理新消息时,Agent 会执行:

async function getContext(sessionId: string) {
  const messages = await db.query(
    'SELECT * FROM messages WHERE session_id = ? ORDER BY timestamp',
    [sessionId]
  );
  return messages; // 返回所有历史记录
}

只要 session_id 没变,历史就会被完整加载。

为什么会话 ID 不变?

会话 ID 的生成逻辑通常基于:

function generateSessionId(channel: string, userId: string): string {
  return `${channel}:${userId}`;
}

对于同一个用户(比如你的 WhatsApp 号码),会话 ID 始终相同。除非你手动删除数据库记录,否则历史会一直累积。

第二轮为什么消耗更快?

假设第一轮累积了 20 万 token 的历史。

第二轮开始时,即使你只发送一条 10 token 的消息("查天气"),AI 接收到的输入是:

[20万token的历史]
+ "查天气" (10 token)
= 200,010 token

API 计费逻辑:

输入: 200,010 token
输出: 50 token (假设回复"今天晴天,20度")
本轮消耗: 200,060 token

如果继续对话 30 轮,每轮新增 100 token,总消耗会达到:

第1轮: 200,060 token
第2轮: 200,160 token
第3轮: 200,260 token
...
第30轮: 203,060 token

总计: 约 603 万 token

这就是为什么第二轮"只发了 20-30 条消息"就触发限额。

设计权衡:为什么不自动截断历史?

截断策略的问题

如果只保留最近 50 轮对话,会出现以下问题:

问题1:上下文断裂

[第1轮] 我是素食主义者
[第2-50轮] 各种对话
[第100轮] 推荐午餐
AI回复: 牛排不错 ← 忘记了用户是素食者

问题2:长期任务失败

[第1-20轮] 逐步构建一个复杂的数据分析任务
[第30轮] 继续刚才的分析
AI: 什么分析? ← 历史被截断,任务链断裂

Clawdbot 的选择

开发团队选择了"完整记忆优先"的策略:

宁愿烧 token,也要保证上下文完整性

这在"个人助手"场景下是合理的,因为用户期望 AI 能"记住"之前说过的事。

代价是用户必须自己管理会话生命周期。

技术解决方案

方案1:手动清理会话

执行命令:

clawdbot session clear <channel> <user-id>

底层执行:

DELETE FROM messages 
WHERE session_id = 'whatsapp:+1234567890';

缺点:AI 会"失忆",之前的上下文全部丢失。

方案2:滑动窗口

修改上下文加载逻辑:

async function getContext(sessionId: string, maxTokens: number = 50000) {
  const messages = await db.query(
    'SELECT * FROM messages WHERE session_id = ? ORDER BY timestamp DESC',
    [sessionId]
  );
  
  let tokens = 0;
  let result = [];
  
  for (const msg of messages) {
    const msgTokens = estimateTokens(msg.content);
    if (tokens + msgTokens > maxTokens) break;
    tokens += msgTokens;
    result.unshift(msg);
  }
  
  return result;
}

这个方案只加载最近的 5 万 token,但会动态调整窗口大小。

方案3:上下文压缩

使用另一个 LLM 对历史进行摘要:

async function compressHistory(messages: Message[]): Promise<string> {
  const summary = await summarizationModel.call({
    prompt: `总结以下对话的关键信息:
    ${messages.map(m => m.content).join('\n')}
    
    只保留重要事实和决策,忽略闲聊。`
  });
  
  return summary;
}

原始 400 轮对话(20 万 token)可能被压缩成 500 token 的摘要。

但这个方案有成本:摘要本身也需要调用 API。

方案4:分层记忆架构

引入三层记忆结构:

1. 短期记忆 (最近50轮,完整保留)
2. 中期记忆 (最近500轮,保留摘要)
3. 长期记忆 (所有历史,只保留关键事实)

查询时,先读短期,再读中期,最后读长期。

这类似人类的记忆模型。

上下文缓存技术

Anthropic 的 Prompt Caching

2025 年底,Anthropic 推出了"提示词缓存"功能(仅企业客户)。

工作原理:

第1轮:
  输入: [历史A] + 新消息1
  缓存: 历史A (存储在服务端)
  
第2轮:
  输入: [缓存引用:历史A] + 历史B + 新消息2
  只计费: 历史B + 新消息2

相同的历史部分不重复计费。

但这个功能目前只对企业开放,个人用户无法使用。

监控与预警

实时 Token 计数

在发送请求前,预估消耗:

async function estimateCost(sessionId: string, newMessage: string) {
  const history = await getContext(sessionId);
  const historyTokens = history.reduce((sum, msg) => 
    sum + estimateTokens(msg.content), 0
  );
  const newTokens = estimateTokens(newMessage);
  
  return {
    inputTokens: historyTokens + newTokens,
    estimatedCost: (historyTokens + newTokens) * 0.000015 // $15/M tokens
  };
}

如果预估消耗超过阈值,拒绝请求并提示用户清理历史。

设置硬性上限

if (estimatedTokens > 100000) {
  throw new Error(
    '上下文过大(超过10万token),请运行 session clear 清理历史'
  );
}

结论

Token 消耗异常不是 bug,而是 LLM 上下文处理机制的必然结果。

核心矛盾在于:

  • 用户期望:AI 能记住所有对话
  • 技术现实:每次加载完整历史都会重新计费

在没有"上下文缓存"技术普及之前,用户必须在"记忆完整性"和"成本控制"之间做出权衡。

建议策略:

  1. 对话超过 100 轮时主动清理
  2. 使用多 Agent 模式隔离不同任务的上下文
  3. 监控 token 消耗,设置预算上限
  4. 等待 Prompt Caching 技术向个人用户开放
posted @ 2026-01-27 14:34  147API  阅读(0)  评论(0)    收藏  举报