본문 바로가기

콩's AI

실전 하네스 엔지니어링 개발 가이드

반응형
하네스 엔지니어링 실전 가이드: AI 에이전트를 통제하는 4가지 기법

실전 하네스 엔지니어링 개발 가이드

팩트체크 수정 사항 (원문 대비)

  • "CLAUDE.md는 60줄 이하" → OpenAI 원문 기준 약 100줄 수준이 실제 검증된 수치
  • 자동 평가 코드 예시의 언어 혼용(JS try {와 Python except를 같은 블록에서 검사) 오류 수정
  • 모델명 "Claude 3.5 Sonnet" → 현재 기준 최신 모델 반영

핵심 원칙

하네스 엔지니어링의 전제는 하나다.

AI의 출력물을 프롬프트로 통제하려 하지 않는다. 기계가 강제하도록 시스템을 설계한다.

프롬프트로 "테스트 꼭 써줘", "예외 처리 빼먹지 마" 하는 것은 권장(Suggestion)이다. 하네스는 그 규칙을 어기면 코드가 시스템 밖으로 나가지 못하게 막는 결정론적 장벽이다.


1. 파일 기반 컨텍스트 관리: AGENTS.md / CLAUDE.md

왜 필요한가

에이전트는 세션이 바뀌면 이전 대화를 잊는다. 새 세션마다 컨벤션을 다시 설명하는 대신, 프로젝트 루트에 규칙 파일을 두고 에이전트가 작업 시작 시 반드시 읽도록 강제하는 방식이다.

설계 원칙

OpenAI 실험이 직접 확인한 실패 패턴이 있다. "한 파일에 모든 규칙을 때려 넣는" 방식은 작동하지 않는다. 이유는 세 가지다.

  • 컨텍스트는 희소 자원이다. 거대한 규칙 파일이 코드와 작업 내용을 밀어낸다.
  • 규칙이 너무 많으면 에이전트가 패턴 매칭으로 응답하고 지시를 진지하게 따르지 않는다.
  • 시간이 지나면 규칙 파일이 썩는다. 어떤 규칙이 아직 유효한지 에이전트도, 인간도 알 수 없게 된다.

올바른 구조: AGENTS.md(또는 CLAUDE.md)는 목차(table of contents) 역할만 한다. 세부 규칙은 docs/ 디렉터리에 분산 보관하고, AGENTS.md에서 링크로 참조한다. OpenAI 실험에서 검증된 적정 길이는 약 100줄 수준이다.

실전 템플릿

 
 
markdown
# AGENTS.md

## Tech Stack
- Backend: FastAPI (Python 3.12), PostgreSQL + PostGIS
- Frontend: Next.js 15, TypeScript strict mode
- Test Runner: pytest / Jest

## 절대 규칙 (반드시 준수)
- 함수는 단일 책임 원칙. 하나의 함수가 두 가지 이상의 역할을 수행하면 분리할 것
- 기존 코드를 `...` 또는 `# 기존 코드 유지` 등으로 생략하지 말 것. 전체 코드를 작성할 것
- 모든 async 함수는 최상단에 try/except를 둘 것 (2026-06-01 장애 이후 필수 규칙)
- 하드코딩된 시크릿, API 키, 비밀번호는 절대 코드에 포함하지 말 것

## 작업 전 필수 확인
- 세부 아키텍처 규칙: docs/architecture.md 참조
- DB 스키마 컨벤션: docs/database.md 참조
- API 응답 형식: docs/api_spec.md 참조

## 자동화 명령
- 단위 테스트: `pytest tests/unit/`
- 린터: `ruff check . && mypy .`
- 전체 빌드: `npm run build && pytest`

## Bug Log (최신순)
- [2026-06-01] 에이전트가 async 에러 핸들링 누락 → 서버 다운. 이후 모든 async 함수 최상단 try/except 필수
- [2026-05-12] N+1 쿼리 문제 발생. ORM 사용 시 joinedload 또는 selectinload 명시적 선언 필수

Bug Log는 핵심이다. 에이전트가 같은 실수를 반복하지 않도록 실제 장애 이력을 컨텍스트에 주입하는 것이 이 파일의 가장 중요한 역할이다.


2. 결정론적 강제 루프 (Self-Correction Loop)

AI에게 "린트해줘"라고 부탁하는 것이 아니다. 린트를 통과하지 못하면 코드가 다음 단계로 넘어가지 못하도록 시스템이 강제한다. 그리고 실패 로그를 인간에게 가져오는 것이 아니라 에이전트에게 자동으로 되돌려 보낸다.

흐름

 
 
AI 코드 생성
    ↓
린터 + 타입 체크 (결정론적, 자동 실행)
    ↓ 실패 시
에러 로그 → AI에게 자동 반환 → 재생성 (최대 3회)
    ↓ 통과 시
단위 테스트 자동 실행
    ↓ 통과 시
Human-in-the-loop: PR 목록에 등록

핵심 구현 코드 (Python)

 
 
python
import subprocess
import anthropic

client = anthropic.Anthropic()

def run_linter(code: str, filepath: str) -> tuple[bool, str]:
    """린터와 타입 체커를 실행하고 결과를 반환한다."""
    with open(filepath, "w") as f:
        f.write(code)
    
    result = subprocess.run(
        ["ruff", "check", filepath],
        capture_output=True, text=True
    )
    
    if result.returncode != 0:
        return False, result.stdout + result.stderr
    return True, ""

def generate_with_self_correction(task: str, max_retries: int = 3) -> str:
    """
    코드 생성 → 린트 → 실패 시 자동 재시도 루프.
    최대 max_retries회 반복 후 마지막 결과를 반환한다.
    """
    messages = [{"role": "user", "content": task}]
    
    for attempt in range(1, max_retries + 1):
        print(f"[시도 {attempt}/{max_retries}] 코드 생성 중...")
        
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            system=open("AGENTS.md").read(),  # AGENTS.md를 시스템 컨텍스트로 주입
            messages=messages
        )
        
        generated_code = response.content[0].text
        
        # 코드 블록 추출 (```python ... ``` 형식)
        if "```python" in generated_code:
            code = generated_code.split("```python")[1].split("```")[0].strip()
        else:
            code = generated_code
        
        passed, error_log = run_linter(code, "/tmp/agent_output.py")
        
        if passed:
            print(f"[시도 {attempt}] 린트 통과. 완료.")
            return code
        
        print(f"[시도 {attempt}] 린트 실패. 에러 로그를 에이전트에게 반환합니다.")
        
        # 에러 로그를 대화 이력에 추가하여 다음 시도에 반영
        messages.append({"role": "assistant", "content": generated_code})
        messages.append({
            "role": "user",
            "content": f"위 코드에서 다음 린트 에러가 발생했습니다. 수정하세요:\n\n```\n{error_log}\n```"
        })
    
    print(f"[경고] {max_retries}회 시도 후에도 린트 미통과. 마지막 코드를 반환합니다.")
    return code

