【自学AI】10 对话越长,成本越高——Token 优化与学习阶段复盘

3411 字
17 分钟
【自学AI】10 对话越长,成本越高——Token 优化与学习阶段复盘

开头:对话历史越长,账单越贵#

我们的系统现在会把完整的对话历史发给 AI。

第 1 轮对话:发 1 条消息。 第 10 轮对话:发 10 条消息。 第 100 轮对话:发 100 条消息。

你看出问题了吗?

每一轮对话,都要把之前所有的历史重新发一遍。 对话越长,每次调用的 Token 数就越多,成本就越高。

在面试系统里,一次完整的面试可能有 20-30 轮对话。 每道题的问答加起来,一次面试的 Token 消耗可能是单次调用的 10 倍以上。

这不现实。我们需要解决它。

功能做完,不是终点。 能跑起来,和能长期运行,是两件事。 Token 成本,是 AI 应用里最容易被忽视、也最容易失控的问题。

解决方案有两个,我们一个一个来看。


一、方案 1:只保留最近的消息#

最简单直接的办法:不管历史有多长,只发最近的 N 条给 AI。

还记得我们在 SessionManager 里写的 getRecentMessages() 方法吗?

getRecentMessages(sessionId: string, count: number = 10): Message[] {
const history = this.getHistory(sessionId);
if (history.length === 0) {
return [];
}
// System Message 一定要保留(它是第一条,定义了 AI 的角色)
const systemMessage = history[0];
// 获取最近 count 条消息
const recentMessages = history.slice(-count);
// 如果最近的消息中不包含 System Message,就手动加上
if (recentMessages[0]?.role !== 'system') {
return [systemMessage, ...recentMessages];
}
return recentMessages;
}

逻辑很清晰,三步:

完整历史(100 条)
取出 System Message(第 1 条,必须保留)
取出最近 10 条消息
拼在一起,发给 AI(最多 11 条)

为什么 System Message 一定要保留?

因为 System Message 是 AI 的”角色定义”——“你是一个资深的 Java 面试官”。 如果丢掉这条,AI 就不知道自己是谁,回答会变得奇怪。 不管历史多长,这条消息都必须在。

System Message 是 AI 的根。 其他消息可以裁剪,根不能丢。

这个方案的优缺点:

  • ✅ 实现简单,成本恒定(不管对话多长,每次最多发 11 条)
  • ❌ 会丢失旧的上下文(第 1-90 轮的内容就消失了)

如果你的场景对历史信息要求不高,这个方案就够了。 但如果你不想丢失信息,看方案 2。


二、方案 2:用 AI 总结长对话#

更聪明的办法:不丢弃旧消息,而是用 AI 把它们压缩成摘要。

100 轮对话的核心内容,其实可以用 2-3 句话概括。 我们让 AI 来做这件事:

/**
* 总结长对话
* 当消息数超过阈值时,自动把旧消息压缩成摘要
*/
async summarizeLongConversation(
sessionId: string,
minMessages: number = 30,
): Promise<void> {
const history = this.getHistory(sessionId);
// 消息数未达到阈值,不需要总结
if (history.length < minMessages) {
return;
}
this.logger.log(`开始总结长对话,消息数: ${history.length}`);
// 总结范围:第 2 条到倒数第 5 条(保留最后 5 条原始消息)
const conversationToSummarize = history.slice(1, -5);
const summaryPrompt = PromptTemplate.fromTemplate(`
请总结以下对话的要点。用 2-3 句话,尽量简洁,保留重要信息。
对话内容:
{conversation}
总结结果:
`);
const model = this.aiModelFactory.createDefaultModel();
const chain = summaryPrompt.pipe(model);
try {
const summary = await chain.invoke({
conversation: conversationToSummarize
.map((m) => `${m.role}: ${m.content}`)
.join('\n\n'),
});
// 用摘要替换旧消息,重组历史
const newHistory: Message[] = [
history[0], // System Message(保留)
{
role: 'system',
content: `【之前对话的总结】${summary.content}`,
},
...history.slice(-5), // 最后 5 条原始消息(保留)
];
const session = this.sessions.get(sessionId);
if (session) {
session.messages = newHistory;
this.logger.log(
`总结完成,消息数从 ${history.length} 减少到 ${newHistory.length}`
);
}
} catch (error) {
this.logger.error(`总结对话失败: ${error}`);
// 总结失败不影响对话继续,只记日志
}
}

总结之后,历史从 100 条变成了 7 条:

压缩前(100 条):
System Message
用户问题 1 / AI 回答 1
用户问题 2 / AI 回答 2
...(96 条)
用户问题 99 / AI 回答 99(最后 5 条)
压缩后(7 条):
System Message
【之前对话的总结】候选人主要考察了 Java 并发和 Spring Boot...
用户问题 96 / AI 回答 96
用户问题 97 / AI 回答 97
用户问题 98 / AI 回答 98(最后 5 条)

