이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Harness Engineering은 프롬프트와 컨텍스트 주변에 무엇을 추가하는가?
  • structured output, retry, fallback, guardrail, tracing은 어디에서 연결되는가?
  • AI 호출 실패를 백엔드 API 실패로 어떻게 변환해야 하는가?
  • side effect가 있는 tool 호출에서 retry가 왜 위험한가?

개요

Harness Engineering은 모델 호출을 안전하게 실행하기 위한 주변 장치를 설계하는 일이다. 프롬프트와 context를 만들고, 모델을 호출하고, 응답을 검증하고, 실패를 분류하고, retry나 fallback을 결정하고, 로그와 trace와 eval 데이터를 남기는 전체 실행 틀이다.

Prompt Engineering이 입력 문장을 다듬고 Context Engineering이 근거 자료를 조립한다면, Harness Engineering은 이 둘을 운영 가능한 시스템으로 감싼다.

왜 백엔드 개발자에게 중요한가

백엔드 서비스는 실패를 다뤄야 한다. LLM API는 일반 DB 호출보다 실패 모드가 다양하다.

  • provider timeout
  • rate limit
  • content policy refusal
  • schema validation 실패
  • partial output
  • streaming 중 연결 끊김
  • tool calling 중복 실행
  • token limit 초과
  • fallback model의 다른 응답 형식

이 실패를 “모델이 가끔 이상해요”로 넘기면 운영할 수 없다. 백엔드는 실패 유형을 분류하고, 사용자에게 줄 응답과 내부 재시도 정책을 정해야 한다.

핵심 원리

Harness는 다음 단계로 볼 수 있다.

  1. 입력 검증
  2. context 조립
  3. prompt version 선택
  4. 모델 호출
  5. 응답 schema 검증
  6. 실패 분류
  7. retry 또는 fallback
  8. 결과 저장 또는 사용자 응답
  9. trace와 eval 데이터 기록

중요한 것은 retry와 fallback이 무조건 좋은 것이 아니라는 점이다. 읽기 전용 요약 요청은 재시도하기 쉽다. 그러나 tool calling이 외부 메일 발송, 결제, DB 변경을 실행했다면 idempotency 없이 재시도하면 같은 작업을 두 번 할 수 있다.

코드로 이해하기

다음 예시는 structured output 검증과 retry를 함께 처리하는 간단한 Python harness다.

from dataclasses import dataclass
from typing import Any
import json
import time
 
from pydantic import BaseModel, ValidationError
 
 
class SummaryResult(BaseModel):
    title: str
    bullets: list[str]
    risk_level: str
 
 
@dataclass(frozen=True)
class LlmResponse:
    content: str
    model: str
    finish_reason: str
 
 
class LlmClient:
    def complete(self, *, prompt: str, timeout_seconds: float) -> LlmResponse:
        raise NotImplementedError
 
 
def call_with_validation(
    *,
    client: LlmClient,
    prompt: str,
    prompt_version: str,
    max_attempts: int = 2,
) -> SummaryResult:
    last_error: Exception | None = None
 
    for attempt in range(1, max_attempts + 1):
        started = time.monotonic()
        try:
            response = client.complete(prompt=prompt, timeout_seconds=8.0)
            parsed: Any = json.loads(response.content)
            result = SummaryResult.model_validate(parsed)
            elapsed_ms = int((time.monotonic() - started) * 1000)
            print({
                "event": "llm.summary.success",
                "prompt_version": prompt_version,
                "model": response.model,
                "attempt": attempt,
                "elapsed_ms": elapsed_ms,
            })
            return result
        except (json.JSONDecodeError, ValidationError) as exc:
            last_error = exc
            print({
                "event": "llm.summary.validation_failed",
                "prompt_version": prompt_version,
                "attempt": attempt,
                "error_type": type(exc).__name__,
            })
            if attempt == max_attempts:
                break
        except TimeoutError as exc:
            last_error = exc
            if attempt == max_attempts:
                break
            time.sleep(0.2 * attempt)
 
    raise RuntimeError("LLM summary failed after validation retries") from last_error

이 코드는 단순하지만 중요한 구조를 보여준다. 모델 응답을 바로 반환하지 않고, 파싱하고, schema를 검증하고, 실패를 로그로 남기고, 제한된 횟수만 재시도한다.

백엔드 설계 판단

Harness를 설계할 때 먼저 요청 유형을 나눈다.

