微软 AI Agent 入门课第 13 课的记忆结构:两种实现的抽象不同

2026-08-18

ai-agents-for-beginners 的第 13 课时,很容易被 README 那一长串记忆类型的定义带着走——working、short-term、long-term、persona、episodic、entity,一路读下来像在背名词。真正有信息量的地方不在那里,而在 13-agent-memory/ 目录下的两个 notebook:13-agent-memory.ipynb13-agent-memory-cognee.ipynb

这两份代码解决的是同一个场景:用户在第一次会话里说了一堆偏好,隔了一阵开一个新会话,agent 还能不能接住。但它们选的抽象完全不同,差异从记忆写进去的那一刻就分叉了。下面沿着代码走一遍。

分叉之前:两份 notebook 共享的那一半

两个 notebook 的上半截几乎一样,都是 Microsoft Agent Framework(notebook 里简称 MAF)的那套写法:

  • 都用 agent_framework.foundry 里的 FoundryChatClient,构造时传 project_endpointmodelcredential
  • 都用客户端对象的 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_dataprinciples_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,
)

SearchTypecognee.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、要 .envCACHING=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_textnode_set 过滤才对得上。代价是它带进来一整套依赖:Redis、额外的 API key、两个本地存储目录,以及 cognify()memify() 这两步预处理。

真要动手,先去把两份 notebook 里 @tool 那几段并排看一遍——两种抽象的全部差异,其实就浓缩在这几个函数签名里。


本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。