RAG 및 로그분석기 개발 (통합·품질 고도화)

목표

  • 로그 분석기가 만든 분석 결과(JSON) 를 기반으로 관련 장애 대응 문서를 검색하는 RAG 연결
  • Chatbot/Agent가 답변할 때 검색된 문서를 참고 문맥(context) 으로 활용
  • docs/knowledge-base 운영 가이드를 기반으로 답변 품질·재현성 향상

핵심: AI가 로그를 직접 분석하는 것이 아니라, 분석기 결과 + RAG 검색 문서를 결합한 컨텍스트로 LLM이 설명을 생성한다.

요약

구분내용
목표분석 결과 기반 RAG 검색 + Chatbot 컨텍스트 주입
현재 방식키워드 토큰 + 가중치 + 인메모리 검색 (VectorDB 미사용)
핵심 8단계DocumentLoader → Splitter → Retriever I/F → InMemoryRetriever → QueryBuilder → KnowledgeService → Chatbot 연결 → 프롬프트 고도화
산출물RAG 구현 + 문서 10개+ + 품질 비교(프롬프트/chunk/overlap/top-k) + 추가 개선 3건+
확장 후보DB 저장, 다중 파일 통합 분석, VectorDB, Redis 상태

RAG 연결

RAG란

Retrieval-Augmented Generation: 외부 지식을 검색한 뒤 그 정보를 바탕으로 답변을 생성하는 방식. LLM의 hallucination을 줄이고 더 개별화된 답변을 만들 수 있음.

  • LLM 자체 지식 = 파라메트릭 메모리
  • RAG로부터 얻는 지식 = 외부 / 논파라메트릭 메모리
  • 검색된 문서가 프롬프트에 포함되어 LLM에 전달됨 (쿼리 + 검색문서 + 추가정보)

RAG의 3 파트

파트역할
인덱싱원시 데이터를 텍스트로 변환해 벡터 DB에 저장
검색사용자 쿼리를 동일 인코더로 벡터화 후 유사 문서 추출
생성검색된 청크 + 쿼리를 조합해 LLM 프롬프트 구성

구현 목적

로그를 직접 AI가 분석하게 하는 것이 아니라, 기존 로그 분석기가 만든 분석 결과를 기반으로 관련 장애 대응 문서를 검색하고, Chatbot/Agent가 답변할 때 참고 문맥으로 활용한다.

구현 순서 (8단계)

1단계. DocumentLoader & frontmatter 파싱

docs/knowledge-base/**/*.md 문서를 KnowledgeDocument 객체로 변환.

완료 기준:

  • MD 파일을 읽을 수 있다
  • frontmatter를 파싱할 수 있다
  • pattern_id, log_type, severity, keywords 검증
  • 본문 content를 분리해서 저장

2단계. Text Splitter / Chunking

