10초 안에 성공할 가능성을 평가한다면, 종료 실패를 관측 중단처럼 취급해 분모에서 빼면 안 됩니다. 전체 실행을 같은 기준으로 나눠 기한 내 성공, 종료 실패, 아직 진행 중을 함께 보고해야 합니다. 단, 각 실행이 종료되었거나 10초 시점까지 관측되어 그때의 상태를 확인할 수 있어야 합니다.
앞선 글에서는 시간 제한과 오른쪽 중도절단, 제한평균을 구분했습니다. 이번에는 그 글에서 남긴 경계 하나를 다룹니다. “아직 끝나지 않았다”와 “실패로 끝났다”는 다릅니다. 완료 시간 분석에 쓰던 관측 중단 처리를 모든 실패 로그에 적용하면, 빨리 실패하는 에이전트가 오히려 유리해질 수 있습니다.
1. 먼저 질문을 고정한다: 시작한 실행 중 몇 개가 10초 안에 성공했나
평가 단위는 한 번 시작한 실행이고, 시간의 원점은 그 실행의 시작입니다. 이 글의 성공 시각은 결과물 생성 시각이 아니라 사전에 정한 검증까지 통과한 시각입니다. 검증에 걸린 시간도 10초에 포함합니다. 종료 실패는 같은 실행 안에서 더 이상 성공할 수 없도록 끝난 결과로 정합니다. 아래 예시는 원리를 설명하기 위한 합성 데이터이며, 실제 모델이나 서비스 성능을 측정한 결과가 아닙니다.
- 성공: 10초 이내에 요구된 최종 상태가 확인되었다.
- 종료 실패: 10초 이내에 실패로 끝났고, 그 실행은 재개되지 않는다.
- 진행 중: 10초까지 관측했지만 성공도 종료 실패도 아직 발생하지 않았다.
여기서 10초는 실행을 강제로 죽이는 제한이 아니라 평가용 스냅샷입니다. 10초 뒤에 성공할 수도 있는 실행은 그 시점의 진행 중에 해당합니다. 반면 3초에 로그 수집이 끊겨 10초 상태를 모른다면 진행 중이라고 쓸 수 없습니다. 그것은 관측 누락이며 이 글의 단순 집계 조건을 충족하지 않습니다.
실행이 “성공했다”고 말하는 것만으로 성공을 확정하지도 않습니다. 예를 들어 파일 생성 과제라면 파일 존재와 요구사항 충족을 확인해야 합니다. Anthropic의 에이전트 평가 설명도 실행 기록과 환경에 남은 최종 결과를 구별합니다. 이 글의 성공 판정 역시 그런 외부 검증 결과에 붙이는 라벨입니다. 출처: Anthropic, Demystifying evals for AI agents
2. 성공과 종료 실패는 경쟁 사건, 진행 중은 남아 있는 상태다
같은 실행에서 성공으로 끝나면 이후 종료 실패로 끝나는 사건은 세지 않습니다. 종료 실패로 끝나면 이후 성공 사건도 발생할 수 없습니다. 이렇게 하나의 종료 사건이 다른 종료 사건을 막는 구조를 경쟁위험(competing risks)으로 볼 수 있습니다. 정확히는 성공과 종료 실패가 두 경쟁 사건이며, 진행 중은 세 번째 사건이 아니라 아직 어느 사건도 일어나지 않은 상태입니다.
이 구분은 용어 문제가 아닙니다. 실패로 실행이 끝났다는 정보를 “그 이후를 보지 못했다”로 바꾸면, 원래 시스템에서 성공할 기회가 사라졌다는 사실이 지워집니다. 경쟁 사건을 검열로 처리한 일반적인 생존곡선의 보완값을 실제 성공확률처럼 읽는 것이 위험한 이유입니다. scikit-survival 0.28.0의 공식 경쟁위험 안내에서도 이 문제와 누적발생확률의 구분을 설명합니다.
다만 모든 통계 분석에서 경쟁 사건을 검열로 부호화하면 틀린다는 뜻은 아닙니다. 원인별 위험률처럼 다른 질문을 다루는 분석에서는 그렇게 부호화할 수 있습니다. 여기서 막으려는 것은 그 결과를 곧바로 “원래 시작한 실행이 기한 안에 성공할 확률”로 해석하는 일입니다.
복잡한 추정기는 아직 필요하지 않습니다. 각 실행의 10초 상태를 모두 알면 세 칸을 직접 세면 됩니다. R survival 패키지의 저자 문서도 검열이 없는 경우 시점별 상태 확률 계산이 단순 집계가 된다고 설명합니다. 여기서는 알려진 종료 사건 또는 10초까지의 관측으로 각 상태를 확인합니다. 출처: Multi-state models and competing risks, 2026년 9월 9일 문서
3. 실패를 뺐더니 A가 더 좋아 보이는 합성 100회 예제
같은 조건에서 A와 B를 각각 100회 실행했다고 가정합니다. 계산을 투명하게 보이기 위해 모든 종료 실패는 2초에, 모든 성공은 6초에 발생하도록 구성했습니다. 나머지는 10초 시점까지 진행 중임을 확인했습니다. 두 모델의 실제 우열을 주장하는 실험이 아니라 분모 오류를 재현하는 예시입니다.
| 10초 시점의 상태 | A: 전체 100회 | B: 전체 100회 |
|---|---|---|
| 기한 내 성공 | 60회 · 60% | 70회 · 70% |
| 종료 실패 | 30회 · 30% | 10회 · 10% |
| 진행 중 | 10회 · 10% | 20회 · 20% |
| 합계 | 100회 · 100% | 100회 · 100% |

