하네스 엔지니어링: 바이브코딩을 프로덕션 엔지니어링으로 바꾸는 방법


1. 하네스 엔지니어링이란 무엇인가

하네스 엔지니어링(Harness Engineering)은 AI 에이전트(agent)가 실제 업무를 수행할 때, 모델 바깥의 실행 환경·도구·권한·컨텍스트·검증·관측성·승인 절차를 설계하는 공학이다.
간단히 말하면 이렇다.
모델이 “두뇌”라면, 하네스는 그 두뇌가 실제 개발 환경에서 안전하게 움직이도록 붙여주는 “작업 장비, 안전벨트, 계기판, 규칙, 검증 장치”다.Martin Fowler는 하네스를 “모델을 제외한 에이전트의 모든 것”에 가깝게 설명한다. 즉 에이전트는 단순히 LLM 하나가 아니라, Model + Harness로 구성된 시스템이다. 하네스에는 프롬프트, 컨텍스트, 도구, 메모리, 테스트, 피드백 루프, 권한 제어, 사람 개입 지점이 포함된다.
OpenAI도 Codex 기반 작업 경험을 설명하면서, 엔지니어의 역할이 단순히 코드를 직접 작성하는 것에서 에이전트가 일할 수 있는 환경과 제약을 설계하는 것으로 확장된다고 설명한다. 이 관점에서 하네스 엔지니어링은 AI 시대의 새로운 소프트웨어 공학 층이다.

2. 왜 하네스가 필요한가

바이브코딩(vibe coding)은 빠르다. 사용자가 “이런 기능 만들어줘”라고 말하면 AI가 파일을 만들고, 코드를 수정하고, 테스트까지 작성한다. 프로토타입에서는 매우 강력하다.
문제는 실제 서비스다.
AI는 보통 다음을 잘한다.
```
- 반복적인 코드 생성
- boilerplate 작성
- 익숙한 프레임워크 패턴 적용
- 테스트 초안 작성
- 작은 버그 수정
- 문서 기반 코드 수정
```
하지만 다음에는 취약하다.
```
- 인증/인가 보안 경계
- 결제 로직
- 개인정보 처리
- 운영 DB 접근
- 장기 유지보수성
- 사내 아키텍처 규칙
- 과거 장애 이력
- 도메인별 암묵적 예외
- 배포 후 실패 비용
```
즉 바이브코딩의 위험은 “AI가 코드를 못 짠다”가 아니다. 더 정확히는 AI가 너무 쉽게 그럴듯한 코드를 만들기 때문에, 사람이 검증하기 전에 위험한 변경이 들어갈 수 있다는 점이다.
하네스 엔지니어링은 이 문제를 이렇게 바꾼다.
```
나쁜 방식:
AI에게 “보안 신경 써서 만들어줘”라고 말한다.

좋은 방식:
AI가 보안 규칙을 위반하면 PR 자체가 생성되지 않게 만든다.
```
프롬프트는 조언이다.하네스는 제도와 인프라다.

3. 기존 에이전트 시스템과 무엇이 다른가

기존 에이전트 시스템은 보통 이런 구조다.
```
사용자 목표
   ↓
LLM이 계획
   ↓
도구 호출
   ↓
결과 관찰
   ↓
다시 계획
   ↓
완료
```
이건 agent loop, 즉 에이전트 실행 루프다.
하네스 엔지니어링은 그 바깥을 설계한다.
```
이 에이전트가 어떤 파일을 읽을 수 있는가?
어떤 파일을 쓸 수 있는가?
어떤 명령은 실행할 수 없는가?
운영 DB에는 접근할 수 없는가?
테스트 실패 시 어떻게 재시도하는가?
보안 스캔 실패 시 누가 승인하는가?
PR에는 어떤 증거를 남겨야 하는가?
나중에 실패 원인을 추적할 수 있는가?
```
즉 기존 agent framework가 “AI가 행동하는 루프”를 만드는 데 집중했다면, 하네스 엔지니어링은 그 행동 루프가 안전하고, 검증 가능하고, 감사 가능하고, 조직의 품질 기준을 따르도록 만드는 구조에 집중한다.
2026년 arXiv에 올라온 하네스 엔지니어링 관련 프리프린트도 소프트웨어 에이전트의 능력을 모델 단독이 아니라 model-harness-environment system, 즉 모델·하네스·환경의 결합으로 봐야 한다고 설명한다. 해당 논문은 task specification, context selection, tool access, memory, observability, verification, permissions 등을 하네스의 책임으로 정리한다. 다만 프리프린트이므로 동료심사를 거친 확정 이론으로 보기보다는 현재 연구 흐름으로 읽는 편이 안전하다.

4. 하네스 엔지니어링의 핵심 구성요소

실무에서 하네스는 보통 다음 층으로 나뉜다.
```
1. 작업 명세 계층
2. 위험도 분류 계층
3. 컨텍스트 수집 계층
4. 에이전트 런타임 계층
5. 도구 접근 계층
6. 샌드박스 실행 계층
7. 검증 게이트 계층
8. 사람 승인 계층
9. 관측성·로그 계층
10. CI/CD 운영 계층
```
전체 구조는 이렇게 볼 수 있다.
```
사용자 요청 또는 GitHub Issue
   ↓
작업 위험도 분류
   ↓
관련 코드·문서·이슈·정책 검색
   ↓
에이전트 실행
   ↓
허용된 도구만 호출
   ↓
샌드박스 안에서 코드 수정
   ↓
테스트·타입체크·린트·보안 스캔
   ↓
위험 작업이면 사람 승인
   ↓
PR 생성
   ↓
CI에서 다시 검증
   ↓
로그와 trace 저장
```
이 구조의 핵심은 모델이 자유롭게 움직이게 두는 것이 아니라, 좁고 명확한 통로를 통해 움직이게 만드는 것이다.

5. 실제 실무 스택 조합

