Chat and Language Models

Chat and Language Models (채팅·언어 모델)

LangChain4j 로 LLM 을 다루는 가장 기본적인 API 를 살펴볼게요. 여기 나오는 ChatModel 은 저수준 API 로 가장 강력하고 유연해요. 상위 레벨의 AI Services 는 이 위에 쌓여 있어요.

출처: 공식문서

두 가지 LLM API 유형

  • LanguageModelString 을 입력받아 String 을 반환하는 아주 단순한 API. 이제 점차 채팅 API 로 대체되고 있어요.
  • ChatModel — 여러 ChatMessage 를 입력받아 하나의 AiMessage 를 반환.

LangChain4j 는 앞으로 LanguageModel 에 대한 지원을 확장하지 않을 예정이에요. 새 기능은 전부 ChatModel API 로 만들어지죠. ChatModel 외에도 EmbeddingModel(텍스트→Embedding), ImageModel(이미지 생성·편집), ModerationModel(유해 콘텐츠 검사), ScoringModel(쿼리 대비 텍스트 관련도 점수·랭킹, RAG 에 유용) 모델 타입이 있어요.

ChatModel API

ChatModel 에는 편의용 chat(String) 메서드가 있는데, StringUserMessage 로 감쌀 필요 없이 빠르게 시험해볼 수 있어요.

public interface ChatModel {

    String chat(String userMessage);
    
    ...
}

여러 메시지를 다루는 오버로드와 요청을 세밀하게 커스터마이즈하는 chat(ChatRequest) 도 있어요:

    ChatResponse chat(ChatMessage... messages);

    ChatResponse chat(List<ChatMessage> messages);

    ChatResponse chat(ChatRequest chatRequest);

ChatRequest 는 빌더로 model name, temperature, topP, topK, frequencyPenalty, presencePenalty, maxOutputTokens, stopSequences, toolSpecifications, toolChoice, responseFormat 등을 지정할 수 있어요. parameters(...) 로 공통·프로바이더별 파라미터를 한 번에 넣을 수도 있어요.

ChatMessage 유형

  • UserMessage — 사용자의 메시지. contents()(텍스트 또는 멀티모달), name(), attributes()(모델에 전송되지 않고 ChatMemory 에 저장되는 속성)를 포함.
  • AiMessage — AI 가 생성한 메시지. text(), thinking()(추론 내용), toolExecutionRequests()(도구 실행 요청), attributes() 포함.
  • ToolExecutionResultMessageToolExecutionRequest 의 실행 결과.
  • SystemMessage — 시스템 메시지. 개발자가 역할·행동·답변 스타일 지시를 넣어요. LLM 은 다른 메시지보다 SystemMessage 에 더 주의를 기울이도록 훈련돼 있어, 엔드 유저가 자유롭게 주입하지 못하게 하는 게 좋아요. 보통 대화의 시작에 위치.
  • CustomMessage — 임의 속성을 담는 커스텀 메시지. 이를 지원하는 ChatModel 구현(현재는 Ollama 만)에서만 사용 가능.

다중 턴 대화

LLM 은 본질적으로 무상태(stateless)라 대화 상태를 직접 관리해야 해요. 다중 턴 대화를 만들려면 이전 메시지를 계속 함께 넘겨야 해요:

UserMessage firstUserMessage = UserMessage.from("Hello, my name is Klaus");
AiMessage firstAiMessage = model.chat(firstUserMessage).aiMessage(); // Hi Klaus, how can I help you?
UserMessage secondUserMessage = UserMessage.from("What is my name?");
AiMessage secondAiMessage = model.chat(firstUserMessage, firstAiMessage, secondUserMessage).aiMessage(); // Klaus

이런 메시지 관리를 손으로 하는 건 번거로워요. 그래서 ChatMemory 개념이 있어요.

ChatResponseAiMessage 외에도 ChatResponseMetadata 를 담는데, 여기에 TokenUsage(입력 토큰·출력 토큰·합계 — 호출 당 비용 계산에 필요)와 FinishReason(생성 중단 사유, 보통 FinishReason.STOP)가 포함돼요.

멀티모달

UserMessageList<Content> 를 가질 수 있고 Content 구현으로 TextContent, ImageContent, AudioContent, VideoContent, PdfFileContent 가 있어요. 이미지 URL 을 원격으로 넘기거나:

UserMessage userMessage = UserMessage.from(
    TextContent.from("Describe the following image"),
    ImageContent.from("https://example.com/cat.jpg")
);
ChatResponse response = model.chat(userMessage);

Base64 바이너리로도 넘길 수 있어요. ImageContent.from(base64Data, "image/jpg"). DetailLevel enum(LOW/HIGH/AUTO)으로 모델이 이미지를 처리하는 방식을 제어할 수도 있어요.

논블로킹 호출 (실험적)

ChatModel 은 호출 스레드를 블로킹하지 않고도 답할 수 있어요:

CompletableFuture<ChatResponse> future = model.chatAsync(chatRequest);

future 를 취소하면 호출자를 풀어주고 진행 중인 HTTP 호출을 중단하려 시도해요. future 는 모델의 transport 스레드에서 완성되므로, 명시적 executor 없이 붙인 연속 작업은 거기서 돌아요 — 논블로킹으로 유지하거나 thenApplyAsync(fn, executor) 를 쓰세요. 논블로킹 경로를 구현하지 않은 프로바이더는 조용히 블로킹하는 대신 AsyncNotSupportedException 으로 뚜렷하게 실패해요. 전체 그림은 Non-blocking and Reactive 참고.

Kotlin 확장

ChatModel Kotlin 확장은 코루틴 기반 비동기 처리를 제공해요. chatAsync(request: ChatRequest)Dispatchers.IOchat 을 감싸는 suspend fun 이고, chat(block: ChatRequestBuilder.() -> Unit) 은 타입 안전 빌더 DSL 로 비동기 실행을 제공해요.

Kotlin 사용자 주의(파괴적 변경): 이제 ChatModel 은 Java 멤버로 chatAsync(ChatRequest): CompletableFuture<ChatResponse> 를 선언해요. Kotlin 에서 같은 시그니처의 멤버가 확장보다 우선하므로, 이전에 suspend 확장(반환 ChatResponse)으로 해석되던 단일 인자 호출이 이제 멤버(반환 CompletableFuture<ChatResponse>)로 해석돼요. 마이그레이션: model.chatAsync(request).await() 을 쓰거나, Dispatchers.IO 를 넘겨 여전히 suspend 확장을 선택하게 하면 돼요.

더 알아보기