微软生成式 AI 入门课里函数定义了却不调用:schema 与消息结构两头查

2026-08-18

函数调用这块最容易卡住的不是概念,是形状。你按印象写了一份工具定义,请求也发出去了,模型回来的却是一段客客气气的自然语言;或者第一轮明明拿到了函数名和参数,把结果塞回去再问一次,模型像完全没看见那份结果。这两种情况绝大多数不是模型”不听话”,而是 schema 的顶层形状、必填字段声明、以及工具结果在消息列表里的位置,有一处对不上。

generative-ai-for-beginners 的第 11 课 11-integrating-with-function-calling/ 正好把这条链路完整写了一遍,python/oai-assignment.ipynbpython/aoai-assignment.ipynb 两个 notebook 的代码单元格,除了客户端初始化那一格之外其余完全相同:oai- 那份是 client = OpenAI() 加写死的 deploymentaoai- 那份显式传 api_keybase_urldeployment 取自环境变量。也就是说下面这些形状问题,两条路线是共通的。下面按排查顺序走,每一步都指到具体文件。

现象归类:先分清是没触发还是没续上

三种表现要分开:

  1. 响应里根本没有函数调用项,模型直接答话;
  2. 有函数调用项,你也执行了本地函数,但第二轮模型仍在重复提问;
  3. 第二轮请求直接报参数不被支持。

这三种对应的排查入口完全不同,混在一起查会绕远路。

第一步:确认模型到底想不想调

可执行的判定动作只有一个——把 response.output 原样打出来。仓库 notebook 里筛选的写法是:

# Extract the function call items from the response output
tool_calls = [item for item in response.output if item.type == "function_call"]

也就是说,在 Responses API 这条路线上,函数调用是 response.output 这个列表里的一个 item,靠 item.type == "function_call" 判定,item 上带 namecall_idarguments 三个属性,arguments 是一段 JSON 字符串,需要 json.loads 再解开。

这里有个仓库内部的不一致值得记一笔:README 里展示的响应示例是 {"type": "function_call", "name": ..., "call_id": ..., "arguments": ...},和代码一致;但 notebook 里紧跟着的那段 markdown 展示的却还是 {"role": "assistant", "function_call": {...}} 这种旧形状。把这两处放在一起看就清楚了:跟着代码单元格走,别跟着那段 markdown 去 message 里找 function_call,否则你会在一个不存在的路径上找半天。

同样的错位还有一处:notebook 的 markdown 写的是”我们通过给请求加上 functions 来做到这点,也就是 functions=functions”,还提到把 function_call 设成 auto;而它自己下面的代码单元格和 README 的正文用的都是 tools=functionstool_choice="auto"。参数名传错了会是什么反应,我们没有跑过,不敢替你断言,但至少不该照着那段过时的正文写。

第二步:schema 的顶层形状

Python 这条路线的工具定义,notebook 代码单元格上方有一行注释写得很直白:Responses API 用的是扁平的 tool 格式,name/description/parameters 都在顶层。完整定义长这样:

functions = [
   {
      "type":"function",
      "name":"search_courses",
      "description":"Retrieves courses from the search index based on the parameters provided",
      "parameters":{
         "type":"object",
         "properties":{
            "role":{
               "type":"string",
               "description":"The role of the learner (i.e. developer, data scientist, student, etc.)"
            }
         },
         "required":[
            "role"
         ]
      }
   }
]

(上面为节选,properties 下另有 productlevel 两项,完整内容以仓库文件为准。)

必填的骨架是:顶层 type 标成 "function",加 namedescriptionparametersparameterstype 通常是 "object",下面挂 properties,每个属性自身带 typedescriptionrequired 是可选项,README 把它单列为 optional property。

为什么强调”扁平”?因为同一课的另一条路线不是这个形状。js-githubmodels/app.js 走的是 @azure-rest/ai-inference/chat/completions,它的工具定义把 name/description/parameters 裹在一层 function 对象里:

const tool = {
    "type": "function",
    "function": {
        name: "getFlightInfo",
        description: "Returns information about the next flight between two cities." +
            "This includes the name of the airline, flight number and the date and time" +
            "of the next flight",
        parameters: {
            "type": "object",
            "properties": { /* 此处省略,完整内容以仓库文件为准 */ },
            "required": [
                "originCity",
                "destinationCity"
            ],
        },
    }
};

两个 SDK 两种嵌套层级:Responses API 那份把 name 直接放顶层,这份则多包了一层 function。如果你手上参考的代码片段来源和你实际用的客户端不是同一条路线,工具定义很可能从一开始就没被当成工具看待。

顺带把路线说清楚:这门课的示例通常有三个版本,oai-* 是 OpenAI、aoai-* 是 Azure OpenAI、第三条是 Microsoft Foundry Models(原 GitHub Models 路线)。第三条要特别注意——00-course-setup/03-providers.mdjs-githubmodels/app.js 的注释都写明 GitHub Models 于 2026 年 7 月底退役,直接替代者是 Microsoft Foundry Models(出自仓库文档)。目录名还叫 js-githubmodels,但里面的代码读的已经是 AZURE_INFERENCE_ENDPOINTAZURE_INFERENCE_CREDENTIAL。前缀名和实际接入目标已经脱节,别再按 GITHUB_TOKEN 那套去配。