这个方案的优缺点:

  • ✅ 保留了重要的历史信息,上下文不会断
  • ✅ 成本大幅降低(100 条 → 7 条)
  • ❌ 多一次 AI 调用(用于生成摘要)
  • ❌ 摘要可能会丢失一些细节

这个方案的本质,是用”理解”代替”记忆”。 不是把所有历史都搬过来,而是提炼出核心,带着核心继续走。 这其实也是人类处理长期记忆的方式。


三、两个方案怎么选#

方案 1:只保留最近消息方案 2:AI 总结
实现复杂度
成本控制恒定(最优)很低(次优)
上下文保留只有最近 N 条保留摘要 + 最近 N 条
额外 AI 调用有(触发总结时)
适合场景短对话、上下文要求不高长对话、需要保留历史信息

我的建议:两个方案组合使用。

正常情况下用方案 1(只保留最近 10 条), 当消息数超过 30 条时触发方案 2(自动总结)。 这样既简单,又能在长对话时保留重要信息。

不是非此即彼,是根据场景组合。 工程上的很多问题,最优解都是”组合”,不是”选一个”。


四、整章知识体系串联#

Token 优化讲完了。

现在退一步,把这一章学到的所有东西串联起来。 你会发现,这些模块不是孤立的,它们共同构成了一套完整的 AI 集成系统。

完整的数据流是这样的:

用户发起对话
[Session Manager] 创建会话,写入 System Message
用户提问
[Token 优化] 检查消息数,决定是否触发总结
[Session Manager] getRecentMessages() 取出最近 N 条
[AI Service] 历史消息 + 新问题 → 组装输入
[LangChain] Prompt → Model → Parser
[AIModelFactory] 返回初始化好的模型实例
DeepSeek API 返回回答
[Session Manager] addMessage() 保存用户问题和 AI 回答
用户看到回答,继续提问
循环...

每个模块的职责:

模块职责关键方法
Session Manager管理会话生命周期和对话历史createSession() / addMessage() / getRecentMessages()
AI Model Factory集中管理模型初始化,统一出口createDefaultModel() / createStableModel()
LangChain串联 Prompt、Model、Parser,让数据自动流动prompt.pipe(model).pipe(parser)
Token 优化控制每次调用的消息数量,降低成本getRecentMessages() / summarizeLongConversation()

这四个模块,分别解决了四个问题:

  • Session Manager → AI 怎么记住上下文?
  • AI Model Factory → 怎么统一管理模型,方便替换?
  • LangChain → 怎么写出清晰、可维护的 AI 代码?
  • Token 优化 → 怎么在保证质量的前提下控制成本?

每个模块都只解决一个问题。 这不是巧合,是设计原则——单一职责。 职责越单一,模块越稳定,改动越安全。


五、四个设计模式#

这一章用到了四个设计模式。 理解这些模式,你就能把这章的思想迁移到任何新项目里。

工厂模式(AIModelFactory)

把模型的创建逻辑集中在一个地方。 以后要换模型(从 DeepSeek 换成 Claude),只改工厂里的一行代码, 其他所有服务不用动。

工厂模式的本质:把”怎么创建”和”怎么使用”分开。 使用者不需要知道对象怎么来的,只需要知道怎么用。

会话模式(Session Manager)

用一个独立的服务管理所有用户的对话状态。 每个用户有自己独立的历史,互不干扰,支持并发。

状态管理集中化,是并发安全的基础。 状态散落在各处,是 bug 的温床。

Pipe 模式(LangChain Chain)

pipe 把多个组件串联,数据自动从一个流向下一个。 代码读起来像一条流水线,逻辑一目了然。

Pipe 模式的本质:把”数据怎么传递”从业务逻辑里剥离出来。 你只需要定义每一步做什么,不需要关心数据怎么流动。

分层架构(整体设计)

Controller 只管接收请求,Service 只管业务逻辑,AI Service 只管 AI 调用。 各层职责明确,改一层不影响其他层。

分层的价值,在于把”变化”限制在一层里。 需求变了,只改对应的层,其他层不受影响。 这是软件工程里对抗复杂度最有效的武器。


六、学完这章,你能做什么#

1. 构建完整的 AI 对话系统

从接收用户输入,到创建会话、调用 AI、保存历史、支持多轮追问—— 完整的链路你都会了。 客服机器人、智能助手、面试系统,核心原理都一样。

2. 快速替换 AI 模型

想从 DeepSeek 换成 OpenAI? 改 AIModelFactory 里的一行初始化代码,其他全部不动。 这是分层架构带来的灵活性。

