AI API 多轮对话怎么控制成本?历史消息裁剪与摘要交接教程
从历史消息分层、Token 预算、摘要交接到 Node.js 裁剪示例,讲清多轮对话如何减少重复输入并保持关键上下文。
AI API 多轮对话怎么控制成本?历史消息裁剪与摘要交接教程
聊天应用每次调用 API 时,模型通常不会自动记住上一次请求。客户端为了保持上下文,会把系统提示词、历史用户消息、历史回答和本轮问题一起发送。对话越长,每一轮重复发送的输入 Token 越多,延迟、上下文占用和费用也会一起上升。
控制成本的重点不是简单地“删除历史”,而是保留完成当前任务真正需要的信息。可以把消息分成固定规则、近期原文、长期事实和已完成任务四层,再根据预算和上下文窗口决定哪些内容继续发送。
一、先算清楚每轮到底发送了什么
一轮请求的输入通常包括:系统提示词、工具定义、历史消息、本轮用户问题、附件转换后的文字,以及可能被重复放入的业务说明。输出则是模型本轮生成的 Token。具体计费还要看服务商的输入/输出单价、缓存规则、倍率和最低计费单位。
可以先用下面的估算式做预算:
单轮费用 ≈ 输入 Token × 输入单价
+ 输出 Token × 输出单价
+ 缓存写入/读取及其他项目费用
这是成本估算,不是账单结算公式。不同模型的 Token 切分不同,中文、代码、表格和图片也不能只按字符数准确换算。最终应以请求明细返回的 usage 和服务商账单为准。计费基础可参考 AI API Token 与计费规则。
二、不要把所有历史消息原样保留
最简单的策略是只保留最近 N 轮,但它可能丢掉早期的任务目标和用户偏好。更稳妥的策略是按信息价值分层:
| 内容 | 建议处理方式 | 原因 |
|---|---|---|
| 系统规则、输出格式、安全边界 | 固定保留并版本化 | 缺失后行为可能改变 |
| 最近几轮问答 | 原文保留 | 需要理解代词、代码和上下文衔接 |
| 已确认的用户偏好、项目约束 | 压缩成事实清单 | 不必重复发送长篇对话 |
| 已完成且不再引用的任务 | 摘要或删除 | 降低输入 Token |
| 大段工具返回、日志和文件 | 提取结论,原文按需检索 | 避免每轮重复塞入 |
裁剪前先定义“保留条件”:当前问题是否引用它?它是否改变下一步决策?如果两个答案都是“否”,就可以从默认上下文移除,而不是为了看起来完整而持续付费。
三、用摘要交接,而不是简单截断
当历史达到预算阈值时,可以让模型把旧消息压缩成结构化摘要,再用摘要替换旧对话。摘要应明确区分事实、未完成事项和不确定内容,避免把推测写成已确认结论。
请把下面的历史压缩成后续对话可用的交接摘要。
只保留:
1. 已确认的目标与约束;
2. 已完成的操作和结果;
3. 未解决的问题与下一步;
4. 必须原样保留的 ID、字段名或代码片段。
不确定的信息标记“待确认”,不要补写历史中没有的事实。
摘要也会产生一次调用费用,因此不要每一轮都重新总结。可在输入 Token 超过上下文预算的 60%~80%,或完成一个阶段性任务后触发。阈值只是工程起点,应根据模型上下文上限、平均输出长度和账单数据调整。
四、Node.js 的最小裁剪示例
下面的代码只演示本地消息如何按预算保留,不负责计算真实 Token。生产环境应使用目标模型对应的 tokenizer 或服务商返回的 usage 进行校准。system 消息始终保留,最近消息从后向前加入,超出估算预算的旧消息被跳过。
function roughTokens(text) {
// 仅用于预筛选,不代表服务商的实际 Token 数。
return Math.ceil(String(text ?? '').length / 2)
}
function messageCost(message) {
const content = typeof message.content === 'string'
? message.content
: JSON.stringify(message.content ?? '')
return roughTokens(content) + 4 // 给 role、分隔符留出粗略余量
}
function buildContext(messages, inputBudget) {
const system = messages.filter(m => m.role === 'system')
const rest = messages.filter(m => m.role !== 'system')
const selected = []
let used = system.reduce((sum, m) => sum + messageCost(m), 0)
for (let i = rest.length - 1; i >= 0; i -= 1) {
const cost = messageCost(rest[i])
if (used + cost > inputBudget) break
selected.unshift(rest[i])
used += cost
}
return { messages: [...system, ...selected], estimatedInputTokens: used }
}
这段代码按字符做粗略估算,不能用于计费、权限或安全决策。对于中文、代码、表格和多模态内容,估算误差可能明显。正式上线前,应为每个模型记录实际 input_tokens,并把裁剪前后差值写入内部指标。
五、为摘要和历史设置安全边界
摘要接口接收的内容可能包含密钥、个人信息、客户代码或内部链接。调用摘要模型前,先脱敏或删除不需要的敏感字段;不要因为“只是内部摘要”就把完整数据库记录发送给第三方服务商。
同时设置三类上限:单次输入 Token、历史消息条数、摘要最大长度。摘要失败时应保留最近几轮并返回可读提示,不要在失败循环中不断重新总结。摘要结果要带版本号和生成时间,旧摘要被替换前先完成一次可恢复的写入。
如果应用支持用户删除会话,原始历史、摘要、缓存和检索索引都应按同一删除规则处理。日志中只记录会话 ID、Token 统计和裁剪原因,不记录完整对话。
六、用数据判断是否真的省钱
上线前后至少比较以下指标:平均输入 Token、输出 Token、单轮费用、首字节延迟、完整耗时、摘要调用占比和用户重新提问率。如果输入下降但重新提问明显增加,说明裁剪过度,节省的费用可能被额外调用抵消。
建议用固定的匿名测试集做 A/B 对比:原始历史、最近 N 轮、结构化摘要三种策略各运行多轮,比较回答是否保留关键事实。不要把单次响应更短直接当作质量提高。
上线前检查清单
- 明确输入、输出、缓存和摘要调用分别如何计费;
- 系统规则、当前任务和必要约束不会被误删;
- 采用 tokenizer 或 usage 校准 Token 估算;
- 为历史、摘要和单次请求设置独立上限;
- 摘要前脱敏,日志不保存完整对话和密钥;
- 用匿名固定测试集比较成本、延迟和回答完整性;
- 用户删除会话时同步处理原文、摘要、缓存和索引。
多轮对话优化的目标是让模型每一轮拿到“足够但不过量”的上下文。不同 API 中转站对 Token、缓存和上下文窗口的计算方式可能不同,实际结算和能力以服务商当前文档及请求明细为准。
延伸阅读:API 提示上下文超限怎么办 · 中转站缓存命中却没省钱