AI & LLM

[Spring AI 공식문서 읽기] 2. ChatClient API

@deveely 2025. 10. 5. 01:18
반응형

 

 

0. Overview

Spring AI의 ChatClient는 AI 모델과 대화하기 위한 도구입니다.

  • 동기식과 스트리밍 방식을 모두 지원하고,
  • Fluent API로 프롬프트를 쉽게 구성할 수 있습니다.

프롬프트는 사용자 메시지와 시스템 메시지로 이루어지며, 실행 시점에 변수를 치환할 수 있는 플레이스홀더도 활용할 수 있습니다. 또한 사용할 AI 모델 종류temperature 값 같은 옵션을 통해 답변의 톤과 창의성을 조절할 수 있습니다.


1. ChatClient 생성

ChatClient 는 ChatClient.Builder 를 통해 생성할 수 있습니다.

Spring Boot 환경에서는 자동 구성된 ChatClient.Builder 를 주입받아 생성할 수 있고, 프로그래밍적인 방법으로 직접 생성할 수 도 있습니다.

1.1 자동구성된 ChatClient.Builder 사용

아래는 간단한 사용사례로 ChatClient.Builder 를 주입받아 ChatClient 를 생성하고 사용자 입력에 대한 String 응답을 가져오는 예시입니다.

call() 메서드는 AI모델로 요청을 보내고, content() 메서드는 AI응답을 문자열로 반환합니다.

@RestController
class MyController {

    private final ChatClient chatClient;

    public MyController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/ai")
    String generation(String userInput) {
        return this.chatClient.prompt()
            .user(userInput)
            .call()
            .content();
    }
}

1.2. 다중 Chat Models 활용하기

하나의 애플리케이션에서 여러 개의 Chat Model을 사용해야 하는 상황이 있습니다. 예를 들어:

  • 작업별 다른 모델 사용 : 복잡한 추론에는 강력한 모델, 단순 작업에는 빠르고 저렴한 모델 활용
  • 장애 대비(Fallback) : 특정 모델 서비스가 불가능할 때 다른 모델로 대체
  • A/B 테스트 : 서로 다른 모델이나 설정을 비교 실험
  • 사용자 선택 제공 : 사용자 취향에 따라 모델을 고를 수 있도록 옵션 제공
  • 전문화된 모델 조합 : 코드 생성용 모델, 창의적 글쓰기용 모델 등 역할별 모델을 함께 사용

자동구성 이용 시 단일 모델에 대한 ChatClient.Builder 만 사용이 가능하기 때문에 애플리케이션 내에서 여러 모델을 이용하려면 자동구성을 비활성화하고 ChatClient 인스턴스를 직접 생성해야합니다.

spring:
  ai:
    chat:
      client:
        enabled: false

ChatModel 에 따라 각각 클라이언트를 구성할 수 있습니다.

@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
        return ChatClient.create(chatModel);
    }

    @Bean
    public ChatClient anthropicChatClient(AnthropicChatModel chatModel) {
        return ChatClient.create(chatModel);
    }
}

Spring 환경에 맞게 모델을 주입받고 적절히 분기하여 사용되게 코드를 구성할 수 있습니다.

@Configuration
public class ChatClientExample {

    @Bean
    CommandLineRunner cli(
            @Qualifier("openAiChatClient") ChatClient openAiChatClient,
            @Qualifier("anthropicChatClient") ChatClient anthropicChatClient) {

        return args -> {
            var scanner = new Scanner(System.in);
            ChatClient chat;

            // Model selection
            System.out.println("\\nSelect your AI model:");
            System.out.println("1. OpenAI");
            System.out.println("2. Anthropic");
            System.out.print("Enter your choice (1 or 2): ");

            String choice = scanner.nextLine().trim();

            if (choice.equals("1")) {
                chat = openAiChatClient;
                System.out.println("Using OpenAI model");
            } else {
                chat = anthropicChatClient;
                System.out.println("Using Anthropic model");
            }

            // Use the selected chat client
            System.out.print("\\nEnter your question: ");
            String input = scanner.nextLine();
            String response = chat.prompt(input).call().content();
            System.out.println("ASSISTANT: " + response);

            scanner.close();
        };
    }
}

