AI 재시도 로그를 작업별로 묶는 법: CSV와 Python 실습

재시도를 포함한 AI 작업을 집계하려면 처음 맡긴 작업 목록과 실제 시도 기록을 분리해야 합니다. 같은 작업을 두 번 실행했다고 작업 수를 두 건으로 늘리지 않습니다. 작업 ID는 그대로 두고 시도 번호를 추가한 뒤, 마지막에 작업 단위로 묶습니다.

이 글은 CSV를 내보낼 수 있는 작은 AI 업무 도구를 운영하며, Python 파일을 실행할 수 있는 독자를 위한 실습입니다. 외부 라이브러리나 API 호출은 필요 없습니다. 목표는 모델 성능을 입증하는 것이 아니라, 재시도 때문에 집계 단위가 섞이는 오류를 잡는 것입니다.

먼저 정할 것: 통과는 HTTP 200이 아닙니다

여기서 pass는 미리 정한 결과 확인 기준을 통과했다는 뜻입니다. 예컨대 문서 분류 작업이라면 정답 레이블과 일치했는지 확인한 결과입니다. 서버가 응답했다는 사실만으로 pass를 쓰지 않습니다. fail은 그 기준을 통과하지 못한 확인 결과, unknown은 결과를 아직 확인하지 못한 상태입니다. 판정 기준과 모델·프롬프트 버전이 다른 실행은 한 묶음으로 섞지 마세요.

아래는 설명을 위해 만든 합성 기록입니다. 실제 고객 요청이나 모델 호출 결과가 아닙니다. A는 두 번째 시도에서 통과했고, B는 첫 시도에서 통과했습니다. C는 두 번 모두 실패 판정, D는 결과 미확인입니다. 작업은 네 건이지만 총 시도는 여섯 회입니다.

가상 작업 A는 실패 후 통과, B는 통과, C는 두 번 실패, D는 결과 미확인. 통과 작업은 4건 중 2건, 통과 시도는 6회 중 2회다.
합성 예시: 같은 로그라도 작업 단위와 시도 단위의 분모가 다릅니다.

기록 양식: 작업 ID, 시도 번호, 판정, 확인 기록

작업 목록은 실행 전에 확정해 보존합니다. 시도 로그에 나타난 ID만 모아 분모를 만들면, 아예 기록이 없는 작업이 사라집니다. 코드의 TASKS가 그 원래 목록이고 CSV가 별도 시도 로그입니다.

task_id는 같은 업무 요청 동안 유지하는 식별자, attempt_no는 1부터 증가하는 시도 번호입니다. verdict에는 pass·fail·unknown만 씁니다. evidence_ref에는 확인 기록을 찾을 수 있는 비민감 참조값을 넣습니다. 참조값이 있다는 것만으로 증거가 검증되는 것은 아닙니다. 아래 check-A1 같은 값은 가상 표식입니다.

이 양식은 이 실습의 설계안이지 공식 로그 표준이 아닙니다. 인증값, 고객 원문, 개인정보를 그대로 넣지 마세요. 필요한 확인 기록은 접근 권한이 있는 별도 저장소에서 관리하고 CSV에는 참조만 남기는 방식으로 시작할 수 있습니다.

복사해서 실행하기

Python 3가 설치된 환경에서 아래 전체 코드를 UTF-8 텍스트 파일 retry_audit.py로 저장하세요. 터미널에서 그 파일이 있는 폴더로 이동한 뒤 python3 retry_audit.py로 실행합니다. Python 3의 실행 명령이 python인 환경에서는 그 명령을 사용하세요. 합성 CSV가 코드 안에 있으므로 별도 파일은 필요 없습니다. 코드는 읽은 기록을 검사하고 화면에 집계만 출력하며, 외부 요청을 보내거나 재시도를 실행하지 않습니다.

import csv
from io import StringIO
from collections import Counter

TASKS = ['A', 'B', 'C', 'D']
CSV = '''task_id,attempt_no,verdict,evidence_ref
A,1,fail,check-A1
A,2,pass,check-A2
B,1,pass,check-B1
C,1,fail,check-C1
C,2,fail,check-C2
D,1,unknown,
'''

