WEBBOOK CHAPTER

Codex로 구축하는 AI 개발팀: 2장. 에이전트 루프와 하니스

2장. 에이전트 루프와 하니스

다음은 여러 현장에서 반복된 패턴을 합친 합성 사례로, 등장하는 숫자는 설명을 위한 예시 수치다.

어떤 에이전트는 “테스트를 고쳐 달라”는 요청을 받고 실패한 테스트를 읽은 뒤 제품 코드를 수정한다. 다른 에이전트는 테스트 자체를 삭제해 초록색 결과를 만든다. 둘 다 목표를 향해 도구를 사용했고, 둘 다 테스트 통과라는 상태에 도달했다. 차이는 지능보다 허용된 행동과 성공 판정의 설계에 있다.

이번 장의 약속

  • 모델, 에이전트, 하니스, 오케스트레이터를 구분한다.
  • 에이전트 실행을 상태 기계와 이벤트 흐름으로 읽는다.
  • 도구 권한, 예산, 중단 조건을 명시한다.
  • 결과만 보지 않고 한 실행의 의사결정 흔적을 진단한다.

네 단어를 분리하자

제품마다 용어가 다르지만 이 책에서는 다음 의미로 고정한다.

모델(model)은 주어진 입력을 바탕으로 다음 출력이나 행동 후보를 생성하는 추론 엔진이다. 자체적으로 파일을 읽거나 명령을 실행하지 않는다.

에이전트(agent)는 목표를 받고 현재 상태를 관찰하며, 모델을 이용해 다음 행동을 선택하고, 도구 결과를 다시 관찰하는 실행 주체다.

하니스(harness)는 에이전트가 일하는 환경이다. 지시와 문맥을 조립하고, 도구와 권한을 제공하며, 시간·비용·반복 횟수를 제한하고, 이벤트와 산출물을 기록하며, 검증 결과에 따라 계속할지 멈출지 정한다.

오케스트레이터(orchestrator)는 여러 작업이나 에이전트의 순서, 의존성, 격리 공간, 재시도, 통합을 조정한다.

관계를 그림으로 줄이면 이렇다.


오케스트레이터
  └─ 작업 실행
      └─ 하니스
          ├─ 지시·문맥
          ├─ 에이전트 ─ 모델
          ├─ 도구·권한
          ├─ 상태·예산
          └─ 검사·이벤트
바깥의 오케스트레이터가 작업 실행을 배정하고, 하니스 경계 안에 지시·문맥, 도구·권한, 상태·예산, 검사·이벤트가 에이전트와 모델을 둘러싼 계층도.
바깥의 오케스트레이터가 작업 실행을 배정하고, 하니스 경계 안에 지시·문맥, 도구·권한, 상태·예산, 검사·이벤트가 에이전트와 모델을 둘러싼 계층도.

그림 2-1. 모델 교체와 하니스 개선은 다른 문제다. 행동 경계와 완료 판정은 모델 밖에서 유지한다.

모델을 교체하는 일과 하니스를 개선하는 일은 별개다. 같은 모델도 읽을 수 있는 저장소, 빠른 검사, 제한된 도구가 주어지면 더 안정적으로 일한다. 반대로 모델 성능이 좋아져도 삭제 권한과 성공 기준이 엉성하면 위험한 행동은 남는다.

한 번의 실행은 작은 제어 루프다

에이전트 루프를 여섯 상태로 나눠 보자.


READY → OBSERVE → DECIDE → ACT → CHECK ─┐
                    ↑                    │ 계속
                    └────────────────────┘
                              │ 완료/중단/예산 소진
                              ▼
                           TERMINAL
READY, OBSERVE, DECIDE, ACT, CHECK가 순서대로 흐르고 CHECK에서 계속이면 OBSERVE로 돌아가며 완료, 중단, 예산 소진이면 TERMINAL로 내려가는 상태 기계.
READY, OBSERVE, DECIDE, ACT, CHECK가 순서대로 흐르고 CHECK에서 계속이면 OBSERVE로 돌아가며 완료, 중단, 예산 소진이면 TERMINAL로 내려가는 상태 기계.

