
Part 2 - 하네스(Harness) 해체하기
요약
단일 프롬프트에 모든 지침을 담는 모놀리식 방식의 한계를 지적합니다. 'Lost in the Middle' 현상으로 인해 모델이 중간 정보를 놓치는 문제를 해결하기 위해 시스템을 계층화할 것을 권장합니다.
핵심 포인트
- 단일 프롬프트 방식은 LLM의 어텐션 분산 문제를 야기함
- 'Lost in the Middle' 현상으로 인해 컨텍스트 중간 정보의 정확도 저하
- 모놀리식 프롬프트는 에이전트의 병렬 실행을 방해함
- 시스템을 Agent, Skill, Orchestrator 계층으로 분리하여 구축 권장
개발자들이 첫 번째 AI 코딩 어시스턴트(AI coding assistant)나 자율 작업자(autonomous worker)를 구축할 때, 종종 단일 지침 문서로 시작하곤 합니다. 그들은 페르소나(persona), 읽어야 할 파일, 코드 작성 규칙, 데이터베이스 제한 사항, 테스트 실행 지침 등을 포함하는 상세한 Markdown 파일(.cursorrules 또는 system_prompt.md)을 작성합니다.
이러한 모놀리식(monolithic) 접근 방식은 실수입니다.
모든 지침을 단일 프롬프트(prompt)에 쏟아붓는 것은 언어 모델(language models)이 어텐션(attention)을 할당하는 방식 때문에 실패하게 됩니다. 최근 여러 기관의 LLM 연구자들은 "Lost in the Middle"로 알려진 한계를 기록했습니다. 그들은 거대 언어 모델(large language models)이 프롬프트의 맨 처음이나 맨 끝에 배치된 정보를 검색하는 데는 매우 효과적이지만, 컨텍스트 윈도우(context window) 중간에 묻혀 있는 정보에 대해서는 정확도가 현저히 떨어진다는 것을 보여주었습니다.
하나의 Markdown 문서에 50개의 규칙을 넣으면, 모델은 처음 몇 개와 마지막 몇 개를 따릅니다. 중간에 있는 40개는 초점에서 벗어납니다. 그리고 이것은 최근 출시된 1M 토큰 모델들과 관계없이 여전히 지속되고 있는 LLM의 알려진 문제입니다.
또한 모놀리식 프롬프트는 병렬 실행(parallel execution)을 방해합니다. 만약 하나의 프롬프트가 전체 작업을 제어한다면, 에이전트(agent)는 파일 읽기, 코드 생성, 테스트 실행을 단일 스레드(single thread)에서 모델이 생각하도록 기다리며 모든 단계를 순차적으로 처리해야만 합니다.
신뢰할 수 있는 시스템을 구축하려면, 하네스(harness)를 Agent, Skill, 그리고 Orchestrator 계층으로 분리하십시오.
Agent 계층 (Reasoning)
Agent는 시스템의 '추론 (Reasoning)' 커널입니다. 이는 특정 페르소나 (Persona), 타겟팅된 지침 (Instructions), 그리고 엄격한 입출력 경계 (Input/Output boundaries)가 설정된 언어 모델 (Language model)입니다.
Agent는 행동을 실행하지 않습니다. 대신 수신된 정보를 바탕으로 행동을 결정합니다.
주의력 손실 (Attention loss)을 방지하기 위해, Agent는 상위 대화 기록 (Parent conversation history)이 없는 독립적인 프롬프트 (Self-contained prompt) 상에서 작동해야 합니다. 개발자들은 종종 전체 채팅 로그를 서브 에이전트 (Sub-agents)에게 전달하곤 합니다. 이를 피하십시오.
컨텍스트 격리 (Context isolation)는 세 가지 속성을 보장합니다:
- 집중 격리 (Focus Isolation): Agent는 무관한 대화 로그의 노이즈로부터 보호되어, 어텐션 윈도우 (Attention window)를 즉각적인 작업에 집중시킬 수 있습니다.
- 비용 제어 (Cost Control): 이력을 제외함으로써 반복 루프 (Iterative loops) 동안 입력 토큰 (Input tokens)을 절약할 수 있습니다.
- 병렬 안전성 (Parallel Safety): Agent는 다른 워커 (Workers)와 가변 상태 (Mutable state)를 공유하지 않으므로, 여러 Agent가 경합 조건 (Race conditions) 없이 병렬로 실행될 수 있습니다.
다음은 구문 감사 (Syntax-auditing) Agent를 위한 간단한 .md 템플릿입니다:
# Agent: Syntax Auditor
역할: 코더가 생성한 Python 코드를 감사합니다.
입력: 파일 경로 및 컴파일러 에러.
...
좋은 Agent가 갖춰야 할 조건
- 좁은 범위 (Narrow Scope): 좋은 Agent는 단일하고 명확하게 정의된 작업을 가집니다.
- 좋은 프롬프트: "당신은 Syntax Auditor입니다. 코드를 검토하고 구문 에러를 나열하십시오. 줄 번호와 설명이 포함된 JSON 리스트를 출력하십시오. 코드를 수정하려고 시도하지 마십시오."
- 최소한의 컨텍스트 (Minimalist Context): 즉각적인 결정을 위해 필요한 정보만 전달합니다.
- 좋은 프롬프트: "이 특정 파일인 app.js를 검토하십시오. 컴파일러 에러: Unexpected token."
- 구조화된 출력 (Structured Outputs): Agent는 JSON 스키마 (JSON schemas)와 같은 구조화된 형식을 통해 통신하며, 이를 통해 정적 코드 (Static code)가 취약한 정규 표현식 매칭 (Regex matching) 없이 결정을 파싱할 수 있게 합니다.
- 좋은 프롬프트: "오직 JSON 블록만 반환하십시오: {"errors": [{"line": 12, "desc": "missing semicolon"}]}"
나쁜 Agent의 특징
나쁜 Agent의 특징
- Do-it-all Agent (모든 것을 다 하는 에이전트): 프로젝트 계획, Python 함수 작성, 테스트 실행, 최종 보고서 작성을 한 번에 시도하는 단일 에이전트 프롬프트 (Single agent prompt). 이러한 설계는 주의력 분산 (attention dilution)을 보장합니다.
- 나쁜 프롬프트: "당신은 풀스택 어시스턴트입니다. 프로젝트를 계획하고, Python 함수를 작성하며, 테스트를 실행하고, 버그를 수정하고, 요약을 작성하십시오. 도움이 필요하면 저에게 물어보세요."
- Mixed Responsibilities (혼합된 책임): 에이전트가 사용하지 않는 도구에 대한 지침이 프롬프트에 포함된 경우. 예를 들어, 개발자가 코드 리뷰어에게 데이터베이스 백업 유틸리티에 대해 설명하는 것과 같습니다.
- 나쁜 프롬프트: "app.py의 코드를 리뷰하십시오. 운영 데이터베이스는 AWS RDS에 호스팅되어 있으며, 데이터베이스 백업은 cron job을 사용하여 매일 자정에 실행된다는 점을 참고하십시오."
- Unstructured Free-Text Output (비구조화된 자유 형식 텍스트 출력): 개발자가 에이전트가 대화체 산문으로 응답하도록 허용하여, 후속 스크립트가 모델의 의도를 추측하기 위해 취약한 정규 표현식 (regular expressions)을 사용하도록 강제하는 경우.
- 나쁜 프롬프트: "코드를 리뷰하고 무엇이 잘못되었는지와 어떻게 수정해야 하는지를 설명하는 단락을 작성하십시오."
Skill Layer (Action) (스킬 레이어 (액션))
스킬 (Skill)은 에이전트가 외부 세계와 상호작용할 수 있게 해주는 원자적 도구 래퍼 (atomic tool wrapper)입니다. 파일 읽기, 웹 검색, 셸 명령 (shell command) 실행 또는 데이터베이스 쿼리 (querying database) 등이 그 예입니다. 에이전트가 '뇌'라면, 스킬은 '손'입니다. 해체된 하네스 (deconstructed harness) 구조에서는 하위 에이전트를 생성하는 능력 (Agent tool) 자체가 하나의 스킬로 취급됩니다.
보안과 신뢰성을 강제하기 위해, 스킬은 최소 권한 원칙 (principle of least privilege)과 심층 방어 (defense-in-depth) 도구 필터링 원칙에 따라 관리되어야 합니다.
- Layered Restrictions (계층적 제한): 에이전트의 역할에 따라 도구를 필터링합니다. 읽기 전용 탐색 에이전트의 경우, API 레이어에서 파일 쓰기 및 실행 도구를 제거해야 하며, 간접 명령 실행 (indirect command execution)을 방지하기 위해 모델 레이어에서 부정 프롬프트 (negative prompts)로 이를 보강해야 합니다.
- Interactive vs Async Safety (대화형 vs 비동기 안전성): 백그라운드 (비동기, asynchronous) 에이전트는 대화형 프롬프트(대화형 사용자 질문 등)가 필요한 도구 사용이 제한되어야 하며, 안전하고 결정론적인 (deterministic) 작업만 허용해야 합니다.
다음은 파일 읽기 스킬을 위한 간단한 .md 템플릿입니다:
# Skill: Read File (스킬: 파일 읽기)
Goal (목표): 로컬 파일 내용을 읽고 반환합니다.
Parameters (매개변수):
...
좋은 스킬은 다음과 같은 특징을 가져야 합니다
- 원자적이고 순수함 (Atomic and Pure): 좋은 스킬은 한 가지 일만 수행합니다.
- 좋은 설계 (Good Design):
def read_file(file_path): return open(file_path).read()
- 좋은 설계 (Good Design):
- 엄격한 스키마 검증 (Strict Schema Validation): 스킬은 매개변수를 시스템에 전달하기 전에 입력 검증을 강제합니다.
- 좋은 설계 (Good Design):
if not file_path.startswith(WORKSPACE_ROOT): raise ValueError("Access Denied")
- 좋은 설계 (Good Design):
- 예측 가능한 에러 로그 (Predictable Error Logs): 명령이 실패할 때, 스킬은 예외 (Exception)를 포착하여 코디네이터 (Coordinator)에게 구조화된 에러 문자열을 반환합니다.
- 좋은 설계 (Good Design):
except FileNotFoundError: return {"status": "error", "message": "File not found"}
- 좋은 설계 (Good Design):
나쁜 스킬의 특징
- 과도하게 광범위한 로직 (Overly Broad Logic): 단일 실행 단계에서 파일을 읽고, 줄을 편집하고, 테스트를 실행하며, GitHub에 푸시(Push)까지 수행하는
manage_codebase라는 도구.- 나쁜 설계 (Bad Design):
def manage_codebase(file_path, edit_content, test_cmd): # 내부에서 모든 것을 수행함
- 나쁜 설계 (Bad Design):
- 검증되지 않은 명령 생성 (Unvalidated Command Generation): LLM에 의해 생성된 가공되지 않은 셸 (Shell) 명령 문자열을 그대로 받아 직접 실행하는 스킬.
- 나쁜 설계 (Bad Design):
def run_command(cmd_string): os.system(cmd_string)
- 나쁜 설계 (Bad Design):
- 침묵하는 실패 (Silent Failures): 도구가 치명적인 예외 (Exception)를 포착하여 콘솔에 경고를 출력하고 빈 문자열을 반환하는 경우. 이는 에이전트 (Agent)가 작업이 실패했음에도 불구하고 성공했다고 가정하게 만듭니다.
- 나쁜 설계 (Bad Design):
except Exception as e: print(e); return ""
- 나쁜 설계 (Bad Design):
오케스트레이터 계층 (Orchestrator Layer - 제어)
오케스트레이터 (Orchestrator)는 하네스 (Harness)의 코디네이터입니다. 이는 언어 모델 (Language Model)이 아니라 Python이나 TypeScript와 같은 정적 코드 (Static Code)로 작성된 결정론적 상태 머신 (Deterministic State Machine)입니다. 오케스트레이터는 실행 흐름을 관리하고, 프로젝트 상태를 추적하며, 다음에 어떤 에이전트를 호출할지 결정하고, 스킬 실행 순서를 정하며, 에러를 처리합니다.
오케스트레이터는 작업을 직접 실행하는 대신 중앙 코디네이터 역할을 수행합니다. 오케스트레이터는 분업을 통해 흐름을 조율합니다:
- 생명주기 관리 (Lifecycle Management): 프로세스의 생명주기를 관리하며, 고아 프로세스 (orphan process) 또는 좀비 프로세스 (zombie process)를 방지하기 위해 백그라운드 작업이 적절히 등록되고 종료되도록 보장합니다.
- 상태 추적 (State Tracking): 깨끗하고 버전이 관리되는 워크스페이스 기록을 유지하며, 작업 진행 상태 (대기 중, 진행 중, 완료)를 추적하고 로그를 라우팅합니다.
- 루프 제어 (Loop Control): 경계 지점에서 예외를 포착하고, API 비용의 폭주를 방지하기 위해 엄격한 반복 타임아웃 (iteration timeouts)을 강제합니다.
다음은 코드-컴파일-테스트 루프를 관리하는 결정론적 오케스트레이터 (deterministic orchestrator) 클래스의 Python 예시입니다:
class CodeGenerationOrchestrator:
"""
에이전트 실행 루프를 관리하는 결정론적 오케스트레이터.
...
좋은 오케스트레이터의 조건은 다음과 같습니다:
- 결정론적 제어 (Deterministic Control): 워크플로우가 정적 코드 (static code)로 작성됩니다. 개발자가 순서를 결정합니다.
- 좋은 제어 예시:
run_agent(PLANNER); run_agent(CODER); run_skill(TEST_RUNNER)
- 좋은 제어 예시:
- 오류 차단 (Error Interception): 오케스트레이터가 경계에서 실패를 포착하여, 트레이스백 (traceback)을 수정 목적으로 코더 에이전트에게 다시 보낼지 아니면 실행을 중단할지 결정합니다.
- 좋은 제어 예시:
success, log = compile_code(); if not success: run_agent(CODER, feedback=log)
- 좋은 제어 예시:
- 상태 관리 (State Management): 오케스트레이터는 환경의 깨끗하고 버전이 관리되는 기록을 유지하여, 작업 실패 시 롤백 (rollback)이 가능하도록 합니다.
- 좋은 제어 예시:
save_git_snapshot(); if tests_fail: rollback_to_snapshot()
- 좋은 제어 예시:
나쁜 오케스트레이터는 다음과 같은 특징을 가집니다:
- 불확실한 라우팅 (Uncertain Routing): 개발자가 LLM에게 다음 단계를 선택하도록 요청합니다. 이 방식은 빈번하게 무한 루프 (infinite loops), 데드락 (deadlocks), 그리고 예측 불가능한 실행 경로 (unpredictable execution paths)를 초래합니다.
- 잘못된 제어 (Bad Control): "여기 도구들이 있습니다. 작업을 완료하기 위해 다음에 어떤 도구를 실행하고 싶은지 말해주세요."
- 상태 혼잡 (State Clutter): 시스템이 매 턴마다 도구 호출 (tool calls)의 전체 실행 이력을 에이전트 (agent)에게 전달하여, 컨텍스트 윈도우 (context window)가 중복된 정보로 빠르게 채워집니다.
- 잘못된 제어 (Bad Control):
history += f"\nRun: {run_id}, Output: {output}" # 매 프롬프트에 20턴 분량의 로그를 주입함
- 잘못된 제어 (Bad Control):
- 타임아웃 제한 없음 (No Timeout Limits): 반복 횟수 제한 없이 루프가 실행됩니다. 에이전트가 동일한 구문 오류 (syntax error)를 무한히 수정하려고 시도하며 API 크레딧을 낭비합니다.
- 잘못된 제어 (Bad Control):
while True: run_agent_loop() # 반복 횟수 제한 체크 없음
- 잘못된 제어 (Bad Control):
종합하기 (Putting It Together)
세 개의 레이어 (layers)가 분업을 통해 협력합니다. 오케스트레이터 (Orchestrator)가 코더 에이전트 (Coder Agent, 추론)에게 작업을 라우팅하면, 코더 에이전트는 write_file 스킬 (Skill, 행동)을 요청하고, 오케스트레이터는 이를 검증하여 실행합니다. 오케스트레이터는 테스트 스킬 (Test Skill)을 실행하며, 실패 시 트레이스백 (traceback)을 에이전트에게 다시 라우팅하여 수정을 요청하고, 테스트가 통과하거나 시도 횟수 제한에 도달할 때까지 루프를 반복합니다. 단일 구성 요소가 전체 작업을 수행하지 않습니다. 코더 에이전트는 코드를 작성하고, 테스트 스킬은 어설션 (assertions)을 실행하며, 오케스트레이터는 루프를 관리합니다. 각 프롬프트 (prompt)는 작게 유지되며, 모델이 보는 지침 (instructions)은 어텐션 (attention)이 유지되는 컨텍스트 윈도우의 가장자리에 위치합니다.
Claude Code에서의 실행 (Execution in Claude Code)
3계층 모델은 아키텍처 (architecture)이지, Claude Code의 기능 목록 (feature list)이 아닙니다. 이를 Claude Code에 매핑하면 각 요소가 예상치 못한 곳에 위치하게 됩니다. 이론에서 정의하는 방식대로 존재하는 구성 요소는 하나도 없습니다.
스킬 (Skill): 내장 도구, MCP, 그리고 SKILL.md
시스템은 두 갈래로 나뉩니다. 내장 도구 (Built-in tools: Read, Write, Bash, Grep)와 MCP 서버는 파일, 셸 (shells), 그리고 외부 시스템에 직접 접근하는 실제 도구 래퍼 (tool wrappers)입니다. 개발자는 read_file을 별도로 등록하지 않습니다. 이는 Read 도구로 존재하며, 외부 연결은 MCP를 통해 라우팅됩니다.
Claude Code가 "Skill (스킬)"이라고 부르는 것은 다릅니다: .claude/skills//SKILL.md 파일입니다. 이것은 지침(instructions), 프롬프트(prompt), 그리고 선택적인 지원 스크립트(supporting scripts)의 번들입니다. 이는 로드 시점에 동적 컨텍스트 주입(dynamic context injection)을 통해 실시간 데이터를 가져옵니다. 예를 들어, !`git diff HEAD`와 같은 라인은 모델이 파일을 읽기 전에 셸 명령(shell command)을 실행하고 그 출력을 인라인(inline)으로 삽입합니다. Skill은 원시 기능(raw capability)이 아닌 절차(procedure)를 패키징합니다. 커스텀 슬래시 명령(slash commands)도 동일한 메커니즘을 사용하므로, .claude/commands/deploy.md와 .claude/skills/deploy/SKILL.md는 모두 /deploy를 생성합니다.
Agent: .claude/agents/.md
이 레이어는 깔끔하게 매핑됩니다. .claude/agents/에 Markdown 파일을 저장하세요:
---
name: syntax-auditor
description: Audits Python for syntax errors. Use after code changes.
...
name과 description만 있으면 됩니다. 본문(Body)은 서브에이전트(subagent)의 시스템 프롬프트(system prompt)가 됩니다. 서브에이전트는 프롬프트와 환경 세부 정보(environment details)를 받으며, 부모 대화 기록(parent conversation history)은 받지 않습니다. 각 서브에이전트는 자신만의 도구 허용 목록(tool allowlist)을 가진 격리된 컨텍스트 창(isolated context window)에서 실행되며, 호출자에게 요약(summary)만 반환합니다. 리드 모델(Lead model)은 Agent 도구(이전에는 Task로 명명되었으며, 여전히 별칭으로 작동함)를 사용하여 서브에이전트를 생성하며, description을 기반으로 언제 위임할지 결정합니다. 여기서 모델을 Haiku로 설정하면 메인 모델에 비해 감사(audit) 비용을 줄일 수 있습니다.
Orchestrator: SKILL.md playbook
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기