Loading... # 4. Function calling 函数调用 # 简介 ## 简介 在上一篇中,我们基于对话实现了一个简单的应用。而实际上,OpenAI API 专门提供了函数调用,用来构建外部应用。 Function calling 简单说,就是给大模型提供外部调用的能力,可以是对外界做出控制,也可以是从外部获取信息。通过外部调用,可以极大的提高 GPT 能做到的事情的上限 > 几个重要的链接: > > OpenAI Function Calling 文档:[https://platform.openai.com/docs/guides/function-calling](https://platform.openai.com/docs/guides/function-calling) (function calling 有新旧两个版本 API,这里使用的是新版 API) > > [BUG] 1106 模型函数调用非 ASCII 字符存在问题的讨论:[https://community.openai.com/t/478500](https://community.openai.com/t/478500) ## 需要准备什么? ### 函数 Function calling,顾名思义,首先要有的就是<span style="font-weight: bold;" data-type="strong">函数(function)</span> ,举一个最简单的例子,获取当前时间(`get_current_time`),这个函数将返回指定时区的当前时间,如果省略参数则默认为 `Asia/Shanghai`。 前排提示:格式化字符串 `"%Y-%m-%d %H:%M:%S, %A"` 的输出内容如 `2024-01-25 15:14:38, Thursday` ```python import datetime import pytz def get_current_time(timezone='Asia/Shanghai'): tz = pytz.timezone(timezone) res = datetime.datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S, %A") return res ``` ### 函数定义 想要让 GPT 去调用函数,还需要想办法告诉他函数有哪些(这里规范可以参考 [OpenAI 文档关于函数的介绍](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools) 和 [JSON Schema](https://json-schema.org/learn/glossary))。GPT 看的不是函数的代码,而是形如下面的函数定义 每个函数格式大致如下 ```json { "type": "function", // 工具的类型,目前只支持 function,即函数调用 "function": { // 函数定义 "name": "get_current_time", // 要调用的函数的名称。必须是 a-z、A-Z、0-9,或包含下划线和破折号,最大长度为 64。 "description": "Get the current time in a specified timezone", // 对函数功能的描述,模型使用它来选择何时以及如何调用该函数。 "parameters": { // 函数接受的参数,描述为 JSON Schema 对象。如果省略则是无参数的函数 } } } ``` ~~JSON Schema 不推荐人工去写,建议写完函数,把类型标注好之后直接扔给 GPT 让他生成。~~ 对于上面的 get_current_time,其定义如下 ```json tool = { "type": "function", "function": { "name": "get_current_time", "description": "Get the current time in a specified timezone", "parameters": { "type": "object", "properties": { "timezone": { "type": "string", "default": "Asia/Shanghai", "description": "The timezone to get the current time for" } }, "required": [] }, }, } ``` # 开始使用 ## 函数处理 先回到第一节中多轮对话的一个 case ```python from cfg import client, Model_GPT3, Model_GPT4 def ask(): res = client.chat.completions.create( model=Model_GPT3, messages=[ {"role": "user", "content": "吃葡萄不吐葡萄皮的下一句是什么?"}, # 用户的第一次输入 {"role": "assistant", "content": "吃葡萄不吐葡萄籽。"}, # AI 的第一次回复 {"role": "user", "content": "请将这两句话翻译成英语"}, # 用户的第二次输入 ], ) return res.choices[0].message.content print(ask()) ``` 我们可以看到,上下文信息是放到了 messages 内,格式形如 `[ { "role": "user | assistant | system", "content": "balbala" } ]` 参考[相关文档](https://platform.openai.com/docs/api-reference/chat/create)和[示例代码](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models),实际上 messages 有四类不同的角色,其中前三类的 `name` 为可选参数,用来区分同类的不同用户(我没用过,文档介绍是常见的如作为示例消息,`example-user`, `example-assistant`,以便和正式的用户消息区分) * `system`:参数有 `{role: 'system', content: string, name?: string}` * `user`:参数有 `{role: 'user', content: string, name?: string}` * `assistant`:参数有 `{role: 'assistant', content?: string, name?: string, tool_calls?: array}` * `tool`:参数有 `{role: 'tool', content: string, tool_call_id: string}` 和函数调用有关的是 `assistant` 和 `tool`,如果需要触发函数调用,则 `assistant` 消息会设置 `tool_calls` 数组,包含想要执行的函数和参数。紧接着,下一条消息则传递 `tool` 类型,用来响应对应的 `tool_call` ,提供调用的返回值。 先看一个示例代码(function_caling/1.py),函数以上面的获取当前时间为例 ```python messages = [ {"role": "user", "content": "现在几点了?"}, ] resp1 = client.chat.completions.create( model=Model_GPT4, messages=messages, tools=[tool], tool_choice="auto", ) msg = resp1.choices[0].message print(msg) ``` 打印出来的消息为 ```python ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_sfcV6G1wJr1ukzkIlta69HFo', function=Function(arguments='{"timezone":"Asia/Shanghai"}', name='get_current_time'), type='function')]) # ==============<span style="font-weight: bold;" class="mark"> 人工格式化一下 </span>============== ChatCompletionMessage( content=None, role='assistant', function_call=None, tool_calls=[ ChatCompletionMessageToolCall( id='call_sfcV6G1wJr1ukzkIlta69HFo', function=Function( arguments='{"timezone":"Asia/Shanghai"}', name='get_current_time' ), type='function' ) ] ) ``` 从字面意思也能看出来他在干啥 然后我们按照规定的形式构建响应。需要严格注意的是,这里的响应 `content` 字段是字符串类型,如果返回多个值或者对象,可以 JSON 化变成字符串。另外需要注意不能返回数字(会报错),如果要返回数字可以是数字的字符串(如 `123` -> `"123"`) ```python call_result = { "role": "tool", "content": get_current_time(), "tool_call_id": msg.tool_calls[0].id, "name": "get_current_time" } messages.append(call_result) ``` 之后,把 AI 的响应以及 call_result 分别放到历史消息内。 ```python messages.append(msg) # 这里可以直接放进去 openai 返回的 message 对象 messages.append(call_result) resp2 = client.chat.completions.create( model=Model_GPT3, messages=messages, ) content = resp2.choices[0].message.content print(content) ``` 然后运行,可以看到输出正确 ```python 现在是北京时间下午5点36分。 ``` 完整代码参考:[function_calling/1.py](https://git.zsh.dev/zsh2517/chatgpt-api-examples/src/branch/master/function_calling/1.py) ## 多函数与自动处理 上文手动逐步实现了一个 function calling 的 demo。这里把他们连起来。大概有以下两个关键步骤 1. 和多轮次对话一样,整体需要一收一发 2. 每次需要检测一下是否有函数调用,有的话触发调用 <span style="font-weight: bold;" data-type="strong">这里以一个简化版的智能家居系统为例</span> <span style="font-weight: bold;" data-type="strong">API 有三个</span> * 获取所有的智能设备状态(`function get_devices(): array<{name: string, type: 'light' | 'window'}>`) * 设置灯的开关和亮度(`function set_light(name: string, set_on: boolean, brightness: number): boolean`) * 设置窗户的开关和大小(`function set_window(name: string, set_open: boolean, angle: number): boolean`) <span style="font-weight: bold;" data-type="strong">举例一些测试 case:</span> * 我要睡觉了,请帮我关灯。(预期:关闭所有的灯) * 我要睡觉了。(预期:关灯,关窗户) * 请帮我打开厕所的灯。(预期:打开厕所灯) * 我要出门了,请帮我通通风。(预期:关窗户,不知道能不能想起来关灯) * 请帮我把关着的灯全都打开,打开的灯全部关闭(???) * 如果窗户现在是开着的,就关掉灯,否则打开所有的灯(~~GPT:(╯‵□′)╯︵┻━┻~~ ) 代码参考:[smart_home/main.py](https://git.zsh.dev/zsh2517/chatgpt-api-examples/src/branch/master/function_calling/smart_home/main.py)。这个需求使用 GPT-3.5 已经足够 这个代码实际上是强制进行的 人 - AI 交互轮流对话,如果 AI 决定调用函数,则调用完函数之后继续调用 AI 且仅允许文字输出。(即不允许 人 -> AI(函数) -> AI(函数)的方式调用) --- # 附录:GPT 自己去写函数定义 <span style="font-weight: bold;" data-type="strong">生成 Function Calling 描述的 prompt</span> ```json 后面的代码是一个功能函数,所有详细的信息都已经写到注释中。你可以根据注释、命名等推断其用法。代码是: ```ts /** * Get the current time in a specified timezone * * @param timezone The timezone to get the current time for * @returns The current time in the specified timezone */ function getCurrentTime(timezone: string = "Asia/Shanghai"): number {} ``` 请将其使用如下的形式,给出函数声明 ```json { "type": "function", "function": { "name": "get_current_time", "description": "Get the current time in a specified timezone", "parameters": { "type": "object", "properties": { "timezone": { "type": "string", "default": "Asia/Shanghai", "description": "The timezone to get the current time for" } }, "required": ["timezone"] }, }, } ``` ``` openapi 规范 demo ```plaintext openapi: "3.1.0" info: title: "ECharts Drawing Service" description: "Allows for the dynamic generation of ECharts visualizations by providing source code." version: "v1.0.0" servers: - url: "https://echarts-demo.zsh2517.workers.dev/" paths: /: post: description: "Draw ECharts based on provided source code." operationId: "drawECharts" requestBody: description: "Source code for ECharts visualization." required: true content: application/json: schema: type: object properties: code: type: string description: "The ECharts source code to be rendered." example: "var option = { /* ECharts configuration */ };" responses: '200': description: "ECharts visualization rendered successfully." content: application/json: schema: type: object properties: url: type: string description: "URL to the rendered ECharts visualization." example: "https://echarts-demo.zsh2517.workers.dev/?id=qst1omeh" '400': description: "Invalid request format or content." '500': description: "Internal server error." ``` 最后修改:2024 年 01 月 25 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