본문 바로가기
푸닥거리

Spring AI 임베딩과 RAG 구현

by ┌(  ̄∇ ̄)┘™ 2026. 1. 11.
728x90

 

 

 

 

Set-ExecutionPolicy RemoteSigned

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

https://docs.spring.io/spring-ai/reference/

 

Introduction :: Spring AI Reference

Support for all major Vector Database providers such as Apache Cassandra, Azure Cosmos DB, Azure Vector Search, Chroma, Elasticsearch, GemFire, MariaDB, Milvus, MongoDB Atlas, Neo4j, OpenSearch, Oracle, PostgreSQL/PGVector, Pinecone, Qdrant, Redis, SAP Han

docs.spring.io

 

 

 

 

 

 

 

 

코사인 유사도 기반 거리

 

데이터를 벡터화 시킴, API 이용

 

 

VectorStore 인터페이스

 

 

 

row 하나가 document 하나

 

Double score 유사도점수 ( != 거리가 아님, 0에 가까울수록 유사도 낮고, 1에 가까울수록 유사도 높음 )

 

 

 

 

public List<Document> searchDocument2(String question) {

List<Document> documents = vectorStore.similaritySearch(

SearchRequest.builder()

.query(question)

.topK(1)

.similarityThreshold(0.4)

.filterExpression("source == '헌법' && year >= 1987")

.build());

return documents;

}



public List<Document> searchDocument2(String question) {

 FilterExpressionBuilder feb = new FilterExpressionBuilder();



 List<Document> documents = vectorStore.similaritySearch(

 SearchRequest.builder()

 .query(question)

 .topK(1)

 .similarityThreshold(0.4)

 .filterExpression(feb

 .and(

 feb.eq("source", "헌법"),

 feb.gte("year", 1987))

 .build())

 .build());

 return documents;

}

 

 

 

 

 

 

 

 

 

 

 

 

http://localhost:50001/get-face-image

 

 

http://localhost:50001/get-face-vector

 

728x90

 

 

 

 

 

 

 

 

 

 

// 유사한 얼굴 찾기: <=>는 pgvector 코사인 거리(0~2)를 구하는 연산자임(0에 가까울수록 유사) / socre 와 반대

String sql = """

SELECT content, (embedding <=> ?::vector) AS similarity

FROM face_vector_store

ORDER BY embedding <=> ?::vector

LIMIT 3

""";

 

 

 

 

 

 

 

 

 

https://www.egovframe.go.kr/home/main.do

 

표준프레임워크 포털 eGovFrame

본문 내용 바로가기 대메뉴 바로가기 소개 구성 구성상세 버전별 구성 오픈소스 SW 현황 아키텍쳐 실행환경 개발환경 운영환경 관리환경 공통컴포넌트 모바일 라이선스 적용사례 추진성과 기술

www.egovframe.go.kr

 

 

 

 

 

검색 증강 생성   RAG, Retrieval Augmented Generation 

 

 

사전 학습 된  LLM 에 기업체 내부 정보를 사용하는 방법

 

1. 파인 튜닝 (  fine-tuning )

2. 도구호출방식 ( MCP, Model Context Protocol )

3. 검색 증강 생성 ( RAG, Retrieval Augmented Generation )

 

Advisor는 Spring AOP의 “실행 단위”이며,
Spring의 모든 AOP 기능은 Advisor를 중심으로 동작한다.

 

ETL 파이프라인 (Extract · Transform · Load)

ETL 파이프라인은 다양한 원천 데이터(Source)를 추출·정제·변환하여
분석·서비스에 적합한 저장소로 안정적으로 적재하는 데이터 처리 흐름이다.

 

 

DocumentTransformer는 RAG(Retrieval-Augmented Generation) 파이프라인에서 문서를 임베딩·검색에 적합한 형태로 변환하는 전처리 컴포넌트를 의미합니다. 주로 원본 문서의 구조·내용을 정규화, 분할, 메타데이터 보강 등의 작업을 수행합니다.

 