하네스 엔지니어링은 특정 라이브러리 하나로 끝나지 않는다. 여러 도구를 목적별로 조합한다.

5.1 에이전트 런타임

대표 선택지는 다음이다.
```
- OpenAI Agents SDK
- LangGraph
- Pydantic AI
- LlamaIndex Workflows
- OpenHands SDK
```
OpenAI Agents SDK는 Agent와 Runner를 통해 turns, tool execution, guardrails, handoffs, sessions를 관리하는 구조다. 즉 단순 API 호출 래퍼가 아니라, 도구 호출형 에이전트 실행 루프를 관리하는 런타임이다.
LangGraph는 durable execution, streaming, human-in-the-loop 같은 장기 실행 에이전트 오케스트레이션 기능에 초점을 둔다. 중간 승인, 실패 후 재시작, 상태 기반 워크플로우가 중요한 경우 적합하다.

5.2 도구 연결 계층

최근에는 MCP(Model Context Protocol, 모델 컨텍스트 프로토콜)가 많이 언급된다. MCP는 AI 애플리케이션이 파일, DB, 검색, 계산기, 업무 시스템 같은 외부 시스템에 연결될 수 있게 하는 오픈 표준이다. 공식 문서는 MCP를 AI 애플리케이션을 외부 데이터·도구·워크플로우에 연결하는 표준으로 설명한다.
하지만 MCP를 붙였다고 자동으로 안전해지는 건 아니다. 하네스에서는 MCP tool마다 다음 제약을 붙여야 한다.
```
- 호출 가능한 tool allowlist
- 위험 tool denylist
- path validation
- argument schema validation
- rate limit
- approval policy
- audit log
```
즉 MCP는 연결 표준이고, 하네스는 그 위의 통제 체계다.

5.3 샌드박스 계층

AI가 만든 코드는 격리된 환경에서 실행해야 한다.
대표 도구는 다음이다.
```
- Docker
- devcontainer
- E2B
- Kubernetes isolated runner
- Firecracker 계열 microVM
- GitHub Actions ephemeral runner
```
E2B는 AI-generated code를 클라우드의 secure isolated sandbox에서 실행할 수 있는 오픈소스 인프라로 설명된다.  Hugging Face의 secure code execution 문서도 LLM 생성 코드를 로컬 환경에서 실행하는 데는 본질적 위험이 있으며, 더 강한 격리를 위해 E2B나 Docker 같은 remote execution 또는 sandbox 접근이 필요하다고 설명한다.

5.4 검증 게이트

하네스에서 가장 중요한 원칙은 이것이다.
모델의 자기평가가 아니라, 외부 검증 시스템의 결과를 믿는다.보통 다음 도구를 사용한다.
```
테스트:
- pytest
- Jest
- Vitest
- Playwright
- Cypress

타입체크:
- mypy
- pyright
- TypeScript tsc

린트:
- ruff
- eslint
- biome

보안:
- Semgrep
- CodeQL
- Snyk
- secret scanning
- dependency scan
```
Semgrep은 CI와 PR 스캔에 통합했을 때 pull request에서 새로 도입된 이슈를 보고하도록 사용할 수 있다.  GitHub CodeQL은 코드의 취약점과 오류를 찾아 GitHub code scanning alert로 표시하는 도구다.

5.5 관측성 계층

운영 환경에서는 “AI가 왜 그렇게 했는지”를 추적할 수 있어야 한다.
대표 도구는 다음이다.
```
- Langfuse
- LangSmith
- OpenTelemetry
- Arize Phoenix
- custom audit log
```
OpenAI Agents SDK는 LLM generation, tool call, handoff, guardrail, custom event를 포함한 tracing을 제공한다.  Langfuse는 OpenAI Agents workflow를 monitor, debug, evaluate하기 위한 통합을 제공한다.
관측성 없이 에이전트를 운영하면 나중에 이런 질문에 답하기 어렵다.
```
- AI가 어떤 파일을 읽고 수정했는가?
- 어떤 테스트가 실패했는가?
- 실패 후 무엇을 바꿨는가?
- 어떤 tool call이 위험했는가?
- 같은 실수가 반복되고 있는가?
- 비용이 어디서 폭주했는가?
```

6. 실무 적용 패턴


6.1 개인 개발자용 최소 하네스

개인 개발자라면 복잡한 플랫폼을 만들 필요는 없다. 최소한 이 정도만 해도 안전성이 크게 올라간다.
```
- AI가 수정할 branch를 따로 만든다.
- repo 전체를 무제한으로 맡기지 않는다.
- 수정 가능한 파일 범위를 정한다.
- .env, secret, production DB 접근을 주지 않는다.
- 테스트가 없으면 먼저 테스트를 만들게 한다.
- pytest/npm test/tsc/eslint를 반드시 실행한다.
- git diff를 사람이 읽고 merge한다.
- GitHub Actions에서 다시 검증한다.
```
개인용 최소 스택:
```
AI coding tool:
- Cursor
- Claude Code
- Codex CLI
- Aider
- OpenHands

검증:
- pytest 또는 Jest
- mypy 또는 TypeScript
- ruff 또는 eslint
- Semgrep

운영:
- Git
- Docker
- GitHub Actions
```

6.2 스타트업 제품팀용 하네스

스타트업에서는 “AI가 issue를 보고 PR까지 생성”하는 구조가 적합하다.
```
입력:
- GitHub issue
- Linear ticket
- Jira ticket

에이전트:
- OpenAI Agents SDK
- Pydantic AI
- LangGraph

컨텍스트:
- 코드 검색
- 문서 검색
- 과거 PR
- 관련 이슈

실행:
- Docker
- E2B
- GitHub Actions runner

검증:
- unit test
- integration test
- type check
- lint
- Semgrep

출력:
- PR
- 변경 요약
- 검증 결과
- 위험도 보고
```
흐름은 다음과 같다.
```
1. issue에 ai-fix 라벨을 붙인다.
2. 하네스가 issue 내용을 읽는다.
3. 위험도 분류를 한다.
4. 관련 코드와 테스트를 검색한다.
5. 샌드박스에서 임시 브랜치를 만든다.
6. AI가 patch를 제안한다.
7. 테스트, 타입체크, 린트, 보안검사를 실행한다.
8. 통과하면 PR을 만든다.
9. 사람이 review하고 merge한다.
```

