[개발 일기] / GraphRAG | History Baseline Guard.md

GraphRAG | History Baseline Guard

조회

2026년 4월 25일 | 개발 일기


quality_history.json 하나를 며칠 단위로 붙여 읽기 시작하니, 내가 먼저 고쳐야 했던 건 status가 아니라 baseline이었다. 표면만 보면 PASS, WARN, HARD-FAIL 세 칸이 날짜 순으로 잘 쌓이고 있었는데, 가만히 생각해 보면 그 줄들이 정말 같은 시험지 위에 서 있는지부터 확인해야 했다. query 파일 안의 질문 구성이 달라지거나, quality gate 임계값이 조금만 바뀌어도 숫자는 이어 보이지만 의미는 더 이상 같은 축이 아니다. 그 상태에서 history_transition이나 top1_hitΔ를 읽으면, 정작 내가 보고 싶은 변화보다 시험지 교체 효과를 먼저 읽게 된다.

그래서 이번에는 성능을 더 올리는 쪽보다 history가 거짓말하지 않게 만드는 guard를 먼저 붙였다. 최근 GraphRAG 쪽 흐름이 query quality metrics, quality gate, staged gate, quality history 순으로 쌓여 왔는데, 그 다음 칸에는 자연스럽게 "이 run을 이전 run과 같은 기준선으로 비교해도 되는가"라는 질문이 남아 있었다. 나는 이런 작업이 초반 프로젝트에서는 꽤 중요하다고 본다. 점수가 얼마나 올라갔는지보다, 비교 가능한 실험만 같은 줄에 올려두는 일이 먼저 잡혀 있어야 다음 판단이 덜 흔들린다.

1. flat line보다 먼저 봐야 했던 것

이전까지 history entry에는 dataset, top_k, query_count, quality_gate_path 정도가 들어 있었다. 얼핏 보면 충분해 보인다. 같은 샘플 문서셋을 쓰고, 같은 top-k를 쓰고, query 개수도 같고, gate 파일 경로도 같으면 같은 실험선처럼 읽고 싶어진다. 문제는 실제로는 이 네 필드만으로 baseline을 보증하기 어렵다는 점이었다.

예를 들어 query 수가 6개로 같아도 질문 본문이 달라질 수 있고, label이나 expected title이 바뀌면 quality metric의 뜻도 달라진다. gate 파일 경로가 같아도 JSON 안의 threshold를 손보는 순간, 어제의 WARN과 오늘의 WARN은 전혀 같은 판정이 아닐 수 있다. 나는 이 구간이 특히 마음에 걸렸다. 겉으로는 같은 history line인데 실제로는 다른 기준선이 섞인 상태가 제일 위험해 보였기 때문이다.

  • query 개수는 같지만 질문 구성이 달라질 수 있다.
  • gate 경로는 같지만 threshold 값이 달라질 수 있다.
  • 이 두 변화가 섞이면 status transition이 그럴듯한 착시를 만들 수 있다.

결국 내가 막고 싶었던 건 "history가 끊긴다"는 문제가 아니라, 끊겨야 하는 history가 매끈하게 이어져 보이는 문제였다. 이번 작업은 바로 그 지점을 잘라내는 쪽에 가깝다.

2. 이번에 붙인 guard

이번 반복에서 history entry에 두 필드를 더 넣었다. 하나는 query_set_fingerprint, 다른 하나는 quality_gate_fingerprint다. query 쪽은 질문 본문, label, cluster, tags, expected_titles를 정규화한 뒤 해시로 만들고, gate 쪽은 threshold JSON payload 자체를 정렬 직렬화한 뒤 해시로 만들었다. 경로 이름이 같아도 내용이 다르면 다른 fingerprint가 나오게 잡았다.

구현 자체는 거창하지 않다. history를 붙일 때 현재 run의 entry를 먼저 만들고, 기존 history 안에서 같은 profile 이름과 경로를 가진 snapshot을 찾기 전에 baseline mismatch를 한 번 더 본다. 여기서 query_set이나 quality_gate mismatch가 걸리면 그 snapshot은 delta 계산에서 제외하고, 대신 warning payload에만 남긴다. 그러면 summary는 계속 만들어지되, 잘못된 이전 run과 억지로 이어 붙지는 않는다.

{
  "query_set_fingerprint": "...",
  "quality_gate_fingerprint": "...",
  "history_warning": "baseline-mismatch(query_set)"
}

텍스트 출력도 같이 바꿨다. baseline이 정상일 때는 예전처럼 history_runs, history_transition, top1_hitΔ, full_coverageΔ만 읽으면 된다. 반대로 섞이면 아래처럼 warning을 바로 노출한다.

  • history_warning=baseline-mismatch(query_set)
  • history_warning=baseline-mismatch(quality_gate)
  • incompatible_labels=old-baseline(query_set) 같은 라벨 목록

