记忆写进去读不出来:微软 AI Agent 入门课第 13 课两种实现的存取键
调 Agent 记忆最常见的一类工单是这样的:日志里明明打了”已保存”,换一个会话再问,Agent 就说没有你的记录。ai-agents-for-beginners 的第 13 课 13-agent-memory/ 下有两份实现,13-agent-memory.ipynb 用一个进程内字典,13-agent-memory-cognee.ipynb 用 Cognee 的知识图谱。两份都会出现这个现象,但根因分别落在不同的位置:一份是键由模型填,一份是写入和读取用的参数名根本不是同一个。
下面按排查顺序走。事实全部来自这两个 notebook 与同目录 README.md,我们没有跑过其中任何一段代码。
现象一:存进去了,同一个进程内换个 user 就读不出来
13-agent-memory.ipynb 的长期记忆是一个模块级字典:
preference_store: dict[str, list[str]] = {}
写入和读取分别包在两个 @tool 函数里:
@tool(approval_mode="never_require")
def save_preference(
user_id: Annotated[str, "User identifier"],
preference: Annotated[str, "A travel preference to remember"],
) -> str:
"""Save a user travel preference to long-term memory."""
preference_store.setdefault(user_id, []).append(preference)
return f"✅ Stored: {preference}"
get_preferences(user_id) 那侧是 preference_store.get(user_id, [])。所以这份实现的键就是 user_id 这个字符串本身,没有归一化,没有前缀,没有校验——大小写不同、多个空格、写成邮箱还是写成昵称,在字典里就是不同的槽位。
关键在于:这个键不是代码填的,是模型填的。 它是工具的入参,值从哪来?看 notebook 里建 agent 的那段 instructions,最后一行写着:
IMPORTANT: Always use user_id='sarah_johnson_123' for all memory operations.
也就是说,键的一致性完全靠一句系统提示词兜着。这在课堂示例里没问题,因为只有一个用户;一旦你把它改成真实场景,模型少复述一次这个 id,写入就落到另一个槽位里去了,读取自然是空的。
怎么确认是这个问题
notebook 里现成给了判定动作,就是那个”验证存了什么”的 cell:
print("📋 Preference store contents:")
for uid, prefs in preference_store.items():
print(f"\n User: {uid}")
for p in prefs:
print(f" - {p}")
直接把 preference_store 的所有键打出来。如果字典里有内容、但键不是你以为的那个字符串,问题就锁定了——不是没存进去,是存到别的键下面了。
处置
从代码语义上看有两个可下手的位置:一是不把 user_id 暴露成模型可填的入参,而是在包装函数里闭包住;二是保留入参但在函数体内做归一化。仓库示例走的是第一条路的反面——它把 user_id 显式声明成了 Annotated[str, "User identifier"] 的工具参数,靠提示词约束取值。课程给的”下一步”里写的是换成数据库或向量存储、加过期时间这类方向,没有给键归一化的实现。这层需要你自己补,不是仓库里现成的东西。
验证
改完之后仍然用上面那个遍历 preference_store.items() 的 cell 看键:连续几轮对话之后,字典里应当只有一个键。出现第二个键就说明约束没生效。
什么情况说明不是这个原因
如果打印出来字典整个是空的,那就不是键的问题,往下看现象二和现象三。另外如果你发现 Agent 压根没调 save_preference,那也不是键的问题——这份 notebook 建 agent 时传的是 tools=[save_preference, get_preferences],而它的 instructions 里第 3 步写的却是 Use search_hotels() to find suitable options。search_hotels 在前一个 cell 里定义了,但没有出现在 tools 列表里。提示词提到的函数和实际注册的工具集对不上,这是仓库当前代码里白纸黑字写着的两处,放在一起就是对不上,我们只指出这个事实,不替作者解释。
现象二:Notebook 重启后全没了
上面那个字典是普通的 Python 对象,作用域就是进程。它跨的是 session 而不是进程:agent.create_session() 新建一个 AgentSession 之后,工作记忆确实是空的,但 preference_store 还在,所以 notebook 里”Sarah 几周后回来”那个场景能读出东西。可一旦 kernel 重启,字典跟着归零。
这一点课程 README.md 在讲短期记忆时说得很直接:AgentSession 保留的上下文在会话结束或应用重启后不会持久化,需要跨会话保留的事实与偏好要放到数据库、向量索引或别的持久化存储里。字典这份实现只解决了”跨 session”,没有解决”跨进程”。
判定动作很简单:重启 kernel,只跑到定义工具那个 cell,然后 print(preference_store)。是 {} 就说明命中。
顺带说一处值得注意的落差:同目录 README.md 的 Available Implementations 一节把 13-agent-memory.ipynb 描述为用 Mem0 与 Azure AI Search 实现记忆,而 notebook 正文里的长期记忆实现是上面那个进程内 dict,注释也自称是模拟的持久化存储。README 描述与 notebook 实际代码在这一点上不一致,以你手上仓库版本的代码为准。
现象三:Cognee 那份,写入用 node_set,读取用 node_name
13-agent-memory-cognee.ipynb 的键更容易对错,因为写和读的参数名不是同一个词。
写入侧是这样的:
await cognee.add(developer_intro, node_set=["developer_data"])
await cognee.add(human_agent_conversations, node_set=["developer_data"])
await cognee.add(python_zen_principles, node_set=["principles_data"])
await cognee.cognify()
读取侧,那个只查原则子集的工具是这样写的:
from cognee.modules.engine.models.node_set import NodeSet
results = await cognee.search(
query_text=query,
query_type=SearchType.GRAPH_COMPLETION,
node_type=NodeSet,
node_name=["principles_data"],
)
三处要对齐:node_type 要传从 cognee.modules.engine.models.node_set 导入的 NodeSet 类,node_name 传的字符串列表要和写入时 node_set 里的字符串逐字相同,query_type 用的是 cognee.modules.search.types 里的 SearchType.GRAPH_COMPLETION。少传 node_type、或者把 "principles_data" 写成 "principles",检索范围就不是你想的那个子集。
同一个 notebook 里另一个工具 search_knowledge 不带 node_type 与 node_name,查的是整张图。所以判定动作是:先用不带 node 过滤的那种调用问一遍。全图能查到、限定子集查不到,就是键对不上;全图也查不到,说明数据根本没进图,往下看现象四。
现象四:每次跑到那个 cell,图就被清空了
Cognee 那份 notebook 在准备存储的 cell 里同时做了两件事——配置目录和清库:
DATA_ROOT = Path('.data_storage').resolve()
SYSTEM_ROOT = Path('.cognee_system').resolve()
DATA_ROOT.mkdir(parents=True, exist_ok=True)
SYSTEM_ROOT.mkdir(parents=True, exist_ok=True)
cognee.config.data_root_directory(str(DATA_ROOT))
cognee.config.system_root_directory(str(SYSTEM_ROOT))
await cognee.prune.prune_data()
await cognee.prune.prune_system(metadata=True)
这里有两个坑叠在一起。
清理时机:prune_data() 与 prune_system(metadata=True) 就写在配置目录之后,没有任何开关。也就是说,凡是从头顺序执行 notebook,或者只是想”重新配一下路径”而回头点了这个 cell,前面 cognee.add() 加进去的数据和 cognify()、memify() 建出来的图都会被清掉。之后你去 search,当然什么都读不出来。判定动作:查一下自己最后一次执行这个 cell 的时间,是不是排在 cognee.add() 之后。要保留数据就别重复执行它。
作用域:两个路径是 Path('.data_storage') 与 Path('.cognee_system'),相对路径经 .resolve() 展开,落点跟着当前工作目录走。在课程目录里起的 kernel 和在 <你的项目目录> 根目录起的 kernel,会各自建一份互不相干的库。判定动作:print(DATA_ROOT),看展开出来的绝对路径是不是你以为的那个。Windows 下这一点尤其容易踩——从编辑器直接打开工作区和先在命令行 cd 到章节目录再起 Jupyter,工作目录经常不是同一个;Linux/macOS 上同样成立,只是路径分隔符不同。(工作目录这一层是通用的运行环境行为,课程文档里没有专门说明,不算该项目的官方内容。)想要固定,把这两行换成绝对路径即可,展开逻辑就不再依赖启动方式。
还有两个前置条件写在这份 notebook 的 Prerequisites 里,缺了也会表现成”读不出来”:需要本地跑着 Redis 用于会话管理,以及 .env 里要有 CACHING=true(notebook 写明这是 Cognee 会话所必需的)。安装那行固定了版本:%pip install agent-framework azure-ai-projects azure-identity "cognee[redis]==0.4.0" -q,这个版本号是仓库当前 notebook 里写死的值,会随课程更新变动。
什么情况说明不是清理和作用域的问题
如果 DATA_ROOT 打出来正确、这一轮也没有重跑过 prune、全图检索仍然什么都没有,那要往前一步看 cognify() 有没有真的执行完——cognee.add() 只是入库,把文本变成图和向量的是 cognify(),memify() 是在已有图上再生成规则的一步。另外这两份 notebook 的凭据方式不同:主 notebook 用 DefaultAzureCredential,Cognee 这份用 AzureCliCredential,都要求 AZURE_AI_PROJECT_ENDPOINT 与 AZURE_AI_MODEL_DEPLOYMENT_NAME 就位。如果报错停在建 FoundryChatClient 这一层,那和记忆的键没有关系,是认证与环境变量的事。
三层记忆的键,分别是什么
把上面的东西收一下,两份 notebook 里”键”其实有三层,弄混了排查就会走岔:
| 记忆层 | 键是什么 | 作用域到哪 |
|---|---|---|
| 工作记忆 | agent.create_session() 返回的 AgentSession 对象本身,传给 agent.run(..., session=session) | 该 session 存活期间 |
| 长期记忆(主 notebook) | save_preference / get_preferences 的 user_id 入参 | 进程内的 preference_store 字典 |
| 长期记忆(Cognee) | 写入 node_set=[...]、读取 node_name=[...] 配 node_type=NodeSet | data_root_directory / system_root_directory 指向的目录 |
第一层换 session 必然读不出来,这是设计如此,notebook 专门用 new_session = agent.create_session() 演示了这一点;第二层和第三层换 session 应当读得出来,读不出来才是 bug。先分清你遇到的是哪一层,再往下查。
以上代码片段均逐字取自仓库中的这两个 notebook,我们没有运行过,以仓库最新代码为准。该课程持续更新,文中涉及的参数名、路径与写死的版本号随版本变动。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。