微软 AI Agent 入门课第 13 课的记忆结构:两种实现的抽象不同
翻 ai-agents-for-beginners 的第 13 课时,很容易被 README 那一长串记忆类型的定义带着走——working、short-term、long-term、persona、episodic、entity,一路读下来像在背名词。真正有信息量的地方不在那里,而在 13-agent-memory/ 目录下的两个 notebook:13-agent-memory.ipynb 和 13-agent-memory-cognee.ipynb。
这两份代码解决的是同一个场景:用户在第一次会话里说了一堆偏好,隔了一阵开一个新会话,agent 还能不能接住。但它们选的抽象完全不同,差异从记忆写进去的那一刻就分叉了。下面沿着代码走一遍。
分叉之前:两份 notebook 共享的那一半
两个 notebook 的上半截几乎一样,都是 Microsoft Agent Framework(notebook 里简称 MAF)的那套写法:
- 都用
agent_framework.foundry里的FoundryChatClient,构造时传project_endpoint、model、credential - 都用客户端对象的
as_agent(...)造 agent,用agent.create_session()拿会话(两份 notebook 里承接FoundryChatClient的变量名不同,一个叫client,一个叫provider) - 长期记忆都不是框架内建的,都是靠
@tool函数把 agent 接到外部存储上
第 13 课 README 对 AgentSession 的定位写得很直白:它是框架内建的短期记忆,只要复用同一个 session,上下文就在;session 结束或者应用重启,这段上下文不保留。所以两个 notebook 演示长期记忆的手法是同一套动作——agent.create_session() 开第二个 session,看 agent 还能不能答上来。
有两处不一样的细节值得先记下来,因为它们会影响你本地能不能跑通:13-agent-memory.ipynb 用的凭据类是 DefaultAzureCredential,而 13-agent-memory-cognee.ipynb 用的是 AzureCliCredential。两份 notebook 的前置条件里都写了要 az login,至于这两个类各自认哪些凭据来源,该课程仓里没有展开说明,需要去 azure-identity 自己的文档确认。另外,前一份里的 @tool 函数是普通同步 def,后一份里全是 async def——函数体里对 cognee.search() 的调用是带 await 的。
顺带说一句:第 13 课这个目录下只有这两个 Python notebook,我们没有在该目录里找到对应的 .NET 版实现。这一课的双实现指的是两种记忆后端,不是两种语言。
数据写进去时是什么形状
第一份:一个 dict[str, list[str]]
13-agent-memory.ipynb 的长期存储就是一行声明:
# --- Persistent preference store (simulated) ---
preference_store: dict[str, list[str]] = {}
注释里自己标了 simulated。写入靠一个工具函数:
@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}"
数据模型就到此为止:一个用户 id 映射到一串自由文本。没有 schema,没有字段,没有去重,也没有更新和删除——append 是只进不出的。同一条偏好说两遍就存两条,前后矛盾的两条偏好(比如预算改了)会并排躺在同一个 list 里,谁也不覆盖谁。
值得注意的是 @tool(approval_mode="never_require") 这个参数:notebook 里三个工具函数全都显式关掉了人工确认。演示流程里这么写省事,但它意味着模型可以自行往存储里写东西,这一点在你把 dict 换成真实数据库时要重新掂量。
第二份:先 add 打标签,再 cognify 成图
13-agent-memory-cognee.ipynb 的写入是三步。第一步是配存储根目录:
DATA_ROOT = Path('.data_storage').resolve()
SYSTEM_ROOT = Path('.cognee_system').resolve()
cognee.config.data_root_directory(str(DATA_ROOT))
cognee.config.system_root_directory(str(SYSTEM_ROOT))
两个目录名都是相对路径,resolve() 是按 notebook 当前工作目录展开的。第二步入库:
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()
差别就在 node_set 这个参数上。原始文本进来的时候就被贴了归属标签,notebook 里分成 developer_data 和 principles_data 两组——开发者画像和历史对话归一组,Python 原则归另一组。cognify() 之后,notebook 开头的自述是把非结构化文本变成一张可查询的知识图谱,背后由向量 embedding 支撑;第 13 课 README 的”Available Implementations”一节对这份 notebook 的一句话概括也是同一个口径。至于 cognify() 内部怎么抽实体、怎么建边,课程仓里没有找到相关说明,那是 Cognee 自己的实现。
第三步是 await cognee.memify(),notebook 里对它的描述是分析知识图谱、生成规则,在已有的图上派生出更多事实与关系。仓库里给的就是这一句加一次调用,更细的行为没有展开说明。
还有一个 visualize_graph('./cognee_graph.html'),把抽出来的实体和关系导成一个 HTML 文件。这个东西对调试很实用——第一份 notebook 里你想知道存了什么,只能 print 那个 dict。
取出来时走的是哪条路
这才是两份实现真正拉开距离的地方。
第一份的召回就是查表:
@tool(approval_mode="never_require")
def get_preferences(
user_id: Annotated[str, "User identifier"],
) -> str:
"""Retrieve all saved travel preferences for a user."""
prefs = preference_store.get(user_id, [])
if not prefs:
return f"No saved preferences for {user_id}."
return "Saved preferences:\n- " + "\n- ".join(prefs)
按 user_id 精确取,取到的是这个用户名下的全部偏好,拼成一段带项目符号的纯文本丢回给模型。没有相关性排序,没有 top-k,没有按当前问题过滤。所以召回粒度就是”一个用户”——记的东西越多,塞进上下文的无关内容就越多,筛选完全交给模型自己。notebook 的 instructions 里也是这么安排的:让 agent 一上来先调 get_preferences()。
第二份的召回是一次图查询:
results = await cognee.search(
query_text=query,
query_type=SearchType.GRAPH_COMPLETION,
)
SearchType 从 cognee.modules.search.types 导入,notebook 里两个工具用的都是 GRAPH_COMPLETION 这个取值。真正体现数据模型价值的是第二个工具 search_principles,它多传了两个参数:
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_set 标签,在这里变成了检索的过滤条件。agent 拿到”这个问题是问 Python 最佳实践的”这个判断之后,可以只在 principles_data 这个子集里找,不会把开发者画像和历史对话一起捞上来。这就是两种抽象最实的差别:第一份的检索键是用户,第二份的检索键是自然语言问题加上一个可选的子集范围。
两份 notebook 的返回值都是字符串——Cognee 那边 cognee.search() 返回一个列表,工具函数里直接 str(results) 转成字符串再回给模型;空结果分别返回 "No relevant knowledge found." 和 "No relevant principles found."。
以上代码片段均照抄自仓库对应的 notebook 单元(个别处为节省篇幅略去了同一格里的建目录语句),未经实测,以仓库最新代码为准。
两个容易踩的地方
第一份里的 search_hotels 没有被注册。 notebook 里定义了三个工具,但建 agent 时是这么写的:
travel_agent = client.as_agent(
tools=[save_preference, get_preferences],
...
)
而同一个 as_agent() 调用里的 instructions 明明写着 3. Use search_hotels() to find suitable options...。工具列表里没有它,提示词里点名了它——这两处摆在一起就是不一致,我们只指出这个现象,不猜作者意图。你照着改的时候留意一下。
顺便看一眼 search_hotels 内部的匹配逻辑:它对 name、location、tags 做子串包含判断,然后有一行兜底 matches = hotels[:3]——查不到时返回列表的前三条。这种”没有结果就给点东西”的写法在演示里无所谓,接到真实数据上就是在给模型喂不相关的候选,而模型并不知道这是兜底结果。
第二份每次跑都会清空。 配置存储根目录的同一格里,紧接着两行就是:
await cognee.prune.prune_data()
await cognee.prune.prune_system(metadata=True)
这是 notebook 为了可重复演示做的重置。你要是在它上面继续加自己的数据,重跑这一格就没了。
第二份的前置条件也重得多:除了 Foundry 的 endpoint 和部署名,还要一个 LLM_API_KEY、要 .env 里 CACHING=true(notebook 写明这是 Cognee session 所必需的),以及一个本地跑着的 Redis 用于 session 管理。Cognee 是带 redis 这个 extra 安装的,并且在 %pip install 那一格里钉了具体版本号——这是仓库当前 notebook 里的钉版,随课程更新会变动,别当成长期有效的版本约束。
Windows 侧的两点
Redis 那条前置,notebook 里只给了一条 docker run 命令这一种装法,没有写 Windows 上的其它方案。在 Windows 上走 Docker 这条路,通常意味着要先准备好 Docker Desktop 与 WSL2 环境——这是 Windows 上跑容器的通用前提,不是该课程仓写明的内容;其它安装方式仓库里没有找到相关说明,需要自己决定。相比之下第一份 notebook 完全没有本地服务依赖,只要 az login 加两个环境变量,Windows 上开箱就少一层麻烦。
另一点是路径。Path('.data_storage').resolve() 展开的是 notebook 进程的当前工作目录,Windows 上如果你是从别处启动的 Jupyter 内核,这两个目录会落在你没预期的地方。想确认落到哪了,把 DATA_ROOT 打出来看一眼最省事。
README 和 notebook 对不上的一处
第 13 课 README 的”Available Implementations”一节写的是:13-agent-memory.ipynb 用 Mem0 和 Azure AI Search 配合 Microsoft Agent Framework 实现记忆。但打开这个 notebook,里面既没有 Mem0 也没有 Azure AI Search,长期存储就是上面那个进程内 dict;notebook 自己的 Next Steps 里反过来写着”把内存 dict 换成数据库或向量存储(例如 Azure AI Search)”。
两处放在一起,结论只有一句:README 描述的是这一课讲到的技术,notebook 里实现的是最小可跑的版本。你按 README 的描述去找 Mem0 的代码是找不到的。这个课程仓持续更新,具体以仓库最新内容为准。
什么时候这个差异会咬到你
不做优劣判断,只说仓库写明的边界落在哪:
如果你的记忆就是”每个用户几条明确的偏好”,键是已知的、量是可枚举的,那么第一份的形状够用——它的检索是确定性的,你能预知模型会看到什么。它的边界在于记忆一多就没法筛,而且没有更新与删除路径。
如果你要记的是文档、历史对话这类没有天然主键的东西,问题也不是”取谁的偏好”而是一句自然语言,那么第一份的 dict.get() 根本无从下手,第二份的 query_text 加 node_set 过滤才对得上。代价是它带进来一整套依赖:Redis、额外的 API key、两个本地存储目录,以及 cognify() 与 memify() 这两步预处理。
真要动手,先去把两份 notebook 里 @tool 那几段并排看一遍——两种抽象的全部差异,其实就浓缩在这几个函数签名里。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。