为什么每次跟 ChatGPT 聊天,它都能"记住"之前说过的话?是服务器存了我们的聊天记录吗?
从一个实验开始
先来看一段代码,我们让 DeepSeek 记住我们的名字,然后再问它:
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: process.env.DEEPSEEK_API_BASE_URL,
});
const chatHistory = [
{ role: 'system', content: '你是一个严谨的助手' },
];
async function chat() {
// 第一次请求:告诉模型我的名字
chatHistory.push({ role: 'user', content: '请记住我叫字节谢' });
const res1 = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: chatHistory,
});
chatHistory.push({ role: 'assistant', content: res1.choices[0].message.content });
// 第二次请求:问模型我叫什么
chatHistory.push({ role: 'user', content: '请问我的名字是什么?' });
const res2 = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: chatHistory,
});
chatHistory.push({ role: 'assistant', content: res2.choices[0].message.content });
console.log(res2.choices[0].message.content);
}
chat();
运行结果:
你的名字叫"字节谢"。
看起来模型"记住"了,对吧?但仔细看代码 —— 我们每次调用 API 都把完整的 chatHistory 传了过去。
这就是 LLM 的无状态本质。
一、什么是"无状态"?
1.1 先理解 HTTP 的无状态
LLM API 本质上是 HTTP 调用。而 HTTP 协议天然就是无状态协议。
知识点:HTTP 的无状态性
HTTP 协议中,每次请求都是完全独立的。服务器处理完一个请求后,就"忘记"了刚才的一切。GET/POST/PUT/DELETE 这些方法,每一次调用都和上一次没有任何关系。
那登录状态怎么来的?靠 Header 里的
Cookie或Authorization令牌。每次请求客户端都主动带上身份凭证,服务器不存你是谁,只校验凭证是否有效。
用一个比喻来理解:
| 模式 | 比喻 | 特点 |
|---|---|---|
| 有状态 | 你去银行柜台,柜员认识你,知道你存了多少钱 | 服务器要记住你是谁,压力大 |
| 无状态 | 你去自助取款机,每次都要插卡输密码 | 服务器不记你是谁,每次自己证明身份 |
1.2 LLM 的无状态同样如此
当你调用 chat.completions.create() 时,DeepSeek/OpenAI 的服务器并不会替你存聊天记录。它只看你这次传过来的 messages 数组里有什么,然后就生成回复。请求结束,关于你的一切它就"忘"了。
核心认知:LLM 的"记忆力",本质上是你每次手动把全部对话历史打包发过去。
二、为什么 LLM 要设计成无状态?
三个字:高并发。
想象一下,如果有 1000 万用户同时聊,每个用户服务器都要维护一份"对话状态",需要多少内存?而且用户的会话可能持续几分钟到几小时,这段时间状态都要占着资源。
无状态的设计带来三个关键优势:
高并发 → 每个请求独立,服务器可以同时处理海量请求
高可用 → 任何一台服务器挂了,请求可以路由到另一台,因为不依赖特定机器
高扩展 → 流量上来了直接加服务器,水平扩展毫无压力(所有服务器对等)
知识点:水平扩展(Horizontal Scaling)
水平扩展 = 加更多机器,而不是给一台机器升配置。因为每个请求都是独立的、不依赖特定服务器,所以请求可以被负载均衡器分发到集群中任何一台机器上。这在有状态架构下很难做到 —— 你必须保证同一个用户的请求每次都打到同一台机器(这叫"会话保持",是分布式系统的一大痛点)。
三、chatHistory 的困境
既然每次要带全部历史,那就带来了一个直接问题:对话越长,messages 越大,token 开销越大。
来看看一次对话中 messages 的增长:
第1轮: [system, user1] → 2 条消息
第2轮: [system, user1, assistant1, user2] → 4 条消息
第3轮: [system, user1, assistant1, user2, assistant2, user3] → 6 条消息
...
第50轮: 101 条消息,可能几万 token
每次请求都要把整个历史重新发送一遍,其中 90% 的内容模型已经"看过"了,完全是重复传输和重复计算。
3.1 LRU 缓存策略
为了解决这个问题,一个朴素的做法是 LRU(Least Recently Used,最近最少使用)缓存。
知识点:LRU 缓存
LRU 的核心思想:当容量满了,淘汰最久没被使用的那个。就像一个只能放 5 本书的书架,你每次拿一本看,看完放回去;当要放第 6 本时,把最久没碰的那本扔掉。
在 LLM 对话场景里:只保留最近的 N 轮对话,把久远的历史"遗忘"掉。这样 tokens 开销可控,而且最近的上下文通常也是最重要的。
但 LRU 也有问题:如果对话还没结束、任务还没完成,你就把早期关键信息丢了呢?
四、从 Prompt 工程到 Loop 工程
作者笔记里列出了一个很有意思的演进路线:
Prompt Engineering → Context Engineering → Loop Engineering
(提示词工程) (上下文工程) (循环工程)
4.1 Prompt Engineering — 抽卡时代
写一段精巧的 prompt,期待模型给出好结果。但就像抽卡游戏一样 —— prompt 设计好,只是提升了"抽到金卡"的概率,并不是 100% 可控。
4.2 Context Engineering — 给模型装上"外挂"
模型不懂的、模型没有的知识,我们通过上下文喂给它:
- RAG(检索增强生成):从知识库里检索相关文档,拼到 prompt 里
- MCP(Model Context Protocol):标准化的工具/数据连接协议
- Skill 系统:预定义的领域能力模块
- claude.md / agent.md:项目级别的系统指令
本质上还是在无状态的基础上,每次精心构造 messages 数组。
4.3 Loop Engineering — 让 AI 自己驱动自己
Harness 循环工程:AI 不只是"一问一答",而是在一个循环中持续运行 —— 观察 → 思考 → 行动 → 观察 → ... 直到任务完成。这已经超越了单次无状态调用的范畴。
五、总结
回看最初那段代码,核心逻辑只有一句:
chatHistory.push({ role: 'user', content: '请问我的名字是什么?' });
const response = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: chatHistory, // ← 关键:每次带上全部历史
});
LLM 的"无状态"总结就是三句话:
- 每次请求都是独立的,服务器不存储任何会话状态
- "记住"是假象,真相是客户端每次手动把全部对话历史带上
- 这是刻意的设计,换来的是高并发、高可用、水平扩展的能力
理解了这一点,你就能明白为什么 AI 应用的架构设计里,上下文管理(怎么选历史、怎么压缩、怎么检索)才是真正的核心难题,而不是"怎么让模型记住更多"。
延伸思考:既然每次都要传全部历史,那 OpenAI 的"记忆"功能(ChatGPT 能跨会话记住你的偏好)又是怎么实现的?答案是 —— 它并不是打破了无状态,而是服务端在收到请求时,从数据库里取出你的"记忆摘要",注入到 system prompt 里,然后再走正常的无状态推理流程。本质还是"手动带上"。
本文基于对 LLM API 调用机制的学习和实践整理,希望能帮你建立对 AI 应用架构的底层认知。