generative-ai-for-beginners 函数调用实战:用 Azure OpenAI Function Calling 实现结构化响应与外部数据集成
【免费下载链接】generative-ai-for-beginners21 Lessons, Get Started Building with Generative AI项目地址: https://gitcode.com/GitHub_Trending/ge/generative-ai-for-beginners
本文基于 generative-ai-for-beginners 课程第 11 课(保加利亚语版教程)展开,完整覆盖 Function Calling 的核心概念、三步创建流程与应用集成代码:你将学会为什么 LLM 的“自由发挥”式响应难以被下游系统消费,如何用functions定义 +function_call="auto"让模型输出严格结构化的函数调用,并把它接入真实 API(Microsoft Learn Catalog)完成一个教育类课程推荐聊天机器人的完整闭环。
课程导读与学习目标
本课是“构建聊天应用”之后的进阶内容,针对两类典型痛点:
- 如何获得更一致的响应格式,让响应在下游系统(API、数据库)中更容易被处理;
- 如何为应用接入其他数据源,进一步丰富聊天机器人能力。
读完本课,你将能够:
- 解释使用 Function Calling 的目的与适用场景;
- 使用 Azure OpenAI Service 配置一次函数调用;
- 为你的应用用例设计高效的函数调用。
本课建议配合仓库中的交互式笔记本实操:保加利亚语版配套笔记本。该笔记本与下文所有代码一一对应(已逐格核对,代码模式完全一致)。
场景:用函数改进教育创业公司的聊天机器人
本课的业务场景:为一个教育初创公司构建功能,让用户通过聊天机器人查找技术课程,系统根据用户的技能水平、当前角色和感兴趣的技术推荐课程。
实现该场景组合了三种能力:
Azure OpenAI:为用户创建聊天体验;Microsoft Learn Catalog API:根据用户请求帮助查找课程;Function Calling:接收用户查询,并将其交给一个发起 API 请求的函数。
先回答一个前置问题:我们为什么需要 Function Calling?
为什么需要函数调用:两个核心限制
在函数调用出现之前,LLM 的响应是非结构化且不一致的。开发者不得不编写复杂的校验代码来处理每一种可能的响应变体;用户也无法得到“斯德哥尔摩现在几点了?”这类答案,因为模型受限于训练数据的时间边界。
Function Calling 是 Azure OpenAI Service 的一项功能,用于克服以下限制:
- 一致的响应格式。如果能控制响应格式,就能更容易地将响应集成到其他下游系统中;
- 外部数据。能够在聊天上下文中使用应用的其他数据源。
用场景演示问题:LLM 返回 JSON 格式的不一致性
下面用一个“学生数据库”例子演示响应格式问题:我们想创建一份学生数据以便为其推荐课程。准备两段结构非常相似的学生描述:
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 连接的 Python 代码,其中设置了api_key与api_version(注意:本课保加利亚语版跟随配套笔记本使用AzureOpenAISDK 的chat.completions接口;仓库英文版同一课已迁移到tools/tool_choice参数风格的新接口,见 英文原版第 11 课,两种写法流程思想完全一致)。
接着创建两段学生描述:
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 或写入数据库。现在创建两个内容完全相同的提示词,指示 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} '''提示词要求 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变量中并赋以user角色,以模拟用户写给聊天机器人的一条消息。两个响应都可以按openai_response1.choices[0].message.content找到。最后用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.7 GPA。原因是 LLM 接收的是以提示词形式写入的非结构化数据,返回的也是非结构化数据。要在存储或使用这些数据前知道“该期待什么”,就必须有一个结构化格式。
函数调用如何解决格式问题
通过函数调用,可以确保拿到结构化数据。这里有一个必须澄清的概念:LLM 并不会真正调用或执行任何函数。实际机制是:我们为 LLM 的响应创建一套结构(函数签名与参数 schema),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)。
创建你的第一次函数调用:三步流程
创建一次函数调用包含三个主要步骤:
- 调用:带着你的函数列表和用户消息调用聊天补全 API;
- 读取:解析模型响应以执行动作,即运行函数或发起 API 请求;
- 再调用:带着函数的返回结果再次调用 API,让模型基于这些信息生成面向用户的回复。
步骤 1:创建消息
第一步是创建用户消息:既可以动态地从文本输入框取值,也可以直接在这里赋固定值。首次接触聊天补全 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 预算。
函数以数组形式创建,每个元素是一个函数,具有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:函数如何工作的描述,这里务必具体、清晰,它是模型判断“何时调用该函数、如何填参”的主要依据;parameters:你希望模型在响应中产出的取值与格式列表,各元素含:type— 属性所存储值的数据类型(此处为object);properties— 模型将使用的具体取值列表,每个属性含:name(键名):模型在格式化响应中使用的属性名,例如product;type:该属性的数据类型,例如string;description:对该属性的具体描述。
此外还有可选属性required:完成函数调用所必需的属性。上例中只有role是必填项,product和level可选。
步骤 3:执行函数调用
定义函数后,需要把它包含进 API 请求——做法是在请求中加入functions参数(本例为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属性里那段 JSON 字符串。
结论:LLM 从传入messages参数的值中提取出了匹配函数参数的数据。回顾一下messages的值:
messages= [ {"role": "user", "content": "Find me a good course for a beginner student to learn Azure."} ]student、Azure、beginner三个词被从消息中抽取出来并作为函数输入。这样使用函数既是从提示词中抽取信息的优秀手段,也是给 LLM 施加结构、获得可复用功能的方式。下一步看看如何在应用里真正用起来。
在应用中集成函数调用:管理流程
测试过 LLM 的格式化响应后,就可以把它集成进应用了。
第 1 步:保存响应消息
先调用服务并把响应消息存入变量:
response_message = response.choices[0].message第 2 步:定义真正执行外部请求的 Python 函数
接下来定义将调用 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)注意两点:我们创建的真实 Python 函数名必须与functions变量中声明的函数名一一对应;同时这里发起的是真实的外部 API 请求(Microsoft Learn API 检索培训模块),只取前 5 条模块的标题与链接,最后以字符串形式返回——因为回填给 LLM 的content需要是字符串。
第 3 步:判断是否要调用函数并分发执行
要判断是否调用 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)available_functions字典把“LLM 声明的函数名”映射到“本应用真实可执行的函数对象”,这是模型意图与应用代码之间的桥梁;json.loads把模型输出的arguments字符串解析成字典,再经**展开为关键字参数。执行后,我们把两样东西追加回messages:模型的助手消息(含function_call字段、content为None)以及role为function的函数结果消息——这构成完整的调用上下文。
实际运行输出如下:
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/?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'>可以看到函数返回的是一个字符串化的列表(<class 'str'>)。
第 4 步:把函数结果送回 LLM,换取自然语言回复
最后,把更新后的messages(现在包含用户消息、函数调用消息、函数结果消息)再次发给 LLM,此时会得到自然语言回复而非 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] (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/en-us/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 数据 → 自然语言答复。
源码佐证:仓库中同一模式的 JavaScript 实现与安全实践
本仓库在同一主题下还提供了一个 JavaScript 示例 js-githubmodels/app.js,用 Azure AI Inference SDK 实现了与上文完全同构的“模型 → 工具调用 → 本地函数 → 回填对话”流程,可以从中印证几个关键工程细节:
- 响应判定方式:JS 示例通过
response.body.choices[0].finish_reason === "tool_calls"判断模型要求调用工具(见 app.js#L127-L134),这与 Python 版“检查response_message.function_call.name是否非空”是等价的防御式判断——不能假设模型一定会调用函数,必须显式检查; - 白名单校验:模型返回的函数名来自模型生成内容,属于不可信输入。JS 示例在分发前先校验函数名是否存在于允许的映射表中,不存在则直接抛错(见 app.js#L141-L145);
- 安全解析参数:对
arguments字符串的 JSON 解析包裹在try/catch中,解析失败时抛出带上下文信息的错误(见 app.js#L147-L153)。
从源码结构看,这些细节正是把教程中的available_functions字典分发模式落地到生产时的加固方向:函数名白名单 + 参数解析错误处理。此外,JS 示例中工具定义采用{"type": "function", "function": {...}}的嵌套结构,与 Python 版的扁平functions数组字段一一对应,印证了两种 SDK 只是序列化形态不同,函数 schema(name/description/parameters/required)的语义完全一致。
多函数场景下,JS 示例同时注册了getFlightInfo与getHotelInfo两个工具,并依赖模型的tool_choice自动选择——这与本课“可以创建多个函数、由 LLM 决定调用哪个”的说明相互印证。
延伸作业
为继续深入 Azure OpenAI Function Calling,教程建议尝试构建以下扩展:
- 为函数增加更多参数,帮助学习者找到更多课程(可参考 Microsoft Learn Catalog API 的开发者参考文档了解可用参数);
- 创建另一个函数调用,从学习者处采集更多维度信息,例如母语(native language);
- 当函数调用和/或 API 调用没有返回合适课程时,编写错误处理逻辑(这一点可参考上文 JS 示例的安全解析写法)。
小结与继续学习
本课的完整技术主线可以概括为:用functions定义把“响应结构”从提示词技巧升级为可校验的 schema;用function_call="auto"让模型负责抽取参数;用应用侧的函数映射字典负责真实执行;最后把执行结果回填对话,由模型生成自然语言答复。它同时解决了响应格式一致性与外部数据接入两个问题,是构建可靠 AI 应用的基础机制。
- 完整可运行代码见 保加利亚语版配套笔记本;
- 对照阅读 英文原版第 11 课(新版 SDK 接口风格)与 JavaScript 示例;
- 下一课:设计 AI 应用的用户体验(第 12 课,保加利亚语版)。
【免费下载链接】generative-ai-for-beginners21 Lessons, Get Started Building with Generative AI项目地址: https://gitcode.com/GitHub_Trending/ge/generative-ai-for-beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考