6.3 엔터프라이즈용 하네스

금융, 의료, 교육, B2B SaaS처럼 실패 비용이 큰 조직에서는 더 강한 구조가 필요하다.
```
Agent runtime:
- LangGraph
- OpenAI Agents SDK
- OpenHands SDK

Tool protocol:
- MCP
- internal tool gateway

Context:
- 사내 문서 RAG
- 코드 검색
- ADR
- 장애 이력
- 보안 정책
- CODEOWNERS

Sandbox:
- Kubernetes isolated runner
- E2B private deployment
- Firecracker 계열 microVM
- ephemeral CI runner

Validation:
- unit test
- integration test
- e2e test
- SAST
- SCA
- secret scanning
- policy-as-code

Approval:
- GitHub CODEOWNERS
- Slack approval
- Jira transition
- security review

Observability:
- OpenTelemetry
- Langfuse
- LangSmith
- audit database
```
엔터프라이즈에서는 AI에게 “더 많은 권한”을 주는 것이 아니라, 오히려 권한을 더 잘게 쪼개고, 승인 절차를 명확히 하는 것이 핵심이다.

7. 실제 구현 예시: 안전한 코딩 하네스

아래는 Python 기준의 최소 구현 예시다.목표는 “AI가 파일을 수정하되, 허용된 경로만 수정하고, 테스트와 린트를 실제로 실행하게 하는 구조”다.

7.1 프로젝트 구조

```
repo/
├─ app/
│  └─ calculator.py
├─ tests/
│  └─ test_calculator.py
├─ harness/
│  ├─ safe_paths.py
│  ├─ policy.py
│  ├─ tools.py
│  ├─ agent.py
│  └─ run_task.py
└─ requirements.txt
```

7.2 예제 대상 코드

```
# app/calculator.py

def divide(a: float, b: float) -> float:
    return a / b
```
요구사항은 다음이다.
```
b가 0이면 Python 기본 ZeroDivisionError를 그대로 노출하지 말고,
ValueError를 발생시켜라.
```
테스트는 다음처럼 둔다.
```
# tests/test_calculator.py

import pytest

from app.calculator import divide


def test_divide_normal_case():
    assert divide(10, 2) == 5


def test_divide_by_zero_raises_value_error():
    with pytest.raises(ValueError):
        divide(10, 0)
```

7.3 path 정책: repo 바깥 접근 차단

```
# harness/safe_paths.py

from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]

ALLOWED_WRITE_DIRS = [
    REPO_ROOT / "app",
    REPO_ROOT / "tests",
]

DENIED_PARTS = {
    ".env",
    ".git",
    "secrets",
    "private_key",
    "id_rsa",
}


def resolve_repo_path(relative_path: str) -> Path:
    """
    사용자가 넘긴 상대 경로를 repo 내부의 실제 경로로 바꾼다.

    핵심:
    - ../../ 같은 path traversal을 막는다.
    - repo 바깥 파일 접근을 막는다.
    - secret 계열 파일 접근을 막는다.
    """
    target = (REPO_ROOT / relative_path).resolve()

    if not str(target).startswith(str(REPO_ROOT)):
        raise PermissionError(f"repo 바깥 경로 접근 차단: {relative_path}")

    lowered = str(target).lower()
    if any(part in lowered for part in DENIED_PARTS):
        raise PermissionError(f"민감 파일 접근 차단: {relative_path}")

    return target


def assert_can_write(relative_path: str) -> Path:
    """
    AI가 파일을 수정하기 전에 반드시 통과해야 하는 정책 검사.
    """
    target = resolve_repo_path(relative_path)

    allowed = any(str(target).startswith(str(base)) for base in ALLOWED_WRITE_DIRS)
    if not allowed:
        raise PermissionError(f"쓰기 허용 범위 밖 파일: {relative_path}")

    return target
```
이 코드가 중요한 이유는 단순하다.AI에게 “민감 파일 읽지 마”라고 말하는 것보다, 애초에 코드로 접근을 막는 것이 훨씬 강하다.

7.4 위험도 분류

```
# harness/policy.py

from dataclasses import dataclass
from typing import Literal


Risk = Literal["low", "medium", "high", "critical"]


@dataclass
class TaskPolicy:
    risk: Risk
    requires_human_approval: bool
    reason: str


HIGH_RISK_KEYWORDS = [
    "auth",
    "authentication",
    "authorization",
    "login",
    "password",
    "payment",
    "billing",
    "invoice",
    "personal data",
    "pii",
    "database migration",
    "production",
]


def classify_task(description: str) -> TaskPolicy:
    """
    단순한 룰 기반 위험도 분류기.

    실무에서는 여기에 LLM 분류기, CODEOWNERS,
    파일 소유권, 장애 이력, 보안 정책을 같이 넣는다.
    """
    text = description.lower()

    for keyword in HIGH_RISK_KEYWORDS:
        if keyword in text:
            return TaskPolicy(
                risk="high",
                requires_human_approval=True,
                reason=f"고위험 키워드 감지: {keyword}",
            )

    return TaskPolicy(
        risk="low",
        requires_human_approval=False,
        reason="일반 코드 수정 작업",
    )
```
이 계층의 목적은 “AI가 결제·인증·개인정보 코드를 아무 승인 없이 수정하는 상황”을 막는 것이다.

7.5 안전 도구 만들기

