首页
/ generative-ai-for-beginners 第 11 课实战:用 Azure OpenAI Function Calling 让大模型调用外部工具、产出结构化数据

generative-ai-for-beginners 第 11 课实战:用 Azure OpenAI Function Calling 让大模型调用外部工具、产出结构化数据

2026-09-04 23:11:56作者:蔡怀权

本文基于《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,由它用自然语言回答用户。

Function Calling 的整体流程:用户提问到 LLM,LLM 生成结构化函数调用,应用执行函数,结果回传 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_KEYAZURE_OPENAI_ENDPOINTAZURE_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) 完成。

创建你的第一个函数调用

创建一次函数调用包含三个主要步骤:

  1. 调用 Responses API,带上你的函数(工具)列表和用户消息;
  2. 读取 模型的响应,执行一个动作——运行函数或 API 请求;
  3. 再次调用 Responses API,把函数的响应带回去,用这些信息生成给用户的最终回答。

LLM 与函数调用之间的三步交互流程

步骤 1:创建消息

第一步是创建用户消息。它可以动态地取自文本输入框的值,也可以直接在这里赋值。第一次使用 Responses API 时,需要定义消息的 rolecontent

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)格式下的一个工具,带有 typenamedescriptionparameters 属性:

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:你希望模型在响应中产出的取值与格式列表,其元素具有如下属性:
    1. type:属性值所存储的数据类型;
    2. properties:模型在响应中使用的具体取值列表:
      1. name:模型在格式化响应中使用的属性名(键),例如 product
      2. type:该属性的数据类型,例如 string
      3. description:对该属性的描述。

另外还有一个可选属性 required——完成函数调用所必需的属性(本例中只有 role 是必需的,productlevel 可选)。

值得一提的是,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=functionstool_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."} ]

studentAzurebeginner 都是从 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 条,取 titleurl)。

那么问题来了:我们有了 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 端点,定义 getFlightInfogetHotelInfo 两个工具,场景是"去迈阿密并订酒店"。与 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_ENDPOINTAZURE_OPENAI_API_KEYBING_MAPS_BASE_URLBING_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 应用设计用户体验。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384