혹은 단일 ChatModel을 이용하더라도 시스템 프롬프트 등 구성을 달리하는 ChatClient 를 여러 개 구성할 수도 있습니다.

ChatClient.Builder builder = ChatClient.builder(myChatModel);

// 기본 클라이언트
ChatClient defaultChatClient = builder.build();

// 시스템프롬프트 커스텀한 클라이언트
ChatClient customChatClient = builder
    .defaultSystemPrompt("You are a helpful assistant.")
    .build();

1.3. 다중 OpenAI API 호환 엔드포인트 연동 예시

OpenAI 모델과 OpenAI API 호환 모델 (e.g. Groq) 을 동시에 사용하는 경우 기준 모델을 mutate() 메서드로 간단히 속성만 변경하여 사용할 수 있습니다.

@Configuration
public class ChatClientConfig {

    @Autowired
    private OpenAiChatModel baseChatModel;

    @Autowired
    private OpenAiApi baseOpenAiApi;

    @Bean
    public ChatClient groqChatClient(OpenAiChatModel groqModel) {
        return ChatClient.builder(groqModel).build();
    }

    @Bean
    public ChatClient gpt4ChatClient(OpenAiChatModel gpt4Model) {
        return ChatClient.builder(gpt4Model).build();
    }

    @Bean
    public OpenAiChatModel groqModel(OpenAiApi groqApi) {
        return baseChatModel.mutate()
            .openAiApi(groqApi)
            .defaultOptions(OpenAiChatOptions.builder()
                .model("llama3-70b-8192")
                .temperature(0.5)
                .build())
            .build();
    }

    @Bean
    public OpenAiChatModel gpt4Model(OpenAiApi gpt4Api) {
        return baseChatModel.mutate()
            .openAiApi(gpt4Api)
            .defaultOptions(OpenAiChatOptions.builder()
                .model("gpt-4")
                .temperature(0.7)
                .build())
            .build();
    }
    
    @Bean
    public OpenAiApi groqApi() {
        return baseOpenAiApi.mutate()
            .baseUrl("<https://api.groq.com/openai>")
            .apiKey(System.getenv("GROQ_API_KEY"))
            .build();
    }

    @Bean
    public OpenAiApi gpt4Api() {
        return baseOpenAiApi.mutate()
            .baseUrl("<https://api.openai.com>")
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .build();
    }
}

2. ChatClient Fluent API

ChatClient의 prompt 메서드를 사용해 세 가지 방식으로 프롬프트를 생성할 수 있습니다.

메서드 설명

prompt() 인자가 없는 이 메서드는 플루언트 API를 시작할 때 사용하며, 사용자(user), 시스템(system) 등의 메시지를 단계적으로 추가해 프롬프트를 구성할 수 있습니다.
prompt(Prompt prompt) Prompt 인스턴스를 인자로 받아, 플루언트 API가 아닌 방식으로 미리 생성한 Prompt 객체를 전달할 수 있습니다.
prompt(String content) 사용자의 텍스트 내용을 바로 전달할 수 있습니다.

3. ChatClient Responses

ChatClient를 사용하여 AI 모델의 응답을 다양한 방식으로 포맷할 수 있는 방법을 제공합니다.

3.1. ChatResponse 반환

AI 모델의 응답은 ChatResponse ****타입으로 정의된 풍부한 구조를 가지고 있습니다.

이 객체에는 응답이 어떻게 생성되었는지에 대한 메타데이터가 포함되어 있으며, 여러 개의 응답을 가질 수도 있습니다. 각 응답은 자체 메타데이터를 포함합니다.

메타데이터에는 응답을 생성하는 데 사용된 토큰 수가 포함되며, 이는 호스팅되는 AI 모델이 요청당 사용된 토큰 수에 따라 과금하기 때문에 중요한 정보입니다.

