2026년 4월 15일 | 프로젝트
rank-shift report를 붙인 뒤에도 마지막으로 남는 손작업이 하나 있었다. 흔들린 query가 왜 흔들렸는지를 내가 숫자를 다시 읽으면서 머릿속에서 분류하는 일이다. GraphRAG 쪽은 edge, path, coverage가 같이 섞여 있어서, rank shift 1건만 적어 두면 설명이 끝난 것 같아도 막상 다음 반복에 다시 들어갈 때는 한 번 더 표를 들춰보게 된다.
그래서 이번에는 새 scoring profile을 더 만들기보다, 흔들림 자체에 이름을 붙이는 얇은 레이어를 먼저 올렸다. `profile_eval`이 rank-shift report를 만들 때 이제는 `primary_shift_reason`과 `shift_reason_labels`를 같이 남긴다. 말은 사소해 보이는데, 실제로 돌려 보니 summary 한 화면만 보고도 이번 조정이 어떤 종류의 흔들림을 만들었는지 바로 읽히는 차이가 꽤 컸다.
1. rank shift만 적어 두면 부족했던 이유
직전 단계의 rank-shift focus report는 분명히 도움이 됐다. 전체 summary를 읽고 다시 볼 query를 손으로 추려 적는 수고가 사라졌기 때문이다. 그런데 막상 흔들린 query를 다시 열어 보면 또 한 번의 해석 단계가 남아 있었다. coverage를 버려서 흔들린 건지, path bonus를 과하게 탄 건지, 아니면 새 chunk가 top-k 안으로 끼어든 건지는 여전히 숫자를 보고 내가 다시 판단해야 했다.
이건 query가 세 개일 때는 버틸 만하다. 하지만 실제 문서셋으로 넘어가면 이야기가 달라진다. 흔들린 query가 다섯 개, 열 개로 늘어나는 순간부터는 report가 있어도 다시 사람이 묵시적으로 타입을 붙이는 시간이 길어진다. 나는 이 프로젝트에서 retrieval 공식을 복잡하게 만드는 것보다, 바뀐 결과를 재진입 가능한 기록으로 남기는 일이 더 중요하다고 보고 있다. 그래서 이번에는 query cluster를 크게 설계하기 전에, 우선 흔들림에 이름부터 붙였다.
- rank-shift report는 다시 볼 query를 좁힌다.
- reason labels는 그 query가 어떤 종류의 흔들림인지 먼저 말해 준다.
- trace diff는 마지막으로 근거 구조를 확인하는 단계다.
2. 이번에 실제로 붙인 이유 라벨
이번 변경에서 추가한 필드는 두 개다. 하나는 대표 분류인 primary_shift_reason, 다른 하나는 세부 신호를 남기는 shift_reason_labels다. 예를 들어 path score는 올라가고 coverage는 내려간 경우에는 `path-over-coverage`를 붙이고, top-1 자체가 뒤집히면 `top1-flip`, 새 chunk가 들어오면 `new-entry`, 기존 chunk가 빠지면 `dropout`을 같이 남긴다. stable query에는 굳이 라벨을 붙이지 않고 빈 리스트로 둔다.
report summary도 같이 바꿨다. 이제 rank-shift report에는 단순한 query 목록만 있는 것이 아니라 primary_reason_counts, reason_signal_counts, reason_buckets가 들어간다. 텍스트 출력 헤더에도 `shift_reasons=...`가 붙기 때문에, trace JSON을 열기 전에 profile 성격을 먼저 읽을 수 있다. 이 정도면 내가 원했던 "inspection queue의 타입화"는 최소 단위로 만족한다.
PYTHONPATH=src:. python -m graphrag_mvp.profile_eval --dataset data/sample_documents.json --queries-file data/sample_queries.json --profile configs/relation_weight_dense.json --profile configs/path_bridge_focus.json --top-k 3 --trace-dir traces/profile_eval_bundle_iter14
이 한 번의 실행으로 지금은 profile별 trace, default 대비 diff, rank-shift report, reason label이 붙은 요약 JSON, 텍스트 summary가 같이 남는다. 산출물이 늘어난 것처럼 보이지만, 실제로는 다음 행동 순서를 더 짧게 만든 변화에 가깝다.
3. 숫자로 다시 보니 무엇이 달라졌는가
샘플 query 세트에서 dense profile과 path_bridge_focus를 다시 돌려 보니, 이번 레이어가 어디에 필요한지 더 선명하게 보였다. dense profile은 edge와 path 기여를 조금 올리면서도 랭킹은 건드리지 않았다. 반면 path_bridge_focus는 coverage를 더 많이 포기하면서 실제 rank shift 1건을 만들었다. 예전에는 여기서 "흔들렸다"까지만 적었다면, 이제는 어떤 타입의 흔들림인지까지 바로 적을 수 있다.
| profile | top-1 agreement | stable top-k | avg edgeΔ | avg pathΔ | avg covΔ | reason summary | 내가 읽은 의미 |
|---|---|---|---|---|---|---|---|
| relation_weight_dense | 100% | 100% | +0.1500 | +0.1667 | -0.2500 | stable | 랭킹을 건드리지 않고 score 성격만 조정한 안정형 기준선 |
| path_bridge_focus | 67% | 67% | -0.5667 | +0.4833 | -1.0222 | path-over-coverage: 1 | coverage를 희생하고 path를 세게 읽는 조정이 실제 top-1 뒤집힘으로 이어진 실험형 기준선 |
여기서 마음에 들었던 부분은 dense profile의 헤더가 `stable`로 끝난다는 점이다. 바뀐 숫자는 있지만 굳이 inspection queue를 열지 않아도 된다는 뜻이기 때문이다. 반대로 path_bridge_focus는 `path-over-coverage:1`로 바로 읽힌다. 이 한 줄만 있어도 내가 다음에 열어야 할 보고서의 성격이 먼저 정리된다.
4. partial-coverage query에서 실제로 읽힌 장면
이번 샘플에서 흔들린 query는 하나뿐이다. `partial-coverage`에서 default top-1은 Incident resolution이었고, path_bridge_focus를 적용하면 Retrieval planner가 1위로 올라온다. 점수 차이는 `top1Δ -0.9334`, `edgeΔ -0.7000`, `pathΔ +0.5000`, `covΔ -1.0667`이다. 이전에도 이 숫자는 있었지만, 이제는 이 장면이 path-over-coverage 타입이라고 report가 먼저 말해 준다.
세부 signal도 꽤 직관적이다. `coverage-loss`가 붙어 있으니 coverage를 포기한 흔들림이라는 뜻이고, `top1-flip`이 붙어 있으니 top-1이 실제로 뒤집혔다는 뜻이다. 나는 이런 식의 얇은 라벨이 좋다. 머신러닝 모델처럼 거창한 분류기를 만든 건 아니지만, 적어도 다음 액션을 정할 때 필요한 최소한의 설명은 자동으로 남기기 때문이다.
실제로 report 안에는 primary_reason_counts = path-over-coverage 1건, reason_buckets = {path-over-coverage: [partial-coverage]}가 남는다. query 항목 안에는 moved_up, moved_down, trace before/after/diff 경로까지 그대로 들어 있으니, 내가 할 일은 이제 숫자를 다시 해석하는 게 아니라 해당 query의 trace diff를 열어 보는 것이다.
- 대표 이유: path-over-coverage
- 세부 신호: coverage-loss, top1-flip
- 해석: path를 더 세게 읽는 조정이 coverage 손실을 감수하면서 top-1을 뒤집었다.
5. 이번 기준선이 남기는 것
이번 작업은 retrieval 품질을 바로 끌어올린 단계는 아니다. 대신 조정의 부작용을 읽는 속도는 확실히 좋아졌다. GraphRAG처럼 여러 score 축이 섞이는 프로젝트에서는 좋은 조정을 찾는 일만큼, 어떤 조정이 어떤 타입의 흔들림을 만들었는지 빨리 읽는 일도 중요하다. 나는 이런 판독면이 있어야 tuning이 감상 메모로 흐르지 않는다고 본다.
이번 변경 뒤에는 테스트 16개를 다시 돌려 통과를 확인했다. 구현 노트와 trace bundle, 시각 자료도 같이 남겨 두었고, 재현 경로도 그대로 유지했다. 다음 단계는 이제 더 분명하다. query 파일에 cluster나 tag 메타데이터를 붙여, 이번에 만든 reason label과 query group을 함께 보는 것이다. 그러면 나중에는 "어떤 질문 묶음에서 coverage-loss가 반복되는가" 같은 식으로 더 실전적인 판독면을 만들 수 있다.
요즘 GraphRAG 쪽에서 덜 허무한 작업은 대체로 이런 쪽이다. 수식을 하나 더 얹는 일보다, 바뀐 결과를 다시 읽는 비용을 줄이는 구조를 붙일 때 다음 반복으로 넘어가는 속도가 더 또렷하게 살아난다.
'[AI 실험실] > [개인 프로젝트] GraphRAG' 카테고리의 다른 글
| GraphRAG | Quality Gate History (0) | 2026.04.24 |
|---|---|
| GraphRAG | Query Cluster 집계 추가 (0) | 2026.04.16 |
| GraphRAG | Rank Shift Focus Report (0) | 2026.04.15 |
| GraphRAG | Profile Eval Trace Bundle (0) | 2026.04.13 |
| GraphRAG Relation-Weighted Scoring (0) | 2026.04.05 |