为什么你的第一个 Agent 记不住你是谁:从 Stateless 开始理解上下文边界
很多团队第一次把 LLM 接进产品,都会遇到一个乍看有点离谱、其实很基础的问题:你在本地明明刚对它说过“我是tc9011”,第二次请求再问“我的名字是什么?”,它却像第一次见你。
这件事经常会被误判成模型不稳定、SDK 有 bug,或者“这个模型不适合做 Agent”。但真正的问题通常更简单:你把 LLM 当成了一个自带会话状态的系统,而大多数模型 API 默认只是一次性推理接口。
如果这层地基没打稳,后面做 memory、RAG、tool calling、agent loop 时,几乎一定会把系统职责和模型能力混在一起。工程上最贵的错误,往往不是代码写错,而是心智模型一开始就歪了。
一个最常见的误会:把聊天窗口的连续感,当成模型天然会记住
开发者很容易被 UI 误导。用户看到的是一个连续对话框,于是直觉上会以为:
- 模型会自动记住上一轮说过的话
- system prompt 只要设一次,以后都会生效
- 第二次请求天然建立在第一次请求之上
但底层 API 根本不是这样工作的。
对绝大多数模型服务来说,一次调用就是一次独立推理。模型能回答什么,取决于这一次请求里实际收到的上下文,而不是你主观上觉得“我们刚刚不是已经聊过了吗”。
为什么这件事值得单独讲
因为它决定了后面整条 Agent 链路的职责分配:
- 模型负责根据当前上下文推理
- 应用负责准备当前上下文
一旦把“记忆”理解成模型内部能力,你后面遇到任何失忆问题,都会先去怪模型;而如果你从一开始就把它理解成上下文装配问题,你接下来就会自然地去检查 history、prompt 重放、会话存储和上下文裁剪。
这两个方向,后续工程成本完全不是一个量级。
下面这个最小示例,正好能说明问题出在哪。
完整代码
// 01-hello-world-gemini.js
// 目标:理解 LLM 的无状态 (Stateless) 特性 (Gemini Edition)
// 每次调用 API 都是一次全新的开始,它不记得之前的对话。
import { GoogleGenerativeAI } from '@google/generative-ai';
import dotenv from 'dotenv';
dotenv.config();
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
async function main() {
console.log("🤖 正在向 Gemini (gemini-flash-latest) 发送请求...");
// 获取模型实例
// 尝试使用 gemini-flash-latest,这是一个指向最新稳定版 Flash 模型的别名
const model = genAI.getGenerativeModel({ model: "gemini-flash-latest" });
// 第一次请求:打个招呼
const prompt1 = "你好!我是tc9011。";
const result1 = await model.generateContent(prompt1);
const response1 = await result1.response;
const text1 = response1.text();
console.log(`\nUser: ${prompt1}\nAI: ${text1}`);
// 第二次请求:试图引用上下文 (将会失败)
// 因为这是全新的生成请求,没有带上历史记录
console.log("\n🤖 发送第二个请求 (不带历史记录)...");
const prompt2 = "我的名字是什么?";
const result2 = await model.generateContent(prompt2);
const response2 = await result2.response;
const text2 = response2.text();
console.log(`\nUser: ${prompt2}\nAI: ${text2}`);
console.log("\n💡 结论:LLM (Gemini) 本身没有记忆。如果不把它之前的回答重新发给它,它就不知道我是谁。");
}
main();这段代码只有两次调用:
- 第一次说“你好!我是tc9011。”
- 第二次问“我的名字是什么?”
从产品视角看,它像一段连续对话;从 API 视角看,它其实是两笔互不相干的请求。
第二次请求里如果没有带上第一轮内容,那模型最合理的行为就是不知道。这里不是“忘了”,而是服务端压根没收到。
这个区别很重要,因为它直接决定你后面调试时看的东西:
- 如果你以为它忘了,你会去怀疑模型质量
- 如果你知道它没收到,你就会去查这次 request payload 里到底塞了什么
先看一个真实运行结果
当前版本运行 node 01-hello-world.js,你会看到类似下面的输出:
🤖 正在向 Gemini (gemini-flash-latest) 发送请求...
User: 你好!我是tc9011。
AI: 你好,tc9011!很高兴认识你。
🤖 发送第二个请求 (不带历史记录)...
User: 我的名字是什么?
AI: 作为一个人工智能,我无法直接知道你的真实姓名。
💡 结论:LLM (Gemini) 本身没有记忆。如果不把它之前的回答重新发给它,它就不知道我是谁。这里真正值得注意的,不是第二轮答错,而是第二轮只能基于当前请求内容回答。从 API 视角看,这不是“忘了”,而是这次请求里根本没有带上“我是tc9011”这句前文。
按调用链拆这段代码
1. 初始化 SDK:只是准备一个调用入口,不是创建会话
import { GoogleGenerativeAI } from '@google/generative-ai';
import dotenv from 'dotenv';
dotenv.config();
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);这里完成的是客户端初始化,不是 session 建立。它的职责只有两件事:
- 从环境变量读取
GEMINI_API_KEY - 创建一个可用来发请求的客户端
工程上更准确的类比是“创建 HTTP client”,而不是“开了一个长期对话房间”。
2. 获取模型实例:选择推理目标,不携带历史状态
const model = genAI.getGenerativeModel({ model: "gemini-flash-latest" });很多人会在这里产生错觉:既然我拿到的是同一个 model 实例,那它是不是会记得前面发生过什么?
不会。
这个对象只是告诉 SDK:后面请求要发给哪个模型。它并不意味着服务端替你维护了一份持久上下文。
3. 第一次 generateContent():一次完整且独立的推理
const prompt1 = "你好!我是tc9011。";
const result1 = await model.generateContent(prompt1);
const response1 = await result1.response;
const text1 = response1.text();这里发生的事情非常单纯:
- 应用把
prompt1发出去 - 模型基于这次收到的上下文生成回答
- 调用结束
到此为止,系统里并没有任何“自动持久化前情提要”的动作。请求结束,服务端就把这次推理当作已经完成的独立事务。
4. 第二次 generateContent():不是追问,而是另一笔新请求
const prompt2 = "我的名字是什么?";
const result2 = await model.generateContent(prompt2);
const response2 = await result2.response;
const text2 = response2.text();用户以为这是“接着刚才聊”;API 看到的是:
当前输入只有一句:我的名字是什么?
没有前文,自然也没有“tc9011”这个答案来源。
工程上你最好把这类调用理解成纯函数:
response = model(current_request_context)它不是从“当前进程记忆”里取上下文,而是从“当前请求内容”里取上下文。
这段代码真正建立起来的,是一个非常重要的职责边界
这篇不只是告诉你“模型默认无状态”,更重要的是让你先接受一个分工:
模型负责什么
- 基于本次上下文做语言推理
- 在当前上下文内补全、归纳、回答
应用负责什么
- 保存历史消息
- 重放 system prompt
- 注入用户资料、工具结果、检索内容
- 决定这一轮到底把什么送进模型
一旦你接受这个分工,后面所有看似复杂的 Agent 设计,都会变得更清楚:不过是在扩展“当前上下文由谁、以什么规则构造”这件事。
工程边界和常见误判
误判 1:模型记性差
很多时候不是记性差,是你根本没把历史带回去。
误判 2:system prompt 设一次就会一直生效
不是。system prompt 也是请求上下文的一部分。这一轮没带,它就不存在。
误判 3:SDK 里有 chat 抽象,说明底层已经有记忆
chat 通常只是帮你在客户端维护 history。那是 SDK 提供的封装便利,不代表底层模型突然变成了有状态系统。
误判 4:模型这么强,应该能“猜到”上下文
真实产品里,最忌讳把确定性需求交给猜。用户名、工单号、上一轮约束,这些都该靠上下文管理解决,不该指望模型蒙对。
把这件事想明白之后,你该怎么排查问题
一旦你先把 LLM 看成无状态推理引擎,排查顺序其实就很清楚了:
- 先看这轮请求到底发了什么
- 再看 system prompt 有没有带上
- 再看历史是不是正确拼接进去了
- 最后才去怀疑模型本身的表现
这套顺序很重要。它能帮你少踩很多坑:表面上像是模型不稳定,实际上只是应用没有把上下文管好。
收尾
如果你是从聊天产品视角进入 Agent,很容易把“连续感”误当成模型能力;但从工程视角看,它其实是应用层显式维护出来的一种体验。
把 Stateless 想明白,后面很多事都会顺:你会知道记忆系统要建在哪里,RAG 在补什么,tool calling 的结果为什么也要回填进上下文,以及为什么一个真正可用的 Agent,核心工作一直都不只是“调模型”,而是“管理上下文”。
下一篇就沿着这条线继续往前走:既然模型默认无状态,那一个看起来能连续聊天的系统,到底是怎么把上下文一轮一轮搬运起来的。

