<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Qdrant on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/qdrant/</link><description>Recent content in Qdrant on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/qdrant/index.xml" rel="self" type="application/rss+xml"/><item><title>第七话｜记忆：模型从不记得任何事，是我们每次把该记住的塞回给它</title><link>https://zh.renxinblog.cn/post/c07-memory/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c07-memory/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c07-memory-cover.png" alt="Featured image of post 第七话｜记忆：模型从不记得任何事，是我们每次把该记住的塞回给它" /&gt;&lt;p&gt;第六话结尾，留下了一个没解决的问题。&lt;/p&gt;
&lt;p&gt;那一篇我们让循环「断了能接回来」：进度落盘、崩溃恢复、幂等防重做。但回头想，我们忙活的始终是「&lt;strong&gt;这一笔订单 O-1001&lt;/strong&gt;」从头到尾的进度。真实世界里，Agent 不会只处理一笔订单——它今天处理一百笔、明天再处理一百笔。于是问题冒出来了：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;处理第 1 笔订单时，Agent 学到「这个 SKU 经常缺货，得先查库存」；到第 100 笔，它&lt;strong&gt;还记得&lt;/strong&gt;吗？还是每一笔都像第一次那样，从头摸索一遍？&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;「记住这一次任务走到哪了」，和「记住过去做过的那些事、攒下的那些经验」，是两码事。前者第六话解决了；后者——&lt;strong&gt;让 Agent 跨任务、跨会话，还记得以前的事&lt;/strong&gt;——正是这一篇要讲的：记忆。&lt;/p&gt;
&lt;p&gt;先立住全文的锚，一句话：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;模型从不记得任何事，是我们每次把该记住的东西检索出来、塞回给它的。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="一先看清模型到底记得什么"&gt;一、先看清：模型到底「记得」什么
&lt;/h2&gt;&lt;p&gt;要讲记忆，得先拆穿一个最常见的误解——&lt;strong&gt;以为模型有记忆&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;模型没有记忆。每一次调用都是独立的：你发给它的，只有当前这一批 &lt;code&gt;messages&lt;/code&gt;。它不记得上一轮说过什么，不记得昨天聊过什么，更不记得半年前你喜欢喝什么。它「看起来记得」，是因为&lt;strong&gt;你每次把历史原样塞回去了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;把这一点说透，用一张表：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;你以为的&lt;/th&gt;
					&lt;th&gt;实际发生的&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;模型记得上一轮&lt;/td&gt;
					&lt;td&gt;你把上一轮对话塞回了 messages&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;模型记得你的名字&lt;/td&gt;
					&lt;td&gt;你把「用户叫张三」这条事实塞回了几轮&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;模型记得你爱喝茶&lt;/td&gt;
					&lt;td&gt;你把「偏好：喝茶」塞回了 prompt&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以「记忆」这件事，责任从来不在模型，而在&lt;strong&gt;运行它的那层代码&lt;/strong&gt;。模型只是个无状态的文本补全器；「记不记得住」，取决于你用什么方式、存到哪里、什么时候检索出来喂给它。&lt;/p&gt;