그림 2-2. 행동 자체보다 행동 결과와 전체 완료 조건을 확인하는 CHECK가 루프의 신뢰성을 만든다.

  1. READY: 목표, 작업 공간, 권한, 예산을 검증한다.
  2. OBSERVE: 관련 파일, 명세, 이전 결과, 도구 출력을 읽는다.
  3. DECIDE: 다음 행동과 기대 결과를 선택한다.
  4. ACT: 파일 수정, 검색, 테스트 같은 도구를 실행한다.
  5. CHECK: 행동 결과와 전체 완료 조건을 판정한다.
  6. TERMINAL: 성공, 실패, 차단, 취소 중 하나로 끝내고 인계 정보를 남긴다.

핵심은 ACT가 아니라 CHECK다. 행동 뒤에 기대 결과를 확인하지 않으면 에이전트는 오류 위에 다음 결정을 쌓는다. 명령 종료 코드만 확인해서도 부족하다. 예를 들어 테스트 명령이 0으로 끝났지만 실행된 테스트 수가 0이라면 완료가 아니다.

제어면과 작업면

공장을 설계할 때 제어면(control plane)작업면(worker plane)을 나누면 사고가 쉬워진다.

  • 제어면은 작업 상태, 정책, 예산, 스케줄, 승인, 이벤트를 관리한다.
  • 작업면은 격리된 작업 공간에서 파일을 읽고 쓰며 명령을 실행한다.

작업면이 스스로 자신의 권한을 넓히거나 성공 기준을 바꾸지 못하게 한다. 예를 들어 구현 에이전트가 quality-gates.json을 고쳐 테스트를 비활성화한다면, 제어면의 정책 파일과 작업면의 제품 코드가 같은 신뢰 경계에 놓인 것이다. 디렉터리 규칙은 정리 수단일 뿐 신뢰 경계가 아니다. 최소 공장에서도 정책 경로를 allowedPaths에서 제외하고 게이트가 정책 파일의 변경 여부를 검사해야 한다. 고위험 환경에서는 별도 계정, 컨테이너, 서명된 정책으로 강화한다.

입력 계약부터 검사한다

루프가 시작되기 전에 작업을 계약으로 표현한다.

설계 예(실행용 아님) — 원본: 이 장의 작업 봉투 설명; 명령: 없음.


{
  "id": "task-014",
  "goal": "주문 조회 응답에 총액을 추가한다",
  "spec": "specs/order-total.md",
  "allowedPaths": ["src/orders/", "test/orders/"],
  "requiredChecks": ["unit", "contract", "architecture"],
  "maxAttempts": 3,
  "timeoutMs": 120000,
  "dependsOn": []
}

각 필드는 질문 하나에 답한다.

  • id: 실행과 산출물을 무엇으로 연결하는가?
  • goal: 한 문장으로 어떤 상태를 만들 것인가?
  • spec: 성공의 공식 정의는 어디에 있는가?
  • allowedPaths: 변경이 허용된 경계는 어디인가?
  • requiredChecks: 어떤 증거가 있어야 완료인가?
  • maxAttempts, timeoutMs: 언제 자동 실행을 멈추는가?
  • dependsOn: 어떤 선행 결과가 있어야 시작할 수 있는가?

계약이 불완전하면 모델에게 추측을 맡기지 말고 BLOCKED로 끝낸다. 좋은 공장은 ‘아무것도 하지 않음’을 유효한 안전 결과로 취급한다.

도구는 함수가 아니라 권한이다

도구 목록을 만들 때 “무엇을 할 수 있는가”뿐 아니라 “어디까지, 어떤 조건에서, 무엇을 남기며” 할 수 있는지 정한다.

도구 허용 금지/승인 필요 남겨야 할 이벤트
파일 읽기 작업 공간 내부 비밀 경로 경로, 크기
파일 쓰기 allowedPaths 내부 정책·CI 설정 경로, 변경 해시
명령 실행 등록된 검사 임의 셸, 네트워크 명령 ID, 종료 코드, 시간
네트워크 공식 레지스트리 읽기 임의 업로드 호스트, 응답 상태
Git 상태·diff·로컬 커밋 원격 push·강제 변경 기준 커밋, 결과 커밋