第三步:required 声明与真实函数签名的落差

这是最容易被忽略的一处。schema 里 required 只写了 role,也就是 productlevel 允许缺席。而 notebook 里真正被调用的 Python 函数是:

def search_courses(role, product, level):

三个形参,都没有默认值。调用处是 function_to_call(**function_args)。把这两处放在一起看:一旦模型只给出 role,展开出来的关键字参数就凑不齐这个签名。仓库的 Assignment 一节自己也列了一条待办——为函数调用或 API 调用没有返回合适结果的情况加上错误处理。所以这不是我们的推测,而是示例代码明确留给读者补的口子。

修的方向有两个:要么把 required 补齐,要么给 Python 函数的参数加默认值。选哪个取决于你的业务能不能接受缺参数查询,仓库没有给结论。

第四步:描述怎么写,会不会影响触发

README 对三个顶层字段的说明是:name 是希望被调用的函数名,description 是这个函数怎么工作的说明——原文强调此处要具体、清晰,parameters 是你希望模型在响应里产出的值与格式。

看仓库自己怎么写 description 更有参考价值:函数级写的是”根据提供的参数从搜索索引中检索课程”,属性级则把可能的取值以举例形式塞进描述里,比如学习者角色那一项写成 (i.e. developer, data scientist, student, etc.),经验等级那一项写成 (i.e. beginner, intermediate, advanced)

这么写有什么用,README 后面自己解释了:用户消息是 "Find me a good course for a beginner student to learn Azure.",模型从中抽出了 studentAzurebeginner 填进参数。描述里给出的那些示例取值,和用户说法之间是有对应关系的。反过来,如果你的属性描述只有一个”角色”两字,模型手上就没有把自然语言映射到这个字段的线索。

还有一句提醒来自 README 的 Important 块:函数定义是随系统消息一起发给模型的,会占用可用 token。这意味着描述不是越长越好,得在”给足线索”和”别把上下文撑满”之间掂量。

处置后怎么验证

验证分两截。

第一截看第一轮:response.output 里能筛出 type == "function_call" 的 item,name 是你注册的函数名,json.loads(tool_call.arguments) 能解出预期的键。

第二截看消息拼装,这是”调了但续不上”那类问题的正主。README 在代码注释里写明:模型的 function_call item 必须先于它的输出被追加进去。仓库里有两种等价写法,README 用的是 messages.append(tool_call),notebook 用的是 messages.extend(response.output),之后再追加结果项:

messages.append(
    {
        "type": "function_call_output",
        "call_id": tool_call.call_id,
        "output": function_response,
    }
)

三点要对:结果项的 typefunction_call_outputcall_id 取自那次调用的 tool_call.call_id,不是自己编的字符串;output 在示例里是字符串(search_courses 结尾是 return str(results))。第二轮请求仍然要带上 tools=functionstool_choice="auto",然后读 second_response.output_text

js-githubmodels/app.js 那条路线的结果项形状不一样,是 {"tool_call_id": ..., "role": "tool", "name": ..., "content": ...},先把 response.body.choices[0].message 整个 push 回历史,再 push 这一项。同样是”先调用项、后结果项”的顺序,但字段名和 Responses API 完全不同,别串。

以上片段均按仓库代码中的接口语义组合,未经实测,以仓库最新代码为准。

什么情况说明不是这个原因

  • 第二轮报”参数不支持”:这多半和采样参数、而非 schema 有关。第 11 课 README 的第二次调用里带了 temperature=0,而 python/oai-assignment.ipynb 里的 deployment 取值是 gpt-5-mini(这是仓库里的示例值)。06-text-generation-apps/README.md 明确写了:Microsoft Foundry 上当前未废弃的是 reasoning 模型(GPT-5 家族、o 系列),它们不支持 temperaturetop_p,也不支持 max_tokens(改用 max_output_tokens),把 temperature 发给 gpt-5-mini 会得到 parameter not supported 的报错。这两处放在一起就是仓库内的一处不一致,遇到这个报错先去掉采样参数,别回头改 schema。
  • 报的是鉴权失败或 endpoint 找不到:那是 provider 配置层的事。Azure 这条路线在 python/aoai-assignment.ipynb 里是把 base_url 指到 {AZURE_OPENAI_ENDPOINT}/openai/v1/,配合 AZURE_OPENAI_API_KEYAZURE_OPENAI_DEPLOYMENT;README 说明因为走的是 v1 端点,不需要再设 api_version
  • 多数提问都能触发,只有个别句式不触发:这落在描述与措辞的匹配上,和顶层结构无关,改 schema 形状不会有帮助。
  • 本地函数抛的是网络或解析异常:那是你自己那段 requests.get 的问题,模型侧已经完成了它该做的部分。

Windows 侧的两点

仓库给出的 .env 准备命令是 cp .env.copy .env,这是 POSIX shell 的写法,Windows 侧的对应做法仓库里没有找到相关说明,请自行换成等价的复制方式。好消息是代码侧不受影响:notebook 用的是 load_dotenv().env 文件读取,不依赖 shell 的环境变量导出方式,所以 exportset 的差异在这里不构成问题。密钥请写进被 gitignore 的 .env,代码里一律用 <YOUR_API_KEY> 这类占位,别硬编码进 notebook 再提交。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。


本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与生成质量的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。