TokenTextSplitter는 RAG 파이프라인에서 텍스트를 “토큰 수 기준”으로 분할하는 TextSplitter 구현체입니다.

문자 수나 줄 단위가 아니라, LLM 토크나이저 기준 토큰 개수를 직접 고려한다는 점이 핵심입니다.

 

 

 

 

 

 

 

 

  // ##### 업로드된 파일로부터 텍스트를 추출하는 메소드 #####
  private List<Document> extractFromFile(MultipartFile attach) throws IOException {
    // 바이트 배열을 Resource로 생성
    Resource resource = new ByteArrayResource(attach.getBytes());

    List<Document> documents = null;
    if (attach.getContentType().equals("text/plain")) {
      // Text(.txt) 파일일 경우
      DocumentReader reader = new TextReader(resource);
      documents = reader.read();
    } else if (attach.getContentType().equals("application/pdf")) {
      // PDF(.pdf) 파일일 경우
      DocumentReader reader = new PagePdfDocumentReader(resource);
      documents = reader.read();
    } else if (attach.getContentType().contains("wordprocessingml")) {
      // Word(.doc, .docx) 파일일 경우
      DocumentReader reader = new TikaDocumentReader(resource);
      documents = reader.read();
    }

    return documents;
  }

 

 

 

 

 

 

  // ##### JSON의 ETL 과정을 처리하는 메소드 #####
  public String etlFromJson(String url) throws Exception {
    // URL로부터 Resource 얻기
    Resource resource = new UrlResource(url);

    // E: 추출하기
    JsonReader reader = new JsonReader(
        resource,
        
        new JsonMetadataGenerator() {
          @Override
          public Map<String, Object> generate(Map<String, Object> jsonMap) {
            return Map.of(
                "title", jsonMap.get("title"),
                "author", jsonMap.get("author"),
                "url", "http://localhost:8080/document/constitution(19880225).json");
          }
        },

        /*jsonMap -> Map.of(
            "title", jsonMap.get("title"),
            "author", jsonMap.get("author"),
            "url", "http://localhost:8080/document/constitution(19880225).json"
        ),*/

        "date", "content"

    );
    
    List<Document> documents = reader.read();
    log.info("추출된 Document 수: {} 개", documents.size());

    // T: 변환하기
    DocumentTransformer transformer = new TokenTextSplitter();
    List<Document> transformedDocuments = transformer.apply(documents);
    log.info("변환된 Document 수: {} 개", transformedDocuments.size());

    // L: 적재하기
    vectorStore.add(transformedDocuments);

    return "JSON에서 추출-변환-적재 완료 했습니다.";
  }

 

 

 

QuestionAnswerAdvisor는 Spring AI에서 RAG(Retrieval-Augmented Generation) 질의응답을 표준화된 방식으로 구성해 주는 Advisor 컴포넌트입니다. 사용자가 질문을 던졌을 때, 벡터 검색 결과를 LLM 프롬프트에 자동으로 주입하고, “근거 기반 답변” 구조를 강제하는 것이 핵심 역할입니다.

 

 

ChatModel vs ChatClient

추상화 레벨 Low-level High-level
실제 LLM 호출 내부적으로 사용
Advisor 지원
RAG 지원
Prompt Template 제한적 풍부
Memory
실무 권장 ❌ (직접 사용 X)

 

 

 

 

Given the context and provided history information and not prior knowledge,

reply to the user comment. If the answer is not in the context, inform

the user that you can't answer the question.

', metadata={messageType=USER}, messageType=USER}], modelOptions=OpenAiChatOptions: {"streamUsage":false,"model":"gpt-4o-mini","temperature":0.7}}, context={qa_retrieved_documents=[Document{id='d3154ab3-5370-4e14-8c47-fad99c77c803', text='⑤대통령의 선거에 관한 사항은 법률로 정한다.

 

