Generative AI for Beginners 第 11 课:用 Azure OpenAI Function Calling 为聊天机器人接入外部数据与工具
函数调用(Function Calling)是 Azure OpenAI 服务的一项能力,它让大语言模型(LLM)不再只是"凭训练数据作答",而是能够按开发者声明的 JSON 结构产出稳定、可解析的响应,再由你的应用代码决定调用哪个真实函数(查数据库、请求外部 API 等),最后把结果回传给模型生成自然语言回复。在 generative-ai-for-beginners 仓库的第 11 课中,它被用于教育创业项目的实战场景:让用户通过聊天机器人按技能水平、当前角色与兴趣产品检索微软官方技术课程。读完本文,你将理解函数调用的原理与典型用例,掌握从"定义函数声明"到"执行真实 Python 函数"再到"把结果喂回模型"的完整三步流程,并学会把它集成进自己的应用。
本课完整配套代码在 translations/cs/11-integrating-with-function-calling/python/aoai-assignment.ipynb(可直接运行的 Jupyter Notebook),你既可以动手执行,也可以按下面的讲解逐步跟进。
一、为什么需要函数调用:两个绕不开的痛点
在前面的课程中你已经学会了不少生成式 AI 能力,但有两个问题仍然待解:
- 响应格式不稳定:LLM 返回的文本是非结构化、不统一的,开发者必须编写大量校验代码来兼容每一种输出变体,才能把响应交给下游系统处理。
- 无法获取外部数据:模型受限于训练数据的截止时间与范围,用户问不出"斯德哥尔摩现在的天气如何"这类需要实时数据的问题。
函数调用正是 Azure OpenAI 用来突破这两点的能力:
- 一致的响应格式(Consistent response format):更好地控制输出格式,就能更方便地把响应集成到下游其他系统;
- 外部数据(External data):在对话上下文中,能够使用应用中其他来源的数据。
注意:使用函数调用时,LLM 本身并不会真正执行任何函数。它做的只是按照你声明的结构去组织响应;真正"决定调用哪个函数并执行"的逻辑写在你自己的应用代码里。这是理解整个机制最关键的一点。
二、用场景演示问题:同构输入为何输出不一致
要理解函数调用的价值,先看一个具体的反例。假设我们要构建一个学生信息库,用于给学生推荐合适的课程。下面两个学生描述在信息构成上高度相似:
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。为此先建立 Azure OpenAI 连接:
import os
import json
from openai import AzureOpenAI
from dotenv import load_dotenv
load_dotenv()
client = AzureOpenAI(
api_key=os.environ['AZURE_OPENAI_API_KEY'], # 默认读取同名环境变量,可省略
api_version="2023-07-01-preview"
)
deployment = os.environ['AZURE_OPENAI_DEPLOYMENT']
这份代码需要你提前配置 AZURE_OPENAI_API_KEY 与 AZURE_OPENAI_DEPLOYMENT 环境变量(可通过 00-course-setup 的本地环境章节了解配置方式)。接着构造两条完全一致的提示词,要求模型把关心的字段提取成 JSON:
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}
'''
随后用 client.chat.completions.create 把提示词作为 user 角色消息发送给模型,模拟用户向聊天机器人发消息:
# 第一个提示词的响应
openai_response1 = client.chat.completions.create(
model=deployment,
messages=[{'role': 'user', 'content': prompt1}]
)
openai_response1.choices[0].message.content
# 第二个提示词的响应
openai_response2 = client.chat.completions.create(
model=deployment,
messages=[{'role': 'user', 'content': prompt2}]
)
openai_response2.choices[0].message.content
通过 openai_response1['choices'][0]['message']['content'] 可以查看返回内容。最后用 json.loads 把响应转成 JSON 对象:
json_response1 = json.loads(openai_response1.choices[0].message.content)
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.8 GPA 两种格式——前者是数值,后者带了单位后缀。根因在于:LLM 输入的是"写在提示词里的非结构化文本",返回的也必然是非结构化文本。当我们想把数据存储或复用时,必须有确定的结构才能知道"接下来拿到的一定是什么"。
函数调用正是用来解决这个格式化问题的方案——为 LLM 声明一套响应结构,由应用依据结构化响应决定调用哪个函数。
三、函数调用的典型使用场景
在动手之前,先看函数调用能让应用变强的几类场景:
- 调用外部工具(Calling External Tools):聊天机器人擅长回答问题,借助函数调用,它可以把用户消息转化为"执行某个具体任务"的动作。例如学生说"给老师发封邮件,说我这门课需要更多帮助",即可触发
send_email(to: string, body: string)这样的函数调用。 - 构造 API 或数据库查询(Create API or Database Queries):用户用自然语言提问,程序将其转换为格式化查询或 API 请求。例如老师问"哪些学生完成了上次作业",可以触发名为
get_completed(student_name: string, assignment: int, current_status: string)的函数。 - 生成结构化数据(Creating Structured Data):用户粘贴一段文本或 CSV,由 LLM 抽取关键信息。例如学生把维基百科上关于和平协议的条目转成 AI 记忆卡片,可借助
get_important_facts(agreement_name: string, date_signed: string, parties_involved: list)完成。
四、创建第一个函数调用:完整三步流程
本课场景需要三样东西协同工作:用 Azure OpenAI 提供对话体验;用 Microsoft Learn Catalog API 帮用户按需检索课程;用 函数调用 接收用户查询并把参数交给真实函数去发 API 请求。
一次完整的函数调用由三个主步骤构成:
- 调用:带着函数清单(声明)和用户消息调用 Chat Completions API;
- 读取:读取模型返回中指示的动作(应执行哪个函数 / API 请求);
- 再次调用:把真实函数的执行结果追加进消息再调一次 Chat Completions API,让模型据此组织给用户的自然语言回答。
步骤 1:创建用户消息
第一步是构造一条用户消息,其值既可以从文本框动态读取,也可以像下面这样直接赋值。消息需要两个字段:role 与 content。role 有三种取值——system(设定规则)、assistant(模型)、user(终端用户)。函数调用场景下我们把它设为 user,并给一个示例问题:
messages = [{"role": "user", "content": "Find me a good course for a beginner student to learn Azure."}]
通过区分不同角色,LLM 能清楚知道哪句话来自系统、哪句来自用户,从而构建可持续追加的对话历史。
步骤 2:声明函数与参数结构
接下来定义函数名和它的参数。本课只声明一个函数 search_courses,但你可按需声明多个。这里的关键机制是:函数声明会连同系统消息一起发给 LLM,因此会占用你可用的 token 额度,声明越多、描述越长,基础 token 消耗越高。
下面把函数定义成数组,每个元素是一个函数,包含 name、description、parameters 三个属性:
functions = [
{
"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:希望模型"点名"调用的函数名,需与后面真实 Python 函数名一一对应;description:对该函数用途的描述。这里越具体、越清晰,模型越容易在恰当的时候选择它;parameters:希望模型在响应中按此结构与格式生成参数的清单,其内部包含:type:参数对象的数据类型(本课为object);properties:模型会使用的具体字段列表,其中每个字段又包含:name:字段键名,即模型在格式化响应里使用的属性名,如product;type:该字段的数据类型,如string;description:对该字段含义的说明;
- 此外还有可选的
required:标明"完成这次函数调用"所必需的字段,例如本例中role必填,而product、level可选。
步骤 3:发起带函数声明的调用
定义好函数后,需要在 Chat Completion 请求里带上它。做法是传入 functions=functions。同时可以把 function_call 设为 auto,即把"是否调用函数、调用哪一个"的决策权交给 LLM,让模型根据用户消息自行判断,而不是由开发者硬编码:
response = client.chat.completions.create(
model=deployment,
messages=messages,
functions=functions,
function_call="auto"
)
print(response.choices[0].message)
此时返回的消息大致如下:
{
"role": "assistant",
"function_call": {
"name": "search_courses",
"arguments": "{\n \"role\": \"student\",\n \"product\": \"Azure\",\n \"level\": \"beginner\"\n}"
}
}
可以看到 search_courses 被"点名",且 arguments 字段里带着一份参数 JSON。模型之所以能把参数填准,是因为它从本次调用传入的 messages 里抽取了信息——回顾一下消息内容是 "Find me a good course for a beginner student to learn Azure",于是 student、Azure、beginner 被分别映射到了 role、product、level:
messages = [{"role": "user", "content": "Find me a good course for a beginner student to learn Azure."}]
这种方式既是从提示词抽取信息的利器,也为 LLM 提供了确定的结构约束,让函数具备可复用性。下一步就是把这套机制真正接进应用。
五、把函数调用集成进应用程序
前面验证了格式化响应,现在把它接入真实应用。整体流程管理分为四步。
第一步:保存模型返回的消息对象
先调用 OpenAI 服务并把结果存到 response_message 变量中,供后续判断使用:
response_message = response.choices[0].message
第二步:编写对应的真实 Python 函数
现在定义一个真实 Python 函数 search_courses,它会向 Microsoft Learn API 发起外部请求检索培训模块。注意它的参数签名(role, product, level)必须与上一步声明的 functions 中的名字一一对应:
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)
函数内部做了这些事:拼装 https://learn.microsoft.com/api/catalog/ 地址;把三个参数作为查询串发出 GET 请求;从返回 JSON 的 modules 数组中取出前 5 条,抽出 title 与 url;以字符串形式返回列表。
第三步:判断是否需要调用函数并完成调度
声明变量 functions 是"给模型看的说明书",Python 函数是"真正干活的实现",怎么把它们对接起来?答案是:检查模型响应中是否包含 function_call,有则据此调用对应的 Python 函数:
# 判断模型是否想调用某个函数
if response_message.function_call.name:
print("Recommended Function call:")
print(response_message.function_call.name)
print()
# 调用该函数
function_name = response_message.function_call.name
available_functions = {
"search_courses": search_courses,
}
function_to_call = available_functions[function_name]
function_args = json.loads(response_message.function_call.arguments)
function_response = function_to_call(**function_args)
print("Output of function call:")
print(function_response)
print(type(function_response))
# 把 assistant 的响应和函数响应都追加回 messages
messages.append( # 追加 assistant 响应
{
"role": response_message.role,
"function_call": {
"name": function_name,
"arguments": response_message.function_call.arguments,
},
"content": None
}
)
messages.append( # 追加函数响应
{
"role": "function",
"name": function_name,
"content": function_response,
}
)
其中最核心的三行是"抽取函数名 → 解析参数 → 发起调用":
function_to_call = available_functions[function_name]
function_args = json.loads(response_message.function_call.arguments)
function_response = function_to_call(**function_args)
先用 available_functions 字典把模型点名的函数名映射到真实的 Python 可调用对象,再用 json.loads 把 arguments(JSON 字符串)解析成字典,最后通过 **function_args 展开为关键字参数完成调用。程序运行输出如下:
Recommended Function call:
{
"name": "search_courses",
"arguments": "{\n \"role\": \"student\",\n \"product\": \"Azure\",\n \"level\": \"beginner\"\n}"
}
Output of function call:
[{'title': 'Describe concepts of cryptography', 'url': 'https://learn.microsoft.com/training/modules/describe-concepts-of-cryptography/'}, {'title': 'Introduction to audio classification with TensorFlow', 'url': 'https://learn.microsoft.com/training/modules/intro-audio-classification-tensorflow/'}, {'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/'}, {'title': 'Getting started with the Microsoft Cloud Adoption Framework for Azure', 'url': 'https://learn.microsoft.com/training/modules/cloud-adoption-framework-getting-started/'}, {'title': 'Set up the Rust development environment', 'url': 'https://learn.microsoft.com/training/modules/rust-set-up-environment/'}]
<class 'str'>
这里有两个细节值得注意:
- 消息追加顺序有讲究:先追加 assistant 的角色与
function_call(content置None),再追加role: "function"的真实函数结果,这样模型才能把"它要求的调用"与"调用返回的数据"对应起来; - 追加的两类消息都是对话上下文的一部分,第二次请求必须把它们一并带上。
第四步:用函数结果生成自然语言回复
最后把更新过的 messages 再次发给 LLM,让它基于真实课程数据,用自然语言而非 API JSON 格式回答用户:
print("Messages in next request:")
print(messages)
print()
second_response = client.chat.completions.create(
messages=messages,
model=deployment,
function_call="auto",
functions=functions,
temperature=0 # 获得一个能看到函数响应的新回复
)
print(second_response.choices[0].message)
输出示例:
{
"role": "assistant",
"content": "I found some good courses for beginner students to learn Azure:\n\n1. [Describe concepts of cryptography]\n2. [Introduction to audio classification with TensorFlow]\n3. [Design a Performant Data Model in Azure SQL Database with Azure Data Studio]\n4. [Getting started with the Microsoft Cloud Adoption Framework for Azure]\n5. [Set up the Rust development environment]\n\nYou can click on the links to access the courses."
}
至此,一次完整的"用户查询 → 结构化函数调用 → 真实 API 数据 → 自然语言回复"闭环就打通了。
六、对照当前仓库:函数调用的两代 API 形态
捷克语课程文档与其配套 notebook 属于较早期的实现,使用上面讲解的 Chat Completions 参数形态(functions=、function_call="auto"、消息中追加 role:"function")。这是 translations/cs/11-integrating-with-function-calling/python/aoai-assignment.ipynb 与 同目录 oai-assignment.ipynb(面向非 Azure 的 OpenAI)中的写法,可直接对照运行。
与此同时,仓库根目录的英文课程已经随 SDK 演进迁移到较新的 Responses API,核心差异可留意:
- 调用入口从
client.chat.completions.create改为client.responses.create,客户端通过base_url=f"{endpoint}/openai/v1/"指向 v1 端点; - 工具声明从嵌套结构改为扁平 schema:每个工具顶层直接携带
type(值为"function")、name、description、parameters四个字段; - 请求参数从
functions=/function_call=变为tools=/tool_choice=; - 响应中的调用项与上下文追加格式也随之改变:从
response.output中筛出item.type == "function_call",把模型的 function_call 项与{"type":"function_call_output","call_id":...,"output":...}一起追加回messages。
想参考新版写法的读者,可以直接对照同一课根目录的实现:
- 11-integrating-with-function-calling/python/aoai-assignment.ipynb:Responses API 版的完整可运行 notebook;
- TypeScript 示例 main.ts:声明
findWeather工具、校验环境变量与 URL、解析function_call并调用 Bing Maps API 的完整参考,其中还包含"超时防止请求挂起""参数安全解析"等工程化细节; - js-githubmodels/app.js:面向 GitHub Models 的 JavaScript 版本,多个工具对象并列传入
tools: [tool, hotels]。
仓库内的 tests/ 与 shared/ 提供了环境变量、API 工具与输入校验等可复用封装(参见 shared/python/env_utils.py),可作为把函数调用接入真实服务时的工程化参考。
七、课后作业与练习方向
要进一步吃透 Azure OpenAI Function Calling,可以自己动手扩展:
- 为
search_courses增加更多参数,让学习者能检索到更精准的课程; - 新增一个函数调用,获取更多学习者信息(例如其母语),再据此给出推荐;
- 增加错误处理:当函数调用或 API 调用没有返回任何合适课程时的兜底逻辑。
提示:可查阅 Microsoft Learn Catalog API 的开发者参考文档,弄清这些数据以何种字段、在何处可用。
八、本课要点速览
- 函数调用解决两大核心问题:响应格式不可控与模型无法接触外部数据;它输出的只是"结构化声明",真正执行函数的是你的应用代码。
- 一次完整调用包含三次往返:带函数清单发消息 → 解析模型返回的
function_call→ 把真实函数结果回传模型生成最终自然语言回答。 - 函数声明由
name、description、parameters(含type/properties/ 可选required)构成,且会占用 token 额度;描述应具体清晰,参数名与真实 Python 函数签名必须保持一致。 - 集成时按"保存响应 → 实现同名 Python 函数 → 用字典映射并
**function_args展开调用 → 顺序追加消息 → 二次请求"的流程落地。 - 若你的代码环境已升级 SDK,请对照仓库新版 notebook 改用 Responses API 的
tools/tool_choice扁平 schema 写法。
完成本课后,可继续学习第 12 课《为 AI 应用设计 UX》:12-designing-ux-for-ai-applications/README.md。
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 StartedRust0624
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

