하네스 엔지니어링

문서버전 1.0 · 업데이트 2026년 6월 28일

LLM을 스캐폴딩(제어 루프·도구·샌드박스·가드레일·평가)으로 감싸 프로덕션에서 안정적으로 동작하게 만드는 일

하네스 엔지니어링 — LLM을 둘러싼 스캐폴딩.

관련 개념

무엇인가

단일 LLM 호출은 똑똑하지만 그 자체로는 불안정합니다.
같은 프롬프트가 어제는 통과하고 오늘은 빗나가며, 한 번의 잘못된 출력이 곧장 사용자에게 닿습니다.

하네스 엔지니어링은 모델 주위에 스캐폴딩(제어 루프, 호출할 도구, 코드 샌드박스, 입출력 검증, 그리고 동작을 증명하는 측정)을 짜는 일입니다. 모델은 한 부품일 뿐이고, 하네스는 그 모델을 믿을 수 있게 만드는 나머지 전부입니다.

flowchart LR
  subgraph harness [하네스]
    loop[에이전트 루프]
    loop --> tools["도구·웹"]
    loop --> sandbox[코드 샌드박스]
    loop -.-> guard[가드레일]
    loop --> obs[("트레이싱·평가")]
  end
  task[작업] --> loop
  loop --> model[(LLM)]
  model --> loop
  loop --> out[결과]
  class harness aaszone

지난 몇 년 사이 무게중심은 프롬프트 엔지니어링에서 하네스 엔지니어링으로 옮겨 갔습니다.
더 영리한 문장을 찾는 일보다, 모델이 틀릴 수 있다는 전제 위에서 그 둘레를 설계하는 일이 신뢰성을 좌우합니다.

flowchart LR
  subgraph pe ["프롬프트 엔지니어링 — 문장을 다듬기"]
    direction LR
    a1["영리한 프롬프트"] --> a2[("LLM")] --> a3["출력"]
  end
  subgraph he ["하네스 엔지니어링 — 둘레를 설계하기"]
    direction LR
    b1["작업"] --> b2["루프·도구·샌드박스·<br/>가드레일·평가"]
    b2 <--> b3[("LLM")]
    b2 --> b4["신뢰할 수 있는 출력"]
  end
  class he aaszone

왜 중요한가

데모와 프로덕션 사이의 간극은 대부분 모델이 아니라 하네스입니다.
데모는 한 번만 성공하면 되지만, 프로덕션은 수천 번의 호출에서 매번 안전하게 동작해야 합니다.
아래 문제들은 더 큰 모델로 잘 풀리지 않습니다 — 구조의 문제이기 때문입니다.

흔한 실패하네스로 해결
멈추지 않는 루프 — 같은 자리를 맴돌거나 끝없는 재시도제한된 에이전트 루프
위험한 부수효과 — 파일 삭제·임의 네트워크 요청코드 샌드박스
위험·주제 이탈 출력 — 민감정보 노출·부적절·주제 이탈가드레일
조용한 오답 — 그럴듯한 거짓을 자신 있게 단언평가·트레이싱
“어제는 됐는데” — 변경 뒤 조용한 품질 저하·비용 급증관측

각 행은 더 똑똑한 모델이 아니라 모델 바깥의 한 겹으로 해결됩니다.
각 해결책이 어떻게 동작하는지는 아래 안전장치에서 하나씩 다룹니다.

능력 — 추론과 행동

하네스는 하나의 도구가 아니라 역할들의 묶음입니다.
그 가장 안쪽이 에이전트가 추론하고 행동하는 토대입니다.
추론은 모델이, 세상과 맞닿는 행동은 도구·웹 접근이 맡습니다.
무엇을 할 수 있는지를 정하는 부분이고, 이를 믿을 수 있게 만드는 층은 그다음입니다.

모델

추론 엔진입니다.
비용·지연·성능의 균형에 따라 교체할 수 있게 두는 것이 핵심이라, 처음부터 한 제공자에 코드를 묶지 않습니다.

직접 호출

게이트웨이

