3. 并行函数调用
默认情况下,在OpenAI 的大模型生态中,2023 年 11 月 6 日或之后发布的任何模型都可能在单个响应中生成多个函数调用,这说明这类模型可以并行调用某个函数。这在一些场景下是非常有用的,比如如果执行给定函数需要很长时间的时候。例如,模型可能会调用函数同时获取 3 个商品信息,但并行调用会在在 tool_calls 数组中产生包含 3 个函数调用的消息。我们来进行如下测试:
python
messages = [
{"role": "user", "content": "你好,你家都卖什么球,什么衣服,什么鞋?"}
]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
response
for tool_call in response.choices[0].message.tool_calls:
print(tool_call)
其中,tool_calls 数组中的每个函数调用都有一个唯一的id :
python
# 这里我们迭代的执行结果:
for tool_call in response.choices[0].message.tool_calls:
arguments = json.loads(tool_call.function.arguments)
product_name = arguments['product_name']
final_res = query_by_product_name(product_name)
print(f"{product_name}: {final_res} \n")
而如果我们想在单次的对话中记录每个函数调用的结果,就可以通过向每个函数调用的对话添加一条新消息来将结果提供回模型,每条消息都包含一个函数调用的结果,并使用tool_call_id引用来自的id tool_calls ,如下所示:
python
product_info = {}
# 遍历工具调用处理每一个产品名称查询
for tool_call in response.choices[0].message.tool_calls:
# 解析调用参数
arguments = json.loads(tool_call.function.arguments)
product_name = arguments['product_name']
# 执行查询并获取结果
query_results = query_by_product_name(product_name)
# 格式化输出到字典, query_results 返回的列表中包含完整的产品信息
# 提取所需信息,假设每个结果包含 'product_name', 'description', 'price' 等字段
if query_results:
for result in query_results:
product_id, name, description, specifications, usage, brand, price, stock = result
product_info[name] = {
"描述": description,
"规格": specifications,
"适用场合": usage,
"品牌": brand,
"价格": f"{price}元",
"库存数量": stock
}
else:
product_info[product_name] = "未找到相关产品数据"
python
# 打印整理好的产品信息字典
for product_name, details in product_info.items():
print(f"产品名称:{product_name}")
if isinstance(details, dict):
for detail_key, detail_value in details.items():
print(f"{detail_key}: {detail_value}")
else:
print(details)
print() # 用于在每个产品信息之后添加一个空行以提高可读性
这是单次函数调用拼接的:
python
```json
final_res = query_by_product_name(product_name)
content = json.dumps({
"product_name": product_name,
"query_by_product_name":final_res
}, ensure_ascii=False),
function_call_result_message = {"role": "tool", "content": str(content), "tool_call_id": response.choices[0].message.tool_calls[0].id}
```
python
response.choices[0].message
messages = [
{"role": "user", "content": "你好,你家都卖什么球,什么衣服,什么鞋?"},
response.choices[0].message,
]
messages
python
for tool_call in response.choices[0].message.tool_calls:
# 解析调用参数
arguments = json.loads(tool_call.function.arguments)
product_name = arguments['product_name']
# 执行查询并获取结果
query_results = query_by_product_name(product_name)
messages.append({"role": "tool", "content": str(query_results), "tool_call_id": tool_call.id})
messages
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
print(response.choices[0].message.content)
4. 多函数调用
多函数调用其实就不是很复杂了。我们只需要新增函数,并且编写具体的函数说明就可以了。比如我们现在接入智能电商客服的第二个功能:可以根据用户对商品的提问查询对应的优化政策,那么接下来我们定义一个` read_store_promotions `函数根据提供的产品名称来读取具体的优惠政策。代码如下:
python
def read_store_promotions(product_name):
# 指定优惠政策文档的文件路径
file_path = 'store_promotions.txt'
try:
# 打开文件并按行读取内容
with open(file_path, 'r', encoding='utf-8') as file:
promotions_content = file.readlines()
# 搜索包含产品名称的行
filtered_content = [line for line in promotions_content if product_name in line]
# 返回匹配的行,如果没有找到,返回一个默认消息
if filtered_content:
return ''.join(filtered_content)
else:
return "没有找到关于该产品的优惠政策。"
except FileNotFoundError:
# 文件不存在的错误处理
return "优惠政策文档未找到,请检查文件路径是否正确。"
except Exception as e:
# 其他潜在错误的处理
return f"读取优惠政策文档时发生错误: {str(e)}"
创建模拟的优惠政策,将其保存为本地的一个 `.txt `文件。
python
# 重新创建一个包含店铺优惠政策的文本文档
promotions_content = """
店铺优惠政策:
1. 足球 - 购买足球即可享受9折优惠。
2. 羽毛球拍 - 任意购买羽毛球拍两支以上,享8折优惠。
3. 篮球 - 单笔订单满300元,篮球半价。
4. 跑步鞋 - 第一次购买跑步鞋的顾客可享受满500元减100元优惠。
5. 瑜伽垫 - 每购买一张瑜伽垫,赠送价值50元的瑜伽教程视频一套。
6. 速干运动衫 - 买三送一,赠送的为最低价商品。
7. 电子计步器 - 购买任意电子计步器,赠送配套手机APP永久会员资格。
8. 乒乓球拍套装 - 乒乓球拍套装每套95折。
9. 健身手套 - 满200元包邮。
10. 膝盖护具 - 每件商品配赠运动护膝一个。
注意:
- 所有优惠活动不可与其他优惠同享。
- 优惠详情以实际到店或下单时为准。
"""
# 将优惠政策写入文件
file_path = './store_promotions.txt'
with open(file_path, 'w', encoding='utf-8') as file:
file.write(promotions_content)
file_path
测试函数功能:
python
product_name = '瑜伽垫'
promotion_details = read_store_promotions(product_name)
print(promotion_details)
定义` read_store_promotions `函数的Json Schema描述,并添加到 tools 列表中。
python
tools = [
{
"type": "function",
"function": {
"name": "query_by_product_name",
"description": "Query the database to retrieve a list of products that match or contain the specified product name. This function can be used to assist customers in finding products by name via an online platform or customer support interface.",
"parameters": {
"type": "object",
"properties": {
"product_name": {
"type": "string",
"description": "The name of the product to search for. The search is case-insensitive and allows partial matches."
}
},
"required": ["product_name"]
}
}
},
{
"type": "function",
"function": {
"name": "read_store_promotions",
"description": "Read the store's promotion document to find specific promotions related to the provided product name. This function scans a text document for any promotional entries that include the product name.",
"parameters": {
"type": "object",
"properties": {
"product_name": {
"type": "string",
"description": "The name of the product to search for in the promotion document. The function returns the promotional details if found."
}
},
"required": ["product_name"]
}
}
}
]
python
available_functions = {"query_by_product_name": query_by_product_name, "read_store_promotions":read_store_promotions}
python
from openai import OpenAI
client = OpenAI()
messages = []
while True:
prompt = input('\n提出一个问题: ')
if prompt.lower() == "退出":
break # 如果输入的是"退出",则结束循环
# 添加用户的提问到消息列表
messages.append({'role': 'user', 'content': prompt})
# 检查是否需要调用外部函数
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
parallel_tool_calls=False # 这里需要格外注意
)
# 提取回答内容
response = completion.choices[0].message
tool_calls = completion.choices[0].message.tool_calls
# 处理外部函数调用
if tool_calls:
function_name = tool_calls[0].function.name
function_args = json.loads(tool_calls[0].function.arguments)
function_response = available_functions[function_name](**function_args)
messages.append(response)
messages.append(
{
"role": "tool",
"name": function_name,
"content": str(function_response),
"tool_call_id": tool_calls[0].id,
}
)
second_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
# 获取最终结果
final_response = second_response.choices[0].message.content
messages.append({'role': 'assistant', 'content': final_response})
print(final_response)
else:
# 打印响应并添加到消息列表
print(response.content)
messages.append({'role': 'assistant', 'content': response.content})
如上面的运行结果所示,在Function Calling架构中,尽管可以通过多函数和并行函数调用逻辑来调用外部函数,实现一些具体的操作流程,但它仍面临一些局限性。例如,当面对用户的单条复杂请求时,如"你家卖健身手套吗?现在有什么优惠?",虽然我们配置了两个相应的外部函数,理论上能够处理这一请求,但当前的架构无法自动按照一定的执行顺序依次调用这些函数,并在同一轮对话中直接输出结果。理想的处理流程应该是:首先通过`query_by_product_name`函数确认是否销售健身手套;如果有,接着调用`read_store_promotions`函数获取关于健身手套的优惠政策;最后,结合产品价格和优惠信息,直接为用户计算出最终结果。这种需要规划和连续决策的能力,已经超出了智能助理的常规范围,而更接近于智能代理的"Planning"能力。因此,这种复杂的需求处理揭示了向真正的智能代理迈进的必要性。
所以我们说,以看到,智能助手能够根据我们的规范操作,让大模型理解用户意图并自动执行一些任务。而智能代理则更进一步,它们不仅自主执行多种任务、做出决策和解决问题,还能与现实世界表而经过上面两个案例的复现,里,应该已经希望大家对这两者之间的区别已有了更清晰而至此,大家也就能理解了:Function Calling不能单独构成智能代理,而只是其组成部分之一。OpenAI通过Assistant API实现智能代理的功能,这一点我们将在后续课程中详细介绍。此外,智能代理的实现并不仅限于OpenAI,基于ReAct理念,任何大型模型都能快速开发出定制化的AI Agent。事实上,许多主流框架都采用了ReAct的技术变种。接下来的课程中,我们将深入探讨ReAct的原理,并通过它的架构思想来实现一个完整的电视智能客服案例。功能和应用范围。
5. 加餐:结构化输出
通过上述过程其实也不难发现,函数调用的过程非常依赖 Json 结构化的输出,默认情况下,当使用函数调用时,OpenAI 的 API 将尽力匹配工具调用的参数,但是这也有风险:在使用复杂模式时大模型有时可能会丢失参数或得到错误的类型。也就是说: 如果 在函数调用阶段 大模型根据 用户的自然语言 没有很好的理解意图,那么其 `function.arguments` 参数不匹配直接会使函数无法执行,从而导致整个函数调用的过程失败。所以,在 2024 年 8 月,OpenAI推出了结构化输出功能,这个功能可以极大的提升函数调用生成的参数与我们在函数定义中提供的 JSON 架构完全匹配的准确率,应该方法也非常简单,如下所示:
这里可以借助`Pydantic `来实现。Pydantic 通过基于 Python 类型标注的模型来确保数据类型正确,其内置实现了一个强大的系统来进行数据解析、校验和文档生成。
python
from pydantic import BaseModel
class GetProductName(BaseModel):
product_name: str
如上所示,我们 定义了一个名为 `GetProductName` 的类,它继承自 `BaseModel`。这种继承方式允许 `GetProductName` 类利用 Pydantic 提供的所有功能,如自动数据验证、序列化和反序列化等。同时 `product_name: str` 指明 `GetProductName` 模型有一个属性 `product_name`,并且这个属性应该是一个字符串类型(str)。这意味着任何尝试创建 `GetProductName` 实例并为 product_name 提供非字符串类型值的操作都将引发类型错误。
然后,通过`openai`的`pydantic_function_tool`方法对工具进行封装。
python
import openai
tools = [openai.pydantic_function_tool(GetProductName)]
messages = [
{"role": "user", "content": "你好,你家都卖什么球?"},
]
response = client.chat.completions.create(
model='gpt-4o-mini',
messages=messages,
tools=tools
)
print(response.choices[0].message.tool_calls[0])
messages = [
{"role": "user", "content": "你好,你家都卖什么鞋?"},
]
response = client.chat.completions.create(
model='gpt-4o-mini',
messages=messages,
tools=tools
)
print(response.choices[0].message.tool_calls[0])
函数调用有几个重要的目的:
-
增强与外部工具的交互:GPT-4 和 GPT-3.5 等LLMs已经过微调,可以识别何时需要调用函数。通过这样做,他们可以输出包含调用函数所需参数的 JSON。此功能可实现与外部工具和 API 的无缝交互。
-
构建LLM支持的聊天机器人和代理:函数调用对于构建有效利用外部工具回答问题的对话代理至关重要。
-
数据提取和标记: LLM支持的解决方案可以提取和标记数据。例如,他们可以从维基百科文章或其他文本源中提取人名。
-
自然语言到 API 调用:函数调用使应用程序能够将自然语言提示转换为有效的 API 调用或数据库查询。它弥合了人类语言和结构化数据交互之间的差距。
-
会话式知识检索引擎: LLMs可以与知识库交互,这使得它们对于构建会话式知识检索引擎很有价值
结构化输出是一项功能,可确保大模型始终生成符合提供的JSON Schema的响应,因此很大程度上,我们无需担心大模型会省略所需的键,或产生无效的枚举值。而目前,OpenAI API 中的结构化输出有两种形式:
-
使用函数调用时
-
使用json_schema响应格式时
函数调用就正如我们上面一直尝试的例子,但除此之外,这个结构化输出也可用于在普通对话过程中结构化模型的输出响应。比如:
python
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]
class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]
completion = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "提取事件信息。"},
{"role": "user", "content": "李华和张明星期五要去参加科学博览会。"},
],
response_format=CalendarEvent,
)
event = completion.choices[0].message.parsed
print(event)
但是,并不是所有模型都支持结构化输出,其官网说明如下所示:https://platform.openai.com/docs/guides/structured-outputs/introduction