품질검사 판정을 결정론적으로: LLM에는 파일 하나의 판단만, 나머지는 코드와 다수결로
재실행마다 파일 판정이 11~58% 바뀌던 품질검사 파이프라인을 파일별 단일 판정, 결정형 규칙, 코드 집계로 다시 짰습니다. 규칙 시험에서 판정 변동 0/23을 얻었고, 실파일에서 남은 흔들림은 같은 텍스트 10회 실험으로 공유 추론 서버까지 좁힌 뒤 N회 다수결로 종합 판정을 안정시켰습니다.
Key Results
- 0/23
- 판정이 바뀐 케이스판정 규칙 재작성 후, OCR 요약 텍스트 입력 실데이터 23건을 같은 입력으로 3회. Qwen3.6-27B
- 9 : 1
- 동일 텍스트 10회 판정 분포공유 vLLM 서버, 같은 OCR 텍스트를 판정 노드에 10회 입력, 경계 파일 1건
- 3.7초
- 파일당 판정 시간도구 호출 제거 후 첨부 직독, 파일당 LLM 호출 1.0회, 29파일 요청
대형 유통 고객의 상품 품질검사 에이전트는 상품마다 제출된 시험성적서, 케어라벨, 수입신고필증 같은 서류를 파일별로 읽고 합격(01), 불합격(02), 해당 없음(03), 보류(10) 코드를 낸 뒤, 상품 단위 종합 코드를 만듭니다. 같은 서류를 다시 넣었을 때 다른 코드가 나오면 그 판정은 믿고 쓸 수 없습니다. 기존 워크플로우의 실행 기록을 분석해 보니 재실행 시 파일 판정이 바뀌는 비율이 11~58%였습니다.
2026년 6월 15일부터 23일까지 이 파이프라인을 다시 설계했습니다. 흔들림을 한 덩어리로 보지 않고, 파일이 어떤 경로로 판정되는지(구조), 어떤 규칙으로 판정되는지(규칙), 판정을 어떻게 합치는지(집계)를 나눠 각각을 결정론적으로 만들었고, 그래도 남은 흔들림은 별도의 실험으로 추론 서버까지 좁혔습니다.
이 글은 그 순서대로, LLM에게는 "파일 하나를 보고 코드 하나를 내는" 판단만 남기고 결정할 수 있는 나머지를 코드로 옮긴 과정, 그리고 마지막에 남은 비결정성을 운영에서 어떻게 다루기로 했는지를 정리합니다.
출발점: 34개 노드의 오케스트레이터와 11~58%의 흔들림
원래 파이프라인은 적재, 상품기술서 추출, 파일 검증, 대조의 단계를 워크플로우 도구 6개, foreach, 라우터, 약 30개의 연결 노드로 엮은 중첩 오케스트레이터였습니다. 전용 QA 노드와 하네스 노드가 섞여 있었고, Judge의 필수 기준은 방어용 메타 규칙 때문에 모두 해제된 상태였으며, 모델 설정 값에 [object Object]라는 문자열이 들어가 있는 버그도 있었습니다. 원본 시스템 프롬프트는 세 단계 합쳐 약 90KB였습니다.
원본 실행 기록에서 측정한 기준선은 이랬습니다. 파일 판정 흔들림 11~58%, Judge 재시도율 16.8~58.2%(대부분 정답에 형식을 문제 삼은 오기각), 라우팅 누락으로 생긴 거짓 보류 1건 이상. 앞서 6월 11일 분석에서는 일부 문서 유형이 판정 단계로 라우팅되지 않아 적재 시 깔린 보류 placeholder가 그대로 남는 경로도 확인했습니다.
제약도 분명했습니다. 판정 모델은 다른 사용자와 함께 쓰는 vLLM 서버의 Qwen3.6-27B였고, 고객 환경의 파드는 이 서버에 직접 닿지 못해 터널을 거쳤습니다. 6월 19일 이후의 작업 조건은 "코드 수정 없이 노드, 설정, 프롬프트만으로"였고, 고객 환경의 인그레스는 실행 스트림(SSE) 응답을 약 298초에서 끊었습니다.
흔들림을 세 층으로 나눠 본 이유
돌아보면 흔들림은 세 층에서 나왔고, 층마다 처방과 검증 방법이 달랐습니다. 한꺼번에 줄이려 했다면 무엇이 효과를 냈는지 가릴 수 없었을 것입니다.
구조 층 파일마다 다른 경로를 탄다, 일부 유형은 판정 단계에 도달하지 않는다
→ 모든 파일이 같은 판정 경로를 지나게 만들고, 누락 보류가 사라지는지 본다
규칙 층 원본 프롬프트의 모호한 표현이 경계 문서에서 판정을 가른다
→ 규칙을 결정형으로 다시 쓰고, 같은 입력 N회 반복으로 변동을 잰다
집계 층 LLM이 6종 코드의 개수 세기와 합산을 맡는다
→ 집계를 코드로 옮기고, 같은 파일 판정에서 같은 종합이 나오는지 본다구조 층의 처방은 오케스트레이터 해체였습니다. 파일별 판정 에이전트 하나와 종합 단계 하나의 단일 선형 흐름으로 줄이고, 모든 파일이 같은 판정을 통과하게 했습니다. 라우팅이 없으니 라우팅 누락형 보류는 구조적으로 생길 수 없습니다. 원본의 34개 노드 오케스트레이터는 설계상 6개 노드로 줄었습니다.
규칙 층: 54개 규칙과 23건 실측
원본 시스템 프롬프트에서 절대 규칙 54개를 추출해 모호한 표현을 결정형으로 다시 썼습니다. 혼용률 허용 오차 분기, 등급 표기 정규화(3-4급은 3.5로 계산), 기준이 없는 항목은 합격 승계, 진위 검증과 판정의 분리, 재질 일치 확인 같은 것들입니다.
Judge도 다시 설계했습니다. 필수 기준은 1개로 줄이고, 점수는 1, 0.8, 0의 세 단계 이산 점수로, 채점 대상은 제출 페이로드로 한정했으며, 기준 이름은 정규화 과정에서 깨지지 않도록 영문으로 바꿨습니다. 결정형 케이스 10건으로 Judge를 격리해 시뮬레이션하자 초기 오기각이 10%였습니다. 보류가 정답인 케이스를 근거와 완결성 축이 "작문이 불완전하다"고 감점한 것인데, 운영에서 보던 재시도 폭주와 계속 보류의 패턴이 그대로 재현된 것이었습니다. Judge 축에 "판독 불가와 해당 없음은 보류의 정당한 사유이므로 감점하지 않는다"를 명시하자 오기각이 0%가 됐습니다(해당 케이스 0.72 → 0.98).
측정은 합성 케이스가 아니라 실데이터로 했습니다. 원본 실행 로그 34건에서 실 픽스처 23건을 뽑았고, 입력에서는 원본 판정의 결론이 새지 않도록 "불합격", "PASS" 같은 결론 표현을 제거하고 실측값과 기준만 남겼습니다. 진위 검증 결과는 실제 파이프라인에서도 도구의 산출물이므로 입력으로 제공했습니다. Qwen3.6-27B(temperature 0.05)로 같은 입력을 3회씩 실행한 결과, 정답 21/23, 판정이 바뀐 케이스 0/23이었습니다. 틀린 2건은 입력에 실제 판독값이 없는 라우팅 누락 아티팩트와 정답 라벨이 없는 경계 사례였고, 둘 다 3회 모두 같은 코드를 냈습니다.
규칙을 더 조이는 것이 항상 낫지는 않았습니다. "기준 없음이면 합격" 규칙을 강화한 다음 버전에서는 오히려 두 파일이 01과 10 사이에서 흔들리고 재시도가 늘었습니다. 그 두 건은 파일 판정이 아니라 종합 단계의 교차 검증이 다룰 문제라고 보고, 파일 판정은 안정성을 우선한 이전 버전으로 되돌렸습니다.
이 0/23은 OCR 요약 텍스트를 입력으로 넣은 판정 규칙 시험의 결과입니다. 실제 파일을 내려받아 이미지와 스캔 문서까지 읽는 전체 파이프라인의 결과는 아니고, 그 차이가 뒤에서 다시 문제가 됩니다.
캔버스 안의 파일별 판정 경로
6월 19일부터는 규칙을 실제 파일 파이프라인으로 옮겼습니다. 판정 워크플로우는 파일 하나를 받아 하네스 노드가 첨부 파일을 직접 보고 한 줄로 답하는 구조입니다.
요청 워크플로우 입력(요청 번호) → 키 추출 → S3 파일 목록 → foreach(판정 워크플로우, 재시도 3) → 출력
판정 워크플로우 loop item(파일 경로) → 하네스(files·images·text 포트 직결, 도구 없음)
→ "FILERESULT|파일번호|코드|사유" 한 줄
클라이언트 청크 실행 · 병합 · 결정적 종합 · N회 다수결이 구조에 이르기까지 네 가지를 고쳤습니다.
첫째, 도구를 뺐습니다. 기존 판정은 세션 저장소에서 필드를 조회하는 도구를 썼는데, 파일 모드에서는 세션이 채워지지 않아 조회가 매번 실패했고, 모델은 파일당 2~3회 호출을 낭비하며 약 17초를 썼습니다. 그런데 파일은 이미 하네스의 files와 images 포트로 직접 첨부되고 있었습니다. 도구를 빼고 "첨부를 보고 한 줄로 답하라"로 바꾸자 파일당 3.7초, LLM 호출 1.0회가 됐고, 17/29에서 멈추던 29파일 요청이 29/29까지 완주했습니다.
둘째, 인그레스 상한을 우회했습니다. 64파일 요청이 47/64에서 끊겼는데, 에러나 연결 해제가 아니라 인그레스가 약 298초에 응답을 끊은 것이었습니다. 처음에는 이미지 처리가 병목이라고 보고 최대 이미지 수를 4에서 2로 줄였지만 효과가 없었습니다. 병목은 비전이 아니라 파일 다운로드와 텍스트 추출이었습니다. foreach는 순차 실행이라 병렬도를 높일 수도 없었습니다. 그래서 파일명 앞자리 숫자로 요청을 3청크로 나눠 실행하고 결과를 병합했습니다. 청크당 30파일 안팎, 최대 203초로 상한에 여유가 생겼고, 숫자로 시작하지 않는 메타데이터 파일이 자동으로 빠지는 부수 효과도 있었습니다.
셋째, 실패를 감지하게 했습니다. 터널이 간헐적으로 끊기면 SSE 이벤트가 비고 워크플로우가 중단되는데, foreach의 재시도로는 연결 단절을 살릴 수 없었습니다. 마지막 처리 번호가 매칭된 파일 수보다 작거나 연결이 끊기면 청크를 자동 재시도(최대 4회)합니다. 더 위험한 것은 조용한 실패였습니다. 터널과 프록시가 함께 내려간 날 하네스 호출 27건이 모델 주소 없이 출력 0으로 끝났는데 foreach는 "완료"로 표시했습니다. 실행 전에 모델 브리지 상태를 확인하는 헬스체크를 넣었습니다.
넷째, 집계를 코드로 옮겼습니다. 캔버스 안의 LLM 종합 노드는 불합격(02)을 한 건 세고도 합격 종합 코드(20)를 내는 등 정해진 산술조차 비결정적으로 어겼고, 무거운 종합 호출은 이벤트 공백을 만들어 클라이언트 연결이 끊기는 원인이 되기도 했습니다. 종합은 결정적 코드로 대체했습니다.
# 종합 코드 결정 규칙(의사 코드)
def total_result_cd(file_codes, missing_required):
if any(cd in ("02", "10") for cd in file_codes):
return "10" # 불합격 또는 보류가 하나라도 있으면 보류
if missing_required:
return "50" # 필수 서류 누락
if file_codes and all(cd == "03" for cd in file_codes):
return "40" # 전부 해당 없음
return "20" # 합격누락 서류 분석과 혼용률·제조국 교차 검증도 같은 코드가 판정 사유를 파싱해 처리합니다.
규칙을 고친 뒤에도 남은 흔들림
실파일로 넘어오면서 한 번 틀린 결론을 냈습니다. 6월 18일까지는 케어라벨 같은 작은 이미지를 비전으로 매번 다시 읽는 것이 흔들림의 원인이라고 보고, 이미지를 OCR한 텍스트를 세션에 주입하는 실험 경로를 만들었습니다. 한 요청에서 두 번 연속 파일별 판정이 같게 나오자 "변동 0에는 결정적 텍스트가 필요하다"고 정리했습니다. 이 경로는 고객 파드가 모델 서버에 직접 닿지 못해 개인 PC를 거쳐야 했고, 큰 페이로드에서는 명령줄 인자 한도를 넘어 조용히 실패했기 때문에 배포할 수 없는 실험 도구였습니다.
6월 19일 비전 입력 그대로 1차 실행의 흔들림을 파일 단위로 추적하자 대부분이 규칙의 빈틈으로 설명됐습니다. 판독 경계에서 03과 10을 오간 파일은 보류 조건이 넓어서, 재질분석서가 01과 03을 오간 것은 재질분석을 성적서로 볼지 정해지지 않아서, 케어라벨 도안이 01과 03을 오간 것은 도안을 표시사항으로 볼지 정해지지 않아서였습니다. 다른 파일의 값과 비교하는 환각성 교차 검증이 01과 02를 오가게 만든 경우도 있었습니다. 그래서 규칙 네 가지를 프롬프트에 결정형으로 넣었습니다.
- 성적서류(견뢰도, pH, 혼용률, 유해물질, 재질분석, 물성, 조성)는 01 또는 02만 낸다.
- 명백한 미달만 02로 하고, "기준 없음, 수치 없음, 비교 불가"는 02·03·10의 사유로 쓰지 않는다. 다른 파일이나 예상치와 비교하지 않고, 기준값과 같거나 높으면 충족이다.
- 참조류는 읽지 못해도 03이고, 10은 첨부가 없거나 파일이 손상됐을 때만 낸다.
- 케어라벨과 표시사항은 도안이나 시안이어도 01 또는 02로 판정한다.
규칙을 결정형으로 다듬은 뒤 7개 요청을 3회씩 실행하자 6개는 판정 변동이 0이었습니다. 비전 판독을 그대로 둔 채 얻은 결과였으므로, 18일의 결론은 부분적으로 틀렸던 셈입니다. 그때까지 흔들림의 주원인은 판독이 아니라 규칙의 모호함이었습니다. 시험성적서가 49개인 64파일 요청 하나에만 변동이 1건 남았는데, 매 실행 다른 파일(견뢰도 경계값, 케어라벨 표시 수치)이었습니다. 특정 파일의 규칙 버그가 아니라 경계 수치 판독의 흔들림이었습니다.
요청마다 5회씩 실행해 보니 모양이 분명해졌습니다. 텍스트가 분명한 PDF는 흔들리지 않았고, 경계 파일만 바뀌었습니다. 기준 충족이 아슬아슬한 성적서는 01과 03 사이, 판독이 어려운 도안과 스캔 문서는 03과 10 사이를 오갔습니다. 이미지 강화나 분류 규칙을 바꾸면 한 요청의 흔들림이 잡히는 대신 다른 요청에서 새로 생겼습니다. 바뀌는 것은 "어느 파일이 흔들리는지"뿐이고 만장일치율은 85~100% 언저리에서 그대로였습니다. 손잡이가 총량을 못 바꾼다면 원인은 손잡이 밖에 있다고 보고, 고치는 대신 원인을 좁히는 실험으로 방향을 바꿨습니다.
가설은 순서대로 기각됐습니다. 이미지를 매번 다르게 읽는다는 가설은 OCR을 한 번만 하고 그 텍스트를 고정해 판정해도 흔들림이 남으면서 기각됐습니다. 그다음 완전히 같은 OCR 텍스트를 같은 프롬프트로 판정 노드에 10회 넣었고, 합격 9회, 불합격 1회가 나왔습니다. 입력이 글자 하나까지 같으니 파일 다운로드, 비전 판독, OCR은 원인에서 빠지고 추론 자체가 남습니다.
같은 날 실험 하나가 그림을 더 구체적으로 만들었습니다. 하네스는 판정 지시 외에도 역할, 규칙, 도구, 단계 설명을 합친 약 4,000자의 프롬프트를 자동으로 붙이는데, 이 경로의 판정은 우리 쪽 부하가 낮을 때도 경계 파일에서 5 대 5로 갈렸습니다. 반면 짧은 지시와 두 글자 코드만 출력하는 직접 호출은 병렬 10개로 20회 호출해도 모두 같았습니다. 긴 프리필과 긴 출력이 수치 노이즈가 경계 토큰을 뒤집을 기회를 늘린다고 해석했습니다. 문제는 짧은 출력의 정확도였습니다. 추론할 자리가 없어지자 4.5급이 기준 3.5급을 넘는데 불합격을 주거나, 시험기관이 종합 합격으로 적은 서류에 불합격을 주는 오판이 나왔고, 프롬프트를 보강해도 고쳐지지 않았습니다.
마지막 실험은 같은 긴 프롬프트와 300토큰 안팎의 사유 출력을 외부의 비공유 전용 엔드포인트에서 10회 실행한 것입니다. 10회 모두 같았습니다. 이 결과로 보면 긴 출력만으로는 흔들림이 생기지 않았고, 공유 서버라는 조건이 함께 필요했습니다. 우리가 받아들인 설명은 공유 vLLM 서버의 연속 배칭입니다. 함께 묶이는 다른 사용자의 요청 구성에 따라 부동소수점 리덕션 순서가 바뀌고, temperature 0의 greedy 디코딩에서도 근소하게 갈리는 경계 토큰의 argmax가 뒤집힌다는 것입니다. 다만 외부 엔드포인트는 같은 Qwen 계열이지만 다른 모델(qwen-plus)이었고 출력 확률을 직접 비교하지는 않았으므로, 관측과 가장 잘 맞는 설명이지 단독으로 증명된 원인은 아닙니다.
운영안: 결정성보다 정확도, 종합은 다수결로
남은 비결정성을 다루는 방법을 네 가지로 비교했습니다.
| 선택지 | 장점 | 단점 | 판단 |
|---|---|---|---|
| 비전 판정 + N회 다수결 + 코드 종합 | 정확도 유지, 9:1 수준의 흔들림은 다수결로 수렴 | 실행 비용 N배, 호출 단위 변동은 남음 | 채택 |
| OCR 고정 + 짧은 직접 판정 | 호출 단위 변동 0, 부하에도 견고 | 추론 여지가 없어 비교 판정 오판, OCR 손실 | 기각 |
| 외부 전용 엔드포인트 | 긴 추론 출력에도 변동 0 | 고객 문서에서 나온 텍스트가 외부로 전송됨 | 승인 전 보류 |
| 전용 vLLM 인스턴스(비공유 배칭) | 정확도와 결정성 동시 | 인프라 확보 필요, 이번 작업 범위 밖 | 다음 단계 |
판단의 기준은 이 판정이 무엇에 쓰이는가였습니다. 파이프라인의 최종 산출물은 상품 단위 종합 코드와 종합 메시지이고, 파일별 코드는 그 근거입니다. 호출 단위의 문자 그대로의 변동 0을 얻으려고 정확도를 포기하면, 흔들리지 않는 오답을 얻게 됩니다. 반대로 흔들림이 9:1 수준이라면 같은 요청을 N회 실행해 파일별로 다수결을 내고, 그 결과를 결정적 코드로 종합하면 종합 코드는 안정됩니다. 외부 전용 엔드포인트는 기술적으로 가장 깔끔했지만, 이미지는 우리 인프라에 남기도록 설계해도 OCR 텍스트 자체에 고객 정보가 포함되므로 고객과 법무의 승인 없이는 운영에 쓸 수 없었습니다. 그래서 외부 엔드포인트용 OCR 실험 워크플로우는 정리하고, 라이브에는 요청 워크플로우와 판정 워크플로우 두 개만 남겼습니다.
결과와 남은 과제
| 지표 | 이전 | 이후 | 측정 조건 |
|---|---|---|---|
| 판정 변동(규칙 시험) | 11~58% | 0/23 | 이전은 원본 실행 기록, 이후는 OCR 요약 텍스트 실데이터 23건 3회(Qwen3.6-27B). 측정 방식이 다름 |
| Judge 오기각 | 10% | 0% | 결정형 케이스 10건, 격리 Judge 시뮬레이션 |
| 파일당 판정 | 2~3회 호출, 약 17초 | 1.0회 호출, 3.7초 | 도구 제거 후 첨부 직독 |
| 29파일 요청 완주 | 17/29 | 29/29 | 같은 요청 |
| 64파일 요청 | 47/64에서 절단 | 3청크 모두 완주, 청크 최대 203초 | 인그레스 약 298초 상한 |
| 실파일 판정 변동 | 경계 파일 흔들림(1차 실행) | 7개 요청 중 6개 변동 0 | 규칙 결정화 후 요청별 3회 |
| 종합 코드 | LLM 노드의 산술 오류 | 7개 요청 모두 안정 | N=5 다수결 + 결정적 종합 |
N=5 측정에서 파일별 만장일치는 요청에 따라 27/27, 11/11, 17/18, 23/25, 27/32, 11~13/16, 55/61이었고, 갈린 파일은 대부분 4:1이라 다수결이 쉽게 수렴했습니다.
배운 것은 두 가지입니다. 하나는 LLM에게 맡길 일의 경계입니다. 경로 선택과 산술은 LLM이 할 이유가 없었고, 코드로 옮기자 그 층의 흔들림은 사라졌습니다. 다른 하나는 원인 분리 실험의 순서입니다. 조정 파라미터가 흔들리는 대상만 옮길 때 입력을 글자 단위로 고정한 반복 실험이 원인을 가장 빨리 좁혔습니다.
남은 과제도 있습니다. N회 다수결은 아직 클라이언트 스크립트에 있어 워크플로우로 옮겨야 합니다. 모델 브리지가 개인 PC의 터널에 의존하고 있어 상주 서버로 옮겨야 데모와 운영에 쓸 수 있습니다. 호출 단위 변동 0이 필요하다면 비공유 배칭의 전용 vLLM 인스턴스를 확보해야 하고, 외부 전용 엔드포인트는 데이터 거버넌스 승인이 전제입니다.