여러 제공자에 걸친 폴백·비용 라우팅.
한 모델이 죽거나 느려질 때 다른 모델로 넘기고, 모든 호출을 하나의 인터페이스 뒤에 모읍니다.

예시. 게이트웨이 폴백

한 제공자가 죽거나 느려져도 게이트웨이가 다른 모델로 자동으로 넘깁니다.

  • 1차 모델이 타임아웃·5xx → 대체 모델로 폴백
  • 비용·지연 기준으로 모델 라우팅
  • 호출부 코드는 그대로, 모델만 교체
flowchart LR
  call["호출"] --> gw{"게이트웨이"}
  gw --> m1["모델 A"]
  m1 -. 실패·지연 .-> gw
  gw -. 폴백 .-> m2["모델 B"]
  m2 --> out["응답"]
  class m1 roleModel
  class m2 roleModel

도구·웹 접근

에이전트가 실제로 할 수 있는 일을 정합니다. (앱 호출, 검색, 최신 데이터 수집, 브라우저 조작 등)
모델의 지식은 학습 시점에 멈춰 있으니, 지금의 실제 데이터는 도구를 통해 들어옵니다.

더 알아보기도구모델이 외부 세계와 상호작용하도록 연결하는 함수 — 웹 검색·스크래핑· 브라우저 조작·앱 통합으로 학습 컷오프 너머의 실데이터와 행동을 잇는 일

안전장치 — 실패를 막는 층

능력만으로는 신뢰성이 보장되지 않습니다. 아래는 왜 중요한가 표의 실패 모드를 한 겹씩 막는 층으로, 처음부터 전부 필요하지는 않고 위험이 있는 곳에만 더합니다.

코드 샌드박스

모델이 짠 코드를 격리해 실행하므로, 잘못된 명령이 내 머신이나 데이터를 건드릴 수 없습니다.
일회용 런타임이 빠르게 떴다 사라지는 구조라, 코드를 직접 실행하는 에이전트의 안전장치가 됩니다. 파일 삭제나 임의 네트워크 요청 같은 위험한 부수효과를 격리합니다.
또 이 격리 환경은 검증 도구이기도 해서, 평가가 모델 코드를 여기서 돌려 없는 API 호출 같은 오류를 잡아냅니다.

예시. 부수효과 격리

모델이 짠 코드를 일회용 격리 환경에서 돌려, 잘못된 명령이 호스트에 닿지 못하게 합니다.
예를 들어, 모델이 rm -rf ~를 생성해도 샌드박스 안에서만 실행되고 사라집니다.

  • 실수로 파일·디렉터리 삭제
  • 외부로 나가는 임의 네트워크 요청
  • 시스템 설정·의존성 훼손
flowchart LR
  code["모델 코드 — rm -rf ~"] --> sandbox["코드 샌드박스"]
  sandbox -. 격리 .- host["내 머신·데이터"]
  class sandbox roleSource

가드레일

잘못된 입력이나 출력이 흐르기 전에, 런타임에 양쪽을 검증·제약합니다.
들어오는 쪽에서는 프롬프트 인젝션·탈옥을 걸러내고,
나가는 쪽에서는 출력 스키마를 강제하며 안전하지 않거나 주제를 벗어난 내용을 막습니다. 사후가 아니라 에이전트가 도는 동안 검사하는 게 핵심이라, 민감정보 노출이나 주제 이탈 같은 위험·주제 이탈 출력을 사용자에게 닿기 전에 막습니다.

예시. 닿기 전에 막기

입력과 출력 양쪽을 런타임에 검사해 위반을 막거나 고칩니다.
들어오는 쪽에서는 검색된 자료에 숨은 “이전 지시 무시…” 같은 인젝션을 차단하고,
나가는 쪽에서는 내 이메일은 abc@test.com 이야내 이메일은 [가려진 이메일] 이야로 마스킹합니다.

  • 입력·검색 자료에 숨은 “이전 지시 무시…” 류 프롬프트 인젝션·탈옥
  • 개인정보·비밀키가 그대로 노출
  • 욕설·혐오 등 부적절한 표현
  • 묻지도 않은 주제로 벗어난 답변
