본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 17. 03:45

프롬프트가 상태 머신(State Machine)이 되도록 방치하지 마세요

요약

프롬프트를 전체 시스템의 런타임으로 사용하는 설계의 위험성을 경고하며, 확률적인 LLM을 결정론적인 워크플로 내의 하나의 단계로 통합하는 아키텍처를 제안합니다.

핵심 포인트

  • 프롬프트에 모든 로직을 맡기면 예측 불가능한 결과가 발생함
  • LLM 호출을 독립적인 타입이 지정된 단계로 분리해야 함
  • 결정론적 워크플로를 통해 테스트 가능성과 제어력을 확보할 것
  • 구조화된 출력과 스키마 검증을 통해 모델의 확률적 특성을 보완

프롬프트가 상태 머신 (State Machine)이 되도록 방치하지 마세요

당신은 6개월 전에 LLM 기능을 출시했습니다. 그런데 이제는 동일한 사용자 입력이... 당신이 지목할 수 있는 그 무엇 때문도 아닌 이유로 완전히 다른 출력을 생성합니다. 샘플링 (Sampling)의 문제일까요? 컨텍스트 (Context)가 가득 차서 청크 (Chunk)가 누락된 시간 때문일까요? 아무도 모릅니다. 이것이 바로 프롬프트가 당신의 런타임 (Runtime)이 되었을 때 발생하는 현상입니다.

함정: 의도치 않은 런타임으로서의 프롬프트

TypeScript에서 이 함정이 어떤 모습인지 보여드리겠습니다:

