赞
踩
在前面 Spring AI Chat 简单示例 中介绍了 Chat 中的 Function 用法,我很好奇这个 Function 是如何被调用的,就在下面代码中加了断点看执行:
执行过程中进入了这个方法,并且符合 Request 类型的参数,这是如何实现的?就像魔法一样神奇,这是 Spring AI 的功能还是 OpenAI 的功能?好奇心引导我必须深入看看。
先找了几个断点尝试跟踪,没有特别好的入口点。在遇到类似问题的时候,你可以试试先把日志级别调成DEBUG或者TRACE,Spring Boot 默认使用的 logback,所以在当前项目 src/main/resources
目录中添加 logback.xml
配置文件:
<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 的方法调用,日志中有参数,下面是第一次调用的日志:
org.springframework.web.client.DefaultRestClient:437
Writing [ChatCompletionRequest[messages=[
ChatCompletionMessage[
content=北京、石家庄的天气怎么样,适合穿什么衣服?温度保留1........太长了省略...
日志的内容看着就是 map.toString()
的样子,效果不好,不如转成 JSON 展示。
打开 DefaultRestClient:437
行加断点,然后使用表达式将结果转换为 JSON,下面是操作截图:
将JSON复制出来,内容如下:
{ "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
,这里的数据是在注册的时候生成的:
FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("CurrentWeather")
.withDescription("Get the weather in location").build()
在上面的 build()
方法调用时,通过下面的代码初始化了 inputTypeSchema
:
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 calling 中有介绍,在 Create chat completion有官方的 Function 功能示例和参数定义:
OpenAI会按照格式要求返回数据,本文示例返回的数据在 DefaultRestClient 639 行断点查看返回值,返回JSON如下:
{ "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,下面是合并后的参数:
{ "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如下:
{ "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 来简单实现这个功能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。