def audit(tasks, text):
    if (not tasks or any(not isinstance(t, str) or not t or t.strip() != t for t in tasks)
            or len(set(tasks)) != len(tasks)):
        raise ValueError('작업 목록은 비어 있거나 중복될 수 없습니다')
    reader = csv.DictReader(StringIO(text, newline=''), strict=True)
    required = ['task_id', 'attempt_no', 'verdict', 'evidence_ref']
    if reader.fieldnames != required:
        raise ValueError('CSV 열 이름 또는 순서가 다릅니다')
    grouped = {task: {} for task in tasks}
    for row in reader:
        if None in row or any(v is None for v in row.values()):
            raise ValueError('CSV 열 수가 다릅니다')
        task, verdict = row['task_id'], row['verdict']
        if task not in grouped or verdict not in {'pass', 'fail', 'unknown'}:
            raise ValueError('등록되지 않은 작업 또는 판정입니다')
        number = int(row['attempt_no'])
        if number < 1 or number in grouped[task]:
            raise ValueError('시도 번호가 잘못됐거나 중복됐습니다')
        if verdict != 'unknown' and not row['evidence_ref'].strip():
            raise ValueError('확정 판정의 확인 기록이 없습니다')
        grouped[task][number] = verdict
    task_counts, attempt_counts = Counter(), Counter()
    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 'pass' in values[:-1]:
            raise ValueError('성공 확인 뒤 추가 시도가 있습니다')
        state = ('pass' if 'pass' in values else
                 'unknown' if not values or 'unknown' in values else 'no_pass')
        task_counts[state] += 1
        attempt_counts.update(values)
    return task_counts, attempt_counts

if __name__ == '__main__':
    tasks, attempts = audit(TASKS, CSV)
    n = sum(attempts.values())
    print(f"작업: {len(TASKS)}, 총 시도: {n}")
    print(f"확인된 성공 작업: {tasks['pass']}/{len(TASKS)}")
    print(f"통과 판정 시도: {attempts['pass']}/{n}")
    print(f"기록된 시도가 모두 fail인 작업: {tasks['no_pass']}, 결과를 확정하지 못한 작업: {tasks['unknown']}")

출력에서 바로 판단할 수 있는 것

작업: 4, 총 시도: 6
확인된 성공 작업: 2/4
통과 판정 시도: 2/6
기록된 시도가 모두 fail인 작업: 1, 결과를 확정하지 못한 작업: 1

확인된 성공 작업은 2/4, 즉 50%입니다. 통과 판정을 받은 시도는 2/6, 약 33.3%입니다. 두 숫자는 서로 모순되지 않습니다. 앞의 질문은 “맡긴 작업 중 몇 건이 통과했나”, 뒤의 질문은 “기록된 시도 중 몇 회가 통과했나”이기 때문입니다. 어느 쪽도 실제 서비스 전체의 성공률 추정치로 일반화할 수 없습니다.

A의 첫 실패 기록을 지우지 마세요. 지우면 재시도 부담이 사라져 보입니다. C는 “확인된 성공 없음”이지, 앞으로도 반드시 실패한다는 뜻이 아닙니다. D는 실패로 단정하지 말고 확인 기록부터 찾습니다. 출력의 “기록된 시도가 모두 fail인 작업”은 기록된 시도에 unknown이 없지만 pass도 없는 경우이고, “결과를 확정하지 못한 작업”은 pass가 없으면서 시도 기록이 없거나 unknown이 남아 있는 경우입니다. 이후 pass가 확인되면 작업은 통과로 집계하지만 이전 unknown 시도는 시도 집계에 그대로 남습니다.

세 군데를 바꿔 분모와 오류를 확인하기

첫째, CSV는 그대로 두고 TASKS에 'E'를 추가해 보세요. 작업 수는 5건, 총 시도는 여전히 6회입니다. 확인된 성공 작업은 2/5, 결과를 확정하지 못한 작업은 2건이 됩니다. 로그가 없는 E를 분모에서 빼지 않았기 때문입니다.

둘째, 원래 예시로 돌아와 A,2,pass,check-A2 행을 한 번 더 붙여 보세요. 같은 작업의 같은 시도 번호가 중복되므로 ValueError: 시도 번호가 잘못됐거나 중복됐습니다로 멈춥니다. 두 행을 서로 다른 시도로 세어서는 안 됩니다.

