使用Azure openai java SDK在流式输出时处理functioncall

emmm,新的功能和api都出了,还在写旧格式的感觉有点过时。
之前都是用非流式的使用function, 刚好遇到这个处理了下顺便记录一下。

流式的function也是分段返回的, 和content类似, 举个例子:

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
data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"role":"assistant","function_call":{"name":"urlContent","arguments":""}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":"{\n"}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":" "}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":" \""}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":"url"}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":"\":"}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":" \""}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":"www"}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":".baidu"}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":".com"}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":"\"\n"}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"delta":{"function_call":{"arguments":"}"}}}]}

data:{"id":"chatcmpl-8KsmRHXFf9HQRr3xDpHzJK6C71hGX","created":1699987827.000000000,"choices":[{"index":0,"finish_reason":"function_call","delta":{}}]}

内容依旧在choices/0/delta中,只不过从content变成了function_call.
content的例子:

1
data:{"id":"chatcmpl-8Krqrqqn1rKPPBatGpBhvOfN3sN7G","created":1699984257.000000000,"choices":[{"index":0,"delta":{"content":" today"}}]}

所以要处理就比较简单了, 在IterableStream<ChatCompletions>的循环中去把function_call给拼出来,之前content只需要拼一个字段,这个拼两个字段即可.
代码如下:

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
String role = null;
StringBuilder contentBuilder = new StringBuilder();
StringBuilder functionCallNameBuilder = new StringBuilder();
StringBuilder functionCallArgumentsBuilder = new StringBuilder();
for (ChatCompletions e : stream) {
try {
final List<ChatChoice> choices = e.getChoices();
if (CollectionUtils.isEmpty(choices)) {
continue;
}
final ChatChoice chatChoice = choices.get(0);
if (chatChoice.getFinishReason() != null) {
break;
}

final ChatMessage chatMessage = chatChoice.getDelta();
if (chatMessage == null) {
continue;
}
if (chatMessage.getRole() != null) {
role = chatMessage.getRole().toString();
}
final FunctionCall functionCall = chatMessage.getFunctionCall();
if (functionCall != null) {
if (functionCall.getName() != null) {
functionCallNameBuilder.append(functionCall.getName());
}
if (functionCall.getArguments() != null) {
functionCallArgumentsBuilder.append(functionCall.getArguments());
}
}
// 这里不写else 不排除这两者都返回的情况
final String content = chatMessage.getContent();
if (content != null) {
contentBuilder.append(content);
}
} catch (IOException ex) {
log.error("chat - for loop", ex);
}
}
if (role == null) {
log.warn("chat - role is null");
return;
}
String content = null;
if (!contentBuilder.isEmpty()) {
content = contentBuilder.toString();
}
// 要用可以直接构造Map或者FunctionCall
String functionMap = null;
if (!functionCallNameBuilder.isEmpty()) {
Map<String, String> map = new HashMap<>();
map.put("name", functionCallNameBuilder.toString());
if (!functionCallArgumentsBuilder.isEmpty()) {
map.put("arguments", functionCallArgumentsBuilder.toString());
}
functionMap = JsonUtils.toString(map).orElseThrow();
}

拿到了content和function的完整结构后续逻辑就和非流式一样了