요청 유형retry 적합성주의점
문서 요약높음비용 제한과 timeout 필요
사용자 입력 분류중간같은 입력의 결정성 필요
RAG 답변중간검색 결과가 틀리면 retry보다 retrieval 개선 필요
메일 발송 tool낮음idempotency key 필수
결제/주문 tool매우 낮음모델이 직접 실행하지 않게 경계 설계
DB 변경 agent낮음승인 단계와 audit log 필요

Harness는 기술이 아니라 업무 위험에 맞춰 달라진다. 고객-facing 기능은 안전한 실패가 중요하고, 사내 도구는 추적성과 재실행 가능성이 중요하다.

장애 상황과 대응

장애가 났을 때는 “모델이 틀림”으로 묶지 않는다.

  • 입력 문제: 사용자 입력이 비어 있거나 너무 길다.
  • context 문제: 검색 결과가 없거나 권한 없는 문서가 섞였다.
  • 모델 호출 문제: timeout, rate limit, provider error가 발생했다.
  • 응답 문제: JSON 파싱이나 schema 검증에 실패했다.
  • 정책 문제: 모델이 refusal을 반환했다.
  • 후처리 문제: 저장, 전송, tool 실행에서 실패했다.

각 단계별로 에러 코드를 분리하면 운영 대응이 쉬워진다. 예를 들어 schema validation 실패율이 늘면 프롬프트나 모델 변경을 의심하고, retrieval empty가 늘면 index와 검색 query를 확인한다.

인프라 협업 포인트

인프라팀과는 다음을 합의한다.

  • 모델 호출 timeout과 전체 API timeout의 관계
  • retry가 트래픽 폭증을 만들지 않도록 backoff와 jitter 적용
  • rate limit 초과 시 queue로 밀지 즉시 실패시킬지
  • fallback model 사용 시 비용과 quota
  • trace sampling 비율과 고카디널리티 필드 제한
  • 장애 시 degrade mode에서 어떤 기능을 끌지

AI 호출은 tail latency가 길어지기 쉽다. 일반 API 서버 스레드나 connection pool을 오래 점유하지 않도록 비동기 처리나 worker 분리를 고려해야 한다.

개인 프로젝트 최소 기준

  • timeout 없는 모델 호출을 만들지 않는다.
  • JSON 응답은 schema 검증을 한다.
  • retry는 최대 횟수를 제한한다.
  • 실패 시 사용자에게 재시도 가능한 메시지를 준다.
  • prompt version과 에러 유형을 로그에 남긴다.

기업 운영 수준 기준

  • 요청 유형별 retry 정책을 문서화한다.
  • provider 장애 대응 런북을 둔다.
  • fallback model과 degrade response를 테스트한다.
  • tool 호출은 idempotency key와 audit log를 가진다.
  • schema validation 실패율, refusal rate, retry count, fallback rate를 지표로 본다.

실전 팁

  • retry는 validation 실패와 provider timeout을 구분해서 적용한다.
  • schema validation 실패가 반복되면 같은 프롬프트를 무작정 재시도하지 말고 repair prompt나 fallback path를 둔다.
  • fallback model은 미리 eval해야 한다. 장애 때 처음 써 보면 출력 형식이 달라서 더 큰 장애가 된다.
  • side effect가 있는 tool은 모델이 바로 실행하기보다 “실행 계획 생성”과 “서버 검증 후 실행”으로 나눈다.

위험 신호!

  • 모든 LLM 실패를 500으로 처리한다.
  • retry가 결제, 메일, DB 변경 tool에도 동일하게 적용된다.
  • fallback model의 출력 schema를 검증한 적이 없다.
  • prompt version 없이 장애 로그를 남긴다.
  • streaming 중 끊긴 partial output을 성공 결과로 저장한다.

확인 질문

확인 질문

  • Harness Engineering은 무엇을 설계하는가?
    • 모델 호출 전후의 입력 검증, context 조립, 응답 검증, retry, fallback, tracing, eval 실행 틀을 설계한다.
  • side effect가 있는 tool 호출에서 retry가 위험한 이유는 무엇인가?
    • 같은 결제, 메일, DB 변경이 중복 실행될 수 있기 때문이다.
  • structured output을 써도 validation 실패 경로가 필요한 이유는 무엇인가?
    • refusal, partial output, provider 오류, schema 불일치가 여전히 발생할 수 있기 때문이다.

Spring AI 적용 연결

참고 문서