OpenAI Agents SDK는 Python 함수를 function tool로 노출할 수 있다. 이때 중요한 점은 AI에게 범용 shell을 주지 않는 것이다. 대신 테스트 실행, 파일 읽기, 파일 쓰기, 검색 같은 좁은 도구만 제공한다. OpenAI Agents SDK는 Python 함수와 docstring을 기반으로 tool schema를 구성하는 function tool 방식을 제공한다.
```
# harness/tools.py

import subprocess

from agents import function_tool

from harness.safe_paths import REPO_ROOT, resolve_repo_path, assert_can_write


def _run_command(args: list[str]) -> str:
    """
    shell 문자열을 받지 않고 list[str]만 받는다.

    shell=True를 쓰지 않는 이유:
    - 명령어 인젝션 위험을 줄인다.
    - AI가 '; rm -rf' 같은 문자열을 섞어도 셸 해석이 일어나지 않는다.
    """
    result = subprocess.run(
        args,
        cwd=REPO_ROOT,
        text=True,
        capture_output=True,
        timeout=60,
        shell=False,
    )

    return (
        f"$ {' '.join(args)}\n\n"
        f"[stdout]\n{result.stdout}\n\n"
        f"[stderr]\n{result.stderr}\n\n"
        f"[exit_code]\n{result.returncode}"
    )


@function_tool
def read_file(path: str) -> str:
    """
    repo 내부 파일을 읽는다.

    Args:
        path: repo root 기준 상대 경로. 예: app/calculator.py
    """
    target = resolve_repo_path(path)

    if not target.exists():
        return f"파일이 존재하지 않습니다: {path}"

    if target.is_dir():
        return f"디렉터리는 읽을 수 없습니다: {path}"

    return target.read_text(encoding="utf-8")


@function_tool
def write_file(path: str, content: str) -> str:
    """
    허용된 경로의 파일만 전체 덮어쓴다.

    Args:
        path: repo root 기준 상대 경로. 예: app/calculator.py
        content: 새 파일 내용
    """
    target = assert_can_write(path)

    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(content, encoding="utf-8")

    return f"파일 저장 완료: {path}"


@function_tool
def search_code(query: str) -> str:
    """
    app, tests 디렉터리에서 문자열을 검색한다.

    Args:
        query: 찾을 문자열. 예: divide
    """
    return _run_command(["rg", "-n", query, "app", "tests"])


@function_tool
def run_pytest() -> str:
    """
    pytest를 실행한다.
    """
    return _run_command(["pytest", "-q"])


@function_tool
def run_mypy() -> str:
    """
    mypy 타입 체크를 실행한다.
    """
    return _run_command(["mypy", "app", "tests"])


@function_tool
def run_ruff() -> str:
    """
    ruff lint를 실행한다.
    """
    return _run_command(["ruff", "check", "app", "tests"])


@function_tool
def run_semgrep() -> str:
    """
    Semgrep 보안 스캔을 실행한다.
    """
    return _run_command(["semgrep", "scan", "--config", "auto", "app", "tests"])
```
이 코드의 본질은 다음이다.
```
AI에게 준 것:
- read_file
- write_file
- search_code
- run_pytest
- run_mypy
- run_ruff
- run_semgrep

AI에게 주지 않은 것:
- raw shell
- production DB 접근
- secret 접근
- 배포 권한
- repo 바깥 파일 접근
```
이게 하네스 엔지니어링의 핵심이다.AI가 할 수 있는 일을 늘리는 게 아니라, 할 수 있는 일을 안전한 형태로 재정의하는 것이다.

7.6 에이전트 정의

```
# harness/agent.py

from agents import Agent

from harness.tools import (
    read_file,
    write_file,
    search_code,
    run_pytest,
    run_mypy,
    run_ruff,
    run_semgrep,
)


coding_agent = Agent(
    name="SafeCodingAgent",
    instructions="""
너는 안전한 코딩 에이전트다.

반드시 다음 규칙을 따른다.

1. 코드를 수정하기 전에 관련 파일을 먼저 읽어라.
2. 파일 구조를 모르면 search_code 도구로 확인해라.
3. 수정은 write_file 도구로만 수행해라.
4. 수정 후 반드시 run_pytest, run_mypy, run_ruff를 실행해라.
5. 보안 관련 코드가 포함되면 run_semgrep도 실행해라.
6. 테스트가 실패하면 실패 로그를 읽고 수정한 뒤 다시 검증해라.
7. 최종 답변에는 다음을 포함해라.
   - 변경한 파일
   - 변경 이유
   - 실행한 검증 명령
   - 통과/실패 결과
   - 사람이 추가 검토해야 할 위험
""",
    tools=[
        read_file,
        write_file,
        search_code,
        run_pytest,
        run_mypy,
        run_ruff,
        run_semgrep,
    ],
)
```
여기서 프롬프트도 중요하지만, 더 중요한 것은 도구 설계다.프롬프트는 에이전트에게 방향을 준다.도구와 정책은 에이전트의 행동 가능 범위를 결정한다.

7.7 실행기

```
# harness/run_task.py

import sys
from dotenv import load_dotenv
from agents import Runner

from harness.agent import coding_agent
from harness.policy import classify_task


load_dotenv()


def main() -> None:
    if len(sys.argv) < 2:
        raise SystemExit(
            "사용법: python -m harness.run_task '작업 설명을 여기에 입력'"
        )

    task_description = sys.argv[1]
    policy = classify_task(task_description)

    print("=== Task Policy ===")
    print(f"risk: {policy.risk}")
    print(f"requires_human_approval: {policy.requires_human_approval}")
    print(f"reason: {policy.reason}")
    print()

    if policy.requires_human_approval:
        print("사람 승인 필요. 에이전트 실행을 중단합니다.")
        return

    prompt = f"""
작업:
{task_description}

허용 범위:
- app/ 아래 파일
- tests/ 아래 파일

완료 조건:
- pytest 통과
- mypy 통과
- ruff 통과
- 변경 요약과 검증 결과 보고

주의:
- 허용 범위 밖 파일은 수정하지 마라.
- 테스트 결과를 실제 도구 실행 결과로 확인하라.
"""

    result = Runner.run_sync(
        coding_agent,
        prompt,
        max_turns=12,
    )

    print("=== Agent Final Output ===")
    print(result.final_output)


if __name__ == "__main__":
    main()
```
실행 예시는 다음이다.
```
python -m harness.run_task \
  "app/calculator.py의 divide 함수가 0으로 나눌 때 ValueError를 발생시키도록 고쳐라"
```
이 작업에서 에이전트가 이상적으로 수행해야 하는 순서는 다음이다.
```
1. search_code("divide")
2. read_file("app/calculator.py")
3. read_file("tests/test_calculator.py")
4. write_file("app/calculator.py", 수정된 코드)
5. run_pytest()
6. run_mypy()
7. run_ruff()
8. 최종 보고
```
최종 코드 결과는 대략 이렇게 된다.
```
# app/calculator.py

def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("division by zero is not allowed")

    return a / b
```

