AI Services
AI Services (선언적 AI 서비스)
ChatModel, ChatMessage, ChatMemory 같은 저수준 컴포넌트를 직접 조합하려면 보일러플레이트가 많이 생겨요. LangChain4j 는 이를 숨기고 인터페이스만 선언하면 프록시 구현체를 만들어 주는 상위 개념, AI Services 를 제공해요. Spring Data JPA 나 Retrofit 방식과 비슷하다고 생각하면 돼요.
출처: 공식문서
AI Services 란
인터페이스에 원하는 API 를 선언하면 LangChain4j 가 그 인터페이스를 구현하는 프록시 객체를 만들어 줘요. 내부적으로 LLM 입력 포맷팅과 출력 파싱을 처리하고, 채팅 메모리·도구·RAG 같은 고급 기능도 지원해요.
가장 간단한 AI Service 먼저 볼게요. String 을 받아 String 을 반환하는 chat 메서드를 가진 인터페이스를 정의하고:
interface Assistant {
String chat(String userMessage);
}
저수준 컴포넌트(ChatModel)를 만든 뒤:
ChatModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.build();
AiServices 로 인스턴스를 만듭니다:
Assistant assistant = AiServices.create(Assistant.class, model);
:::note
Quarkus 와 Spring Boot 애플리케이션에서는 자동 설정이 Assistant 빈 생성을 처리해요. AiServices.create(...) 를 호출할 필요 없이 필요한 곳에 주입/autowire 만 하면 돼요.
:::
String answer = assistant.chat("Hello");
System.out.println(answer); // Hello, how can I help you?
동작 원리는 간단해요. 인터페이스 Class 와 저수준 컴포넌트를 AiServices 에 넘기면 프록시 객체를 만들고, 입력·출력 변환을 모두 처리해요. String 입력을 UserMessage 로 바꾸고, 반환 시 AiMessage 를 String 으로 변환하죠.
@SystemMessage
시스템 프롬프트를 지정해야 한다면 @SystemMessage 어노테이션을 써요. 내부적으로 SystemMessage 로 변환되어 LLM 에 전달돼요.
interface Friend {
@SystemMessage("You are a good friend of mine. Answer using slang.")
String chat(String userMessage);
}
Friend friend = AiServices.create(Friend.class, model);
String answer = friend.chat("Hello"); // Hey! What's up?
@SystemMessage(fromResource = "my-prompt-template.txt") 로 리소스에서 프롬프트 템플릿을 불러올 수도 있고, 인터페이스 레벨에 선언하면 상속 메서드를 포함한 모든 메서드에 적용돼요. 메서드에 선언한 쪽이 인터페이스보다 우선해요.
시스템 메시지를 동적으로 주고 싶으면 systemMessageProvider(chatMemoryId -> "...") 로 채팅 메모리 ID(사용자·대화)마다 다른 메시지를 줄 수 있어요. systemMessageTransformer(...) 는 매 호출마다 시스템 메시지를 변형해요.
@UserMessage
시스템 메시지를 지원하지 않는 모델이거나, UserMessage 를 쓰고 싶다면 @UserMessage 로 프롬프트 템플릿을 지정해요. {{it}} 자리 표시자로 메서드 인자를 주입할 수 있어요. 예를 들어 감정 분석:
interface SentimentAnalyzer {
@UserMessage("Does {{it}} has a positive sentiment?")
boolean isPositive(String text);
}
boolean positive = sentimentAnalyzer.isPositive("It's wonderful!"); // true
반환 타입 (Return Types)
AI Service 메서드는 여러 타입을 반환할 수 있어요:
String— LLM 출력을 가공 없이 그대로 반환- Structured Outputs 가 지원하는 타입 — LLM 출력을 원하는 타입으로 파싱해 반환
TokenStream— 응답을 생성되는 대로 스트리밍CompletableFuture<T>/CompletionStage<T>,Flow.Publisher<...>— 호출 스레드를 블로킹하지 않고 실행 (실험적)
Result<T> 로 감싸면 AI Service 호출에 대한 부가 메타데이터를 얻을 수 있어요: TokenUsage(도구 실행 등 여러 번 LLM 호출 시 합산), RAG 로 검색된 Content 소스, 실행된 도구 목록, 마지막 FinishReason, 중간·최종 ChatResponse 등.
Result<List<String>> result = assistant.generateOutlineFor("Java");
List<String> outline = result.content();
TokenUsage tokenUsage = result.tokenUsage();
List<Content> sources = result.sources();
List<ToolExecution> toolExecutions = result.toolExecutions();
FinishReason finishReason = result.finishReason();
Result<T>의T는 LLM 이 실제로 생성할 수 있는 타입이어야 해요.ChatResponse,ChatMessage,TextSegment,Embedding,TokenUsage같은 LangChain4j 자체 타입은T로 쓸 수 없고, 그렇게 하면IllegalConfigurationException이 나요.
구조화 출력 (Structured Outputs)
복잡한 Java 객체를 LLM 에서 받고 싶다면 반환 타입을 바꾸면 돼요.
boolean:
interface SentimentAnalyzer {
@UserMessage("Does {{it}} has a positive sentiment?")
boolean isPositive(String text);
}
Enum:
enum Priority { CRITICAL, HIGH, LOW }
interface PriorityAnalyzer {
@UserMessage("Analyze the priority of the following issue: {{it}}")
Priority analyzePriority(String issueDescription);
}
// Priority priority = ...analyzePriority("The main payment gateway is down..."); // CRITICAL
POJO — @Description 로 LLM 이해를 돕는 설명을 붙일 수 있어요:
class Person {
@Description("first name of a person")
String firstName;
String lastName;
LocalDate birthDate;
Address address;
}
@Description("an address")
class Address {
String street;
Integer streetNumber;
String city;
}
interface PersonExtractor {
@UserMessage("Extract information about a person from {{it}}")
Person extractPersonFrom(String text);
}
스트리밍
TokenStream 반환 타입으로 응답을 토큰 단위로 스트리밍할 수 있어요. 콜백 체인으로 각 이벤트를 처리하고 .start() 로 시작해요:
TokenStream tokenStream = assistant.chat("Tell me a joke");
tokenStream
.onPartialResponse((String partialResponse) -> System.out.println(partialResponse))
.onPartialThinking((PartialThinking partialThinking) -> System.out.println(partialThinking))
.onRetrieved((List<Content> contents) -> System.out.println(contents))
.onIntermediateResponse((ChatResponse intermediateResponse) -> System.out.println(intermediateResponse))
.onPartialToolCall((PartialToolCall partialToolCall) -> System.out.println(partialToolCall))
.beforeToolExecution((BeforeToolExecution beforeToolExecution) -> System.out.println(beforeToolExecution))
.onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
.onCompleteResponse((ChatResponse response) -> futureResponse.complete(response))
.onError((Throwable error) -> futureResponse.completeExceptionally(error))
.start();
채팅 메모리 ID, 문서 로드, RAG, 벡터 저장소, Retrieval Augmenter 설정, Auto-Moderation(부적절 콘텐츠 탐지 시 ModerationException 발생), AI Service 간 연결(한 AI Service 를 다른 AI Service 의 도구로 사용) 등의 고급 기능도 같은 방식으로 구성해요. 자세한 내용은 원문을 참고하세요.
더 알아보기
- Spring Boot 통합 —
@AiService자동 설정과 빈 와이어링 - Chat and Language Models — 저수준
ChatModelAPI - 채팅 메모리 — 대화 상태 유지
- 도구 호출 — AI Service 를 도구로 구성
- RAG —
ContentRetriever연동 - Streaming 튜토리얼 by Siva