Spring AI의 Advisors API는 Spring 애플리케이션에서 AI 기반 상호작용을 가로채고(intercept), 수정하고(modify), 향상(enhance)할 수 있는 유연하고 강력한 방법을 제공합니다.
Advisors API를 활용하면 개발자는 더 정교하고, 재사용 가능하며, 유지 관리가 용이한 AI 컴포넌트를 만들 수 있습니다.
주요 이점에는 반복되는 생성형 AI 패턴을 캡슐화하고, 대규모 언어 모델(LLM)에 보내고 받는 데이터를 변환하며, 다양한 모델과 사용 사례에서 이식성을 제공하는 것이 포함됩니다.
아래 예제와 같이 ChatClient API를 사용해 기존 Advisor를 구성할 수 있습니다
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();
빌드 시점에 builder의 defaultAdvisors() 메서드를 사용하여 Advisor를 등록하는 것이 권장됩니다.
또한 Advisor는 Observability 스택에도 참여하므로, 실행과 관련된 메트릭과 트레이스를 확인할 수 있습니다.
1. 핵심 컴포넌트
Advisor API는 비스트리밍(non-streaming) 시나리오를 위한 CallAdvisor 와 CallAdvisorChain,
스트리밍(streaming) 시나리오를 위한 StreamAdvisor 와 StreamAdvisorChain으로 구성됩니다.
또한 프롬프트 요청을 표현하는 ChatClientRequest 와 채팅 완료(Chat Completion) 응답을 표현하는 ChatClientResponse를 포함합니다.
이 두 객체 모두 advisor 체인 전체에서 상태를 공유하기 위한 advise-context를 보유하고 있습니다.

adviseCall() 과 adviseStream() 메서드는 Advisor의 핵심 메서드로, 일반적으로 다음과 같은 작업을 수행합니다.
- unsealed Prompt 데이터 검사
- Prompt 데이터 커스터마이징 및 보강(augmenting)
- advisor 체인의 다음 엔티티 호출
- 요청을 차단(옵션)
- Chat Completion 응답 검사
- 처리 오류를 나타내기 위해 예외 발생
또한 getOrder() 메서드는 체인 내에서 Advisor의 실행 순서를 결정하며 getName() 메서드는 고유한 Advisor 이름을 제공합니다.
Spring AI 프레임워크가 생성하는 Advisor Chain은 getOrder() 값에 따라 정렬된 여러 Advisor를 순차적으로 호출할 수 있도록 합니다. 값이 낮을수록 먼저 실행되며, 마지막 Advisor(자동으로 추가됨)가 실제로 요청을 LLM에 전달합니다.
아래 흐름도는 Advisor Chain과 Chat Model 간의 상호작용을 설명합니다.