8. CI에서 최종 검증하기

에이전트가 로컬 또는 샌드박스에서 “통과했다”고 해도, 최종 신뢰는 CI가 담당해야 한다.
```
# .github/workflows/ai-harness-check.yml

name: AI Harness Check

on:
  pull_request:
    branches: [main]

permissions:
  contents: read
  security-events: write

jobs:
  validate:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run tests
        run: pytest -q

      - name: Run type check
        run: mypy app tests

      - name: Run lint
        run: ruff check app tests

      - name: Run Semgrep
        run: semgrep scan --config auto app tests
```
보안 강도를 더 높이려면 CodeQL도 추가한다.
```
# .github/workflows/codeql.yml

name: CodeQL

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

permissions:
  security-events: write
  packages: read
  actions: read
  contents: read

jobs:
  analyze:
    name: CodeQL Analyze
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: python

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
```
CI의 의미는 단순 자동화가 아니다.하네스 관점에서 CI는 AI의 작업물을 조직의 품질 기준으로 다시 검증하는 최종 게이트다.

9. 샌드박스 적용

로컬에서 subprocess를 실행하는 구조는 간단하지만, 실제 서비스에서는 격리된 환경이 필요하다.
E2B를 쓰면 AI-generated code를 샌드박스에서 실행할 수 있다. E2B는 AI agent를 위한 secure computer와 sandbox runtime을 제공한다고 설명한다.
개념 코드는 다음과 같다.
```
# harness/e2b_sandbox_example.py

from dotenv import load_dotenv
from e2b_code_interpreter import Sandbox

load_dotenv()


def run_in_e2b(code: str) -> str:
    """
    E2B 샌드박스에서 코드를 실행한다.

    실무에서는 여기에 다음을 붙인다.
    - repo clone
    - dependency install
    - patch apply
    - pytest/mypy/ruff/semgrep 실행
    """
    sbx = Sandbox.create()

    execution = sbx.run_code(code)

    return str(execution)


if __name__ == "__main__":
    output = run_in_e2b(
        """
print("샌드박스 내부에서 실행 중")
print(1 + 2)
"""
    )

    print(output)
```
실제 운영 흐름은 다음에 가깝다.
```
1. GitHub issue 수신
2. 샌드박스 생성
3. repo clone
4. dependency install
5. AI가 patch 생성
6. patch 적용
7. 테스트·타입체크·린트·보안 스캔 실행
8. 통과하면 PR 생성
9. 실패하면 로그를 agent에게 다시 제공
10. 반복 횟수 초과 시 사람에게 넘김
```
샌드박스를 쓴다고 모든 문제가 해결되는 건 아니다. 반드시 다음을 지켜야 한다.
```
- production credential을 넣지 않는다.
- 실제 결제 API key를 넣지 않는다.
- 운영 DB에 연결하지 않는다.
- 외부 네트워크 접근을 제한한다.
- 실행 시간과 리소스를 제한한다.
- 모든 파일 변경과 tool call을 기록한다.
```

10. LangGraph로 확장하는 방식

OpenAI Agents SDK는 빠르게 도구 호출형 에이전트를 만들기 좋다.하지만 승인, 재시도, 장기 실행, 상태 저장이 많아지면 LangGraph 스타일이 더 적합하다.
LangGraph는 node와 edge로 workflow를 구성하는 방식이다. 각 node는 현재 state를 받아 처리하고, edge가 다음 단계를 결정한다.
하네스를 LangGraph로 설계하면 다음과 같다.
```
classify_task
   ↓
collect_context
   ↓
generate_patch
   ↓
apply_patch
   ↓
run_checks
   ↓
security_review
   ↓
human_approval?
   ↓
create_pr
```
간단한 골격은 다음과 같다.
```
from typing import TypedDict, Literal

from langgraph.graph import StateGraph, START, END


class HarnessState(TypedDict):
    task: str
    risk: Literal["low", "medium", "high", "critical"]
    context: str
    patch_summary: str
    check_output: str
    approved: bool


def classify_task_node(state: HarnessState) -> dict:
    task = state["task"].lower()

    if "payment" in task or "auth" in task or "database" in task:
        return {"risk": "high"}

    return {"risk": "low"}


def collect_context_node(state: HarnessState) -> dict:
    return {
        "context": "관련 파일과 테스트, 문서, 과거 PR을 수집했다는 가정"
    }


def generate_patch_node(state: HarnessState) -> dict:
    return {
        "patch_summary": "LLM이 생성한 patch 요약"
    }


def run_checks_node(state: HarnessState) -> dict:
    return {
        "check_output": "pytest passed\nmypy passed\nruff passed"
    }


def route_after_checks(state: HarnessState) -> str:
    if state["risk"] in {"high", "critical"}:
        return "wait_for_approval"

    return "create_pr"


def wait_for_approval_node(state: HarnessState) -> dict:
    return {"approved": False}


def create_pr_node(state: HarnessState) -> dict:
    print("PR 생성")
    print(state["patch_summary"])
    print(state["check_output"])
    return {}


builder = StateGraph(HarnessState)

builder.add_node("classify_task", classify_task_node)
builder.add_node("collect_context", collect_context_node)
builder.add_node("generate_patch", generate_patch_node)
builder.add_node("run_checks", run_checks_node)
builder.add_node("wait_for_approval", wait_for_approval_node)
builder.add_node("create_pr", create_pr_node)

builder.add_edge(START, "classify_task")
builder.add_edge("classify_task", "collect_context")
builder.add_edge("collect_context", "generate_patch")
builder.add_edge("generate_patch", "run_checks")

builder.add_conditional_edges(
    "run_checks",
    route_after_checks,
    {
        "wait_for_approval": "wait_for_approval",
        "create_pr": "create_pr",
    },
)

builder.add_edge("wait_for_approval", END)
builder.add_edge("create_pr", END)

graph = builder.compile()

result = graph.invoke(
    {
        "task": "app/calculator.py divide 버그 수정",
        "risk": "low",
        "context": "",
        "patch_summary": "",
        "check_output": "",
        "approved": False,
    }
)

print(result)
```
이 방식은 특히 다음 상황에서 유리하다.
```
- 작업 시간이 길다.
- 중간에 사람 승인이 필요하다.
- 실패 지점부터 재시작해야 한다.
- 여러 에이전트가 역할을 나눠야 한다.
- audit trail이 중요하다.
```

