프롬프트 체이닝 (Prompt Chaining): 복잡한 작업을 단순한 단계로 나누는 방법
요약
복잡한 작업을 한 번에 처리하려 할 때 발생하는 LLM의 오류를 방지하기 위해, 작업을 작은 단계로 나누어 순차적으로 실행하는 프롬프트 체이닝 기법을 소개합니다. 각 단계의 출력이 다음 단계의 입력이 되는 구조를 통해 모델의 정확도와 신뢰성을 높일 수 있습니다.
핵심 포인트
- 복잡한 지시사항을 한 번에 전달하면 모델이 맥락을 놓치거나 오류를 범할 확률이 높음
- 프롬프트 체이닝은 작업을 단일 책임 원칙에 따라 여러 단계의 호출로 분리함
- 각 단계의 응답을 다음 단계의 입력값으로 사용하여 데이터의 순수성을 유지함
- 이력서 처리 예시와 같이 단계별로 정보를 추출, 계산, 분류, 생성하는 흐름 구축 가능
제가 코드에서 LLM을 사용하여 유용한 무언가를 처음 시도했을 때, 저는 모델에게 모든 것을 한 번에 수행하도록 요청했습니다: "이 리뷰를 분석하고, 주요 감정을 추출하고, 분류한 뒤, 결과를 JSON으로 반환해줘". 모델은 시도했지만, 과정 중 어딘가에서 맥락을 놓쳤고, 단계들을 뒤섞었으며, 최종 JSON은 쓸모가 없었습니다. 해결책은 더 나은 모델을 사용하는 것이 아니었습니다. 문제를 잘게 나누는 것이었습니다.
시작하기 전에: 이 포스트를 따라오려면 LLM이 무엇인지(ChatGPT나 Claude처럼 지시사항으로부터 텍스트를 생성하는 언어 모델)를 알고 있어야 하며, TypeScript나 JavaScript를 작성해 본 경험이 있어야 합니다. 그것만 있으면 됩니다.
문제점: 모든 것을 한 번에 요청하기
슈퍼마켓에 갔는데, 문을 열고 들어서자마자 누군가가 당신이 적을 시간도 주지 않고 10가지 서로 다른 물건 목록을 외치는 상황을 상상해 보세요. 아마 빵과 우유는 제대로 챙기겠지만, 결국 절반은 잊어버리거나 잘못된 제품을 사게 될 것입니다. AI 모델에게도 똑같은 일이 일어납니다.
모델에 지시사항을 보낼 때마다 이를 **프롬프트 (prompt)**라고 부릅니다. 즉, 당신이 보내는 텍스트이자 모델이 해결하기를 원하는 질문 또는 작업입니다. 프롬프트가 네 가지 서로 다른 작업을 섞어서 전달하면, 모델은 그 모든 것을 동시에 "기억"하고 있어야 합니다. 보통 처음에는 잘 시작하지만, 어느 시점에 오류를 범하게 됩니다. 그리고 그 오류는 끝까지 이어집니다.
해결책은 간단합니다: 한 번의 호출에 한 단계씩만 수행하는 것입니다.
체이닝 (Chaining)의 작동 방식
프롬프트 체이닝 (Prompt chaining)은 복잡한 작업을 **LLM에 대한 일련의 호출 체인 (chain of calls)**으로 나눕니다. 각 호출은 문제의 단 한 부분만을 해결하며, 해당 단계의 응답은 다음 단계의 입력값이 됩니다.
구체적인 예시를 들어보겠습니다. 당신의 회사로 들어오는 이력서들을 처리하고 싶다고 가정해 봅시다:
-
Call 1 (호출 1): "이 문서에서 경력 사항만 추출하세요."
-
Call 2 (호출 2): "해당 발췌본을 바탕으로 총 경력 연수를 계산하세요."
-
Call 3 (호출 3): "해당 연수를 바탕으로 프로필을 Junior, Mid, 또는 Senior로 분류하세요."
-
Call 4 (호출 4): "해당 분류를 바탕으로 지원자에게 보낼 후속 이메일 초안을 작성하세요."
각 호출 (Call)은 단일 책임 (single responsibility)을 가집니다. 4단계의 모델은 세 페이지 분량의 이력서 전체를 읽을 필요가 없습니다. 3단계에서 전달된 깔끔한 레이블('Senior')만 전달받아 이를 바탕으로 작업하면 됩니다.
이 패턴은 클래식 소프트웨어 분야에서 수십 년 동안 존재해 왔습니다. 이를 **파이프 앤 필터 (Pipe & Filter)**라고 부릅니다. 데이터가 일련의 독립적인 변환 과정을 거치며, 각 필터는 한 가지 일을 잘 수행합니다. LLM (Large Language Models)은 단순히 또 다른 유형의 필터일 뿐입니다.
언제 사용하는 것이 합리적인가?
작업에 **명확한 순차적 하위 단계 (sequential sub-steps)**가 있을 때 사용하는 것이 합리적입니다. SEO 기사를 작성하는 것이 교과서적인 예시입니다. 먼저 개요를 생성하고, 그다음 각 섹션을 개발하며, 마지막으로 톤 (tone)을 검토합니다. 각 단계는 이전 단계에 의존하지만 서로 다른 일을 수행합니다.
또한 중간 결과 (intermediate results)를 검증해야 할 때도 합리적입니다. 만약 1단계에서 텍스트로부터 엔티티 (entities)를 추출했는데 결과가 빈 값으로 돌아온다면, 2단계와 3단계에 비용을 들일 이유가 없습니다. 거기서 멈출 수 있기 때문입니다.
사용하지 말아야 할 때를 아는 것도 똑같이 중요합니다. 작업이 단순하다면 ("이 내용을 세 가지 포인트로 요약하세요", "이 문장을 번역하세요"), 잘 작성된 단일 프롬프트 하나로 해결됩니다. 단계를 추가하는 것은 지연 시간 (latency)과 비용만 증가시킬 뿐입니다. 파이프라인 (pipeline)을 설계하기 전에 스스로에게 물어보세요: "잘 구조화된 단일 호출로 이 문제를 해결할 수 있는가?" 만약 대답이 "예"라면, 그런 방식으로 진행하세요. 개발자를 위한 프롬프트 엔지니어링 (prompt engineering for developers)에 관한 포스트에서 이러한 효과적인 프롬프트를 구축하기 위한 기본 패턴들을 다룹니다.
게이트 (The gate): 오류의 증폭을 방지하는 방법
단계 사이에는 검증(validate)을 수행해야 합니다. 이것이 바로 **게이트 (gate)**입니다. 즉, 다음 단계로 넘어가기 전에 이전 단계의 결과가 유효한지 확인하는 코드 블록을 의미합니다.
게이트가 없다면 다음과 같은 일이 발생합니다. 1단계에서 잘못된 형식의 텍스트를 생성합니다(모델이 예상된 응답을 지어내거나 왜곡하는 것을 **환각 (hallucination)**이라고 합니다). 2단계는 이를 입력값으로 받아 더 나쁜 결과를 만들어냅니다. 3단계는 2단계가 만든 것을 입력받습니다. 최종 출력에 도달할 때쯤이면 원래의 오류는 증폭되어 있으며, 어디서부터 시작되었는지 알 수 없는 상태가 됩니다.
가장 단순한 형태의 게이트는 true 또는 false를 반환하는 함수입니다. 만약 false를 반환한다면, **조기 종료 (early exit)**를 적용합니다. 즉, 파이프라인의 끝에 도달하기 전에 종료하고 명확한 에러 메시지를 반환하며, 추가적인 API 호출이나 비용을 소모하지 않습니다.
AI 에이전트의 가드레일 (Guardrails in AI agents)은 더 넓은 개념(시스템이 할 수 있는 일에 대한 일반적인 경계)이지만, 순차적 파이프라인(sequential pipeline)에서 게이트는 그 개념의 가장 구체적이고 실용적인 형태입니다.
TypeScript를 이용한 단계별 파이프라인 구현
다음은 작동 가능한 예시입니다. 프레임워크 없이 Anthropic SDK만을 사용하여 게이트가 포함된 두 단계를 구현했습니다.
import Anthropic from "@anthropic-ai/sdk";
// 클라이언트 생성 (ANTHROPIC_API_KEY 환경 변수 사용)
...
2단계를 주목해 보세요. resena가 아닌 resumen을 전달하고 있습니다. 분류 모델(classification model)이 원문 전체를 읽을 필요는 없습니다. 전달하는 데이터 양을 줄이는 것이 더 저렴하고 빠릅니다.
이 지점에서 컨텍스트 윈도우 (context window)(단일 호출 시 모델에 보낼 수 있는 텍스트의 양이며, 최대 제한이 있음)가 중요해집니다. 긴 파이프라인(pipeline)에서 각 단계에서 전달하는 내용을 필터링하지 않으면, 결과가 개선되지 않으면서 비용만 폭발적으로 증가합니다. 컨텍스트 윈도우와 모범 사례 (context window and best practices)에 관한 게시물에서 이를 관리하는 방법에 대해 자세히 다룹니다.
누구나 저지르는 실수들
적은 작업에 너무 많은 단계
단 두 단계로 해결될 수 있는 일에 6단계 파이프라인을 사용하는 경우입니다. 각 추가 단계는 지연 시간(latency)을 늘리고 새로운 장애 지점(point of failure)을 추가합니다. 항상 최소한의 단계로 시작하고, 결과가 그럴 만한 가치가 있을 때만 단계를 추가하십시오.
모든 컨텍스트를 각 호출에 전달하기
가장 비용이 많이 드는 실수입니다. **토큰 (Tokens)**은 모델이 텍스트를 측정하는 단위입니다(단어 조각과 유사함). LLM API는 토큰 단위로 비용을 청구합니다. 즉, 보내는 데이터와 받는 데이터 모두에 대해 비용이 발생합니다. 만약 3단계에서 1단계와 2단계의 전체 이력을 포함한다면, 그 비용은 단계 수만큼 배수로 증가합니다. 해당 단계에 필요한 정제된 데이터만 전달하십시오. 반대의 트레이드오프(tradeoff)도 존재합니다. 너무 많이 잘라내면 다음 단계의 결과가 약해질 수 있습니다. 전체 이력을 다 보내는 것이 아니라, 해당 단계가 충분한 신호(signal)를 가질 수 있도록 필요한 정보만 전달해야 합니다.
에러 핸들링 (error handling) 없는 호출
특히 초기에 가장 자주 반복되는 것을 본 실수입니다. 1단계가 조용히 실패하고 후속 단계들이 잘못된 데이터를 받는, 게이트(gate)가 없는 파이프라인입니다. 시스템은 연쇄적으로 실패하며, 어느 시점에서 오류가 발생했는지 알 수 없고, 사용자는 맥락 없는 일반적인 메시지만 받게 됩니다. 게이트는 최적화 도구가 아니라, 시스템을 사용 가능하게 만드는 필수 요소입니다.
검증되지 않은 사용자 입력
만약 파이프라인이 사용자로부터 오는 텍스트(리뷰, 양식, 모든 자유 입력 필드)를 처리한다면, 해당 텍스트는 프롬프트 (Prompt)에 직접 보간 (Interpolate)됩니다. 사용자는 콘텐츠 내에 지시 사항을 주입하여 모델의 동작을 변경할 수 있습니다. 프로덕션 (Production) 환경에서는 프롬프트에 사용하기 전에 입력값을 검증하고 제한해야 합니다: 최대 길이, 허용된 문자 등 귀하의 사례에 적합한 방식이면 무엇이든 좋습니다.
원자적 작업 (Atomic tasks)을 위한 프롬프트 체이닝 사용
"이 단어를 영어로 번역해줘"와 같은 작업은 파이프라인이 필요하지 않습니다. 근본적으로 단순한 작업에 단계들을 추가하고 있는 자신을 발견한다면, 한 걸음 물러나 실제 문제가 아키텍처 (Architecture)가 아닌 프롬프트에 있는 것은 아닌지 스스로에게 물어보십시오. 복잡성에는 비용이 따릅니다: 더 많은 코드, 더 많은 장애 지점 (Points of failure), 더 높은 지연 시간 (Latency).
구현 체크리스트 (Implementation checklist)
- 파이프라인의 각 단계는 단일하고 명확한 책임을 갖는다
- 각 단계 쌍 사이에 검증 게이트 (Validation gate)가 존재한다
- 게이트는 디버깅을 돕는 에러 메시지와 함께 조기 종료 (Early exit)를 구현한다
- 각 단계는 전체 이력이 아닌, 필요한 컨텍스트 (Context)만 전달받는다
- 단계의 수는 작업에 필요한 최소한의 수이다
- 디버깅을 용이하게 하기 위해 각 단계의 결과가 로그 (Log)에 기록된다
자주 묻는 질문 (Frequently Asked Questions)
프롬프트 체이닝 (Prompt chaining)과 AI 에이전트 (AI agent)의 차이점은 무엇인가요?
에이전트는 어떤 단계를 어떤 순서로 수행할지, 그리고 언제 멈출지를 스스로 결정합니다. 프롬프트 체이닝은 사전에 정의된 고정된 시퀀스 (Sequence)입니다. 필요한 단계가 정확히 무엇인지 알고 있다면 프롬프트 체이닝을 사용하십시오. 훨씬 더 예측 가능하며, 무언가 실패했을 때 디버깅하기가 더 쉽습니다.
파이프라인을 구축하기 위해 LangChain과 같은 프레임워크 (Framework)가 필요한가요?
아니요. 이 포스트의 예제는 어떤 프레임워크도 사용하지 않습니다. 2단계 또는 4단계 정도의 파이프라인을 구축할 때는 프레임워크를 추가하는 것이 불필요한 의존성 (Dependencies)과 추상화 계층 (Layers of abstraction)을 더하게 됩니다. TypeScript의 함수 몇 개만으로도 훨씬 적은 복잡성으로 정확히 동일한 작업을 수행할 수 있습니다.
만약 1단계의 모델이 잘못된 형식을 생성하면 어떻게 되나요?
그것이 바로 게이트 (gate)가 필요한 이유입니다. 계속 진행하기 전에 형식을 확인하고, 만약 출력이 검증 (validation)을 통과하지 못하면 파이프라인 (pipeline)은 중단되고 명확한 에러를 반환합니다. 게이트가 없다면, 해당 잘못된 형식이 전파되어 나중에 실패의 근원을 추적하는 것이 거의 불가능해집니다.
만약 1단계에서 JSON을 반환한다면, 가장 직접적인 게이트는 try { JSON.parse(output) } catch { return { error: "Invalid format" } }입니다. 만약 열거형 값 ("positive" / "negative" / "neutral")을 기대한다면, 정규 표현식 (regex)만으로도 충분합니다: /^(positive|negative|neutral)$/i.test(output). 2단계로 넘어가기 전, 여기서 실패를 잡아내는 것이 명확한 에러를 받는 것과 40분 동안 디버깅 (debugging)을 하는 것의 차이를 만듭니다.
2단계의 모델은 1단계가 무엇을 했는지 기억하나요?
아니요, 해당 정보를 명시적으로 전달하지 않는 한 그렇지 않습니다. LLM (Large Language Model)에 대한 각 호출 (call)은 제로 상태에서 시작됩니다. 호출 사이에는 메모리 (memory)가 없습니다. 만약 2단계가 1단계의 무언가를 알아야 한다면, 프롬프트 (prompt)에 직접 전달해야 합니다. 이는 사람들이 처음에 가장 혼란스러워하는 부분 중 하나이며, 단계 사이에 어떤 정보가 전달될지 설계하는 것이 매우 중요한 이유이기도 합니다.
단계가 너무 많으면 어떻게 되나요?
실무적으로, 단일 작업을 위해 4~5단계를 넘어서는 것은 대개 문제를 과도하게 나누고 있다는 신호입니다. 중간 결과물에 대한 통제력을 잃지 않으면서 일부 단계를 병합할 수 있는지 스스로에게 물어보세요. 만약 그렇다면, 병합하십시오.
이 기사는 원래 AI Coding Patterns — AI와 함께 프로그래밍을 배우는 시각적이고 인터랙티브한 코스에서 게시되었습니다. 코스를 살펴보세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기