아래 예시는 call() 메서드 호출 후 chatResponse()를 사용하여 메타데이터가 포함된 ChatResponse 객체를 반환하는 방법을 보여줍니다.

ChatResponse chatResponse = chatClient.prompt()
    .user("Tell me a joke")
    .call()
    .chatResponse();

3.2. Entity 반환

AI 모델의 응답을 단순 문자열이 아니라 모델 객체로 매핑해 반환하고 싶을 때가 있습니다.

ChatClient의 entity() 메서드를 통해 응답을 특정 클래스로 매핑할 수 있습니다.

record ActorFilms(String actor, List<String> movies) {}

// 무작위 배우 대상 배우, 영화목록 형태 응답을 기대
ActorFilms actorFilms = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorFilms.class);

혹은 아래와 같이 List, Map 과 같은 복잡한 타입을 알려줄 수도 있습니다.

// Tom Hanks와 Bill Murray 배우의 영화목록 형태 응답을 기대
List<ActorFilms> actorFilms = chatClient.prompt()
    .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorFilms>>() {});

3.3. 응답 Streaming

stream() 메서드를 사용하면 아래와 같이 비동기(reactive) 방식으로 응답을 스트리밍할 수 있습니다:

Flux<String> output = chatClient.prompt()
    .user("Tell me a joke")
    .stream()
    .content();

앞으로는 stream() 메서드를 사용할 때 Java 엔티티를 바로 반환할 수 있는 편의 메서드가 제공될 예정입니다만 현재 버전에서는 아래 예시처럼 Structured Output Converter를 사용해 스트리밍된 응답을 직접 변환해야 합니다.

아래 예시에서는 param() 메서드를 활용한 파라미터를 사용하는 방법을 함께 보여줍니다. (자세한 내용은 문서의 후반부에서 설명됩니다)

var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<ActorsFilms>>() {});

Flux<String> flux = this.chatClient.prompt()
    .user(u -> u.text("""
                        Generate the filmography for a random actor.
                        {format}
                      """)
            .param("format", this.converter.getFormat()))
    .stream()
    .content();

String content = this.flux.collectList().block().stream().collect(Collectors.joining());

List<ActorsFilms> actorFilms = this.converter.convert(this.content);

4. 프롬프트 템플릿

ChatClient는 런타임에 변수가 치환되는 템플릿 형태로 사용자(user)와 시스템(system) 텍스트를 제공할 수 있습니다.

String answer = ChatClient.create(chatModel).prompt()
    .user(u -> u
            .text("Tell me the names of 5 movies whose soundtrack was composed by {composer}")
            .param("composer", "John Williams"))
    .call()
    .content();

내부적으로 ChatClient는 PromptTemplate ****클래스를 사용해 사용자 및 시스템 텍스트를 처리하며, 런타임에 제공된 값으로 변수를 치환합니다. 이때 TemplateRenderer 구현체를 사용합니다.

기본적으로 Spring AI는 StTemplateRenderer 구현체를 사용하며, 이는 Terence Parr가 개발한 오픈 소스 StringTemplate 엔진을 기반으로 합니다.

또한 Spring AI는 템플릿 처리가 필요하지 않은 경우를 위해 NoOpTemplateRenderer 도 제공합니다.

<aside> 💡

ChatClient에 직접 설정한 TemplateRenderer(예: .templateRenderer() 사용)는 빌더 체인에서 직접 정의한 프롬프트 내용(예: .user(), .system())에만 적용됩니다.

이는 QuestionAnswerAdvisor와 같이 어드바이저 내부에서 사용되는 템플릿에는 영향을 주지 않으며,

해당 어드바이저들은 자체적인 템플릿 커스터마이징 메커니즘을 가지고 있습니다.

(자세한 내용은 Custom Advisor Templates 섹션을 참고하십시오.)

</aside>

만약 다른 템플릿 엔진을 사용하고 싶다면, TemplateRenderer 인터페이스를 직접 구현하여 ChatClient에 제공할 수 있습니다.