11. 하네스 설계 원칙


11.1 raw shell을 주지 말 것

나쁜 설계:
```
run_shell(command: str)
```
좋은 설계:
```
run_unit_tests()
run_typecheck()
run_linter()
run_security_scan()
read_file(path)
write_file(path, content)
```
범용 shell은 너무 강하다.하네스에서는 범용 능력을 좁은 안전 도구로 쪼갠다.

11.2 프롬프트보다 코드로 강제할 것

약한 방식:
```
운영 DB는 절대 건드리지 마.
```
강한 방식:
```
if target_env == "production":
    raise PermissionError("Agent cannot access production database")
```
AI에게 금지사항을 설명하는 것은 필요하지만 충분하지 않다.금지는 코드, 네트워크, 권한, CI policy로 강제해야 한다.

11.3 검증은 시스템이 수행할 것

나쁜 방식:
```
AI: 테스트는 통과할 것 같습니다.
```
좋은 방식:
```
CI:
- pytest passed
- mypy passed
- ruff passed
- semgrep passed
```
모델의 판단은 참고다.최종 근거는 검증 결과다.

11.4 위험도별 자동화 수준을 다르게 둘 것

```
문서 수정:
- 자동 PR 가능

UI 문구 수정:
- 자동 PR 가능

테스트 추가:
- 자동 PR 가능

일반 API 로직:
- PR + 사람 리뷰

인증/인가:
- 사람 승인 필수

결제:
- 사람 승인 필수

개인정보 처리:
- 사람 승인 필수

DB migration:
- 별도 승인 필수

운영 배포:
- 원칙적으로 사람 승인 필수
```
하네스 엔지니어링의 목표는 모든 것을 자동화하는 것이 아니다.자동화해도 되는 것과 사람이 반드시 봐야 하는 것을 나누는 것이 핵심이다.

12. 적용 순서

실제 팀에 도입한다면 다음 순서가 좋다.

1단계: 읽기 전용 에이전트

처음부터 코드 수정을 맡기지 않는다.
```
- 이슈 요약
- 관련 파일 찾기
- 영향 범위 분석
- 테스트 후보 제안
- 위험도 분류
```
이 단계에서는 에이전트가 코드를 쓰지 않는다.

2단계: 테스트 작성 에이전트

다음으로 테스트만 작성하게 한다.
```
- tests/ 아래만 수정 가능
- app/ 수정 금지
- 테스트 실패 로그 보고
```
테스트는 기능 요구사항을 고정하는 역할을 한다.AI가 구현 전에 테스트를 만들면, 이후 구현의 기준이 생긴다.

3단계: 저위험 코드 수정

다음은 낮은 위험도의 코드만 맡긴다.
```
- 문서
- UI copy
- 단순 유틸 함수
- 타입 오류 수정
- lint fix
- 테스트 보강
```
이 단계부터는 PR 생성까지 자동화할 수 있다.

4단계: 샌드박스와 CI 필수화

이 단계에서는 모든 AI 실행을 격리한다.
```
- Docker 또는 E2B
- production secret 미주입
- mock DB 사용
- timeout 설정
- resource limit 설정
- CI 재검증
```

5단계: 고위험 도메인 승인 체계

마지막으로 고위험 도메인을 명확히 분리한다.
```
- auth
- payment
- user data
- database migration
- deployment
- infrastructure
```
이 영역은 AI가 patch를 제안할 수는 있어도, 사람 승인 없이 반영하면 안 된다.

13. 하네스 엔지니어링의 본질

하네스 엔지니어링은 “프롬프트를 잘 쓰는 법”이 아니다.또 “AI 코딩 툴을 하나 도입하는 법”도 아니다.
더 본질적으로는 다음이다.
AI가 실수할 것을 전제로, 그 실수가 시스템으로 들어가기 전에 막고, 발견하고, 되돌리고, 기록하게 만드는 소프트웨어 공학이다.기존 바이브코딩은 빠르지만, 검증과 통제 장치가 약하면 위험하다.하네스 엔지니어링은 그 속도를 버리지 않으면서, 다음을 추가한다.
```
- 허용 범위
- 금지 범위
- 컨텍스트 공급
- 샌드박스 실행
- 테스트 검증
- 보안 스캔
- 사람 승인
- 감사 로그
- 실패 재현성
```
그래서 한 문장으로 정리하면 다음과 같다.
하네스 엔지니어링은 AI 에이전트를 “그럴듯한 코드를 만드는 도구”에서 “검증 가능한 소프트웨어 생산 과정에 참여하는 통제된 작업자”로 바꾸는 공학이다.앞으로의 개발자는 AI에게 단순히 “코드 짜줘”라고 말하는 사람이 아니라,AI가 안전하게 일할 수 있는 작업장, 도구, 규칙, 검증 체계를 설계하는 사람이 된다.
이 변화의 핵심은 모델이 아니다.모델은 계속 바뀐다. GPT, Claude, Gemini, Llama, Codex 계열은 계속 교체될 수 있다.
하지만 좋은 하네스는 남는다.
```
모델은 교체 가능해야 한다.
도구는 제한 가능해야 한다.
검증은 자동화되어야 한다.
권한은 최소화되어야 한다.
위험 작업은 승인되어야 한다.
모든 행동은 추적 가능해야 한다.
```
이 원칙이 지켜질 때, 바이브코딩은 단순한 감각적 자동완성이 아니라 실제 프로덕션 엔지니어링의 일부가 된다.

 

 

