에이전트 task 하나를 직접 만들어보면
Harbor, Prime Verifiers, Inspect에서 같은 원장 정리 작업을 만든다. 완성된 파일 구조, 만드는 순서, LLM이 들어가는 자리를 함께 연다.
“거래 내역을 정리해서 통화별 합계를 JSON으로 저장해라.” 에이전트에게 줄 일은 한 문장이다. 그런데 같은 거래가 두 번 수정됐다면 어느 금액을 더해야 할까. 결제가 취소됐다면 이전 결제 기록도 빼야 할까. 합계는 맞지만 근거로 적은 거래 목록이 틀렸다면 성공일까.
Task를 만드는 사람은 이 질문에 먼저 답해야 한다. 프레임워크는 그 결정을 실행할 파일과 객체, 실행 순서를 제공한다. 이번 글에서는 일곱 줄짜리 합성 원장을 task로 만든다. 세 프레임워크에 넣는 입력과 정답은 같게 두고, 무엇을 작성하고 무엇이 실행되는지 따라가 보자.
1. 먼저 끝내야 할 일을 고정한다
원장에는 다섯 event의 기록이 일곱 줄 들어 있다. e1과 e4가 수정됐기 때문이다. 각 event에서 가장 큰 revision을 남긴 다음, 최신 상태가 settled인 항목만 합산한다. 금액은 소수점 없는 최소 화폐 단위이며 환전하지 않는다.
| event | revision | 통화 | 금액 | 상태 | 이번 합계에 넣는가 |
|---|---|---|---|---|---|
| e1 | 1 | USD | 1200 | settled | 더 최신 기록이 있어 제외 |
| e1 | 2 | USD | 1300 | settled | 포함 |
| e2 | 1 | USD | -200 | settled | 포함 |
| e3 | 1 | EUR | 700 | pending | 제외 |
| e4 | 1 | EUR | 500 | settled | 최신 기록에서 취소됐으므로 제외 |
| e4 | 2 | EUR | 500 | void | 제외 |
| e5 | 1 | EUR | 350 | settled | 포함 |
만들어야 할 결과는 아래와 같다. included에는 합계에 반영한 event와 revision을 정렬해서 넣는다. USD 1100은 e1의 1300에서 e2의 200을 뺀 값이다. e4는 예전에 결제됐어도 최신 상태가 취소이므로 EUR 합계에 들어가지 않는다.
{"totals":{"EUR":350,"USD":1100},"included":["e1@2","e2@1","e5@1"]}
여기까지가 작성자가 정한 입력과 성공 조건이다. “대충 맞는 요약”을 받는 문제가 아니므로 LLM judge는 필요하지 않다. JSON 형식과 값, 근거 목록을 코드로 검사할 수 있다. 실제 고객 데이터가 아니라 이 글을 위해 만든 입력을 사용한다.
2. 완성된 task를 폴더째 열어본다
Harbor에서는 이 작업을 디렉터리로 묶는다. instruction.md는 요청이고, environment/는 에이전트가 시작할 작업 공간이다. solution/에는 작성자가 아는 풀이를 둔다. tests/에는 결과물을 검사하는 코드를 둔다. task.toml은 이 조각들을 어떤 조건으로 실행하고 무엇을 결과물로 거둘지 정한다.1
원장, 작업 공간, 채점기를 어디에 두는가
저자가 만든 Harbor 원장 예제. 작성 시점에는 아래 13개 파일이 있고, 제출물과 점수는 실행 뒤 생긴다.
① 실행 시점 / agent 작업 공간
/app/ledger.jsonenvironment/Dockerfile이 넣은 7행이번 실행: OracleAgent + solve.sh
/app/submission/output.json실행 전에 정답이 채워져 있지 않다./app/submission만 전달
② 채점 시점 / 새 verifier
tests/fixtures/expected.jsontests/common.pyagent 이미지에 넣지 않은 파일/logs/verifier/reward.txt/logs/verifier/contract.jsonsolution/의 위치: 작성자가 준비한 기준 풀이다. OracleAgent로 검증할 때만 /solution에 넣어 실행한다. 일반 평가 대상 LLM에게 정답 풀이를 기본으로 주는 설정이 아니다. 위 컨테이너 분리는 공식 hello-world의 기본 구성이 아닌 이 예제의 선택이다.
trial/result.json 점수와 오류 trial/artifacts/... 선언한 결과 파일 trial/agent/, trial/verifier/ 로그파일을 눌러 실제 생성된 내용을 연다
파일은 직접 작성한 완성 예제에서 읽어 넣었다. Harbor 구현의 고정 코드 대응. 색상은 접근 경계를 설명하며 전체 보안 인증을 뜻하지 않는다.
위 원장 task에서 에이전트가 읽을 입력은 /app/ledger.json이다. 에이전트는 /app/submission/output.json을 만들어야 한다. 말로 “완료했습니다”라고 답해도 그 파일을 대신하지 못한다.
task.toml에서 다음 두 부분을 연결했다.
artifacts = ["/app/submission"]
[verifier]
environment_mode = "separate"
이 구성에서는 agent 실행이 끝나면 Harbor가 선언된 산출물을 수집해 새 verifier 컨테이너로 전달한다. 채점기는 그곳의 tests/fixtures/expected.json과 제출물을 비교한다. agent가 작업 중 만든 다른 파일까지 통째로 이어받는 방식은 아니다. 입력을 넣는 Dockerfile과 검사용 Dockerfile도 따로 작성했다.2
이 분리는 이번 예제에서 선택한 설계다. Harbor의 공식 hello-world는 같은 컨테이너에서 검사하는 기본 구성을 사용한다. 지시문대로 hello.txt를 만들었는지 확인하는 작은 예제이며, 별도 verifier의 보안 경계를 보여주는 예제는 아니다. task 안에 tests/라는 이름의 폴더가 있다는 것만으로 정답이나 채점 코드가 안전하게 숨겨지는 것도 아니다.3
기준 풀이와 채점기도 역할이 다르다. solution/actor.py는 입력을 읽어 답을 만든다. tests/common.py의 grade_artifact()는 제출된 답을 검사한다. 기준 풀이가 잘못돼도 이를 발견할 수 있도록, 검사 기준은 별도로 계산해 저장한 JSON을 사용했다.
# 이 원장의 채점 연결. 전체 형식 검사는 common.py에 있다.
grade = grade_artifact(output_text)
Path("/logs/verifier/reward.txt").write_text(str(grade.reward))
Harbor는 이 reward 파일을 읽어 trial의 result.json에 기록한다. 결과 파일, 채점 이유, agent 로그는 다른 기록이다. 제출물이 틀려서 0점인 경우와 채점기가 죽어서 reward 파일을 만들지 못한 경우를 같은 값으로 덮으면 수정할 곳을 찾기 어려워진다.4
3. 같은 원장이 다른 프레임워크에 들어가면
Prime Intellect의 Verifiers에서는 같은 작업을 Python의 TaskData, Task, Taskset으로 표현했다. 이 글이 읽은 고정 버전은 v1 API다. 검색에서 나오는 이전 SingleTurnEnv나 Rubric 예제와 섞으면 파일부터 달라진다.5
TaskData에는 모델에게 줄 prompt와 채점에 쓸 answer를 담는다. Taskset.load()는 task를 내놓고, Task의 @reward 메서드는 실행 기록인 Trace에서 최종 응답을 찾아 검사한다. 선택한 harness가 모델과 도구를 어떻게 호출할지 맡고, runtime이 그 실행을 받친다.6
Inspect는 평가를 조립하는 단위가 더 직접 보인다. Sample에 입력을 넣고, Task에 generate() solver와 scorer를 붙인다. 이번 adapter는 모델이 반환한 JSON 문자열을 state.output.completion에서 읽어 같은 grade_artifact()에 보낸다. 공식 최소 예제도 Sample → generate() → exact()를 한 파일에 담는다.7
7행 원장은 같고, 결과를 넘기는 길이 다르다
모두 이 글의 원장 adapter다. 각 프레임워크의 모든 기능이나 기본 실행 모드를 대표하지 않는다.
Harbor
파일을 만드는 task
별도 verifier를 선택한 구성
실제 실행: 기준 풀이와 오답 script. LLM agent는 호출하지 않았다.
설정 파일 열기 ↗Prime Verifiers v1
Python task + chat 응답
null harness / subprocess 구성
실제 실행: localhost scripted endpoint. Python 코드 사이 구분이며 OS 격리가 아니다.
Task 클래스 열기 ↗Inspect AI
Sample + solver + scorer
Python 평가 구성
기존 실제 실행: MockLLM. 기본 generate가 target을 보내지 않아도 solver 코드는 target에 접근할 수 있다.
실행 adapter 열기 ↗아래는 저장된 두 실행 결과다. 버튼은 기록을 전환하며 모델이나 Docker를 호출하지 않는다.
두 실행의 실제 판정. 정상 완료한 오답의 0점과 실행 예외를 구분한다.
세 표현에서 업무 규칙은 같다. 달라지는 것은 결과가 넘어가는 방식과 신뢰해야 할 코드다. Harbor 예제는 파일을 새 컨테이너로 옮긴다. 이번 Verifiers와 Inspect 예제는 모델 응답과 Python 객체를 이용한다. 둘도 더 복잡한 도구나 환경을 붙일 수 있지만, 이 예제에서 컨테이너 격리를 했다고 말할 수는 없다.
정답의 위치도 두 단계로 읽어야 한다. “정답을 모델 prompt에 넣지 않았다”와 “agent 실행 코드가 정답에 접근할 수 없다”는 다르다. Inspect의 TaskState에는 target 접근자가 있다. 기본 generate()가 이를 prompt에 넣지 않아도 신뢰하는 Python solver는 접근할 수 있다. Verifiers의 answer 역시 TaskData를 다루는 작성자 코드가 볼 수 있는 값이다. 그림에서 데이터 구분선과 컨테이너 경계선을 다르게 그린 이유다.8
4. 빈 뼈대에서 쓸 수 있는 task까지
Harbor의 harbor task init은 디렉터리와 설정을 만든다. 기본 instruction은 비어 있고 테스트에는 pass가 남는다. Prime의 vf-init도 TaskData, Task, Taskset의 뼈대를 만들지만 업무별 reward와 load 구현은 작성자에게 남긴다. 둘 다 “무엇을 성공으로 볼지”를 자동으로 결정해 주는 명령은 아니다.9
각 단계에서 누가 어떤 파일을 만드는가
저자가 제안한 제작 절차다. 단계를 선택하면 framework가 해 주는 일과 직접 설계할 일, 실패했을 때 돌아갈 곳이 나온다.
- 사람의 책임
- 만드는 파일 또는 기록
- Framework가 맡는 범위
- LLM 사용 여부
- 통과 여부를 확인하는 방법
실행 순서와 사전 설치. 7번 실제 모델 평가와 8번 학습은 이 글에서 미실행이다. init 성공은 task 완성 판정이 아니다.
이 글의 원장을 만드는 과정에서 가장 먼저 정할 것은 Docker 이미지나 모델 이름이 아니다. 취소된 거래를 빼는 규칙과 결과 JSON의 모양이다. 이를 지시문에 쓰고, 그 규칙을 구분할 입력을 만든다. e4의 과거 settled와 최신 void가 함께 있는 이유도 이 조건을 검사하기 위해서다.
그다음 기준 풀이와 채점기를 따로 만든다. 올바른 JSON이 통과하는지만 보면 부족하다. USD를 1300으로 잘못 합친 답, 최신 revision을 무시한 답, 숫자를 문자열로 쓴 답, 아무것도 쓰지 않은 답을 넣어야 한다. reward: 1이라는 필드를 덧붙여 스스로 성공을 선언한 답도 허용하지 않는다. 이 원장의 출력 계약은 totals와 included 두 키뿐이다.
어느 단계로 돌아갈지도 다르다. oracle이 틀리면 풀이와 정답을 함께 살핀다. 무동작인데 통과하면 초기 상태와 테스트를 살핀다. 컨테이너를 못 띄우면 Dockerfile과 실행 환경을 본다. 정직한 풀이가 지시문에 없던 조건 때문에 실패하면 지시문과 채점 기준을 다시 맞춘다. 이 반복 절차는 글에서 제안하는 제작 방법이며, 세 제품에 같은 버튼으로 내장된 기능은 아니다.
첫 task를 실제로 만드는 명령
원장 task 예제 ZIP 다운로드 / 세 프레임워크의 설치와 실행법
압축을 풀고 agent-eval-task-notebook 폴더로 이동한 다음 아래 명령을 실행한다. 실제 고객 데이터와 모델 키는 필요 없다.
unzip agent-eval-task-notebook.zip
cd agent-eval-task-notebook첨부 예제는 기존 폴더를 덮어쓰지 않는다. 압축을 푼 예제 폴더에서 아래 명령을 실행하면 /tmp/my-first-ledger에 위 그림의 13개 파일이 생긴다. Python만 사용하며 모델이나 Docker를 아직 실행하지 않는다.
python3 rewrite-v2/examples/make_task.py --output /tmp/my-first-ledger
정답 JSON을 파일에 저장하고 채점기부터 연결해 볼 수 있다.
printf '%s\n' '{"totals":{"EUR":350,"USD":1100},"included":["e1@2","e2@1","e5@1"]}' > /tmp/ledger-answer.json
python3 rewrite-v2/examples/check_answer.py /tmp/ledger-answer.json
# {"reward": 1.0, "reason": "correct"}
USD 값을 1300으로 바꾸면 wrong_result가 나온다. 이것은 채점기만 확인한 단계다. 로컬 Docker와 고정된 Harbor 의존성을 준비한 뒤에는 같은 폴더를 실제 trial로 실행한다.
HARBOR_TELEMETRY=0 experiments/harbor/.venv/bin/harbor run \
-p /tmp/my-first-ledger -a oracle -e docker -k 1 -n 1 \
--max-retries 0 --jobs-dir /tmp/ledger-jobs --job-name first-oracle --quiet
oracle은 solution/solve.sh를 실행하는 adapter다. 이 명령에는 실제 LLM 호출이 없다. 실행 후 trial 폴더의 artifacts/app/submission/output.json, verifier/contract.json, result.json을 열면 입력에서 결과와 판정까지 이어진 것을 확인할 수 있다. 설치 명령과 오답 task 실행은 예제 실행법에 있다.
Prime 예제도 처음에는 Task.score()를 직접 실행한다. 그다음 null harness와 로컬 scripted endpoint를 연결해 요청, 응답, Trace, reward가 이어지는지 확인한다. 이름이 null이어도 해당 harness는 모델 endpoint를 호출한다. 무료 모드라는 뜻으로 읽으면 안 된다. 제공한 실행기는 원격 업로드를 끄고 로컬 runtime과 loopback endpoint를 명시한다.10
5. LLM은 세 자리에 서로 다르게 들어간다
Task를 작성하는 LLM, 평가받는 agent의 LLM, 답을 판정하는 judge를 하나의 상자로 그리면 누가 누구를 검사하는지 사라진다. 이 원장에서는 작성에 LLM을 쓸 수 있고, 완성된 task에 평가 대상 모델을 연결할 수 있다. 그러나 정답 검사는 코드로 끝난다.
누가 만들고, 누가 풀고, 누가 판정하는가
작성 단계 → 실행 단계 → 채점 단계. 같은 모델을 설정해도 이 세 역할의 입력, 목적과 검증 책임은 다르다.
작성 도우미 LLM
- 입력
- 업무 규칙, 허용한 입력 예시
“최신 revision만 쓰고 void는 뺀다.” - 출력
- instruction, 데이터, reference, test의 초안
- 다음 확인
- 사람이 독립 정답과 반례로 검사한다.
Harbor init은 LLM을 호출하지 않는다. Prime proposer_solver는 별도 공식 생성 사례다.
평가 대상 LLM
- 입력
- 지시문 + 보이는 상태 + 허용 도구의 결과
- 출력
- 도구 호출 또는 최종 JSON 응답
- 다음 확인
- 실제 제출물과 runtime error를 기록한다.
이번 RUN은 oracle, MockLLM, localhost stub을 사용했다. 실제 추론은 하지 않았다.
Judge LLM
- 입력
- 제출물 + rubric + 필요한 reference
- 출력
- 판정과 이유, 해석한 score
- 다음 확인
- 사람 판정과 비교하고 편향, 공략, 오류를 검사한다.
점선은 대안 경로다. evaluator와 다른 모델인지 실제 설정으로 확인해야 한다.
grade_artifact() → reward + reason정답이 규칙으로 정해지므로 judge 호출 없이 검사한다.초안 작성, agent trial, judge 호출의 비용은 분리한다. 이번 실행의 유료 API 호출 없음은 실제 모델의 비용 측정이 아니다.
작성 도우미에게는 업무 규칙과 허용된 예시를 준다. 지시문, 초기 파일, reference 코드, 테스트 초안을 받을 수 있다. 출력이 곧 정답은 아니다. 취소 규칙을 누락한 지시문과 같은 버그를 가진 풀이, 테스트를 한 모델이 한꺼번에 만들면 셋이 함께 틀릴 수 있다. 사람은 독립 정답과 오답 대조군으로 이를 확인해야 한다.
이 역할이 실제 코드로 구현된 사례도 있다. Verifiers의 공식 proposer_solver는 proposer 모델이 {problem, answer}를 만들고 이를 새 task 데이터로 변환한다. 해당 변환 코드가 JSON과 정수 형태를 확인하는 것과, answer가 수학적으로 옳다고 독립 검증하는 것은 다르다. “도구로 검산하라”는 prompt를 넣었다고 정답 검증이 끝나는 것도 아니다.11
평가 대상 agent는 실행 때 지시문과 관찰, 도구 결과를 받는다. Harbor에서는 선택한 agent가 터미널 도구를 이용해 파일을 바꿀 수 있다. 이번 Verifiers와 Inspect adapter에서는 prompt를 받고 JSON 문자열을 반환한다. 실제 모델의 선택은 여기서 하며, 같은 task라도 모델과 harness, 도구, 실행 예산이 바뀌면 별도 조건으로 기록해야 한다.
Judge는 제출물과 rubric, 필요하면 reference를 받아 판정을 내린다. 정답을 코드로 적기 어려운 품질 평가에 선택할 수 있다. Prime의 judge 구현은 별도 모델 호출과 verdict 해석을 제공하고, Inspect도 모델 채점 scorer를 제공한다. 다만 role 이름이 grader라고 해서 평가 대상 모델과 다른 모델이 자동 확보되는 것은 아니다. 모델 설정과 실제 호출을 확인해야 한다.12
작성 중에 LLM을 쓰는 나머지 자리
| 맡길 일 | 주는 입력 → 받는 결과 | 사람이 확인할 것 |
|---|---|---|
| 환경과 데이터 초안 | 스키마, 허용한 업무 규칙 → 초기 파일, Dockerfile 후보 | 실제 빌드, 데이터 유효성, 권한과 불필요한 외부 접근 |
| reference 생성 | 지시문과 예제 → 기준 풀이 코드 | 별도 계산한 정답, 경계 사례, oracle 실패 원인 |
| rubric 개선 | 초안 rubric과 반례 → 수정 제안 | 성공 기준이 업무 목적을 유지하는지, 숨은 조건이 생기지 않았는지 |
| 오류 분석 | 필요한 범위의 로그, 오답과 오류 → 원인 가설 | 실행 기록에 원인이 있는지, 수정 후 대조군을 다시 통과하는지 |
이 표 전체가 각 프레임워크에 내장됐다는 뜻은 아니다. 예를 들어 Harbor의 check는 LLM 기반 task 품질 검토 기능이며, 이 snapshot의 기본 설정은 Claude Code와 claude-sonnet-4-6이다. 무료 문법 검사처럼 무심코 실행할 명령이 아니다. 우리는 이를 실행하지 않았다. 위 표의 환경 작성과 오류 분석 절차는 사람이 별도 도구와 검토를 연결하는 작업이다.13
호출 비용도 역할별로 나눠야 한다. 작성용 호출, 반복 trial의 agent 호출, judge 호출은 서로 다른 입력과 출력 토큰을 소비한다. 컨테이너 실행 비용은 또 별도다. 이번 실험은 scripted 응답과 코드 채점으로 연결을 검증했으며 유료 모델 비용이나 실제 모델 성능을 측정하지 않았다. 유료 모델 API를 호출하지 않았다는 사실을 실제 평가가 저렴하다는 결론으로 바꿀 수는 없다.
6. 예제를 자기 업무로 바꾸는 지점
원장에 refunded라는 상태가 추가됐다고 해보자. 가장 먼저 할 일은 새 조건을 업무상 어떻게 다룰지 결정하는 것이다. 그다음 지시문과 입력 사례, 기대 정답, 기준 풀이, 채점기와 대조군을 함께 바꾼다. 프레임워크가 대신 정해 주는 부분은 없다.
Harbor에서는 제출 파일 경로와 verifier로 넘길 artifact가 추가되는지도 확인한다. Prime에서는 TaskData의 필드와 prompt, @reward가 서로 맞는지 확인한다. 이번 Inspect adapter에서는 Sample에 담는 입력과 solver의 결과, scorer가 읽는 별도 expected.json을 맞춘다. 이 adapter는 Sample.target을 채점에 사용하지 않는다. 모델이 정답을 알아낸 것인지, 실수로 정답을 입력에 넣은 것인지도 다시 검사한다.
이번 추가 실행에서는 Harbor의 정상 풀이와 의도한 오답이 각각 1과 0으로 판정됐다. Prime에서는 13개 대조군을 두 번씩 score 경로와 로컬 harness 경로로 확인했다. Inspect의 기존 26회 실행 기록도 같은 원장과 grader에 연결돼 있다. 이 결과는 task의 배선과 판정이 계획대로 움직였다는 증거다. 실무 난도, 실제 LLM의 도구 사용, judge의 신뢰도, 학습 효과는 아직 측정하지 않았다.14
첫 task를 만들고 나면 재사용할 부분도 구체적으로 보인다. 실행 순서와 로그, 모델 연결은 프레임워크에서 가져온다. 업무의 초기 상태, 성공 조건, 정답과 반례는 작성자가 만든다. 다음 task에 남겨야 할 것은 이 다섯 가지가 맞물려 돌아가는 예제와, 어디를 바꾸면 어떤 검사를 다시 해야 하는지에 대한 기록이다.
코드와 실행 근거
이 글은 2026-09-17에 고정한 Harbor f68ece24, Verifiers 5a69fb4d, Inspect 456d982e를 기준으로 한다. 아래 GitHub 링크는 전체 commit SHA에 고정했다. 공식 hello-world의 구조와 우리가 작성한 원장 예제를 구분했고, 직접 실행하지 않은 실제 LLM과 judge에 성능 수치를 붙이지 않았다.
-
Harbor task 경로. 원장 task를 만드는 코드는 make_task.py다. ↩
-
Harbor 별도 verifier. 저자 예제의 task.toml과 두 Dockerfile은 첫 그림에서 열 수 있다. ↩
-
공식 hello-world, 검사 코드, shared 기본값. 공식 예제는 구조만 읽었으며 실행 중 의존성 설치를 포함한다. ↩
-
pinned package의 v1 안내. 라이브 문서와 버전 차이는 공개 코드 해설에 남긴다. ↩
-
TaskData와 Task, Taskset. 원장 구현은 taskset.py다. ↩
-
Inspect TaskState.target. 정답의 의미상 구분과 OS 격리의 구분은 신뢰 경계의 코드 해설에 정리했다. ↩
-
Harbor init, 빈 테스트, Verifiers init의 정확한 템플릿 경로와 실행은 Prime의 고정 코드 해설에 기록한다. ↩
-
Prime 원장 예제, 실제 로컬 harness 결과. 이 endpoint는 미리 만든 응답을 반환하며 추론 모델이 아니다. ↩
-
공식 proposer_solver. 구체적인 함수와 검증 범위는 공개 코드 해설에 연결한다. ↩
-
Inspect 모델 채점, grader role 처리. Prime judge의 호출과 usage 분리는 공개 코드 해설에 기록한다. ↩
-
Harbor check의 agent/model 설정. 실제 실행하지 않았다. ↩
-
Harbor 첫 task 실행, 기존 원장 실험의 범위와 결과, Prime score 대조군. 26회는 정답 4회와 의도한 실패 22회를 포함하며 모델 성공률로 읽지 않는다. ↩