[Spring AI + Ollama + Rag 도입기] 자동화 창고 챗봇 서비스 구현 ④

양현지·2026년 5월 15일

연구

목록 보기
21/26

🤔 현재 문제점

  • LLM이 항상 Chroma 벡터 검색 결과만 Context로 받는다. "출고작업 몇건" 같은 DB 조회가 필요한 질문도 문서에서 찾으려 한다.
  • SQL 생성은 되는데 Chroma에서 엉뚱한 스키마를 가져와서 잘못된 SQL이 만들어지거나 실행 실패한다.

문서 기반 RAG는 나쁘지 않다.

> 문서 내용을 기반으로 적절한 응답을 생성한다.

그러나, DB 실시간 조회가 필요한 경우 바로 형편없어진다.

방향성을 다시 정립해보자.

Phase 0. 현재 : 기본 QA

  • 정적 RAG 데이터(매뉴얼, DB 설계서) 기반 기본적 QA e,g. 긴급 출고 방법은? 모니터링 화면에서 확인할 수 있는 RTV 정보는 무엇인가 ? 스태커크레인 수동 조작 절차는?
  • OracleDB 연동되었으나, LLM이 퀄리티 좋은 쿼리를 짜주지는 못함.,.. 모델 → ollama CPU 온리 모델도 가능은 함 (llama3.2:3b~8b)
      →  (인터넷, 무료 but 토큰 제한) groq / google(gemini) api key 

Phase I. 데모용 (단기) : QA+ DB조회

  • 정적 RAG 데이터 (설계서, 운영방안, 매뉴얼,,) + DB 기반 QA e,g. 현재 재고 파렛트 종류는? 이 창고는 몇 row-bay-level로 구성되었어? 02.26일자 출고공파렛트 현황 요약해줘. 모델 → llama3.1:8b (8gb)~ llama3.3:70b ↑ (폐쇄망ok, but GPU 없이 검증불가) → GPT-4o , Claude Sonnet 4.5 , Claude Haiku (폐쇄망x, 토큰 비쌈-검증불가) GPU → 최소 rtx 3090~ 4090

Phase II. 운영용 (*TBD) : 복합분석 + 의사결정

  • Phase I + 통신/제어 로그 등의 RAG 데이터 등 ChromaDB에 동적 적재 ⇒ 의사 결정 지원 및 최적화 e,g. 이상 징후(에러)가 빈번한 or 최근 사용량이 많은 스태커 호기는? 존별 처리량 불균형이 있나?

PhaseIII. 장기 목표 (TBD) : 자동화 지능화

  • WCS 개입 최소화 하여 물류 흐름 최적화 제안 및 적용 + 예측 유지보수(예지정비) e,g. 출고 우선수위 자동 조정 제안 모델 → 추후 fine-tuned 70b+ GPU → a10g or rtx 5090 or A100 or 온프레미스 gpu 서버

1. Groq vs Google(Gemini) API KEY

오픈 API로 RAG 파이프라인 먼저 맛보기 ( groq 이나, google ai studio api 키 발급)

  • groq VS google(Gemini) : 둘 다 API 키 발급 플랫폼

1. Groq (그록) API특징: Llama, Gemma 등 오픈소스 언어모델을 초고속(LPU 하드웨어 기반)으로 무료 제공하는 데 특화되어 있습니다.

속도가 압도적으로 빠릅니다.

현재 무료 등급(Free Tier)에서도 강력한 성능의 모델(예: Llama 3)을 제약 없이 빠르게 테스트할 수 있습니다.

2. Google Gemini (구글 제미나이) API특징: Google의 고성능 최신 AI 모델과 방대한 컨텍스트 창(긴 문서 처리 능력)을 제공합니다.

기본 제공되는 무료 플랜(Free Tier)이 있어 가볍게 테스트하거나 소규모 프로젝트를 개발하기 좋습니다.