대부분의 Python 개발자들은 **비동기 작업(Background Task)**을 처리할 때 Celery를 떠올린다.

하지만 Celery는 설정이 복잡하고, 유지보수가 어렵고, 과한 기능이 많아 작은 프로젝트에서는 오히려 불편하다.

 

📌 RQ (Redis Queue)와 Dramatiq은 이런 문제를 해결하는 가볍고 강력한 대안이다.

특히 RQ는 간단한 Redis 기반 큐 시스템, Dramatiq은 Celery와 비슷하면서도 훨씬 직관적이고 빠른 대안이다.

1. RQ (Redis Queue): 초간단 Background Task 라이브러리

 

RQ는 Celery보다 훨씬 단순한 구조로, Redis만 있으면 즉시 사용 가능하다.

설치부터 사용까지 1분이면 충분하다.

 

🚀 RQ 설치 및 사용법

pip install rq
import time
from redis import Redis
from rq import Queue

# Redis 연결 및 큐 생성
redis_conn = Redis()
queue = Queue(connection=redis_conn)

# 비동기 실행할 함수 정의
def background_task(n):
    time.sleep(n)
    return f"완료: {n}초 후"

# 작업을 큐에 넣기
job = queue.enqueue(background_task, 5)

print(f"작업 ID: {job.id}")  # 작업 ID 출력

🔹 RQ Worker 실행 (작업 처리)

 

RQ는 Celery처럼 복잡한 설정 없이 worker 실행만으로 비동기 작업을 처리할 수 있다.

rq worker

결과:

RQ Worker가 실행되면서 대기 중인 작업을 즉시 처리한다.

Celery처럼 복잡한 설정 없이, 단순한 작업을 Redis에서 관리할 때 매우 유용하다.

2. Dramatiq: Celery를 완벽하게 대체할 강력한 백그라운드 태스크 라이브러리

 

Celery는 강력하지만, 설정이 너무 복잡하고 무겁다는 단점이 있다.

Dramatiq은 Celery와 거의 동일한 기능을 제공하지만 훨씬 가볍고 빠르다.

 

🚀 Dramatiq 설치 및 기본 사용법

pip install dramatiq redis
import dramatiq
import time

# 비동기 태스크 정의
@dramatiq.actor
def background_task(n):
    time.sleep(n)
    print(f"완료: {n}초 후")

# 태스크 실행
background_task.send(5)

🔹 Dramatiq Worker 실행

 

Celery처럼 복잡한 celeryconfig.py 설정 없이, 단순히 worker만 실행하면 된다.

dramatiq my_script

Dramatiq의 장점

Celery보다 설정이 간편하고,

Redis, RabbitMQ, Kafka 등 다양한 메시지 브로커를 지원,

멀티 프로세싱과 멀티스레딩 지원으로 성능이 뛰어나다.

📌 RQ vs Celery vs Dramatiq 비교

기능RQCeleryDramatiq

설치 난이도 매우 쉬움 복잡함 쉬움
메시지 브로커 Redis Redis, RabbitMQ, SQS Redis, RabbitMQ, Kafka
성능 가벼움 무거움 빠름
비동기 작업 지원 지원 지원
멀티 프로세스 지원 지원 지원 (최적화)

RQ는 간단한 작업 큐,

Dramatiq은 Celery를 대체할 강력한 옵션이다.

🚀 결론: 언제 어떤 걸 써야 할까?

 

RQ를 선택해야 할 때

Redis만 사용하고 싶을 때

단순한 백그라운드 태스크 큐가 필요할 때

빠르게 개발하고 싶을 때

 

Dramatiq을 선택해야 할 때

Celery의 기능이 필요하지만 더 가볍고 빠른 솔루션이 필요할 때

RabbitMQ, Kafka 등 다양한 브로커를 활용할 때

성능 최적화가 중요한 시스템에서 사용할 때

📌 Celery가 너무 무겁다면?

📌 RQ와 Dramatiq을 적극 고려해보자!

 

 

GPT-4, Claude, Mistral 등 다양한 AI 모델이 쏟아지는 시대다. 하지만 모델 선택과 비용 문제가 개발자들에게는 가장 큰 고민이다.

특히 API 요청을 최적화하고 비용을 줄이는 방법이 절실한데, 이를 해결해 줄 강력한 솔루션이 바로 LiteLLM이다.

 

LiteLLM은 하나의 통합 API로 OpenAI, Anthropic, Mistral, Llama 등 여러 LLM(Large Language Model) API를 동시에 다룰 수 있는 라이브러리다.

즉, 코드를 수정하지 않고도 다양한 LLM을 교체하며 최적의 성능과 비용을 찾을 수 있다.

LiteLLM이 해결하는 문제

 

❌ 1. 특정 AI API에 종속되는 문제 (Vendor Lock-in)

기존에는 OpenAI API를 쓰면 코드를 OpenAI 전용으로 작성해야 했음

하지만 Anthropic Claude, Mistral, Llama 같은 더 저렴하고 빠른 대안이 계속 등장

