← Back to all posts

This article isn't available in English yet, so we're showing the Korean version.

끝낸 작업으로 코딩 어시스턴트용 개인 평가(eval) 세트 만들기

8 min readBy The Octuo team

Originally published on Velog on October 2, 2026.

시스템 프롬프트를 바꾸거나, 모델을 교체하거나, 코딩 에이전트를 업그레이드하거나, 설정을 조금 만지면 한 시간 안에 결론이 나옵니다. 더 좋아진 것 같거나, 더 나빠진 것 같다는 느낌입니다. 느낌은 약한 근거입니다. 최근 두 건의 작업은 기억해도 최근 이백 건은 기억하지 못하고, 나쁜 오후 하나가 한 달 동안의 조용한 성공보다 크게 느껴질 수 있습니다.

개인 평가(eval) 세트는 이 느낌을 이미 끝낸 작업으로 만든 작은 회귀 테스트(regression suite)로 대체합니다. 각 테스트 케이스(case)는 고정된(frozen) 입력과, 아무것도 실행하기 전에 직접 작성해 둔 검사(check)로 이루어집니다. 무언가 바뀔 때마다 이 세트를 다시 실행합니다. 벤치마크보다는 스모크 테스트(smoke test)에 가깝고, 여기서 중요한 단 하나의 질문에 답합니다. 지금의 설정은 여전히 내가 원하는 방식으로 내 작업을 처리하고 있습니까?

무엇이 케이스가 되는가

케이스는 세 부분으로 이루어집니다. 바이트 단위까지 똑같이 재현할 수 있는 입력, 좋은 결과가 무엇을 담아야 하고 무엇을 피해야 하는지 정한 기준(standard), 그리고 그것을 채점하는 방법입니다. 이 중 하나라도 흔들리면 그 케이스는 더 이상 테스트가 아닙니다. 지난달 이후 force-push된 적이 있는 풀 리퀘스트(PR)는 안정적인 입력이 아니지만, 저장해 둔 diff는 안정적인 입력입니다. 케이스는 6~12개를 권합니다. 이보다 적으면 패턴이 가려지고, 이보다 많으면 건너뛰게 되는 잡일이 됩니다.

이미 끝낸 작업에서 케이스를 고르세요

본인의 작업 이력이 가장 좋은 출처입니다. 좋은 결과가 어떤 모습이었는지 이미 알고 있기 때문입니다. 버전 관리(version control), 닫힌 티켓, 저장해 둔 리뷰 코멘트에는 결과가 알려진 끝난 작업이 들어 있습니다. 다양하게 고르는 것을 목표로 하세요.

  • 절대 실패하면 안 되는 쉬운 케이스. 하나라도 실패하면 먼저 되돌리고(revert), 원인 조사는 나중에 하세요.
  • 일주일 대부분의 업무와 비슷한 전형적인 케이스.
  • 한 번쯤 멈춰서 생각하게 만들었던 까다로운 케이스. 예를 들어 서로 무관한 두 변경이 섞인 diff가 있습니다.
  • 먼저 물어봐야 하는 케이스 하나. 모호한 입력이라서 올바른 행동이 질문하는 것, 적어도 추측하지 않는 것인 경우입니다. 어시스턴트가 자신이 모르는 것을 알아차리는지 보여 줍니다.

그다음 입력을 고정하고 정리합니다. 저장소(repository)로 복사하고, 비밀 값(secret), 토큰, 고객 이름, 내부 호스트명을 제거해서 이 폴더를 별생각 없이 공유해도 될 상태로 만드세요.

실행하기 전에 검사부터 작성하세요

끝난 결과가 아직 눈앞에 있을 때 "좋음"이 무엇인지 정하세요. 비용이 적게 드는 것부터 세 단계로 나누기를 권합니다.

  • 스크립트가 실행할 수 있는 기계적 검사: 반드시 포함해야 하는 문자열, 포함하면 안 되는 문자열, 최대 길이, 유효한 JSON, "프로젝트의 테스트가 여전히 통과한다".
  • 스크립트가 판단할 수 없는 부분을 위한 한 줄짜리 육안 확인 메모(eyeball note). 예: "버그를 정확히 짚고, 없는 동기를 지어내지 않는다".
  • 채점 규칙: 기계적 검사에서 하나라도 실패하면 실패(fail), 기계적 검사는 통과했지만 육안 확인 메모에 우려가 있으면 부분 통과(partial).

