AI 에이전트가 복잡한 작업을 수행하게 하면서 한 가지 사실을 깨달았다. 프롬프트를 잘 짜는 것만으로는 한계가 있다. 에이전트가 몇 시간 동안 돌아가는 작업을 수행할 때 생기는 문제는 모델이 멍청해서가 아니라, 에이전트를 둘러싼 구조(harness)가 없기 때문이다. 20252026년을 기점으로 "하네스 엔지니어링(Harness Engineering)"이라...
AI 에이전트가 복잡한 작업을 수행하게 하면서 한 가지 사실을 깨달았다. 프롬프트를 잘 짜는 것만으로는 한계가 있다. 에이전트가 몇 시간 동안 돌아가는 작업을 수행할 때 생기는 문제는 모델이 멍청해서가 아니라, 에이전트를 둘러싼 구조(harness)가 없기 때문이다.
2025~2026년을 기점으로 "하네스 엔지니어링(Harness Engineering)"이라는 개념이 AI 에이전트 개발에서 독립적인 분야로 자리를 잡고 있다. 이 글에서는 Anthropic 엔지니어링 블로그 글 두 편과 Martin Fowler의 시각을 중심으로 하네스가 무엇인지, 왜 필요한지, 실제로 어떻게 설계하는지 정리한다.
하네스는 AI 에이전트가 제대로 동작할 수 있도록 감싸는 실행 구조 다. 구체적으로는:
프롬프트 엔지니어링이 "모델에게 무엇을 말할 것인가"에 관한 것이라면, 하네스 엔지니어링은 "모델이 어떤 환경에서 어떤 흐름으로 작동할 것인가"에 관한 것이다.
Martin Fowler는 이를 이렇게 정리한다. 하네스는 에이전트를 제약하고 신뢰할 수 있게 만드는 도구와 실천의 집합이다. 에이전트에게 무한한 자유를 주는 것이 아니라, 일관된 결과물을 내도록 안내하는 구조를 만드는 것이다.
Anthropic의 엔지니어 Prithvi Rajasekaran이 장시간 에이전트 시스템을 만들면서 반복적으로 마주친 실패 패턴은 두 가지다.
컨텍스트 윈도우가 채워지면서 모델이 일관성을 잃는다. 더 심각한 것은 "컨텍스트 불안(Context Anxiety)" — 모델이 컨텍스트 한계에 가까워진다고 느끼면 작업을 제대로 완수하지 못했음에도 "완료됐습니다"라고 선언해버린다.
실제로 Claude Sonnet 4.5를 테스트했을 때 컨텍스트 압축(compaction)만으로는 충분하지 않았고, 컨텍스트 리셋 이 하네스 설계의 핵심 요소가 됐다.
에이전트에게 자신이 생성한 결과물을 평가하게 하면 품질이 낮아도 높게 평가하는 경향이 강하다. "자신이 만든 작업을 평가하라고 하면 에이전트는 자신 있게 그 작업을 칭찬하는 경향이 있다."
이 두 문제를 해결하는 핵심 통찰은 단순하다. 생성과 평가를 분리하라.
Rajasekaran은 프론트엔드 디자인 품질을 높이기 위해 GAN(생성적 적대 신경망) 구조에서 아이디어를 가져왔다. 생성자(Generator)와 평가자(Evaluator)를 분리한 것이다.
평가자는 네 가지 기준으로 결과물을 채점한다:
| 기준 | 설명 |
|---|---|
| 디자인 품질 | 시각적 일관성, 정체성이 있는가 |
| 독창성 | AI 기본값이 아닌 커스텀한 결정이 있는가 |
| 완성도 | 기술 구현 수준이 충분한가 |
| 기능성 | 실제로 사용할 수 있는가 |
평가자는 Playwright MCP를 통해 실제 렌더링된 페이지를 직접 조작해본 뒤 점수를 매겼다. 생성자는 평가자의 피드백을 받아 반복적으로 개선했다.
핵심 발견: 독립된 평가자를 회의적으로 튜닝하는 게, 생성자가 스스로 비판적이 되도록 만드는 것보다 훨씬 쉽다.
더 복잡한 작업(풀스택 앱 개발)에는 3개 에이전트 구조가 등장한다.
[Planner]
└── 간단한 브리프를 상세 스펙으로 확장
└── AI 기능 기회 식별
↓
[Generator]
└── React + FastAPI + SQLite로 기능 구현
└── 기능을 점진적으로 추가
↓
[Evaluator]
└── Playwright로 기능 테스트
└── 기준에 따라 점수 및 피드백 제공
└── 다시 Generator로 피드백 전달에이전트 간 통신은 구조화된 파일 을 통해 이뤄진다. Planner가 "스프린트 계약(sprint contract)"을 파일로 남기면, Generator는 이를 읽고 구현 전에 성공 조건을 미리 파악한다.
| 방식 | 소요 시간 | 비용 | 품질 |
|---|---|---|---|
| 단일 에이전트 | 20분 | $9 | 핵심 기능 동작 안 함 |
| 풀 하네스 | 6시간 | $200 | 물리 엔진, 반응형 UI, AI 기능 모두 동작 |
비용이 22배 더 들었지만, 단일 에이전트로는 불가능했던 결과물이 나왔다.
에이전트가 여러 세션에 걸쳐 작업할 때의 핵심 문제는 세션 간 기억이 없다는 것 이다. 새 세션이 시작될 때마다 이전에 뭘 했는지 모른다. Anthropic의 두 번째 글은 이 문제를 다룬다.
비유: "교대 근무하는 엔지니어들. 새 엔지니어가 올 때마다 이전 교대의 기억이 없다."
첫 세션에서 딱 한 번 실행된다. 이후 모든 세션이 의존할 인프라를 만드는 것이 역할이다.
Initializer가 만드는 것:
├── feature_list.json # 전체 기능 목록 (초기에는 모두 "failing")
├── claude-progress.txt # 에이전트 행동 기록
├── init.sh # 개발 환경을 한 번에 실행하는 스크립트
└── 초기 git commit # 생성된 모든 것을 기록feature_list.json에는 200개 이상의 기능이 "failing" 상태로 표시된다. 에이전트에게 이 리스트 없이 작업하면 "완료됐다"고 선언하기 너무 쉽기 때문이다. 그리고 엄격한 규칙이 들어간다: "테스트를 삭제하거나 수정하는 것은 허용되지 않는다. 이는 기능 누락이나 버그로 이어질 수 있다."
이후 모든 세션에서 실행된다. 각 세션 시작 시 정해진 절차를 따른다:
1. pwd 실행 (작업 디렉토리 확인)
2. git log + progress 파일 읽기 (현재 상태 파악)
3. feature_list.json에서 우선순위 높은 미완성 기능 선택
4. init.sh 실행 (환경 세팅)
5. 기본 e2e 테스트 실행 (앱이 깨진 상태에서 시작하지 않도록)
6. 기능 하나 구현 → 커밋 → progress 업데이트앱이 깨진 상태에서 새 기능을 추가하기 시작하는 것을 방지하는 게 핵심이다.
| 실패 패턴 | Initializer 해결책 | Coding Agent 해결책 |
|---|---|---|
| 너무 일찍 완료 선언 | feature_list.json 생성 | 리스트를 읽고 하나씩 처리 |
| 버그 있는 코드 방치 | git + progress 노트 | 검증 테스트로 시작 |
| 앱 실행 방법 모름 | init.sh 스크립트 작성 | 세션 시작 시 init.sh 실행 |
| 기능 완료 조기 선언 | feature list scaffold | e2e 테스트로 자기 검증 |
Fowler는 OpenAI 팀의 사례를 바탕으로 하네스를 세 가지 범주로 정리한다.
에이전트가 올바른 컨텍스트에 접근할 수 있도록 만드는 작업이다.
에이전트가 의도된 설계 경계를 벗어나지 않도록 하는 구조적 가드레일이다.
코드베이스가 시간이 지나면서 썩는 것을 방지한다.
핵심 원칙: "에이전트가 어려움을 겪을 때, 그것을 신호로 받아들인다. 무엇이 없는지를 — 도구, 가드레일, 문서 — 파악하고 저장소에 피드백한다."
이론은 충분하다. 실제로 하네스를 만들 때 어떻게 접근하면 좋을지 정리한다.
장시간 작업은 단일 컨텍스트 안에서 완주하려 하지 말고, 명확한 체크포인트를 가진 독립 세션 으로 나눈다. 각 세션은:
를 명확히 해야 한다.
에이전트의 기억은 컨텍스트 윈도우뿐이다. 세션 간 유지할 것들은 반드시 파일 시스템(또는 DB)에 써둔다.
project/
├── .agent/
│ ├── progress.md # 완료된 것, 현재 상태
│ ├── feature_list.json # 남은 작업 목록
│ ├── decisions.md # 중요한 아키텍처 결정 기록
│ └── init.sh # 환경 재현 스크립트에이전트가 새 세션을 시작할 때 가장 먼저 이 파일들을 읽게 만든다.
자기 평가 편향을 없애려면 평가를 독립 에이전트에게 맡겨야 한다. 평가자 에이전트는:
평가자가 테스트 도구(Playwright, 단위 테스트 실행기)를 직접 사용할 수 있으면 더 좋다.
에이전트에게 기능을 한 번에 하나씩 구현하게 하고, 각 기능 완료 후 반드시 커밋하게 한다. 이유:
하네스는 한 번 만들고 끝이 아니다. 에이전트가 같은 지점에서 반복적으로 실패한다면 그것은 하네스에 무언가가 빠졌다는 신호 다. 도구인지, 컨텍스트 정보인지, 가드레일인지 파악해서 하네스에 추가한다.
이론을 쌓고 나면 자연스럽게 이런 질문이 생긴다. "내가 만든 것에는 하네스가 얼마나 있는가?" 그런데 더 중요한 질문은 "내 하네스는 어떻게 자라고 있는가"다. 하네스는 한 번 만들고 끝이 아니라 프로젝트 규모·기간·실패 경험에 따라 진화하는 구조다.
개인 프로젝트 두 개를 비교해봤다. v1은 CLI 도구(dooray-cli), v2는 웹툰 생성 Next.js 앱(webtoon-maker-v1)이다. 둘 다 내가 Claude Code와 함께 만들었고, 나중에 만든 쪽이 앞 프로젝트에서 얻은 실패 경험을 하네스에 명시적으로 녹여낸다.
TypeScript CLI 하나짜리 프로젝트다. 하네스라고 부를 만한 것은 거의 전부 루트 CLAUDE.md 한 장에 담겨 있다.
dooray-cli/
├── CLAUDE.md # 60줄 — 스택, 디렉터리, 컨벤션, 주의사항
├── .claude/skills/ # plan-and-build, release 2개
├── skills/ # 공개 스킬 (다른 사용자용)
├── prompts/ # task-create.md 한 장
└── tasks/ # 3개 (flat 구조, phase 개념 없음)이 단계에서 하네스의 역할은 단순하다. 에이전트가 "이 프로젝트에서 HTTP는 ky를 쓰고 에러는 DoorayCliError로 통일한다"는 규칙만 알면 된다. ADR도 없고, 아키텍처 레이어도 없고, 모델 라우팅도 없다. 프로젝트가 작고 기능 축적 속도가 빠르지 않으니 이 정도로 충분했다.
하지만 몇 달 지나면서 드러난 한계는 있었다.
CLAUDE.md 문장으로만 존재하면 에이전트는 잊어버린다.Next.js + Prisma + Gemini API 풀스택 앱이다. v1에서 겪은 한계가 그대로 v2 하네스의 설계 요구사항이 됐다.
1. 컨텍스트 문서를 용도별로 쪼갠다 — 상황별 컨텍스트 엔지니어링
v1은 CLAUDE.md 한 장에 모든 걸 썼다. v2는 루트 CLAUDE.md를 라우터로 만들고, 실제 사실 정보는 7개 문서로 분산한다.
webtoon-maker-v1/
├── CLAUDE.md # 규칙과 라우팅만 — 사실은 docs/ 참조
└── docs/
├── prd.md # 새 기능 추가 전
├── adr.md # 기술 결정 시
├── data-schema.md # API 연동/타입 정의 시
├── flow.md # UI/UX 수정 시
├── code-architecture.md # 코드 작성 시 항상
├── ai-strategy.md # AI 호출/프롬프트 수정 시
└── collaboration.md # 컴포넌트 신규/수정 시CLAUDE.md 상단에는 "언제 어떤 문서를 읽어야 하는지" 테이블이 있다. 에이전트가 docs/adr.md(713줄) 같은 대형 문서를 통째로 읽지 않아도, 작업 종류에 따라 필요한 섹션만 집어 읽을 수 있다. 이게 v1에 없던 컨텍스트 라우팅이다.
2. 실패 패턴을 ADR로 축적 — 상황별 필수 참조
v1에서 놓쳤던 "실패 경험 축적"을 v2는 상황 ↔ ADR 매핑 테이블로 해결한다.
| 상황 | 필수 확인 ADR |
| ----------------------------------------------- | ------------------------------------------ |
| Base UI Select 사용 | ADR-121 — items prop 없으면 id가 노출됨 |
| 인터랙티브 버튼 추가 | ADR-122 — native <button> 금지 |
| Gemini LLM 호출 추가 | ADR-043 + ADR-047 + ADR-072 |
| API route / Application layer 수정 | ADR-135 — 3-tier 경계 + ESLint 4종 |
| useEffect 작성 | ADR-115 — setState 금지, 파생 계산만 |이건 Fowler가 말한 엔트로피 관리의 실제 구현이다. 같은 버그가 두 번 일어나면 ADR로 결정론적으로 기록하고, 에이전트는 해당 상황에 진입하기 전에 강제로 그 ADR을 읽는다. v1에는 "주의사항" 섹션에 bullet 몇 줄로 녹아 있었을 내용이, v2에서는 명시적 검색 인덱스로 올라왔다.
3. 아키텍처 제약을 ESLint로 강제 — 말에서 코드로
v1의 "상대경로 금지"는 문장이다. v2의 3-tier 경계는 ESLint 규칙이다.
| 레이어 | 담당 | 금지 |
|---|---|---|
actions/ | Controller — "use server", Zod 파싱 | 비즈니스 로직, 트랜잭션 |
app/api/**/route.ts | Controller — HTTP 파싱, SSE 래핑 | @/actions/*·@/lib/db/* 직접 import |
lib/application/ | 트랜잭션 경계, 2+ domain 조합 | next/server·next/navigation |
lib/ai/ | 모델 호출, 프롬프트 조립 | DB 접근, revalidate |
lib/db/ | Prisma 쿼리, 순수 반환 | revalidate, 비즈니스 정책 |
에이전트가 경계를 넘어가면 pnpm run ci가 실패한다. 이게 Fowler의 아키텍처 제약이다. v1은 규칙을 "기억해 달라"고 부탁했고, v2는 규칙을 결정론적으로 실행한다.
4. task phase 원자화 — 컨텍스트 저하 대응
v1 task는 flat 3개였다. v2 task는 80개 이상이고, 각 plan은 여러 phase로 쪼개져 있다. 핵심 규칙은 다음과 같다.
이건 Anthropic 글의 Initializer-Executor 패턴과 정확히 같은 해결책이다. "교대 근무하는 엔지니어"에게 이전 교대의 기억이 없어도 git log와 phase 프롬프트만 읽으면 작업을 재개할 수 있다. v1에서 "task 하나가 너무 커서 세션 중간에 컨텍스트가 불안해지는" 경험이 v2의 phase 제한 규칙으로 환원됐다.
5. 모델 라우팅 — 비용까지 하네스로 본다
v2는 한 발 더 나간다. 같은 작업이라도 어떤 모델로 돌리느냐를 하네스가 지정한다.
- 논의·계획·docs 작성: main 세션 (opus 허용)
- task phase 실행: sonnet 기본 — rename, 리팩토링, 다중 파일 수정도 sonnet
- task phase에서 opus 사용 예외:
- 새 아키텍처 설계가 phase 안에 있는 경우
- 복잡 알고리즘 설계 (AI 파이프라인 신규 설계)
- 기계적 작업은 opus 금지Rajasekaran이 "강력한 모델에서는 플래너가 불필요해진다"고 관찰한 것의 반대 면이다. 모델이 좋아져도 어떤 작업에 어떤 모델을 쓸지는 여전히 하네스가 결정해야 한다. v1에는 없던 축이다.
두 프로젝트를 Fowler의 세 축에 맞춰 놓으면 진화의 방향이 보인다.
| 축 | v1 (dooray-cli) | v2 (webtoon-maker-v1) |
|---|---|---|
| 컨텍스트 엔지니어링 | 단일 CLAUDE.md | 7개 docs + 상황별 라우팅 테이블 |
| 아키텍처 제약 | 문장으로만 명시 | 3-tier + ESLint 경계 + pnpm run ci |
| 엔트로피 관리 | 없음 (커밋 메시지에만) | ADR + 상황↔ADR 매핑 테이블 |
| 실행 단위 | flat task 3개 | phase 원자화 + 5개 제한 + 자기완결 |
| 모델 라우팅 | 없음 | opus/sonnet 규칙 명시 |
핵심은 v2의 규칙들이 전부 v1에서 겪은 구체적 실패에 대응한다는 점이다. 하네스는 위에서 내려찍는 설계가 아니라 아래에서 쌓이는 것이다. 에이전트가 반복해서 막히는 지점이 곧 하네스에 다음으로 추가해야 할 컴포넌트다.
그리고 한 가지 더. v2는 이미 완성형이 아니다. 평가자(Evaluator)를 독립 에이전트로 돌리는 루프는 아직 없고, 엔트로피 관리를 자동화하는 "가비지 컬렉션 에이전트"도 없다. v3가 온다면 아마 이쪽에서 출발할 것이다.
Y Combinator CEO Garry Tan이 자신의 실제 Claude Code 개발 환경을 오픈소스로 공개한 것이 gstack이다.
"The model is commodity. The harness is moat."
모델 자체보다 모델을 감싸는 구조가 경쟁력이라는 하네스 엔지니어링의 핵심 명제를 그대로 실천한 프로젝트다.
하나의 AI에게 모든 걸 맡기는 대신, 실제 엔지니어링 조직의 역할 구조를 하네스로 투영한다. Claude라는 동일한 모델이지만 각 커맨드마다 독립된 시스템 프롬프트를 가진 다른 "역할"로 동작한다.
/plan-ceo-review ← CEO 페르소나로 전략 검토
/plan-eng-review ← 엔지니어링 매니저로 기술 계획 검토
/review ← 코드 리뷰어
/qa ← QA 리드
/ship ← 릴리스 매니저개발 사이클 전체를 커버한다: Think → Plan → Build → Review → Test → Ship → Retro
| gstack 커맨드 | 하네스 원칙 |
|---|---|
/qa, /qa-only | 생성자-평가자 분리 — 만든 사람과 검증하는 사람을 분리 |
/plan-ceo-review, /plan-eng-review, /plan-design-review | 역할별 평가자 — 관점이 다른 독립 평가자 |
/learn | 상태 외부화 — 프로젝트별 패턴을 세션 간 누적 |
/guard, /careful | 아키텍처 제약 — 에이전트가 경계를 벗어나지 않도록 제어 |
/investigate | 컨텍스트 엔지니어링 — 작업 전 충분한 맥락 수집 |
/retro | 엔트로피 관리 — 반복 패턴 인식 및 개선 |
실제 검증된 워크플로우다. YC CEO가 자신의 일상 개발에 쓰는 설정을 그대로 공개했다. 한 CTO는 gstack의 코드 리뷰 기능이 팀이 발견하지 못한 XSS 취약점을 잡아냈다고 보고했다.
/learn 커맨드는 프로젝트별 패턴을 세션을 넘어 누적한다. 사용할수록 해당 코드베이스에 맞는 하네스로 진화한다. 하네스가 고정된 구조가 아니라 적응하는 구조가 될 수 있다는 걸 보여준다.
Google Gemini 환경으로 포팅한 파생 프로젝트까지 등장했다. 아이디어 자체가 모델에 종속되지 않는다는 뜻이다.
# 설치
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
cd ~/.claude/skills/gstack && ./setup흥미로운 관찰이 있다. Rajasekaran은 Claude Opus 4.6으로 테스트하면서 하네스 컴포넌트를 하나씩 제거해봤다.
강력한 모델에서는:
하지만 평가자는 여전히 필요했다. 특히 작업 복잡도가 높아질수록 평가자의 가치가 오히려 커졌다.
결론: 하네스의 각 컴포넌트는 모델이 스스로 할 수 없다는 가정을 인코딩한다. 그 가정이 여전히 유효한지 주기적으로 검증해야 한다.
LangChain은 모델 교체 없이 하네스 엔지니어링만으로 14퍼센트포인트 성능 향상을 이뤄냈다. Vercel은 도구 복잡도를 줄이는 하네스 개선으로 100% 정확도를 달성했다. 하네스 엔지니어링은 "프롬프트 엔지니어링"의 다음 단계로 자리잡고 있다.
프롬프트 엔지니어링 → 컨텍스트 엔지니어링 → 하네스 엔지니어링
에이전트를 만드는 것보다 에이전트를 둘러싼 시스템 을 설계하는 것이 점점 더 중요해지고 있다.