- Spring AI 프레임워크는 사용자의 Prompt 로부터 ChatClientRequest 를 생성하고, 빈 advisor context 객체를 함께 초기화합니다.
- 체인에 포함된 각 Advisor는 요청을 처리하며 필요하면 이를 수정할 수 있습니다. 또는 요청을 다음 엔티티에 전달하지 않고 차단(block) 할 수도 있으며, 이 경우 해당 Advisor가 직접 응답을 작성해야 합니다.
- 프레임워크에서 제공하는 마지막 Advisor가 최종적으로 요청을 Chat Model에 전달합니다.
- Chat Model이 반환한 응답은 다시 Advisor Chain을 거치며 전달되고, 이 과정에서 응답은 ChatClientResponse 로 변환됩니다. 이 객체에는 공유된 advisor context 인스턴스가 포함됩니다.
- 각 Advisor는 이 단계에서 응답을 검사하거나 수정할 수 있습니다.
- 최종적으로 완성된 ChatClientResponse 는 ChatCompletion 을 추출하여 클라이언트에 반환됩니다.
1.1. Advisor Order
Advisor 체인에서 실행 순서는 getOrder() 메서드에 의해 결정됩니다.
- 값이 낮은 Advisor가 먼저 실행됩니다.
- Advisor 체인은 **스택(stack)**처럼 동작합니다:
- 체인의 첫 번째 Advisor가 요청(request)을 가장 먼저 처리합니다.
- 동시에 이 Advisor가 응답(response)을 가장 마지막에 처리합니다.
- 실행 순서를 제어하려면:
- Ordered.HIGHEST_PRECEDENCE(가장 작은 값)에 가깝게 설정하면 Advisor가 요청을 가장 먼저 처리하고 응답을 가장 마지막에 처리하게 됩니다.
- Ordered.LOWEST_PRECEDENCE(가장 큰 값)에 가깝게 설정하면 Advisor가 요청을 가장 마지막에 처리하고 응답을 가장 먼저 처리하게 됩니다.
- 값이 클수록 우선순위가 낮게 해석됩니다.
- 여러 Advisor가 동일한 order 값을 가진 경우, 실행 순서는 보장되지 않습니다.
참고로, 다음은 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();
}
<aside> 💡
입력과 출력 양쪽에서 모두 체인의 첫 번째가 되어야 하는 사용 사례의 경우:
- 입력용 Advisor와 출력용 Advisor를 별도로 구현합니다.
- 각각을 다른 order 값으로 설정합니다.
- 두 Advisor 간에 상태를 공유하기 위해 advisor context를 사용합니다. </aside>
2. API 개요
주요 Advisor 인터페이스들은 org.springframework.ai.chat.client.advisor.api 패키지에 위치해 있습니다. 커스텀 Advisor를 만들 때 접하게 될 핵심 인터페이스들은 다음과 같습니다.
public interface Advisor extends Ordered {
String getName();
}
동기식 및 반응형 방식을 위한 두가지 서브 인터페이스가 있습니다.
public interface CallAdvisor extends Advisor { // sync
ChatClientResponse adviseCall(
ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);
}
public interface StreamAdvisor extends Advisor { // reactive
Flux<ChatClientResponse> adviseStream(
ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);
}
각 방식에 해당하는 Advisor가 실행되기 위한 체인도 동작방식별로 존재합니다.
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();
}
3. Advisor 구현
Advisor를 생성하려면 CallAdvisor 또는 StreamAdvisor(또는 둘 다)를 구현하면 됩니다. 핵심 메서드는 논스트리밍의 경우 nextCall()이고, 스트리밍 advisor의 경우 nextStream()입니다.
3.1 Examples
관찰(observing) 및 보강(augmenting) 사용 사례를 구현하는 방법을 보여주기 위해 몇 가지 실습 예제를 제공하겠습니다.
3.1.1 Logging Advisor
체인에서 다음 Advisor를 호출하기 전에는 ChatClientRequest, 호출한 후에는 ChatClientResponse를 로깅하는 간단한 로깅 Advisor를 구현할 수 있습니다. 이 Advisor는 요청과 응답을 관찰만 하며 수정하지는 않습니다. 본 구현은 비스트리밍과 스트리밍 시나리오를 모두 지원합니다.
public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {
private static final Logger logger = LoggerFactory.getLogger(SimpleLoggerAdvisor.class);
@Override
public String getName() {
// Advisor 에 유니크한 이름 제공
return this.getClass().getSimpleName();
}
@Override
public int getOrder() {
// 실행 순서 제어
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);
}
private void logRequest(ChatClientRequest request) {
logger.debug("request: {}", request);
}
private void logResponse(ChatClientResponse chatClientResponse) {
logger.debug("response: {}", chatClientResponse);
}
}
스트리밍 방식 예제에서 사용된 MessageAggregator는 Flux 형태의 응답들을 단일 ChatClientResponse로 집계하는 유틸리티 클래스입니다. 이는 스트림의 개별 항목이 아니라 전체 응답을 관찰해야 하는 로깅 또는 기타 처리에 유용합니다. 또한 읽기 전용으로 동작하므로, MessageAggregator 내부에서는 응답을 변경할 수 없습니다.
3.3.2 Re-Reading (Re2) Advisor
Re-Reading Improves Reasoning in Large Language Models라는 논문에서는 Re-Reading(Re2) 이라는 기법을 소개합니다. 이 기법은 대규모 언어 모델(LLM) 의 추론(reasoning) 능력을 향상시키는 방법으로, 입력 프롬프트를 아래와 같이 보강(augmenting) 하는 과정을 필요로 합니다.
{Input_Query}
Read the question again: {Input_Query}
LoggingAdvisor 예시와 달리 before, after 로 나누어 접근하는 방법도 존재합니다.
Re2 적용은 요청 전 단계에서만 작업하면 되므로 before 메서드를 활용하여 구현할 수 있습니다.
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) {
// 요청에서 사용자 입력 프롬프트를 Re2 적용된 프롬프트로 바꾼다
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() {
return this.order;
}
public ReReadingAdvisor withOrder(int order) {
this.order = order;
return this;
}
}
3.3.3. Spring Built-in Advisors
Spring AI Framework 에서는 AI와 여러 상호작용을 하기 위한 여러 built-in advisor를 제공합니다.
유형 Advisor 이름 설명
| Chat Memory Advisors | MessageChatMemoryAdvisor | 메모리를 검색하여 메시지 컬렉션으로 프롬프트에 추가. 대화 히스토리 구조 유지. (모든 AI 모델이 지원하는 것은 아님) |
| PromptChatMemoryAdvisor | 메모리를 검색하여 프롬프트의 시스템 텍스트에 통합 | |
| VectorStoreChatMemoryAdvisor | VectorStore에서 메모리를 검색하여 프롬프트의 시스템 텍스트에 추가. 대규모 데이터셋에서 효율적인 검색 및 정보 추출에 유용 | |
| Question Answering Advisor | QuestionAnswerAdvisor | Vector Store를 사용하여 질의응답 기능 제공. 일반적인 RAG 패턴 구현 |
| RetrievalAugmentationAdvisor | org.springframework.ai.rag 패키지의 빌딩 블록을 사용하여 일반적인 RAG 플로우 구현. Modular RAG Architecture 기반 | |
| Reasoning Advisor | ReReadingAdvisor | LLM 추론을 위한 RE2(Re-Reading) 전략 구현. 입력 단계에서 이해력 향상. 논문 기반 |
| Content Safety Advisor | SafeGuardAdvisor | 모델이 유해하거나 부적절한 콘텐츠를 생성하지 않도록 방지하는 간단한 Advisor |
3.4. Streaming VS Non-streaming

