generative-ai-for-beginners 第 11 课:用函数调用(Function Calling)让生成式 AI 应用接入外部数据与结构化输出
【免费下载链接】generative-ai-for-beginners21 Lessons, Get Started Building with Generative AI项目地址: https://gitcode.com/GitHub_Trending/ge/generative-ai-for-beginners
本文围绕本仓库第 11 课文档展开,完整讲解「函数调用」这一 Azure OpenAI 核心能力:它如何解决 LLM 响应格式不一致、无法访问外部实时数据这两大痛点,并通过「教育创业公司课程推荐聊天机器人」的完整案例,带你从零创建第一个函数调用,并把函数调用真正集成进应用程序。读完本文,你将掌握函数调用的原理、三步式调用流程、函数 schema 的每个字段含义,以及消息往返编排的完整代码模式。
为什么需要函数调用
在前面的课程中我们已经见识了 LLM 的强大,但它也存在两个明显局限:
- 响应非结构化、不一致。函数调用出现之前,LLM 的响应既无结构也不稳定,开发者不得不编写复杂的校验代码,去处理每一种可能的响应变体。
- 无法访问训练时间之后的外部数据。模型的知识被限制在训练数据的时间点上,因此用户问"斯德哥尔摩现在的天气如何?"这类实时问题,模型无法回答。
函数调用(Function Calling)是 Azure OpenAI 服务提供的一项能力,正是用来克服上述限制:
- 一致的响应格式:能更好地控制响应格式,就能更轻松地把响应集成到下游的其他系统;
- 外部数据:可以把应用中其他来源的数据,引入到聊天上下文中使用。
场景演示:先看看"格式不一致"到底有多痛
本文的完整场景是:为一家教育创业公司构建"课程推荐聊天机器人",用户通过聊天找到符合自己技能水平、当前角色和兴趣技术的课程。解决方案组合三样东西:用Azure OpenAI提供聊天体验、用Microsoft Learn Catalog API按用户请求检索课程、用Function Calling把用户查询送入函数以发起 API 请求。
动手前,先看一个最能说明问题的例子。假设我们要建立学生数据库以便推荐合适课程,下面是两条包含非常相似数据的学生描述。我们想让 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'], # this is also the default, it can be omitted api_version = "2023-07-01-preview" ) deployment=os.environ['AZURE_OPENAI_DEPLOYMENT']上面这段 Python 代码通过api_type、api_base、api_version、api_key等参数配置与 Azure OpenAI 的连接。需要注意的是,仓库内更新的教学文件(例如 英文版 README 与配套 notebook)已迁移到 Responses API 的 v1 端点写法:改用OpenAI(api_key=..., base_url=f"{endpoint.rstrip('/')}/openai/v1/"),无需再传api_version,对应实现可参考 shared/python/api_utils.py 中的create_azure_openai_client。
第二步:创建两条学生描述
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."第三步:构造两条完全相同的提取指令
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} '''这两条提示词要求 LLM 提取信息并以 JSON 格式返回。
第四步:把提示词发送给 LLM
# response from prompt one openai_response1 = client.chat.completions.create( model=deployment, messages = [{'role': 'user', 'content': prompt1}] ) openai_response1.choices[0].message.content # response from prompt two openai_response2 = client.chat.completions.create( model=deployment, messages = [{'role': 'user', 'content': prompt2}] ) openai_response2.choices[0].message.content提示词保存在messages变量中,role设为user,用以模拟用户向聊天机器人发消息。
第五步:用json.loads把响应转成 JSON
# Loading the response as a JSON object 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 不执行函数,只负责"产生结构"
那么如何解决格式问题?答案是函数调用。使用函数调用时,LLM 实际上并不会真的去调用或运行任何函数,而是由我们为 LLM 创建一套它必须遵循的响应结构;然后应用根据这些结构化响应,决定在程序里执行哪个真实函数。
之后,我们把函数返回的数据拿回来再发回给 LLM,LLM 用自然语言回答用户的提问。这也是后续集成章节中"两次调用"循环的由来。
函数调用的典型使用场景
函数调用能在很多场景下显著改进应用:
- 调用外部工具:聊天机器人擅长回答问题,借助函数调用,它还能利用用户消息完成特定任务。例如学生说"给我的老师发一封邮件,说我需要这门课的更多帮助",即可触发
send_email(to: string, body: string)这个函数调用。 - 生成 API 或数据库查询:用户用自然语言查找信息,被转换为格式化查询或 API 请求。例如老师问"哪些学生完成了最后一次作业",可调用
get_completed(student_name: string, assignment: int, current_status: string)函数。 - 生成结构化数据:用户可以把一段文本或 CSV 交给 LLM 提取关键信息。例如学生把关于和平协议的维基百科文章转成 AI 记忆卡片,可通过
get_important_facts(agreement_name: string, date_signed: string, parties_involved: list)完成。
创建你的第一个函数调用
函数调用创建过程包含 3 个主要步骤:
- 调用Chat Completions API,传入函数列表和用户消息;
- 读取模型响应并执行动作,即运行函数或 API 调用;
- 再次调用Chat Completions API,把函数返回的响应一并传入,让模型据此生成对用户的回复。
第 1 步:创建消息
第一步是创建用户消息。可以从文本输入动态赋值,也可以在这里直接赋值。初次使用 Chat Completions 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 能清楚知道哪部分是系统说的、哪部分是用户说的,从而基于对话历史持续构建上下文。
第 2 步:创建函数
接下来定义函数及其参数。这里只使用一个名为search_courses的函数,但你可以创建多个函数。
重要:函数会包含在发给 LLM 的系统消息中,并计入可用 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 | 你要调用的函数名称 |
description | 对函数工作方式的说明,这里要写具体、清晰,它直接决定 LLM 在何时选择该函数 |
parameters | 模型生成响应时将要使用的取值列表与格式 |
parameters内部由若干条目组成,每个条目包含:
type—— 属性存储的数据类型(如object);properties—— 模型在响应中会使用的具体取值列表,其中每一项又包含:name:模型在格式化响应中使用的属性名,例如product;type:该属性的数据类型,例如string;description:对该属性的说明。
此外还有一个可选属性required,列出函数调用完成所必需的属性。
在仓库配套实现中可以看到同样 schema 的其他形态:JavaScript 版 js-githubmodels/app.js 定义了getFlightInfo、getHotelInfo两个工具,TypeScript 版 typescript/function-app/src/main.ts 定义了findWeather工具,并给unit参数增加了enum: ["C", "F"]取值约束——枚举是约束参数取值范围的实用技巧。
第 3 步:发起函数调用
定义好函数后,把它加入 Chat Completions 请求,通过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函数被调用,其参数列在 JSON 响应的arguments属性中。
LLM 之所以能提炼出匹配函数参数的数据,是因为它从messages参数提供的值中完成了提取。回顾一下消息内容:
messages= [ {"role": "user", "content": "Find me a good course for a beginner student to learn Azure."} ]显然,student、Azure、beginner这三个词从messages中被提取出来,作为函数输入。以这种方式使用函数,是从提示词中抽取信息、为 LLM 提供结构、并沉淀可复用功能的好方法。
版本说明:上述代码采用 Chat Completions API 的
functions/function_call参数。仓库当前英文版文档与配套 notebook(python/aoai-assignment.ipynb)已升级为 Responses API 写法:请求参数变为tools=functions与tool_choice="auto",返回结构变为response.output中type为function_call的条目(含call_id与arguments)。两种写法核心思想一致,本文后续集成代码仍以关联文档的 Chat Completions 写法为主线。
把函数调用集成进应用程序
验证了 LLM 的格式化响应之后,就可以把它集成到应用中。核心是管理好"两次请求"的消息流:第一次拿到结构化函数调用,执行真实函数,把结果拼回消息列表,第二次再让 LLM 用自然语言总结。
管理调用流程
第 1 步:保存模型返回的消息
response_message = response.choices[0].message第 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 条结果)。
从工程角度看,这类"带超时、带重试、带错误处理"的 HTTP 请求在仓库中已有封装可复用,例如 shared/python/api_utils.py 的make_safe_request提供了默认 30 秒超时与 3 次重试;TypeScript 示例 typescript/function-app/src/main.ts 则为 Bing Maps 请求设置了 10 秒超时并对失败返回结构化错误。
第 3 步:检查模型是否要求调用函数,并完成"名称 → 函数"映射
有了functions变量和对应的 Python 函数,如何把它们映射起来?答案是:检查 LLM 响应中是否包含function_call,若包含则调用指定函数:
# Check if the model wants to call a function if response_message.function_call.name: print("Recommended Function call:") print(response_message.function_call.name) print() # Call the function. 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)) # Add the assistant response and function response to the messages messages.append( # adding assistant response to messages { "role": response_message.role, "function_call": { "name": function_name, "arguments": response_message.function_call.arguments, }, "content": None } ) messages.append( # adding function response to messages { "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)运行上述代码的输出:
输出
{ "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/?WT.mc_id=api_CatalogApi'}, {'title': 'Introduction to audio classification with TensorFlow', 'url': 'https://learn.microsoft.com/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'>注意available_functions字典充当白名单:只有注册过的函数才允许被调用。仓库的 JavaScript 示例对此有更严格的安全处理——调用前用Object.prototype.hasOwnProperty.call(namesToFunctions, functionName)校验函数名是否在白名单中,并对JSON.parse包裹 try/catch 防止畸形参数导致崩溃,见 js-githubmodels/app.js;TypeScript 版同样在 typescript/function-app/src/main.ts 对参数解析做了保护。这些都是在生产环境集成函数调用时必须补齐的防线。
第 4 步:把更新后的messages再次发给 LLM,获得自然语言回复
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 ) # get a new response from GPT where it can see the function response 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] (https://learn.microsoft.com/training/modules/describe-concepts-of-cryptography/?WT.mc_id=api_CatalogApi)\n2. [Introduction to audio classification with TensorFlow](https://learn.microsoft.com/training/modules/intro-audio-classification-tensorflow/?WT.mc_id=api_CatalogApi)\n3. [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)\n4. [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)\n5. [Set up the Rust development environment](https://learn.microsoft.com/training/modules/rust-set-up-environment/?WT.mc_id=api_CatalogApi)\n\nYou can click on the links to access the courses." }至此,模型已经"看到"了真实 API 返回的课程列表,并用自然语言组织成带链接的推荐回复。这里把temperature设为0,让第二次总结的输出更确定、更贴近事实,减少编造。
仓库配套实现一览
除了文档代码,本课目录下还提供了可直接运行的多语言实现,供对照学习:
- Python notebook:python/aoai-assignment.ipynb(Azure OpenAI 版)、python/oai-assignment.ipynb(OpenAI 版),均采用 Responses API 的
tools/tool_choice写法,并用function_call_output回填函数结果。 - JavaScript(GitHub Models / Azure AI Inference):js-githubmodels/app.js,通过
@azure-rest/ai-inference客户端调用/chat/completions,演示航班 + 酒店查询双工具场景。 - TypeScript(Azure OpenAI):typescript/function-app/src/main.ts,使用官方
openaiSDK 的client.responses.create,演示天气查询,并在其中演示了 URL 校验、超时、参数白名单校验等安全最佳实践;运行脚本见 typescript/function-app/package.json。 - 公共工具:shared/python/api_utils.py 提供
create_azure_openai_client(v1 端点客户端工厂)与make_safe_request(带超时重试的 HTTP 封装)。
课后练习
要深入掌握 Azure OpenAI 函数调用,可以动手完成以下挑战:
- 为
search_courses函数增加更多参数(例如课程时长、认证类型),帮助学习者找到更匹配的课程; - 新建另一个函数调用,采集学习者更多信息,例如母语(
native_language); - 为函数调用和/或 API 调用增加错误处理——当没有返回合适课程时给出兜底提示。
提示:可查阅 Microsoft Learn Catalog API 的开发者参考文档,确认上述数据在 API 中的字段与位置。
学完本课,还可以继续阅读本仓库的第 12 课 为 AI 应用设计 UX,了解如何把这些能力包装成更好的用户界面与交互体验。
【免费下载链接】generative-ai-for-beginners21 Lessons, Get Started Building with Generative AI项目地址: https://gitcode.com/GitHub_Trending/ge/generative-ai-for-beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考