微软 AI Agent 入门课第 12 课:summarize_preferences 落在五块里的哪块
多轮对话跑到第五轮,用户忽然说”日期改了,我改成十月去”。这时候你要决定:前面四轮里哪些东西可以扔,哪些必须留。扔错了 Agent 会忘掉预算,留错了它会拿着旧日期继续查机票。
问题是——你连”要扔的是哪一块”都还没定义清楚,就没法写这个裁剪逻辑。ai-agents-for-beginners 第 12 课 12-context-engineering/ 的开头做的正是这件事:先把上下文拆开,再谈怎么管。这篇就沿着这一课的 README 和它唯一那个 notebook 走一遍,看拆分维度落到代码里分别是哪一行。
五类拆分,先记住它们的边界不一样
12-context-engineering/README.md 的 Types of Context 一节列了五类:Instructions、Knowledge、Tools、Conversation History、User Preferences。数量我回源数过,就是五个 bullet。
课程正文对这五类的界定各不相同,但真正有用的是它们的生命周期不一样:
- Instructions:README 归入这一类的是 prompt、system message、few-shot 示例,以及工具的描述文本。它写明这里是 prompt engineering 与 context engineering 交汇的地方。这一块通常是静态的。
- Knowledge:事实、从数据库检索到的信息、长期记忆。README 在这一条里点名了 RAG。
- Tools:外部函数、API 和 MCP Server 的定义,以及调用它们拿回来的结果。注意后半句——工具返回值也算上下文,很多人只算工具定义。
- Conversation History:与用户的对话本身,会随时间线性变长。
- User Preferences:从用户身上长期学到的偏好。
拆成五类的意义在于,压缩策略不能一视同仁:对话历史可以摘要掉,用户偏好摘要掉就等于失忆。这一课的代码示例,恰恰就卡在这两类的交界处。
这一课只有 Python 一份实现
先把前置条件说清楚,这一步经常被跳过。
12-context-engineering/code_samples/ 目录下只有两个文件:12-chat_summarization.ipynb 和 vacation_agent_scratchpad.md。别的课常见的 NN-dotnet-agent-framework 那一份,这一课里我们没有找到。要跟着跑,只有 Python 这条路。
notebook 第一个代码单元装的是 agent-framework、azure-ai-projects、azure-identity、python-dotenv。随后读两个环境变量 AZURE_AI_PROJECT_ENDPOINT 与 AZURE_AI_MODEL_DEPLOYMENT_NAME,缺任何一个就 raise ValueError,报错文案里写明可以放 .env 文件、也可以放 shell 环境变量。仓库根的 .env.example 给了这两项的样例值,其中部署名一项写的是 gpt-5-mini——这只是仓库里的示例值,不是要求。
客户端用的是 FoundryChatClient(project_endpoint=..., model=..., credential=DefaultAzureCredential())。走的是 Azure 的凭据链,不是往代码里塞 key。Windows 侧要补一句通用做法(这不是课程官方内容):如果你选择走 shell 环境变量而不是 .env,环境变量要设在真正跑 kernel 的那个进程上,在另一个终端窗口里设是不会被这个 notebook 读到的;这类调用会把对话内容发到云端并产生费用,密钥与数据边界请自己评估。
摘要工具落在哪一块:Conversation History 进,User Preferences 出
这是本篇的落点。notebook 里的摘要工具定义只有四行:
@tool(approval_mode="never_require")
def summarize_preferences(conversation_notes: str) -> str:
"""Summarize accumulated user preferences into a compact format."""
return f"[SUMMARY] User preferences recorded: {conversation_notes}"
对着五类拆分看,这个函数的位置就很清楚了:
入参 conversation_notes 是 Conversation History 那一块,出参是打了 [SUMMARY] 前缀的 User Preferences 记录。它做的是跨类搬运——把会不断变长的那一类,转成体积稳定的那一类。
但有一处必须说破,不然读者会误解这段代码:压缩不是这个函数做的。函数体只有一次字符串格式化,没有任何截断、去重或摘要逻辑。真正生成摘要文本的是模型——它按 instructions 的要求把对话浓缩成 conversation_notes 再传进来。这个工具承担的是”落点”和”标记”,不是”算法”。你把它抄进自己的项目里,如果不写对应的 instructions,它什么也不会发生。
对应的 instructions 也在同一个 notebook 里,分成两段带标题的规则。前一段 CONTEXT MANAGEMENT RULES 的第一条要求”收集到若干用户偏好之后调用 summarize_preferences() 记录一份紧凑摘要”;后一段 PLANNING PROCESS 一共四条,第二条是”用这个工具摘要偏好”,第四条是”偏好变化时更新摘要”。工具挂载写在 tools=[summarize_preferences]。
装饰器上的 approval_mode="never_require" 是仓库示例里的取值。这一课的文件里没有解释它的语义,解释写在别处:02-explore-agentic-frameworks/code_samples/02-python-agent-framework.ipynb 的说明文字写明这个取值表示工具自动执行、不需要用户确认;04-tool-use/code_samples/04-python-agent-framework.ipynb 则给出了 approval_mode="always_require" 的用法,并说明这个参数控制工具调用在执行前是否需要人工审批。也就是说,第 12 课这个摘要工具走的是”不拦”这一档——真实业务里要不要沿用,得看你的工具会不会产生副作用。
对话历史这一块,在代码里长什么样
第五类里最容易被忽略的是”它到底存在哪”。notebook 用的是会话对象:
session = agent.create_session()
response = await agent.run("I'm planning a trip to Japan. I love sushi, temples, and photography.", session=session)
response = await agent.run("My budget is $3000 and I'll be traveling solo for 10 days in April.", session=session)
response = await agent.run("Based on everything I've told you so far, what's the one thing you'd recommend I not miss?", session=session)
Conversation History 这一块就寄生在这个 session 上:每次 agent.run 带同一个 session,历史就累积一轮。文中的具体金额、天数都是仓库示例对话里的原文,不是什么推荐配置。
再往下几轮,notebook 故意安排了一次改主意——第五轮把出行时间从四月改成十月看秋色,第六轮要求复述完整计划。这一手正对着 README 后半段列的失效模式里的 Context Clash:早先的错误假设留在上下文里没清掉,和后来的新信息打架。README 给的处置是 pruning(新信息到达时删掉过时的、冲突的)和 offloading(另开 scratchpad 空间处理)。开头那个”日期改了”的场景,属于这一类,而不属于要靠摘要解决的 Context Distraction。
仓库里那个 scratchpad 文件,和 notebook 是对不上的
code_samples/ 下还躺着一个 vacation_agent_scratchpad.md。它的结构是两个二级标题:## User Preferences 和 ## Completed Tasks,每条记录前面带一个方括号时间戳。文件本身是仓库里的一份静态素材,它是怎么来的,仓库里没有找到相关说明。
把它和 notebook 并排放,会发现两处对不上,这两处都是白纸黑字:
其一,notebook 里的 summarize_preferences 不写任何文件,它只 return 一个字符串。整个 notebook 里没有出现写文件的代码。
其二,notebook 的 setup 单元 from datetime import datetime 和 from pathlib import Path 都 import 了,但后续所有代码单元里都没有再用到这两个名字。而 scratchpad 文件里每条记录恰好都带时间戳。
notebook 结尾的 Next Steps 第一条写的就是 “Implement a full scratchpad tool with file-based persistence”。也就是说:落盘版的 scratchpad 是这一课留的作业,不是它已经实现的东西。notebook 里既没有 open()、也没有 Path.write_text() 这类调用,所以这个 md 文件不是它产生的。这一点先弄清楚,能省掉一轮”为什么我这边没生成文件”的排查。
顺带说一句 README 对 scratchpad 与 memories 的分工:scratchpad 管单次会话内的记录,跨会话的那部分归 memories。这两个词在中文语境里经常被混着叫,课程里是分开的。
拆完之后,怎么知道哪一块真的被换掉了
README 有一节 Inspecting Context,讲的是改完策略之后怎么确认下一次模型调用真的收到了预期的东西。它给的那个调试问句很实用:Agent 是加载了太多上下文、加载了错的上下文,还是缺了它需要的上下文。
关键在于它明确说了:回答这个问题不需要记录原始 prompt、工具输出或记忆内容。生产环境应该记的是计数、id、哈希和策略标签——比如选择环节记候选了多少条、选中多少条、是哪条规则把其余的过滤掉了;压缩环节记来源区间或 trace id、摘要 id、压缩前后的估算 token 数,以及原文有没有被排除出下一次调用;记忆与 RAG 环节记文档 id、记忆 id、分数和脱敏状态,而不是检索到的全文。
这一条对 User Preferences 那一块尤其要紧——它恰恰是最容易含敏感信息的一类,而 summarize_preferences 的返回值里原样带着用户说过的话。真要在生产里加日志,README 这一节的取向是记 id 不记正文。
课程也把边界写在了那一节的结尾:目标不是留住更多上下文,而是留下足够的证据,让开发者能判断跑的是哪条策略、有没有按预期改变下一次模型调用。
以上代码片段均原样取自仓库文件;该项目持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,以仓库最新代码为准。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。