每次和大模型聊天,它为什么能"记住"你上一句说了什么?你真的理解背后的"无状态"原理吗?
一、引言:一次"失忆"实验
先来看一段简单的代码:
javascript
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: process.env.DEEPSEEK_BASE_URL,
});
const chatHistory = [
{ role: 'system', content: '你是一个严谨的助手' }
];
async function testStateless() {
// 第一次请求:告诉模型我的名字
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.push({ role: 'user', content: '请问我的名字是什么?' });
const response2 = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: chatHistory // 👈 还是把整个历史发过去
});
console.log(response2.choices[0].message.content);
// 输出:"你的名字是字节戴"
}
testStateless();
如果第二次请求我们只发 [{ role: 'user', content: '请问我的名字是什么?' }],不带上历史消息------模型会怎么回答?
它会一脸茫然。因为它根本"不认识"你。
这就是今天要聊的核心话题 ------ LLM 的无状态本质,以及从 Prompt Engineering 到 Context Engineering 再到 Loop Engineering 的三层进化。
二、LLM 调用本质:一次 HTTP 请求而已
抛开所有花哨的 UI 和封装,调用大模型 API 的本质是什么?
HTTP 请求 → 显卡计算 → 返回生成结果
仅此而已。你通过 HTTPS 发一段 JSON 过去,服务器上的 GPU 跑一遍推理,然后把生成的 token 序列还给你。
这意味着三件事:
- 每次请求都是独立的 ------ 服务器不记得你上一秒说过什么
- 服务器不存储客户端状态 ------ 不存在"会话"(Session)这个概念
- 可以水平扩展 ------ 你的两次请求可以落到不同机器上,结果无差异
这就是 Stateless(无状态)。
和 HTTP 协议本身一样:HTTP 是无状态协议,每一次 GET/POST 都是独立的。Web 应用通过 Cookie / Authorization Header 来"伪造"有状态,而大模型应用则通过每次手动带上全部对话历史来实现"记忆"。
sql
HTTP 无状态协议
├── GET / POST / PUT / DELETE ... 每次独立
└── + Header (Cookie? Authorization?) → "伪造"有状态
LLM 无状态调用
├── 每次 POST /v1/chat/completions 都是独立的
└── + 手动拼接完整 messages[] → "伪造"有记忆
核心洞察:大模型本身没有"记忆",是你每次把记忆掏出来给它看一遍。
三、"有状态"的诱惑与代价
有人会问:为什么不直接让服务端维护会话状态?像传统 Web 应用的 Session 那样?
答案是:压力太大了。
想象一下,如果服务端要为每个用户维护上下文:
| 维度 | 无状态(Stateless) | 有状态(Stateful) |
|---|---|---|
| 服务器压力 | 低,不存状态 | 高,每个会话占用内存/显存 |
| 水平扩展 | ✅ 任意机器处理任意请求 | ❌ 需要会话亲和性(sticky session) |
| 并发能力 | ✅ 天然支持高并发 | ❌ 状态同步是瓶颈 |
| 容错性 | ✅ 一台挂了另一台顶上 | ❌ 状态丢失不可恢复 |
| 公平性 | ✅ 所有请求一视同仁 | ❌ 老用户占用资源更多 |
大模型推理本身就是计算密集型 + 显存饥渴的任务。如果还要维护几百万用户的对话状态,KV Cache 会迅速撑爆显存。
所以,无状态不是选择,是必然。
四、对话历史的代价:Token 膨胀危机
既然要"手动带历史",那就必须面对一个现实问题:
messages 数组越来越大 → Token 开销线性增长 → 钱越烧越多
举个例子,假设你和模型聊了 100 轮:
erlang
第 1 轮:200 tokens
第 2 轮:400 tokens
第 3 轮:600 tokens
...
第 100 轮:20,000 tokens ← 你每一轮都在为历史买单
这还不算模型回复的 tokens。用 DeepSeek 的价格算,如果单次对话持续 100 轮,单是输入 token 的成本就能翻几十倍。
更致命的是上下文窗口限制。 即使现在主流模型支持 128K、1M token 的上下文,也总有装满的那一天。
LRU(最近最少使用)策略
一个自然的优化思路是 LRU:
javascript
// 伪代码
function pruneHistory(messages, maxTokens) {
let totalTokens = countTokens(messages);
while (totalTokens > maxTokens && messages.length > 2) {
// 保留 system prompt 和最近的对话
const removed = messages.splice(1, 2); // 移除最早的一对 user+assistant
totalTokens -= countTokens(removed);
}
return messages;
}
最近在聊的留下,久远的适当删除。 这个思路简单实用,但也丢了潜在的上下文------比如你在第 1 轮告诉了模型你的名字,第 50 轮问它时,它已经"忘"了。
这就是为什么实际工程中需要更精细的策略:摘要压缩、向量检索(RAG)、分层记忆。
五、三层进化:从 Prompt 到 Loop
聊完原理,我们升级视角,从工程哲学的层面看整个 AI 应用开发的演化路径。
5.1 第一层:Prompt Engineering(提示词工程)
这是大多数人入门的方式。
你 → 写 Prompt → 模型 → 回复
你在聊天框里反复调整措辞,希望能稳定产出高质量回答。你维护历史对话,你整理知识库文件(claude.md、agent.md),你精心设计 system prompt。
但本质上,Prompt Engineering 像是在抽卡:
好的 Prompt 设计只能提升你"抽到金卡"的概率,做不到 100% 可控。
同样的 Prompt,模型有时输出惊艳,有时胡言乱语。这是 LLM 的概率本质决定的。
5.2 第二层:Context Engineering(上下文工程)
当你发现"光靠 Prompt 不够"时,你开始进入上下文工程的领域。
核心思路:模型不懂的东西,是因为你还没喂给它。
| 手段 | 解决的问题 |
|---|---|
| RAG(检索增强生成) | "模型不知道" → 实时检索外部知识 |
| MCP(协议/工具调用) | "模型做不到" → 赋予它调接口的能力 |
| Skills / Agent | "模型不会规划" → 给它一套工作流 |
Context Engineering 不再纠结于"怎么问",而是关注**"给什么"**。
你不再期望模型记住一切------你构建一套系统,在需要时精准注入上下文。
5.3 第三层:Loop Engineering(循环工程)
这是最前沿的一层,也是 AI Agent 能真正"干活"的核心。
传统的人机交互:
用户输入 → 模型输出 → 结束
Loop Engineering 的模式:
markdown
用户输入 → 模型规划 → 执行动作 → 观察结果 → 反馈调整 → 再次执行 → ... → 达到目标
↑__________________________________________________↓
这就是 Claude Code、Cursor、Devin 这类产品背后的核心范式 ------ Harness(马具)思维。
你的角色从"写 Prompt 的人"变成了"套马的人":
- 你给 AI 设定目标和约束
- AI 自己在 Loop 中迭代
- 你只在关键节点介入
markdown
┌─────────────────────────────────────────┐
│ Loop Engineering (Harness) │
│ │
│ 用户 ──→ 定义目标 ──→ AI Loop ──→ 结果 │
│ │ │ │
│ │ ┌────────▼──────────┐ │
│ │ │ 观察 → 思考 → 执行 │ │
│ │ │ ↑___________↓ │ │
│ │ └───────────────────┘ │
│ │ │ │
│ └──── 人在环路外 ──────────┘ │
└─────────────────────────────────────────┘
六、ChatHistory 的工程化挑战
回到最接地气的部分。不管你用哪一层工程,ChatHistory(对话历史)的管理是绕不开的。结合前面的分析:
| 挑战 | 说明 | 应对 |
|---|---|---|
| 完整性 | 不维护完整历史,模型的理解就会断裂 | 持久化存储,结构化记录 |
| Token 膨胀 | 消息越来越多,成本线性增长 | LRU 淘汰 + 摘要压缩 + RAG 检索 |
| 任务未完成就被截断 | 聊着聊着上下文满了,任务还没做完 | 任务拆解 + 检查点保存 |
| 容量规划 | 什么时候该删、该压缩、该检索? | 基于 token 预算的主动管理 |
一个好的 ChatHistory 管理策略,几乎决定了一个 AI 应用能不能"长期跑下去"。
七、总结:三个核心认知
梳理完这些,我希望你带走三个核心认知:
认知一:LLM 就是无状态的
不用心存幻想。它每一次推理都从零开始。你感受到的"记忆",是你自己维护的。
认知二:无状态 = 高并发 + 高可用 + 水平扩展
这不是缺陷,这是架构选择。正是因为无状态,AI 服务才能支撑千万级用户同时使用。
认知三:工程能力分层演进
vbnet
Prompt Engineering → 会问问题
Context Engineering → 会给信息
Loop Engineering → 会造系统
你在哪一层,决定了你的 AI 应用有多大的想象空间。
八、附:完整 Demo 代码
最后附上完整的演示代码,它虽然短,但完美诠释了"无状态"的核心理念:
javascript
import OpenAI from 'openai';
import { config } from 'dotenv';
config();
const client = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: process.env.DEEPSEEK_BASE_URL,
});
const chatHistory = [
{ role: 'system', content: '你是一个严谨的助手' }
];
async function testStateless() {
console.log('=== 第一次请求:告诉模型我的名字 ===');
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
});
console.log('模型回复:', response.choices[0].message.content);
console.log('\n=== 第二次请求:问模型我是谁 ===');
chatHistory.push({
role: 'user',
content: '请问我的名字是什么?'
});
const response2 = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: chatHistory // 还是带全部历史
});
chatHistory.push({
role: 'assistant',
content: response2.choices[0].message.content
});
console.log('模型回复:', response2.choices[0].message.content);
console.log('\n=== 完整对话历史 ===');
console.log(JSON.stringify(chatHistory, null, 2));
}
testStateless().catch(err => console.error(err));
运行它,然后把第二次请求的 messages 改成只带当前问题------你会立刻"看见"什么叫无状态。
延伸思考:如果 LLM 是有状态的,Claude Code 在每次迭代时就不需要重新读取文件、不需要每次把上下文重传一遍。但也正因为它是无状态的,才有了 RAG、MCP、Agent 这一整个生态。无状态不是问题,无状态是土壤。