
刚开始写 LLM 接口时,我一直有一个很自然的误解:
既然我是在和 AI 连续聊天,那模型应该记得前面说过什么。
比如我先告诉它:
text
请记住我叫小林。
下一轮再问:
text
请问我的名字是什么?
如果它能回答出来,我们很容易以为"模型记住了我"。
但从接口调用的角度看,事情并不是这样。
大多数 LLM API 本身是无状态的。模型不是一个一直坐在那里等你的聊天对象,而更像一个每次被临时叫醒的计算服务。你把本次请求需要的信息发过去,它根据这次输入生成结果,然后这次请求结束。
下一次再请求,如果你不把前面的内容带上,它就不知道之前发生了什么。
什么是无状态?
无状态,英文叫 stateless。
它的意思是:每一次请求都是独立的,服务端不会默认依赖上一轮请求。
这和 HTTP 协议很像。
你打开一个网页、提交一个表单、调用一个接口,本质上都是一次次独立的 HTTP 请求。HTTP 本身并不会天然记住"这个用户刚才做了什么"。如果服务器想识别你是谁,通常要靠 cookie、token、session、Authorization header 这些机制。
LLM API 也类似。
模型收到一次请求,只能看到这次请求里给它的内容。它并不会自动知道你上一轮说了什么,也不会自动保存所有聊天历史。
所以所谓"连续对话",并不是模型自己保存了记忆,而是应用层每次都把必要的历史对话重新发给模型。
为什么 LLM 要设计成无状态?
直觉上看,有状态好像更方便:用户说过什么,服务端都记住,下次直接接着聊。
但在真实工程里,这会带来很大压力。
LLM 服务面对的是大量用户请求。如果服务端要长期保存每个用户的完整对话状态,就会出现几个问题:
- 服务器压力变大
- 扩容更困难
- 请求很难分发到任意机器
- 故障恢复更复杂
- 隐私和数据管理成本更高
无状态的好处是,每一次请求都可以被任意一台服务器处理。
只要请求本身带齐了必要信息,后端就不需要关心"之前是哪台机器处理了这个用户"。这对高并发、高可用、水平扩展都很重要。
所以无状态不是 LLM 的缺陷,而是一种很常见的后端设计。
真正的问题是:既然模型不记得,我们就要自己决定,这次请求里应该带什么。
messages 才是模型看到的上下文
在 Chat Completions 这类接口里,我们经常会传一个 messages 数组:
js
const messages = [
{ role: "system", content: "你是一个严谨的助手" },
{ role: "user", content: "请记住我叫小林" },
{ role: "assistant", content: "好的,我记住了,你叫小林。" },
{ role: "user", content: "请问我的名字是什么?" }
];
这里最关键的一点是:
模型不是从某个隐藏记忆里知道"我叫小林",而是因为这句话仍然在 messages 里面。
也就是说,messages 就是模型这一次能看到的上下文。
如果第二次请求只发这一句:
js
[
{ role: "user", content: "请问我的名字是什么?" }
]
模型就没有可靠依据知道你是谁。
它可能会猜,也可能会说不知道。无论哪种,本质上都不是"忘了",而是你这次请求没有给它上下文。
一个最小的 chatHistory 例子
下面这个最小 demo,就是用 chatHistory 来模拟连续对话:
js
const chatHistory = [
{ role: 'system', content: '你是一个严谨的助手' }
];
chatHistory.push({
role: 'user',
content: '请记住我叫小林'
});
const response = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: chatHistory
});
chatHistory.push({
role: 'assistant',
content: response.choices[0].message.content
});
第一轮请求结束后,代码会把模型的回复也放回 chatHistory。
然后第二轮再问:
js
chatHistory.push({
role: 'user',
content: '请问我的名字是什么?'
});
const response2 = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: chatHistory
});
看起来像模型"记住了",其实是程序把前面的对话保存到了数组里,并且下一次请求时又整体传了过去。
这个细节非常重要。
很多人刚开始写聊天应用时,只保存用户消息,不保存模型回复。这样上下文就会断掉一半。因为模型下一轮不仅需要知道用户说过什么,也需要知道自己上一轮回答过什么。
对话历史应该是一条完整链路:
text
system 规则
user 第一次输入
assistant 第一次回复
user 第二次输入
assistant 第二次回复
...
这条链越完整,模型越能理解当前对话发展到哪里。
chatHistory 不能无限变长
既然每次都带上历史,那能不能把所有对话永久塞进 messages?
理论上简单,工程上不现实。
因为 messages 越长,token 开销越大。
token 变多以后,会带来几个直接问题:
- 请求成本变高
- 响应速度变慢
- 更容易超过上下文窗口
- 旧信息干扰新任务
- 模型注意力被无关内容分散
所以聊天系统不能只会"追加历史",还要学会"管理历史"。
这就从简单调用 API,进入了上下文工程。
LRU 思路:最近的不一定最重要,但通常更有用
LRU 这个概念,可以帮助理解聊天历史裁剪。
LRU 是 Least Recently Used,意思是最近最少使用的内容优先淘汰。
放到聊天上下文里,可以粗略理解成:
最近聊到的内容更可能马上继续用,太久之前、又不重要的内容,可以优先压缩或移除。
比如用户一直在调试一个报错,最近 10 轮里出现的错误信息、代码片段、修改记录,通常比很早之前的一句闲聊更重要。
但这里也不能机械地按时间删除。
有些信息虽然很早,但非常关键:
text
这个项目用 Vue 3。
接口统一走 /api。
不要改动用户已有代码。
所有回答用中文。
这些信息不能因为"旧"就删掉。
所以真实的上下文管理,通常要结合几种策略:
- 最近对话直接保留
- 重要事实长期保存
- 长对话定期总结
- 项目资料按需检索
- 固定规则放进 system prompt 或项目规则文件
无状态把责任交给了应用层
理解无状态以后,再看很多 AI 应用,就会清楚很多。
ChatGPT、Cursor、Claude Code、Codex 这类工具之所以看起来很懂当前任务,并不是因为底层模型天然记得一切,而是它们在模型外面做了大量上下文管理。
比如:
- 当前文件内容
- 相关代码片段
- 终端输出
- 用户最近的指令
- 项目规则文件
- 工具执行结果
- RAG 检索到的资料
- MCP 提供的外部上下文
这些东西都会被选择性地组织进模型当前这次请求里。
所以 AI 工程里有一句很核心的话:
模型负责生成,应用负责给上下文。
模型越强,确实越能推理;但如果上下文给错了、给少了、给乱了,它还是会跑偏。
从 Prompt 到 Context,再到 Harness
早期大家很关注 Prompt Engineering,也就是怎么把一句提示词写好。
但当你开始做真实 AI 应用时,会发现一句 prompt 远远不够。
因为模型本身有几个结构性限制:
- 无状态:不会自动记住历史
- 有上下文窗口:不能无限塞内容
- 概率生成:同样输入也可能有不同输出
- 不能直接操作外部世界:需要工具和 runtime
所以后面会出现 Context Engineering。
它关心的不是"怎么问一句话",而是"这次请求应该带哪些背景、规则、资料、历史和工具结果"。
再往后是 Harness Engineering。
也就是在模型外面搭一套更完整的工程系统:记忆、工具、循环、校验、权限、规则、上下文压缩,让模型的能力变得更稳定、更可控。
无状态就是这条线的起点。
因为只要模型不自动记得,开发者就必须学会设计记忆。
总结
LLM API 通常是无状态的。
它不会天然记得上一轮对话。我们看到的"连续聊天",本质上是应用层每次都把必要历史放进 messages,让模型在本次请求里重新看到上下文。
所以写 AI 应用时,不能只会调用接口,还要想清楚:
- 哪些历史要带上
- 哪些内容可以删掉
- 哪些信息应该总结
- 哪些资料应该检索
- 哪些规则应该固定保存
无状态让服务更容易扩展,也把上下文管理这件事交给了开发者。
真正的 AI 工程能力,往往就藏在这些取舍里。