理解 LLM 的无状态架构:从原理到实践

发布时间:2026/6/23 1:10:52
理解 LLM 的无状态架构:从原理到实践 TL;DR— LLM API 本质是无状态 HTTP 调用。每次请求都是独立的模型不记得你上一轮说了什么。记忆是我们在客户端手动拼接chatHistory伪造出来的。本文从架构原理出发结合可运行 Demo层层递进地讲清楚这件事。 一、调用 LLM 接口的本质是什么当我们写下一行client.chat.completions.create(...)时底层到底发生了什么┌──────────┐ HTTP POST ┌──────────────┐ GPU 推理 ┌──────────┐ │ 客户端 │ ────────────── │ LLM API 服务 │ ──────────── │ 模型算力 │ │ (你的代码) │ ────────────── │ (DeepSeek / │ ──────────── │ (GPU 集群) │ └──────────┘ JSON Response │ OpenAI 等) │ └──────────┘ └──────────────┘本质就是三次握手后的 HTTP 调用 算力生成。把一堆文本messagesPOST 过去服务器跑一遍 GPU 推理然后把生成的文本返回给你。和调用一个 RESTful API 没有本质区别。这引出了一个关键架构命题为了支持高并发、高可用后端必须是 Stateless无状态的。 二、什么是无状态2.1 HTTP 天然就是无状态的HTTP 协议本身就是一个无状态协议。每一次 GET / POST 请求都是独立的服务器处理完就忘掉。GET /api/user/123 → 服务器返回用户数据 → 服务器完事忘了 POST /api/chat → 服务器返回模型回复 → 服务器完事忘了那登录态是怎么来的——靠的是Header 中的 Cookie / Authorization Token由客户端每次主动携带而不是服务器记住了谁。2.2 无状态 vs 有状态维度 无状态 (Stateless) 有状态 (Stateful)每次请求独立不依赖之前请求依赖服务器保存的上下文服务器负担低不需要存客户端信息高需要维护会话状态水平扩展✅ 任意一台服务器都能处理❌ 需要会话亲和性sticky session容错性✅ 一台挂了换一台即可❌ 状态丢失则会话中断类比自动售货机投币→出货→遗忘餐厅服务员记住每桌点了什么2.3 试想 LLM 服务器有状态会怎样用户 A: 我叫小明 → 服务器 1 记住了 用户 A: 我叫什么 → 负载均衡到了服务器 2 → 服务器 2???如果每台服务器都要维护上百万用户的对话状态内存直接爆炸更别提扩缩容、故障转移的噩梦。所以LLM API 必须是 Stateless 的——服务器不保存任何对话上下文。 三、那模型是怎么记住对话的答案是它根本没记。是你每次把全部历史对话拼好重新发给它。3.1 运行底层规则用户输入拼接全部历史消息HTTP POST 到 LLM模型生成回复将回复追加到历史三个核心原则LLM 是无状态的——它不记得上一轮说了什么想让它懂你——每次手动带上全部对话历史⚖️服务器端并发友好——请求在任何一台机器上运行都没差别3.2 看代码 下面是一段真实的 Demo演示了让模型记住名字这件事是怎么靠手拼chatHistory做到的importOpenAIfromopenai;import{config}fromdotenv;config();constclientnewOpenAI({apiKey:process.env.DEEPSEEK_API_KEY,baseURL:process.env.DEEPSEEK_API_BASE_URL,});// 核心历史对话数组这是我们手动维护的记忆constchatHistory[{role:system,content:你是一个严谨的助手}];asyncfunctiontestStateless(){// ──── 第一轮告诉模型名字 ────console.log( 第一次请求告诉模型一个信息);chatHistory.push({role:user,content:请记住我的名字叫零零发});constresponseawaitclient.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( 第二次请求直接问我是谁);chatHistory.push({role:user,content:请问我的名字是什么});constresponse2awaitclient.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( 最终 chatHistory:,JSON.stringify(chatHistory,null,2));}testStateless().catch(err{console.log(❌,err);});3.3 有状态 vs 无状态代码差异一针见血源码中有一段被注释掉的代码恰好展示了有状态幻想和无状态现实的对比constresponseawaitclient.chat.completions.create({model:deepseek-v4-flash,// ❌ 有状态的幻想写法被注释掉了// messages: [// { role: system, content: 你是一个严谨的助手 },// { role: user, content: 请记住我的名字叫零零发 }// ]// ✅ 无状态的正确写法messages:chatHistory// 把整个历史数组传过去});再对比第二轮constresponse2awaitclient.chat.completions.create({model:deepseek-v4-flash,// ❌ 有状态的幻想写法被注释掉了// messages: [// { role: user, content: 请问我的名字是什么 }// ]// ✅ 无状态的正确写法messages:chatHistory// 再次把整个历史数组传过去});一张表看清区别 有状态幻想 无状态现实第一轮发的 messages[system, user-1][system, user-1]← 一样第二轮发的 messages[user-2]← 只有当前[system, user-1, assistant-1, user-2]←完整历史模型知道之前聊了什么吗❌ 不知道。它只看到请问我的名字是什么✅ 知道。它看到了完整的对话链服务器的负担 需要为每个用户存历史无法扩展 零负担收到什么处理什么为什么这是幻想HTTP 是无状态的服务器不会帮你记住客户端自己维护chatHistory每次全量携带核心差异一句话有状态 期望服务器帮你记。无状态 你自己记好每次全量带上。chatHistory就是这个记忆载体——一个客户端维护的数组每次请求都完整发送。代码中被注释掉的部分正是新手最容易踩的坑以为上一轮消息服务器已经知道了这轮只发新消息就行——实际上那样做模型完全不知道你在说什么。3.4 另一个关键事实模型回复不加入chatHistory 模型不知道自己刚才说了什么。注意代码中每次client.chat.completions.create()后都紧跟着一句chatHistory.push({role:assistant,content:response.choices[0].message.content});如果漏掉这一步下一轮对话中模型就看不到自己上一轮的回复上下文就断了。这不是模型记不住——而是我们根本没把那条消息放进下一轮的messages里。⚠️ 四、chatHistory模式的问题手拼历史对话虽然能工作但随着对话增长问题逐渐暴露4.1 消息膨胀 → Token 开销指数增长第 1 轮: 2 条消息 (system user) 第 2 轮: 4 条消息 第 3 轮: 6 条消息 ... 第 N 轮: 2N 条消息每轮对话的 Token 消耗 前面所有轮次的总和。聊得越久单次请求越贵、越慢。4.2 容量天花板模型都有上下文窗口限制Context Window。当chatHistory超过这个窗口必须裁切。但简单粗暴地删掉最早的消息模型就丢失了早期记忆。4.3 LRU 缓存策略一种折中┌─────────────────────────────────────────────┐ │ chatHistory 数组 │ ├──────────┬──────────┬──────────┬───────────┤ │ 第1轮对话 │ 第2轮对话 │ 第3轮对话 │ ...第N轮 │ │ (丢弃) │ (保留) │ (保留) │ (保留) │ └──────────┴──────────┴──────────┴───────────┘ ↑ Token 容量上限类似 LRULeast Recently Used缓存保留最近聊的丢弃久远的。但这对长线任务是个问题——任务还没完成早期关键信息就已经被淘汰了。 五、演进从 Prompt 到 Context 到 LoopLLM 工程能力的升级路径本质是在无状态地基上层层搭建有状态的抽象阶段名称核心思路典型手段 L1Prompt Engineering写高质量 Prompt把上下文塞进 messagesSystem Prompt、Few-shot、历史对话拼接 L2Context Engineering动态检索 工具调用扩展上下文边界RAG 知识库、MCP 工具、Skill 调用 L3Loop Engineering循环编排把 LLM 嵌入工程流水线Agent Harness、自主循环、多步推理L1 — Prompt Engineering 当前最普遍的实践。通过精心设计systemprompt 手动维护chatHistory 知识库文件如CLAUDE.md、AGENTS.md塞进上下文。优点简单直接痛点像抽卡——Prompt 质量能提高抽到金卡的概率但不是特别可控L2 — Context Engineering LLM 的知识有截止日期也不知道你的私有数据。所以需要RAG检索增强生成从外部知识库拉相关资料注入上下文MCP / Skill让 LLM 调用外部工具获取实时数据、操作外部系统L3 — Loop Engineering ⚙️当前的前沿方向。把 LLM 嵌入一个**循环编排引擎Harness**中┌─────────────────────────────────────────────┐ │ Harness (编排引擎) │ │ │ │ ┌──────┐ ┌──────────┐ ┌─────────┐ │ │ │ 思考 │ → │ 执行动作 │ → │ 观察结果 │ │ │ └──────┘ └──────────┘ └─────────┘ │ │ ↑ ↓ │ │ └──────── 循环迭代 ──────────┘ │ └─────────────────────────────────────────────┘每次调 LLM 仍然是无状态的但Harness在客户端维护状态、决策循环、工具结果、多轮推理——用工程手段在无状态地基上盖出了有状态的 AI Agent。 六、总结无状态 LLM 架构全景 HTTP 协议 (Stateless) ═══════════════════════════════════════ │ │ │ 每次请求 独立的 POST │ │ 服务器不保存任何对话上下文 │ │ 可水平扩展任意服务器都能处理 │ ═══════════════════════════════════════ │ │ 之上构建 ▼ ═══════════════════════════════════════ │ chatHistory 数组 │ │ 客户端手动维护记忆 │ │ 每轮拼接全部消息再发出去 │ ═══════════════════════════════════════ │ │ 之上再构建 ▼ ═══════════════════════════════════════ │ Context / Loop Engineering │ │ RAG 工具调用 循环编排 │ │ 在无状态地基上盖出有状态 Agent │ ═══════════════════════════════════════核心认知一句话LLM 没有记忆——你每次带上的messages数组就是它的全部世界。理解了这个你就理解了为什么chatHistory这么重要、为什么 Token 消耗随对话增长、以及为什么所有 AI 工程最终都在围绕上下文管理做文章。