무료 플랜 대비 더 높은 사용량과 기능을 원할 경우, Google Cloud 기반의 종량제(Pay-as-you-go)로 전환하여 유연하게 확장할 수 있습니다.

⚠️ 환경변수 영구 등록 후에는 반드시 새 PowerShell을 열어야 반영이 된다

# GROQ API 키
[System.Environment]::SetEnvironmentVariable("GROQ_API_KEY", "gsk_전체키", "User")
# Google AI API 키
[System.Environment]::SetEnvironmentVariable("GOOGLE_AI_API_KEY", Alz_전체키", "User")

2. RAG 프롬프트 가이드

개발을 하면서 RAG에 프롬프트 가이드를 왜 작성해줘야할까 의문이 들었다.

e,g, 가령 다음과 같이 가이드를해주고 있다.

private String classifyQuestion(String question, ChatClient chatClient) {
        String result = chatClient.prompt()
                .system("""
                        질문을 분석해서 필요한 데이터 소스를 판단하세요.
                        
                        DB: 현재 데이터 조회 (건수, 현황, 수량, 날짜별, 작업, 재고, 에러, 알람, 위치)
                        DOC: 사용법, 절차, 설명, 매뉴얼, 설계서 내용
                        BOTH: DB + 문서 둘 다 필요
                        LLM: 일반 지식으로 답변 가능 (기술 설명, 개념 등)
                        
                        DB, DOC, BOTH, LLM 중 하나만 출력하세요.
                        """)
                .user(question)
                .call()
                .content()
                .trim()
                .toUpperCase();

        if (result.contains("BOTH")) return "BOTH";
        if (result.contains("DB")) return "DB";
        if (result.contains("DOC")) return "DOC";
        return "LLM";
    }
 private String generateSql(String question, String schemaContext, ChatClient chatClient) {
        String result = chatClient.prompt()
                .system("""
                        당신은 Oracle DB 전문가입니다.
                        제공된 [스키마 정보]를 참고하여 질문에 맞는 Oracle SQL을 생성하세요.

                        규칙:
                        - SELECT 문만 생성
                        - SQL 코드만 출력 (설명, 마크다운 없이)
                        - FETCH FIRST 20 ROWS ONLY 로 결과 제한
                        - 스키마명 없이 테이블명만 사용
                        - TOP 문법 사용 금지 (Oracle 미지원)
                        - DB 조회가 필요 없는 질문이면 정확히 'NONE' 만 출력
                        """)
                .user("""
                        [스키마 정보]
                        %s

                        [질문]
                        %s

                        SQL (또는 NONE):
                        """.formatted(schemaContext, question))
                .call()
                .content();

        return result.replaceAll("```sql", "")
                    .replaceAll("```", "")
                    .trim();
    }
 private String generateAnswer(String question, String dbContext,
                                 String vectorContext, ChatClient chatClient) {
      StringBuilder context = new StringBuilder();
      if (!dbContext.isEmpty()) {
          context.append("[실시간 DB 데이터]\n").append(dbContext).append("\n\n");
      }
      if (!vectorContext.isEmpty()) {
          context.append("[문서 데이터]\n").append(vectorContext);
      }
      if (context.isEmpty()) {
          context.append("관련 데이터 없음");
      }

      return chatClient.prompt()
              .options(org.springframework.ai.chat.prompt.ChatOptions.builder()
                  .model(modelConfig.getCurrentModel())
                  .build())
              .system("""
                          당신은 자동화창고(WCS) 시스템 및 도메인의 전문가 어시스턴트입니다.
                          답변 우선순위:
                              1. [DB 데이터] 있으면 → DB 데이터 기반 답변
                              2. [문서 데이터] 있으면 → 문서 기반 답변  
                              3. 둘 다 없으면 → 보유한 지식으로 자유롭게 답변
                          
                          DB나 문서 데이터를 추측하거나 만들어내지 마세요.
                          답변은 한국어로 작성하세요.
                      """)
              .user("""
                      %s

                      [질문]
                      %s
                      """.formatted(context.toString(), question))
              .call()
              .content();
  }

💡 RAG 프롬프트 설계 핵심 가이드효과적인 RAG 시스템을 구축하기 위해 프롬프트 작성 시 반드시 고려해야 할 핵심 요소이다.

  1. 컨텍스트(Context) 고립 및 환각 방지
    LLM이 외부 문서보다 자신이 사전에 학습한 지식을 우선시하여 거짓 정보를 생성(할루시네이션)하는 것을 막아야 합니다.지시어 예시: "오직 제공된 내의 정보만 사용하여 답변하시오.", "외부 지식을 사용하지 마시오."

  2. 출처(Citation) 및 근거 명시
    답변의 신뢰성을 높이려면 AI가 어느 문서에서 정보를 가져왔는지 역추적할 수 있어야 합니다.지시어 예시: "답변의 각 문장 끝에 [문서 제목]을 인용 표기하시오."

  3. 모호한 질문에 대한 대응 지침
    검색된 문서에 질문과 일치하는 내용이 없을 때 LLM이 엉뚱한 말을 하지 않도록 대응 방식을 미리 지정합니다.지시어 예시: "문서에서 질문에 대한 답을 찾을 수 없는 경우, 문서를 유추하여 작성하지 말고 '제공된 문서에서 관련 정보를 찾을 수 없습니다'라고만 답하시오."

  4. 출력 형식(Format) 제어
    사용자가 원하는 형태로 정보를 가공하여 출력하도록 포맷을 제한합니다.지시어 예시: "결과는 반드시 3개의 글머리 기호로 요약하시오.", "표 형태로 정리하시오."

    결론은 고로 필요하다 !

💡 도커 내 ChromaDB의 매 실행(STOP-START)마다 RAG 데이터를 POST 해줘야할까?

  • Chroma 데이터 유지 여부는 볼륨 마운트에 달려있다.
    # 현재 설정 확인
    docker inspect chromadb --format "{{json .Mounts}}"
    PS C:\Project\wms-rag-service> docker inspect chromadb --format "{{json .Mounts}}"
    [{"Type":"bind","Source":"C:\\Project\\wms-rag-service\\chroma-data","Destination":"/chroma/chroma","Mode":"","RW":true,"Propagation":"rprivate"}]
    위와 같다면, Source 경로(C:\Project\wms-rag-service\chroma-data) 내 SQLite 파일로 저장되어있다.
    *# 파일 목록
    Get-ChildItem C:\Project\wms-rag-service\chroma-data -Recurse
    *# 문서 수 확인
    Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/v2/tenants/SpringAiTenant/databases/SpringAiDatabase/collections/82977eae-5e2e-47ae-bbdc-3735b506cf0a/count"
    이미 업로드 된 RAG 데이터 정보를 확인해볼 수 있다.

만약 업로드 후에도 경로 내 sqlite가 안생긴다면, chromadb 중단-삭제 후 아래와 같이 -v(볼륨마운트)옵션을 추가한 뒤 다시 도커를 실행해보자.

 PS C:\Project\wms-rag-service> docker run -d -p 8000:8000 -v ./chroma-data:/data chromadb/chroma
cd84452fdecff908c12a89718b4f8af333076e874958b1f614b5932bef1b6486

2-2. 보안은 어떻게 지켜질까?

창고 내 모든 사용자에게 챗봇 서비스가 제공된다고 치자.

사용자는 이와 같이 특정 USER의 PASSWORD를 질문해보았다.

위와 같이 암호화된 데이터지만, 결국 민감한 데이터의 원본 그 자체를 넘겨준 셈이다.

RAG 프롬프트로 가이드를 한다면 어느정도 보안을 보장할 수 있을까?

-> NO.

  • 프롬프트 가이드 -> 소프트 블로킹 (우회 가능)
  • 코드 레벨 차단 -> 하드 블로킹 (우회 불가)

0개의 댓글