&lt;p&gt;这不是杞人忧天。MemGPT 那篇论文（arXiv 2310.08560）做过一个「Deep Memory Retrieval」实验：问一个五个会话之前讨论过的话题，裸的 GPT-4 只有 32.1% 答对——因为相关历史早被截断了；而 MemGPT 靠「把该记住的换进上下文」这套机制，做到了 92.5%。&lt;strong&gt;同样的模型，差距全在有没有一层可靠的记忆系统。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一篇要做的，就是把「你每次塞回去」这件事，从&lt;strong&gt;临时的、随手的、手忙脚乱的&lt;/strong&gt;，变成&lt;strong&gt;分层的、持久的、可检索的&lt;/strong&gt;——这才是「记忆系统」四个字的真正含义。&lt;/p&gt;
&lt;h2 id="二四层记忆各管一段"&gt;二、四层记忆，各管一段
&lt;/h2&gt;&lt;p&gt;人的记忆不是一坨，是分层的。这个分层不是拍脑袋——在 Agent 领域，有一篇被广泛引用的论文把它正式化了：&lt;strong&gt;CoALA（Cognitive Architectures for Language Agents，arXiv 2309.02427）&lt;/strong&gt;。它借鉴经典的认知架构（Soar 等），把语言 Agent 的记忆拆成一层工作记忆 + 三种长期记忆，正好对应四个名字：工作、情节、语义、程序。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层&lt;/th&gt;
					&lt;th&gt;是什么&lt;/th&gt;
					&lt;th&gt;存哪&lt;/th&gt;
					&lt;th&gt;什么时候失效&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;工作记忆 Working&lt;/td&gt;
					&lt;td&gt;窗口内的完整对话&lt;/td&gt;
					&lt;td&gt;内存 &lt;code&gt;messages&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;窗口满、进程重启&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;情节记忆 Episodic&lt;/td&gt;
					&lt;td&gt;会话内发生过的关键事实&lt;/td&gt;
					&lt;td&gt;SQLite&lt;/td&gt;
					&lt;td&gt;需主动写、主动读&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;语义记忆 Semantic&lt;/td&gt;
					&lt;td&gt;跨会话的事实、偏好、知识&lt;/td&gt;
					&lt;td&gt;向量库（检索）&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;不检索等于不存在&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;程序记忆 Procedural&lt;/td&gt;
					&lt;td&gt;怎么做事的方法、流程&lt;/td&gt;
					&lt;td&gt;代码 / Skill&lt;/td&gt;
					&lt;td&gt;本篇不展开&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;分清楚每层管什么、存哪、怎么失效，是设计记忆系统的第一步。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层&lt;/th&gt;
					&lt;th&gt;是什么&lt;/th&gt;
					&lt;th&gt;存哪&lt;/th&gt;
					&lt;th&gt;什么时候失效&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;工作记忆 Working&lt;/td&gt;
					&lt;td&gt;窗口内的完整对话&lt;/td&gt;
					&lt;td&gt;内存 &lt;code&gt;messages&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;窗口满、进程重启&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;情节记忆 Episodic&lt;/td&gt;
					&lt;td&gt;会话内发生过的关键事实&lt;/td&gt;
					&lt;td&gt;SQLite&lt;/td&gt;
					&lt;td&gt;需主动写、主动读&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;语义记忆 Semantic&lt;/td&gt;
					&lt;td&gt;跨会话的事实、偏好、知识&lt;/td&gt;
					&lt;td&gt;向量库（检索）&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;不检索等于不存在&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;程序记忆 Procedural&lt;/td&gt;
					&lt;td&gt;怎么做事的方法、流程&lt;/td&gt;
					&lt;td&gt;代码 / Skill&lt;/td&gt;
					&lt;td&gt;本篇不展开&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;四层各存各的，但最终殊途同归——都要在模型需要时，被检索出来、塞回给模型：&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[工作记忆&lt;br&gt;内存 messages] --&gt; E[拼进 prompt]
 B[情节记忆&lt;br&gt;SQLite 事实表] --&gt; E
 C[语义记忆&lt;br&gt;向量库检索] --&gt; E
 D[程序记忆&lt;br&gt;代码/Skill] --&gt; E
 E --&gt; F[无状态的模型&lt;br&gt;记不记得住全靠这次喂进去]&lt;/pre&gt;&lt;p&gt;逐层说。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作记忆&lt;/strong&gt;就是第四、五话里那个 &lt;code&gt;messages&lt;/code&gt; 列表——模型现在「正在看」的东西。它最直接，也最脆弱：窗口一满，最早的东西被挤掉；进程一重启，全没了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情节记忆&lt;/strong&gt;是「这条对话里发生过什么」：用户说他叫张三、说他爱喝茶。把关键事实&lt;strong&gt;显式提取&lt;/strong&gt;出来，存成结构化条目。它的价值不是「存了」，而是&lt;strong&gt;比全量历史更省 token、更抗干扰&lt;/strong&gt;，而且落盘后能活过重启。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;语义记忆&lt;/strong&gt;是「跨会话你还知道什么」：不管哪一任会话，Agent 都「知道」这个用户是 Java 后端、爱喝茶。它靠&lt;strong&gt;向量检索&lt;/strong&gt;——写入时把知识向量化，检索时算相似度召回。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;程序记忆&lt;/strong&gt;是「怎么做一件事」：不是「记住了什么事实」，而是「记住了怎么做」。它和前三层有本质区别——前三层记的是&lt;strong&gt;可遗忘的、会过期的&lt;/strong&gt;事实，程序记忆记的是&lt;strong&gt;可复用、经过验证的&lt;/strong&gt;方法。这是第九话 Skill 系统的引子，这里只点一句。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：分层不是为了显得高级，而是因为&lt;strong&gt;不同记忆的失效方式不同&lt;/strong&gt;——窗口会满、会话会结束、进程会重启。分不清层，就不知道该把什么存进哪一层，也不知道什么时候该去检索什么。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="三情节记忆把关键事实提取出来落进-sqlite"&gt;三、情节记忆：把关键事实提取出来，落进 SQLite
