Advisors API

Advisors API

Spring AI Advisors API는 Spring 애플리케이션에서 AI 기반 상호작용을 가로채고, 수정하고, 강화하는 유연하고 강력한 방법을 제공해요. Advisors API를 활용하면 더 정교하고 재사용 가능하며 유지보수하기 쉬운 AI 컴포넌트를 만들 수 있어요.

핵심 이점은 반복되는 생성형 AI 패턴을 캡슐화하고, LLM과 주고받는 데이터를 변환하며, 다양한 모델과 사용 사례에 걸친 이식성을 제공하는 거예요.

다음 예시처럼 ChatClient API로 기존 어드바이저를 구성할 수 있어요:


ChatMemory chatMemory = ... // Initialize your chat memory store
VectorStore vectorStore = ... // Initialize your vector store

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(), // chat-memory advisor
        QuestionAnswerAdvisor.builder(vectorStore).build()    // RAG advisor
    )
    .build();

var conversationId = "678";

String response = this.chatClient.prompt()
    // Set advisor parameters at runtime	
    .advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
    .user(userText)
    .call()
	.content();

어드바이저는 빌드 시점에 빌더의 defaultAdvisors() 메서드로 등록하는 것을 권장해요.

Advisors는 관측성 스택에도 참여해서, 실행과 관련된 메트릭·트레이스를 볼 수 있어요.

출처: 공식문서

핵심 컴포넌트

API는 비스트리밍 시나리오용 CallAdvisor·CallAdvisorChain, 스트리밍 시나리오용 StreamAdvisor·StreamAdvisorChain으로 구성돼요. 또한 봉인되지 않은(unsealed) Prompt 요청을 나타내는 ChatClientRequest, Chat Completion 응답용 ChatClientResponse도 포함해요. 둘 다 어드바이저 체인 전반에 걸쳐 상태를 공유하는 advise-context를 담아요.

adviseCall()adviseStream()이 핵심 어드바이저 메서드예요. 보통 봉인되지 않은 Prompt 데이터 검사, Prompt 데이터 커스터마이즈·증강, 체인의 다음 엔티티 호출, 선택적으로 요청 차단, 채팅 완성 응답 검사, 처리 오류를 알리는 예외 던지기 같은 동작을 수행해요.

추가로 getOrder() 메서드가 체인에서 어드바이저 순서를 결정하고, getName()이 고유한 어드바이저 이름을 제공해요.

Spring AI 프레임워크가 만드는 어드바이저 체인(Advisor Chain)getOrder() 값으로 정렬된 여러 어드바이저를 순차 호출하게 해줘요. 낮은 값이 먼저 실행돼요. 자동으로 추가되는 마지막 어드바이저는 요청을 LLM으로 보내요.

어드바이저 체인과 Chat Model 사이의 상호작용을 보여주는 흐름:

  1. Spring AI 프레임워크가 사용자의 Prompt와 빈 어드바이저 context 객체에서 ChatClientRequest를 만들어요.
  2. 체인의 각 어드바이저가 요청을 처리하고, 잠재적으로 수정해요. 또는 다음 엔티티를 호출하지 않고 요청을 차단할 수도 있어요. 후자의 경우 어드바이저가 응답을 채울 책임을 져요.
  3. 프레임워크가 제공하는 최종 어드바이저가 요청을 Chat Model로 보내요.
  4. Chat Model의 응답은 어드바이저 체인을 거쳐 다시 전달되고 ChatClientResponse로 변환돼요. 나중에 공유된 어드바이저 context 인스턴스를 포함해요.
  5. 각 어드바이저가 응답을 처리하거나 수정할 수 있어요.
  6. ChatCompletion을 추출해 최종 ChatClientResponse가 클라이언트에 반환돼요.

어드바이저 순서

