AI 재시도 비용을 합산하는 법: 실패·미확인 시도까지 남기는 원장

재시도 비용을 합산할 때는 성공한 시도만 남기면 안 됩니다. 실패한 시도와 결과를 확인하지 못한 시도에도 확인된 비용이 있다면 합산합니다. 반대로 비용 자체가 아직 확정되지 않았다면 0원으로 채우지 않고, 확인된 소계와 미확정 항목을 나눠 보고해야 합니다.

앞선 작업·시도 로그 집계 실습에 비용 열을 더해 보겠습니다. 이번 목표는 재시도를 더 하라는 결론이 아니라, 지금까지 쓴 비용을 빠뜨리지 않는 원장을 만드는 것입니다. Python 3를 실행할 수 있으면 따라 할 수 있고, 추가 라이브러리나 외부 API는 필요 없습니다.

먼저 두 가지 미확인을 구별합니다

결과 미확인과 비용 미확인은 서로 다른 상태입니다. 도구 응답을 받지 못해 작업 결과가 unknown이어도, 비용 기록에서 25원이 확인됐다면 비용에는 25원을 남깁니다. 반대로 작업 결과는 pass여도 비용이 아직 집계되지 않았다면 그 비용을 None으로 기록합니다. 성공 여부만 보고 비용의 유무를 결정하지 않습니다.

아래 숫자는 전부 설명을 위한 가상 원 단위 비용입니다. 특정 모델의 가격이나 실제 청구 내역이 아닙니다. 판정 pass는 정해 둔 결과 확인 기준을 통과했다는 뜻입니다. 예제는 같은 통화와 비용 범위, 같은 관측 마감 시각을 사용하고, 작업별 시도가 순차적으로 진행되며 pass 뒤에는 종료됐다고 가정합니다.

작업 네 건, 시도 여섯 회의 비용

작업시도별 판정과 비용작업 비용
Afail 30원 → pass 40원70원
Bpass 20원20원
Cfail 30원 → fail 50원80원
Dunknown 25원25원

전체 관측 비용은 70 + 20 + 80 + 25 = 195원입니다. 확인된 성공 작업은 A와 B 두 건입니다. 전체 비용을 성공 작업 수로 나누면 195 ÷ 2 = 97.5원입니다. 이는 실패·미확인 작업에 쓴 비용까지 성공 두 건에 배분한 지표이지, 성공 작업에서만 직접 발생한 평균 비용이 아닙니다.

성공 작업 A와 B의 직접 비용만 평균 내면 (70 + 20) ÷ 2 = 45원입니다. 97.5원과 45원은 분자가 다르므로 둘 다 계산상 맞지만, 서로 바꿔 쓰면 안 됩니다. 성공한 시도의 비용만 고르면 (40 + 20) ÷ 2 = 30원이 되어 A의 첫 실패 비용까지 사라집니다. 여기서는 전체 원장의 97.5원을 주 지표로, 성공 작업의 45원을 보조 설명으로 구분합니다.

합성 원장의 A 비용 70원, B20원, C80원, D25원. 전체 195원을 성공2건으로 나누면97.5원이며 성공 작업A와B의 직접 비용평균45원과 다르다. 비용 자체가 미확정이면 총액도 확정하지 않는다.
합성 예시입니다. 결과가 미확인인 D도 비용 25원이 확인됐으므로 전체 비용에 포함합니다. 두 비율은 서로 다른 질문에 답합니다.

복사해서 실행하는 비용 원장

아래 코드를 retry_cost.py라는 UTF-8 파일로 저장하고 python3 retry_cost.py로 실행하세요. Python 실행 명령이 python인 환경에서는 그 명령을 쓰면 됩니다. 원본 작업 목록 TASKS와 시도 목록 ROWS를 분리했습니다. 행은 작업 ID, 시도 번호, 판정, 비용 순서입니다. 비용은 0 이상의 정수 또는 None만 받으며, 금액처럼 보이는 문자열·실수·True는 거부합니다. 실제 CSV를 연결할 때는 열과 통화를 먼저 검증하고 명시적으로 변환하세요.

from fractions import Fraction

TASKS = ['A', 'B', 'C', 'D']
# task_id, attempt_no, verdict, cost_won
ROWS = [
    ('A', 1, 'fail', 30), ('A', 2, 'pass', 40),
    ('B', 1, 'pass', 20),
    ('C', 1, 'fail', 30), ('C', 2, 'fail', 50),
    ('D', 1, 'unknown', 25),
]

