LLM 无状态本质与上下文工程:从 Prompt 到 Context 的进化——为什么 AI 总是"失忆"?
本文深入解析 LLM 的无状态(Stateless)本质,揭示为什么每次调用 API 都是"全新的开始",并系统讲解从 Prompt Engineering 到 Context Engineering 再到 Loop Engineering 的进化路径,助你构建真正"有记忆"的 AI 应用。
前言
你有没有遇到过这样的困惑:
你告诉 AI "我叫张三",下一秒问 "我是谁",它却回答 "我不知道你的名字"。
这不是 AI 笨,而是 LLM 的本质是无状态的。每次调用 API,对服务器来说都是一次全新的、独立的请求——它不记得你们上一轮聊过什么。
本文将带你理解这一底层机制,并掌握让 AI"有记忆"的核心技术:上下文工程。
一、LLM 的无状态本质
1.1 调用 LLM 接口的本质是什么?
你的程序 ──HTTP POST──→ LLM 服务器
│ │
│ { │
│ model: "gpt-4", │
│ messages: [...] │ ← 每次请求都是独立的
│ } │
│ │
└────── JSON 响应 ───────┘
本质:HTTP 调用 + 算力消耗 + 生成结果。
LLM 服务器面对的不是"一个用户的长对话",而是无数个独立的 HTTP 请求。为了支撑高并发、高可用,后端必须设计为 无状态(Stateless)。
1.2 什么是无状态(Stateless)?
无状态意味着:
- 每次请求都是独立的,不依赖于之前的请求
- 服务器不存储客户端的任何状态
- 服务器可以水平扩展,任何一台机器处理任何请求都没差别
有状态服务: 无状态服务:
┌─────────┐ ┌─────────┐
│ 用户A │──→ 服务器1 │ 用户A │──→ 负载均衡 ──→ 任意服务器
│ (状态) │ (记住用户A) │ │
└─────────┘ └─────────┘
↑
┌─────────┐ │
│ 用户B │──→ 服务器2 │ 用户B │──→ 负载均衡 ──→ 任意服务器
│ (状态) │ (记住用户B) │ │
└─────────┘ └─────────┘
问题:用户A下次请求到了服务器2,服务器2不认识他
优势:任何请求到任何服务器,结果都一样
💡 为什么 LLM 必须无状态? 如果服务器要"记住"每个用户的对话历史,面对数百万并发用户,内存会瞬间爆炸。无状态设计让服务器可以无限水平扩展。
1.3 HTTP 本身就是无状态协议
HTTP 请求1:GET /api/user
← 服务器返回用户信息
HTTP 请求2:GET /api/orders
← 服务器不知道你是谁!
HTTP 协议天生不记住"你是谁"。那网站是怎么记住登录状态的?
答案:通过 Header 中的 Cookie / Authorization / Session ID。
HTTP 请求1:POST /login
→ 服务器返回 Set-Cookie: session_id=abc123
HTTP 请求2:GET /api/orders
→ Cookie: session_id=abc123 ← 客户端主动带上身份
→ 服务器查数据库:session_id=abc123 → 用户ID=42
但 LLM API 不同:
LLM API 请求:
Authorization: Bearer sk-xxx ← 只认证身份,不恢复对话
Body: { messages: [...] } ← 对话历史由客户端维护
⚠️ 关键区别:普通网站的 Cookie 用于"恢复会话状态",而 LLM API 的 Authorization 只用于"身份认证"。对话历史必须由客户端在每次请求时手动带上。
二、实战演示:AI 的"失忆"与"记忆"
2.1 演示一:无状态——AI 不记得你
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,
});
async function testStateless() {
// 第一次请求:告诉 AI 我的名字
console.log('第一次请求:告诉模型我叫零零发');
const response1 = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: [
{ role: 'system', content: '你是一个严谨的助手' },
{ role: 'user', content: '请记住,我的名字叫零零发' }
]
});
console.log('模型回复:', response1.choices[0].message.content);
// 输出:"好的,我记住了,您的名字是零零发。"
// 第二次请求:直接问 AI 我叫什么
console.log('\n第二次请求:直接问我是谁');
const response2 = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: [
{ role: 'user', content: '请问我的名字是什么?' }
]
});
console.log('模型回复:', response2.choices[0].message.content);
// 输出:"抱歉,我不知道您的名字。" ← 失忆了!
}
testStateless();
为什么失忆了?
请求1 和 请求2 是两个完全独立的 HTTP 请求:
请求1:
messages: [
{ role: 'user', content: '请记住,我的名字叫零零发' }
]
请求2:
messages: [
{ role: 'user', content: '请问我的名字是什么?' } ← 没有上一轮的信息!
]
2.2 演示二:有记忆——手动带上对话历史
// 对话历史数组
const chatHistory = [
{ role: 'system', content: '你是一个严谨的助手' }
];
async function testWithHistory() {
// 第一次请求
console.log('第一次请求:告诉模型我叫字节');
chatHistory.push({
role: 'user',
content: '请记住我叫字节'
});
const response1 = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: chatHistory // 带上完整历史
});
// 将模型回复也加入历史
chatHistory.push({
role: 'assistant',
content: response1.choices[0].message.content
});
console.log('模型回复:', response1.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));
}
为什么记得了?
请求2 的 messages 包含了请求1 的全部内容:
messages: [
{ role: 'system', content: '你是一个严谨的助手' },
{ role: 'user', content: '请记住我叫字节' }, ← 第一轮用户
{ role: 'assistant', content: '好的,我记住了...' }, ← 第一轮模型
{ role: 'user', content: '请问我的名字是什么?' } ← 第二轮用户
]
LLM 看到完整的上下文,自然能回答出"字节"。
三、从 Prompt Engineering 到 Context Engineering
3.1 三代工程化方法的演进
第一代:Prompt Engineering(提示词工程)
├── 精心打磨一条 Prompt
├── 抽卡式:质量不可控
└── "prompt 质量只能提升抽到金卡的概率"
第二代:Context Engineering(上下文工程)
├── 维护对话历史(chatHistory)
├── 注入知识库(RAG)
├── 配置文件(claude.md / agent.md)
└── 让 LLM "懂" 我们
第三代:Loop Engineering(循环工程)
├── 生成 → 校验 → 迭代
├── 自动化工作流
└── Harness AI 工程
3.2 Prompt Engineering 的局限
用户:写一个小红书文案
Prompt 1(普通):"写一个小红书美妆文案"
→ 结果一般
Prompt 2(优化):"你是一位资深小红书美妆博主,
写一篇关于口红的文案,
标题带数字,正文<300字,
结尾有行动号召"
→ 结果好一些
Prompt 3(极致):"你是一位拥有100万粉丝的..."
→ 结果更好一些,但...
问题:
- 每次都要重新写 Prompt
- 质量仍然不可控(抽卡)
- 没有记忆,每次从零开始
💡 Prompt Engineering 的天花板:Prompt 质量只能提升"抽到金卡"的概率,不是特别可控。
3.3 Context Engineering:让 LLM "懂" 我们
Context Engineering 的核心:不是让 LLM 记住,而是每次请求时把该记得的都带上。
Context 的来源:
├── 对话历史(chatHistory)
│ └── 用户和模型的完整对话记录
├── 知识库(RAG)
│ └── 检索到的相关文档片段
├── 配置文件
│ ├── claude.md(Claude Code 的配置)
│ └── agent.md(Agent 的人设和规则)
├── 实时信息
│ └── 当前时间、用户信息、环境状态
└── 工具结果
└── 上一次工具调用的返回数据
Context 的组装:
const messages = [
// 1. System 提示:全局人设和规则
{ role: 'system', content: '你是小红书美妆专家...' },
// 2. 知识库注入:RAG 检索结果
{ role: 'system', content: '相关知识:口红成分...' },
// 3. 对话历史:用户和模型的交互
{ role: 'user', content: '我想买一支口红' },
{ role: 'assistant', content: '推荐您试试...' },
// 4. 当前用户输入
{ role: 'user', content: '有没有更便宜的?' }
];
3.4 Loop Engineering:自动化迭代
Context Engineering + 自动化循环 = Loop Engineering
设定目标 + 规则
│
▼
AI 生成(基于 Context)
│
▼
自动校验(基于规则)
│
▼
不达标 → 更新 Context → 再生成
│
▼
达标 → 输出结果
🎯 三代进化:Prompt(单点优化)→ Context(系统记忆)→ Loop(自动迭代)
四、chatHistory 的隐患与优化
4.1 chatHistory 的问题
const chatHistory = [
{ role: 'system', content: '...' }, // 50 tokens
{ role: 'user', content: '你好' }, // 10 tokens
{ role: 'assistant', content: '...' }, // 200 tokens
{ role: 'user', content: '...' }, // 50 tokens
{ role: 'assistant', content: '...' }, // 200 tokens
// ... 100 轮对话后 ...
];
// 总 tokens:可能超过 10000+
// 费用:按 token 计费,越来越贵
// 性能:上下文太长,模型处理变慢
三大问题:
| 问题 | 影响 | 解决方案 |
|---|---|---|
| Token 开销大 | 费用增加 | 精简历史、摘要总结 |
| 上下文窗口限制 | 超出模型最大长度 | 滑动窗口、分段处理 |
| 信息噪声 | 久远信息干扰当前判断 | LRU 淘汰、重要性筛选 |
4.2 LRU 淘汰策略
LRU(Least Recently Used):最近最少使用
对话历史:
[系统提示, 用户1, 助手1, 用户2, 助手2, ..., 用户100, 助手100]
↑ ↑
最早(可删除) 最近(保留)
策略:
- 保留最近的 N 轮对话
- 删除久远的历史
- 系统提示和关键信息始终保留
// 简单的 LRU 实现
function trimHistory(history, maxRounds = 10) {
// 保留 system 提示
const systemMessages = history.filter(m => m.role === 'system');
// 保留最近 maxRounds 轮对话
const recentMessages = history
.filter(m => m.role !== 'system')
.slice(-maxRounds * 2); // 每轮包含 user + assistant
return [...systemMessages, ...recentMessages];
}
4.3 摘要总结策略
原始历史(100轮):
用户:你好
助手:你好,有什么可以帮你的?
用户:我想买口红
助手:推荐您试试...
...(97轮)...
用户:有没有更便宜的?
摘要后(3条):
[系统提示]
[摘要:用户想买口红,预算有限,已推荐3款]
[用户:有没有更便宜的?]
async function summarizeHistory(history) {
const response = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: [
{
role: 'user',
content: `请总结以下对话的核心信息,100字以内:
${JSON.stringify(history)}`
}
]
});
return response.choices[0].message.content;
}
五、知识图谱
LLM 无状态与上下文工程
├── LLM 无状态本质
│ ├── HTTP 调用 + 算力生成
│ ├── 无状态设计原因(高并发/水平扩展)
│ ├── HTTP 无状态协议
│ └── Cookie/Authorization 的区别
├── 实战演示
│ ├── 无状态:AI 失忆(两次独立请求)
│ └── 有记忆:chatHistory 维护上下文
├── 三代工程化进化
│ ├── Prompt Engineering(提示词工程)
│ │ └── 抽卡式,质量不可控
│ ├── Context Engineering(上下文工程)
│ │ ├── chatHistory 对话历史
│ │ ├── RAG 知识库
│ │ ├── claude.md / agent.md 配置
│ │ └── 实时信息注入
│ └── Loop Engineering(循环工程)
│ └── 生成 → 校验 → 自动迭代
├── chatHistory 优化
│ ├── Token 开销问题
│ ├── LRU 淘汰策略
│ └── 摘要总结策略
└── 核心洞察
└── "LLM 不记忆,我们帮它记住"
六、总结
本文深入解析了 LLM 的无状态本质与上下文工程:
- LLM 是无状态的:每次 API 调用都是独立的 HTTP 请求,服务器不存储任何对话状态。
- HTTP 协议本身无状态:网站通过 Cookie/Session 维持状态,但 LLM API 的 Authorization 只用于认证,不恢复对话。
- 让 LLM"记得"的唯一方法:每次请求时手动带上完整的
messages数组(对话历史)。 - 从 Prompt 到 Context 到 Loop:Prompt Engineering(单点优化)→ Context Engineering(系统记忆)→ Loop Engineering(自动迭代)。
- chatHistory 的隐患:Token 开销大、上下文窗口限制、信息噪声。需要通过 LRU 淘汰、摘要总结等策略优化。
- 核心洞察:LLM 不记忆,我们帮它记住。Context Engineering 的本质是"在每次请求时,把该记得的都带上"。
🚀 学习建议:理解无状态本质后,在设计 AI 应用时,始终问自己:"这次请求带上了足够的上下文吗?" 同时注意控制 Token 开销,避免历史无限膨胀。
参考资源
📌 标签:#LLM #无状态 #Stateless #ContextEngineering #PromptEngineering #chatHistory #Token优化 #AI架构
💬 互动:你在维护对话历史时遇到过哪些坑?Token 开销如何控制?欢迎在评论区分享!