微软 AI Agent 入门课第 9 课的元认知:自评插在哪、评完给谁用
「让 Agent 评估自己」这句话听着没什么争议,落到代码里却马上要回答两个很具体的问题:这个自评动作插在哪一步执行,以及评出来的东西交给谁。ai-agents-for-beginners 的第 9 课 09-metacognition 给了答案,而且给了两个不一样的答案——它们在同一个 notebook 里,写法完全不同,能拿到的东西也完全不同。
下面顺着 09-metacognition/code_samples/09-python-agent-framework.ipynb 走一遍。这一课的 code_samples/ 目录下只有这一个 Python notebook,本文讲的全部是它,不涉及其它语言的写法。
先看这个 notebook 拿什么在跑
开头几格是环境。安装那格写的是 %pip install agent-framework azure-ai-projects azure-identity python-dotenv -q,markdown 里列的前置条件是两条:配好 Azure OpenAI 部署的环境变量,以及用 az login 完成 Azure CLI 登录。
接下来这格值得单独说一句,因为它决定了你会不会在半路上拿到一个语焉不详的报错:
endpoint = os.getenv("AZURE_AI_PROJECT_ENDPOINT")
deployment_name = os.getenv("AZURE_AI_MODEL_DEPLOYMENT_NAME")
missing = [k for k, v in {
"AZURE_AI_PROJECT_ENDPOINT": endpoint,
"AZURE_AI_MODEL_DEPLOYMENT_NAME": deployment_name
}.items() if not v]
缺哪个就把哪个的名字拼进 ValueError 里抛出来,不留到后面再炸。前面还有一行 dotenv.load_dotenv(),也就是说这两个变量既可以来自 .env 文件,也可以来自进程环境。Windows 上我更建议直接用 .env,因为 PowerShell 与 bash 设环境变量的语法不一样,走文件可以绕开这层差异——这是通用做法,不是课程官方给的指引;课程仓 00-course-setup/README.md 自己写明的是另一件事:多数 notebook 通过 AzureCliCredential / DefaultAzureCredential 拿 az login 的会话来认证,因此不需要 API key,.env 里放的是 endpoint 与部署名;同一份文档也提醒,少数课程与可选集成仍会用到 key,具体看每一课自己列的前置条件。
客户端的构造是 FoundryChatClient(project_endpoint=endpoint, model=deployment_name, credential=DefaultAzureCredential())。另外顶部还有一行 logging.getLogger("agent_framework.foundry").setLevel(logging.ERROR),把这个 logger 的级别压到 ERROR。
第一个自评插入点:写在 instructions 里
工具是两个,用 @tool(approval_mode="never_require") 装饰。主查询 get_flight_times 命中不了就 raise Exception(f"404: No flights found for {destination} in primary system");备用的 get_flight_times_backup 用 backup_flights.get(destination, "No flights found for ... Please try again later.") 兜底。注意这个不对称:主的会抛异常,备的永远不抛,查不到也是返回一段普通字符串。
然后 client.as_agent(...) 建 agent,instructions 里编了号:先试主系统,主系统 404 就承认错误再试备用,全程向用户交代发生了什么,两个系统都失败就道歉。最后一句是这样:
After each response, briefly evaluate whether your answer was complete and helpful.
这就是第一个自评插入点——它没有单独的执行阶段,它就是提示词的最后一句,跟着同一次 agent.run() 一起被生成出来。这意味着自评结论和给用户的正文混在同一段文本里,你的调用方拿到的是一个字符串,里面既有航班信息也有”我觉得这个回答还算完整”。想在程序里拿到自评结论,得自己再解析一遍。
顺带把两处放在一起看:instructions 第 4 条设定的是”两个系统都失败”的场景,而备用工具在代码里根本不会以异常形式失败,它只会返回那句 Please try again later.。也就是说这条分支能不能被触发,取决于模型能不能从一句普通返回文本里读出”这其实是失败”。仓库里没有为这种情况写额外的判定逻辑。
第二个自评插入点:另起一个 agent,跑第二遍
第二种写法是把评估拆成独立一步。notebook 里又建了一个 agent,名字叫 ResponseEvaluator:
evaluation_agent = client.as_agent(
tools=[get_flight_times, get_flight_times_backup],
name="ResponseEvaluator",
instructions="""You are a quality evaluator for travel agent responses.
Given a travel question and the agent's response, evaluate:
1. Completeness: Did it answer all parts of the question? (1-5)
2. Accuracy: Is the information correct? (1-5)
3. Helpfulness: Would a traveler find this useful? (1-5)
Provide a brief evaluation with scores and one suggestion for improvement.""",
)
三个维度,每个维度一个分档区间,最后要一条改进建议。1-5 是仓库这段示例提示词里写的取值区间,不是什么框架规定。
然后把待评的内容拼成 eval_prompt 这个 f-string,再 await evaluation_agent.run(eval_prompt)。到这里,自评的位置就很清楚了:它插在主 agent 的一次完整 run() 之后,是独立的第二次模型调用,输入是「问题 + 上一轮回答」拼出来的文本。
有一处值得你在照抄之前看清楚。这个 notebook 里前面跑了两次测试:先问 Paris(命中主系统),再问 Berlin(只在备用系统里有),两次都赋给同一个变量 response。而 eval_prompt 的第一行是写死的 Question: What flights are available to Paris?,插值进去的却是 {response}——此刻它保存的是 Berlin 那次的结果;而这一格里紧挨着的注释写的是 # Evaluate the agent's response from Test 1。问题行、被插值的变量、注释说明,这三处对不上。这不是什么大事故,但如果你把这段结构原样搬到自己的流水线里,评估器收到的是哪一轮的回答,值得先确认一遍。
另外,evaluation_agent 是带着 tools=[get_flight_times, get_flight_times_backup] 建的,评估器手上有和被评者一样的两个工具。仓库里没有解释这个选择,我也不替它解释。
评出来的东西,在这一课里去了哪
答案很直白:print("=== Self-Evaluation ===") 然后 print(evaluation)。
第 9 课的评估结果是一段自由文本,没有 schema,没有被解析成字段,没有回写进任何状态,也没有触发重跑。课程末尾的 summary 把这一课归纳为自反思、带 fallback 的错误恢复、独立评估器三件事——它演示的是这三件事长什么样,不是一套可以直接接进 CI 的评估管线。
同一课的 README 里那个 HotelRecommendationAgent 也是类似的分寸。reflect_on_choice() 读 self.previous_choices[-1],拿 get_user_feedback() 的模拟反馈,如果是 "bad" 就把新策略追加进 self.corrected_choices,然后返回一句 f"Reflecting on choice. Adjusting strategy to {new_strategy}."。但请留意示例最后三步:第三步是 agent.recommend_hotel(hotels, 'highest_quality')——策略是调用方手写传进去的,reflect_on_choice() 本身并没有改变任何会影响下一次推荐的字段,corrected_choices 也没有被 recommend_hotel() 读取。反思的结论和真正生效的参数之间,这段示例里还隔着一个人。
想让评估结果真的”被用”,课程里有现成的样子
同一个仓库的第 16 课给了对照。16-deploying-scalable-agents/code_samples/16-python-agent-framework.ipynb 里有个 evaluation_gate,签名是 async def evaluation_gate(test_cases: list[dict], threshold: float = 0.8) -> bool。它遍历 TEST_CASES,每条用 score_response(result.text, case["expected"]) 打分——那个打分函数做的是关键词命中率,hits / len(expected_keywords);单条达标线在函数体里写死成 s >= 0.5,只有汇总那一步用了 threshold 参数,最后 return pass_rate >= threshold。上面那个 0.8 与 0.5 是仓库当前代码里的取值,随版本可能变动。
差别不在于哪种更高级,而在于返回类型:第 9 课返回给你一段话,第 16 课返回一个 bool,调用处接成 gate_passed,可以直接拿去决定发不发布。评估输出要被程序消费,就得先有一个能被 if 判断的东西。
这个 gate 还有两处仓库自己写明的边界,照抄前值得知道:一是那格 markdown 写明打分器是”简单的关键词重合检查”,只为让 notebook 自包含,生产里应换成 LLM-as-judge 或框架自带的 evaluator;二是代码注释写着 # Default agent (used by the evaluation gate, which does not route).——support_agent = agent_for(SMALL_MODEL),也就是说被 gate 打分的是固定模型档位的那个 agent,而线上请求走的是 handle_support_request 里的路由。你门禁住的东西和你真正服务的东西不完全是同一个。
.agents/skills/deploying-scalable-agents/SKILL.md 里还给了另一种”评估结果被消费”的形态:@tool(approval_mode="always_require"),用在大额退款这类动作上。它不打分,它直接把动作卡在那里等人签。第 9 课那两个工具用的是 never_require,是另一头。
落到你自己的流程上
把上面这些串起来,第 9 课其实给了三种自评位置,选哪种取决于你想拿到什么:
写进 instructions 最省事,一次调用搞定,代价是结论和正文黏在一起,程序拿不到干净的信号;另起一个评估 agent 跑第二遍,能拿到独立的一段评价,但它默认还是自由文本,你得自己定输出格式才能解析;把评估做成返回布尔值的 gate,才谈得上”被用”,代价是你得先有一个测试集和一个打分函数——第 16 课那个关键词命中率就是最省事的一版,仓库自己也说了它是权宜之计。
最后重复一句 notebook 里的前置条件:这些示例都会真的调用云端服务,az login 的身份、endpoint 与部署名都指向你自己的资源。写评估循环时尤其要注意,评估器是额外的一轮模型调用,测试集有多少条就多跑多少次。这个课程持续更新,上面涉及的文件路径、装饰器参数、函数签名与默认值都随版本变动,以仓库最新内容为准。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。