개발 취재 노트
AI 글 생성 파이프라인, 테스트는 다 통과했는데 실제로 돌리면 통과 글이 0개일 때
테스트가 모두 통과해도 실제 글의 통과율은 따로 확인해야 한다. 주제 선정 → 웹 조사 → 집필 → 심사를 거치는 IT 문제해결 글 생성 파이프라인에서 테스트 9개가 통과했지만, 실제 Claude CLI 실행은 후보 31개 중 통과 0개로 보고됐다. 출처 충돌 단계에서 27개가 탈락했고, 모델 심사에 간 4개는 고쳐 쓰기(repair)를 한 번씩 거친 뒤에도 탈락했다 .
결론
- 단계별 탈락 수부터 확인한다. 이번 실행의 대부분은 repair 대상인 집필 후 심사에 도달하기 전에
evidence단계에서source_conflict로 탈락했다 . - repair 시도 수와 회복 수를 나눠 본다. 보고된 심사 탈락 4개는 모두
repair_attempts:1이었지만 회복은 0개였다. 실행됐다는 기록만으로 수정 품질이나 구현 전체의 정상 여부를 확정할 수는 없다 . - 테스트 종류를 구분한다. 일반 픽스처 엔드투엔드 실행은 repair를 타지 않았지만, 별도 repair 테스트는 실패 후 수정 경로를 검증했다. 둘 다 실제 모델의 품질 개선을 입증하지는 않는다 .
이 사건은 해결 완료 사례가 아니다. 다음 확인 대상은 출처 충돌로 탈락한 근거가 실제 모순인지, 기기·버전 조건 차이인지다. 조건 차이의 오탐 가능성은 제기됐지만, 27개를 개별 재검증한 결과는 남아 있지 않다 .
수치는 한 번의 실행에 대한 assistant의 요약 보고다. 원시 실행 로그를 독립적으로 확인한 결과가 아니며, 이전 실행과 조건이 같았는지도 확인되지 않았다. 따라서 변경의 효과 크기나 IT 글 자동화 전체의 가능성을 이 숫자만으로 판정하지 않는다 .
상황
만들던 것은 IT 문제해결 글을 자동으로 생성하는 프로그램이다. discover → research → write → 게이트 구조로 후보 주제를 고르고, 웹에서 근거를 모아 글을 쓴 뒤 심사한다. 통과한 글은 저장하며, 실패 지점에 따라 집필 전에 탈락할 수도 있다 .
이번 사건과 관련된 검사는 다음과 같다.
| 검사 | 역할 |
|---|---|
| 주제 점수 | minimum_topic_score 미만이면 웹 조사·집필 전에 탈락 |
근거 검사(evidence) | 조사한 출처의 충돌(source_conflict)로 탈락 |
모델 심사(model_review) | 집필 후 근거와의 모순이나 지어낸 구체값 등을 검사 |
앞선 실행은 통과 2개·폐기 309개로 집계됐다. 폐기 중 주제 점수 미달이 290개, 모델 심사가 10개, 근거 단계가 9개였다. 당시에는 주제 문턱이 가장 큰 탈락 지점이었다 .
증상
이 집계를 바탕으로 다음 변경을 넣었다 .
minimum_topic_score를 75에서 68로 낮췄다.revise()에 심사 지적 사항과 근거를 전달해 문제 부분을 최소 수정하게 했다.max_repair_attempts기본값은 1이다.- 심사 기준을 조정해 근거와 모순되거나 지어낸 구체값은 탈락시키고, 근거로 반박되지 않는 안전한 상식은 허용하도록 했다.
이어 조사 전에 유사 제목을 거르는 maximum_title_similarity: 0.8도 추가했다. 기존 6개, repair 관련 2개, 중복 검사 1개를 합친 테스트 9개가 모두 통과했다고 보고됐다 .
하지만 실제 Claude CLI 실행 결과는 43분 동안 후보 31개, 통과 0개였다. 탈락 내역은 evidence 27개, model_review 4개, 주제 점수 미달 0개였다 .
원인 분석
1. 테스트가 확인한 범위와 실제 품질은 달랐다
일반 픽스처는 심사를 통과하는 설정이었다. 엔드투엔드 결과는 ready 2 / rejected 1, 통과 글의 repair_attempts [0,0]으로 보고됐다. 따라서 이 실행은 repair의 효과를 보여주지 않는다 .
반면 별도 repair 테스트에는 첫 심사 실패 후 수정해 통과하는 시나리오가 있었다. 완료 보고에는 “repair로 심사 실패 글 회복”, “repair=0이면 즉시 폐기” 두 테스트가 명시돼 있다. repair 경로를 전혀 테스트하지 않은 것은 아니다. 다만 정해진 테스트 시나리오의 통과와 실제 모델이 쓴 글의 회복은 다른 확인 항목이다 .
2. 가장 많은 탈락은 repair보다 앞에 있었다
실제 실행에서는 27개가 출처 충돌로 탈락했다. 당시 구현 설명에 따르면 repair는 집필 후 평가 실패를 고치는 기능이며, 이 근거 단계의 탈락에는 적용되지 않았다 .
결과 보고는 evidence_gate가 conflicts를 하나라도 받으면 폐기한다고 설명했다. 기기·버전마다 메뉴가 다른 것을 출처의 모순으로 과잉 판정했을 가능성도 함께 제기했다. 그러나 실제 모순과 조건 차이의 비율은 확인되지 않았다. 출처 충돌이 가장 큰 탈락 사유였다는 것과, 그 판정이 잘못됐다는 것은 구분해야 한다 .
3. 한 번의 repair로 회복된 글은 없었다
심사 탈락 4개는 모두 repair를 한 번 거쳤다. 보고된 오류 예시는 Google 근거의 사실을 Apple·Samsung 것처럼 표기한 출처 귀속 오류와, Pixel 한정 사실의 일반화였다 .
당시 해석은 출처가 뒤섞여 최소 수정 한 번으로 해결되지 않았다는 것이었다. 이 기록으로 확인할 수 있는 범위는 이번 4개가 한 번의 수정 후에도 탈락했다는 것이다. 이런 오류는 언제나 한 번으로 못 고친다거나, 두 번이면 해결된다고 단정할 수 없다 .
다음에 확인할 것
이 세션의 마지막 결과 보고에도 수율을 회복한 후속 실행은 없다. 주제 점수 미달은 이번 실행에서 0개였지만, 이전 실행과 조건이 같다는 근거가 없어 이를 변경 효과의 크기로 환산할 수는 없다 .
다음은 이 글의 검증 제안이며, 완료된 해결책이 아니다.
- 출처 충돌 탈락 건의 근거를 읽고 실제 모순과 기기·버전 조건 차이를 구분한다.
- 조건 차이의 오탐이 확인되면 조사 프롬프트와 근거 검사 규칙을 수정하고 다시 측정한다.
- repair 전후 글에서 출처 귀속과 적용 범위가 어떻게 바뀌었는지 비교한다. 횟수만 늘려 효과를 가정하지 않는다.
1단계 예시: 충돌 분류표
아래 표는 분류 방법을 보이려고 새로 만든 합성 예시다. 세션의 27개 실제 근거가 아니다. 탈락한 후보마다 충돌한 두 출처의 주장을 나란히 적고, 기기·버전 같은 조건을 뽑아 비교한다.
| 후보 | 출처 A 주장 | 출처 B 주장 | 조건 비교 | 판정 |
|---|---|---|---|---|
| x1 | "설정 > 블루투스에서 기기 삭제" (Pixel, Android 14) | "설정 > 연결된 기기에서 삭제" (Galaxy, One UI 6) | 기기가 다름 | 조건 차이 (오탐) |
| x2 | "재부팅 후 페어링 유지됨" (같은 기기·같은 버전) | "재부팅 후 페어링 초기화됨" (같은 기기·같은 버전) | 조건 같음, 결론 반대 | 실제 모순 |
| x3 | "펌웨어 1.2에서 해결" | "업데이트 후에도 재현" (펌웨어 버전 미기재) | 한쪽 조건 없음 | 판정 보류 |
판정 규칙은 세 가지다.
- 조건이 서로 다르고 각 주장이 자기 조건 안에서 성립하면 조건 차이다. 글에 조건을 명시하면 통과시킬 수 있다.
- 조건이 같은데 결론이 반대이면 실제 모순이다. 탈락이 맞다.
- 조건이 한쪽에만 적혀 있으면 보류다. 조건을 다시 조사해서 위 둘 중 하나로 옮긴다.
27개 전부를 이렇게 분류하면 조건 차이 n / 실제 모순 m / 보류 k가 나온다. 이 비율이 나와야 근거 검사 규칙을 고칠지 판단할 수 있다. 위 x1~x3은 방법의 예이며 실제 비율과 무관하다.
집계 코드
집계 예시도 합성 입력이다. 세션의 실제 코드나 리포트 스키마가 아니며, 아래 필드가 있다고 가정한다.
| 필드 | 뜻 |
|---|---|
id | 후보 식별자 |
status | 최종 상태. ready(통과) 또는 rejected(탈락) |
stage | 탈락한 단계(topic_score, evidence, model_review). 통과 후보는 done |
repair_attempts | 고쳐 쓰기를 시도한 횟수 |
from collections import Counter
results = [
{"id": "c01", "status": "rejected", "stage": "evidence", "repair_attempts": 0},
{"id": "c02", "status": "rejected", "stage": "evidence", "repair_attempts": 0},
{"id": "c03", "status": "rejected", "stage": "evidence", "repair_attempts": 0},
{"id": "c04", "status": "rejected", "stage": "model_review", "repair_attempts": 1},
{"id": "c05", "status": "rejected", "stage": "model_review", "repair_attempts": 1},
{"id": "c06", "status": "ready", "stage": "done", "repair_attempts": 1},
{"id": "c07", "status": "ready", "stage": "done", "repair_attempts": 0},
{"id": "c08", "status": "rejected", "stage": "topic_score", "repair_attempts": 0},
]
rejected = [r for r in results if r["status"] == "rejected"]
by_stage = Counter(r["stage"] for r in rejected)
repaired = [r for r in results if r.get("repair_attempts", 0) > 0]
recovered = [r for r in repaired if r["status"] == "ready"]
print(by_stage)
print(f"repair 시도 후보 {len(repaired)} / 회복 후보 {len(recovered)}")
예상 출력은 다음과 같다.
Counter({'evidence': 3, 'model_review': 2, 'topic_score': 1})
repair 시도 후보 3 / 회복 후보 1
읽는 법: 탈락 6개 중 3개가 evidence에서 멈췄고, repair를 시도한 3개(c04·c05·c06) 중 c06 하나만 통과했다. 세션의 실제 실행은 이 형태로 보면 evidence 27, model_review 4, repair 시도 4, 회복 0으로 보고됐다 .
테스트로 실패 후 수정 흐름을 확인하고, 실제 실행에서는 단계별 탈락과 수정 후 회복을 별도로 측정한다. 이 둘을 함께 봐야 다음에 고칠 지점을 정할 수 있다.
취재 후기 — 초록불 아홉 개와 0개의 글
repair_attempts, 고쳐 쓰기를 시도한 횟수를 보자. 0인지 1인지 확인하면 수정 시도가 기록됐는지는 구분할 수 있어 .repair_attempts:1이야. 한 번씩 시도했지만 통과한 글은 없다고 나와 .[0,0]이었어. 별도 repair 테스트에는 첫 심사 실패 후 수정해서 통과하는 시나리오가 있었고 .evidence, 조사한 근거를 검사하는 단계야. 27개 모두 source_conflict, 출처 충돌로 탈락했다고 나와 .conflicts가 하나라도 있으면 폐기한다고 나와. 기기마다 메뉴가 다른 것까지 충돌로 잡았을 가능성도 적혀 있어 .