首页
/ Generative AI for Beginners 第 11 课:用 Azure OpenAI Function Calling 为聊天机器人接入外部数据与工具

Generative AI for Beginners 第 11 课:用 Azure OpenAI Function Calling 为聊天机器人接入外部数据与工具

2026-09-06 18:57:38作者:劳婵绚Shirley

函数调用(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 能力,但有两个问题仍然待解:

  1. 响应格式不稳定:LLM 返回的文本是非结构化、不统一的,开发者必须编写大量校验代码来兼容每一种输出变体,才能把响应交给下游系统处理。
  2. 无法获取外部数据:模型受限于训练数据的截止时间与范围,用户问不出"斯德哥尔摩现在的天气如何"这类需要实时数据的问题。

函数调用正是 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_KEYAZURE_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.73.8 GPA 两种格式——前者是数值,后者带了单位后缀。根因在于:LLM 输入的是"写在提示词里的非结构化文本",返回的也必然是非结构化文本。当我们想把数据存储或复用时,必须有确定的结构才能知道"接下来拿到的一定是什么"。

函数调用正是用来解决这个格式化问题的方案——为 LLM 声明一套响应结构,由应用依据结构化响应决定调用哪个函数。

函数调用流程图:用户输入 → LLM 返回结构化 function_call → 应用执行函数 → 结果回传 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 请求。

一次完整的函数调用由三个主步骤构成:

  1. 调用:带着函数清单(声明)和用户消息调用 Chat Completions API;
  2. 读取:读取模型返回中指示的动作(应执行哪个函数 / API 请求);
  3. 再次调用:把真实函数的执行结果追加进消息再调一次 Chat Completions API,让模型据此组织给用户的自然语言回答。

LLM 与函数的交互流程:messages 循环往返于模型与应用函数之间

步骤 1:创建用户消息

第一步是构造一条用户消息,其值既可以从文本框动态读取,也可以像下面这样直接赋值。消息需要两个字段:rolecontentrole 有三种取值——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 消耗越高。

下面把函数定义成数组,每个元素是一个函数,包含 namedescriptionparameters 三个属性:

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:希望模型在响应中按此结构与格式生成参数的清单,其内部包含:
    1. type:参数对象的数据类型(本课为 object);
    2. properties:模型会使用的具体字段列表,其中每个字段又包含:
      • name:字段键名,即模型在格式化响应里使用的属性名,如 product
      • type:该字段的数据类型,如 string
      • description:对该字段含义的说明;
  • 此外还有可选的 required:标明"完成这次函数调用"所必需的字段,例如本例中 role 必填,而 productlevel 可选。

步骤 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",于是 studentAzurebeginner 被分别映射到了 roleproductlevel

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 条,抽出 titleurl;以字符串形式返回列表。

第三步:判断是否需要调用函数并完成调度

声明变量 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.loadsarguments(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_callcontentNone),再追加 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")、namedescriptionparameters 四个字段;
  • 请求参数从 functions= / function_call= 变为 tools= / tool_choice=
  • 响应中的调用项与上下文追加格式也随之改变:从 response.output 中筛出 item.type == "function_call",把模型的 function_call 项与 {"type":"function_call_output","call_id":...,"output":...} 一起追加回 messages

想参考新版写法的读者,可以直接对照同一课根目录的实现:

仓库内的 tests/shared/ 提供了环境变量、API 工具与输入校验等可复用封装(参见 shared/python/env_utils.py),可作为把函数调用接入真实服务时的工程化参考。

七、课后作业与练习方向

要进一步吃透 Azure OpenAI Function Calling,可以自己动手扩展:

  • search_courses 增加更多参数,让学习者能检索到更精准的课程;
  • 新增一个函数调用,获取更多学习者信息(例如其母语),再据此给出推荐;
  • 增加错误处理:当函数调用或 API 调用没有返回任何合适课程时的兜底逻辑。

提示:可查阅 Microsoft Learn Catalog API 的开发者参考文档,弄清这些数据以何种字段、在何处可用。

八、本课要点速览

  • 函数调用解决两大核心问题:响应格式不可控模型无法接触外部数据;它输出的只是"结构化声明",真正执行函数的是你的应用代码。
  • 一次完整调用包含三次往返:带函数清单发消息 → 解析模型返回的 function_call → 把真实函数结果回传模型生成最终自然语言回答。
  • 函数声明由 namedescriptionparameters(含 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

登录后查看全文
热门项目推荐
相关项目推荐