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 단위로.
완료 기준:
KnowledgeDocument→KnowledgeChunk목록 변환- 각 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_result→KnowledgeSearchResult반환- 내부에서 QueryBuilder와 Retriever를 orchestration
- Retriever 구현체에 직접 의존하지 않음 (인터페이스 의존)
7단계. Chatbot / Agent 연결
검색된 문서를 LLM 프롬프트에 컨텍스트로 주입.
8단계. 텍스트 및 프롬프트 고도화
프롬프트 구조·문서 포맷·컨텍스트 사이즈 튜닝으로 답변 품질 향상.
현재 구현 방식
키워드 토큰 + 가중치 + 메모리 키워드 검색 (VectorDB 미사용).
전체 흐름
RAG 문서들
→ 키워드 추출
→ 점수 계산용 index 생성
→ 질문 토큰 비교
→ 점수 높은 문서 선택
→ 선택 문서를 LLM에 전달
처리 순서:
- 각 문서 내용을 키워드 중심으로 토큰화 + 토큰별 가중치 부여
- 질문 내용도 키워드 중심으로 토큰화
- 각 문서 score 계산
- 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)

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

이 문서는 섹션이 모두 max_chunk_chars(기본 1200)보다 짧으므로 ## 섹션 = chunk 1:1로 떨어진다.
모든 chunk가
docs_id=FGC-01출처를 공유한다. 그래서 검색 시 같은 문서의 여러 섹션이 상위에 올라오면 “검색 chunk는 여러 개, 문서는 1개”가 된다. (문서당 chunk 수는 기본 2개로 제한)
1-3. 긴 섹션일 때만 일어나는 2차 분할 (chunk size / overlap)