flowchart LR
  inp["입력 — …이전 지시 무시…"] --> ginp{"가드레일 · 입력"}
  ginp -- 인젝션 차단 --> drop["거절"]
  ginp -- 통과 --> loop["에이전트 루프"]
  loop --> gout{"가드레일 · 출력"}
  gout -- 위반 --> fix["마스킹 — ⟨가림⟩"]
  gout -- 통과 --> user["사용자"]
  fix --> user
  class ginp roleGuard
  class gout roleGuard

평가

지표와 테스트로 품질을 채점해, 변경이 실제로 도움이 됐는지 살펴봅니다.
눈대중 대신 충실도·관련성·정확성 같은 지표를 CI에서 돌리면, 회귀를 사람보다 파이프라인이 먼저 잡습니다. 그럴듯한 거짓을 자신 있게 내놓는 조용한 오답을 걸러내는 일입니다.
주장을 곧이곧대로 믿지 않고 종류에 맞는 검사로 거르며, 그 과정은 관측(트레이싱)에 남습니다.

예시. 주장을 근거와 대조해 채점

답변의 사실 주장을 평가근거(검색 문맥이나 레퍼런스)와 대조해 점수를 매깁니다.
RAG라면 모델이 참고한 바로 그 검색 문맥이 곧 채점 기준입니다.
코드 주장은 pandas.read_yaml 같은 함수가 진짜 있는지를 정적 검사나 코드 샌드박스 실행으로 확인합니다.
매긴 점수를 임계값으로 걸러 차단·재시도하는 흐름은 아래 오케스트레이션에서 다룹니다.

  • 출처 없는 수치 — “이 작업은 GPU를 20개 썼다”처럼 근거 없이 단언
  • 미묘하게 틀린 사실 — “Python 3.9부터 지원”이라지만 실제로는 3.11
  • 없는 API·함수pandas.read_yaml()처럼 존재하지 않는 함수를 호출
flowchart LR
  ans["답변 주장 — 인용 A·B·C"] --> eval["평가"]
  src[("근거 · 검색 문맥/레퍼런스")] --> eval
  eval --> sa["인용 A · 20점"]
  eval --> sb["인용 B · 80점"]
  eval --> sc["인용 C · 60점"]
  class eval roleEval
  class src roleSource

관측

프로덕션에서 모든 단계·토큰·비용을 트레이싱해 회귀를 일찍 잡습니다.
무엇이 호출됐고 어디서 느려졌으며 비용이 어디로 새는지 보이지 않으면 고칠 수도 없습니다.
변경 뒤 조용히 무너지는 “어제는 됐는데” 류의 회귀를 사용자보다 먼저 포착합니다.

예시. 회귀를 일찍 잡기

모든 단계·토큰·비용을 트레이싱해, 변경 뒤의 회귀를 사용자보다 먼저 봅니다.

  • 모델 교체 뒤 미묘하게 떨어진 정확도
  • 프롬프트 수정 뒤 늘어난 토큰·비용
  • 외부 도구 변경으로 깨진 흐름
flowchart LR
  run["프로덕션 실행"] --> trace[("트레이싱 — 단계·토큰·비용")]
  trace --> gate{"회귀?"}
  gate -- 예 --> notify["알림 · 원인 추적"]
  gate -- 아니오 --> ok["정상"]
  class trace roleTrace

오케스트레이션 — 사이클을 돌리는 척추

이제 조각들을 한데 엮습니다. 루프는 추론→행동→관찰을 순서대로 호출하는 척추이고, 게이트에서 멈출지·재시도할지·통과시킬지를 정하며, 매 단계를 관측(트레이싱)에 남깁니다.
앞서 본 모델·도구·평가·샌드박스가 이 루프의 각 단계를 채웁니다.

에이전트 루프

추론→행동 사이클을 조율하고 상태를 관리하며, 도구를 부를지 멈출지 정합니다.
루프가 곧 제어 흐름이라, 단계 수·재시도·분기를 명시적으로 두면 모델이 헤맬 때 무한히 도는 대신 정해진 한도에서 멈춥니다. 같은 자리를 맴돌거나 끝없이 재시도하는 멈추지 않는 루프를 막는 장치입니다.

