티스토리 뷰

 

 

 

6월 16일에 OpenAI가 업데이트 되었다.

 

몇 가지 모델이 업데이트 되었고, 토큰 수 증가와 함께 Function calling이란 기능도 함께 추가되었다.

 

이전 포스팅에서 토큰 수 증가로 대화 유지를 원활하게 할 수 있다는 것에 대해 다뤘으니,

 

이번에는 Function calling에 대해 알아보자.

 

Function calling이란?

Open AI reference에서 Function calling의 일부를 발췌했다.

API 호출에서, 사용자가 gpt-3.5-turbo-0613, gpt-4-0613 모델에 함수를 설명할 수 있습니다. 모델이 이러한 함수를 호출하기 위한 인수가 포함된 JSON 개체를 출력하도록 지능적으로 선택하도록 할 수 있습니다. Chat Completions API는 함수를 호출하지 않습니다. 대신 모델은 코드에서 함수를 호출하는 데 사용할 수 있는 JSON을 생성합니다.

 

쉽게 설명하면, GPT와의 대화에서 GPT에 입력해놓은 함수의 파라미터를 추출할 수 있도록 한다는 것이다.

 

이름이 Function calling이라 함수를 호출해줄 것이라 생각했다면, 아쉽지만 거기까지는 아니라는 것

 

물론 프롬프트를 통해 cmd를 통해 특정 자바 파일을 실행해 함수를 호출 한 것처럼 할 수 있다고는 알고 있는데,

 

그정도 기능까지는 해당 API 옵션에서는 제공하고 있지 않다는 말이다.

 

일단, function calling을 사용해보기 위해서 open AI에서 제공하는 예시를 그대로 가져와서 사용했다. 

 

다만, Python으로 짜여져 있던 걸 Java로 변환해서 사용했다. 때문에 Json 파싱이 굉장히 귀찮고 짜증났다.

 

바로 사용해보자.

 

사용법

이전과 같이 feign client를 사용했다.

 

Configuration

public class OpenAIHeaderConfiguration {
    @Value("${spring.openAI.APIKey}")
    String openAIAPIKey;

    @Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
            requestTemplate.header("Authorization", openAIAPIKey);
            requestTemplate.header("USER-AGENT", "Mozilla/5.0");
            requestTemplate.header("Content-Type", "application/json");
        };
    }
}

Client

@FeignClient(name = "OpenAIClient", url = "https://api.openai.com/v1", configuration = {OpenAIHeaderConfiguration.class})
public interface OpenAIFeignClient {

    @RequestMapping(method = RequestMethod.POST, value = "/chat/completions")
    ChatResponseDto chatCompletion(@RequestBody ChatRequestDto chatRequestDto);

}

Impl

public ChatResponseDto getChatCompletionFunctionCall(List<ChatMessage> messages, List<ChatFunctionsVo> functionsVo) {
    ChatResponseDto chatResponseDto = openAIFeignClient.chatCompletionFunctionCall(
            ChatRequestFunctionCallDto.builder()
                                      .model(CHAT_GPT_MODEL)
                                      .functionCall(new FunctionCallVo("getCurrentWeather"))
                                      .messages(messages)
                                      .functions(functionsVo)
                                      .build());
    return chatResponseDto;
}

FuctionCallVo 객체

@Data
@NoArgsConstructor
@AllArgsConstructor
public class FunctionCallVo{
    private String name;
}

 

이전 대화 유지에서 썼던 chatAPI를 사용한건 맞는데, functionCall 옵션과 function 옵션을 추가해줬다.

 

여기서 functionCall 옵션은 디폴트로는 auto가 사용되고, fucntionCall을 사용하기 싫으면 none을 사용하면 된다.

 

fucntionCall에 내가 사용할 함수명을 넣어주면 무조건 functionCall로 답변을 준다.(2023.7.19 수정)

 

functionCall로 답변이 오면 content는 null이다.

 

이점에 유의해서 객체를 작성해야 한다.

 

function 옵션은 좀 긴 json이라 자바에서 다루기는 조금 불편했다...

 

function에 들어갈 객체

public class ChatFunctionsVo {
    private String name;
    private String description;
    private Parameters parameters;
    @Data
    public static class Parameters {
        private String type;
        private Properties properties;
        private List<String> required;

        @Data
        public static class Properties {
            private Location location;
            private Format format;

            @Data
            public static class Location {
                private String type;
                private String description;
            }

            @Data
            public static class Format {
                private String type;
                private List<String> degrees;
                private String description;
            }
        }

    }
}

function 생성 매서드

public List<ChatFunctionsVo> setFunctionList() {
    List<ChatFunctionsVo> functionList = new ArrayList<>();
    ChatFunctionsVo chatFunctionsVo = new ChatFunctionsVo();
    chatFunctionsVo.setName("getCurrentWeather");
    chatFunctionsVo.setDescription("Get the current weather");

    ChatFunctionsVo.Parameters parametersVo = new ChatFunctionsVo.Parameters();
    parametersVo.setType("object");
    parametersVo.setRequired(new ArrayList<>(List.of("location", "format")));

    ChatFunctionsVo.Parameters.Properties properties = new ChatFunctionsVo.Parameters.Properties();
    ChatFunctionsVo.Parameters.Properties.Location location = new ChatFunctionsVo.Parameters.Properties.Location();
    location.setType("string");
    location.setDescription("The city and state, e.g. San Francisco, CA.");

    ChatFunctionsVo.Parameters.Properties.Format format = new ChatFunctionsVo.Parameters.Properties.Format();
    format.setType("string");
    format.setDegrees(new ArrayList<>(List.of("celsius", "fahrenheit")) );
    format.setDescription("The temperature unit to use. Infer this from the users location.");

    properties.setLocation(location);
    properties.setFormat(format);

    parametersVo.setProperties(properties);

    chatFunctionsVo.setParameters(parametersVo);

    functionList.add(chatFunctionsVo);
    return functionList;
}

