이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Spring AI에서 prompt, context, advisor, memory는 각각 어떤 책임을 가지는가?
  • Prompt engineering에서 context engineering, harness engineering으로 어떻게 확장되는가?
  • Advisor ordering과 memory conversation id를 왜 조심해야 하는가?
  • 대화 memory를 운영 서비스에서 안전하게 쓰려면 무엇이 필요한가?

flowchart LR
    subgraph INPUT["요청 입력"]
        USER["[User Input]<br/>질문, locale, intent<br/>검증된 원문"]
        STATE["[Server Context]<br/>tenant, role, quota<br/>권한 있는 상태"]
    end

    subgraph ASSEMBLY["Context Harness"]
        PROMPT["[Prompt Template]<br/>역할과 출력 규칙<br/>version 관리"]
        RETRIEVAL["[Retrieved Context]<br/>source, metadata<br/>권한 필터 적용"]
        MEMORY["[Chat Memory]<br/>대화 이력<br/>CONVERSATION_ID"]
        ADVISOR["[Advisor Chain]<br/>공통 보강 로직<br/>순서가 품질에 영향"]
    end

    subgraph MODEL["모델 호출"]
        REQUEST["[Prompt Request]<br/>instruction + context<br/>token budget"]
        RESPONSE["[Model Response]<br/>answer candidate<br/>후처리 대상"]
    end

    subgraph CONTROL["검증과 기록"]
        VALIDATE["[Validation]<br/>schema, source, policy<br/>실패 경로 분리"]
        TRACE["[Trace]<br/>promptVersion, memoryId<br/>quality 분석"]
    end

    USER --> PROMPT
    STATE --> RETRIEVAL
    STATE --> MEMORY
    PROMPT --> ADVISOR
    RETRIEVAL --> ADVISOR
    MEMORY --> ADVISOR
    ADVISOR --> REQUEST --> RESPONSE --> VALIDATE --> TRACE

    classDef input fill:#E3F2FD,stroke:#1565C0,color:#0D47A1;
    classDef assembly fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
    classDef model fill:#FFF3E0,stroke:#EF6C00,color:#E65100;
    classDef control fill:#F3E5F5,stroke:#6A1B9A,color:#4A148C;
    class USER,STATE input;
    class PROMPT,RETRIEVAL,MEMORY,ADVISOR assembly;
    class REQUEST,RESPONSE model;
    class VALIDATE,TRACE control;

개요

Spring AI의 prompt, advisor, memory는 문자열 조립 도구가 아니다. 모델 입력을 어떤 데이터와 정책으로 구성할지 정하는 실행 하네스다.

초기에는 prompt 문구를 잘 쓰는 것이 중요해 보인다. 하지만 실전에서는 어떤 context를 넣을지, 어떤 순서로 advisor를 적용할지, 어떤 memory를 다시 주입할지가 더 큰 영향을 준다.

원리

책임을 나누면 다음과 같다.

  • Prompt: 모델에게 전달하는 instruction과 user input의 형식
  • Context: 검색 결과, 권한, 사용자 상태, 대화 이력, 정책, source
  • Advisor: 모델 호출 전후에 반복 패턴을 삽입하는 interceptor
  • Memory: 다음 호출에 재사용할 대화 이력
  • Harness: 위 요소를 조립하고 검증하고 기록하는 실행 장치

Spring AI Advisors API는 LLM 요청을 가로채고 보강하는 구조를 제공한다. 따라서 advisor는 AOP와 비슷하게 보일 수 있지만, 모델 입력과 출력 품질에 직접 영향을 준다는 점을 기억해야 한다.

실습

prompt registry를 작게 만든다.

record PromptSpec(String version, String system, String userTemplate) {
}
 
@Component
class SupportPromptRegistry {
 
    PromptSpec supportAnswer() {
        return new PromptSpec(
                "support-answer-v2",
                """
                You answer customer support questions.
                Use only supplied context.
                If context is insufficient, mark escalation required.
                """,
                """
                question:
                {question}
 
                context:
                {context}
                """
        );
    }
}

context 조립은 모델 호출과 분리한다.

@Service
class SupportContextAssembler {
 
    private final KnowledgeSearch knowledgeSearch;
 
    SupportContextAssembler(KnowledgeSearch knowledgeSearch) {
        this.knowledgeSearch = knowledgeSearch;
    }
 
    String assemble(String tenantId, String userId, String question) {
        return knowledgeSearch.searchAuthorized(tenantId, userId, question).stream()
                .limit(5)
                .map(block -> "[source=%s]\n%s".formatted(block.sourceId(), block.text()))
                .collect(Collectors.joining("\n\n"));
    }
}

memory는 conversation id를 명시한다.

@Service
class ConversationAssistant {
 
    private final ChatClient chatClient;
    private final ConversationAccessPolicy accessPolicy;
 
    ConversationAssistant(ChatClient chatClient, ConversationAccessPolicy accessPolicy) {
        this.chatClient = chatClient;
        this.accessPolicy = accessPolicy;
    }
 
    String reply(String tenantId, String userId, String conversationId, String message) {
        accessPolicy.checkOwner(tenantId, userId, conversationId);
 
        return chatClient.prompt()
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
                .user(message)
                .call()
                .content();
    }
}

성공 경로는 prompt, context, memory가 각각 테스트 가능한 단위로 분리되는 것이다.

자주 나는 오류는 memory conversation id를 빼는 것이다. Spring AI memory advisors는 ChatMemory.CONVERSATION_ID가 필요하며 없으면 런타임 예외가 난다.

운영에서는 memory 저장소, TTL, 삭제, 민감정보 마스킹, 대화 요약 정책을 정한다.

실전 판단

advisor를 붙일 때는 순서를 문서화한다.

  • memory를 먼저 넣고 RAG를 검색할 것인가?
  • 현재 질문만으로 RAG를 검색할 것인가?
  • 권한 필터는 어느 계층에서 적용되는가?
  • logging advisor가 민감정보를 보기 전에 masking되는가?

순서가 바뀌면 답변 품질과 보안 결과가 달라질 수 있다.

실전 팁

  • prompt version을 code constant나 registry로 관리한다.
  • context assembler는 모델 없이 단위 테스트한다.
  • advisor chain version을 로그에 남긴다.
  • memory는 기본 OFF에서 시작하고 필요한 유스케이스에만 켠다.
  • memory에 tool 결과를 저장할지 별도 정책을 둔다.

위험 신호!

  • prompt가 여러 파일과 Controller에 흩어져 있다.
  • context를 하나의 거대한 문자열로만 관리한다.
  • advisor 순서를 설명할 수 없다.
  • memory에 민감정보가 저장되는데 TTL과 삭제 정책이 없다.
  • 대화 이력이 다른 사용자나 테넌트와 섞일 가능성이 있다.

확인 질문

확인 질문

  • prompt와 context의 차이는 무엇인가?
    • prompt는 전달 형식이고 context는 그 안에 넣을 데이터와 정책이다.
  • advisor를 harness로 보는 이유는 무엇인가?
    • 모델 호출 전후에 memory, retrieval, logging, safety 같은 실행 정책을 삽입하기 때문이다.
  • memory 사용 시 ChatMemory.CONVERSATION_ID가 중요한 이유는 무엇인가?
    • 대화 이력을 올바른 conversation에 묶고 누락 시 런타임 실패를 피하기 위해서다.
  • memory 운영에서 필요한 정책은 무엇인가?
    • TTL, 삭제, 민감정보 처리, 사용자/테넌트 격리, 요약, 오염 제거 정책이다.

학습 연결

참고 문서