예시1. 단계 한도에서 멈추기

작업을 추론→도구 호출→관찰로 돌리되, 한도를 넘으면 멈추고 보고합니다.

  • 같은 검색을 반복하는 무한 루프
  • 실패한 도구 호출을 끝없이 재시도
  • 답에 다가가지 못하고 맴도는 추론
flowchart LR
  task["작업"] --> reason["추론"]
  reason --> tool["도구 호출"]
  tool --> obs["관찰"]
  obs --> gate{"한도 초과?"}
  gate -- 아니오 --> reason
  gate -- 예 --> stop["중단 · 보고"]
  reason -.기록.-> trace[("트레이싱")]
  tool -.기록.-> trace
  class reason roleModel
  class tool roleTool
  class trace roleTrace

예시2. 평가 점수로 게이트하기

평가가 매긴 점수를 루프가 임계값으로 걸러, 미만이면 차단해 출처를 재확인하고 통과면 채택합니다.

flowchart LR
  q["질문"] --> model["모델"]
  model --> ans["답변 — 인용 A·B·C"]
  ans --> eval["평가"]
  src[("근거 · 검색 문맥/레퍼런스")] --> eval
  eval --> sa["인용 A · 20점"]
  eval --> sb["인용 B · 80점"]
  eval --> sc["인용 C · 60점"]
  sa --> gate{"70점 미만?"}
  sb --> gate
  sc --> gate
  gate -- 예 --> block["차단 → 출처 재확인"]
  gate -- 아니오 --> ok["채택"]
  model -.기록.-> trace[("트레이싱")]
  eval -.기록.-> trace
  class model roleModel
  class eval roleEval
  class trace roleTrace
  class src roleSource

예시3. 코드를 검증하고 재시도

모델이 짠 코드를 코드 샌드박스나 정적 검사로 돌려, 오류가 나면 고쳐 재시도하고 통과면 채택합니다.
pandas.read_yaml()처럼 없는 함수는 AttributeError로 걸립니다.

flowchart LR
  q["질문"] --> model["모델"]
  model --> code["코드 — pandas.read_yaml()"]
  code --> check["샌드박스 실행 · 정적 검사"]
  env[("실행 환경 · 의존성/스텁")] --> check
  check --> err{"에러·미존재?"}
  err -- 예 --> fix["수정·재시도"]
  err -- 아니오 --> pass["통과"]
  model -.기록.-> trace[("트레이싱")]
  check -.기록.-> trace
  class model roleModel
  class check roleEval
  class trace roleTrace
  class env roleSource

어떻게 접근하나

한꺼번에 다 만들지 않습니다. 위험이 나타나는 순서대로 한 겹씩 더하는 게 핵심입니다.

  1. 루프 + 모델로 시작 — 가장 단순한 추론→행동 루프 하나
  2. 에이전트가 코드를 돌리면 샌드박스 — 부수효과를 격리
  3. 출력이 사용자에게 닿으면 가드레일 — 닿기 전에 검증
  4. 반복을 시작하면 평가 + 트레이싱 — 측정할 수 없는 건 개선할 수 없으니까요.

각 단계는 앞 단계가 만든 위험에 대응합니다.
필요하기 전에 미리 쌓지 말고, 문제가 보일 때 그 자리에 한 겹을 더하세요.

기억할 원칙

  • 작게 시작해 측정으로 키운다 — 평가·트레이싱이 없으면, 무엇을 더해야 할지조차 알 수 없습니다.
  • 의심스러우면 막는다 — 가드레일과 샌드박스는 애매할 때 통과가 아니라, 차단을 기본값으로 둡니다.
  • 모델은 갈아끼울 수 있게 둔다 — 게이트웨이를 두면 한 제공자에 묶이지 않고, 더 싸거나 빠른 모델로 옮기기 쉽습니다.
  • 루프에는 한도를 둔다 — 단계·비용·시간 상한이 폭주하는 에이전트를 멈춥니다.

관련 도구

관련 글