제68조 ①대통령의 임기가 만료되는 때에는 임기만료 70일 내지 40일 전에 후임자를 선거한다.

②대통령이 궐위된 때 또는 대통령 당선자가 사망하거나 판결 기타의 사유로 그 자격을 상실한 때에는 60일 이내에

후임자를 선거한다.', media='null', metadata={chunk_index=4, parent_document_id=16f659e3-e126-41e5-abe6-38ab2f6bbae6, source=헌법법, page_number=7, distance=0.5497711, total_chunks=11}, score=0.4502289295196533}, Document{id='d8d7ded9-c962-4e75-8a22-5e0ecb7ccd6c', text='제70조 대통령의 임기는 5년으로 하며, 중임할 수 없다.

 

제71조 대통령이 궐위되거나 사고로 인하여 직무를 수행할 수 없을 때에는 국무총리, 법률이 정한 국무위원의 순서로

그 권한을 대행한다.', media='null', metadata={chunk_index=6, parent_document_id=16f659e3-e126-41e5-abe6-38ab2f6bbae6, source=헌법법, page_number=7, distance=0.6054909, total_chunks=11}, score=0.39450907707214355}, Document{id='a4597e3f-16f0-4b46-a484-986b8e71c1de', text='정 또는 폐지되었던 법률은 그 명령이 승인을 얻지 못한 때부터 당연히 효력을 회복한다.

⑤대통령은 제3항과 제4항의 사유를 지체없이 공포하여야 한다.

 

제77조 ①대통령은 전시ㆍ사변 또는 이에 준하는 국가비상사태에 있어서 병력으로써 군사상의 필요에 응하거나 공공', media='null', metadata={chunk_index=2, parent_document_id=0e164d30-eb45-40d7-8132-dfd1177d1c5c, source=헌법법, page_number=8, distance=0.61336184, total_chunks=11}, score=0.38663816452026367}]}]

 

 

 

 

청크 크기를 너무 줄이면 문장의 의미를 잃게 됨

 

 

    // 프롬프트를 LLM으로 전송하고 응답을 받는 코드
    String answer = this.chatClient.prompt()
        .system("프롬프트 컨텍스트에 없는 내용은 모른다고 답변을 하세요.")
        .user(question)
        .advisors(questionAnswerAdvisor)
        .call()
        .content();
    return answer;

 

 

 

 

RetrievalAugmentationAdvisor는 Spring AI에서

RAG(Retrieval-Augmented Generation)를 가장 일반화된 형태로 구현하는 핵심 Advisor입니다.

앞서 QuestionAnswerAdvisor보다 범용적·저수준이며, “검색 결과를 프롬프트 컨텍스트로 주입하는 역할”에 집중합니다.

 

    // 프롬프트를 LLM으로 전송하고 응답을 받는 코드
    String answer = this.chatClient.prompt()
        .user(question)
        .advisors(
          MessageChatMemoryAdvisor.builder(chatMemory).build(), // 프롬프트에 이전 대화 기록 추가, 이전 대화 이력(Context)을 프롬프트에 주입
          retrievalAugmentationAdvisor // 이전 대화기록을 완전한 대화 완성, VectorStore 검색 결과(Context)를 프롬프트에 주입
        )
        .advisors(advisorSpec -> advisorSpec.param(
            ChatMemory.CONVERSATION_ID, conversationId))
        .call()
        .content();
    return answer;
  }

 

 

 

ChatMemory.CONVERSATION_ID 는 Spring AI에서

대화 이력을 “하나의 세션”으로 묶기 위한 핵심 식별자입니다.

이 값이 없거나 잘못 관리되면, ChatMemory는 의도하지 않은 대화를 섞거나 아예 기억하지 못합니다.

 

ChatMemory.CONVERSATION_ID

ChatMemory가 “이 요청이 어떤 대화에 속하는지”를 판단하기 위한 세션 키

 

 

 

QueryTransformer

→ “사용자 질문을 검색에 적합한 형태로 변환”

 