또는 기본적으로 제공되는 StTemplateRenderer 를 계속 사용하되, 사용자 정의 설정으로 커스터마이징할 수도 있습니다.

예를 들어, 기본적으로 템플릿 변수는 {} 구문으로 식별됩니다.

프롬프트에 JSON을 포함할 계획이라면 JSON의 중괄호({})와 충돌을 피하기 위해 다른 구분자를 사용할 수 있습니다.

아래 예시는 템플릿 변수 식별자를 < 와 > 로 변경하는 예제입니다.

TemplateRenderer customTemplateRenderer = StTemplateRenderer.builder()
    .startDelimiterToken('<')
    .endDelimiterToken('>')
    .build();

String answer = ChatClient.create(chatModel).prompt()
    .user(u -> u
            .text("Tell me the names of 5 movies whose soundtrack was composed by <composer>")
            .param("composer", "John Williams"))
    .templateRenderer(customTemplateRenderer)
    .call()
    .content();

5. call() return values

ChatClient에서 call() 메서드를 지정한 후에는 응답을 다양한 형태로 받을 수 있는 여러 옵션이 있습니다.

메서드 설명

String content() 응답의 문자열 내용만 반환합니다.
ChatResponse chatResponse() 여러 Generations(응답 후보)을 포함하고, 응답 생성에 사용된 토큰 수 등 메타데이터를 담은 ChatResponse 객체를 반환합니다.
ChatClientResponse chatClientResponse() ChatResponse 와 함께 ChatClient 실행 컨텍스트를 포함하는 객체를 반환합니다. 이를 통해 어드바이저 실행 중 사용된 추가 데이터(예: RAG 플로우에서 검색된 문서 등)에 접근할 수 있습니다.
ResponseEntity<?> responseEntity() 상태 코드, 헤더, 바디를 포함한 전체 HTTP 응답을 ResponseEntity 로 반환합니다. 저수준 HTTP 세부 정보가 필요할 때 유용합니다.

entity() 메서드로 특정 모델로 바로 변환할 수 있습니다.

메서드 설명

entity() Java 타입으로 변환된 엔티티를 반환합니다.
entity(ParameterizedTypeReference type) 컬렉션 형태의 엔티티 타입을 반환할 때 사용합니다.
entity(Class type) 특정 단일 엔티티 타입으로 변환합니다.
entity(StructuredOutputConverter structuredOutputConverter) StructuredOutputConverter 인스턴스를 지정해 문자열을 원하는 엔티티 타입으로 변환합니다.

<aside> 💡

call() 메서드를 호출하는 것만으로는 실제 AI 모델 실행이 이루어지지 않습니다. 이 메서드는 Spring AI에게 동기 호출을 할지 스트리밍 호출을 할지를 지정하는 역할만 합니다. 실제 AI 모델 호출은 content(), chatResponse(), responseEntity() 와 같은 메서드를 호출할 때 수행됩니다.

</aside>

6. stream() return values

ChatClient에서 stream() 메서드를 지정한 후에는 응답을 받을 수 있는 몇 가지 옵션이 있습니다

메서드 설명

Flux content() AI 모델이 생성하는 문자열을 Flux 형태로 반환합니다.
Flux chatResponse() 메타데이터를 포함한 ChatResponse 객체를 Flux 형태로 반환합니다.
Flux chatClientResponse() ChatResponse 와 ChatClient 실행 컨텍스트를 포함하는 ChatClientResponse 객체를 Flux 형태로 반환합니다. 이를 통해 어드바이저 실행 중 사용된 추가 데이터(예: RAG 플로우에서 검색된 문서 등)에 접근할 수 있습니다.

7. Using Defaults

@Configuration 클래스에서 기본 시스템 텍스트를 설정하여 ChatClient를 생성하면 런타임 코드가 간결해집니다. 기본값을 설정해두면 ChatClient를 호출할 때 사용자 텍스트만 지정하면 되며, 매번 요청마다 시스템 텍스트를 설정할 필요가 없습니다.

7.1. Default System Text 설정하기

