generative-ai-for-beginners 第 11 课实战:用 Azure OpenAI Function Calling 让大模型调用外部工具、产出结构化数据
本文基于《Generative AI for Beginners》仓库第 11 课文档(translations/ar/11-integrating-with-function-calling/README.md 对应的英文原版 11-integrating-with-function-calling/README.md),完整讲解 Function Calling(函数调用)这一机制:它是什么、为什么需要它、如何用 Azure OpenAI 的 Responses API 定义并触发一个函数调用,以及如何把模型返回的结构化调用参数映射到你自己的真实函数(如调用 Microsoft Learn Catalog API 搜索课程),最终把函数结果回传给模型生成自然语言回答。读完本文,你将能够独立解释 Function Calling 的用途,为应用设计有效的函数定义,并在 Python / Node.js / TypeScript 三种栈中落地完整闭环。
前言与学习目标
在进入正题之前,先回顾一下本课要解决的问题。前几课你已经学会了提示词工程、文本生成、聊天应用等能力,但仍然有两个痛点:
- 大模型(LLM)的响应格式不一致、不结构化,下游代码必须写大量校验逻辑来兜底每一种可能的格式变体;
- 模型的知识截止于训练数据,无法回答"斯德哥尔摩现在天气如何"这类需要实时外部数据的问题,也难以让应用消费其他数据源。
Function Calling 正是 Azure OpenAI 服务用来克服这两类限制的机制:一致的响应格式(更好地控制响应结构,便于在下游系统集成)与外部数据接入(在聊天上下文中使用应用其他来源的数据)。
本课覆盖三块内容:
- 解释什么是函数调用以及它的适用场景;
- 使用 Azure OpenAI 创建一个函数调用;
- 把函数调用集成进一个真实应用。
学完之后你应该能:说明使用函数调用的目的;使用 Azure OpenAI 服务配置一次完整的函数调用;针对自己应用的用例设计有效的函数调用。
场景:用函数升级课程推荐聊天机器人
本课的贯穿场景是为一家教育初创公司构建一个功能:让用户通过聊天机器人查找技术课程,并根据其技能水平、当前角色和感兴趣的技术推荐课程。完成该场景需要三种组件配合:
- Azure OpenAI:为用户提供聊天体验;
- Microsoft Learn Catalog API:根据用户请求帮助查找课程;
- Function Calling:承接用户的查询,将其交给一个函数去执行实际的 API 请求。
为什么需要 Function Calling
在 Function Calling 出现之前,LLM 的响应是非结构化且不稳定的。开发者不得不编写复杂的校验代码来应对每一种响应变体;同时,由于模型受限于训练数据的时间边界,用户无法从模型那里得到实时信息(例如某城市的当前天气)。
Function Calling 作为 Azure OpenAI 服务的特性,恰好针对上述两个局限:
- 一致的响应格式:如果能更好地控制响应格式,就能更容易地把响应集成到下游的其他系统;
- 外部数据:能够把来自应用其他数据源的数据用在聊天上下文中。
一个关键认知:使用 Function Calling 时,LLM 本身并不会真正调用或执行任何函数。实际流程是:我们为 LLM 定义一个结构(函数签名与参数模式),让它在响应中遵循这个结构输出"要调用哪个函数、传什么参数";然后由我们的应用代码解析这个结构化响应,真正去执行对应的函数(或 API 请求);最后再把函数结果送回给 LLM,由它用自然语言回答用户。
用场景演示问题:无约束提示词下的格式不一致
如果想动手运行下面的示例,仓库提供了配套笔记本 11-integrating-with-function-calling/python/aoai-assignment.ipynb;也可以只读代码,本文会完整演示函数如何帮助解决格式问题。
假设我们要建立一张学生数据表,以便为他们推荐合适的课程。下面有两条内容结构非常相似的学生描述:
第 1 步:创建到 Azure OpenAI 资源的连接。 由于 Responses API 由 Azure OpenAI(Microsoft Foundry)的 v1 端点提供,只需要用标准 OpenAI 客户端指向 <endpoint>/openai/v1/ 即可,无需再设置 api_version:
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
# Responses API 由 Azure OpenAI (Microsoft Foundry) v1 端点提供,
# 因此我们把 OpenAI 客户端指向 <your-endpoint>/openai/v1/
endpoint = os.environ['AZURE_OPENAI_ENDPOINT']
client = OpenAI(
api_key=os.environ['AZURE_OPENAI_API_KEY'],
base_url=f"{endpoint.rstrip('/')}/openai/v1/",
)
deployment = os.environ['AZURE_OPENAI_DEPLOYMENT']
这里需要配置三个环境变量:AZURE_OPENAI_API_KEY、AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_DEPLOYMENT。配套的 aoai-assignment.ipynb 中也采用了完全一致的客户端初始化方式。
第 2 步:创建两条学生描述。
student_1_description="Emily Johnson is a sophomore majoring in computer science at Duke University. She has a 3.7 GPA. Emily is an active member of the university's Chess Club and Debate Team. She hopes to pursue a career in software engineering after graduating."
student_2_description = "Michael Lee is a sophomore majoring in computer science at Stanford University. He has a 3.8 GPA. Michael is known for his programming skills and is an active member of the university's Robotics Club. He hopes to pursue a career in artificial intelligence after finishing his studies."
我们希望把上面的学生描述发送给 LLM 来解析数据,这些数据稍后可以被应用使用、发送到 API 或存入数据库。
第 3 步:构造两条完全相同的提示词,指示 LLM 我们关心哪些信息:
prompt1 = f'''
Please extract the following information from the given text and return it as a JSON object:
name
major
school
grades
club
This is the body of text to extract the information from:
{student_1_description}
'''
prompt2 = f'''
Please extract the following information from the given text and return it as a JSON object:
name
major
school
grades
club
This is the body of text to extract the information from:
{student_2_description}
'''
第 4 步:把提示词发送给 LLM。 使用 client.responses.create 发送,提示词存入 input 变量并指定角色为 user,以模拟用户向聊天机器人发出的消息:
# 第一条提示词的响应
openai_response1 = client.responses.create(
model=deployment,
input = [{'role': 'user', 'content': prompt1}],
store=False,
)
openai_response1.output_text
# 第二条提示词的响应
openai_response2 = client.responses.create(
model=deployment,
input = [{'role': 'user', 'content': prompt2}],
store=False,
)
openai_response2.output_text
第 5 步:用 json.loads 把响应转成 JSON 对象:
# 将响应加载为 JSON 对象
json_response1 = json.loads(openai_response1.output_text)
json_response1
响应 1:
{
"name": "Emily Johnson",
"major": "computer science",
"school": "Duke University",
"grades": "3.7",
"club": "Chess Club"
}
响应 2:
{
"name": "Michael Lee",
"major": "computer science",
"school": "Stanford University",
"grades": "3.8 GPA",
"club": "Robotics Club"
}
尽管两条提示词完全相同、学生描述结构相似,grades 属性却出现了不同的格式:一次是 3.7,另一次是 3.7 GPA。原因是 LLM 接收的是以提示词形式存在的非结构化数据,返回的也往往是非结构化数据——我们必须有一个结构化的格式,才能在存储或使用这些数据时知道该预期什么。
这正引出了 Function Calling 的解法:通过函数调用,我们可以确保拿到结构化数据。再强调一次核心机制——LLM 并不真正运行函数,而是遵循我们定义的结构输出响应,我们据此决定在应用里运行哪个函数。执行函数后,把返回值再送回 LLM,LLM 便用自然语言回答用户的查询。
函数调用的典型用例
函数调用可以在很多场景中改进你的应用,文档列举了三类:
- 调用外部工具。聊天机器人擅长回答用户问题,借助函数调用,它还能利用用户消息完成具体任务。例如学生可以对机器人说"给我导师发一封邮件,说我在这一科目上需要更多帮助",这可以触发一次
send_email(to: string, body: string)的函数调用。 - 生成 API 或数据库查询。用户可以用自然语言查找信息,信息会被转换为格式化的查询或 API 请求。例如老师问"哪些学生完成了上次的作业",可以调用
get_completed(student_name: string, assignment: int, current_status: string)。 - 生成结构化数据。用户可以给模型一段文本或 CSV,让它提取重要信息。例如学生可以把一篇关于和平协议的维基百科文章转成 AI 学习卡片,通过
get_important_facts(agreement_name: string, date_signed: string, parties_involved: list)完成。
创建你的第一个函数调用
创建一次函数调用包含三个主要步骤:
- 调用 Responses API,带上你的函数(工具)列表和用户消息;
- 读取 模型的响应,执行一个动作——运行函数或 API 请求;
- 再次调用 Responses API,把函数的响应带回去,用这些信息生成给用户的最终回答。
步骤 1:创建消息
第一步是创建用户消息。它可以动态地取自文本输入框的值,也可以直接在这里赋值。第一次使用 Responses API 时,需要定义消息的 role 和 content。
role 可以是 system(制定规则)、assistant(模型)或 user(终端用户)。对于函数调用,这里把它设为 user 并附上一句示例问题:
messages= [ {"role": "user", "content": "Find me a good course for a beginner student to learn Azure."} ]
通过分配不同角色,LLM 能清楚地区分"这是系统说的还是用户说的",从而帮助构建一段 LLM 可以在此基础上继续推进的对话历史。
步骤 2:创建函数
接下来定义函数及其参数。这里只用一个名为 search_courses 的函数,但你也完全可以定义多个。
重要:函数会被包含在发给 LLM 的系统消息中,并计入你可用的 token 预算。
函数被创建为一个条目数组,每个条目是 Responses API 扁平(flat)格式下的一个工具,带有 type、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.)"
},
"product":{
"type":"string",
"description":"The product that the lesson is covering (i.e. Azure, Power BI, etc.)"
},
"level":{
"type":"string",
"description":"The level of experience the learner has prior to taking the course (i.e. beginner, intermediate, advanced)"
}
},
"required":[
"role"
]
}
}
]
逐个说明各字段:
name:希望被调用的函数名;description:函数如何工作的描述——这里务必具体、清晰,模型靠它判断何时调用该函数;parameters:你希望模型在响应中产出的取值与格式列表,其元素具有如下属性:type:属性值所存储的数据类型;properties:模型在响应中使用的具体取值列表:name:模型在格式化响应中使用的属性名(键),例如product;type:该属性的数据类型,例如string;description:对该属性的描述。
另外还有一个可选属性 required——完成函数调用所必需的属性(本例中只有 role 是必需的,product 与 level 可选)。
值得一提的是,TypeScript 示例 typescript/function-app/src/main.ts 中的 findWeather 工具还展示了 enum 约束的用法:unit 参数被声明为 enum: ["C", "F"],把参数取值范围直接约束在模式里,这是比自然语言描述更强的格式保障。
步骤 3:发起函数调用
定义好函数后,需要把它包含进 Responses API 的请求中,方式为添加 tools 参数,即 tools=functions。
同时还可以把 tool_choice 设为 auto,含义是:让 LLM 根据用户消息自行决定应调用哪个函数,而不是由我们硬性指定。下面调用 client.responses.create 的代码中,tools=functions 与 tool_choice="auto" 的组合就是把"何时调用"的选择权交给了模型:
response = client.responses.create(model=deployment,
input=messages,
tools=functions,
tool_choice="auto",
store=False)
print(response.output)
现在返回的响应中,response.output 里会包含一个 function_call 条目,形如:
{
"type": "function_call",
"name": "search_courses",
"call_id": "call_abc123",
"arguments": "{\n \"role\": \"student\",\n \"product\": \"Azure\",\n \"level\": \"beginner\"\n}"
}
可以看到函数 search_courses 被调用了,且参数列在 JSON 响应的 arguments 属性中。结论是:LLM 能够从我们传给 input 参数的那条消息中提取出数据来填充函数参数。再看一眼 messages 的值:
messages= [ {"role": "user", "content": "Find me a good course for a beginner student to learn Azure."} ]
student、Azure 和 beginner 都是从 messages 中被提取出来并设为函数输入的。以这种方式使用函数,既是从提示词中提取信息的好办法,也是给 LLM 提供结构、获得可复用能力的好办法。
把函数调用集成到应用中
测试过 LLM 的结构化响应后,就可以把它集成到应用里了。
管理流程
按下面几个步骤走:
第 1 步:调用服务并提取函数调用条目。
response_items = response.output
tool_calls = [item for item in response_items if item.type == "function_call"]
第 2 步:定义真正调用 Microsoft Learn API 获取课程列表的函数。
import requests
def search_courses(role, product, level):
url = "https://learn.microsoft.com/api/catalog/"
params = {
"role": role,
"product": product,
"level": level
}
response = requests.get(url, params=params)
modules = response.json()["modules"]
results = []
for module in modules[:5]:
title = module["title"]
url = module["url"]
results.append({"title": title, "url": url})
return str(results)
注意这里我们创建了一个与 functions 变量中函数名相对应的真实 Python 函数,并且发起了真正的外部 API 调用以获取所需数据——本例中是向 Microsoft Learn Catalog API 查询培训模块(返回前 5 条,取 title 和 url)。
那么问题来了:我们有了 functions 变量和一个对应的 Python 函数,如何让模型把两者对应起来,从而调到我们的 Python 函数?
第 3 步:检查是否需要调用 Python 函数。 需要查看 LLM 响应中是否包含 function_call 条目,并调用被指名的那个函数:
# 检查模型是否想调用函数
if tool_calls:
for tool_call in tool_calls:
print("Recommended Function call:")
print(tool_call.name)
print()
# 调用函数
function_name = tool_call.name
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)
print("Output of function call:")
print(function_response)
print(type(function_response))
# 把函数调用及其结果加回对话。
# 模型的 function_call 条目必须先于其输出被追加。
messages.append(tool_call) # 模型的 function_call 条目
messages.append( # 函数结果
{
"type": "function_call_output",
"call_id": tool_call.call_id,
"output": function_response,
}
)
其中三行代码是关键——提取函数名、解析参数并发起调用:
function_to_call = available_functions[function_name]
function_args = json.loads(tool_call.arguments)
function_response = function_to_call(**function_args)
运行后的输出大致如下:
Recommended Function call:
search_courses
Output of function call:
[{'title': 'Describe concepts of cryptography', 'url': 'https://learn.microsoft.com/training/modules/describe-concepts-of-cryptography/?WT.mc_id=api_CatalogApi'}, {'title': 'Introduction to audio classification with TensorFlow', 'url': 'https://learn.microsoft.com/en-us/training/modules/intro-audio-classification-tensorflow/?WT.mc_id=api_CatalogApi'}, {'title': 'Design a Performant Data Model in Azure SQL Database with Azure Data Studio', 'url': 'https://learn.microsoft.com/training/modules/design-a-data-model-with-ads/?WT.mc_id=api_CatalogApi'}, {'title': 'Getting started with the Microsoft Cloud Adoption Framework for Azure', 'url': 'https://learn.microsoft.com/training/modules/cloud-adoption-framework-getting-started/?WT.mc_id=api_CatalogApi'}, {'title': 'Set up the Rust development environment', 'url': 'https://learn.microsoft.com/training/modules/rust-set-up-environment/?WT.mc_id=api_CatalogApi'}]
<class 'str'>
第 4 步:把更新后的 messages 再次发送给 LLM,这样就能收到自然语言响应,而不是结构化的 API JSON 响应:
print("Messages in next request:")
print(messages)
print()
second_response = client.responses.create(
input=messages,
model=deployment,
tool_choice="auto",
tools=functions,
temperature=0,
store=False,
) # 获取模型的新响应,此时模型已经能看到函数响应
print(second_response.output_text)
输出:
I found some good courses for beginner students to learn Azure:
1. [Describe concepts of cryptography](https://learn.microsoft.com/training/modules/describe-concepts-of-cryptography/?WT.mc_id=api_CatalogApi)
2. [Introduction to audio classification with TensorFlow](https://learn.microsoft.com/en-us/training/modules/intro-audio-classification-tensorflow/?WT.mc_id=api_CatalogApi)
3. [Design a Performant Data Model in Azure SQL Database with Azure Data Studio](https://learn.microsoft.com/training/modules/design-a-data-model-with-ads/?WT.mc_id=api_CatalogApi)
4. [Getting started with the Microsoft Cloud Adoption Framework for Azure](https://learn.microsoft.com/training/modules/cloud-adoption-framework-getting-started/?WT.mc_id=api_CatalogApi)
5. [Set up the Rust development environment](https://learn.microsoft.com/training/modules/rust-set-up-environment/?WT.mc_id=api_CatalogApi)
You can click on the links to access the courses.
至此,完整的闭环打通了:用户提问 → 模型产出结构化 function_call → 应用执行真实 API 请求 → 结果作为 function_call_output 回到对话 → 模型用自然语言作答。
跨语言实现对照:仓库中的 Node.js 与 TypeScript 示例
同一套流程在仓库中还有两种语言的参照实现,可以对照理解"函数名映射 + 结果回传"这一步在不同 API 风格下的形态差异。
Node.js 版本(11-integrating-with-function-calling/js-githubmodels/app.js) 使用 Azure AI Inference SDK(@azure-rest/ai-inference,见 package.json)访问 /chat/completions 端点,定义 getFlightInfo 与 getHotelInfo 两个工具,场景是"去迈阿密并订酒店"。与 Python 版的 Responses API 扁平工具格式不同,它采用的是 chat/completions 的嵌套格式({ type: "function", function: { name, description, parameters } })。其核心流程与 Python 版一一对应:
- 通过
response.body.choices[0].finish_reason === "tool_calls"判断模型是否要求调用工具(对应 Python 版检查function_call条目); - 用
namesToFunctions字典把toolCall.function.name映射到本地可执行函数(对应 Python 版的available_functions); - 执行完函数后,把结果以
{ tool_call_id, role: "tool", name, content }的形式追加进messages,再发起第二次请求(对应 Python 版的function_call_output条目)。
这个示例还包含值得借鉴的健壮性细节:在 namesToFunctions 中校验函数名是否存在于白名单(防止模型请求未知函数),以及对 arguments 的 JSON 解析包了 try/catch 错误处理——这些正是 Python 版教程留作练习的"错误处理"部分的具体落地方式。
TypeScript 版本(typescript/function-app/src/main.ts) 则演示了"天气查询 + Bing Maps API"场景,并使用 OpenAI 官方 SDK 指向 <endpoint>/openai/v1/ 端点调用 client.responses.create,与 Python 版同属 Responses API 风格。从源码结构看,它在函数执行环节加入了一组明显面向生产环境的防护措施:启动时强制校验 AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_API_KEY、BING_MAPS_BASE_URL、BING_API_KEY 四个环境变量;isValidUrl 要求端点必须为 HTTPS;所有查询参数经 URLSearchParams 编码以防注入;axios 请求设置 10 秒超时防止挂起;出错时只记录状态码等安全信息而不打印完整报错(避免泄露敏感数据)。如果要把第 11 课的函数调用从笔记本搬进生产应用,这个文件的"安全注释"(源码中以 SECURITY: 标注)是很好的自查清单。
任务(课后练习)
为了继续深入 Azure OpenAI Function Calling,可以尝试构建:
- 为函数添加更多参数,帮助学习者找到更多课程(可用参数的完整定义可参考 Learn Catalog API 的开发者参考文档);
- 创建另一个函数调用,接收更多学习者信息,例如母语;
- 当函数调用和/或 API 调用没有返回任何合适课程时,创建错误处理(Node.js 示例中的白名单校验与 JSON 解析 try/catch 可直接参考)。
小结
Function Calling 的本质是"把 LLM 从自由文本生成器变成结构化协作者":我们用 JSON Schema 风格的工具定义约束模型的输出,模型从对话中抽取参数并生成 function_call,应用侧负责真实的副作用(调用 Learn Catalog API、发邮件、查数据库),最后把结果喂回模型完成自然语言收尾。掌握"定义工具 → 提取调用 → 白名单映射执行 → 结果回传 → 二次生成"这条主线后,你可以把它迁移到仓库中 Python(aoai-assignment.ipynb)、Node.js(js-githubmodels/app.js)、TypeScript(typescript/function-app/src/main.ts)三种实现中验证。
完成本课之后,可以继续学习 12-designing-ux-for-ai-applications/README.md,了解如何为 AI 应用设计用户体验。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

