在前面 Spring AI Chat 简单示例 中介绍了 Chat 中的 Function 用法,我很好奇这个 Function 是如何被调用的,就在下面代码中加了断点看执行:
执行过程中进入了这个方法,并且符合 Request 类型的参数,这是如何实现的?就像魔法一样神奇,这是 Spring AI 的功能还是 OpenAI 的功能?好奇心引导我必须深入看看。
跟踪代码 - 加日志 先找了几个断点尝试跟踪,没有特别好的入口点。在遇到类似问题的时候,你可以试试先把日志级别调成DEBUG或者TRACE,Spring Boot 默认使用的 logback,所以在当前项目 src/main/resources
目录中添加 logback.xml
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <configuration scan ="true" > <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > %d{HH:mm:ss.SSS} [%thread] %-5level %logger:%L - %msg%n</pattern > </encoder > </appender > <shutdownHook class ="ch.qos.logback.core.hook.DelayingShutdownHook" > <delay > 5 seconds</delay > </shutdownHook > <root level ="DEBUG" > <appender-ref ref ="STDOUT" /> </root > </configuration >
配置日志后执行代码,发现了两次 OpenAI 的方法调用,日志中有参数,下面是第一次调用的日志:
1 2 3 4 org.springframework.web.client.DefaultRestClient:437 Writing [ ChatCompletionRequest[ messages=[ ChatCompletionMessage[ content=北京、石家庄的天气怎么样,适合穿什么衣服?温度保留1. ... ... .太长了省略...
日志的内容看着就是 map.toString()
的样子,效果不好,不如转成 JSON 展示。
跟踪代码 - 看数据 打开 DefaultRestClient:437
行加断点,然后使用表达式将结果转换为 JSON,下面是操作截图:
将JSON复制出来,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { "messages" : [ { "content" : "北京、石家庄的天气怎么样,适合穿什么衣服?温度保留1位小数。" , "role" : "user" } ] , "model" : "gpt-3.5-turbo" , "max_tokens" : 200 , "stream" : false , "temperature" : 0.4 , "tools" : [ { "type" : "function" , "function" : { "description" : "Get the weather in location" , "name" : "CurrentWeather" , "parameters" : { "$schema" : "https://json-schema.org/draft/2020-12/schema" , "type" : "object" , "properties" : { "location" : { "type" : "string" } , "unit" : { "type" : "string" , "enum" : [ "C" , "F" ] } } } } } ] }
上面执行的内容中,除了我们的 messages
信息外,还多了 tools
信息,这里是我们定义的 CurrentWeather
,这里的数据是在注册的时候生成的:
1 2 3 FunctionCallbackWrapper.builder(new MockWeatherService ()) .withName("CurrentWeather" ) .withDescription("Get the weather in location" ).build()
在上面的 build()
方法调用时,通过下面的代码初始化了 inputTypeSchema
:
1 2 3 4 5 6 7 8 if (this .inputType == null ) { this .inputType = resolveInputType(this .function); }if (this .inputTypeSchema == null ) { boolean upperCaseTypeValues = this .schemaType == SchemaType.OPEN_API_SCHEMA; this .inputTypeSchema = ModelOptionsUtils.getJsonSchema(this .inputType, upperCaseTypeValues); }
在调用方法的时候我们指定了要使用 CurrentWeather
,就把这个函数的信息写入了请求的 JSON 中。
OpenAI Function 文档 在 OpenAI 官方文档中 Function calling 中有介绍,在 Create chat completion 有官方的 Function 功能示例和参数定义:
客户端执行函数 OpenAI会按照格式要求返回数据,本文示例返回的数据在 DefaultRestClient 639 行断点查看返回值,返回JSON如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 { "id" : "chatcmpl-953FrCIXAc3l4qmmY4u6cx48hecGN" , "choices" : [ { "finish_reason" : "tool_calls" , "index" : 0 , "message" : { "role" : "assistant" , "tool_calls" : [ { "id" : "call_nq3fMQLC6MRz4ZaNcYRey18C" , "type" : "function" , "function" : { "name" : "CurrentWeather" , "arguments" : "{\"location\": \"北京\", \"unit\": \"C\"}" } } , { "id" : "call_OJCtiJdxqWFWt1MF2taIbfvW" , "type" : "function" , "function" : { "name" : "CurrentWeather" , "arguments" : "{\"location\": \"石家庄\", \"unit\": \"C\"}" } } ] } } ] , "created" : 1710991179 , "model" : "gpt-3.5-turbo-0125" , "system_fingerprint" : "fp_4f0b692a78" , "object" : "chat.completion" , "usage" : { "completion_tokens" : 55 , "prompt_tokens" : 93 , "total_tokens" : 148 } }
客户端需要在自己本地使用参数调用Function,下面看看这段代码的逻辑。
第一次调用得到返回值时,上图124行代码的 response 会经过下面 handleFunctionCallOrReturn
方法的判断,如果不包含 toolFunctionCall
就直接返回,如果包含就继续处理,在 145行的 doCreateToolResponseRequest
方法中 Spring AI 将 arguments
反序列化为参数对象然后调用 Function。 然后将 Function 的结果添加到会话历史 messages
中再次调用 147 行的 callWithFunctionSupport
方法形成递归,这里会在 124 行再次调用 OpenAI,下面是合并后的参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 { "messages" : [ { "content" : "北京、石家庄的天气怎么样,适合穿什么衣服?温度保留1位小数。" , "role" : "user" } , { "role" : "assistant" , "tool_calls" : [ { "id" : "call_bSRms7bCa3ZDpSlMIs1Hzfc8" , "type" : "function" , "function" : { "name" : "CurrentWeather" , "arguments" : "{\"location\": \"北京\", \"unit\": \"C\"}" } } , { "id" : "call_jQ9nBPsOzJGsBrwYBVHhd6iw" , "type" : "function" , "function" : { "name" : "CurrentWeather" , "arguments" : "{\"location\": \"石家庄\", \"unit\": \"C\"}" } } ] } , { "content" : "{\"temp\":6.054343984099231,\"unit\":\"C\"}" , "role" : "tool" , "name" : "CurrentWeather" , "tool_call_id" : "call_bSRms7bCa3ZDpSlMIs1Hzfc8" } , { "content" : "{\"temp\":31.57208925230239,\"unit\":\"C\"}" , "role" : "tool" , "name" : "CurrentWeather" , "tool_call_id" : "call_jQ9nBPsOzJGsBrwYBVHhd6iw" } ] , "model" : "gpt-3.5-turbo" , "max_tokens" : 200 , "stream" : false , "temperature" : 0.4 , "tools" : [ { "type" : "function" , "function" : { "description" : "Get the weather in location" , "name" : "CurrentWeather" , "parameters" : { "$schema" : "https://json-schema.org/draft/2020-12/schema" , "type" : "object" , "properties" : { "location" : { "type" : "string" } , "unit" : { "type" : "string" , "enum" : [ "C" , "F" ] } } } } } ] }
消息的角色分别是 user、assistant、tool,此次请求返回结果中没有了 toolFunctionCall
,这就是我们需要的最终结果,JSON如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "id" : "chatcmpl-953TUtPNLBaeqfXGDMSW6fRnjm1VF" , "choices" : [ { "finish_reason" : "stop" , "index" : 0 , "message" : { "content" : "北京的天气为23.1°C,石家庄的天气为0.1°C。根据当前天气情况,建议穿着:\n\n- 北京:春秋过渡季节,建议穿着长袖衣物或薄外套。\n- 石家庄:寒冷天气,建议穿着厚外套、围巾和手套等保暖衣物。" , "role" : "assistant" } } ] , "created" : 1710992024 , "model" : "gpt-3.5-turbo-0125" , "system_fingerprint" : "fp_fa89f7a861" , "object" : "chat.completion" , "usage" : { "completion_tokens" : 124 , "prompt_tokens" : 197 , "total_tokens" : 321 } }
注意: 上面的截图和JSON分别是不同调用产生的,数据不一致。
经过上面的分析了解了 Function 的实现原理后,在使用提示词和 Function 时需要有一定的联系,需要让 OpenAI 能够推测中我们需要的参数,将 Function 的执行结果作为追加的提示词,让 OpenAI 利用追加的信息来生成最终的结果。
根据 Function 的用法,我来假设一个场景,比如在一个支持根据语音转文字,文字在通过 OpenAI 生成出差申请的场景时,可以口述:“我要在下周一出差从石家庄到北京,坐高铁去客户现场,和客户讨论具体的技术方案。”,我们可以将信息转换为 Function 所需的参数,然后校验信息,符合要求后调用我们业务接口创建出差申请,我们下一篇就结合提示词模板和 Function 来简单实现这个功能。