아래 예제와 같이 LLM 시스템 대화의 기본 행동태도를 ChatClient 설정으로 변경해줄수 있습니다.

이 내용은 해당 ChatClient의 모든 system 메시지에 적용됩니다.

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("You are a friendly chat bot that answers question in the voice of a Pirate")
            .build();
    }

}

테스트하기위한 간단한 RestController 를 작성하고 호출합니다.

@RestController
class AIController {

	private final ChatClient chatClient;

	AIController(ChatClient chatClient) {
		this.chatClient = chatClient;
	}

	@GetMapping("/ai/simple")
	public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
		return Map.of("completion", this.chatClient.prompt().user(message).call().content());
	}
}

아래와 같이 응답하는 것을 확인할 수 있습니다.

❯ curl localhost:8080/ai/simple
{"completion":"Why did the pirate go to the comedy club? To hear some arrr-rated jokes! Arrr, matey!"}

7.2. Default System Text 에 Parameter 적용하기

아례 예시에서는 설정 시점에 파라미터를 갖는 defaultSystem을 설정해두고 런타임에 값을 지정하는 예시입니다.

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
            .build();
    }

}

ChatClient 의 system 메서드를 통해 파라미터를 지정합니다.

@RestController
class AIController {
	private final ChatClient chatClient;

	AIController(ChatClient chatClient) {
		this.chatClient = chatClient;
	}

	@GetMapping("/ai")
	Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message, String voice) {
		return Map.of("completion",
				this.chatClient.prompt()
						.system(sp -> sp.param("voice", voice))
						.user(message)
						.call()
						.content());
	}

}

7.3. 다른 설정가능한 default 값

ChatClient.Builder 정의 시점에 system 외 다른 기본값도 설정할 수 있습니다.

메서드 설명

defaultOptions(ChatOptions chatOptions) ChatOptions 클래스에서 정의된 범용 옵션이나, OpenAiChatOptions 와 같은 모델별 옵션을 전달할 수 있습니다. 모델별 ChatOptions 구현체에 대한 자세한 내용은 JavaDocs를 참고하십시오.
defaultFunction(String name, String description, java.util.function.Function<I, O> function) name 은 사용자 텍스트에서 함수를 참조할 때 사용되며, description 은 함수의 목적을 설명하고 AI 모델이 올바른 함수를 선택해 정확한 응답을 생성하도록 돕습니다. function 인자는 모델이 필요 시 실행할 Java 함수 인스턴스입니다.
defaultFunctions(String… functionNames) 애플리케이션 컨텍스트에 정의된 java.util.Function 빈(bean)의 이름을 지정합니다.
defaultUser(String text)defaultUser(Resource text)defaultUser(Consumer<UserSpec> userSpecConsumer) 기본 사용자 텍스트를 정의합니다. Consumer<UserSpec> 을 사용하면 람다식으로 사용자 텍스트와 기본 파라미터를 지정할 수 있습니다.
defaultAdvisors(Advisor… advisor) Advisor 를 통해 프롬프트를 구성하는 데이터를 수정할 수 있습니다. QuestionAnswerAdvisor 구현체를 사용하면 사용자 텍스트와 관련된 컨텍스트 정보를 프롬프트에 추가해 Retrieval Augmented Generation 패턴을 구현할 수 있습니다.
defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer) Consumer<AdvisorSpec> 을 사용해 여러 Advisor 를 설정할 수 있습니다. 예를 들어, QuestionAnswerAdvisor 를 추가하여 사용자 텍스트 기반으로 관련 컨텍스트를 프롬프트에 덧붙이는 Retrieval Augmented Generation 패턴을 구성할 수 있습니다.

이러한 기본 설정은 런타임에 동일한 이름에서 default 접두어가 없는 메서드를 사용하여 재정의할 수 있습니다.

  • options(ChatOptions chatOptions)
  • function(String name, String description, java.util.function.Function<I, O> function)
  • functions(String… functionNames)
  • user(String text), user(Resource text), user(Consumer<UserSpec> userSpecConsumer)
  • advisors(Advisor… advisor)
  • advisors(Consumer<AdvisorSpec> advisorSpecConsumer)

