2026년 4월 16일 | 프로젝트
query 세트가 여섯 개쯤만 되어도, 어떤 질문 묶음이 흔들렸는지를 내가 다시 손으로 묶어 보는 시간이 은근히 길어졌다. 최근 GraphRAG 쪽은 rank shift 이유 라벨까지는 잘 남기고 있었는데, 그다음 단계에서 여전히 coverage-sensitive 류 질문이 같이 무너졌는지, 아니면 한두 개 query만 우연히 흔들린 건지를 내가 다시 눈으로 분류해야 했다.
그래서 이번에는 scoring 공식을 하나 더 늘리기보다, query 파일 자체에 묶음 정보를 넣고 group 단위로 읽는 판독면을 먼저 붙였다. `profile_eval`이 이제는 `{label, query, cluster, tags}` 구조를 읽고, default 대비 결과 안에 cluster_summaries와 tag_summaries를 같이 남긴다. 말하자면 query 하나의 흔들림을 보는 단계에서, 어떤 질문 가족이 같이 흔들리는지 보는 단계로 한 칸 올라간 셈이다.
1. 왜 query group을 먼저 붙였는가
직전 단계의 reason label은 분명 도움이 됐다. `path-over-coverage`, `coverage-loss`, `top1-flip` 같은 신호를 붙여 두니, 흔들린 query를 다시 열 때 해석 속도는 빨라졌다. 그런데 여기에도 여전히 마지막 손작업이 남아 있었다. 예를 들어 `partial-coverage`와 `service-dashboard`가 둘 다 흔들렸다면, 그 두 건이 사실상 같은 종류의 질문인지, 혹은 완전히 다른 사례인지까지는 summary가 직접 말해 주지 않았다.
나는 이 프로젝트에서 숫자보다 재진입 비용을 더 신경 쓰고 있다. score delta를 예쁘게 남기는 일보다, 다음 반복에서 무엇을 다시 볼지 빠르게 정해 주는 면이 더 중요하다는 뜻이다. 실제 문서셋으로 가면 query는 몇 개가 아니라 몇십 개가 될 텐데, 그때는 개별 query 라벨보다 어느 cluster가 같이 흔들리는지가 먼저 보여야 실험이 덜 흐려진다. 그래서 이번에는 retrieval 공식을 바꾸는 대신, query group 요약을 먼저 붙였다.
- query line은 개별 질문의 top-1, edge, coverage 변화를 본다.
- cluster summary는 같은 질문 묶음이 같이 흔들리는지 본다.
- tag summary는 질문 성격이 겹치는 축을 더 가볍게 다시 본다.
2. 이번에 실제로 바뀐 것
구현은 생각보다 단순하다. query 파일 포맷을 문자열 리스트에서 한 단계만 넓혔다. 이제는 `label`, `query` 외에 `cluster`, `tags`를 같이 넣을 수 있고, 로더가 이 메타데이터를 읽어서 비교 결과와 text summary까지 그대로 끌고 간다. 덕분에 프로필을 돌린 뒤 곧바로 cluster-groups, tag-groups 섹션이 생기고, 각 query 줄에도 `cluster=...`, `tags=...`가 같이 찍힌다.
이번 샘플 query 세트는 세 묶음으로 나눴다. `incident-core`에는 핵심 장애 경로 질문을, `doc-bridges`에는 문서 연결형 질문을, `coverage-sensitive`에는 coverage를 덜 보기 시작할 때 흔들릴 가능성이 높은 질문을 넣었다. 태그는 `dashboard`, `partial-coverage`, `docs`, `stable`, `path-shift` 같은 가벼운 축으로 붙였다. 여기서 중요한 점은 taxonomy를 화려하게 만드는 게 아니라, 다음 실험을 다시 열 때 바로 의미가 떠오를 정도로만 얇게 붙이는 것이다.
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_iter15_cluster_groups
이 한 번의 실행으로 지금은 profile별 trace, default 대비 diff, rank-shift report, 그리고 그 안의 cluster / tag summary까지 같이 남는다. 예전 같으면 output을 본 뒤 내가 메모장에 다시 "이건 dashboard 쪽 질문들"이라고 적었을 부분이, 이제는 코드 안에서 바로 계산된다.
3. profile 수준에서 읽힌 차이
샘플 숫자는 꽤 분명했다. relation_weight_dense는 top-1 agreement 100%, stable top-k 100%로 남았다. score 성격은 조금 달라지지만 질문 묶음 자체는 건드리지 않는 안정형 기준선에 가깝다. 반대로 path_bridge_focus는 평균 path delta는 올리지만 coverage delta를 크게 깎으면서, 전체 top-1 agreement를 67%까지 낮췄다. 여기까지만 보면 그냥 "조금 공격적인 profile" 정도로 읽힐 수 있다.
| profile | top-1 agreement | stable top-k | avg pathΔ | avg covΔ | reason summary | 내가 읽은 의미 |
|---|---|---|---|---|---|---|
| relation_weight_dense | 100% | 100% | +0.1667 | -0.2500 | stable | 랭킹은 건드리지 않고 점수 성격만 바꾸는 안정형 기준선 |
| path_bridge_focus | 67% | 50% | +0.4833 | -1.0222 | path-over-coverage:3 | cluster별로 흔들림이 갈리는 실험형 프로필 |
이번에 좋았던 지점은 바로 그 다음 줄이었다. 이제는 같은 profile이라도 어떤 cluster를 주로 흔드는지가 같이 나온다. 즉 profile 전체 평균 하나만 보는 대신, 어디에 비용이 생기는지를 group별로 바로 읽을 수 있다. 나는 이 한 칸이 생긴 것만으로도 tuning 메모가 훨씬 덜 감상적으로 바뀌었다.
4. cluster로 다시 보니 뭐가 달라졌는가
path_bridge_focus를 cluster로 묶어 다시 보면 패턴이 더 선명하다. `incident-core`는 두 질의 모두 버텼다. 반면 `coverage-sensitive`는 두 질의가 모두 흔들렸고 stable top-k는 0%까지 내려갔다. `doc-bridges`는 중간이었다. 한 건은 유지됐고, `manual-incident` 한 건만 실제 top-1 뒤집힘으로 이어졌다. 즉 이 profile이 무조건 나쁜 것이 아니라, coverage를 덜 봐도 괜찮은 묶음과 바로 무너지는 묶음이 갈린다는 사실이 이번에 드러난 셈이다.
| cluster | query 수 | top-1 agreement | stable top-k | rank shift | 대표 이유 | 내가 읽은 의미 |
|---|---|---|---|---|---|---|
| incident-core | 2 | 100% | 100% | 0 | stable | 핵심 경로 질의는 끝까지 버텼다. |
| doc-bridges | 2 | 50% | 50% | 1 | path-over-coverage:1 | 문서 연결형 질의는 일부만 흔들렸다. |
| coverage-sensitive | 2 | 50% | 0% | 2 | path-over-coverage:2 | coverage를 덜 보자마자 가장 먼저 무너진 묶음이었다. |
특히 `coverage-sensitive`가 `path-over-coverage:2`로 바로 찍히는 장면이 좋았다. 이전에도 개별 query를 열면 알 수 있는 정보였지만, 이제는 summary에서 바로 "이 profile은 coverage를 깎으면서 이 질문 묶음을 먼저 흔든다"고 읽힌다. 이런 식이면 다음 실험도 더 직접적으로 잡힌다. 예를 들면 "coverage-sensitive만 덜 흔들리게 하고 doc-bridges 이득은 유지할 수 있는가" 같은 질문으로 바로 넘어갈 수 있다.
5. tag 집계가 남긴 실전적인 포인트
tag summary는 cluster보다 더 가볍지만 생각보다 유용했다. `dashboard` 태그가 붙은 세 질의는 top-1 agreement 67%, stable top-k 33%였다. `partial-coverage` 태그 두 건은 둘 다 흔들렸다. 반면 `stable` 태그 세 건은 전부 유지됐다. 나는 이런 가벼운 축이 있어야 나중에 실제 서비스 질문을 넣었을 때도 taxonomy를 무겁게 만들지 않고 빠르게 판독면을 키울 수 있다고 본다.
| tag | query 수 | top-1 agreement | stable top-k | rank shift | 내가 읽은 포인트 |
|---|---|---|---|---|---|
| dashboard | 3 | 67% | 33% | 2 | Customer Dashboard가 섞인 질문은 흔들림이 바로 드러났다. |
| partial-coverage | 2 | 50% | 0% | 2 | 불완전 매칭을 허용하는 질문은 path bias 영향을 크게 받았다. |
| docs | 2 | 50% | 50% | 1 | 문서 브리지 질문은 한 건만 top-1이 뒤집혔다. |
| path-shift | 1 | 0% | 0% | 1 | manual-incident 한 건이 바로 shift로 이어졌다. |
이번 변경이 retrieval 품질을 바로 끌어올린 것은 아니다. 대신 어떤 질문 묶음에 비용이 생기는지를 빨리 읽는 면은 확실히 좋아졌다. GraphRAG처럼 edge, path, coverage가 같이 섞이는 프로젝트에서는 공식 하나를 더 얹는 일보다, 그 공식이 어느 query 가족에 부작용을 만드는지 먼저 읽는 편이 다음 반복을 훨씬 덜 허무하게 만든다.
6. 이번 기준선 이후에 바로 해볼 것
이제 남은 일은 명확하다. 샘플용 cluster/tag를 실제 문서셋용 taxonomy로 바꾸고, group별 precision / coverage 지표를 붙여야 한다. 그다음에는 `coverage-sensitive` 묶음만 유독 약해지는 profile을 빠르게 걸러낼 수 있을 것이다. 이번에는 질문 묶음을 읽는 판독면을 만들었고, 다음에는 그 판독면 위에 실제 품질 지표를 얹는 순서가 맞아 보인다.
테스트는 현재 17개를 다시 돌려 통과를 확인했다. sidecar 저장소에는 `Add GraphRAG query cluster summaries` 커밋까지 남겼다. 당장 체감되는 변화는 화려하지 않지만, query 수가 늘어날수록 이런 얇은 집계 레이어가 실험 리듬을 꽤 많이 살려 준다. 나는 요즘 GraphRAG에서 이런 종류의 정리가 성능 수치 하나보다 더 오래 남는 기준선이라고 느끼고 있다.
'[AI 실험실] > [개인 프로젝트] GraphRAG' 카테고리의 다른 글
| GraphRAG | History mismatch 리포트 추가 (0) | 2026.04.27 |
|---|---|
| GraphRAG | Quality Gate History (0) | 2026.04.24 |
| GraphRAG | Rank Shift Reason Labels (0) | 2026.04.15 |
| GraphRAG | Rank Shift Focus Report (0) | 2026.04.15 |
| GraphRAG | Profile Eval Trace Bundle (0) | 2026.04.13 |