DocumentRetriever

→ “변환된 질의를 기반으로 문서를 검색”

 

RAG 파이프라인에서의 위치

 

User Question

QueryTransformer ← 질의 재작성

DocumentRetriever ← 벡터/키워드 검색

Documents

Advisor (QA / RAG)

LLM

 

 

 

 

 

 

1. text-embedding-3-small 사용 시 관찰된 결과 

 

 
 

 

text-embedding-3-small을 사용하고, HNSW 인덱스를 비활성화한 상태에서 ETL은 정상적으로 완료되었으나

질의 결과에서는 의미적으로 중요한 조항을 검색하지 못하는 현상이 관찰되었다.

 

구체적으로는,

 

질문이 약관에 실제로 명시된 무면허운전·음주운전 면책 조항을 묻고 있음에도 불구하고

 

RAG 응답은

 

“해당 보상 제한 항목은 약관에 언급되어 있지 않다”

와 같은 부정확한 결론을 반환하였다.

 

이는 PDF 자체에 해당 조항이 존재함에도 불구하고,

임베딩 벡터 공간에서 *“무면허운전 / 음주운전 / 보상 제한”*이라는 조건적 의미 관계가 충분히 보존되지 못했음을 의미한다.

 

즉, text-embedding-3-small은

 

키워드 중심 검색에는 일정 수준 유효하지만

 

법률·약관 문서처럼 ‘원칙 + 예외 + 면책’ 구조가 강한 텍스트에서는

의미 구분력이 부족하여,

 

Retriever 단계에서 적절한 chunk를 상위 후보로 올리지 못하는 한계가 있었다.

 

 

2. text-embedding-3-large 사용 시 관찰된 결과 

 

 

 .

 

 

임베딩 모델을 text-embedding-3-large로 변경한 이후,

동일한 PDF와 동일한 질문에 대해 응답 품질이 명확히 개선되었다.

 

RAG 응답은 다음과 같은 특징을 보였다.

 

무면허운전, 음주운전 사고 시 보상 제한 항목을 조목조목 나열

 

“보상되지 않는다”는 결론이

 

운전자의 위법 행위

 

정상적인 운전 불가능 상태

 

범죄 목적 사용

과 같은 약관상의 면책 논리와 직접 연결되어 설명됨

 

실제 약관 문체와 유사한 서술 방식 유지

 

이는 text-embedding-3-large가

 

3,000차원 이상의 고차원 벡터를 통해

 

조건문, 예외 조항, 부정 표현(보상하지 않는다)을

 

의미적으로 더 정밀하게 분리·표현할 수 있기 때문으로 해석된다.

 

결과적으로, Retriever가 올바른 조항 단위 chunk를 상위에 랭크시키는 데 성공하면서

LLM이 근거 기반 답변을 생성할 수 있었다.

 

 

[결론]

 

자동차 보험 약관과 같이 조건·예외·면책 구조가 강한 문서를 대상으로 RAG를 수행한 결과,

text-embedding-3-small 모델은 기본적인 키워드 유사성 검색에는 활용 가능하나,

법률적 의미 관계를 정확히 반영하기에는 한계가 있음을 확인하였다.

 

반면 text-embedding-3-large 모델은 고차원 임베딩을 통해

약관 조항 간의 조건 논리와 부정 표현을 정밀하게 벡터화할 수 있었으며,

특히 무면허운전·음주운전과 같은 면책 질문에서 명확하고 근거 있는 답변을 생성하였다.

 

또한 RAG 성능은 단순히 임베딩 모델의 크기에 의존하지 않고,

조항 단위로 의미를 보존하는 트랜스포밍(잘게 분할) 전략과 결합될 때 극대화됨을 확인하였다.

따라서 자동차 보험 약관 RAG에서는

text-embedding-3-large + 의미 단위 분할이 가장 적합한 조합임을 결론으로 도출할 수 있다.

 

 

 

728x90

댓글