函数调用和 Agent:微软生成式 AI 入门课第 11 到 17 课跨了什么
很多人跟着 generative-ai-for-beginners 走到第 11 课 11-integrating-with-function-calling/,把 search_courses 那个例子照着抄完,会产生一种「我已经做出 Agent 了」的错觉——模型自己挑了函数、自己填了参数、我这边执行完把结果喂回去,它还用自然语言总结了一遍。等翻到第 17 课 17-ai-agents/README.md,又看到一堆 AgentExecutor、Planner、threads 这样的名词,就开始怀疑:这两课到底差在哪?是不是第 17 课只是把第 11 课包了一层糖衣?
这篇不做优劣判断,只对照两课都白纸黑字写明的三件事:下一步由谁决定、有没有循环、状态归谁管。这三条决定了你手上的代码到底属于哪一形态。
分界线一:if tool_calls: 这一行是谁写的
第 11 课 README 把整个流程直接标成了三步:调 Responses API 并带上 tools 与用户消息、读模型响应去执行函数或 API 调用、把函数结果再送一次 Responses API 生成给用户看的回答。三步是课程原文写死的结构,不是我归纳的。
关键在第二步。模型返回的 response.output 里可能带一个 function_call 项,长这样(这是课程文档里的示例响应,call_id 的值只是示例):
{
"type": "function_call",
"name": "search_courses",
"call_id": "call_abc123",
"arguments": "{\n \"role\": \"student\",\n \"product\": \"Azure\",\n \"level\": \"beginner\"\n}"
}
课程随后给出的做法是自己在 Python 里筛:
response_items = response.output
tool_calls = [item for item in response_items if item.type == "function_call"]
然后靠一张手写的字典把名字映射到真函数:
available_functions = {
"search_courses": search_courses,
}
function_to_call = available_functions[function_name]
function_args = json.loads(tool_call.arguments)
function_response = function_to_call(**function_args)
README 在讲清「什么是 function calling」时有一句很容易被跳过:使用 function calling 时 LLM 并不真的调用或运行任何函数,它只是按你给的结构产出一份响应,由你的应用去决定运行什么。也就是说,第 11 课形态里,「下一步做什么」这个决策的执行权完全在你写的那个 if tool_calls: 分支里,模型只提供一个建议。分发表 available_functions 是你维护的,函数名对不上就是 KeyError,没有任何一层会替你兜底。
同一课的 js-githubmodels/app.js 把这层责任写得更直白:它先判断 finish_reason === "tool_calls",再用 Object.prototype.hasOwnProperty.call(namesToFunctions, functionName) 校验模型给出的函数名是否在允许列表里,不在就抛错,代码注释里逐字标了 SECURITY:。这道白名单校验是应用层写的,不是模型或 SDK 送的。
第 17 课的形态则把这一步收进了框架的某个具体组件里。README 写明 LangChain Agents 用内置的 AgentExecutor 来管理 state,它接收定义好的 agent 与可用的 tools;TaskWeaver 里承担规划的是 Planner,它是一个 LLM,接过用户请求后把需要完成的任务拆开,再从叫 Plugins 的工具集合里挑;JARVIS 的特点则是用一个 LLM 管 state、而工具本身是其它 AI 模型(对象检测、转写、图像描述这类专用模型)。Microsoft Agent Framework 这边,README 写的是给 agent 传普通 Python 函数、带类型标注的参数会被自动转成 schema。
对比一下就清楚了:第 11 课里那张 available_functions 字典和那段 JSON schema 是你手写的两份东西,且必须自己保持一致;Agent Framework 的写法里 schema 是从函数签名生成的。这是两课在同一件事上给出的两种不同交代,而不是同一件事的两种说法。
分界线二:第 11 课里没有循环
这条最容易被误读。第 11 课的完整链路是:第一次 client.responses.create → 执行本地函数 → 把结果拼回 messages → second_response = client.responses.create(...) → print(second_response.output_text)。到此为止。 课程代码里没有任何一处再去检查 second_response 是否又带回了新的 function_call 项,也没有 while 或递归。它是一条直线,长度固定为两次模型调用。
js-githubmodels/app.js 更直接:它在取工具调用前写了注释 We expect a single tool call,并且用 message.tool_calls.length === 1 卡死了只处理一个的情况。
顺带一提,同一课的 README 与 notebook 11-integrating-with-function-calling/python/aoai-assignment.ipynb 在这里给的写法并不一致:README 用 for tool_call in tool_calls: 遍历,notebook 里则是 tool_call = tool_calls[0] 只取第一个;回填历史时 README 写 messages.append(tool_call),notebook 写 messages.extend(response.output)。两处都在仓库里,我们只把这个差异指出来,不推断哪种是作者的本意。课程持续更新,以仓库最新内容为准。
第 17 课这边,能找到的循环证据在 AutoGen 那一节。README 说 AutoGen 的重心是对话,agent 是 conversable 的——一个 LLM 可以和另一个 LLM 开始并持续一段对话来完成任务,并给出了这样一条 system message:
system_message="For weather related tasks, only use the functions you have been provided with. Reply TERMINATE when the task is done."
一个流程如果只走固定两步,是不需要「完成时回复 TERMINATE」的。需要终止信号,说明存在不定轮次的往复。同一节还写明,工具函数可以被自动执行、也可以按用户输入再执行,取决于你的配置——这是循环里插人工确认的位置。
分界线三:messages 那个列表是谁的
第 11 课的状态就是一个普通 Python 列表。你自己 append,自己保证顺序。README 特别标了一句:模型的 function_call 项必须先于它的输出被追加进去。结果项的形状是固定的:
{
"type": "function_call_output",
"call_id": tool_call.call_id,
"output": function_response,
}
call_id 是把结果和刚才那次调用对上号的钥匙,配错了就是错配,没有别的地方替你校验。另外课程的调用里都带了 store=False,也就是不依赖服务端留存这轮对话——进程一退,messages 就没了。
第 17 课列出的几种做法,状态都从你手里挪到了框架里。README 写 Microsoft Agent Framework 通过 threads 管理对话上下文,agent 会跟踪消息历史(用户请求、工具调用与结果),并且 threads 可以被持久化,从而让一段对话暂停后再恢复;LangChain 的 AgentExecutor 也存 chat history;TaskWeaver 除了 Planner,还有一个叫 experience 的机制,README 写明它把对话上下文长期存进 YAML 文件,可以配置成让 LLM 在同类任务上参考过往对话——注意它同时写了 Plugins 是以 embeddings 存储的,便于 LLM 检索到正确的插件。
这就是「状态归属」的实际含义:不是抽象的架构偏好,而是关掉进程之后那段上下文还在不在。
那么什么时候该从前者换到后者
按上面三条倒推,判据其实很硬:
- 一次工具调用能不能封顶? 能,第 11 课的形态就够了,两次模型调用把事办完,代码全在你眼皮底下。不能封顶——模型可能要先查 A 再据此查 B——你就得自己写循环和终止条件,这时候第 17 课里 AutoGen 那种带 TERMINATE 约定的形态才有意义。
- 要不要跨会话恢复? 要,就去看 threads 这类可持久化的机制;不要,
messages列表配store=False反而更省事。 - 需不需要看清「它为什么挑了这个工具」? 第 17 课 README 提到 LangChain 团队为此做了 LangSmith,也提到 Microsoft Agent Framework 通过 OpenTelemetry 提供内置可观测性。第 11 课的形态里,你能看到的只有你自己
print出来的东西。
有几个维度我们没有依据,不比。 第 17 课的目录下只有 README.md 和图片,没有代码示例目录,这一课给的是框架介绍与片段,不是可以照着落地的工程;而第 11 课有完整的 notebook 与 js-githubmodels/app.js。所以两者在错误处理、并发、重试上的实际差别,我们在仓库里找不到可对照的材料,不做判断。第 11 课的作业一节倒是明确把「函数调用或 API 调用没有返回合适课程时的错误处理」留给读者自己写,说明课程正文本身也没覆盖这块。
两个会咬到你的细节
第一个,第三条 provider 路线的名字已经不作数了。 第 11 课有一个 js-githubmodels/ 目录,但打开 app.js 会看到它读的环境变量是 AZURE_INFERENCE_CREDENTIAL 与 AZURE_INFERENCE_ENDPOINT,注释里逐字写着这些值要从 Microsoft Foundry 项目的 Overview 页面取。仓库 00-course-setup/03-providers.md 也写明 GitHub Models 将于 2026 年 7 月底退役,直接替代者是 Microsoft Foundry Models。今天这个时间点已经过去了。所以选路线时,第三条应当理解为 Microsoft Foundry Models(原 GitHub Models 路线),别再按目录名去配 GITHUB_TOKEN。三条路线的总说明就在 00-course-setup/03-providers.md,配之前先看那一页。
第二个,采样参数别照抄。 第 11 课 README 的第二次调用里带了 temperature=0,但同一仓库的 06-text-generation-apps/README.md 写明:Microsoft Foundry 上当前未废弃的是 reasoning 模型(GPT-5 家族、o 系列),它们不支持 temperature 与 top_p,也不支持 max_tokens(改用 max_output_tokens),传了会收到参数不支持的报错。两处都在仓库里,摆在一起就是个需要你自己判断的落差——用哪个模型部署,决定了那行 temperature=0 能不能留。
本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与生成质量的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。
本文对照的是同一项目内的两种用法,依据均为上述仓库内容,不对两种用法做优劣排名, 选型结论只在仓库文档写明的能力边界内成立。文中提到的 LangChain、AutoGen、TaskWeaver、JARVIS, 均只引用该课程仓库对它们的描述,不涉及这些项目自身仓库的内容。 安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。