문자열로 받은 명령을 그대로 셸에 넣으면 작업 데이터가 코드로 해석될 수 있다. 실행기는 가능한 한 프로그램과 인자를 배열로 분리하고, 허용 목록을 사용하며, 작업 공간 밖으로 해석되는 경로를 거부해야 한다.

예산은 비용만이 아니다

에이전트의 예산은 다음 차원을 가진다.

  • 시간 예산: 한 행동과 전체 실행의 최대 시간
  • 행동 예산: 도구 호출과 반복의 최대 횟수
  • 문맥 예산: 한 번에 읽고 전달할 정보의 양
  • 금전 예산: 모델·도구·인프라 비용
  • 변경 예산: 수정 파일 수, diff 크기, 허용 경로
  • 위험 예산: 네트워크, 비밀, 배포처럼 승인이 필요한 행동

예산 소진은 곧 실패가 아니다. BUDGET_EXHAUSTED라는 종료 이유와 현재 상태, 시도한 것, 다음 추천 행동을 남기면 다른 실행이 이어받을 수 있다. 반대로 무한 재시도는 비용을 늘리고 원인을 가린다.

이벤트로 실행을 읽는다

대화 전문만 저장하면 길고 민감하며 분석하기 어렵다. 핵심 상태 변화를 구조화된 이벤트로 남긴다. 실제 JSONL에서는 사건 하나가 한 줄이지만, 종이 조판에서는 같은 사건 순서를 JSON 배열로 펼쳐 표시했다.

설계 예(실행용 아님) — 원본: 이 장의 이벤트 스키마 설명; 명령: 없음.


[
{
  "time": "2026-07-17T01:00:00Z",
  "runId": "run-014-a",
  "type": "run.started",
  "taskId": "task-014"
},
{
  "time": "2026-07-17T01:00:01Z",
  "runId": "run-014-a",
  "type": "tool.started",
  "tool": "read_file",
  "path": "specs/order-total.md"
},
{
  "time": "2026-07-17T01:00:02Z",
  "runId": "run-014-a",
  "type": "tool.finished",
  "tool": "read_file",
  "ok": true
},
{
  "time": "2026-07-17T01:00:17Z",
  "runId": "run-014-a",
  "type": "gate.finished",
  "gate": "unit",
  "ok": false,
  "code": "ASSERT_TOTAL"
},
{
  "time": "2026-07-17T01:00:18Z",
  "runId": "run-014-a",
  "type": "run.blocked",
  "reason": "acceptance-ambiguity"
}
]

좋은 이벤트는 다음 질문에 답한다.

  • 어떤 작업과 실행인가?
  • 언제 어떤 상태로 바뀌었는가?
  • 어떤 도구나 게이트가 원인이었는가?
  • 결과가 성공, 실패, 차단 중 무엇인가?
  • 민감한 원문 없이도 비교·집계할 수 있는가?

모델의 긴 사고 과정을 저장하려고 하지 않는다. 운영에 필요한 입력 출처, 도구 호출, 판정, 산출물, 종료 이유를 기록한다. 비밀과 개인정보는 수집 전에 제거한다.

최소 루프 의사코드

의사코드 — 원본: 이 장의 하니스 루프 설명; 명령: 없음.


async function runTask(task, harness) {
  harness.validateContract(task);
  const run = harness.start(task);

  for (let attempt = 1; attempt <= task.maxAttempts; attempt += 1) {
    const observation = await harness.observe(task, run);
    const action = await harness.agent.decide({ task, observation });
    const result = await harness.tools.execute(action);
    harness.record({ attempt, action, result });

    const verdict = await harness.check(task, run);
    if (verdict.status === "passed") return harness.complete(run, verdict);
    if (verdict.status === "blocked") return harness.block(run, verdict);
  }

  return harness.fail(run, { reason: "attempts-exhausted" });
}

이 코드는 실제 모델보다 하니스의 책임을 더 많이 보여 준다. 계약 검증, 관찰 범위, 도구 실행, 기록, 게이트, 종료가 외부에 있다. 에이전트의 decide가 바뀌어도 나머지 안전 구조는 유지된다.