8. Advisors

Advisors API는 Spring 애플리케이션에서 AI 기반 상호작용을 가로채고(intercept), 수정하고(modify), 향상시키는(enhance) 유연하고 강력한 방법을 제공합니다.

AI 모델을 사용자 입력과 함께 호출할 때 자주 사용하는 패턴 중 하나는 프롬프트에 컨텍스트 데이터(contextual data)를 추가하거나 보강하는 것입니다.

이 컨텍스트 데이터는 여러 유형일 수 있으며, 일반적으로 다음과 같은 유형이 있습니다.

  • Your own data : AI 모델이 학습하지 않은 데이터입니다. 모델이 유사한 데이터를 학습했더라도, 추가된 컨텍스트 데이터가 응답 생성 시 우선적으로 반영됩니다.
  • Conversational history : 채팅 모델의 API는 상태를 저장하지 않습니다. 예를 들어, 모델에 본인의 이름을 알려도 이후 대화에서 기억하지 못합니다. 이전 상호작용을 고려하도록 하려면 매 요청마다 대화 이력을 함께 전달해야 합니다.

8.1. ChatClient에 Advisor 설정

ChatClient의 유창한(Fluent) API는 AdvisorSpec 인터페이스를 제공하여 어드바이저를 구성할 수 있게 합니다.

이 인터페이스는 파라미터를 추가하거나, 한 번에 여러 파라미터를 설정하거나, 하나 이상의 어드바이저를 체인에 추가하는 메서드를 제공합니다.

interface AdvisorSpec {
    AdvisorSpec param(String k, Object v);
    AdvisorSpec params(Map<String, Object> p);
    AdvisorSpec advisors(Advisor... advisors);
    AdvisorSpec advisors(List<Advisor> advisors);
}

<aside> 💡

체인에 어드바이저가 추가되는 순서는 매우 중요합니다. 이 순서가 실행 순서를 결정하기 때문입니다. 각 어드바이저는 프롬프트나 컨텍스트를 어떤 방식으로든 수정하며, 앞선 어드바이저가 만든 변경 사항은 다음 어드바이저로 전달됩니다.

</aside>

아래 구성에서 MessageChatMemoryAdvisor 는 먼저 실행되어 대화 이력을 프롬프트에 추가합니다. 그 다음 QuestionAnswerAdvisor 가 사용자의 질문과 추가된 대화 이력을 기반으로 검색을 수행하여, 더 관련성 높은 결과를 제공할 수 있습니다.

ChatClient.builder(chatModel)
    .build()
    .prompt()
    .advisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(),
        QuestionAnswerAdvisor.builder(vectorStore).build()
    )
    .user(userText)
    .call()
    .content();

8.2. Retrieval Augmented Generation

Advisor를 활용한 Retrieval Augmented Generation (RAG) 내용은 별도 가이드 문서에서 상세하게 확인할 수 있습니다.

8.3. Logging

SimpleLoggerAdvisor 는 ChatClient의 요청과 응답 데이터를 로깅하는 어드바이저입니다. 이 어드바이저는 AI 상호작용을 디버깅하거나 모니터링할 때 유용하게 사용할 수 있습니다.

Logging을 활성화하려면 ChatClient를 생성할 때 어드바이저 체인에 SimpleLoggerAdvisor를 추가하면 됩니다.

일반적으로 체인의 끝부분에 추가하는 것을 권장합니다.

ChatResponse response = ChatClient.create(chatModel).prompt()
        .advisors(new SimpleLoggerAdvisor())
        .user("Tell me a joke?")
        .call()
        .chatResponse();

로그를 확인하려면 application.properties 또는 application.yaml 파일에 다음과 같이 설정해

advisor 패키지의 로깅 레벨을 DEBUG로 지정합니다.

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

또한, 아래와 같이 생성자를 사용하면 AdvisedRequest와 ChatResponse에서 어떤 데이터를 로깅할지 커스터마이징할 수 있습니다.