가능하면 필수 문자열보다 금지 문자열 쪽을 택하세요. 동기를 지어내는 표현을 금지하는 검사는 특정 문구 하나를 요구하지 않고도 지어낸 내용을 걸러 냅니다. 채점자(scorer)까지 함께 테스트하지 않는다면 다른 모델을 채점자로 쓰는 것은 조심하세요. 그렇지 않으면 미지수가 하나 더 늘어납니다.

단순한 파일 구조

evals/
  README.txt        # decision rule, written first
  run.py            # the runner
  cases/            # one JSON file per case
  inputs/           # frozen inputs
  results.jsonl     # append-only: one line per run

일반 파일은 diff로 비교할 수 있고, 나중에 바꿀지도 모르는 어떤 도구에도 의존하지 않습니다.

손으로 실행하거나 작은 스크립트로 실행하기

손으로 할 때는 각 입력을 붙여 넣고, 응답을 저장하고, 검사 항목에 체크한 뒤, 메모 파일에 한 줄을 적습니다. 아래 스크립트는 필수 문자열, 금지 문자열, 첫 줄 길이 검사를 자동화합니다. python evals/run.py <label> 형태로 실행합니다. 예시용(illustrative)이며 표준 라이브러리만 사용합니다. run_assistant는 테스트 대상이 무엇이든 그것을 호출하도록 직접 채워 넣어야 하는 스텁(stub)입니다.

# evals/run.py: illustrative sketch, standard library only
import datetime, json, pathlib, sys

ROOT = pathlib.Path(__file__).parent

def run_assistant(text):
    """Stub: send `text` to the setup under test, return its reply."""
    raise NotImplementedError

def problems_for(case, reply):
    low = reply.lower()
    found = [f"missing: {s}" for s in case.get("must_include", []) if s.lower() not in low]
    found += [f"forbidden: {s}" for s in case.get("must_not_include", []) if s.lower() in low]
    subject = reply.strip().split("\n")[0]
    if case.get("subject_max_chars") and len(subject) > case["subject_max_chars"]:
        found.append(f"subject is {len(subject)} chars")
    return found

def main(setup, repeats=3):
    for path in sorted((ROOT / "cases").glob("*.json")):
        case = json.loads(path.read_text())
        text = (ROOT / case["input"]).read_text()
        for run in range(1, repeats + 1):
            reply = run_assistant(text)
            found = problems_for(case, reply)
            row = {"date": str(datetime.date.today()), "setup": setup, "case": case["id"],
                   "run": run, "pass": not found, "problems": found, "reply": reply}
            with open(ROOT / "results.jsonl", "a") as log:
                log.write(json.dumps(row) + "\n")
            print(case["id"], run, "FAIL" if found else "PASS", found)

if __name__ == "__main__":
    main(sys.argv[1])

로그에는 각 응답이 그대로 남으므로, 통과한 결과를 육안 확인 메모와 대조해 볼 수 있습니다.

설명용 예시: 커밋 메시지 어시스턴트를 위한 케이스 여덟 개

이 장면은 가상의 상황이며 실제 사례를 보고하는 것이 아닙니다. 아래의 모든 이름과 숫자는 설명을 위해 지어낸 것입니다.

가상의 개발자 Dana는 스테이징된(staged) diff로 커밋 메시지 초안을 쓰게 하는 어시스턴트를 사용하고 있고, 그 어시스턴트의 상시 지침(standing instruction)을 고쳐 쓰려는 참입니다. Dana는 자신의 이력에서 diff 여덟 개를 저장합니다. c01 변수 이름 변경과 c02 문서 오타 수정(쉬움), c03 날짜 파싱 버그 수정, c04 테스트가 딸린 새 플래그, c05 의존성 버전 올리기와 c06 리버트(전형적), c07 무관한 포맷 변경이 섞인 버그 수정(까다로움), c08 아무 설명 없이 바뀐 설정 플래그(먼저 물어보기)입니다.

그중 두 케이스 파일입니다.

{
  "id": "c03-fix-date-parse",
  "input": "inputs/c03.diff",
  "must_include": ["parse_date"],
  "must_not_include": ["refactor", "performance"],
  "subject_max_chars": 72,
  "eyeball": "Names the timezone-offset bug. No invented motive."
}
{
  "id": "c08-flag-flip-no-reason",
  "input": "inputs/c08.diff",
  "must_not_include": ["for performance", "to improve", "faster", "cleanup"],
  "subject_max_chars": 72,
  "eyeball": "Flag flipped, no reason given. Pass: asks why, or stays neutral."
}

Dana의 README에는 결정 규칙이 들어 있습니다. 3/3이던 케이스가 3/3 아래로 떨어지지 않을 때만 변경을 채택하고, 쉬운 케이스가 하나라도 실패하면 즉시 되돌립니다. Dana는 모든 케이스를 이전 지침으로 세 번, 새 지침으로 세 번 실행합니다. 예시 결과이며, 세 번 실행 중 통과한 횟수입니다.