처음 정한 질문의 답은 A 60%, B 70%입니다. 그런데 종료 실패를 평가 대상에서 빼면 A는 60 ÷ 70 = 85.7%, B는 70 ÷ 90 = 77.8%가 됩니다. 성공한 실행을 하나도 늘리지 않았는데 순서가 뒤집힙니다. 종료 실패가 많은 A의 분모가 더 크게 줄었기 때문입니다.
85.7%라는 나눗셈 자체가 틀린 것은 아닙니다. 그것은 “10초까지 종료 실패하지 않은 실행 중 성공한 비율”입니다. 문제는 조건부 비율에 원래 질문의 이름을 붙이는 데 있습니다. 또한 이 단순한 제외 계산이 모든 데이터에서 검열 기반 추정과 같다는 뜻도 아닙니다. 여기서는 분모를 바꿨을 때 무엇이 사라지는지만 확인합니다.
진행 중을 빼고 종료된 실행만 보는 지표도 별개입니다. 성공 ÷ (성공 + 종료 실패)로 계산하면 A 66.7%, B 87.5%입니다. 종료 결과의 품질을 살펴보는 보조 지표로는 쓸 수 있지만, 아직 끝나지 않은 실행이 있으므로 기한 내 성공률을 대신하지는 못합니다.
분모를 유지하는 원칙은 누락 결과와 분모 민감도를 다룬 글과 이어집니다. 다만 이번 예시의 종료 실패는 누락이 아니라 관측한 결과입니다. 결과를 모르는 경우와 결과가 실패인 경우를 같은 빈칸으로 만들면 안 됩니다.
4. Python 표준 라이브러리로 분류와 순위 역전을 확인한다
다음 코드는 Python 3.10 이상에서 실행할 수 있습니다. 패키지 설치, 모델 호출, API 키는 필요하지 않습니다. 합성 실행을 만들고 10초 상태를 집계하며, 세 비율의 합계와 분모 변경에 따른 순위 역전을 검증합니다. 유한한 비음수 합성 시간을 전제로, 10초 이전에 관측이 끊긴 행은 조용히 제외하지 않고 오류로 처리합니다. NaN·무한대·음수 시간은 거부하지만, 실로그의 잘못된 자료형과 모든 업무 규칙을 검증하는 코드는 아니므로 그대로 운영에 붙이지 마세요.
"""Synthetic, fully observed fixed-horizon outcomes. Python standard library only."""
from collections import Counter
from dataclasses import dataclass
from math import isclose, isfinite
HORIZON = 10.0
@dataclass(frozen=True)
class Run:
outcome: str | None
event_time: float | None
observed_until: float
def state_at(run, horizon):
if not all(isfinite(t) and t >= 0 for t in (horizon, run.observed_until)):
raise ValueError("Invalid observation time")
if run.outcome not in (None, "success", "terminal_failure"):
raise ValueError("Invalid terminal outcome")
if (run.outcome is None) != (run.event_time is None):
raise ValueError("Outcome and event time must appear together")
if run.event_time is not None:
if not isfinite(run.event_time) or not 0 <= run.event_time <= run.observed_until:
raise ValueError("Event outside observed interval")
if run.event_time <= horizon:
return run.outcome
if run.observed_until < horizon:
raise ValueError("Unknown at horizon: do not relabel as pending")
return "pending"
def summarize(runs, horizon):
if not runs:
raise ValueError("Empty cohort")
counts = Counter(state_at(run, horizon) for run in runs)
rates = {key: counts[key] / len(runs) for key in
("success", "terminal_failure", "pending")}
assert sum(counts.values()) == len(runs)
assert isclose(sum(rates.values()), 1.0)
return counts, rates
def cohort(successes, failures, pending):
return ([Run("success", 6.0, 6.0)] * successes
+ [Run("terminal_failure", 2.0, 2.0)] * failures
+ [Run(None, None, HORIZON)] * pending)
cohorts = {"A": cohort(60, 30, 10), "B": cohort(70, 10, 20)}
results = {}
for label, runs in cohorts.items():
counts, rates = summarize(runs, HORIZON)
excluded = counts["success"] / (len(runs) - counts["terminal_failure"])
ended = counts["success"] / (counts["success"] + counts["terminal_failure"])
results[label] = rates["success"], excluded
print(f'{label}: success={rates["success"]:.1%}, '
f'failure={rates["terminal_failure"]:.1%}, '
f'pending={rates["pending"]:.1%}')
print(f' failure-excluded={excluded:.1%}; ended-only={ended:.1%}')
assert results["A"][0] < results["B"][0] # Correct fixed-horizon ranking
assert results["A"][1] > results["B"][1] # Reversal after excluding failures
assert state_at(Run("success", 10.0, 10.0), 10.0) == "success"
assert state_at(Run("success", 12.0, 12.0), 10.0) == "pending"
try:
summarize([Run(None, None, 3.0)], HORIZON)
except ValueError as error:
print(f"Missing-log check: {error}")
else:
raise AssertionError("An unknown outcome was accepted")
print("All checks passed.")
핵심 출력은 A의 성공·종료 실패·진행 중이 60%·30%·10%, B가 70%·10%·20%라는 것입니다. 실패 제외 지표는 각각 85.7%, 77.8%로 역전됩니다. 마지막에는 관측 누락을 거부했음을 확인하고 “All checks passed.”를 출력합니다.
코드의 observed_until은 관측이 확인된 마지막 시점입니다. 2초에 종료 실패를 확인했다면 그 실행을 10초까지 계속 관측할 필요는 없습니다. 실패가 되돌아가지 않는다는 정의 아래 10초 상태도 이미 알기 때문입니다. 반대로 종료 사건 없이 3초까지만 봤다면 10초 상태는 알 수 없습니다. 이 차이가 단순한 “마지막 로그 시간” 집계보다 중요합니다.
5. 실무에서는 상태 정의를 먼저 고친다
재시도 가능한 오류를 종료 실패로 세지 않습니다. 도구 호출 한 번이 실패했어도 에이전트가 재시도해 같은 실행 안에서 성공할 수 있다면 중간 오류입니다. 최종 실패 여부는 오류 코드의 이름이 아니라 실행의 종료 규칙으로 판정해야 합니다. 자동 재시도를 허용하는 평가라면 그 재시도 정책도 A와 B에서 같게 유지합니다.
실행 단위와 사용자 과제 단위를 섞지 않습니다. 한 실행을 끝내고 새 실행으로 재시작하는 시스템에서는 실행별 종료 실패가 맞습니다. 그러나 평가 대상이 여러 시도를 묶은 사용자 과제라면 첫 시도의 실패는 과제 전체의 종료 실패가 아닐 수 있습니다. 몇 번의 시도를 하나로 묶는지, 시간과 비용을 어디서부터 누적하는지 먼저 고정해야 합니다.
강제 시간 초과와 관측용 시점을 구별합니다. 실제 시스템이 10초에 실행을 강제로 끝낸다면 “10초에도 계속 진행 중”이라고 쓰지 않습니다. 평가 규약에 따라 기한 종료를 실패 하위 유형으로 기록하거나 별도 종료 상태로 둡니다. 또 정확히 10초에 성공이 확인된 경우를 포함할지 정해야 합니다. 위 코드는 10초 이내를 포함하고, 해당 시각의 종료 사건을 먼저 판정합니다.
판정 지연도 실행 지연과 구별해 기록합니다. 이 글은 검증 완료까지를 성공 시간으로 정했으므로 결과물은 나왔지만 검증이 끝나지 않은 실행도 10초 시점에는 진행 중입니다. 그 안에 ‘결과물 생성 중’과 ‘검증 대기’를 하위 상태로 남겨 지연 원인을 분리하세요. 12초에 검증을 통과했다고 6초의 결과물 생성 시각으로 성공을 소급하지 않습니다. 결과물 생성까지의 시간을 평가하려면 별도 지표로 정의해야 합니다. 상태 증거 자체가 사라졌다면 진행 중이 아니라 관측 누락입니다. 안전한 거절이나 사람에게 인계하는 행동도 과제의 정답일 수 있으므로 무조건 실패로 분류하지 않습니다.
6. 세 칸을 함께 보면 개선 방향도 달라진다
예시에서 B는 기한 내 성공이 10%포인트 많고 종료 실패는 20%포인트 적지만, 진행 중은 10%포인트 많습니다. 이를 단순히 “더 느리다”라고 단정할 수는 없습니다. A가 일찍 실패로 빠져나간 실행도 있기 때문입니다. B의 진행 중 실행은 시간이 더 있으면 성공할지, 계속 지연될지 별도 확인이 필요합니다.
따라서 개선 과제도 나누어 읽습니다. 종료 실패가 많으면 실패 이유와 복구 정책을 확인하고, 진행 중이 많으면 대기·도구 호출·반복 루프를 살펴봅니다. 성공 비율이 같더라도 실패와 진행 중의 구성이 다르면 이용자가 겪는 문제와 대응 방법이 달라집니다. 이 분석은 원인 진단을 시작할 지점을 제공할 뿐, 로그를 보지 않고 원인까지 증명하지는 않습니다.
진행 중을 성공 가능성이 없는 결과로 단정할 필요도 없습니다. 다만 “10초 안에 성공했는가”라는 이진 평가에서는 아직 성공하지 않은 실행입니다. 이진 점수에는 0이 들어가더라도 원인 분석용 상태 라벨은 유지할 수 있습니다. 점수를 매기는 일과 무슨 일이 있었는지 기록하는 일은 다릅니다.
7. 이 집계로 알 수 없는 것
이 예시는 10초 상태를 모두 확인한 동일 가중치 실행의 관측 비율입니다. 실제 모집단에서의 정확한 성공확률이나 A와 B 차이의 통계적 유의성을 증명하지 않습니다. 실제 비교에는 같은 과제 구성, 반복 실행과 변동성, 과제별 묶임을 고려한 불확실성 평가가 필요합니다. 10초를 결과를 본 뒤 유리하게 고르지 않는 것도 중요합니다.
관측 기간이 제각각이거나 기한 전에 로그가 끊긴 실행이 있다면 위 비율을 그대로 일반화할 수 없습니다. 누락 원인과 관측 중단 가정을 확인하고, 필요하면 경쟁 사건을 반영한 누적발생확률 추정으로 넘어가야 합니다. 진행 중인 실행의 10초 이후 성공률, 실패가 없었다면 얻었을 가상의 성공률, 모델 변경의 인과효과 역시 이 표만으로는 알 수 없습니다.
실무에서 남길 최소 보고는 간단합니다. “같은 평가 조건의 전체 100회에서, 10초 이내 성공 70회·종료 실패 10회·진행 중 20회. 기한 이전 관측 누락 없음. 정확히 10초의 종료 사건 포함.” 그 다음에 지연시간이나 비용 지표를 붙입니다. 성공을 얼마나 빨리 했는지 비교하기 전에, 성공하지 않은 실행을 어디에 넣었는지부터 확인해야 합니다.
검증 기준일: 2026년 10월 23일. 수치는 교육용 합성 데이터이며 코드로 재현했습니다. 외부 자료는 사건·상태·검열의 개념과 평가 결과 정의를 확인하는 데 사용했고, AI 실행에 대한 분류 규약과 예시는 이 글의 적용 설계입니다.
실행 확인: 2026년 10월 23일, Python 3.12.14에서 위 합성 코드를 실행해 비율 합계, 순위 역전, 10초 경계, 이후 사건의 분류, 조기 관측 누락 거부를 확인했습니다. 별도 9개 검사에서는 NaN·무한대·음수 시간의 거부도 확인했습니다. 실제 AI 모델 시험이나 생존모형 적합은 수행하지 않았습니다. 이 코드는 고정 합성 자료를 설명하는 교육용 예제이며 모든 비정상 자료형과 업무 규칙을 방어하는 운영용 입력 검증기는 아닙니다. 특정 AI 모델이나 SDK에 의존하지 않습니다.