SimpleLoggerAdvisor(
    Function<ChatClientRequest, String> requestToString,
    Function<ChatResponse, String> responseToString,
    int order
)

사용 예시:

SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
    request -> "Custom request: " + request.prompt().getUserMessage(),
    response -> "Custom response: " + response.getResult(),
    0
);

이를 통해 로깅되는 정보를 애플리케이션의 요구 사항에 맞게 조정할 수 있습니다.

9. Chat Memory

ChatMemory 인터페이스는 채팅 대화 메모리 저장소를 나타냅니다. 이 인터페이스는 대화에 메시지를 추가하고, 대화에서 메시지를 조회하며, 대화 기록을 초기화하는 메서드를 제공합니다.

현재 기본으로 제공되는 구현체는 MessageWindowChatMemory 입니다.

MessageWindowChatMemory 는 지정된 최대 크기(기본값: 20개 메시지)까지 메시지 윈도우를 유지하는 채팅 메모리 구현체입니다.

메시지 개수가 이 한도를 초과하면 가장 오래된 메시지부터 제거되지만, 시스템 메시지는 유지됩니다.

새로운 시스템 메시지가 추가되면 기존의 모든 시스템 메시지가 메모리에서 제거됩니다.

이를 통해 대화에서 가장 최근의 컨텍스트를 항상 유지하면서도 메모리 사용량을 제한할 수 있습니다.

MessageWindowChatMemory 는 ChatMemoryRepository 추상화를 기반으로 하며,

이는 채팅 대화 메모리를 저장하기 위한 다양한 구현체를 제공합니다.

사용 가능한 구현체에는 InMemoryChatMemoryRepository, JdbcChatMemoryRepository,

CassandraChatMemoryRepository, Neo4jChatMemoryRepository 등이 있습니다.

자세한 내용과 사용 예시는 Chat Memory 문서를 참고하십시오.

10. Implementation Notes

  • ChatClient 는 명령형(imperative)과 반응형(reactive) 프로그래밍 모델을 함께 사용하는 독특한 API입니다.
    • 대부분의 애플리케이션은 보통 둘 중 하나만 사용하지만, ChatClient 는 두 모델을 모두 지원합니다.
  • 모델 구현의 HTTP 클라이언트 상호작용을 커스터마이징할 때는 RestClient 와 WebClient 모두를 설정해야 합니다.
    • Spring Boot 3.4의 버그 때문에 spring.http.client.factory=jdk 속성을 반드시 설정해야 합니다. 그렇지 않으면 기본값이 reactor 로 설정되며, ImageModel 과 같은 일부 AI 워크플로가 정상적으로 동작하지 않습니다.
  • 스트리밍(streaming)은 반응형 스택에서만 지원됩니다.
    • 명령형 애플리케이션에서도 스트리밍을 사용하려면 반응형 스택(예: spring-boot-starter-webflux)을 반드시 포함해야 합니다.
  • 비스트리밍(non-streaming)은 서블릿 스택에서만 지원됩니다.
    • 반응형 애플리케이션에서 비스트리밍 호출을 사용하려면 서블릿 스택(예: spring-boot-starter-web)을 포함해야 하며, 일부 호출은 블로킹될 수 있습니다.
  • 도구 호출(tool calling) 은 명령형 방식으로 동작하며, 이는 워크플로를 블로킹하게 만듭니다.
    • 이로 인해 Micrometer 관측이 부분적 또는 중단될 수 있습니다.
    • (예: ChatClient span 과 tool calling span 이 연결되지 않아 첫 번째 span 이 미완성 상태로 남을 수 있습니다.)
  • 내장 어드바이저는 일반 호출에서는 블로킹 방식으로, 스트리밍 호출에서는 논블로킹 방식으로 동작합니다.
    • 어드바이저의 스트리밍 호출에서 사용되는 Reactor Scheduler 는 각 어드바이저 클래스의 Builder를 통해 설정할 수 있습니다.

참고

Spring AI Reference - Chat Client API

반응형