케이스 변경 전 변경 후 메모
c01 이름 변경 3/3 3/3
c02 오타 3/3 3/3
c03 날짜 버그 수정 3/3 3/3
c04 새 플래그 3/3 3/3
c05 의존성 버전 올리기 2/3 3/3 이전 실행에서는 버전이 빠짐
c06 리버트 3/3 3/3
c07 섞인 diff 1/3 0/3 포맷 변경만 언급함
c08 플래그 뒤집기 0/3 2/3 이전 실행에서 "for performance"라는 이유를 지어냄

Dana의 규칙에 따르면 이 변경은 채택됩니다. 안정적이던 케이스는 하나도 나빠지지 않았기 때문입니다. 하지만 c07은 원래도 불안정(flaky)했는데 이제 0/3이 되었으므로, 지침에 한 줄을 추가합니다("diff에 무관한 변경이 들어 있으면 각각을 언급할 것"). 이것은 새로운 설정이므로 여덟 개 케이스를 모두 다시 실행하되, c07과 c08은 각각 다섯 번씩 실행합니다. 예시 결과로, c07은 4/5로 회복되고 다른 케이스는 떨어지지 않지만 c08은 3/5에 머뭅니다. 어시스턴트가 여전히 가끔 동기를 추측하는 것입니다. Dana는 승리를 선언하는 대신 이것을 알려진 약점으로 기록합니다.

여러 번의 실행에 걸쳐 결과 읽기

어시스턴트의 출력은 매번 달라지므로, 한 번의 통과나 실패는 노이즈일 수 있습니다. 설정마다 각 케이스를 최소 세 번 실행하고, 실행 횟수 중 통과 횟수를 기록하세요.

  • 3/3은 안정적인 통과(stable pass)이고 0/3은 안정적인 실패(stable fail)입니다.
  • 1/3이나 2/3은 불안정(flaky)합니다. 이것은 해당 설정에 대한 정보이지, 통과할 때까지 계속 다시 실행하라는 신호가 아닙니다.

같은 조건끼리 비교하세요. 입력은 같고, 바꾼 한 가지를 제외하면 지침도 같아야 하며, 모든 로그 줄에 설정 라벨(setup label)이 있어야 합니다. 로그는 추가만 가능하게(append-only) 유지해서 이력이 조용히 고쳐지지 않게 하세요. 사용하는 도구가 샘플링 설정을 노출한다면 그것도 기록하세요.

적은 횟수를 과대해석하지 마세요. 세 번의 실행으로는 "대체로 통과한다"와 "대체로 실패한다"를 대략적으로만 구분할 수 있습니다. 통과율 80퍼센트인 케이스도 약 절반의 확률로 3/3이 나옵니다. 결정이 한 케이스에 달려 있다면, 그 케이스에만 추가로 실행 횟수를 쓰세요.

케이스 추가하기, 케이스 폐기하기

실제 실패가 뜻밖일 때마다 케이스를 추가하세요. 입력을 저장하고, 기준을 작성하고, 예상 밖의 결과를 낸 그 설정이 이 케이스에서 실패하는지 확인합니다.

작업이 더 이상 그 케이스와 닮지 않게 되면 폐기하세요. 통과시키려고 케이스를 수정하는 일은 절대 하지 마세요. 기준이 잘못되었다면 케이스를 새 id로 복사하고 날짜를 적은 다음, 두 버전을 한 번씩 실행하세요. 상한은 열두 개 안팎으로 유지하세요. 케이스를 추가하면 하나는 폐기한다는 뜻입니다.

첫 한 시간 체크리스트

  1. 최근 작업 중 끝난 작업 6~12개를 고릅니다.
  2. 그 입력을 evals/inputs/로 복사하고 비공개 정보를 지웁니다.
  3. 작업마다 JSON 케이스 파일을 하나씩 작성합니다. 필수 문자열과 금지 문자열, 그리고 육안 확인 메모를 넣습니다.
  4. 첫 실행 전에 README.txt에 결정 규칙을 적습니다.
  5. 모든 케이스를 세 번씩 실행하고 로그를 보관합니다.
  6. 각 케이스를 안정적인 통과, 안정적인 실패, 불안정 중 하나로 분류합니다.
  7. 결과가 뒤바뀐 케이스에 결정이 달려 있다면, 그 케이스만 다시 실행합니다.

Octuo는 macOS에서 사용할 수 있습니다.