微软 AI Agent 入门课第 10 课:报销 demo 里可观测与评估落在哪几行
第 10 课 10-ai-agents-production/README.md 的标题是 Observability & Evaluation,正文里给了一段 OpenTelemetry 的接入示例,又给了一段 Langfuse 手工建 span 的示例,然后写了一句:本章的 example notebook 会演示怎么给 MAF(Microsoft Agent Framework)agent 做 instrument,链接指向 code_samples/10-expense_claim-demo.ipynb。
不少人是照着这句话打开那个 notebook 的,然后在里面搜 tracer 搜不到。这篇就把这个 notebook 从头到尾过一遍,把「可观测」「错误处理」「评估」这三块在代码里的实际位置指出来——包括其中一块并不在这个文件里。
先把这条链路走通
10-expense_claim-demo.ipynb 做的事是:读一张本地收据图,抽出报销条目,再生成一封发给财务的报销邮件。代码顺序是这样的。
第一段是环境变量。notebook 从 dotenv.load_dotenv() 之后读两个变量:AZURE_AI_PROJECT_ENDPOINT 和 AZURE_AI_MODEL_DEPLOYMENT_NAME,然后拼一个 missing 列表,任何一个为空就 raise ValueError。仓库根目录的 .env.example 里这两个键都在。
第二段建客户端,用的是 agent_framework.foundry 里的 FoundryChatClient,凭据是 azure.identity 的 DefaultAzureCredential()。
第三段是数据结构:一个 Expense(date / description / amount / category 四个 pydantic 字段)和一个 ExpenseFormatter,后者的 parse_expenses() 负责把 date|description|amount|category 这种竖线分隔、分号断条的字符串解析成 Expense 列表。
第四段是两个工具函数,都挂了 @tool(approval_mode="never_require"):generate_expense_email 负责算总额、拼邮件正文;load_receipt_image 负责把图片读成 base64 的 data URI 返回。
第五段用 client.as_agent(...) 建了 OCRAgent 和 EmailAgent 两个 agent,各自带一个工具。最后一段用 WorkflowBuilder(start_executor=ocr_agent).add_edge(ocr_agent, email_agent).build() 串成一条边,再 workflow.run(prompt, stream=True),在 async for 里按 event.type == "output" 且 isinstance(event.data, AgentResponseUpdate) 过滤事件、打印 update.text。
链路很短:两个 agent、一条边、两个工具。下面按三块拆。
可观测这一块:只有日志级别和一次流式打印
这个 notebook 里跟可观测沾边的代码,一共两处。
第一处在导入区的最前面,是这两行:
import logging
logging.getLogger("agent_framework.foundry").setLevel(logging.ERROR)
注意它的方向——把 agent_framework.foundry 这个 logger 的级别抬到 ERROR,也就是把这个客户端在 INFO/WARNING 级别上的输出压掉了。作为演示 notebook 这样做输出会干净,但如果你是照着这个文件往自己的工程里搬,这一行是要先删掉的:它压掉的正好是你排查问题时想看的那一层。
第二处在最后那个事件循环里,靠 update.author_name 是否变化来判断该不该打一条分隔线,然后 print(update.text, end="", flush=True)。它给你的是「现在哪个 agent 在说话、说了什么」这一层可见性,仅此而已。
至于 README 里给的那段:
from agent_framework.observability import get_tracer, get_meter
以及 Langfuse SDK 那段 langfuse.start_span(name="my-span"),在 10-expense_claim-demo.ipynb 里都没有出现。README 写着这个 notebook 会演示 instrument MAF agent,而 notebook 里没有对应代码,这两处是对不上的——把这个事实摆出来就够了,我们不替作者解释原因。
同一个 code_samples/ 目录下还有一个 10-python-agent-framework.ipynb,那个文件的 markdown 里明说 observability 这块是「adding timing and logging」,代码就是 start_time = time.time()、跑完算 elapsed 再打印,并写明「In production you would integrate with OpenTelemetry or a similar tracing backend」。所以第 10 课的两个 Python notebook,实际给到的可观测都停在计时与打印这一层,OTel 那段是 README 的示例代码,不是任何一个 notebook 的既有实现。
错误处理这一块:三个不同的去向
这个 demo 值得看的地方在这里。它对错误的处理有三处,而这三处的去向完全不同,混在一起容易踩坑。
第一处是中断。 环境变量缺失时 raise ValueError,整个 cell 停住,附带的消息里会把缺的键名列出来。这是最直白的一种。
第二处是落到 stdout。 ExpenseFormatter.parse_expenses() 先按分号切条,再按竖线把每条切成四段,只有 len(parts) == 4 才往下走;构造 Expense 的那一步单独 try,捕获 ValueError 之后:
print(f"[LOG] Parse Error: Invalid data in '{expense_str}': {e}")
然后继续下一条。这里其实有两层跳过,得分清。落进 except 的,典型是 float(amount.strip()) 转不动的情况——金额那一栏不是数字,这条被跳过,痕迹留在 stdout 那个 [LOG] 前缀上。而更安静的是另一层:竖线段数不等于四的那条,if len(parts) == 4 这个判断直接把它漏过去了,连 [LOG] 都不打,代码里没有对应的 else 分支。
两层跳过的后果是一样的:这条报销不进 expense_list,generate_expense_email 算总额时它就不在里面,而返回的邮件正文本身完全正常,看不出少了一笔。还有一点值得一并留意:Expense.date 声明的类型是 str,Field 的 description 里写着期望 dd-MMM-yyyy 格式,但 description 只是描述,不是校验——日期写成别的样子不会触发上面任何一层跳过,它会原样进到邮件正文里。这几处是这个 demo 里最容易被漏掉的地方。
第三处是把错误交给模型。 load_receipt_image 的 except 分支是这样收尾的:
except Exception as e:
error_msg = f"[LOG] Error loading image '{image_path}': {str(e)}"
print(error_msg)
return error_msg
它 return 了错误字符串,而不是抛出。工具调用的返回值会回到模型手里,所以图片读不到的时候,OCRAgent 收到的不是异常,是一段以 [LOG] Error loading image 开头的文本,它接下来怎么处置这段文本由模型自己决定。这个写法在 demo 里没问题,但要落到生产就得想清楚:工具函数吞掉异常并返回错误文本,等于把故障判定权交给了模型。
除了这三处,还有两道 prompt 层的兜底:generate_expense_email 在解析结果为空时返回 "No valid expenses found to include in the email.",OCRAgent 的 instructions 里也写了一句「If no expenses are found, return ‘No expenses detected’」。前者是代码保证的,后者只是写给模型的要求,两者的可靠性不是一回事。
评估这一块:不在这个 notebook 里
10-expense_claim-demo.ipynb 里没有任何评估代码。这个 demo 从头到尾没有对输出做过打分或断言,跑完就是把邮件正文打印出来。
第 10 课的评估代码在同目录的 10-python-agent-framework.ipynb,形态是「用第二个 agent 当评审」:client.as_agent(name="ResponseEvaluator", instructions=...),instructions 里列了 Completeness / Accuracy / Helpfulness / Overall Score 四项、各按 1-5 打分,然后 await evaluator.run(f"Evaluate this travel agent response:\n\n{response}")。README 侧则把评估分成 offline evaluation(用有标准答案的数据集、可以放进 CI/CD)和 online evaluation(在真实流量上看)两类,并写明通常先做 offline。
所以如果你是奔着「评估怎么写」去翻第 10 课的,别停在报销 demo 上,那份代码不在那儿。
demo 自己标了一条限制,不要跳过
10-expense_claim-demo.ipynb 在跑 workflow 那个 cell 之前,有一段 markdown 的 Note,原文写明:这个 workflow 目前是把收据图片当 base64 文本传的,大多数 chat model 不会把它当成图片处理,而且可能超出模型的上下文窗口;建议改用 Azure AI Vision(或别的 OCR 工具)先做 OCR、只把抽出来的文本传进去,或者重构成用 image_url 形式的消息发图。仓库示例里点名的那个模型是 gpt-5-mini,那只是仓库里写的示例值。
这条 Note 的意思很明确:这个 notebook 的 OCR 路径本身就是被作者标记为有问题的。谁要拿它当报销自动化的起点,第一件事就是把图片传输方式换掉,而不是先去接 tracer。
Windows 上怎么确认自己这份能跑通
仓库里带了一个 PowerShell 的 notebook 批量执行脚本 scripts/validate-notebooks.ps1,它会用 nbconvert 无头跑课程里的 Python notebook(.NET 的默认排除,要跑得加 -IncludeDotnet),打出 PASS/FAIL 矩阵,并把执行副本、每个 notebook 的日志和 results.json 写到输出目录。脚本头部的 .EXAMPLE 里给的用法是:
pwsh scripts/validate-notebooks.ps1
pwsh scripts/validate-notebooks.ps1 -Filter '08-*' -Timeout 600
pwsh scripts/validate-notebooks.ps1 -Python "C:/path/to/python.exe" -List
-Filter 的说明写的是「applied to the notebook’s repo-relative path」,所以换个通配符就能只跑某一课。-Python 这个参数在 .agents/skills/testing-course-samples/SKILL.md 里补了一句用途:当 python 不在 PATH 上时用它显式指定解释器,并点名了 Windows Store 的 python 别名这种情况——Windows 上这一脚基本人人踩过。前置条件那节还写明要先完成 az login,因为示例走的是 Entra ID 免密钥凭据。
Linux/macOS 侧同样是 pwsh 起这个脚本。脚本头部写明 -Python 默认走自动探测(依次试 python、python3、py -3),-OutputDir 的默认值是 $env:TEMP 下的一个子目录——这两项跨平台时的表现我们没有验证过,拿不准就显式把两个参数都传上。
以上命令均为仓库脚本自带的示例,我们没有执行过,请以仓库最新内容为准。
回到开头那个问题
把三块并排看:这个 demo 里可观测只有一行日志级别设置加一次流式打印,错误处理散在三处且去向各不相同(中断 / 打到 stdout / 返回给模型),评估干脆不在这个文件里。README 描述的那套生产化能力,是这一课的目标,不是这个 notebook 的现状。
看这类课程仓的 demo 时,这个区分挺要紧:章节 README 讲的是应该有什么,code_samples/ 里的文件讲的是这一版实际写了什么,两者对不上是常态。真正能拿走的东西,是 parse_expenses 那种「跳过坏数据继续跑」和 load_receipt_image 那种「返回错误字符串」的取舍——你在自己的 Agent 里迟早也要做同样的决定,而这两种写法在链路末端造成的差别,比接不接 OTel 更早咬到你。该项目持续更新,文中涉及的文件路径、类名与参数写法以仓库最新内容为准。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。