체인에서 어드바이저의 실행 순서는 getOrder() 메서드로 결정돼요. 이해해야 할 핵심 사항:

  • 순서 값이 낮은 어드바이저가 먼저 실행돼요.
  • 어드바이저 체인은 스택처럼 동작해요:
    • 체인의 첫 어드바이저가 요청을 먼저 처리해요.
    • 또한 응답을 마지막으로 처리해요.
  • 실행 순서를 제어하려면:
    • 체인에서 어드바이저가 먼저 실행(요청 처리에서 첫, 응답 처리에서 마지막)되도록 Ordered.HIGHEST_PRECEDENCE에 가깝게 순서를 설정하세요.
    • 체인에서 어드바이저가 마지막으로 실행(요청 처리에서 마지막, 응답 처리에서 첫)되도록 Ordered.LOWEST_PRECEDENCE에 가깝게 순서를 설정하세요.
  • 값이 높을수록 우선순위가 낮은 것으로 해석돼요.
  • 여러 어드바이저가 같은 순서 값이면 실행 순서가 보장되지 않아요.

참고: 순서와 실행 순서 사이의 모순처럼 보이는 것은 어드바이저 체인의 스택 같은 성질 때문이에요:

  • 최고 우선순위(가장 낮은 순서 값)의 어드바이저가 스택 맨 위에 추가돼요.
  • 스택이 풀리면서 요청을 가장 먼저 처리해요.
  • 스택이 되감기면서 응답을 가장 마지막에 처리해요.

상기시키자면, Spring Ordered 인터페이스의 의미는:

public interface Ordered {

    /**
     * Constant for the highest precedence value.
     * @see java.lang.Integer#MIN_VALUE
     */
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

    /**
     * Constant for the lowest precedence value.
     * @see java.lang.Integer#MAX_VALUE
     */
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

    /**
     * Get the order value of this object.
     * <p>Higher values are interpreted as lower priority. As a consequence,
     * the object with the lowest value has the highest priority (somewhat
     * analogous to Servlet {@code load-on-startup} values).
     * <p>Same order values will result in arbitrary sort positions for the
     * affected objects.
     * @return the order value
     * @see #HIGHEST_PRECEDENCE
     * @see #LOWEST_PRECEDENCE
     */
    int getOrder();
}

팁: 입력·출력 양쪽 모두에서 체인의 첫 위치에 있어야 하는 사용 사례:

  1. 각 측면에 별도의 어드바이저를 사용하세요.
  2. 서로 다른 순서 값으로 구성하세요.
  3. 어드바이저 컨텍스트로 둘 사이에 상태를 공유하세요.

API 개요

주요 Advisor 인터페이스는 org.springframework.ai.chat.client.advisor.api 패키지에 있어요. 자신만의 어드바이저를 만들 때 마주칠 핵심 인터페이스:

public interface Advisor extends Ordered {

	String getName();

}

동기·리액티브 어드바이저의 두 하위 인터페이스:

public interface CallAdvisor extends Advisor {

	ChatClientResponse adviseCall(
		ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);

}

그리고:

public interface StreamAdvisor extends Advisor {

	Flux<ChatClientResponse> adviseStream(
		ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);

}

Advice 체인을 계속하려면 Advice 구현에서 CallAdvisorChainStreamAdvisorChain을 사용하세요:

public interface CallAdvisorChain extends AdvisorChain {

	/**
	 * Invokes the next {@link CallAdvisor} in the {@link CallAdvisorChain} with the given
	 * request.
	 */
	ChatClientResponse nextCall(ChatClientRequest chatClientRequest);

	/**
	 * Returns the list of all the {@link CallAdvisor} instances included in this chain at
	 * the time of its creation.
	 */
	List<CallAdvisor> getCallAdvisors();

}

그리고:

public interface StreamAdvisorChain extends AdvisorChain {

	/**
	 * Invokes the next {@link StreamAdvisor} in the {@link StreamAdvisorChain} with the
	 * given request.
	 */
	Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest);

	/**
	 * Returns the list of all the {@link StreamAdvisor} instances included in this chain
	 * at the time of its creation.
	 */
	List<StreamAdvisor> getStreamAdvisors();

}

어드바이저 구현하기

어드바이저를 만들려면 CallAdvisorStreamAdvisor(또는 둘 다)를 구현하세요. 구현할 핵심 메서드는 비스트리밍용 nextCall(), 스트리밍 어드바이저용 nextStream()이에요.

예시

관찰·증강 사용 사례용 어드바이저를 구현하는 방법을 몇 가지 실습 예시로 보여드릴게요.

로깅 어드바이저