Git Pre-commit Hook 연동

코드가 커밋되기 전에 자동으로 실행되도록 .pre-commit-config.yaml에 등록한다.

 
 
yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
      - id: ruff-format
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.9.0
    hooks:
      - id: mypy

이렇게 하면 에이전트가 커밋하려 해도 린트 미통과 코드는 Git이 자동으로 차단한다.


3. 대립형 멀티 에이전트 (Generator vs Critic)

담합을 방지하는 핵심 조건은 물리적 격리다. 같은 세션 안에서 "이제 비판자 역할을 해줘"라고 지시하면 앞선 대화 맥락에 오염되어 비판이 무뎌진다. Generator와 Critic은 반드시 별도 API 호출로 분리해야 한다.

구현 코드

 
 
python
import anthropic

client = anthropic.Anthropic()

GENERATOR_SYSTEM = """
당신은 기능 구현에 집중하는 시니어 백엔드 개발자입니다.
FastAPI와 PostgreSQL을 사용하며, AGENTS.md의 모든 규칙을 준수합니다.
요구사항에 맞는 코드를 완성된 형태로 작성하세요.
"""

CRITIC_SYSTEM = """
당신은 20년 경력의 까다로운 수석 아키텍트입니다.
주어진 코드를 반드시 무너뜨려야 합니다.

아래 항목을 반드시 검토하고 각각 판정하세요:
1. 3개월 뒤 장애를 일으킬 수 있는 구조적 결함 (최소 2개 이상 제시)
2. 보안 취약점 (SQL Injection, 인증 누락, 시크릿 노출 등)
3. 예외 처리 누락 또는 부실한 에러 핸들링
4. 성능 문제 (N+1 쿼리, 미인덱싱, 불필요한 루프 등)

결론은 반드시 아래 두 가지 중 하나로만 끝내야 합니다:
- APPROVED: 모든 항목이 허용 기준 이내일 때만
- REJECTED: 하나라도 심각한 문제가 있을 때

문제를 찾지 못하면 당신의 평가 점수는 감점됩니다.
"""