- Non-streaming Advisor 는 요청, 응답을 단일객체로 한번에 처리합니다.
요청 → [Advisor 1] → [Advisor 2] → [Chat Model - 10초 대기]
↓
"AI는 인공지능입니다..."
↓
응답 ← [Advisor 1] ← [Advisor 2] ← 완성된 응답
- Streaming Advisor는 요청은 한번 처리하되 응답은 여러 번 호출될 수 있습니다.
- 응답 Chunk 만큼 수행될 수 있음
요청 → [Advisor 1] → [Advisor 2] → [Chat Model]
↓
"AI는" (0.1초)
↓
응답 조각1 ← [Advisor 1 AFTER] ← [Advisor 2 AFTER] ← "AI는"
↓
"인공지능을" (0.2초)
↓
응답 조각2 ← [Advisor 1 AFTER] ← [Advisor 2 AFTER] ← "인공지능을"
↓
"의미합니다" (0.3초)
↓
응답 조각3 ← [Advisor 1 AFTER] ← [Advisor 2 AFTER] ← "의미합니다"
Logging 을 예시로 보면 아래와 같이 동작할 수 있습니다.
// Non-streaming: 전체 응답 한 번에 로깅
.map(response -> {
log.info("완성된 응답: {}", response.getContent());
// 출력: "완성된 응답: AI는 인공지능을 의미합니다..."
return response;
});
// Streaming: 각 조각마다 로깅
.map(chunk -> {
log.info("응답 조각: {}", chunk.getContent());
// 출력1: "응답 조각: AI는"
// 출력2: "응답 조각: 인공지능을"
// 출력3: "응답 조각: 의미합니다"
return chunk;
});
3.5 Best Practices
- 특정 Advisor가 특정 작업에 집중하도록 구현한다 (모듈화)
- Advisor 간 상태 공유가 필요한 경우 adviseContext 를 활용한다.
- 유연성을 극대화하기 위해 Advisor 구현 시 Streaming, Non-streaming 방식 모두 구현하는 것이 좋다.
- 적절한 데이터 흐름을 보장하기 위해 Advisor 체인 순서를 잘 구성하는 것이 중요하다
'AI & LLM' 카테고리의 다른 글
| Vector DB - Qdrant 를 활용하여 벡터 데이터 다뤄보기 (0) | 2025.10.19 |
|---|---|
| [Spring AI 공식문서 읽기] 2. ChatClient API (0) | 2025.10.05 |
| Spring AI 를 활용한 MCP 서버 구현 및 Claude AI에 적용하기 (0) | 2025.10.03 |
| [Spring AI 공식문서 읽기] 1. Spring AI 핵심 개념과 컨셉 (1) | 2025.08.31 |
| 랭체인(Langchain) 기본 개념과 간단한 활용 예제 (1) | 2025.08.24 |