체인의 다음 어드바이저 호출 전 ChatClientRequest를, 후에 ChatClientResponse를 로깅하는 간단한 로깅 어드바이저를 구현할 수 있어요. 어드바이저는 요청과 응답만 관찰하고 수정하지 않아요. 이 구현은 비스트리밍·스트리밍 시나리오를 모두 지원해요.

public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {

	private static final Logger logger = LoggerFactory.getLogger(SimpleLoggerAdvisor.class);

	@Override
	public String getName() { // <1>
		return this.getClass().getSimpleName();
	}

	@Override
	public int getOrder() { // <2>
		return 0; 
	}


	@Override
	public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
		logRequest(chatClientRequest);

		ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);

		logResponse(chatClientResponse);

		return chatClientResponse;
	}

	@Override
	public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest,
			StreamAdvisorChain streamAdvisorChain) {
		logRequest(chatClientRequest);

		Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest);

		return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponses, this::logResponse); // <3>
	}

	private void logRequest(ChatClientRequest request) {
		logger.debug("request: {}", request);
	}

	private void logResponse(ChatClientResponse chatClientResponse) {
		logger.debug("response: {}", chatClientResponse);
	}

}

<1> 어드바이저의 고유한 이름을 제공. <2> 순서 값을 설정해 실행 순서를 제어. 낮은 값이 먼저 실행. <3> MessageAggregator는 Flux 응답을 단일 ChatClientResponse로 집계하는 유틸리티 클래스. 스트림의 개별 항목이 아니라 전체 응답을 관찰하는 로깅·다른 처리를 할 때 유용. MessageAggregator에서 응답을 바꿀 수는 없는데, 읽기 전용 연산이기 때문.

Re-Reading (Re2) 어드바이저

"Re-Reading Improves Reasoning in Large Language Models" 논문은 LLM의 추론 능력을 향상시키는 Re-Reading(Re2) 기법을 소개해요. Re2 기법은 입력 프롬프트를 이렇게 증강해야 해요:

{Input_Query}
Read the question again: {Input_Query}

사용자의 입력 쿼리에 Re2 기법을 적용하는 어드바이저를 이렇게 구현할 수 있어요:


public class ReReadingAdvisor implements BaseAdvisor {

	private static final String DEFAULT_RE2_ADVISE_TEMPLATE = """
			{re2_input_query}
			Read the question again: {re2_input_query}
			""";

	private final String re2AdviseTemplate;

	private int order = 0;

	public ReReadingAdvisor() {
		this(DEFAULT_RE2_ADVISE_TEMPLATE);
	}

	public ReReadingAdvisor(String re2AdviseTemplate) {
		this.re2AdviseTemplate = re2AdviseTemplate;
	}

	@Override
	public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) { // <1>
		String augmentedUserText = PromptTemplate.builder()
			.template(this.re2AdviseTemplate)
			.variables(Map.of("re2_input_query", chatClientRequest.prompt().getUserMessage().getText()))
			.build()
			.render();

		return chatClientRequest.mutate()
			.prompt(chatClientRequest.prompt().augmentUserMessage(augmentedUserText))
			.build();
	}

	@Override
	public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
		return chatClientResponse;
	}

	@Override
	public int getOrder() { // <2>
		return this.order;
	}

	public ReReadingAdvisor withOrder(int order) {
		this.order = order;
		return this;
	}

}

<1> before 메서드가 Re-Reading 기법을 적용해 사용자의 입력 쿼리를 증강. <2> 순서 값을 설정해 실행 순서를 제어. 낮은 값이 먼저 실행.

Spring AI 내장 어드바이저

Spring AI 프레임워크는 AI 상호작용을 강화하는 여러 내장 어드바이저를 제공해요. 사용 가능한 어드바이저 개요:

채팅 메모리 어드바이저

채팅 메모리 저장소의 대화 히스토리를 관리하는 어드바이저:

  • MessageChatMemoryAdvisor
    • 메모리를 검색해 프롬프트에 메시지 컬렉션으로 추가해요. 이 접근 방식은 대화 히스토리의 구조를 유지해요. 다만 모든 AI 모델이 이 방식을 지원하지는 않아요.
  • VectorStoreChatMemoryAdvisor
    • VectorStore에서 메모리를 검색해 프롬프트의 시스템 텍스트에 추가해요. 대규모 데이터셋에서 관련 정보를 효율적으로 검색하고 꺼내는 데 유용한 어드바이저예요.