def audit(tasks, rows):
    if (not tasks or any(type(t) is not str or not t or t.strip() != t for t in tasks)
            or len(set(tasks)) != len(tasks)):
        raise ValueError('작업 목록 오류')
    grouped = {t: {} for t in tasks}
    for row in rows:
        if not isinstance(row, (tuple, list)) or len(row) != 4:
            raise ValueError('행은 네 필드여야 합니다')
        task, number, verdict, cost = row
        if type(task) is not str or task not in grouped:
            raise ValueError('미등록 작업')
        if type(number) is not int or number < 1 or number in grouped[task]:
            raise ValueError('시도 번호 오류 또는 중복')
        if type(verdict) is not str or verdict not in {'pass', 'fail', 'unknown'}:
            raise ValueError('판정 오류')
        if cost is not None and (type(cost) is not int or cost < 0):
            raise ValueError('비용은 0 이상의 정수 또는 None이어야 합니다')
        grouped[task][number] = (verdict, cost)
    subtotal, unresolved, passed, successful_cost = 0, 0, 0, 0
    details = {}
    for task, attempts in grouped.items():
        numbers = sorted(attempts)
        if numbers != list(range(1, len(numbers) + 1)):
            raise ValueError('시도 번호 누락')
        values = [attempts[n] for n in numbers]
        if any(v == 'pass' for v, c in values[:-1]):
            raise ValueError('통과 뒤 추가 시도')
        known = sum(c for v, c in values if c is not None)
        missing = sum(c is None for v, c in values)
        # 작업의 시도 기록 자체가 없으면 전체 원장도 완전하지 않습니다.
        unresolved += missing + (not values)
        subtotal += known
        is_pass = any(v == 'pass' for v, c in values)
        passed += is_pass
        if is_pass:
            successful_cost += known
        details[task] = (known, missing, 'pass' if is_pass else 'not_confirmed')
    complete = unresolved == 0
    return {
        'tasks': len(tasks), 'attempts': len(rows), 'details': details,
        'known_subtotal': subtotal, 'unresolved_entries': unresolved,
        'verified_successes': passed,
        'total': subtotal if complete else None,
        'allocated_per_success': Fraction(subtotal, passed) if complete and passed else None,
        'successful_task_mean': Fraction(successful_cost, passed) if complete and passed else None,
    }

if __name__ == '__main__':
    result = audit(TASKS, ROWS)
    print(result)

출력 사전에서 total은 195, verified_successes는 2입니다. allocated_per_success의 Fraction(195, 2)는 정확한 분수 195/2, 즉 97.5원을 뜻합니다. successful_task_mean의 Fraction(45, 1)은 45원입니다. 부동소수점 근삿값을 원장에 누적하는 대신 정수로 합산한 뒤 Fraction으로 비율을 표현했습니다.

비용이 하나 빠졌을 때의 정답은 170원이 아닙니다

D의 비용 25를 None으로 바꿔 다시 실행해 보세요. known_subtotal은 170, unresolved_entries는 1, total과 두 평균은 None이 됩니다. 170원은 확인된 소계일 뿐 전체 확정 비용이 아닙니다. 미확정 금액을 0으로 채우면 “총비용 170원, 성공당 85원”이라는 잘못된 확정 보고가 만들어집니다.

TASKS에 E를 추가하고 시도 기록은 넣지 않아도 total은 None입니다. 실행 자체가 없었던 것인지 로그가 빠졌는지 이 자료만으로 알 수 없기 때문입니다. 이 보수적인 실습 설계는 미관측 작업을 공짜로 처리하지 않습니다. 실제 시스템에서 미실행이 확인되었다면 별도 상태와 근거로 표현하도록 확장해야 합니다.

성공이 0건이면 비용 총액이 알려져 있어도 성공당 비용은 정의하지 않습니다. 코드는 0으로 나누지 않고 None을 반환합니다. 같은 작업·시도 번호의 중복, 중간 번호 누락, pass 뒤 추가 시도는 오류입니다. 원장 행을 임의로 삭제하거나 번호를 바꿔 통과시키지 말고 실행 기록과 대조하세요.

보고서에 복사할 양식

관측 마감 시각 / 실행 버전:
작업 목록과 로그 출처:
통화 / 포함 비용 / 제외 비용:
원래 작업 수 / 기록된 시도 수:
확인된 비용 소계:
비용 미확정 또는 시도 기록이 없는 항목 수:
전체 원장 비용 확정 여부와 근거:
확인된 성공 작업 수와 판정 기준:
전체 비용 ÷ 성공 작업 수: (계산 가능할 때만)
남은 확인 항목:

이 예제의 전체 원장 비용은 모델 호출 외에 검수 인건비·인프라·세금까지 포함한 사업 총원가가 아닙니다. 실제로 포함한 항목을 먼저 정해야 합니다. 여러 통화를 그대로 합하거나, 예상 비용과 확정 청구액을 구분 없이 섞지 마세요. 환급·할인·조정액도 별도 설계가 필요합니다. 이 코드는 음수 금액을 허용하지 않으므로 환급 원장에는 그대로 쓰지 않습니다.

검증한 범위와 다음 판단

2026년 11월 8일 Python 3.12.14에서 본문의 합성 데이터를 실행했습니다. 기본 계산, 비용 None, 작업 기록 부재, 성공 0건과 여덟 가지 잘못된 입력을 포함한 12개 점검을 통과했습니다. 실제 모델 호출이나 청구 시스템 대조, 분산 로그의 완전성 검증은 수행하지 않았습니다. 마지막 시도 자체가 빠진 경우는 번호가 연속이어도 알아낼 수 없으므로 완료 기록·청구 원장과 별도 대조해야 합니다.

공식 문서 확인일은 2026년 11월 8일입니다. Python fractions 문서는 정수 분자·분모로 유리수를 구성하는 방법을 설명합니다. Python 내장 함수 문서에서 bool이 int의 하위 클래스임을 확인했습니다. 코드가 type(cost) is int를 쓰는 이유는 True를 1원으로 받지 않기 위해서입니다. 원장 열과 보류 규칙은 이 실습의 설계안이며 공식 회계 표준이 아닙니다.

이 원장은 이미 관측한 비용을 정리합니다. 다음 재시도를 허용할지 판단하려면 성공 가능성·추가 비용·권한·종료 조건을 따로 검토해야 합니다. 97.5원이 낮아 보인다는 이유만으로 unknown 작업을 다시 실행하지 않습니다. 비용을 빠뜨리지 않는 일과 다음 행동을 승인하는 일은 여기서 갈라집니다.


RUDA DIRECTOR · 루다(이루다)

작성자·공개 작업·편집 기준 · 문의·정정 요청

다른 글 보기 · 주제 탐색