함수는 openAI에서 사용한 예시를 그대로 사용했다.

 

날씨에 대한 질문을 하면 그에 대한 함수를 매핑해서 답변해준다.

 

서비스 로직

public ChatResponseDto getGptAnswerFunctionCall(RequestMemorizeMessageDto requestMemorizeMessageDto) {
    List<ChatMessage> chatList = new ArrayList<>();
    MemorizeMessageVo requestMemorizeMessageVo = new MemorizeMessageVo();

    if(ObjectUtils.isEmpty(requestMemorizeMessageDto.getCacheId())) {
        chatList.add(new ChatMessage("system", "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."));
    } else {
        requestMemorizeMessageVo = cacheService.getMemorizeMessageCache(requestMemorizeMessageDto.getCacheId());
        chatList = requestMemorizeMessageVo.getChatList();
        cacheService.clearCache(requestMemorizeMessageDto.getCacheId());
    }

    chatList.add(new ChatMessage("user", requestMemorizeMessageDto.getMessage()));

    List<ChatFunctionsVo> functionList = setFunctionList();

    ChatResponseDto chatResponseDto = openAiService.getChatCompletionFunctionCall(chatList, functionList);

    chatList.add(new ChatMessage(chatResponseDto.getChoices().get(0).getMessage().getRole(), chatResponseDto.getChoices().get(0).getMessage().getContent()));
    requestMemorizeMessageVo.setChatList(chatList);

    String id = cacheService.saveMemorizeMessageCache(requestMemorizeMessageVo);
    chatResponseDto.setCacheId(id);

    // Usage에 totalTokens를 보고 16k가 넘어갔을 때 처리

    return chatResponseDto;
}

Function  calling 기능을 제대로 이용하려면 이전 대화를 유지해야하기 때문에, 이전에 사용했던 대화 기억하기 매서드를 활용했다.

 

대화를 처음 주고 받을 때는, caheId가 없기 때문에 프롬프트를 넣어줬는데 해당 프롬프트는 openAI의 레퍼런스에서 예시로 사용한 프롬프트를 그대로 사용했다.

 

또, 중요한 점은 chatAPI 응답형식에 function_call을 추가해야한다.

public class ChatMessage implements Serializable {

    private String role;
    @JsonInclude(value = JsonInclude.Include.NON_NULL)
    private String content;
    @JsonInclude(value = JsonInclude.Include.NON_NULL)
    @JsonProperty("function_call")
    private Object functionCall;
    
    public ChatMessage(String role, String content) {
        this.role = role;
        this.content = content;
    }

}

파이썬 예시에서는 String형태인줄 알았는데 알고보니 Json 형식이어서, 계속 안받아져서 한동안 고생했다...

 

이제 구현은 끝났으니 바로 사용해보자.

 

사용 결과

첫번째 질문은 cacheId를 주고 받지 않고 그냥 질문을 던진다.

cache Id와 함께 GPT의 추가질문에 다시 답변을 만들어 보내면

 

아래와 같이 json 형태의 함수에 맞게 파싱된 json 형태의 데이터가 답변으로 온다.

이제 이걸 사전에 만들어놓은 외부 연동 API에 반영해서 사용하면 되지.. 않을까?

 

아무튼 function calling의 기능을 사용하면 위와 같이 content에는 값이 들어오지 않고,

 

function_call이라는 변수에 값을 채워서 보내준다.

 

그래서 이걸 어디에 쓸까..?

당장 떠오르진 않지만, 챗봇을 만드는데 나름 유용할 수 있는 생각이 든다.

 

GPT가 응답을 생성해 줄 수 있지만, 2021년 9월까지의 데이터만을 가지고 있기 때문에 최신 답변을 제공해 줄 수 없다는 한계가 있다.

 

때문에, 챗봇 개발에 외부 API와 연동 및 최신데이터를 함께 쓸 수 있도록 작업해 준 것 같다.

 

그리고 함수를 여러개 등록해 놓을 수 있기 때문에, 다양한 질문 변수에 대응하도록 구현할 수 도 있을 것 같다.

 

마치며

블로그엔 모든 세세한 코드까지 올리긴 힘들어서 깃헙에도 정리해서 올리고 싶은데, 기존에서 너무 많이 바꿔서 브랜치를 따로 관리하도록 해야할 것 같다.

 

RestTemplate에서 Feign Client로 변경했고, 별도의 객체도 많이 만들었고, 또...

 

바로바로 했으면 좋았을 거 같은데 블로그에 빨리 소개하고 싶어서 정리도 제대로 안해뒀다.

 

그래도 언젠간 할 듯...

 

또 바쁨+아픔 때문에 글이 몇개 밀렸는데, 빠르게 업데이트해야 될 것 같다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함