문서 하나를 검색 가능한 작은 단위(KnowledgeChunk)로 분할. 각 섹션(##)을 chunk 단위로.

완료 기준:

  • KnowledgeDocumentKnowledgeChunk 목록 변환
  • 각 chunk가 pattern_id, log_type, title, section, content 유지
  • 긴 문서는 max_chunk_chars 기준으로 분할

3단계. Retriever 인터페이스 정의

검색기 공통 인터페이스 정의. 추후 InMemoryKeywordRetriever 외 다른 방식(예: pgvector)을 도입할 수 있도록.

완료 기준:

  • retrieve(query, top_k=3) 인터페이스 정의
  • RetrievalResult 모델 생성
  • StaticRetriever 같은 테스트용 구현체 생성

4단계. InMemoryKeywordRetriever 구현

외부 검색 엔진·embedding·pgvector 없이 순수 문자열 매칭 으로 검색.

완료 기준:

  • query를 normalize / tokenize
  • chunk의 content, title, pattern_id, keywords와 비교
  • score 계산
  • score 순으로 Top-K 반환

5단계. QueryBuilder 구현

분석 결과(JSON)는 Retriever가 바로 쓸 수 없음. 검색어 문자열로 변환 하는 단계.

입력 예시:

{
  "log_type": "GC",
  "detected_patterns": ["FULL_GC", "LONG_PAUSE"],
  "signals": [
    { "code": "GC_LONG_PAUSE", "message": "pause time 증가" }
  ]
}

출력 예시:

gc full gc long pause gc long pause pause time 증가

완료 기준:

  • log_type, detected_patterns, signals.code, signals.message, summary 추출
  • 검색 가능한 query 문자열 생성

6단계. KnowledgeService 통합

RAG 구성요소를 묶는 서비스 계층. 분석 결과를 입력받아 QueryBuilder → Retriever → 검색 결과 반환.

KnowledgeService
├─ DocumentLoader
├─ TextSplitter
├─ QueryBuilder
└─ Retriever

완료 기준:

  • analysis_resultKnowledgeSearchResult 반환
  • 내부에서 QueryBuilder와 Retriever를 orchestration
  • Retriever 구현체에 직접 의존하지 않음 (인터페이스 의존)

7단계. Chatbot / Agent 연결

검색된 문서를 LLM 프롬프트에 컨텍스트로 주입.

8단계. 텍스트 및 프롬프트 고도화

프롬프트 구조·문서 포맷·컨텍스트 사이즈 튜닝으로 답변 품질 향상.

현재 구현 방식

키워드 토큰 + 가중치 + 메모리 키워드 검색 (VectorDB 미사용).

전체 흐름

RAG 문서들
→ 키워드 추출
→ 점수 계산용 index 생성
→ 질문 토큰 비교
→ 점수 높은 문서 선택
→ 선택 문서를 LLM에 전달

처리 순서:

  1. 각 문서 내용을 키워드 중심으로 토큰화 + 토큰별 가중치 부여
  2. 질문 내용도 키워드 중심으로 토큰화
  3. 각 문서 score 계산
  4. Top-K 추출 후 LLM에 전달

메모리 키워드 검색 방식

POST /api/v1/rag/build-response-input 호출마다 create_chatbot_service()가 knowledge base를 다시 로드. → VectorDB와 달리 별도 DB / 인덱스를 저장하지 않음.

# knowledge_service.py
loader = DocumentLoader(base_dir)
 
# document_loader.py
def load_all(self) -> list[KnowledgeDocument]:
    if not self._base_dir.is_dir():
        raise KnowledgeDocumentLoadError(...)
    paths = sorted(self._base_dir.glob("**/*.md"))
 
# rag_prompt.py
@router.post("/build-response-input", response_model=RagBuildResponseInputResponse)
def build_response_input(body: RagBuildResponseInputRequest):
    """분석 결과와 enable_rag 플래그로 LLM용 prompt_input을 생성합니다."""
    service = create_chatbot_service(enable_rag=body.enable_rag, top_k=body.top_k)

Chunk 생성 방식

1-1. 문서 1개가 chunk가 되기까지 (Sequence)

Untitled-4.png

1-2. 실제 예시 — full-gc-long-pause.md (GC-RUNBOOK-FGC-01)

image-2026-6-2_14-19-16_masked.png

이 문서는 섹션이 모두 max_chunk_chars(기본 1200)보다 짧으므로 ## 섹션 = chunk 1:1로 떨어진다.

모든 chunk가 docs_id=FGC-01 출처를 공유한다. 그래서 검색 시 같은 문서의 여러 섹션이 상위에 올라오면 “검색 chunk는 여러 개, 문서는 1개”가 된다. (문서당 chunk 수는 기본 2개로 제한)

1-3. 긴 섹션일 때만 일어나는 2차 분할 (chunk size / overlap)

image-2026-6-2_14-17-51.png

섹션 하나가 max_chunk_chars를 넘으면, 그 안에서 글자 수 기준으로 다시 쪼개고 경계를 overlap_chars만큼 겹친다.

다음 chunk 시작점 = 직전 끝 − overlap_chars (예: 300−150=150 → 450−150=300 …)

핵심 규칙(코드 기준):

  • 다음 chunk 시작점 = 직전 끝 - overlap_chars → 그래서 경계 문맥이 겹쳐 끊기지 않음.
  • overlap_chars는 항상 max_chunk_chars보다 작아야 함 (overlap >= chunkValueError).
  • 우리 GC 문서는 섹션 최대 423자라 기본값(1200)에서는 2차 분할이 일어나지 않음 → overlap 효과도 기본 설정에선 보이지 않음.

1-4. 한 줄 요약

MD 파일
  → frontmatter 파싱(출처·메타 확보)
  → ## (H2) 섹션 단위로 1차 분할        ← 의미 단위 경계
  → (섹션이 max_chunk_chars 초과 시) 글자수 2차 분할 + overlap
  → 각 chunk에 출처(docs_id/pattern_id/title/section) 복사
  → 검색 색인
  • 1순위 경계는 ## 섹션. chunk가 company_policy, symptoms처럼 섹션 단위로 떨어지는 이유.
  • chunk size·overlap은 “섹션이 길 때만” 작동하는 2차 안전장치.
  • 쪼개도 각 chunk는 출처 metadata를 그대로 들고 있어, 검색 후 “어느 문서/섹션에서 나왔는지” 추적 가능.

score 계산 방식

이 RAG는 VectorDB(임베딩) 없이 키워드 토큰 + 가중치 합산 방식으로 score를 계산한다. (services/rag/keyword_retriever.py)

계산 절차

  1. 질의 토큰화: analysis_result JSON을 QueryBuilder가 검색어 문자열로 만들고, 소문자화·특수문자 제거 후 토큰(단어) 집합으로 분해.
  2. chunk별 매칭: 각 chunk를 6개 필드(본문/keyword/docs_id/pattern_id/title/section)로 인덱싱하고, 질의 토큰 1개가 어느 필드에 들어있는지에 따라 가산점을 더함. 한 토큰이 여러 필드에 걸리면 중복 가산.
  3. priority 보너스: score가 0보다 큰 chunk에 한해, 문서 frontmatter의 prioritymin(priority,100)/10만큼 추가로 더함.
  4. 정렬·선택: score 내림차순 정렬 후 top_k 선택(같은 문서 chunk 수는 기본 2개로 제한).

가중치 표 (질의 토큰 1개당)

매칭 위치가산점의미
본문(searchable 전체)+1.0문서 어딘가에 그 단어가 있음
keywords (frontmatter)+2.0작성자가 강조한 핵심어
docs_id+4.0사내 문서 식별자와 직접 연관
pattern_id+3.0오류 패턴 ID와 직접 연관
title+1.5문서 제목에 등장
section (## 섹션명)+1.0해당 섹션명에 등장
priority 보너스+min(priority,100)/10score>0인 문서에 1회 가산 (사내 우선문서 부양)

본문/keyword/docs_id/pattern_id/title/section은 각 토큰마다 독립적으로 중복 가산된다. 예: 토큰 oom이 본문·keyword·docs_id·pattern_id·title에 모두 있으면 1.0+2.0+4.0+3.0+1.5 = 11.5점.

실제 분해 — 질의 B의 top1 score는 왜 47.5 vs 23.0인가

질의 B 토큰: dump, exceeded, full, gc, heap, java, limit, object, oom, outofmemoryerror, overhead, retained, space, 확인 …

FULL top1 — OOM-01 / company_policy = 47.5 (정답 문서)
토큰가산점매칭 위치
dump+3.0본문+1.0 · keyword+2.0
gc+5.0본문+1.0 · docs_id+4.0
heap+7.5본문+1.0 · keyword+2.0 · pattern_id+3.0 · title+1.5
java+4.5본문+1.0 · keyword+2.0 · title+1.5
object+1.0본문+1.0
oom+11.5본문+1.0 · keyword+2.0 · docs_id+4.0 · pattern_id+3.0 · title+1.5
outofmemoryerror+3.0본문+1.0 · keyword+2.0
retained+3.0본문+1.0 · keyword+2.0
space+3.0본문+1.0 · keyword+2.0
확인+1.0본문+1.0

→ 토큰 합계 42.5 + priority 보너스 5.0 (priority 50/10) = 47.5

토큰가산점매칭 위치
full+4.5본문+1.0 · keyword+2.0 · title+1.5
gc+11.5본문+1.0 · keyword+2.0 · docs_id+4.0 · pattern_id+3.0 · title+1.5
heap+1.0본문+1.0
object+1.0본문+1.0
retained+1.0본문+1.0

→ 토큰 합계 19.0 + priority 보너스 4.0 (priority 40/10) = 23.0

무엇이 점수 차이를 만들었나
  • FULL의 정답 문서는 핵심어 oom(+11.5), heap(+7.5)이 pattern_id·docs_id·keyword·title까지 다중 매칭되어 고득점.
  • SMALL의 대체 문서는 정작 oom·outofmemoryerror·overhead·space 같은 질의 핵심어와 매칭되지 않고, 범용 토큰 gc(+11.5) 하나에 점수가 쏠려 23.0에 그침.
  • 즉 score는 “질의의 결정적 키워드가 그 문서의 식별 필드(pattern_id/docs_id/keywords/title)에 얼마나 정확히 박혀 있는가” 를 반영한다. 그래서 정답 문서가 KB에 있으면 점수가 2배 이상으로 벌어진다.

테스트 결과

문서 개수별 RAG 검색 품질 비교 (소규모 KB vs 전체 KB)

목적

Step7 산출물 요건 2번(docs/knowledge-base에 문서 10개 이상 추가 후, 문서 개수에 따라 답변이 어떻게 달라지는지 보여주기)을 충족하기 위해, GC 문서를 늘리기 전(6개)과 늘린 후(전체 19개, GC 12개) 의 검색 품질을 비교한다.

KB크기(SMALL/FULL) × top_k(1,3,10) × chunk(100,250,1200,100000) × overlap(0,150,200) × 질의(A,B)

방법

  • 대상: 실제 services/rag 파이프라인, Docker 이미지, LLM 미호출 검색 단계
  • SMALL KB: 확장 전 GC 6개 (AF-01, FGC-01, AF-02, PF-01, META-01, G1H-01)
  • FULL KB: 전체 19개 (GC 12개 + tomcat 3 + apache 2 + thread 2)
  • 두 질의 내용
    • 질의 A — Allocation Failure + Full GC + Long Pause (확장 전에도 커버되던 영역)
    • 질의 B — Heap OOM + GC Overhead (확장으로 새로 추가된 영역: OOM-01, OVH-01)
  • 표 컬럼: top1 문서 / top1 score / 최저 score / 검색수·문서수
  • 제약: overlap >= chunk 조합은 코드가 막으므로 N/A (chunk=100 + overlap≥100)
  • 스크립트: rag_kb_size_grid.py (레포 루트)
“검색수 / 문서수”의 의미 (chunk vs 문서)

이 표에서 검색/문서수가 예를 들어 3/2로 적힌 것은 “검색된 chunk 3개, 그 chunk들이 속한 서로 다른 문서 2개” 라는 뜻이다.

핵심은 top_k가 문서 개수가 아니라 chunk(조각) 개수를 센다는 점이다.

  • DocumentLoader가 문서를 읽고, MarkdownTextSplitter가 각 문서를 ## 섹션 단위로 여러 chunk로 쪼갠다. 이때 각 chunk는 “내가 어느 문서에서 나왔다”는 출처 정보(document_path, docs_id, pattern_id)를 그대로 복사해서 갖는다.
  • 검색기는 이 chunk들을 점수 매겨 정렬한 뒤 상위 top_k개를 고른다. 즉 top_k=3이면 chunk 3개를 가져온다.
  • 이 3개가 모두 다른 문서일 수도 있고, 같은 문서의 다른 섹션 2개 + 다른 문서 1개일 수도 있다. 후자가 바로 3/2이다.

예 — 질의 A, top_k=3 (대표 케이스):

1. AF-01 / response_template (42.5)  ← 문서 AF-01
2. FGC-01 / company_policy (42.0)     ← 문서 FGC-01
3. FGC-01 / check_points (42.0)       ← 문서 FGC-01 (같은 문서의 다른 섹션)
→ 검색 chunk 3개, 그러나 distinct 문서는 AF-01, FGC-01 2개 = "3/2"
이건 무작위가 아니라 “고정 규칙”의 결과다

문서수가 1개일지 2개일지 3개일지는 점수 분포 + 두 가지 고정 규칙으로 결정된다. 같은 입력이면 항상 같은 결과(재현 100%)이다.

검색 순서 (keyword_retriever.py, knowledge_service.py):

  1. 점수순 정렬: 모든 chunk를 score 내림차순 정렬. 동점이면 chunk_id 알파벳순으로 깨므로 동점이어도 무작위가 아님.
  2. 후보 넉넉히 확보: 우선 top_k의 5배(최대 50)까지 후보를 뽑음 (candidate_top_k = max(top_k, min(top_k*5, 50))).
  3. 문서당 상한 적용: 점수 높은 순으로 담되, 한 문서에서 최대 2개 chunk까지만(max_chunks_per_document=2) 우선 선발. 상한을 넘으면 건너뜀.
  4. 부족하면 backfill: 위 규칙으로 top_k를 못 채우면, 건너뒀던 chunk로 마저 채움.

왜 상한이 2인가: 2KnowledgeService의 코드 기본값이며, 운영 진입점(from_knowledge_base())이 이 값을 그대로 사용한다. 이론적 최적값이 아니라, Step7 3차 개선(rag_3차_검색품질_개선.md)에서 “한 문서가 top_k를 독식해 같은 문서 섹션만 채워지는 현상”을 막되, 그 문서의 핵심 섹션 2개 정도는 함께 보여주려는 절충값이다. 1로 바꾸면 무조건 서로 다른 문서에서만 뽑고(다양성↑), 3 이상으로 풀면 한 문서가 top_k를 독식할 수 있다.

그래서 1/2/3이 갈리는 규칙

top_k=3 기준, 상위 점수 chunk들이 몇 개 문서에 퍼져 있느냐 + 상한 2가 결정한다:

상위 chunk 출처 분포상한(2) 적용결과
docA, docB, docC (다 다름)그대로3/3
docA, docA, docBA 2개 + B 1개3/2 ← 질의 A
점수 붙는 문서가 사실상 1개뿐A 2개 담고 모자라 같은 문서로 backfill3/1 ← 질의 B SMALL
  • 질의 A 3/2: AF-01이 1위, FGC-01이 2·3위(같은 문서 두 섹션) → A 1개 + FGC 2개.
  • 질의 B SMALL 3/1: OOM 질의에 점수가 붙는 문서가 FGC-01 거의 하나뿐이라, 다른 문서로 채우지 못하고 같은 문서 조각으로 backfill됨.
docker run --rm \
  -v "$(pwd)/rag_kb_size_grid.py:/app/rag_kb_size_grid.py:ro" \
  -w /app log-analysis:latest \
  python rag_kb_size_grid.py

질의 A — Allocation Failure + Full GC + Long Pause (기존 커버 영역)

A · top_k = 1

SMALL KB (6개)

chunkoverlaptop1 문서top1 score최저 score검색/문서수
1000FGC-0142.042.01/1
100150N/A (overlap≥chunk)---
100200N/A (overlap≥chunk)---
2500AF-0142.542.51/1
250150AF-0142.542.51/1
250200AF-0142.542.51/1
12000AF-0142.542.51/1
1200150AF-0142.542.51/1
1200200AF-0142.542.51/1
1000000AF-0142.542.51/1
100000150AF-0142.542.51/1
100000200AF-0142.542.51/1

FULL KB (19개) — 모든 조합 SMALL과 동일 (top1 AF-01 42.5, FGC-01 42.0)

top_k=1에서는 SMALL·FULL이 모든 chunk·overlap 조합에서 동일. 정답 문서가 SMALL에도 있어 차이 없음.

A · top_k = 3

SMALL KB (6개) — top1 AF-01 42.5 (chunk 250100000), FGC-01 42.0 (chunk 100), 검색/문서수 3/2, 최저 score 41.042.0 FULL KB (19개) — SMALL과 전 조합 동일

overlap=0→150에서 최저 score 41.5→42.0 상승은 두 KB 공통 (overlap 효과)

A · top_k = 10

SMALL KB (6개)

chunkoverlaptop1 문서top1 score최저 score검색/문서수
1000FGC-0142.028.510/3
2500AF-0142.516.010/5
250150AF-0142.517.010/5
12000~200AF-0142.517.010/5
1000000~200AF-0142.517.010/5

FULL KB (19개) — top1 동일(AF-01 42.5), chunk 100/overlap 0은 최저 28.5(10/3), 그 외는 최저 21.5(10/5)

top_k=10에서만 차이: SMALL은 후보가 적어 최저 score 16~17, FULL은 21.5. top1은 둘 다 동일(AF-01 42.5).

질의 A 요약

top_kSMALL top1FULL top1차이
1AF-01 42.5AF-01 42.5없음
3AF-01 42.5AF-01 42.5없음
10AF-01 42.5 (최저 16~17)AF-01 42.5 (최저 21.5)하위권 최저 score만

이미 커버되던 영역은 문서를 늘려도 상위 결과가 같다. 차이는 top_k=10의 꼬리 부분뿐.

질의 B — Heap OOM + GC Overhead (확장 신규 영역)

B · top_k = 1

SMALL KB (6개) — OOM 문서 없음: 모든 조합에서 top1 = FGC-01 (23.0) ❌ 오답, 1/1 FULL KB (19개) — OOM 런북 보유: 모든 조합에서 top1 = OOM-01 (47.5) ✅ 정답, 1/1

B · top_k = 3

SMALL KB (6개) — top1 FGC-01 23.0 ❌, 최저 18.521.0, 검색/문서수 3/1(chunk 100) 또는 3/2 FULL KB (19개) — top1 OOM-01 47.5 ✅, 최저 39.046.5, 3/1(chunk 100) 또는 3/2

B · top_k = 10

SMALL KB (6개) — top1 FGC-01 23.0 ❌, 최저 11.515.5, 10/310/5 FULL KB (19개) — top1 OOM-01 47.5 ✅, 최저 18.522.0, 10/310/5

질의 B 요약 — 결정적 차이

top_kSMALL top1FULL top1
1FGC-01 / 23.0 ❌ (오답)OOM-01 / 47.5 ✅ (정답)
3FGC-01 / 23.0 ❌OOM-01 / 47.5 ✅
10FGC-01 / 23.0 ❌OOM-01 / 47.5 ✅

모든 36개 조합에서 SMALL은 OOM 질문에 엉뚱한 Full GC 런북(FGC-01)을 1위로, FULL은 정답 OOM 런북(OOM-01)을 1위로. top_k·chunk·overlap을 어떻게 바꿔도 결과가 뒤집히지 않음.

대표 케이스 상세 (top_k=3, chunk=1200, overlap=150)

질의 A — 기존 커버 영역

SMALL · 6개 (FULL · 19개와 동일)

순위문서섹션score
1RUNBOOK-AF-01response_template42.5
2RUNBOOK-FGC-01company_policy42.0
3RUNBOOK-FGC-01check_points42.0

질의 B — 확장 신규 영역

SMALL · 6개 — OOM 문서가 없어 Full GC/AF 런북으로 채움 (오답)

순위문서섹션score
1RUNBOOK-FGC-01recommended_actions23.0
2RUNBOOK-FGC-01company_policy22.0
3RUNBOOK-AF-01company_policy18.5

FULL · 19개 — 정답 OOM/Overhead 런북 (정답)

순위문서섹션score
1RUNBOOK-OOM-01company_policy47.5
2RUNBOOK-OOM-01detection_signals47.5
3RUNBOOK-OVH-01company_policy39.0

종합 결론

관점질의 A (기존 영역)질의 B (신규 영역)
top_k 1~3 상위 결과SMALL = FULL 완전 동일SMALL ❌오답 / FULL ✅정답 (전 조합)
top1 score동일 (42.5)23.0 → 47.5 (2배+)
top_k=10 최저 score16~17 → 21.511.5 → 18.5
파라미터로 극복 가능?불가 (문서 추가만이 해결)

핵심 인사이트:

  1. 문서 추가 효과는 “이미 다루던 주제”가 아니라 “새로 커버하는 주제”에서 결정적이다. 질의 A는 KB 크기와 무관하게 상위 결과가 같지만, 질의 B는 SMALL이면 전 조합에서 오답, FULL이면 전 조합에서 정답이다.
  2. 파라미터 튜닝(top_k·chunk·overlap)은 KB 커버리지 공백을 메울 수 없다. 질의 B의 모든 조합에서 SMALL은 FGC-01/23.0, FULL은 OOM-01/47.5로 고정 — top_k를 10으로 키우고 chunk·overlap을 바꿔도 SMALL이 정답을 찾는 일은 없었다.
  3. 키워드 RAG는 관련 문서가 없어도 빈손으로 두지 않고 가장 비슷한 문서를 낮은 score(23.0)로 1위에 올린다. 그래서 커버리지가 부족한 KB는 “그럴듯하지만 틀린” 문맥을 LLM에 줄 수 있다.
  4. 따라서 문서 개수 확장의 진짜 가치는 score 평균 상승이 아니라 커버리지 공백을 메워 오답을 방지하는 것이다. GC를 6→12개로 늘리며 OOM·Overhead·ZGC·Leak·To-space 패턴을 채운 것이 질의 B의 정답률을 0→100%로 바꿨다.

RAG 적용 결과

image-2026-6-2_15-48-15.png image-2026-6-2_15-52-16_masked.png

부록. 재현

rag_kb_size_grid.py(레포 루트)가 SMALL(GC 6개) vs FULL(19개)을 전체 조합으로, KB별 분리 표로 출력한다. SMALL 구성은 스크립트의 SMALL_KB_PATHS로 조정할 수 있다.