def generator_agent(task: str) -> str:
    """Generator: 기능 코드를 작성한다."""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        system=GENERATOR_SYSTEM + "\n\n" + open("AGENTS.md").read(),
        messages=[{"role": "user", "content": task}]
    )
    return response.content[0].text

def critic_agent(generated_code: str) -> tuple[bool, str]:
    """
    Critic: 생성된 코드를 독립 세션에서 검토한다.
    반환값: (승인 여부, 검토 내용)
    """
    response = client.messages.create(
        model="claude-opus-4-6",   # Critic은 더 강력한 모델 사용 권장
        max_tokens=2048,
        system=CRITIC_SYSTEM,
        messages=[{
            "role": "user",
            "content": f"아래 코드를 검토하라:\n\n```python\n{generated_code}\n```"
        }]
    )
    
    review = response.content[0].text
    approved = "APPROVED" in review.upper() and "REJECTED" not in review.upper()
    return approved, review

def harness_pipeline(task: str, max_revision_rounds: int = 2) -> dict:
    """
    Generator → Critic → 미승인 시 Generator에게 피드백 재주입 → 재검토
    """
    print(f"[Generator] 코드 작성 시작...")
    code = generator_agent(task)
    
    for round_num in range(1, max_revision_rounds + 1):
        print(f"[Critic] {round_num}차 검토...")
        approved, review = critic_agent(code)
        
        if approved:
            print(f"[Critic] 승인. {round_num}차 검토 통과.")
            return {"status": "approved", "code": code, "review": review}
        
        print(f"[Critic] 거부. Generator에게 피드백 반환...")
        
        if round_num < max_revision_rounds:
            # Critic의 지적 사항을 Generator에게 재주입
            revision_task = (
                f"원래 요구사항: {task}\n\n"
                f"다음 문제점이 지적되었습니다. 수정하세요:\n\n{review}"
            )
            code = generator_agent(revision_task)
    
    # 최종 라운드에서도 미승인이면 인간에게 에스컬레이션
    return {"status": "needs_human_review", "code": code, "review": review}


if __name__ == "__main__":
    result = harness_pipeline(
        task="사용자 인증 토큰을 검증하는 FastAPI 미들웨어를 작성하라."
    )
    print(f"\n최종 상태: {result['status']}")
    print(f"\n[코드]\n{result['code']}")
    print(f"\n[Critic 검토]\n{result['review']}")

4. 자동 평가 레이어 (Scoring)

느낌이나 육안으로 보는 것이 아니라, 규칙 위반을 수치로 환산한다. 원문 코드 예시는 Python과 JavaScript의 에러 처리 구문을 같은 블록에서 검사하는 논리 오류가 있었다. 아래는 언어를 분리하여 수정한 버전이다.

 
 
python
import ast
import re