셋째, 다시 원래 예시로 돌아와 A의 두 번째 시도 번호만 2에서 3으로 바꾸세요. ValueError: 시도 번호가 누락됐습니다가 나옵니다. 이는 2번 기록을 찾아야 한다는 신호입니다. 오류를 없애려고 번호를 임의로 고치기보다 실제 실행 기록과 대조하세요.

내 CSV에 적용할 때 바꿀 곳

TASKS를 실행 전에 만든 작업 목록으로 바꾸고 CSV 문자열을 내 파일 내용으로 바꿉니다. Python 공식 csv 문서는 파일을 열 때 newline=""을 쓰도록 안내합니다. 실제 파일을 읽으려면 다음처럼 할 수 있습니다. 내보내기 도구가 UTF-8 BOM을 붙이면 utf-8-sig로 읽습니다. 다른 인코딩 파일은 먼저 내보내기 설정을 확인하세요.

with open("attempts.csv", encoding="utf-8-sig", newline="") as f:
    tasks, attempts = audit(TASKS, f.read())
print(tasks, attempts)

숫자 열도 CSV에서 문자열로 읽히므로 코드에서 정수로 변환합니다. 같은 작업·시도 번호가 두 번 있거나, 번호가 1 다음 3으로 건너뛰거나, 확정 판정에 확인 참조가 없으면 집계하지 않고 오류로 멈춥니다. 원본 행을 임의로 삭제해 통과시키지 말고 왜 중복·누락됐는지 확인하세요.

이 코드가 해결하지 않는 것

한 작업의 시도가 순차적으로 진행되고, 통과 확인 후 종료되는 기록만 받습니다. 병렬 시도나 통과 후 재평가가 필요한 업무에는 그대로 적용하지 마세요. 누락된 마지막 시도는 번호 검사만으로 알아낼 수 없습니다. 실행 시스템의 완료 기록·예정 시도 수와 별도 대조해야 합니다. 작업 목록과 시도 로그를 언제까지 관측한 자료인지도 함께 보존하세요. 시간 제한, 비용, 작업별 난이도, 여러 실행 버전의 비교는 이 코드 범위 밖입니다.

이 집계는 중복 결제·중복 게시를 막지 않습니다. 같은 task_id를 붙였다고 서버의 멱등성이 생기지 않습니다. 특히 외부 쓰기 요청의 결과가 unknown이면, 다시 실행하기 전에 실제 반영 상태부터 확인해야 합니다.

실제로 확인한 범위와 출처

2026년 10월 28일 Python 3.11.9에서 본문의 합성 CSV와 코드를 실행해 위 출력과 세 가지 변형 결과를 확인했습니다. 확인 범위는 이 파일 안의 집계와 입력 오류 처리입니다. 실제 모델 호출, 고객 데이터, 분산 실행 시스템을 시험한 결과는 아닙니다.

공식 문서 확인일: 2026년 10월 28일. Python 3.11 csv 문서의 DictReader, 문자열 필드 처리, newline 지침을 참고했습니다. 작업 단위 판정과 CSV 열 설계는 이 글의 제안입니다.

관련 글: 기록을 해석하는 다음 단계

이 실습은 AI 평가 연재의 확률 계산을 실제 기록 구조와 연결합니다. 첫 성공까지의 단계 수에서는 다음 사용자 요청 한 건을 한 단계로 셌습니다. 여기서는 같은 작업의 반복 시도를 묶으므로 관측 단위가 다릅니다. 실패 뒤 남은 집단의 구성을 해석하기 전에도 어떤 작업이 아직 성공하지 못했는지 구분해야 합니다. 이 CSV만으로 숨은 작업 유형이나 원인을 추정할 수는 없습니다.

재시도 상한과 기대 순가치, 진단 후 복구와 남은 단계로 넘어가려면 비용·상태·행동·종료 조건을 별도로 기록해야 합니다. 이 글의 네 작업과 여섯 시도는 앞선 모형의 확률을 측정하거나 재시도를 허용하는 근거가 아닙니다.

결과가 빠졌을 때 분모와 미확인 결과를 다루는 법에서는 미확인 결과를 제외한 비율이 무엇을 놓치는지 설명합니다. 재시도 전에 결과부터 확인하기는 외부 쓰기 결과가 불확실할 때의 행동 기준을 다룹니다. 이 글의 오프라인 집계와 실제 재시도 허용 판단을 구분해 읽으세요.


다른 글 보기 · 주제 탐색 · 작성자와 편집 기준 · 문의·정정 요청