【从零理解 Spring AI · 02】大模型为什么“记不住“也“不知道“
【从零理解 Spring AI · 02】大模型为什么"记不住"也"不知道"
写在开头
上一篇结尾我留了一个悬念:模型的三个边界,决定了 AI 应用的三套机制。
这篇专门讲其中最容易被误解的两个:“记不住"和"不知道”。
先看两个我亲身踩过的场景:
场景一:我告诉模型"我叫张三,负责订单系统",聊得好好的。第三轮我问"我负责哪个系统?“——它猜了一个"支付系统”。
场景二:我问它"你们最近发布的模型有什么新特性?"——它滔滔不绝讲了一堆特性名,有条有理。然后我一对官方文档,一半是编的。
一开始我以为这是模型"不够聪明"或者"版本太旧"。后来才明白:这两个行为都不是 bug,而是这类系统的构造方式决定的。搞不清这两件事,后面的对话记忆、检索增强就永远只能"照抄配置",不知道自己在配什么。
这篇就把这两个边界掰开讲透。不用任何数学,只需要一个比喻和几个实验。
一、它根本没有"记忆"这个部件
1.1 关键认知:每次请求都是独立的
很多人对大模型的想象是这样的:
用户 ──→ [ 会思考、有记忆的大脑 ] ──→ 回答
觉得模型像一个大脑,聊过的东西会"存在里面"。
真实情况更接近这样:
用户 ──→ [ 一个无状态的函数 ] ──→ 回答
↑
输入什么就处理什么
模型是一个纯函数:你发一段文字过去,它根据这段文字算出下一段文字返回。它内部没有一块"存储区"在等着装你的对话历史。 这次请求和上次请求之间,没有任何东西被保留下来。
所以"我叫张三"这句话,在第一轮请求结束后就消失了。第二轮你说"我叫什么",模型收到的输入里只有这一句——它怎么答?
1.2 用 Spring AI 把这件事做实
不废话,直接验证。写两个独立的调用:
@RestController
public class MemoryDemoController {
private final ChatClient chatClient;
public MemoryDemoController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/round1")
public String round1() {
return chatClient.prompt()
.user("记一下:我叫张三,负责订单系统。")
.call()
.content();
}
@GetMapping("/round2")
public String round2() {
return chatClient.prompt()
.user("我叫什么名字?负责什么系统?")
.call()
.content();
}
}
先访问 /round1,模型会回应"好的张三,已记下"之类的确认(注意:它只是礼貌性地复述,什么都没存)。
再访问 /round2,典型输出是:
抱歉,您没有告诉过我您的名字,我也不知道您负责什么系统。
两个请求之间唯一的联系,是你的浏览器会话——而不是模型。 这就是"无状态"三个字的具体样子。
1.3 那多轮对话是怎么实现的?
既然模型无状态,那 ChatGPT 那种"连续对话"是怎么回事?
答案朴素得让人失望:每一轮,把之前的对话历史全部重新发一遍。
第 1 轮发送:[ 你叫什么 ]
第 2 轮发送:[ 你叫什么, 我叫张三, 我叫什么名字? ]
第 3 轮发送:[ 你叫什么, 我叫张三, 我叫什么名字?, 张三, ... ]
所谓"记忆",不是模型记住了,而是你每次都把"该记住的内容"抄在考卷最上面。
这个认知一建立,很多事情立刻通了:
- 为什么对话越长,响应越慢、费用越高——历史是随请求重复发送的;
- 为什么换一个新会话,模型就"失忆"了——新会话没有带历史;
- 为什么"记忆"需要一个专门的组件来管——手工拼历史消息既繁琐又容易出错。
二、上下文窗口:一张固定大小的桌子
2.1 "带历史"有一个硬性上限
历史不能无限带。模型一次能处理的输入长度是有上限的,这个上限就是"上下文窗口"。
你可以把一次请求想象成一张固定大小的桌子:

一次请求的全部内容都要摆上这张桌子
桌子上要同时放下四样东西:
| 内容 | 说明 | 会变化吗 |
|---|---|---|
| 系统提示词 | 角色设定、行为规则 | 基本固定 |
| 对话历史 | 之前每一轮的问答 | 每轮都在变长 |
| 本轮问题 | 用户这次问的 | 每轮不同 |
| 回答空间 | 模型输出的预留位置 | — |
注意最后一样:窗口装的不只是输入,还包括模型的输出。你让它写一篇长文,就得给它留出足够的空位。
2.2 桌子坐不下怎么办
历史一直变长,迟早把桌子占满。这时会发生两种事之一:
- 框架帮你截断:把最早的历史丢掉,只留最近的——于是模型"忘了"开头说过什么;
- 直接报错:请求被拒绝。
无论哪种,结果都是同一个:你无法依赖"很久之前说过的话"。
2.3 由此推出的记忆策略
理解了"桌子"模型,记忆的所有设计就只是在回答一个问题:桌子坐不下时,留什么、丢什么?
三种典型策略:
| 策略 | 做法 | 代价 |
|---|---|---|
| 全量携带 | 历史全带上 | 简单可靠,但很快撑爆窗口 |
| 滑动窗口 | 只带最近 N 轮 | 早期信息直接丢失 |
| 摘要压缩 | 把旧历史压成一段摘要 | 省空间,但细节没了 |
没有完美选项,都是取舍。Spring AI 把这些做成了可配置的能力,下一篇之后的记忆篇会展开。
三、它"不知道":知识有一个截止点
3.1 模型的世界是冻结的
第二个边界更容易被忽略。
模型的能力来自训练——读海量文本,学语言规律。而训练总要在一个时间点结束。这意味着:
模型知道的一切,都停留在训练数据截止的那一刻。
在那之后发生的事,它一无所知:
- 你公司的产品文档、内部制度——训练数据里根本不存在;
- 今天的新闻、昨天的价格——发生在截止点之后;
- 只有你团队知道的那套黑话——从来没人公开写过。
它不是"不想说",是真的没见过。
3.2 比不知道更麻烦的:它不知道自己不知道
如果模型在不知道时老老实实说"我不知道",那问题不大。
坏就坏在它的生成机制上:模型是"接龙机器"——根据前文,一个词一个词地生成"最像接下来会出现的词"。它没有"这道题我不会"的开关,它的工作方式就是无论如何都要把话接下去。
于是当你问一个它不知道的问题时,会发生什么?它按照"这类问题通常怎么回答"的语言模式,流畅地生成一段看起来很专业的答案——里面的机构名、日期、参数、结论,全是语言惯性拼出来的。
这就是"幻觉"的来源:不是它在撒谎,而是"编"和"答"在它那里是同一个动作。
3.3 两个高危场景
结合实战,这两种提问方式最容易触发幻觉:
- 问具体事实:某个函数的参数名、某次发布的日期、某个数字——语言模式能编出以假乱真的样子;
- 问你的私有领域:内部系统、行业黑话——模型没见过,只能拿"最相似公开知识"来套。
应对思路也是两条,正好对应后面的两篇:
- 教它说"不知道":在系统提示词里明确"资料中没有就说没有"——能压一部分,但压不干净;
- 给它资料:把正确的、最新的资料检索出来放进上下文,让它照着资料答——这就是检索增强(RAG),第四辑展开。
四、两个边界,两条架构路线
现在把第一篇的预告兑现:为什么说这两个边界"直接决定了架构"?

第一篇的边界-机制对照图,这里再放一次
把两篇的内容合起来看:
记不住(无状态 + 窗口有限)
└─→ 每轮重带历史 → 需要管理历史 → 【对话记忆】组件
不知道(知识截止 + 必然幻觉)
└─→ 需要外部资料 → 检索后放进上下文 → 【检索增强 RAG】
做不到(只会输出文字)
└─→ 需要真实操作 → 让模型指挥代码 → 【工具调用】
Spring AI 里每一个"听起来很高大上"的组件,都是这三条推导链的终点。 记忆组件在管理"桌上放多少历史",RAG 组件在管理"桌上放哪些资料",工具调用在解决"模型说的话怎么变成真实操作"。
理解了边界,组件就不是 API,而是答案。
五、这篇的四个要点
- 模型是无状态函数——“记忆"的本质是每轮重发历史,没有任何东西存在模型"里面”。
- 上下文窗口是固定大小的——系统提示词、历史、问题、回答空间都要摆上去;历史越长,可用空间越少。
- 知识有截止点,且它不知道自己不知道——幻觉不是撒谎,是"编"和"答"共用同一个生成动作。
- 三个边界分别推导出三套机制——记忆、检索增强、工具调用,这就是 Spring AI 核心组件的"存在理由"。
延伸思考
生产环境里,"记忆怎么管"会立刻遇到一个现实问题:多用户共用同一个模型服务,历史怎么隔离? 张三和李四的对话历史混在一起是要出事故的。Spring AI 用"会话 ID"来隔离,而会话 ID 怎么设计、历史存哪里(内存?数据库?Redis?)、存多久——这些是记忆篇要展开的工程问题。
先记住这个方向即可。
下一篇
边界讲完了,下一篇开始正式进入 Spring AI 的世界——
《Spring AI 在 Spring 生态里的位置》:为什么"自己发 HTTP 调模型"是下策?Spring AI 的四层结构分别管什么?以及它和你已经用了多年的 Spring Boot 自动配置是怎么配合的。
如果你已经忍不住想先写代码,也可以先跳去看那篇的最小可用示例——但这篇的边界认知,是你后面读懂每一段设计的前提。
本专栏按 Spring AI 当前版本讲解。
更多推荐


所有评论(0)