你真的理解 LLM 的"无状态"吗?从一段代码讲起

27 阅读6分钟

为什么每次跟 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 里的 CookieAuthorization 令牌。每次请求客户端都主动带上身份凭证,服务器不存你是谁,只校验凭证是否有效。

用一个比喻来理解:

模式比喻特点
有状态你去银行柜台,柜员认识你,知道你存了多少钱服务器要记住你是谁,压力大
无状态你去自助取款机,每次都要插卡输密码服务器不记你是谁,每次自己证明身份

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 的"无状态"总结就是三句话:

  1. 每次请求都是独立的,服务器不存储任何会话状态
  2. "记住"是假象,真相是客户端每次手动把全部对话历史带上
  3. 这是刻意的设计,换来的是高并发、高可用、水平扩展的能力

理解了这一点,你就能明白为什么 AI 应用的架构设计里,上下文管理(怎么选历史、怎么压缩、怎么检索)才是真正的核心难题,而不是"怎么让模型记住更多"。


延伸思考:既然每次都要传全部历史,那 OpenAI 的"记忆"功能(ChatGPT 能跨会话记住你的偏好)又是怎么实现的?答案是 —— 它并不是打破了无状态,而是服务端在收到请求时,从数据库里取出你的"记忆摘要",注入到 system prompt 里,然后再走正常的无状态推理流程。本质还是"手动带上"。


本文基于对 LLM API 调用机制的学习和实践整理,希望能帮你建立对 AI 应用架构的底层认知。