一、引言:揭开 LLM 调用的本质
当我们在终端敲下 await client.chat.completions.create(...) 这行代码时,背后究竟发生了什么?
答案比大多数人想象的要简单------这不过是一次 HTTP 请求。你的代码将一段 JSON 数据(包含模型名称、消息列表和参数配置)通过 HTTPS 协议发送到远端的推理服务器,服务器消耗 GPU 算力完成前向推理,然后将生成的 token 序列化为 JSON 返回。整个过程在几百毫秒内完成,连接关闭,状态清零,然后服务器转身去处理下一个请求。
这就是 LLM 调用的本质:一次标准的 HTTP 请求-响应交互,消耗算力,生成结果,然后遗忘一切。
理解这一点,是理解现代 AI 工程化所有上层建筑的地基。
二、有状态 vs 无状态:一场架构的必然选择
要理解"无状态"的价值,我们首先需要理解它的对立面------"有状态"。
想象一个最简单的有状态 LLM 服务:用户 A 发起对话,服务器在内存中开辟一块空间,存储 A 的对话历史。用户 A 再次发消息时,请求必须被路由到同一台服务器的同一块内存上,取出历史,拼接到新的 prompt 中,再推理。如果用户 A 的第二次请求不幸被负载均衡器分配到了另一台服务器上,那台服务器就会一脸茫然------"你是谁?我们聊过什么?"
这种架构在低并发场景下可以勉强运转,但一旦面对日均百万级请求的生产环境,问题就立刻暴露:
- 内存压力巨大:每维护一个活跃会话,服务器就要在内存中持有一份对话历史。十万并发用户 × 平均 8K token 的上下文,内存开销足以压垮任何单机。
- 无法水平扩展:状态绑定在特定节点上,新增服务器无法分担带状态的请求,扩容变成了一场"状态迁移"的噩梦。
- 单点故障风险:那台存着用户状态的服务器一旦宕机,所有绑定会话全部丢失,用户体验瞬间归零。
而无状态架构的思路则截然不同:服务器不保留任何客户端上下文,每次请求都必须自包含全部信息。 这恰好就是 HTTP 协议的原生设计哲学------每个 GET/POST 请求都独立无依赖,服务器处理完就忘记。RESTful API 的设计原则,比如用 Header 传递身份认证(Authorization),用 Body 承载请求载荷,正是这一哲学的工程化表达。
这带来三个决定性优势:
- 水平扩展自由:因为每个请求都自包含,它落在集群中的任何一台服务器上都能被正确处理。加机器就加容量,线性扩容不需要任何状态同步。
- 容错性极强:一台服务器宕机,请求自动转移到另一台,用户完全无感。没有状态就没有"状态丢失"。
- 公平性:所有请求在服务器眼中是平等的,不存在"老用户优先"或"粘性会话"导致的资源分配不均。
三、LLM 的无状态实践:每次都带上全部对话
无状态的原理很清晰,但落地到 LLM 场景中有一个不可回避的挑战:大模型本身没有记忆。
你问 ChatGPT"我叫什么名字",它能答上来,不是因为它记住了你,而是因为客户端在每次请求时,都默默地把"用户说:请记住我叫字节戴 → 助手说:好的我记住了 → 用户说:请问我的名字是什么?"这整段对话历史,原封不动地塞进了 messages 数组里。
这正是 demo/index.mjs 中演示的核心逻辑。看看这段代码:
php
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.push({ role: 'user', content: '请问我的名字是什么?' });
const response2 = await client.chat.completions.create({
model: 'deepseek-v4-flash',
messages: chatHistory // 带上完整历史
});
这个简单 demo 揭示了无状态 LLM 的核心工程范式:状态不存储在服务端,而是由客户端维护,并在每次请求时全量提交。 模型每次推理都像是在读一个全新的剧本------它从第一个字读到最后一个字,然后即兴续写下一段。它不知道上一场戏发生了什么,因为它根本没见过上一场戏的剧本。是客户端把前后两场戏的剧本缝在了一起,才让它看起来"记得"。
四、chatHistory 的代价:从便利到瓶颈
但这个范式有自己的代价。随着对话持续,chatHistory 数组会不可遏制地膨胀。第 10 轮的对话历史可能是第 1 轮的 10 倍。而 LLM 推理的 token 开销与输入长度呈超线性增长------更长的上下文意味着更多的注意力计算、更高的 GPU 显存占用、更慢的响应速度,以及最重要的------更高的 API 费用账单。
这就引出了一个问题:如果 token 开销会随着对话增长而无限膨胀,为什么我们在 ChatGPT 或 Claude 里能连续聊上几十轮而不觉得明显变慢?
答案在于 LRU(最近最少使用)淘汰策略 与容量限制(capacity) 的组合。客户端的上下文窗口有一个隐式的"容量上限"------可能是模型的最大上下文长度(如 128K token),也可能是应用层设定的更保守的阈值(如 32K token)。当历史消息逼近这个上限时,最"远"的消息会被摘要、截断或直接丢弃,只保留最近交互的"工作集"。这就像你的大脑------你不会记得三天前的午饭吃了什么,但你会记得五分钟前别人对你说了什么,因为"刚才的信息"对当前任务的完成仍然关键。
但这又带来了一个新困境:哪些历史是可以丢的? 把用户在第 2 轮提到的一个关键约束条件删掉,第 15 轮的回复可能就完全偏离了方向。单纯的 LRU 策略无法区分"不再需要的闲聊"和"贯穿全程的核心需求"。
五、从 Prompt Engineering 到 Loop Engineering:三阶段的认知升级
正是上述困境,推动了对 LLM 交互方式的持续演进。readme 中勾勒了一条清晰的三阶段升级路径:
第一层:Prompt Engineering(提示词工程)。 这是最原始也最直觉的手段------精心设计 system prompt,字斟句酌地撰写每一条指令,试图让模型在一次性的 prompt 中理解所有需求。这就像抽卡:好的 prompt 能提高抽到金卡的概率,但结果本质上不可精确控制。它的天花板在于------无论 prompt 写得多好,模型始终只能看到"当前这一次"的信息。
第二层:Context Engineering(上下文工程)。 从这里开始,重心从"写好 prompt"转向"管好上下文"。RAG(检索增强生成)将外部知识库的动态检索结果注入上下文,MCP(模型上下文协议)让模型能按需调用外部工具获取实时信息,Skills 则以可组合的方式封装领域能力。核心思想是:与其寄望于模型"知道",不如确保模型"看到"。
第三层:Loop Engineering(循环工程)。 这是当前的最前沿------在上下文工程的基础上,引入反馈闭环。Harness AI 工程将单次推理扩展为"执行→观察→反思→再执行"的自主循环。每一次迭代都是一次独立的、无状态的 LLM 调用,但循环本身产生了涌现的"状态感"------就像电影是由一帧帧静止的画面连起来产生的运动错觉。
六、结语
回过头来看,无状态不是 LLM 的缺陷,而是 LLM 工程化的基石。正是因为每次请求都是自包含的、无依赖的、可独立处理的,我们才能在其上构建出弹性伸缩的集群、实现容错转移、控制 token 预算、并在无状态的原语之上,通过上下文工程和循环工程,一层层地堆叠出越来越智能的系统行为。
理解了这一点,再看那些眼花缭乱的 AI 框架和产品,你会发现它们的底层逻辑其实是相通的------都在做同一件事:在无状态的 HTTP 请求之上,精密地管理"该带哪些信息给模型"这件事。 无论是 Chatbot 的对话历史、RAG 的检索片段、Agent 的工具调用结果,还是 Skill 的领域指令------本质上,它们都是在构造一个比上一次更"完整"的 messages 数组,然后发起一次新的、独立的、无状态的 HTTP 请求。
这就是 LLM 工程的终极秘密:模型不需要记忆,你需要。