def evaluate_python_code(code: str) -> dict:
    """Python 코드의 품질 점수를 산출한다."""
    score = 100
    issues = []

    # 1. 예외 처리 검사 (AST 기반 - 문자열 검색보다 정확)
    try:
        tree = ast.parse(code)
        has_try_except = any(isinstance(node, ast.Try) for node in ast.walk(tree))
        if not has_try_except:
            score -= 30
            issues.append("예외 처리(try/except) 누락 (-30)")
    except SyntaxError as e:
        score -= 50
        issues.append(f"문법 오류: {e} (-50)")

    # 2. 디버그 코드 잔재 검사
    if re.search(r'\bprint\s*\(', code):
        score -= 10
        issues.append("print() 디버그 코드 잔재 (-10)")

    # 3. 함수 길이 검사 (단일 함수가 50줄 초과 시 경고)
    try:
        tree = ast.parse(code)
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                func_lines = node.end_lineno - node.lineno
                if func_lines > 50:
                    score -= 15
                    issues.append(f"함수 '{node.name}'이 {func_lines}줄로 과도하게 김 (-15)")
    except Exception:
        pass

    # 4. 하드코딩 시크릿 패턴 검사
    secret_patterns = [
        r'(?i)(api_key|secret|password|token)\s*=\s*["\'][^"\']{8,}["\']',
        r'(?i)Bearer\s+[A-Za-z0-9\-_\.]{20,}',
    ]
    for pattern in secret_patterns:
        if re.search(pattern, code):
            score -= 40
            issues.append("하드코딩된 시크릿 또는 토큰 감지 (-40)")
            break

    # 5. 로깅 부재 검사
    if "logging" not in code and "logger" not in code:
        score -= 10
        issues.append("로깅 코드 없음 (-10)")

    return {
        "score": max(0, score),
        "grade": "PASS" if score >= 70 else "FAIL",
        "issues": issues
    }

주의: 이 스코어링은 1차 기계 필터다. 점수가 높아도 비즈니스 로직 오류나 아키텍처 문제는 잡지 못한다. Critic 에이전트와 병행 운영해야 의미가 있다.


5. 관측 가능성 (Observability)

에이전트들이 내부에서 무슨 판단을 내리는지 블랙박스로 두면 안 된다. 추후 장애 원인을 추적하고 하네스를 개선하려면 로그가 필수다.

 
 
python
import json
import datetime

class HarnessLogger:
    def __init__(self, log_file: str = "harness_audit.jsonl"):
        self.log_file = log_file
    
    def log(self, event: str, data: dict):
        entry = {
            "timestamp": datetime.datetime.utcnow().isoformat(),
            "event": event,
            **data
        }
        with open(self.log_file, "a") as f:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")
        print(f"[LOG] {event}: {json.dumps(data, ensure_ascii=False)[:200]}")

외부 도구로는 LangSmith(LangChain 생태계 연동 시)나 Arize Phoenix(모델 무관 오픈소스)가 실제 운용되는 관측 도구다. 직접 구축할 여유가 없다면 위와 같은 JSONL 로그부터 시작하는 것이 현실적이다.


전체 아키텍처 요약

 
 
[사람: 요구사항 작성]
        ↓
[AGENTS.md 컨텍스트 주입]
        ↓
[Generator 에이전트] ──── 코드 생성
        ↓
[자동 평가 레이어] ──── 점수 산출 (70점 미만 즉시 반려)
        ↓ 통과
[린터 / 타입 체크] ──── 결정론적 기계 검사 (최대 3회 자동 재시도)
        ↓ 통과
[Critic 에이전트] ──── 구조적 결함 / 보안 / 성능 검토 (격리 세션)
        ↓ APPROVED
[Human-in-the-loop] ──── PR 등록 → 사람 최종 확인
        ↓
[Git 커밋] ──── Pre-commit Hook이 린트 재검사 (최후 방어선)

각 단계는 이전 단계를 통과하지 못하면 다음으로 넘어갈 수 없다. "느낌"이 개입하는 지점은 오직 마지막 Human-in-the-loop 단계뿐이다.


빠른 시작 순서 (우선순위)

구축 비용 대비 효과가 높은 순서로 나열했다.

  1. AGENTS.md 작성 — 오늘 당장 할 수 있다. Bug Log를 쌓는 습관이 핵심이다.
  2. Pre-commit Hook 설치 — pip install pre-commit && pre-commit install 한 줄로 시작.
  3. Self-Correction Loop — 위 Python 스크립트를 프로젝트에 추가.
  4. Critic 에이전트 — Generator와 모델을 분리하고 격리 세션으로 운영.
  5. 자동 평가 + 관측 로그 — 스코어링 함수와 JSONL 로그를 파이프라인에 연결.
반응형

⚠️ 광고 차단 프로그램 감지

애드블록, 유니콘 등 광고 차단 확장 프로그램을 해제하거나
화이트리스트에 추가해주세요.