LiteLLM을 사용하면 코드를 수정하지 않고도 다양한 AI 모델로 즉시 전환 가능

 

💰 2. API 비용 절감

GPT-4-turbo는 훌륭하지만 가격이 비싸다

경우에 따라 Claude 3, Mistral 7B 같은 모델이 더 싸고 빠를 수 있음

LiteLLM을 사용하면 비용 대비 최적의 모델을 자동 선택 가능

 

🚀 3. 로드 밸런싱 & 장애 대응

특정 API가 느려지거나 장애가 발생하면 자동으로 다른 LLM으로 전환 가능

여러 API 제공자를 조합하여 자동 로드 밸런싱 및 페일오버(failover) 가능

LiteLLM 설치 및 기본 사용법

 

LiteLLM을 설치하려면 간단히 다음 명령어를 실행하면 된다.

pip install litellm

이제 OpenAI API를 호출하는 기존 코드에서 LiteLLM으로 쉽게 변경할 수 있다.

import litellm

# OpenAI API처럼 사용 가능
response = litellm.completion(
    model="gpt-4-turbo",
    messages=[{"role": "user", "content": "AI 모델 추천해줘"}],
    api_key="your-openai-key"
)

print(response['choices'][0]['message']['content'])

기존 OpenAI API 코드와 100% 호환되기 때문에, 기존 코드를 수정할 필요 없이 바로 적용할 수 있다.

🚀 다양한 LLM을 자유롭게 전환하기

 

LiteLLM은 단순히 OpenAI만 지원하는 게 아니다.

Anthropic Claude, Mistral, Azure OpenAI, Groq 같은 다양한 모델을 한 줄만 변경하여 교체 가능하다.

# OpenAI 대신 Claude 3 사용
response = litellm.completion(
    model="claude-3-opus",
    messages=[{"role": "user", "content": "AI 모델 추천해줘"}],
    api_key="your-anthropic-key"
)
# Mistral 7B 사용 (오픈소스 모델)
response = litellm.completion(
    model="mistral-7b",
    messages=[{"role": "user", "content": "빠르고 저렴한 AI 추천해줘"}],
    api_key="your-mistral-key"
)

코드를 수정할 필요 없이, API 키만 변경하면 다른 AI 모델을 사용할 수 있다.

🛠️ LiteLLM의 고급 기능

 

🔄 1. 자동 모델 선택 (Failover & Load Balancing)

 

어떤 모델이 가장 빠르고 저렴한지 자동으로 선택할 수 있다.

예를 들어, GPT-4가 느리다면 Claude 3로 자동 전환하도록 설정 가능하다.

response = litellm.completion(
    model=["gpt-4-turbo", "claude-3-opus", "mistral-7b"],
    messages=[{"role": "user", "content": "현재 시간은?"}],
    api_key={"openai": "your-openai-key", "anthropic": "your-anthropic-key"}
)

리스트 형태로 여러 모델을 입력하면 자동으로 최적의 모델을 선택하여 요청을 보낸다.

💰 2. API 비용 절감을 위한 라우팅

 

LiteLLM을 사용하면 요청을 자동으로 가장 저렴한 API로 라우팅 가능하다.

예를 들어, 간단한 요청은 Mistral 7B에 보내고, 복잡한 요청만 GPT-4로 보내는 식이다.

def choose_model(user_query):
    if len(user_query) < 50:
        return "mistral-7b"  # 가벼운 요청은 저렴한 모델 사용
    else:
        return "gpt-4-turbo"  # 복잡한 요청은 GPT-4 사용

response = litellm.completion(
    model=choose_model("간단한 질문이야"),
    messages=[{"role": "user", "content": "간단한 질문이야"}]
)

비용을 절약하면서도 성능을 유지할 수 있다.

🏎️ 3. 로컬 모델 (Llama3, Mixtral)도 사용 가능

 

LiteLLM은 클라우드 API뿐만 아니라, 로컬에서 실행되는 모델도 지원한다.

즉, OpenAI API와 로컬 Llama3 모델을 동시에 사용할 수도 있다.

response = litellm.completion(
    model="http://localhost:8000/v1/completions",  # 로컬 Llama3 API
    messages=[{"role": "user", "content": "로컬 모델 사용해줘"}]
)

비용 절감과 프라이버시 보호를 위해 로컬 모델을 함께 활용할 수 있다.

💡 LiteLLM을 활용하면 이런 문제가 해결된다

문제LiteLLM 솔루션

특정 API(OpenAI)에 종속됨 다양한 LLM을 자유롭게 전환 가능
비용이 너무 비쌈 자동으로 저렴한 모델을 선택하여 비용 절감
API 응답 속도가 느림 여러 API를 조합하여 로드 밸런싱
특정 API가 다운됨 자동으로 다른 API로 전환 (Failover)
로컬 모델과 클라우드 API를 함께 사용하고 싶음 OpenAI + Llama3 같이 혼합 사용 가능

이제 OpenAI, Claude, Mistral, Llama3를 쉽게 조합하여 최적의 AI 환경을 만들 수 있다.

🚀 결론: LiteLLM을 사용해야 하는 이유

 

LiteLLM은 단순한 API Wrapper가 아니다.

AI 비용 절감, 성능 최적화, 장애 대응까지 해결할 수 있는 강력한 솔루션이다.

 

📌 언제 LiteLLM을 써야 할까?

 

OpenAI API 비용이 너무 부담될 때 → Mistral, Claude 등 저렴한 대안 자동 선택

API 장애나 속도 문제를 해결하고 싶을 때 → Failover & Load Balancing 지원

여러 LLM을 자유롭게 전환하며 실험하고 싶을 때

로컬 Llama3 같은 모델과 클라우드 API를 함께 사용하고 싶을 때

 

지금 당장 LiteLLM을 도입하면 비용 절감과 성능 최적화를 동시에 잡을 수 있다.

+ Recent posts