async function handleUserRequest(input: string): Promise<string> {
  const prompt = `
    You are a helpful assistant.
...

여기서 모델이 모든 것을 수행하고 있습니다: 의도 (Intent)를 결정하고, 데이터를 수집하고, 출력을 포맷팅하며, 무엇을 유지할지 선택합니다. 이것은 '풋건 (Footgun, 실수하기 쉬운 설계)'입니다. 당신은 런타임을 확률적 함수 (Stochastic function)에 넘겨버린 것입니다.

Gartner는 많은 실패한 에이전트형 AI (Agentic AI) 프로젝트의 원인을 불분명한 가치와 부적절한 리스크 제어 때문이라고 분석합니다. 결정론적 (Deterministic)이고 테스트 가능한 워크플로 (Workflow)는 이 두 가지 문제를 모두 해결합니다. 해결책은 더 나은 프롬프트가 아닙니다. 해결책은 프롬프트를 아키텍처 (Architecture)로 사용하는 것을 중단하는 것입니다.

여기서 "결정론적 (Deterministic)"이라는 말이 의미할 수 있는 것과 없는 것

당신이 제어할 수 있는 것과 제어할 수 없는 것에 대해 솔직해지십시오.

제어할 수 없는 것: 모델의 정확한 출력. 모델은 설계상 확률적 (Probabilistic)입니다.

제어할 수 있는 것:

  • 출력의 형태 (구조화된 출력 (Structured output) 및 스키마 검증 (Schema validation))
  • 모델 호출 전후에 실행되는 단계들
  • 모델에 입력되는 데이터
  • 출력이 검증에 실패했을 때 발생하는 일
  • 결과가 되돌릴 수 없는 어떤 작업에 반영되기 전에 사람이 검토할지 여부

여기서 결정론적 (Determinism)이라는 것은 다음과 같은 의미입니다: 매번 동일한 입력, 동일한 워크플로 단계, 동일한 가드레일 (Guardrails). 매번 동일한 토큰 (Tokens)이 나오는 것을 의미하는 것이 아닙니다. 이것은 현실적이고 달성 가능한 목표입니다. 또한 팀이 빠르게 움직일 때 건너뛰기 쉬운 부분이기도 합니다.

모델 호출 주변의 타입이 지정된 워크플로 단계

작업을 별개의 타입이 지정된 단계들로 나누십시오. 각 단계는 명확한 입력 타입 (Input type)과 명확한 출력 타입 (Output type)을 가집니다. 모델 호출은 파이프라인 (Pipeline)의 한 단계일 뿐, 전체가 아닙니다.

type WorkflowInput = {
  userId: string;
  rawRequest: string;
...

각 단계는 독립적으로 단위 테스트 (Unit test)가 가능합니다. classify를 모킹 (Mock)하여 고정된 ModelOutput을 반환하게 함으로써, respond를 완전히 격리된 상태에서 테스트할 수 있습니다. 프롬프트가 런타임 (Runtime)이었을 때는 불가능했던 일입니다.

Diagram of the typed LLM workflow: WorkflowInput feeds enrich(), producing EnrichedInput, which feeds classify() (LLM call), producing ModelOutput, which feeds respond(), producing WorkflowResult

계약으로서의 구조화된 출력 (Structured output) + 스키마 검증 (Schema validation)

구조화된 데이터가 필요한 경우, 모델 호출 단계에서 절대로 가공되지 않은 문자열 (Raw string)을 반환해서는 안 됩니다. JSON 모드 (JSON mode), 도구 호출 (Tool calling), 또는 스키마 제약이 있는 완성 (Schema constrained completion)을 사용한 다음, 즉시 검증하십시오.

import { z } from "zod";

const ModelOutputSchema = z.object({
...

Zod는 당신에게 계약 (Contract)을 제공합니다. 모델이 이탈 (Drift)하더라도, 검증 단계에서 앱의 나머지 부분이 출력을 확인하기 전에 이를 잡아냅니다. "LLM 응답을 어떻게 검증하나요?"라는 질문에 대한 답은 다음과 같습니다: 신뢰 (Trust)가 아니라, 파싱 (Parse) 시점의 스키마 검증 (Schema validation)입니다.

재시도 (Retries), 멱등성 (Idempotency), 그리고 실패 게이트 (Failure gates)

검증 실패가 조용히 크래시 (Crash)를 일으키게 해서는 안 됩니다. 모델 호출을 재시도 예산 (Retry budget)과 타입이 지정된 실패 신호 (Typed failure signal)로 감싸십시오:

type ClassifyResult =
  | { ok: true; data: ModelOutput }
  | { ok: false; reason: "validation" | "timeout" | "rate_limit"; raw?: string };
...

재시도가 외부 상태 (External state)에 영향을 줄 때는 멱등성 (Idempotency)이 중요합니다. 워크플로 (Workflow)가 모델 단계 내부에서 API를 호출한다면, 재시도로 인해 부수 효과 (Side effect)가 중복 발생하지 않도록 멱등성 키 (Idempotency key)로 감싸십시오. 이를 제어하는 것은 워크플로 계층입니다. 모델 자체는 이를 제어할 수 없습니다.

인간 게이트 (Human gate)가 위치해야 할 곳

하이브리드 메모리 및 검색 (Retrieval) 접근 방식 (요청 시작 시 자동 검색 및 명시적 저장 결합)은 에이전트 상태 (Agent state)를 예측 가능하게 유지합니다. 마지막 단계를 자동화하지 말아야 할 시점을 아는 것 또한 마찬가지입니다.

영향력이 크거나 되돌릴 수 없는 단계는 확정(committing)하기 전에 제어 게이트(control gate)를 통해 사람에게 전달되어야 합니다. 이는 LLM이 나쁘기 때문이 아닙니다. 어떤 결정들은 실제적인 결과(consequences)를 초래하며, 잘못된 결정의 비용이 자동화로 얻는 이득보다 크기 때문입니다.

async function runWorkflow(input: WorkflowInput): Promise<WorkflowResult> {
  const enriched = await enrich(input);
  const classifyResult = await classifySafe(enriched);
...

제어 게이트(control gate)는 워크플로(workflow) 내의 타입이 지정된 분기(typed branch)여야 하며, 프롬프트 지시사항(prompt instruction)이 아닙니다. "확신이 들 때만 이것을 수행하세요"는 가드레일(guardrail)이 아닙니다. 타입이 지정된 분기(typed branch)가 가드레일입니다.

이것이 전체 시스템에 어떻게 적용되는지 더 깊이 알고 싶다면, 이러한 패턴들을 대규모로 연결하는 방법을 포함하여 에이전트를 위한 프로덕션 아키텍처 (production architecture for agents)에 대해 작성해 두었습니다.

FAQ

LLM 출력을 어떻게 결정론적(deterministic)으로 만드나요?
모델 자체를 결정론적으로 만들 수는 없습니다. 모델 주변의 시스템을 결정론적으로 만들어야 합니다. 스키마 검증이 된 구조화된 출력 (Schema validated structured output), 타입이 지정된 워크플로 단계 (typed workflow steps), 그리고 실패 신호가 포함된 재시도 게이트 (retry gates)가 실질적인 레버(levers)입니다. 모델은 타입이 지정되고 테스트 가능한 파이프라인 (pipeline) 내에서 격리된 하나의 블랙박스(black box) 단계일 뿐입니다.

구조화된 출력 (structured output)이란 무엇인가요?
구조화된 출력은 모델이 자유 형식의 산문(freeform prose) 대신 사용자가 정의한 스키마(schema)에 따라 데이터를 반환하는 것을 의미합니다. 대부분의 제공업체는 JSON 모드 또는 함수 호출 (function calling)을 지원합니다. 스키마 라이브러리를 사용하여 즉시 결과를 파싱(parse)하고 검증합니다. 만약 스키마와 일치하지 않는다면, 이를 가벼운 경고가 아닌 실패한 호출로 취급해야 합니다.

LLM 응답을 어떻게 검증하나요?
응답을 JSON으로 파싱한 다음, 스키마 검증기(schema validator)를 통해 실행합니다. TypeScript 프로젝트에서는 Zod가 흔히 사용되는 선택지입니다. safeParse 호출을 통해 데이터가 포함된 성공 또는 조치 가능한 에러가 포함된 실패라는 타입이 지정된 결과(typed result)를 얻을 수 있습니다. 실패는 단순히 로그를 남기고 넘어갈 사례가 아니라, 처리해야 할 예외(exception)입니다.

결정론적 워크플로 (deterministic workflows)가 전체 프로덕션 시스템에 어떻게 부합하는지에 대해 더 깊이 알고 싶다면, 제 사이트에서 에이전트를 위한 전체 프로덕션 아키텍처 (production architecture for agents)를 다루고 있습니다.

만약 AI 제품을 위한 Next.js (Next.js for AI products)를 엔드 투 엔드 (end to end)로 구축하고 싶다면, 그것이 바로 제가 수행하는 작업의 종류입니다.

아래에 댓글을 남겨주세요. 사람들이 프로덕션 환경에서 LLM 기능을 테스트 가능하게 (testable) 유지하기 위해 어떤 패턴을 사용하는지 궁금합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0