Spring AI Function 的实现原理?

在前面 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 来简单实现这个功能。


Spring AI Function 的实现原理?
https://blog.mybatis.io/post/31ff1e6c.html
作者
Liuzh
发布于
2024年6月26日
许可协议