3. 控制 API 成本

知道怎么用 getRecentMessages() 限制每次调用的 Token 数, 知道什么时候触发 AI 总结。 上线之后,成本在你的掌控之中,不会等到收到账单才发现超支。

4. 写可测试的代码

所有的 AI 调用都经过 AIModelFactory, 测试时只需要 mock 这个工厂,不会产生真实的 API 调用。 单元测试跑得快,也不花钱。

5. 监控和调试

所有调用都经过 AI Service,统一加日志:

this.logger.log(`调用 AI,会话: ${sessionId},消息数: ${messages.length}`);
this.logger.log(`AI 回答完成,输入 Token: ${inputTokens},输出 Token: ${outputTokens}`);

有了这些日志,成本分析、性能瓶颈、错误排查,都有据可查。


七、常见问题 Q&A#

Q1:invoke 和 stream 怎么选?

invokestream
返回时机等 AI 回答完才返回一边生成一边返回
适合场景结果短、需要完整数据再处理结果长、想让用户实时看到输出
用户体验等待后一次性出现像打字机一样逐字出现

面试系统的评估打分用 invoke(需要完整 JSON 才能解析), 对话回答用 stream(让用户实时看到 AI 在打字)。

选 invoke 还是 stream,本质是在问: 用户需要等结果,还是需要看过程? 评估需要结果,对话需要过程。

Q2:对话历史应该保存多久?

内存中:只保留活跃会话(1 小时无活动自动过期)
数据库:持久化保存,保留 30 天
用户可以主动删除自己的历史

不要把所有历史都堆在内存里,内存是有限的。 活跃会话放内存,历史归档放数据库。

内存是工作台,数据库是档案室。 工作台只放当前要用的,不用的归档。

Q3:AI 调用失败了怎么办?

加指数退避重试:

async callAIWithRetry(messages: Message[], maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await this.aiService.call(messages);
} catch (error) {
if (i === maxRetries - 1) throw error;
// 第 1 次失败等 1 秒,第 2 次等 2 秒,第 3 次等 4 秒
await sleep(1000 * Math.pow(2, i));
}
}
}

重试 3 次还是失败,才真正抛出错误。 大部分的 API 超时,重试一次就能解决。

Q4:Token 成本怎么估算?

// DeepSeek 价格(以官网为准,可能会变)
const INPUT_PRICE = 0.00000014; // 每个输入 token
const OUTPUT_PRICE = 0.00000028; // 每个输出 token
const cost = inputTokens * INPUT_PRICE + outputTokens * OUTPUT_PRICE;

建议每次调用都记录 Token 数,每周统计一次成本,发现异常及时处理。

不记录就不知道,不知道就失控。 成本监控,是 AI 应用上线后最重要的运维工作之一。

Q5:用户数据怎么保护?

  • API Key 放环境变量,绝对不要硬编码
  • 对话历史如果要持久化,敏感内容要加密
  • 对用户输入做基本验证,防止注入攻击
  • 不要把用户的对话内容发给第三方

数据安全不是上线后再考虑的事。 从第一行代码开始,就要有这个意识。


结尾:好的 AI 系统,首先是好的架构#

让我把这一章的核心内容做最后一次总结:

章节学到了什么
AI 集成策略为什么要分层,直接 fetch 的 6 个代价
LangChain 入门四个核心组件,用 pipe 串联
LangChain 实战AIModelFactory,写出干净可维护的代码
多轮对话管理Session Manager,让 AI 记住上下文
Token 优化两个方案,在质量和成本之间找平衡

这一章的代码不复杂,但思想很重要

分层、工厂、会话、Pipe——这些设计模式不是 AI 专属的, 它们在任何系统里都适用。 你今天学的这套思路,不管以后用什么框架、换什么模型,都能直接迁移过去。

技术会过时,思想不会。 DeepSeek 可能被更好的模型替代,LangChain 可能被新框架取代—— 但分层、单一职责、关注点分离,这些原则不会变。 学会了思想,工具换了也不慌。

下一章,我们会学 RAG(检索增强生成)。

现在的系统,AI 只能基于对话历史回答问题。 加上 RAG 之后,用户可以上传一份内部文档,AI 读取这个文档来回答问题—— AI 不仅能记住对话,还能获取外部知识。

这是让 AI 真正”懂你的业务”的关键一步。


昇哥 · 2026年3月 全栈开发 × AI 学习途中,把踩过的坑写下来

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【自学AI】10 对话越长,成本越高——Token 优化与学习阶段复盘
https://blog.fridolph.top/posts/2026-02-18__ai-token/
作者
Fridolph
发布于
2026-02-18
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录