
AI도 인간도 흔들린다. 그럼에도 일정한 출력을 내기 위한 「구조」에 대하여
요약
AI의 확률적 특성과 인간 입력의 불확실성으로 인한 출력 불안정성을 해결하기 위한 시스템 설계 전략을 다룹니다. 부품을 안정시키기보다 흔들림을 흡수하여 일정한 출력을 만드는 '신뢰성 설계(Reliability Design)' 패턴을 제안합니다.
핵심 포인트
- LLM의 확률적 샘플링 특성상 완전한 결정성 보장은 어려움
- 출력 스키마를 먼저 정의하여 자유도를 제한하는 구조 설계 필요
- Pydantic 등을 활용한 구조화된 출력(Structured Output) 활용
- 형태는 맞지만 의미가 틀린 경우를 대비한 검증 게이트(Validation Gate) 설치
- 검증 실패 시를 대비한 대체 경로(Fallback) 설계의 중요성
AI를 업무나 프로덕트에 도입하려고 하면, 비교적 빠른 단계에서 똑같은 벽에 부딪힌다. 바로 「출력이 안정적이지 않다」는 것이다.
같은 프롬프트 (Prompt)를 던져도 돌아오는 JSON의 키 (Key)가 미묘하게 다르다. 어제는 작동하던 추출 처리가 오늘은 설명문이 섞여서 망가진다. 프롬프트를 작성하는 인간 측도 매번 같은 정밀도로 작성할 수는 없다. AI의 출력도, 인간의 입력도 본질적으로 흔들린다.
그럼에도 프로덕트나 업무 시스템은 「일정한 출력」을 요구한다. API의 응답은 정해진 스키마 (Schema)여야 하고, 배치 처리 (Batch Processing)는 매번 같은 결과를 반환하기를 원한다. 이 격차 — 흔들리는 부품으로 흔들리지 않는 출력을 만드는 것 — 를 어떻게 메울 것인가가 AI를 「똑똑하게 사용하는 것」보다 먼저 중요하다.
결론부터 말하자면, 효과적인 것은 똑똑한 AI도 능숙한 프롬프트도 아니다. 흔들림을 흡수하여 일정한 출력으로 수렴시키는 「구조」, 즉 예전부터 묵묵히 해왔던 시스템 연계 및 신뢰성 설계 (Reliability Design) 패턴이다. 이 기사에서는 그 패턴을 7가지로 정리한다.
먼저 전제를 맞춰두자. LLM의 출력은 확률적으로 샘플링 (Sampling)되므로, temperature=0으로 설정하더라도 완전한 결정성 (Determinism)은 보장되지 않는다 (같은 입력이라도 실행 환경이나 버전에 따라 흔들린다). 프롬프트를 추가하면 추가할수록 지시 사항이 충돌하여 다른 방식으로 망가질 수도 있다.
인간 측도 마찬가지다. 프롬프트를 작성하는 엔지니어의 컨디션, 지식, 그날의 집중력에 따라 입력의 질은 들쭉날쭉하다. 「좋은 프롬프트를 쓰면 안정된다」는 것은 숙련도라는 속성을 프롬프트로 옮겼을 뿐, 흔들림의 발생원 자체가 사라진 것은 아니다.
따라서 전략을 바꾼다. 부품 (AI·인간)을 안정시키려 노력하는 것을 그만두고, 흔들리는 부품을 전제로 하여 출구에서 일정하게 만드는 구조를 만든다. 이는 새로운 발상이 아니라, 불안정한 네트워크나 불안정한 하드웨어 위에서 안정적인 시스템을 만들어온 아주 평범한 신뢰성 설계의 응용이다.
가장 효과적인 것은 이것이다. 자유로운 텍스트로 응답하게 하면 무한히 흔들리기 때문에, 출력 스키마를 먼저 정의하고 그것에 부합할 때까지 거부한다.
from pydantic import BaseModel, ValidationError
class ExtractResult(BaseModel):
title: str
...
OpenAI / Anthropic의 Structured Output (JSON 스키마를 전달하여 그 형태로만 응답하게 하는 기능)이나 함수 호출 (Tool Use)도 사상은 같다. 「자유롭게 써도 좋아」를 그만두고 「이 타입으로 응답해」로 바꾸는 것이다. 흔들림의 자유도를 스키마의 양만큼 깎아낸다.
스키마가 맞더라도 내용이 타당하다는 보장은 없다. amount: -5와 같이 「형태는 맞지만 의미가 망가진」 출력을 막기 위해, 검증 게이트 (Validation Gate)를 하나 사이에 둔다.
def validate(result: ExtractResult) -> ExtractResult:
if result.amount < 0:
raise ValueError(f"amount가 음수입니다: {result.amount}")
...
포인트는 「AI의 출력을 1차 정보로서 신뢰하지 않는다」는 것이다. 생성 AI는 유창하게 틀리기 때문에, 유창함에 이끌려 검증을 생략하면 망가진 출력이 그대로 하류 (Downstream)로 흘러간다. 게이트는 다소 번거롭더라도 반드시 설치해야 한다.
검증에서 거부된 후에는 어떻게 할 것인가. 실패 시의 대체 경로 (Fallback)를 처음부터 준비해 둔다.
def extract_with_fallback(text: str) -> ExtractResult:
try:
return validate(parse_llm_output(call_llm(text)))
...
폴백 (Fallback)이 없으면 흔들림이 단 한 번 발생하는 것만으로 처리 전체가 중단된다. 대체 경로가 있다면 흔들림은 「상정 범위 내의 분기」가 된다. 중요한 것은 폴백의 실패를 무시하지 않는 것이다. 최종적으로 실패한다면 그것은 하류에 「실패했다」고 전달해야 한다 (후술).
AI를 포함한 처리는 재시도 (Retry)가 늘어난다. 재시도할 때마다 부작용 (Side Effect)이 이중으로 발생하면 출력은커녕 상태 자체가 흔들린다. 같은 입력이라면 몇 번을 실행해도 같은 결과가 나오도록 (멱등성, Idempotency) 설계한다.
def upsert(record_id: str, result: ExtractResult):
# INSERT가 아닌 UPSERT. 재실행해도 중복되지 않음
db.execute(
...
)
멱등성을 확보해 두면 재시도, 폴백, 병렬 실행이 안전해진다. 「한 번 더 돌려도 괜찮다」는 안심감이 불안정한 부품을 다루는 토대가 된다.
폴백 (Fallback)과 유사하지만, 재시도 (Retry)는 무한 루프의 위험이 있으므로 구분하여 작성한다. 지수 백오프 (Exponential Backoff) + 상한 횟수를 설정하여, 반드시 포기할 시점을 결정한다.
import time
def retry(fn, max_attempts=3):
for attempt in range(max_attempts):
...
「성공할 때까지 돌린다」는 언뜻 쉬워 보이지만, 변동성 (Fluctuation)이 구조적인 경우 (프롬프트가 근본적으로 잘못된 경우 등)에는 영원히 끝나지 않는다. 상한을 정해두고, 초과하면 인간에게 넘긴다.
자동화를 진행할수록 「AI의 출력으로 인해 불가역적인 조작 (공개·송금·삭제)이 실행된다」는 리스크가 커진다. 불가역 조작 직전에 인간의 승인을 한 단계 삽입한다.
def publish(article, auto_checks_passed: bool, human_approved: bool):
if not auto_checks_passed:
raise GateError("자동 체크 미통과")
...
기계 체크와 인간 체크는 역할이 다르다. 기계는 「형식·임계값·집합 일치」를 고속으로 걸러내고, 인간은 「이것을 정말로 내보내도 되는가」라는 문맥 판단을 한다. 두 가지를 직렬로 배치하면 변동성을 놓치는 일이 줄어든다.
마지막으로 전체 구조에 관한 이야기. 생성·검증·정형·저장을 하나의 거대한 함수에 몰아넣으면, 어디에서 변동이 생겼는지 알 수 없게 된다. 공정을 분할하여 각 공정의 입출력을 고정한다.
[생성] --raw text--> [스키마 강제] --typed--> [검증] --valid--> [정형] --> [멱등 저장]
↑ 변동성은 이 2개 공정에 가둔다
이렇게 하면 변동성이 발생할 수 있는 곳은 「생성」과 「스키마 강제」의 경계뿐이 된다. 그보다 하류는 타입 (Type)이 보장된 세계이므로 안정적으로 작성할 수 있다. 디버깅 시에도 「어느 공정에서 망가졌는지」를 일의적으로 파악할 수 있다.
지금까지 언급한 7가지 — 스키마 강제·검증 게이트·폴백·멱등성·재시도 상한·인간 게이트·파이프라인 분할 — 은 모두 AI 전용 신기술이 아니다. 불안정한 네트워크나 외부 API를 상대로 시스템을 안정시키기 위해 예전부터 해왔던, 수수한 연계·신뢰성 설계 패턴이다.
AI의 등장으로 「똑똑한 부품」은 손에 넣었다. 하지만 똑똑한 부품은 동시에 변동하는 부품이기도 하다. 변동하는 부품을 일정한 출력으로 수렴시키는 일은 AI가 똑똑해져도 사라지지 않는다. 오히려 부품이 강력해진 만큼, 그 출력을 받아내는 「구조」의 설계가 병목 (Bottleneck)으로 옮겨오고 있다.
프롬프트를 다듬는 것도 중요하지만, 그 앞단에서 「변동하더라도 일정하게 수렴하는 구조」를 한 겹 끼워 넣는다. AI를 업무에 도입하여 출력의 불안정성으로 고민하고 있다면, 우선 이 7가지 패턴 중 무엇이 결여되어 있는지 살펴보면 좋다. 대부분의 경우, 부족한 것은 똑똑함이 아니라 변동성을 받아내는 틀이다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기