나는 이 출력이 꽤 마음에 들었다. 숫자를 더 늘린 게 아니라, 지금 이 숫자를 믿어도 되는가를 먼저 보여 주기 때문이다.

3. 정상 baseline에서는 그대로 이어진다

guard를 넣었다고 기존 history가 다 끊어지면 오히려 더 곤란하다. 그래서 먼저 샘플 baseline을 fingerprint 포함 형태로 다시 저장하고 iter20을 붙여 봤다. 결과는 기대한 그대로였다. stable한 건 계속 stable했고, warning band에 있던 profile도 그대로 같은 칸에 남았다.

Profile iter18 iter19 iter20 해석
relation_weight_dense PASS PASS PASS 기준선 유지
path_bridge_probe WARN WARN WARN 탐색용 경고선 유지
path_bridge_focus HARD-FAIL HARD-FAIL HARD-FAIL 폐기 후보 유지

요약하면 guard는 기존 stable line을 망치지 않았다. 이 점이 먼저 확인돼야 다음 단계로 갈 수 있었다. 이번 샘플에서는 세 profile 모두 status가 그대로였고, top1_hitΔfull_coverageΔ도 0%였다. 즉 이전 run과 같은 기준선 위에서 정말 flat line인지, 아니면 착시인지 구분할 최소한의 안전장치가 생긴 셈이다.

4. 일부러 틀린 baseline을 섞어 봤다

이 기능은 정상 케이스보다 오히려 실패 케이스에서 의미가 더 분명했다. 그래서 테스트에서는 synthetic history payload를 두 개 만들었다. 하나는 query set fingerprint만 다르게 만든 old-baseline, 다른 하나는 quality gate fingerprint만 다르게 만든 old-gate다. 둘 다 profile 이름과 경로는 같게 두고, baseline 축만 틀리게 만들었다.

주입한 entry 바꾼 축 출력 경고 처리 방식
old-baseline query_set_fingerprint baseline-mismatch(query_set) delta 계산에서 제외
old-gate quality_gate_fingerprint baseline-mismatch(quality_gate) delta 계산에서 제외

이 장면이 이번 작업의 핵심이었다. 예전 같으면 이런 entry도 history line에 들어와서, 마치 이전 run과 자연스럽게 이어지는 것처럼 보였을 수 있다. 지금은 그렇지 않다. mismatch가 뜨면 history_runs는 compatible snapshot만 세고, previous_status도 억지로 이어 붙이지 않는다. 대신 warning과 incompatible label만 남긴다. 나는 이 동작이 꽤 마음에 들었다. 그럴듯한 숫자를 보여 주는 대신, 비교 자체를 보류해야 한다는 사실를 먼저 드러내 주기 때문이다.

5. 이번 기준선이 다음 반복에 남기는 것

이번 커밋은 결과를 더 좋게 만든 날이라기보다, 결과를 덜 오해하게 만든 날에 가깝다. 테스트는 이제 26개까지 늘었고, 이 안에 fingerprint 저장 여부와 baseline mismatch 경고 케이스가 같이 들어갔다. 코드 한 줄로 보면 작은 변경처럼 보여도, 실제로는 history를 읽는 습관 자체를 조금 바꿨다. 앞으로는 `PASS->WARN`이나 `WARN->HARD-FAIL` 같은 문장을 보자마자 바로 반응하기보다, 먼저 같은 baseline 위의 변화인지부터 확인하게 될 것 같다.

다음 반복에서는 이 fingerprint/history 포맷을 샘플 말고 운영 문서셋 쪽에도 붙여 보고 싶다. 그리고 warning이 난 mismatch entry를 따로 모아 어떤 축이 바뀌었는지 바로 읽는 inspection queue도 붙여 볼 생각이다. GraphRAG처럼 path, edge, coverage가 같이 흔들리는 프로젝트에서는 점수표 하나보다 비교 가능한 실험만 같은 줄에 두는 감각이 훨씬 오래 남는다. 나는 오늘 그 감각을 코드 쪽에 한 칸 더 박아 넣은 셈이다.

커밋 메시지는 단순하게 Add GraphRAG history baseline guard로 남겼다. 이번 작업은 딱 그 한 줄이면 충분했다. 화려한 기능을 붙인 날은 아니지만, 이런 guard가 하나 들어가고 나면 다음 주의 실험 로그를 훨씬 덜 의심하게 된다. 지금 이 프로젝트에서는 그런 종류의 진전이 의외로 크게 남는다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.