질문 답변 어드바이저
  • QuestionAnswerAdvisor
    • vector store를 사용해 질문 답변 기능을 제공하며, Naive RAG(Retrieval-Augmented Generation) 패턴을 구현해요.
  • RetrievalAugmentationAdvisor
    • org.springframework.ai.rag 패키지에 정의된 구성 요소를 사용해 일반적인 Retrieval Augmented Generation(RAG) 흐름을 구현하고 Modular RAG 아키텍처를 따르는 어드바이저예요.
추론 어드바이저
도구 호출 어드바이저
  • ToolCallingAdvisor
    • 어드바이저 체인의 일부로 도구 호출 루프를 처리해요. 이 어드바이저는 ChatClient가 항상 자동 등록하며(명시적으로 끄지 않으면), 그래서 정적 도구가 구성되지 않아도 다른 어드바이저가 런타임에 주입한 도구가 지원돼요. 모델이 요청한 도구 호출을 실행하고 결과를 다시 보내며 더 이상 도구 호출이 필요 없을 때까지 반복해요. 두 번째 ToolCallingAdvisor가 자동 등록되는 것을 막는 마커 인터페이스인 ToolAdvisor를 구현해요.
    • 전체 문서는 Recursive Advisors - ToolCallingAdvisorChatClient - Tool Calling 참고.
콘텐츠 안전 어드바이저
  • SafeGuardAdvisor
    • 모델이 해롭거나 부적절한 콘텐츠를 생성하는 것을 막도록 설계된 간단한 어드바이저예요.

스트리밍 vs 비스트리밍

  • 비스트리밍 어드바이저는 완전한 요청·응답으로 동작해요.
  • 스트리밍 어드바이저는 요청·응답을 연속 스트림으로 처리하며 리액티브 프로그래밍 개념(응답에 Flux 등)을 사용해요.
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain chain) {
    
    return  Mono.just(chatClientRequest)
            .publishOn(Schedulers.boundedElastic())
            .map(request -> {
                // This can be executed by blocking and non-blocking Threads.
                // Advisor before next section
            })
            .flatMapMany(request -> chain.nextStream(request))
            .map(response -> {
                // Advisor after next section
            });
}

모범 사례

  1. 어드바이저를 특정 작업에 집중시켜 모듈성을 높이세요.
  2. 필요할 때 adviseContext로 어드바이저 간 상태를 공유하세요.
  3. 최대 유연성을 위해 어드바이저의 스트리밍·비스트리밍 버전을 모두 구현하세요.
  4. 적절한 데이터 흐름을 보장하려면 체인에서 어드바이저 순서를 신중히 고려하세요.

Breaking API 변경 사항

어드바이저 인터페이스

  • 1.0 M2에는 별도의 RequestAdvisorResponseAdvisor 인터페이스가 있었어요.
    • RequestAdvisorChatModel.callChatModel.stream 메서드 전에 호출됐어요.
    • ResponseAdvisor는 이 메서드들 후에 호출됐어요.
  • 1.0 M3부터 이 인터페이스들은 다음으로 대체됐어요:
    • CallAroundAdvisor
    • StreamAroundAdvisor
  • ResponseAdvisor의 일부였던 StreamResponseMode는 제거됐어요.
  • 1.0.0부터 이 인터페이스들이 대체됐어요:
    • CallAroundAdvisorCallAdvisor, StreamAroundAdvisorStreamAdvisor, CallAroundAdvisorChainCallAdvisorChain, StreamAroundAdvisorChainStreamAdvisorChain.
    • AdvisedRequestChatClientRequest, AdvisedResponseChatClientResponse.

컨텍스트 맵 처리

  • 1.0 M2:
    • 컨텍스트 맵은 별도의 메서드 인자였어요.
    • 맵은 변경 가능하고 체인을 따라 전달됐어요.
  • 1.0 M3:
    • 컨텍스트 맵은 이제 AdvisedRequestAdvisedResponse record의 일부예요.
    • 맵은 불변(immutable)이에요.
    • 컨텍스트를 업데이트하려면 updateContext 메서드를 사용하세요. 이 메서드는 업데이트된 내용으로 새 수정 불가능 맵을 만들어요.

더 알아보기