섹션 하나가 max_chunk_chars를 넘으면, 그 안에서 글자 수 기준으로 다시 쪼개고 경계를 overlap_chars만큼 겹친다.
다음 chunk 시작점 = 직전 끝 − overlap_chars (예: 300−150=150 → 450−150=300 …)
핵심 규칙(코드 기준):
- 다음 chunk 시작점 =
직전 끝 - overlap_chars→ 그래서 경계 문맥이 겹쳐 끊기지 않음. overlap_chars는 항상max_chunk_chars보다 작아야 함 (overlap >= chunk면ValueError).- 우리 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)
계산 절차
- 질의 토큰화:
analysis_result JSON을 QueryBuilder가 검색어 문자열로 만들고, 소문자화·특수문자 제거 후 토큰(단어) 집합으로 분해. - chunk별 매칭: 각 chunk를 6개 필드(본문/keyword/docs_id/pattern_id/title/section)로 인덱싱하고, 질의 토큰 1개가 어느 필드에 들어있는지에 따라 가산점을 더함. 한 토큰이 여러 필드에 걸리면 중복 가산.
- priority 보너스: score가 0보다 큰 chunk에 한해, 문서 frontmatter의
priority를min(priority,100)/10만큼 추가로 더함. - 정렬·선택: 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)/10 | score>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
SMALL top1 — FGC-01 / recommended_actions = 23.0 (오답, OOM 문서가 없어 대체됨)
| 토큰 | 가산점 | 매칭 위치 |
|---|---|---|
| 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):
- 점수순 정렬: 모든 chunk를 score 내림차순 정렬. 동점이면
chunk_id알파벳순으로 깨므로 동점이어도 무작위가 아님. - 후보 넉넉히 확보: 우선
top_k의 5배(최대 50)까지 후보를 뽑음 (candidate_top_k = max(top_k, min(top_k*5, 50))). - 문서당 상한 적용: 점수 높은 순으로 담되, 한 문서에서 최대 2개 chunk까지만(
max_chunks_per_document=2) 우선 선발. 상한을 넘으면 건너뜀. - 부족하면 backfill: 위 규칙으로 top_k를 못 채우면, 건너뒀던 chunk로 마저 채움.
왜 상한이 2인가:
2는KnowledgeService의 코드 기본값이며, 운영 진입점(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, docB | A 2개 + B 1개 | 3/2 ← 질의 A |
| 점수 붙는 문서가 사실상 1개뿐 | A 2개 담고 모자라 같은 문서로 backfill | 3/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개)
| chunk | overlap | top1 문서 | top1 score | 최저 score | 검색/문서수 |
|---|---|---|---|---|---|
| 100 | 0 | FGC-01 | 42.0 | 42.0 | 1/1 |
| 100 | 150 | N/A (overlap≥chunk) | - | - | - |
| 100 | 200 | N/A (overlap≥chunk) | - | - | - |
| 250 | 0 | AF-01 | 42.5 | 42.5 | 1/1 |
| 250 | 150 | AF-01 | 42.5 | 42.5 | 1/1 |
| 250 | 200 | AF-01 | 42.5 | 42.5 | 1/1 |
| 1200 | 0 | AF-01 | 42.5 | 42.5 | 1/1 |
| 1200 | 150 | AF-01 | 42.5 | 42.5 | 1/1 |
| 1200 | 200 | AF-01 | 42.5 | 42.5 | 1/1 |
| 100000 | 0 | AF-01 | 42.5 | 42.5 | 1/1 |
| 100000 | 150 | AF-01 | 42.5 | 42.5 | 1/1 |
| 100000 | 200 | AF-01 | 42.5 | 42.5 | 1/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개)
| chunk | overlap | top1 문서 | top1 score | 최저 score | 검색/문서수 |
|---|---|---|---|---|---|
| 100 | 0 | FGC-01 | 42.0 | 28.5 | 10/3 |
| 250 | 0 | AF-01 | 42.5 | 16.0 | 10/5 |
| 250 | 150 | AF-01 | 42.5 | 17.0 | 10/5 |
| 1200 | 0~200 | AF-01 | 42.5 | 17.0 | 10/5 |
| 100000 | 0~200 | AF-01 | 42.5 | 17.0 | 10/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_k | SMALL top1 | FULL top1 | 차이 |
|---|---|---|---|
| 1 | AF-01 42.5 | AF-01 42.5 | 없음 |
| 3 | AF-01 42.5 | AF-01 42.5 | 없음 |
| 10 | AF-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_k | SMALL top1 | FULL top1 |
|---|---|---|
| 1 | FGC-01 / 23.0 ❌ (오답) | OOM-01 / 47.5 ✅ (정답) |
| 3 | FGC-01 / 23.0 ❌ | OOM-01 / 47.5 ✅ |
| 10 | FGC-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 |
|---|---|---|---|
| 1 | RUNBOOK-AF-01 | response_template | 42.5 |
| 2 | RUNBOOK-FGC-01 | company_policy | 42.0 |
| 3 | RUNBOOK-FGC-01 | check_points | 42.0 |
질의 B — 확장 신규 영역
SMALL · 6개 — OOM 문서가 없어 Full GC/AF 런북으로 채움 (오답)
| 순위 | 문서 | 섹션 | score |
|---|---|---|---|
| 1 | RUNBOOK-FGC-01 | recommended_actions | 23.0 |
| 2 | RUNBOOK-FGC-01 | company_policy | 22.0 |
| 3 | RUNBOOK-AF-01 | company_policy | 18.5 |
FULL · 19개 — 정답 OOM/Overhead 런북 (정답)
| 순위 | 문서 | 섹션 | score |
|---|---|---|---|
| 1 | RUNBOOK-OOM-01 | company_policy | 47.5 |
| 2 | RUNBOOK-OOM-01 | detection_signals | 47.5 |
| 3 | RUNBOOK-OVH-01 | company_policy | 39.0 |
종합 결론
| 관점 | 질의 A (기존 영역) | 질의 B (신규 영역) |
|---|---|---|
| top_k 1~3 상위 결과 | SMALL = FULL 완전 동일 | SMALL ❌오답 / FULL ✅정답 (전 조합) |
| top1 score | 동일 (42.5) | 23.0 → 47.5 (2배+) |
| top_k=10 최저 score | 16~17 → 21.5 | 11.5 → 18.5 |
| 파라미터로 극복 가능? | — | 불가 (문서 추가만이 해결) |
핵심 인사이트:
- 문서 추가 효과는 “이미 다루던 주제”가 아니라 “새로 커버하는 주제”에서 결정적이다. 질의 A는 KB 크기와 무관하게 상위 결과가 같지만, 질의 B는 SMALL이면 전 조합에서 오답, FULL이면 전 조합에서 정답이다.
- 파라미터 튜닝(top_k·chunk·overlap)은 KB 커버리지 공백을 메울 수 없다. 질의 B의 모든 조합에서 SMALL은
FGC-01/23.0, FULL은OOM-01/47.5로 고정 — top_k를 10으로 키우고 chunk·overlap을 바꿔도 SMALL이 정답을 찾는 일은 없었다. - 키워드 RAG는 관련 문서가 없어도 빈손으로 두지 않고 가장 비슷한 문서를 낮은 score(23.0)로 1위에 올린다. 그래서 커버리지가 부족한 KB는 “그럴듯하지만 틀린” 문맥을 LLM에 줄 수 있다.
- 따라서 문서 개수 확장의 진짜 가치는 score 평균 상승이 아니라 커버리지 공백을 메워 오답을 방지하는 것이다. GC를 6→12개로 늘리며 OOM·Overhead·ZGC·Leak·To-space 패턴을 채운 것이 질의 B의 정답률을 0→100%로 바꿨다.
RAG 적용 결과

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