&lt;/h2&gt;&lt;p&gt;先从最容易落地的一层下手：情节记忆。&lt;/p&gt;
&lt;p&gt;第六话我们已经建立了一个认知——&lt;strong&gt;进度要显式化，不和消息耦合&lt;/strong&gt;。情节记忆同理：与其每次把整段对话塞回去，不如把它&lt;strong&gt;缩成几条结构化事实&lt;/strong&gt;。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EpisodicMemory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;CREATE TABLE IF NOT EXISTS facts (
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; id INTEGER PRIMARY KEY AUTOINCREMENT,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; subject TEXT NOT NULL, -- 事实主体
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; fact TEXT NOT NULL, -- 事实内容
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; session_id TEXT NOT NULL, -- 产生于哪个会话
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; created_at TEXT NOT NULL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; )&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;INSERT INTO facts(subject, fact, session_id, created_at) VALUES (?,?,?,?)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TZ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意这里多了一个字段 &lt;code&gt;session_id&lt;/code&gt;。它记录「这条事实产生于哪一场会话」，让每一条事实都可追溯归属。至于「情节记忆里的哪些事实，该升格为跨会话的语义记忆」——那是另一道工序，本篇 demo 还没做，但 &lt;code&gt;session_id&lt;/code&gt; 是它将来成立的地基：没有归属，就谈不上「把某个会话学到的、沉淀成全局长效的」。&lt;/p&gt;
&lt;p&gt;跑起来，你会看到最想看到的那一幕——&lt;strong&gt;进程重启后，事实还在&lt;/strong&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[1] 情节记忆：关键事实落 SQLite，重启读回
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;情节记忆已写入 SQLite（3 条）：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 用户名: 张三
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 偏好: 喝茶，尤其是龙井
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 职业: Java 后端工程师
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;（模拟进程重启：新开连接，读回）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;重启后读回的事实：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 用户名: 张三 (session=session-A)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 偏好: 喝茶，尤其是龙井 (session=session-A)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 职业: Java 后端工程师 (session=session-A)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;「进程重启」这一下，正是第六话「断点恢复」的自然延伸：第六话让&lt;strong&gt;这一次任务的进度&lt;/strong&gt;活过崩溃，这一篇让&lt;strong&gt;过去会话的事实&lt;/strong&gt;活过重启。同样是「落盘」，落的东西不一样。&lt;/p&gt;
&lt;p&gt;但情节记忆有个天花板：它只能&lt;strong&gt;原样读回&lt;/strong&gt;。你知道「用户爱喝茶」这条事实存在，可当用户问出一句你从没存过原句的话——比如「我平时喜欢喝什么呀？」——你怎么知道该把「喝茶」这一条翻出来？&lt;strong&gt;能存，还得能找。&lt;/strong&gt; 这就引出下一层。&lt;/p&gt;
&lt;h2 id="四语义记忆不检索等于不存在"&gt;四、语义记忆：不检索，等于不存在
&lt;/h2&gt;&lt;p&gt;语义记忆要解决的，是「&lt;strong&gt;从哪里找&lt;/strong&gt;」的问题。&lt;/p&gt;
&lt;p&gt;存了一万条事实，用户问一句新的话，你不可能把一万条全塞给模型（塞不下，也污染）。你得&lt;strong&gt;按语义相似度，挑最相关的几条&lt;/strong&gt;。这就是「检索」，也是语义记忆和情节记忆的分水岭：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;情节记忆回答「发生过什么」，语义记忆回答「和这个问题最相关的是什么」。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;检索靠的是&lt;strong&gt;向量&lt;/strong&gt;：把每段文字映射成一个高维向量，语义相近的文字，向量在空间里挨得近。这里的关键，是用&lt;strong&gt;真实 Embedding 模型&lt;/strong&gt;（bge-m3，1024 维）把文字变向量，而不是手动分词去凑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;bge-m3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;prompt&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EMBEDDING_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/embeddings&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;embedding&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后存入 Qdrant 向量库，检索时算余弦相似度召回。跑起来看真实的检索结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[2] 语义记忆：bge-m3 + Qdrant，跨会话召回（含同义改写）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 问「用户喜欢喝什么？」→ 命中 [用户喜欢喝茶，尤其是龙井] score=0.753
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 问「用户爱喝什么饮料？」→ 命中 [用户喜欢喝茶，尤其是龙井] score=0.705
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 问「用户职业是什么？」→ 命中 [用户职业是 Java 后端工程师，擅长并发编程] score=0.704
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 问「博客写什么？」→ 命中 [用户的博客主题是 AI Agent 开发] score=0.604
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;盯住第二行。问的是「&lt;strong&gt;爱喝&lt;/strong&gt;什么饮料」，命中的是「&lt;strong&gt;喜欢喝&lt;/strong&gt;茶」——&lt;strong&gt;两个词字面上完全不同，但向量空间里挨得很近&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这就是真实 Embedding 的价值，也是它区别于「土法分词」的地方。用纯 bigram 分词做检索，「爱喝」和「喜欢喝」一个字都对不上，匹配为零；而 bge-m3 把同义改写投影到相近向量，「爱喝」和「喜欢喝」照样能对上。&lt;strong&gt;语义检索的「语义」二字，靠的就是这一步。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="五这一层真正的坑上下文的位置"&gt;五、这一层真正的坑：上下文的位置
&lt;/h2&gt;&lt;p&gt;语义检索把最相关的东西找出来了，但故事还没完——&lt;strong&gt;找出来之后，塞回给模型时，放在哪个位置，也决定它「记不记得住」。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2023 年一篇论文《Lost in the Middle》（arXiv 2307.03172）量了这个位置效应，结论很扎心：&lt;strong&gt;模型是「U 形」阅读者&lt;/strong&gt;——最关注上下文的两头，几乎忽略中间。在「键值检索」任务里，答案 key 放开头或结尾时准确率约 95%，放中间时跌到约 40%；换成多文档问答，从最好位置到最差位置的落差约 25–30%，而且&lt;strong&gt;连明确标榜「长上下文」的模型也躲不掉&lt;/strong&gt;——这不是窗口不够大，是注意力模式本身的限制。&lt;/p&gt;
&lt;p&gt;这对记忆系统的启示一句话就够了：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;检索回来之后，别随手一塞。&lt;/strong&gt; 最重要的几条放到上下文两头，中间留给次要信息。检索只是第一步，排序和摆放同样决定模型「用没用上」你找到的记忆。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这一篇的 demo 检索结果只有几条、短文不触发这个效应，所以这里只作一句提醒——真正做长上下文记忆时，位置问题会和检索问题一起蹦出来。&lt;/p&gt;
&lt;h2 id="六程序记忆怎么做事是另一类记忆"&gt;六、程序记忆：怎么做事，是另一类记忆
&lt;/h2&gt;&lt;p&gt;四层讲了三层，最后一层只点一句，因为它是独立的一篇。&lt;/p&gt;
&lt;p&gt;前三层记的都是&lt;strong&gt;事实&lt;/strong&gt;：你叫什么、爱喝什么、是做什么的。但有一类「记忆」被漏掉了——&lt;strong&gt;怎么把事情做对的方法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;「这个 SKU 常缺货，得先查库存」是事实。「处理订单要先查库存、再扣库存、扣完发通知、全程落盘」是&lt;strong&gt;方法&lt;/strong&gt;。方法不存成事实，方法要存成&lt;strong&gt;可重复调用的流程&lt;/strong&gt;——这就是第九话要讲的 &lt;strong&gt;Skill&lt;/strong&gt;。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;程序记忆和前三层的本质区别：事实会过期，方法可复用。把「经过验证的做法」固化成资产，Agent 才不是每次都从零摸索。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这里先埋下这个钩子，到此为止。&lt;/p&gt;
&lt;h2 id="七动手实验--失败路径"&gt;七、动手实验 + 失败路径
&lt;/h2&gt;&lt;p&gt;看完原理，跑一遍真的。任务是「让 Agent 跨会话认得你」：先自报姓名与偏好，落盘；再模拟重启、换新会话；最后问一个需要召回的开放式问题。&lt;/p&gt;
&lt;p&gt;配套代码在 GYA 仓库（Python）与 GYA-Java 仓库（Java 21），两条命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 离线回归：不依赖 Embedding/Qdrant/LLM（5 项测试）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m unittest test_memory -v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 完整演示：真实 bge-m3 + Qdrant + SQLite&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;QDRANT_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;你的key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 memory_demo.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最想让你看到的三段输出，已经贴在第三、四节了。这里补它最有意思的&lt;strong&gt;失败路径&lt;/strong&gt;——也就是「无记忆」时的样子。同样的问法，不给任何上下文：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[3] 无记忆对照：模型没有上下文时，答不上&amp;#39;我是谁&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 模型: 我不知道你是谁，也不知道你叫什么名字。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;有记忆时它能答「你是张三，爱喝茶」；没记忆时它只能老实说「我不知道」。这一对照，把「模型从不记得任何事」这句话坐实了——&lt;strong&gt;差别不在模型，全在我们有没有把该记住的塞回去。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;跑不通时，最常见的三个坑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Embedding/Qdrant 没起&lt;/strong&gt;：语义部分会自动降级到离线 TF-IDF（会打 ⚠️）。真实检索需要 Ollama bge-m3 在线、Qdrant 可访问、&lt;code&gt;QDRANT_API_KEY&lt;/code&gt; 正确。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qdrant 写入报 400「not a valid point ID」&lt;/strong&gt;：Qdrant 的 point id 只认无符号整数或 UUID，不认数字字符串。demo 里已用整数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Java 版报 &lt;code&gt;SQLITE_IOERR_DELETE&lt;/code&gt;&lt;/strong&gt;：受限沙箱环境（CI/容器化 shell）会拦 SQLite 默认 journal 的 unlink 调用。Java 版 &lt;code&gt;newDb()&lt;/code&gt; 里已用 &lt;code&gt;PRAGMA journal_mode=OFF&lt;/code&gt; 绕过；正常本地终端可去掉这两行，恢复崩溃安全。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="八总结和下一篇"&gt;八、总结和下一篇
&lt;/h2&gt;&lt;p&gt;四层记忆到这里立住了：工作记忆管「现在看什么」，情节记忆管「这次发生过什么」，语义记忆管「跨会话知道什么」，程序记忆管「怎么做」。它们的共同病根只有一个——&lt;strong&gt;模型无状态，记不记得住全靠我们把该记住的东西，从持久化存储里检索出来、塞回去。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一篇让 Agent 记住了「你是谁、你要什么」。但有个更根本的能力还没碰：它&lt;strong&gt;怎么接上外部世界&lt;/strong&gt;——查天气、查库存、调别人家的服务，谁来定接口、谁来负责发现和调用？这是第八话要讲的：&lt;strong&gt;MCP&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;演示代码&lt;/strong&gt;：Python 版在 &lt;a class="link" href="https://github.com/renxin2024/GYA/tree/main/c07-memory" target="_blank" rel="noopener"
 &gt;GYA 仓库 &lt;code&gt;c07-memory&lt;/code&gt;&lt;/a&gt;，Java 21 等价实现在 &lt;a class="link" href="https://github.com/renxin2024/GYA-Java/tree/main/c07-memory" target="_blank" rel="noopener"
 &gt;GYA-Java 仓库 &lt;code&gt;c07-memory&lt;/code&gt;&lt;/a&gt;。按各自 README 的命令跑，不要复制这篇文章里零散的代码块。
&lt;strong&gt;参考&lt;/strong&gt;：Sumers 等, &lt;a class="link" href="https://arxiv.org/abs/2309.02427" target="_blank" rel="noopener"
 &gt;CoALA: Cognitive Architectures for Language Agents&lt;/a&gt;（工作/情节/语义/程序四层记忆的出处）；Packer 等, &lt;a class="link" href="https://arxiv.org/abs/2310.08560" target="_blank" rel="noopener"
 &gt;MemGPT: Towards LLMs as Operating Systems&lt;/a&gt;（分层记忆与 Deep Memory Retrieval 数据）；Liu 等, &lt;a class="link" href="https://arxiv.org/abs/2307.03172" target="_blank" rel="noopener"
 &gt;Lost in the Middle&lt;/a&gt;（上下文位置的 U 形效应）；Embedding 模型 BAAI &lt;code&gt;bge-m3&lt;/code&gt;（1024 维）。检索与无记忆对照 Trace 为 2026-09 本机实测（Python 与 Java 双跑，分数一致）。&lt;/p&gt;

 &lt;/blockquote&gt;</description></item></channel></rss>