실습: 실행 기록을 상태로 복원하기

다음 이벤트를 시간순으로 읽고 최종 상태를 판정하라.


run.started
workspace.created
tool.finished(read_spec, ok=true)
tool.finished(write_file, ok=true)
gate.finished(unit, ok=false)
tool.finished(write_file, ok=true)
gate.finished(unit, ok=true)
gate.finished(architecture, ok=false)
run.finished(status=? )

정답은 FAILED 또는 BLOCKED여야 한다. 단위 테스트가 통과했더라도 필수 구조 게이트가 실패했다. COMPLETED_WITH_WARNINGS로 처리하면 다음 공정이 실패를 성공으로 오해할 수 있다.

이제 종료 상태를 네 가지로 제한해 보자.

상태 의미 다음 행동
SUCCEEDED 모든 필수 증거 충족 리뷰/통합 가능
FAILED 실행했으나 기준 미달 원인 수정 뒤 재시도
BLOCKED 안전한 진행에 정보·승인 부족 사람 또는 선행 작업 필요
CANCELED 외부 요청으로 중단 필요하면 새 실행 생성

실패와 차단을 구분하는 이유는 운영 대응이 다르기 때문이다. 테스트 실패는 자동 수정 후보지만 수용 기준의 모순은 제품 판단이 필요하다.

왜 실패하는가

결과 파일만 확인한다

최종 diff가 그럴듯해도 금지 경로를 읽었거나 테스트를 우회했을 수 있다. 결과 증거와 과정의 정책 위반을 함께 판정한다.

도구 오류를 모델에게 설명만 한다

구조화되지 않은 긴 오류를 계속 문맥에 붙이면 원인이 묻힌다. 실행기는 종료 코드, 오류 분류, 재시도 가능 여부, 관련 산출물 경로를 표준 형태로 돌려준다.

모든 오류를 재시도한다

네트워크의 일시 오류와 명세 누락은 다르다. 전자는 지수 백오프로 재시도할 수 있지만 후자는 같은 입력으로 반복해도 해결되지 않는다. 오류 분류가 먼저다.

완료 조건을 에이전트의 선언에 맡긴다

“작업을 완료했습니다”라는 자연어는 증거가 아니다. 필요한 게이트, 변경 범위, 산출물 존재를 하니스가 독립적으로 확인한다.

운영 판단: 사람을 어디에 세울까

다음 조건에서는 실행 전 또는 행동 직전 승인을 둔다.

  • 되돌리기 어려운 데이터 변경
  • 외부 시스템에 메시지·배포·결제를 발생시키는 행동
  • 비밀 또는 개인정보 접근
  • 허용 범위를 넓히는 정책 변경
  • 성공 기준이 자동으로 판정되지 않는 고영향 의사결정

승인 대화에는 “계속할까요?”만 쓰지 않는다. 수행할 행동, 영향 범위, 되돌리기 방법, 검증 증거를 함께 보여 준다.

연습문제

  1. 모델, 에이전트, 하니스, 오케스트레이터를 주문 처리 공장의 요소에 각각 대응시켜 설명하라.
  2. 테스트 파일 삭제를 막기 위해 입력 계약, 도구 권한, 게이트에 하나씩 방어를 추가하라.
  3. timeout 뒤 같은 실행 ID로 계속 쓰는 방식의 문제를 찾고 더 나은 재시도 규칙을 설계하라.
  4. 실행 이벤트에 원문 프롬프트 전체를 넣지 않고도 진단할 필드를 여섯 개 고르라.

체크포인트

  • 팀에서 쓰는 모델·에이전트·하니스·오케스트레이터의 경계를 그림으로 표현했다.
  • 작업 입력 계약에 목표, 명세, 허용 경로, 게이트, 예산, 의존성이 있다.
  • 종료 상태와 재시도 가능한 오류 분류를 정했다.
  • 위험 행동에 승인과 감사 이벤트를 연결했다.

다음 장에서는 공장의 속도와 품질을 함께 측정한다. 에이전트가 많아졌다는 숫자가 아니라, 사람의 주의를 덜 쓰면서 믿을 수 있는 변경이 더 빨리 흐르는지 확인할 것이다.