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

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

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

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的例子:

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

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

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的完整结构后续逻辑就和非流式一样了