공정 비교는 평가 파이프라인을 의심하는 데서 시작했습니다: 대조군 오염, 평가기 루프 차단, 회수 지표 결함
사내 규정 165문항으로 경쟁 클라우드 에이전트와 비교하기 전에 평가 파이프라인 자체를 검증했습니다. 적재 결함, 오염된 대조군, 상대 설정 오인, 응답 시간을 부풀리던 평가기, 회수를 과소 집계하던 지표를 차례로 찾아 고친 뒤 같은 조건에서 80.0% 대 66.7%를 측정했습니다.
Key Results
- 80.0% vs 66.7%
- 사내 규정 165문항 정확도같은 답변 모델(gpt-5.4-mini)·시스템 프롬프트·청크 2,000/200·임베딩, judge claude-sonnet-4-5 정확도 100점, 근거 수 자동 구성 1회 실행
- 54 s → 16.8 s
- 평가기 기록 문항당 시간 중앙값평가 런 3건 동시, judge 동기 호출을 스레드로 분리 전후, 서버 노드 실행 시간 11~14 s
- 57 / 235
- 누락으로 집계되던 정답 규정골든셋 파일명(.docx)과 우회 적재 파일명(.md)의 확장자 불일치, 확장자 무시 대조로 재계산
캐피탈 고객은 사내 규정을 검색해 답하는 에이전트를 경쟁 클라우드 서비스로 만들어 운영하고 있었고, 고객이 직접 수행한 50문항 평가에서 그 에이전트가 98%, XGEN의 벡터 검색 구성이 86%를 받았습니다. 요청은 같은 질문으로 다시 측정해 객관적으로 비교해 달라는 것이었습니다.
비교 실험에서 가장 쉽게 놓치는 것은 비교 대상이 아니라 비교 장치입니다. 문서가 제대로 적재됐는지, 대조군이 정말 대조군인지, 평가기가 기록하는 시간이 실제 실행 시간인지, 회수 지표가 정답을 제대로 세는지 중 하나만 어긋나도 점수 차이는 검색 품질이 아닌 다른 것을 말하게 됩니다. 이 비교에서는 다섯 군데가 실제로 어긋나 있었습니다.
이 글은 그 다섯 군데를 어떤 신호로 발견했고, 어떤 순서로 원인을 좁혔고, 무엇을 고쳤는지를 다룹니다. 점수 자체보다 점수를 믿을 수 있게 만든 과정에 초점을 둡니다.
두 에이전트를 같은 평가 파이프라인에 넣기
비교의 기준은 하나로 정했습니다. 점수 차이는 검색과 답변 구성에서만 나와야 한다. 그래서 경쟁 에이전트도 XGEN 워크플로우 안에서 실행했습니다. 경쟁 에이전트를 부르는 등록 API 호출 노드를 워크플로우에 두고, XGEN 에이전트와 같은 플랫폼 평가 기능(tester)에 같은 질문 파일과 같은 judge 설정으로 등록했습니다.
[사내 규정 165문항 xlsx] → 평가 API (judge claude-sonnet-4-5, 정확도 100점)
├─ 경쟁 에이전트 런 : 등록 API 호출 노드 → 경쟁 서비스 에이전트
├─ XGEN 런 (사전 검색) : 검색 노드(질의 분할) → 에이전트(gpt-5.4-mini)
└─ XGEN 런 (도구 모드) : 에이전트가 검색 도구 호출 → 같은 답변 모델
문항마다: 워크플로우 실행(SSE) → 답변 수집 → judge 채점 → 실행 시간 기록질문셋은 고객이 만든 165문항입니다. 단일 조회, 관계형, 교차 문서가 55문항씩이고 문항마다 정답 발췌와 정답 문서명이 있습니다. 맞춘 조건은 답변 모델(gpt-5.4-mini, 온도 1), 고객의 시스템 프롬프트, 청크 2,000자와 겹침 200자, 임베딩 모델(text-embedding-3-small), 근거 수, judge와 채점 척도입니다. judge는 답변 모델과 다른 계열을 써서 답을 만든 모델이 자기 답을 채점하지 않게 했습니다. 비교용 컬렉션은 443문서, 14,924청크였고, 실제 벡터 DB에서 청크 본문 최대 2,000자, 연속 청크 겹침 197~200자를 확인했습니다.
평가는 고객 체험 환경과 사내 개발 환경 두 곳에서 실행했습니다. 평가 런은 서버 프로세스 메모리 안의 백그라운드 태스크로 돌고, 여러 런을 동시에 등록하는 일이 잦았습니다. 이 운영 조건이 뒤에서 설명할 결함 두 개의 배경이 됩니다.
결과를 믿기 전에 의심한 다섯 군데
첫 측정을 받아들이기 전에 실행 기록을 열어 봤고, 조건을 맞추는 동안 다음 결함들이 하나씩 드러났습니다.
| 결함 | 처음 보인 신호 | 실제 원인 | 그대로 뒀다면 |
|---|---|---|---|
| 문서 적재 | 자율규제 문서 질문에 XGEN만 답하지 못함 | 67건 중 66건이 서버 Word 파서 실패로 오류 문자열 1청크만 적재 | 47문항이 검색 로직과 무관하게 오답 |
| 대조군 | 벡터 대조군 결과가 그래프 검색 결과와 같음 | 그래프가 있는 컬렉션에서 일반 검색이 그래프 검색으로 자동 전환 | 그래프 대 벡터 차이를 잘못 귀속 |
| 상대 설정 | 보고서의 상대 검색 설정 칸이 미확인 상태 | 경쟁 쪽 근거 수는 3이 아니라 5 | 앞선 회차가 3 대 5 비교 |
| 평가기 시간 | 기록 54초 대 서버 실행 11~14초 | 비동기 평가 루프 안의 judge 동기 호출 | 응답 시간 비교 왜곡 |
| 회수 지표 | 교차 문서 회수 절대값이 비정상적으로 낮음 | 파일명 정확 일치 대조, .docx 대 .md | 정답 규정 235건 중 57건 누락 집계 |
적재 결함은 원인 수정이 아니라 우회로 처리했습니다. 로컬의 같은 원문 파일은 서버와 같은 버전의 python-docx로 정상 열렸고, 새로 업로드해도 서버에서 같은 오류가 반복됐습니다. 서버 처리 경로의 원인 추적은 별도 과제로 남기고, 로컬에서 문단과 표를 마크다운으로 변환해 같은 내용을 다시 적재했습니다. 이 우회가 뒤의 지표 결함을 만든 출발점이기도 합니다.
평가기가 응답 시간을 부풀린 과정
경쟁 에이전트와 비교하면 사전 검색 구성의 문항당 시간이 기록상 54초였습니다. 그런데 같은 시간대의 서버 로그로 노드 타임라인을 보면 질의 분할 약 1초, 검색 7~8초, 답변 3~5초로 합계 11~14초였습니다. 40초 가까운 차이가 어디서 오는지 알아야 응답 시간 비교를 쓸 수 있었습니다.
다만 느리다는 기록에는 두 종류가 섞여 있었습니다. 하나는 실제 지연이었습니다. 같은 날 한 런에서 문항 시간이 40, 58, 245초로 튀어, 검색 API를 직접 호출해 보니 1회 14.5초였고 trace에서는 그래프 확장 6.0초, 청크별 개체 조회 6.3초가 대부분이었습니다. DB 세션 활동을 샘플링하니 컬렉션 전체 청크 수를 세는 count(DISTINCT chunk_id)가 매핑 표 약 44만 행을 읽어 3.5초씩 걸렸고, 이 값이 검색 한 번에 세 번 계산됐습니다. 직전에 들어간 허브 개체 제외 로직의 분모였고, 185청크짜리 고객센터 컬렉션에서는 드러나지 않았던 퇴행이었습니다. 요청당 1회 캐시와 인덱스를 건너뛰며 세는 재귀 CTE(loose index scan)로 바꾸자 같은 값 14,809를 세는 시간이 3.3초에서 0.9초로, 검색 1회는 14.5초에서 6.8초로 줄었습니다. 순위 결과가 바뀌지 않았다는 것은 다른 컬렉션의 순위 재측정으로 확인했습니다.
검색이 7~8초로 내려온 뒤에도 기록은 여전히 길었습니다. 그래서 측정 착시를 의심하고, 실행 API를 직접 호출하면서 SSE 이벤트가 클라이언트에 도착한 시각을 기록하는 프로브를 만들었습니다. 서버 노드는 11~14초 만에 끝났는데 완료 이벤트는 40~63초 뒤에 도착했습니다. 결정적인 단서는 초기 이벤트였습니다. 서버 타임스탬프가 같은 이벤트들이 한 번은 5초, 한 번은 14초 뒤에 도착했습니다. 실행 자체가 아니라 실행기 큐를 클라이언트로 흘려보내는 소비 코루틴이 이벤트 루프 시간을 받지 못하고 있다는 뜻이었습니다.
가능한 원인을 하나씩 지웠습니다. 가드레일 클라이언트 호출은 0건, LLM SDK의 재시도와 이벤트 루프 종료 오류도 0건, Redis 오류도 0건이었습니다. 워크플로우 로그에는 공백이 없었는데, 노드는 워커 스레드에서 돌기 때문에 로그는 계속 남고 루프 위의 소비자만 멈추는 구조와 일치했습니다. 스택을 직접 보려고 py-spy를 시도했지만 컨테이너에 ptrace 권한이 없어 실패했고, 코드를 읽어 원인을 찾았습니다.
평가 런의 백그라운드 함수는 async인데, 그 안에서 judge 채점 함수를 동기 HTTP로 직접 호출하고 있었습니다. judge 호출 하나가 5~40초 걸리는 동안 프로세스의 이벤트 루프 전체가 멈췄고, 런 3건이 동시에 돌면 루프는 대부분의 시간 동안 정지해 있었습니다. 그 결과 각 문항의 실행 시간 기록에 다른 런의 judge 시간이 얹혔고, 실행이 가장 긴 사전 검색 구성이 가장 크게 손해를 봤습니다.
pr = judge_with_preset(...) # 이전: async 루프 안의 동기 HTTP 호출
pr = await asyncio.to_thread(judge_with_preset, ...) # 이후: 블로킹 호출을 워커 스레드로배포 뒤 같은 조건(런 3건 동시)에서 사전 검색 구성의 기록은 평균 16.4초, 중앙값 16.8초로 서버 실행 시간에 근접했습니다. 검색 퇴행과 평가기 착시를 따로 분리하지 않았다면, 둘 중 하나만 고치고 나머지를 다른 원인으로 오해했을 것입니다.
무엇과 비교하고 있는지 확인하기
대조군 문제는 두 번 나왔습니다. 처음에는 순수 벡터 검색 대조군을 만들었는데 결과가 그래프 검색과 같았습니다. 실행 로그에 "온톨로지 그래프가 있어 온톨로지 검색으로 전환"이 찍혀 있었습니다. 그래프가 있는 컬렉션에서는 사전 검색 경로가 자동으로 그래프 검색으로 바뀌고 있었고, 검색 도구 모드(light)에는 이 전환이 없다는 것을 확인해 그쪽을 대조군으로 삼았습니다.
두 번째는 2주 남짓 뒤에 드러났습니다. 다른 보고서를 만들면서 "기존 검색"으로 표기하던 사전 검색 워크플로우의 로그를 다시 보니 같은 전환 로그가 있었습니다. 이 자동 전환은 그보다 앞서 제가 넣은 코드였습니다. 그동안 "그래프 검색 대 기존 검색"으로 읽던 차이는 사실 그래프 대 벡터가 아니라 다중 질의와 동적 top-k의 효과였습니다. 보고서 표기를 "정보검색 노드(단일 질의, 고정 top-k)"로 고쳐 먼저 내고, 순수 벡터 기준선을 다시 측정했습니다.
| 기준선 만드는 방법 | 장점 | 단점 | 판단 |
|---|---|---|---|
| 그래프 없는 컬렉션을 새로 만듦 | 노드 설정을 바꾸지 않아도 됨 | 자동 백필이 600초 주기로 그래프를 빌드해 곧 그래프가 생김 | 기각 |
| 검색 도구(light) 모드 노드 | 자동 전환이 없는 순수 벡터 경로 | 검색 호출 횟수를 모델이 정함(한 문항 6회, 다른 구성 22회 관찰) | 기준선으로 채택 |
| 사전 검색의 자동 전환 코드 제거 | 기본 검색 노드는 항상 벡터, 그래프 검색은 전용 노드로만 | 그래프가 빌드된 컬렉션의 기존 고객 워크플로우 동작이 바뀜 | 제거 변경 제출, 배포 후 로그 확인 항목으로 관리 |
상대 설정도 같은 원리로 다시 확인했습니다. 앞선 회차들은 전달받은 설정 자료를 따라 경쟁 쪽 근거 수를 3으로 보고 맞췄습니다. 이후 고객 체험 환경 안에서 플랫폼 인증 함수로 토큰을 받아 경쟁 에이전트 정의를 읽기 전용 GET으로 조회했습니다. 사내 규정 에이전트는 처음부터 키워드 검색(simple)에 top_k 5였고, 3은 고객센터 에이전트의 값이었습니다. 버전 이력을 보면 비교 기간 내내 설정이 바뀌지 않았으므로, 앞선 일부 회차는 XGEN 3 대 경쟁 5의 비교였습니다. 그래서 그 회차들의 수치는 결과에서 뺐고, 이 글의 결과 회차는 5 대 5로 실행한 것입니다. 같은 조회로 경쟁 쪽이 질의 시점에 키워드 검색만 쓴다는 것도 확인했습니다. 맞춰 둔 임베딩 모델은 경쟁 쪽 질의에서는 쓰이지 않는 조건이었습니다.
근거 수를 숫자로 맞춰도 같은 조건은 아닙니다. 청크와 검색 방식이 다르면 같은 5개에 담기는 내용이 다르기 때문입니다. 그래서 근거 5개 고정 구성과 엔진이 문항마다 근거 수를 정하는 자동 구성을 함께 실행했습니다. 자동 구성이 실제로 고른 근거 수는 단일 질의 기준 최소 10, 중앙값 11, 최대 30이었고, 질의 분할 뒤 병합하면 실행당 중앙값 19개였습니다.
지표와 실행 인프라의 결함
검색 회수를 정확히 세야 하는 질문이 생기자 지표 결함이 드러났습니다. 교차 문서 문항의 정답 규정 회수가 비정상적으로 낮아 원인을 보니, 골든셋의 정답 파일명은 .docx인데 파싱 실패를 우회하느라 적재한 자율규제 문서는 .md였습니다. 순위 스크립트가 파일명을 정확 일치로 대조하고 있어 정답 규정 235건 중 57건이 구조적으로 "누락" 처리되고 있었습니다. 확장자를 떼고 대조하도록 스크립트를 고치고, 그때까지 보고서와 내부 문서에 인용한 회수 수치를 전부 재계산했습니다. 우회 적재라는 운영상의 결정이 지표 정의와 충돌한 사례였습니다.
골든셋 자체도 검수했습니다. 평가에 쓴 파일의 질문과 기대답변 165건이 고객 원본 파일과 순서와 본문까지 같고, 각 런의 문항 스냅샷과도 불일치가 0인지 확인했습니다. 다섯 구성이 모두 틀린 11문항(교차 문서 10, 관계형 1)은 기대답변의 3-gram이 지정 문서 청크 합집합에 87~99% 존재했고, 165건 전수로 넓혀도 164건이 90% 이상, 1건이 80%대였습니다. 앵커 용어가 지정 문서에 모두 등장하는 것도 165/165로 확인해, 답이 문서에 없는 문항은 없다고 판단했습니다. 그 11문항의 답변은 세 엔진 모두 대부분 두세 개 규정 중 하나만 인용한 요약이었습니다.
실행 인프라에도 결함이 두 가지 있었습니다. 하나는 런의 수명입니다. 평가 런이 서버 메모리의 백그라운드 태스크라 재개 기능이 없는데, 개발 환경에서는 머지가 있을 때마다 워크플로우 파드가 재시작돼 하루에 네 번 런이 중간에 멈췄습니다. 고객 체험 환경에서도 재배포 중이던 런 두 건이 pending 상태로 고착됐습니다. 6분 동안 진행이 없으면 런을 삭제하고 다시 등록하는 감독 스크립트를 만들고, 165문항을 유형별 55문항 세 묶음으로 나눠 병렬 실행해 재시작에 노출되는 시간을 35분으로 줄였습니다.
다른 하나는 judge 실패 정책입니다. judge 호출이 네트워크 오류로 한 번만 실패해도 플랫폼이 런 전체를 중단했습니다. 실제로 한 런은 119번째 문항에서 멈췄습니다. 중단된 런의 부분 점수는 쓰지 않고 처음부터 다시 등록해 완료본만 집계했습니다. 재시도 없이 런을 중단하는 정책은 별도 검토 과제로 남겼습니다.
같은 조건에서 한 번 실행한 결과
| 구성 | 정확도 | 단일 조회 | 관계형 | 교차 문서 | 문항당 시간 중앙값 |
|---|---|---|---|---|---|
| 경쟁 에이전트 | 66.67% (110/165) | 49 | 42 | 19 | 9.1 s |
| XGEN 사전 검색, 근거 5 | 78.79% (130/165) | 55 | 49 | 26 | 16.8 s |
| XGEN 사전 검색, 근거 자동 | 80.00% (132/165) | 55 | 51 | 26 | 15.8 s |
| XGEN 도구 모드, 근거 자동 | 78.79% (130/165) | 52 | 50 | 28 | 10.9 s |
측정 조건은 judge claude-sonnet-4-5 정확도 100점, 같은 답변 모델과 프롬프트, 근거 수 5 대 5(자동 구성은 엔진 결정)입니다. 교차 문서는 모든 구성에서 가장 낮았습니다(경쟁 35%, XGEN 47~51%). 응답 시간은 경쟁 에이전트가 짧았습니다.
| 평가 파이프라인 지표 | 이전 | 이후 | 측정 조건 |
|---|---|---|---|
| 평가기 기록 문항당 시간 중앙값 | 54 s | 16.8 s | 사전 검색 근거 5, 런 3건 동시, judge 호출 스레드 분리 전후 |
| 검색 API 1회 | 14.5 s | 6.8 s | 1.5만 청크 컬렉션, 청크 수 집계 캐시와 loose index scan 전후 |
| 누락으로 집계된 정답 규정 | 57 / 235 | 0 (구조적 누락 해소) | 파일명 확장자 무시 대조로 재계산 |
| 골든셋 스냅샷 불일치 | 미확인 | 0 / 165 | 고객 원본 파일과 런 문항 스냅샷 대조 |
회고와 남은 과제
이 비교에서 배운 것은 평가 파이프라인도 테스트 대상이라는 점입니다. 대조군은 로그로 실제 경로를 확인해야 했고, 상대 설정은 전달받은 자료가 아니라 실제 정의에서 확인해야 했고, 평가기가 기록한 시간은 서버 타임라인과 대조해야 했고, 회수 지표는 운영상의 우회가 정의를 바꾸지 않았는지 확인해야 했습니다. 특히 대조군 오염의 두 번째 사례는 제가 넣은 자동 전환 코드에서 나왔습니다. 편의를 위한 자동 동작이 실험 설계에서는 통제 변수를 몰래 바꿀 수 있다는 것을 직접 겪었습니다.
결과의 한계도 분명합니다. 구성마다 한 번씩 실행한 값이고, 답변 모델의 온도가 1이며 judge도 온도 1이라 같은 답이 회차마다 약 ±5%p 흔들립니다. judge가 다른 회차(gpt-5.4-mini 채점)의 수치는 더 관대한 채점이라 직접 비교하지 않았습니다. 한 고객의 문서와 운영 설정에서 측정한 값이므로 제품 일반의 우열로 읽을 수 없습니다. 남은 과제는 judge 온도 0 또는 2회 채점 합의로 채점 재현성을 확보하는 것, judge 1회 실패로 런 전체가 중단되는 정책을 재시도 가능한 구조로 바꾸는 것, 그리고 서버 Word 파서가 특정 문서에서 실패하는 원인을 서버 경로에서 추적하는 것입니다.