1. 项目缘起与核心定位
第一次看到“claude-mem”这个命名,我的直觉是:这应该是一个围绕对话记忆管理的工具或中间件。拆开来看,“claude”指向的是对话式AI的交互场景,“mem”则是memory的缩写,合在一起就是“为对话式AI提供记忆能力”。这个命名方式在开发者圈子里很常见,用短横线连接两个语义单元,既保留了可读性,又方便在命令行和包管理器中引用。
我在实际工作中接触过不少团队,他们用对话式AI做客服辅助、内部知识问答、代码审查助手,几乎所有人都会撞上同一堵墙:AI没有持久记忆。每次对话都是白纸一张,上一轮聊过的内容,下一轮就忘了。用户不得不反复粘贴背景信息,体验割裂,效率低下。claude-mem要解决的就是这个问题——让对话式AI能够记住之前聊过什么,并且在后续对话中自动调用这些记忆。
这个项目适合谁?如果你正在做对话式AI的应用开发,或者你是一个重度AI工具使用者,经常需要跟AI进行多轮深度协作,那claude-mem这类工具就值得你花时间研究。它不要求你懂深度学习,但需要你对API调用、数据存储、上下文窗口这些概念有基本认知。哪怕你只是刚入门的开发者,跟着本文的步骤走,也能搭出一个可用的记忆系统。
我先把话说在前面:记忆管理这件事,听起来简单,做起来坑很多。存什么、怎么存、什么时候取、取多少、怎么防止记忆污染,每一个环节都有讲究。下面我会从设计思路开始,一步步拆解,把我在这个方向上踩过的坑和总结的经验都摊开来讲。
2. 记忆系统的整体设计与思路拆解
2.1 为什么对话式AI需要外挂记忆
对话式AI的上下文窗口是有限的。以目前主流的对话模型为例,上下文窗口从几千token到几十万token不等。你可能会想,窗口这么大,直接把所有历史对话都塞进去不就行了?理论上可以,但实际中有三个问题。
第一,成本。token是要花钱的,每次请求都把全部历史带上,费用会线性增长。一个跑了三个月的客服助手,历史对话可能几十万字,每次请求都带上,账单会非常难看。
第二,噪声。历史对话里大量内容是寒暄、重复确认、无关闲聊。这些内容混在上下文里,会干扰模型对当前问题的判断。我实测过,把无关历史塞进上下文,模型回答的准确率会下降,有时候还会被带偏。
第三,窗口上限。再大的窗口也有上限,超过就截断。截断策略如果做得粗糙,可能把关键信息截掉,留下的是废话。
所以,外挂记忆系统的核心思路是:把历史对话中有价值的信息提取出来,结构化存储,在需要的时候精准检索,只把最相关的部分注入当前上下文。这样既控制了token消耗,又提高了信息密度,还突破了窗口限制。
2.2 记忆的分层模型
我在设计记忆系统时,习惯把它分成三层:短期记忆、长期记忆、工作记忆。这个分层借鉴了认知科学的模型,但在工程上非常实用。
短期记忆就是当前会话的最近几轮对话。这部分直接放在上下文里,不需要额外检索,因为它是连续的、强相关的。一般保留最近5到10轮,具体取决于每轮的长度。
长期记忆是跨会话的持久化信息。比如用户的偏好、项目背景、之前讨论过的结论。这部分存在数据库里,需要的时候通过检索调出来。
工作记忆是当前任务相关的临时信息。比如用户正在处理一个具体问题,相关的背景资料、中间结论,这些信息在当前任务结束后可以丢弃或归档。
claude-mem这类工具,主要解决的是长期记忆的存储和检索。短期记忆靠上下文窗口本身就能处理,工作记忆更多是应用层的逻辑。长期记忆才是真正需要外挂系统的部分。
2.3 存储方案选型:为什么不是简单的键值对
很多人第一反应是用键值对存储记忆,比如user_id -> memory_text。这个方案在极简场景下能用,但很快就会遇到瓶颈。
键值对的问题在于检索维度单一。你只能按user_id查,没法按语义查。用户问了一个新问题,你没法从记忆库里找到“语义上最相关”的记忆片段,只能把该用户的所有记忆都拉出来。记忆一多,又回到了上下文爆炸的老问题。
所以,记忆存储需要支持语义检索。这就引出了向量数据库。把记忆片段转成向量,存进向量库,查询时把当前问题也转成向量,做相似度搜索,返回最相关的几条。这个方案在工程上已经非常成熟,开源的有Chroma、Qdrant、Milvus,云服务也有不少选择。
但纯向量检索也有问题。它擅长语义相似,但不擅长精确匹配。比如用户说“我上次说的那个项目”,向量检索可能找不到,因为“那个项目”没有具体语义。这时候需要结合元数据过滤,比如按时间范围、按会话ID、按标签来筛选。
我的建议是:向量检索 + 元数据过滤 + 关键词匹配,三路召回,然后融合排序。这个方案复杂度适中,效果在实际场景中很稳。
2.4 记忆的写入策略:什么时候存,存什么
记忆写入不是越多越好。存得太多,检索时噪声大;存得太少,关键信息丢失。我总结了几条写入原则。
原则一:只存结论和事实,不存过程。比如用户说“我对比了A和B两个方案,A的延迟更低但成本更高,B反之,最终我选了A因为延迟是硬指标”。这里要存的是“用户选了A,原因是延迟优先”,而不是把整个对比过程都存下来。
原则二:存用户明确表达的信息,不存AI的推测。AI的推测可能出错,如果存进去,后续会被当成事实引用,造成记忆污染。只存用户确认过的内容。
原则三:定期压缩和合并。同一个话题的记忆,隔一段时间要合并成一条更精炼的记录。比如用户分三次说了项目的三个特点,可以合并成一条“项目特点:X、Y、Z”。
原则四:支持显式删除和修正。用户说“之前说的那个不对,改成这样”,系统要能定位到旧记忆并更新。这个功能在实际使用中非常重要,但很多简易实现都忽略了。
2.5 记忆的读取策略:取多少,怎么排序
读取策略直接决定了注入上下文的内容质量。取太多,token浪费且噪声大;取太少,可能漏掉关键信息。
我的经验值是:每次检索返回3到5条记忆,总长度控制在500到1000 token。这个量级在大多数场景下够用,且不会对上下文造成太大压力。
排序策略上,我一般用相关性得分 + 时间衰减 + 重要性权重的加权公式。相关性得分来自向量相似度,时间衰减让新记忆优先,重要性权重可以由用户标记或系统根据记忆类型自动赋值。
还有一个细节:去重。向量检索可能返回语义重复的记忆,比如同一条信息在不同时间被存了多次。需要在返回前做去重,否则会浪费token。
3. 核心细节解析与实操要点
3.1 记忆片段的结构设计
一条记忆片段应该包含哪些字段?我经过多次迭代,最终固定为以下结构:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | string | 唯一标识,建议用UUID |
| content | string | 记忆正文,精炼后的文本 |
| embedding | vector | 语义向量,维度取决于模型 |
| session_id | string | 来源会话ID |
| user_id | string | 用户标识 |
| tags | list | 标签,用于元数据过滤 |
| importance | float | 重要性权重,0到1 |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 最后更新时间 |
| access_count | int | 被检索次数,用于热度排序 |
| source | string | 来源类型,如user_input、ai_summary |
这个结构看起来字段不少,但每个都有用。tags用于精确过滤,importance用于排序加权,access_count可以识别高频记忆,source用于区分用户输入和AI摘要。
注意:
embedding字段的维度必须和检索时用的模型一致。如果你中途换了embedding模型,旧记忆的向量就废了,需要重新生成。所以选模型时要慎重,尽量选稳定、长期支持的。
3.2 记忆提取的触发时机
什么时候触发记忆提取?我见过几种方案,各有优劣。
方案一:每轮对话结束后提取。优点是及时,缺点是频繁调用提取逻辑,成本高,而且很多轮对话其实没有值得存的内容。
方案二:会话结束时提取。优点是批量处理,成本低;缺点是如果会话很长,中间的关键信息可能被淹没。
方案三:混合策略。每轮对话后做一个轻量判断,如果检测到“值得记忆”的信号(比如用户说了偏好、结论、重要事实),就立即提取;否则等会话结束再批量处理。
我推荐方案三。轻量判断可以用规则实现,比如检测关键词(“我决定”、“记住”、“以后都”、“我的偏好是”),或者用一个小的分类模型。这样既保证了关键信息的及时捕获,又控制了成本。
3.3 向量化模型的选择
embedding模型的选择直接影响检索质量。我对比过几个主流方案:
| 模型类型 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 通用文本embedding | 语义理解强,多语言支持好 | 维度高,存储和计算成本大 | 记忆内容复杂、多语言 |
| 轻量embedding | 速度快,成本低 | 语义精度稍弱 | 记忆内容短、单语言 |
| 领域微调embedding | 特定领域精度高 | 需要训练数据,通用性差 | 垂直领域应用 |
我的建议是:先用通用模型跑起来,等数据量大了、发现检索不准了,再考虑换模型或微调。不要一上来就追求最优,工程上够用就好。
还有一个实操细节:embedding要归一化。归一化后,余弦相似度计算可以用点积代替,速度快很多。大多数向量库都支持归一化存储,记得在写入前处理。
3.4 检索结果的融合排序
前面提到三路召回:向量检索、元数据过滤、关键词匹配。三路结果怎么融合?
我用的是一种简化的加权融合方法。每路召回给出一个得分,然后按权重相加。向量检索的权重最高,一般0.6到0.7;元数据过滤的权重0.2左右;关键词匹配的权重0.1到0.2。具体权重需要根据你的场景调,没有万能值。
融合后还要做多样性控制。如果返回的5条记忆都是同一个话题的,那其他话题的信息就被挤掉了。可以用MMR(最大边际相关性)算法,在相关性和多样性之间取平衡。这个算法不难实现,核心思想是:每次选一条与已选集合最不相似、但与查询最相关的记忆。
3.5 记忆的更新与冲突处理
记忆冲突是实际使用中最头疼的问题之一。用户先说“我喜欢用A方案”,后来说“A方案不行,改用B方案”。如果两条记忆都存着,检索时可能同时返回,模型就懵了。
处理冲突有几种策略:
策略一:时间优先。新记忆覆盖旧记忆。简单粗暴,但可能误伤。比如用户只是临时改变主意,后来又改回去了。
策略二:显式标记。让用户或系统显式标记哪条记忆有效。这个最准确,但增加操作负担。
策略三:冲突检测 + 自动合并。检测到语义冲突时,生成一条新记忆,内容为“用户先选择A,后改为B,当前有效选择为B”。这样保留了演变过程,又明确了当前状态。
我一般用策略三为主,策略二为辅。冲突检测可以用向量相似度加规则判断,比如两条记忆相似度高但内容矛盾(可以用一个小的NLI模型判断),就触发合并。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
假设你用Python来做,基础环境需要Python 3.9以上。核心依赖包括:
pip install openai chromadb sentence-transformers fastapi uvicorn这里解释一下每个依赖的作用。openai用于调用对话模型和embedding接口(如果你用其他模型,换成对应的SDK)。chromadb是向量数据库,轻量、易用,适合快速原型。sentence-transformers提供本地embedding模型,不想调API时可以用。fastapi和uvicorn用于搭建记忆服务的HTTP接口。
如果你打算用本地embedding,首次运行会下载模型文件,大概几百MB,需要耐心等待。用API embedding则不需要下载,但每次调用有网络延迟和费用。
提示:生产环境建议把向量库换成Qdrant或Milvus,Chroma在数据量超过十万条后性能会下降。但原型阶段Chroma足够用,别过度设计。
4.2 记忆存储模块的实现
先定义记忆的数据结构,用Pydantic做校验:
from pydantic import BaseModel from typing import List, Optional from datetime import datetime class Memory(BaseModel): id: str content: str embedding: Optional[List[float]] = None session_id: str user_id: str tags: List[str] = [] importance: float = 0.5 created_at: datetime updated_at: datetime access_count: int = 0 source: str = "user_input"然后是存储层,封装Chroma的操作:
import chromadb class MemoryStore: def __init__(self, persist_dir="./memory_db"): self.client = chromadb.PersistentClient(path=persist_dir) self.collection = self.client.get_or_create_collection( name="memories", metadata={"hnsw:space": "cosine"} ) def add(self, memory: Memory): self.collection.add( ids=[memory.id], embeddings=[memory.embedding], documents=[memory.content], metadatas=[{ "session_id": memory.session_id, "user_id": memory.user_id, "tags": ",".join(memory.tags), "importance": memory.importance, "created_at": memory.created_at.isoformat(), "source": memory.source }] ) def search(self, query_embedding, user_id, top_k=5): results = self.collection.query( query_embeddings=[query_embedding], n_results=top_k, where={"user_id": user_id} ) return results这段代码的关键点是where过滤,确保只检索当前用户的记忆。多用户场景下,这个过滤必不可少,否则会串数据。
4.3 记忆提取逻辑的实现
记忆提取我用一个两阶段方案。第一阶段用规则快速筛选,第二阶段用模型精炼。
import re MEMORY_SIGNALS = [ r"我(决定|选择|确定|偏好|喜欢|习惯)", r"记住|记一下|以后都|下次", r"结论是|最终|总结一下", r"我的(项目|团队|产品|需求)是" ] def should_extract(text: str) -> bool: for pattern in MEMORY_SIGNALS: if re.search(pattern, text): return True return False def extract_memory(text: str, llm_client) -> str: prompt = f"""从以下对话中提取值得长期记忆的信息。 只提取用户明确表达的事实、偏好、结论。 不要提取寒暄、重复确认、AI的推测。 输出格式:一句话,精炼,不超过50字。 如果没有值得记忆的内容,输出"无"。 对话内容: {text} """ response = llm_client.chat(prompt) return response.strip()这个方案的好处是成本低。大部分对话轮次不会触发提取,只有包含信号词的才调用模型。实测下来,提取频率大概占对话轮次的20%到30%,成本可控。
4.4 记忆检索与注入的实现
检索环节,我做了三路召回加融合。先看代码骨架:
def retrieve_memories(query, user_id, store, embedder, top_k=5): # 第一路:向量检索 query_emb = embedder.encode(query) vector_results = store.search(query_emb, user_id, top_k=top_k*2) # 第二路:关键词匹配(简化版,用Chroma的where_document) keyword_results = store.collection.query( query_texts=[query], n_results=top_k, where={"user_id": user_id} ) # 第三路:元数据过滤(按标签或时间) # 这里省略具体实现,根据业务需求定制 # 融合排序 merged = merge_and_rank(vector_results, keyword_results) return merged[:top_k]融合排序的merge_and_rank函数,核心是给每路结果打分再加权。向量检索的得分用1 - distance,关键词匹配的得分用匹配词数归一化。然后按权重相加,排序取前top_k。
注入上下文时,把检索到的记忆拼成一段文本,放在系统提示或用户消息前面:
def build_context(memories, current_query): if not memories: return current_query memory_text = "\n".join([f"- {m}" for m in memories]) context = f"""以下是与当前对话相关的历史记忆: {memory_text} 当前问题:{current_query} """ return context注意:记忆注入的位置会影响模型的行为。放在系统提示里,模型会把它当成背景知识;放在用户消息里,模型会把它当成当前输入的一部分。我一般放在系统提示里,效果更稳。
4.5 记忆更新与冲突处理的实现
冲突处理我用了一个简单的相似度加矛盾检测方案:
def check_conflict(new_memory, existing_memories, embedder, threshold=0.85): new_emb = embedder.encode(new_memory) for mem in existing_memories: sim = cosine_similarity(new_emb, mem.embedding) if sim > threshold: # 相似度高,检查是否矛盾 if is_contradictory(new_memory, mem.content): return mem return None def is_contradictory(text_a, text_b): # 简化版:检测否定词和反义词 # 生产环境建议用NLI模型 negations = ["不", "别", "取消", "改成", "换成"] for neg in negations: if neg in text_a and neg not in text_b: return True return False检测到冲突后,生成一条合并记忆,把旧记忆标记为失效:
def merge_conflict(old_memory, new_memory, store): merged_content = f"用户先选择:{old_memory.content};后改为:{new_memory.content};当前有效:{new_memory.content}" # 更新旧记忆的importance为0,或直接删除 store.update(old_memory.id, {"importance": 0.0}) # 存入合并后的新记忆 store.add(Memory(content=merged_content, ...))这个方案不完美,但能覆盖大部分常见冲突场景。更复杂的冲突需要NLI模型,成本会高一些。
5. 常见问题与排查技巧实录
5.1 检索不准的排查思路
检索不准是最常见的问题。表现是:明明存了相关记忆,但检索时没返回,或者返回了不相关的记忆。
排查步骤我一般按这个顺序来:
第一步,检查embedding是否正常。把查询文本和记忆文本分别embedding,算一下余弦相似度。如果相似度低于0.5,说明embedding模型可能不适合你的场景,或者文本太短、信息量不足。
第二步,检查向量库的索引。Chroma默认用HNSW索引,如果数据量小,可能还没建好索引。可以强制刷新或重建索引。
第三步,检查过滤条件。where条件写错是高频问题。比如user_id字段类型不一致,存的时候是字符串,查的时候传了整数,就查不到。
第四步,检查top_k设置。top_k太小,相关记忆可能排在后面被截掉。可以先把top_k调大,看相关记忆的排名,再决定合适的值。
我整理了一个速查表:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 完全查不到 | 过滤条件错误 | 检查where字段类型和值 |
| 相关记忆排名靠后 | embedding质量差 | 换模型或微调 |
| 返回不相关记忆 | 相似度阈值太低 | 提高阈值或加关键词过滤 |
| 结果重复 | 记忆未去重 | 检索后做去重 |
| 结果过时 | 时间衰减未生效 | 加时间权重 |
5.2 记忆污染的预防与清理
记忆污染是指错误或过时的记忆被反复检索和引用,导致模型输出错误。这个问题很隐蔽,因为污染是渐进的,一开始不容易发现。
预防措施有三条。第一,只存用户确认的信息。AI的推测、假设、临时结论,一律不存。第二,定期审计。每周或每月拉出所有记忆,人工抽查或让模型判断哪些可能过时。第三,支持用户反馈。用户说“这个记忆不对”,系统要能定位并删除或修正。
清理策略上,我一般用时间加访问频率的组合。超过90天未被访问且重要性低于0.3的记忆,自动归档或删除。被频繁访问的记忆,即使旧,也保留。
提示:删除记忆前建议先归档,不要直接物理删除。万一误删,还能恢复。归档表可以定期清理,比如保留一年。
5.3 性能优化的实操经验
记忆系统跑起来后,性能瓶颈通常出现在两个地方:embedding计算和向量检索。
embedding计算优化:批量处理。不要一条一条算,攒一批一起算,能显著减少API调用次数或GPU利用率。缓存。相同的文本不要重复embedding,用哈希做key缓存结果。
向量检索优化:控制数据量。单用户记忆超过一万条后,检索速度会下降。可以按时间分片,只检索最近半年的,更早的归档。索引参数调优。HNSW的ef_search参数影响检索速度和精度,可以适当调低换取速度。
还有一个容易被忽略的点:连接池。如果记忆服务是HTTP接口,每次请求都新建数据库连接会很慢。用连接池复用连接,能提升不少。
5.4 多用户场景的隔离问题
多用户场景下,数据隔离是红线。我见过因为过滤条件写漏,导致A用户看到B用户记忆的事故。虽然是在测试环境,但教训深刻。
隔离要在三个层面做。存储层:每条记忆必须带user_id,查询时必须带where={"user_id": xxx}。服务层:接口鉴权,确保请求中的用户身份和查询的用户身份一致。展示层:返回结果前再校验一次user_id,双重保险。
如果用户量很大,可以考虑按用户分库。每个用户一个独立的collection或独立的数据库文件。这样隔离最彻底,但管理成本高。中小规模用共享库加过滤就够了。
5.5 记忆系统的评估方法
怎么判断记忆系统好不好用?我一般看四个指标。
召回率:相关记忆被检索到的比例。可以人工标注一批查询和对应记忆,算召回率。目标值0.8以上。
准确率:检索到的记忆中,真正相关的比例。目标值0.7以上。
token效率:注入上下文的记忆token数,与带来的效果提升的比值。这个比较主观,但可以对比开启和关闭记忆系统时的任务完成质量。
用户满意度:最直接的指标。让用户评分,或者观察用户是否还需要重复提供背景信息。
评估要定期做,因为记忆库在增长,检索质量可能随时间变化。我一般每两周跑一次评估,发现问题及时调整。
6. 记忆系统的扩展方向
6.1 从被动记忆到主动记忆
目前的记忆系统是被动的:用户提问,系统检索。更高级的形态是主动记忆:系统在对话过程中主动判断“这个信息值得记”,并主动询问用户“要不要记住这个”。
主动记忆的关键是意图识别。需要判断用户说的话是临时信息还是长期有效信息。这个判断可以用规则加模型的方式做。规则覆盖高频模式,模型处理边界情况。
主动记忆还要解决打扰问题。不能每句话都问“要不要记”,那样用户会烦。我的做法是:只在检测到强信号时询问,且每个会话最多询问两次。用户拒绝后,本次会话不再询问。
6.2 记忆的可视化与管理界面
用户需要能看到系统记住了什么,并且能编辑、删除。一个简单的管理界面包括:记忆列表、搜索框、编辑按钮、删除按钮、标签管理。
界面不用复杂,但搜索和删除是刚需。用户发现记忆错了,要能快速找到并删掉。我见过一些实现,记忆存进去就找不到了,只能清空重来,体验很差。
如果做Web界面,可以用FastAPI加一个简单的前端。如果做命令行工具,提供list、search、delete几个命令就够了。
6.3 记忆的共享与协作
团队场景下,记忆可能需要共享。比如一个项目组共用一套记忆,新成员加入后能快速了解项目背景。
共享记忆的难点在权限控制。哪些记忆是私人的,哪些是团队共享的,需要明确标记。检索时,私人记忆只对本人可见,共享记忆对团队成员可见。
实现上,可以在记忆结构里加一个visibility字段,值为private或shared。检索时根据当前用户和记忆的可见性做过滤。团队ID也要加进去,确保跨团队隔离。
6.4 记忆的版本控制
记忆会更新,更新就需要版本控制。用户可能想回滚到之前的版本,或者查看记忆的演变过程。
简单的版本控制可以用追加式存储:每次更新不覆盖旧记录,而是新增一条,旧记录标记为superseded。检索时只返回最新版本。这样保留了历史,又不会干扰检索。
更复杂的版本控制可以引入分支概念,但我觉得对大多数场景来说,追加式存储就够了。别过度设计。
7. 我踩过的坑与实操心得
第一个坑是embedding模型换得太随意。项目初期我用了一个轻量模型,后来觉得效果不好,换了一个更大的模型。结果旧记忆的向量和新查询的向量不在同一个空间,检索完全失效。教训是:embedding模型一旦选定,要么不换,要么全量重新生成。换模型前一定要评估迁移成本。
第二个坑是记忆提取过于激进。一开始我设置了很宽松的提取规则,几乎每轮对话都提取。结果记忆库迅速膨胀,检索时噪声很大,模型经常被无关记忆干扰。后来改成严格规则加模型判断,提取频率降到20%左右,效果反而更好。记忆不是越多越好,精炼才是关键。
第三个坑是忽略了时区问题。记忆的created_at用了本地时间,但服务器在另一个时区,导致时间衰减计算错误。新记忆被当成旧记忆,排序全乱。后来统一用UTC时间存储,展示时再转本地时区。时间处理一定要用UTC,这是铁律。
第四个坑是没有做记忆去重。同一个信息被多次提取,存了多条。检索时返回一堆重复内容,浪费token。后来加了去重逻辑,相似度超过0.95的记忆自动合并。去重要在写入时做,不要等到检索时。
第五个坑是检索结果没有多样性控制。有一次用户问了一个综合问题,检索返回的5条记忆全是同一个子话题的,其他相关话题一条没有。模型回答得很片面。后来加了MMR算法,多样性明显改善。相关性重要,多样性也重要。
最后分享一个小技巧:给记忆加一个“最后访问时间”字段。检索时,如果一条记忆很久没被访问,可以降低它的权重。这样能自动淘汰过时记忆,不需要手动清理。我用了这个策略后,记忆库的自然衰减效果很好,维护成本降低了不少。
这个方向还有很多可以深挖的地方,比如记忆的自动摘要、跨会话的上下文继承、记忆与知识库的融合。我目前还在探索记忆的主动遗忘机制——不是所有记忆都值得永久保留,如何让系统学会“忘记”,可能比学会“记住”更难。