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 能记住所有对话
- 技术现实:每次加载完整历史都会重新计费
在没有"上下文缓存"技术普及之前,用户必须在"记忆完整性"和"成本控制"之间做出权衡。
建议策略:
- 对话超过 100 轮时主动清理
- 使用多 Agent 模式隔离不同任务的上下文
- 监控 token 消耗,设置预算上限
- 等待 Prompt Caching 技术向个人用户开放
浙公网安备 33010602011771号