微软 AI Agent 入门课第 4 课的工具注册:函数怎么变成能调的工具
写 Agent 的人多半都手搓过一遍 function calling:自己拼一份 JSON schema 交给模型,模型返回一个 tool call,你从返回里把函数名和参数抠出来,json.loads 一下调本地函数,再把结果按规定格式塞回消息数组,发第二次请求拿最终答复。ai-agents-for-beginners 的 04-tool-use/README.md 里把这一整套手写流程完整列了出来——包括 tools 数组里的 type/name/description/parameters 结构、tool_choice="auto"、以及把结果作为 function_call_output 项追加回 messages 时要带上 call_id。这段能读,但真要维护三五个工具,schema 和函数签名两头对不齐是迟早的事。
第 4 课的 Python notebook 走的是另一条路:04-tool-use/code_samples/04-python-agent-framework.ipynb。这篇只讲这一份文件里的写法——同一课还有 .NET 版,在 04-tool-use/code_samples/04-dotnet-agent-framework.md 和同名 .cs 里,两边接口不一样,别混着看。
前置条件
这一段最容易被跳过,但缺一项后面全是报错。
00-course-setup/README.md 的 Requirements 一节写明需要 Python 3.12+,并特别提示:如果本机没有 3.12,装好后要用 python3.12 建 venv,才能从 requirements.txt 装到正确版本。同一节还写明 .NET 示例需要 .NET 10 SDK 或更高,以及需要 Azure CLI 做认证。
notebook 第一个代码格原样是:
%pip install agent-framework azure-ai-projects azure-identity python-dotenv -U -q
环境变量只有两个,00-course-setup/README.md 给的 .env 填写示例是:
AZURE_AI_PROJECT_ENDPOINT=https://<your-project>.services.ai.azure.com/api/projects/<your-project-id>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5-mini
其中部署名那一行是仓库示例文件里的值,换成你自己 Foundry 项目里部署的模型名。notebook 会自己检查这两项,任一为空就 raise ValueError,把缺的变量名列出来,所以不用猜是不是配漏了。
Windows 侧的两条命令和 bash 侧不一样,仓库都给了。建 venv 后激活,Windows 命令提示符下是 venv\Scripts\activate,zsh/bash 下是 source venv/bin/activate;复制环境变量模板,PowerShell 用 Copy-Item .env.example .env,zsh/bash 用 cp .env.example .env。
还有个容易踩的不一致:notebook 里构造客户端用的是 DefaultAzureCredential,而 04-tool-use/README.md 正文里那段示例用的是 AzureCliCredential。两处都在仓库里写着,登录方式的实际影响以你所在环境的认证配置为准。notebook 里的客户端构造是这样:
client = FoundryChatClient(
project_endpoint=endpoint,
model=deployment_name,
credential=DefaultAzureCredential()
)
一个函数变成工具,装饰器读走了什么
notebook 在定义工具那一格前面的说明里,把 @tool 读走的东西列了三条:docstring 成为模型看到的工具描述;类型注解(包括带描述的 Annotated)定义工具 schema;approval_mode 控制这次调用是否需要用户先批准。
对着代码看就很清楚。最简单的一个工具没有参数:
@tool(approval_mode="never_require")
def get_destinations() -> list[str]:
"""Get available vacation destinations."""
return ["Barcelona", "Paris", "Berlin", "Tokyo", "Sydney", "New York City"]
那一行 docstring 不是给同事看的注释,它就是模型判断”该不该调这个函数”的唯一依据。同理,带参数的工具靠 Annotated 的第二项给每个参数补描述:
@tool(approval_mode="never_require")
def get_flight_info(
origin: Annotated[str, "Origin airport code"],
destination: Annotated[str, "Destination airport code"],
) -> str:
"""Get flight information between two cities."""
Annotated[str, "Origin airport code"] 里的字符串就是手写 schema 时 properties.origin.description 那个位置的东西。手写版里你要在两处维护同一件事(函数签名一处、schema 一处),装饰器版把它合并成一处:注解写错,模型看到的 schema 就错,没有第二个地方兜底。
顺带一提,第 14 课的 notebook 里出现了另一种写法,@tool(description="Check hotel room availability for a destination city"),即用参数显式给描述,同时函数还带着自己的 docstring。两种写法在仓库里都存在;当 description 与 docstring 同时出现时以哪个为准,仓库里没有找到相关说明。
注册的位置:挂在 as_agent 上,不是挂在 run 上
工具定义完,第 4 课把三个函数收进一个列表,再一次性交给客户端:
travel_tools = [get_destinations, check_availability, get_flight_info]
agent = client.as_agent(
name="TravelToolAgent",
instructions="You are a travel agent. Use the available tools to answer questions about destinations, availability, and flights.",
tools=travel_tools,
)
response = await agent.run(
"What destinations do you have? Which ones are still available?"
)
注册点就是 as_agent(...) 的 tools=。run() 那一步只传用户消息,工具清单在建 agent 时就定好了。这个位置在整个课程里是一致的:第 11 课的 11-agentic-protocols/code_samples/11-mcp-agent-framework.ipynb 里同样是 client.as_agent(tools=[search_accommodations, get_local_experiences], ...)。
一个细节值得留意:04-tool-use/README.md 正文那段示例传的是 tools=get_current_time——单个函数,没有包成列表;notebook 传的是列表。两种形式仓库里都写着,写多个工具时按 notebook 的列表形式来最省事。
对比一下手写版就知道装饰器省掉了哪一段:README 那条手写路线里,你要自己从 response.output 里筛出 item.type == "function_call" 的项,按 tool_call.name 分支到对应函数,json.loads(tool_call.arguments) 取参数,再把结果拼成 {"type": "function_call_output", "call_id": ..., "output": ...} 追回 messages,然后发第二次 client.responses.create。装饰器版里这一整段不出现在你的代码里——04-tool-use/README.md 的说法是框架负责模型与你的代码之间的来回通信。也就是说,call_id 对不上、第二次请求忘了带 tools 这类手写常见问题不会再由你处理,代价是这一段你也不再直接可见。
返回值这一头同样是注解在管
工具的入参靠 Annotated 描述,出参靠返回类型注解。第 4 课三个工具的返回注解分别是 -> list[str] 和 -> str,函数体里返回的都是普通 Python 值:check_availability 从一个内联字典里取一条形如 "Available - 3 spots left" 的字符串(这些是仓库示例里的假数据),查不到就返回 "Unknown destination"。模型拿到的就是这个字符串本身,工具里没有查到东西时返回什么话,直接决定模型接下来怎么答——所以”查无此项”那条分支的措辞不是可有可无的兜底。
返回值也可以是 Pydantic 模型。第 3 课的 get_destination_details 返回注解写的是 -> DestinationRecommendation,函数体里返回的就是构造好的模型对象,这样工具出参和 agent 最终输出两头都是带类型的结构。
结构化输出:这一课的说法和代码对不上
notebook 里”Structured Output with Tools”那一节的文字说,把 response_format 设成一个 Pydantic 模型,就能强制 agent 返回带类型的 JSON 而不是自由文本;课末小结也重复了这句。但同一节的代码格里,structured_agent = client.as_agent(...) 只传了 name / instructions / tools,response_format 一次都没有出现在这份 notebook 的代码里——两个 Pydantic 模型 BookingRecommendation 和 TravelPlan 定义了,却没被挂上去。
真正把它传进去的写法在别的课里。第 3 课 03-agentic-design-patterns/code_samples/03-python-agent-framework.ipynb 是在调用时传:
response = await structured_agent.run(
"Recommend 3 destinations for a culture-loving traveler with a $2500 budget",
options={"response_format": TravelRecommendations},
)
if response and response.value:
result: TravelRecommendations = response.value
第 14 课的 14-microsoft-agent-framework/code-samples/14-human-loop.ipynb 则是在建 agent 时传,用的键名是 default_options:
chat_client.as_agent(
instructions=(...),
tools=[hotel_booking],
default_options={"response_format": BookingCheckResult},
)
所以这三处放在一起就能看出关系:options 是单次调用的,default_options 是 agent 默认的,而第 4 课的 notebook 讲了这个能力却漏了那一行代码。你照着第 4 课抄,结果拿到自由文本,不是你写错了。要补的话按第 3 课的形式把 options={"response_format": TravelPlan} 加到 run() 上,再从 response.value 取结果——以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。
边界
approval_mode在这份 notebook 的表格里只列了两个取值:"never_require"(自动执行,不需要用户确认)和"always_require"(每次调用都要用户先批准)。别的取值仓库里没有找到相关说明。- notebook 用
book_flight演示approval_mode="always_require",但那一格只是定义工具并打印属性,没有给出批准动作怎么发生、批准回执长什么样的代码。人在回路的完整流程是第 14 课14-human-loop.ipynb的另一套 workflow 写法。 - 什么工具该设成
always_require,仓库自己在.agents/skills/deploying-scalable-agents/SKILL.md里给了取向:把它用在”大额退款”这类动作上;notebook 的表格也写明用于有副作用的操作,例如订机票、刷信用卡。判据是这次调用会不会在外部系统里留下不可撤销的痕迹,而不是这个工具重不重要。 04-tool-use/code_samples/下有个test_demo_plugins.py,开头是from demo_tool_agent import TimePlugin, CalculatorPlugin,但仓库里找不到demo_tool_agent这个模块,TimePlugin/CalculatorPlugin也和本课@tool的写法不是一回事。别照着它去找配套代码。- 这些示例会连到云端服务,会产生费用,也会把你的输入送出去。凭据走
az login会话,别把密钥写进代码。README 的可信任一节另外提醒了一类风险:模型动态生成 SQL 时,处置方式是把数据库访问权限配成只读,而不是指望提示词约束。 - 该课程持续更新,上面的文件路径、依赖与接口写法都可能随版本变动,以仓库最新内容为准。
怎么确认注册对了
不跑 agent 也能验一部分。notebook 最后一格原样是这两行:
print("Tool name:", book_flight.name)
print("Approval mode:", book_flight.approval_mode)
被 @tool 装饰后的对象带 name 和 approval_mode 属性,能直接读出来——这就是检查”函数确实变成工具了、审批模式确实是我写的那个”最省事的办法,尤其是当你把 approval_mode 写进某个配置再拼进装饰器时。
其余两处:环境变量配没配对,看 notebook 开头那段检查会不会抛 ValueError;结构化输出配没配上,看 response.value 是不是 None——第 3 课的示例在 else 分支里打印的正是 “No validated structured response was returned.”,拿不到 value 说明 response_format 没生效,而不是模型答得不好。
至于每个工具的描述写得够不够模型区分,仓库里没有给出可执行的检查手段;能做的只是回头看 docstring 和 Annotated 里的那句话,它们就是模型能看到的全部信息。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。