L21 — Prompt Chaining: 한 방에 다 시키지 말고 단계로 쪼개기 (수정중)
이 강의는 Antonio Gulli,「Agentic Design Patterns」 1장 Prompt Chaining 을 우리 standarda 스택에 비춰 읽는다. 다음 강의 L22 Planning과 한 쌍이다 — chaining 은 고정된 분해, planning 은 동적 분해.
1. 책의 개념과 예시 (Agentic Design Patterns 1장)
정의: Prompt Chaining 은 복잡한 작업을 작고 개별적인 LLM 호출의 선형 연쇄(linear sequence)로 분해하고, 한 단계의 출력이 다음 단계의 입력(컨텍스트) 이 되게 하는 패턴이다. 책의 정의 그대로:
"A complex task is decomposed into a linear sequence of smaller, discrete LLM calls. The output of one step becomes the input (context) for the next step."
소프트웨어로 치면 고전적인 Pipe & Filter(유닉스 파이프 A | B | C)와 같은 모양이다 — 일반형은 ① 추출(Extraction) → ② 분석(Analysis) → ③ 합성(Synthesis)(원문 정제·구조화 → 핵심 추론 → 사람이 읽을 최종 답).
책의 대표 예시 — 시장 조사 보고서 (텍스트 사례)
책이 이 패턴을 소개하며 가장 먼저 드는 예시다. "시장 조사 보고서를 분석해서 핵심을 요약하고, 데이터 근거와 함께 트렌드를 뽑고, 마케팅팀에 보낼 이메일 초안까지 써줘" 를 한 프롬프트로 시키면 — 요약은 잘해도 데이터 추출이나 이메일 작성에서 무너지기 쉽다(책: the model might summarize well but fail to extract data or draft an email properly). 그래서 책은 이걸 3단계 체인으로 가른다:
- 요약: "Summarize the key findings of the following market research report"
- 트렌드 분석: "Using the summary, identify the top three emerging trends and extract the specific data points that support each trend" — 1단계 요약을 입력으로 받아 작업 범위가 확 좁아졌다.
- 이메일 작성: "Draft a concise email to the marketing team that outlines the following trends and their supporting data"
각 단계는 앞 단계의 (검증된) 출력만 받으므로 프롬프트가 짧고 또렷하다 — 이게 '한 방'보다 안정적인 이유다.
책의 코드 예시 — "사양 텍스트 → 구조화 JSON" (2단계 체인)
1장의 hands-on 예시는 노트북 사양 한 줄을 두 단계로 처리한다 (LangChain LCEL 의 | 파이프로 엮음):
# 책 1장 (Chapter_01_Prompt_Chaining) — 발췌
llm = ChatOpenAI(temperature=0)
# 단계 1: 원문에서 기술 사양만 추출
prompt_extract = ChatPromptTemplate.from_template(
"Extract the technical specifications from the following text:\n\n{text_input}"
)
# 단계 2: 추출된 사양을 cpu/memory/storage 키의 JSON 으로 변환
prompt_transform = ChatPromptTemplate.from_template(
"Transform the following specifications into a JSON object with "
"'cpu', 'memory', and 'storage' as keys:\n\n{specifications}"
)
extraction_chain = prompt_extract | llm | StrOutputParser() # 단계 1을 체인으로
full_chain = ( # 단계 1의 출력을
{"specifications": extraction_chain} # 단계 2의 {specifications} 로 흘려보냄
| prompt_transform | llm | StrOutputParser()
)
full_chain.invoke({"text_input":
"The new laptop model features a 3.5 GHz octa-core processor, "
"16GB of RAM, and a 1TB NVMe SSD."})
# → {"cpu": "3.5 GHz octa-core", "memory": "16GB", "storage": "1TB NVMe SSD"}
위 코드 예시는 §1 텍스트 사례(시장 조사 3단계)의 아이디어를 가장 작은 2단계(추출→변환) 로 보여주는 hands-on 버전이다. 읽을 점 둘:
extraction_chain의 출력이{specifications}로 그대로 들어간다 — 이게 "출력→입력" 연쇄의 핵심이다. 한 LLM 에게 "사양 뽑아서 JSON 으로 줘"를 한 방에 안 시키고, 뽑기와 형식 변환을 갈랐다.|파이프가 Pipe & Filter 를 코드로 옮긴 것이다.prompt → llm → parser가 한 필터, 그 출력이 다시 다음 필터로 들어간다.
책이 드는 활용처 7가지
책은 이 패턴이 잘 맞는 곳으로 일곱 가지를 든다:
- 정보 처리 워크플로우(Information Processing) · 2. 복잡한 질의 응답(Complex Query Answering) · 3. 데이터 추출·변환(Data Extraction & Transformation) · 4. 콘텐츠 생성 워크플로우(Content Generation) · 5. 상태를 가진 대화 에이전트(Conversational Agents with State) · 6. 코드 생성·정제(Code Generation & Refinement) · 7. 멀티모달·다단계 추론(Multimodal & multi-step reasoning)
뒤(§3~4)에서 보겠지만 — 우리 standard_utils 의 파싱은 ③ 데이터 추출·변환, /develop 파이프라인은 ① 정보 처리 워크플로우의 실제 사례다.
그럼 왜 굳이 이렇게 가르나? — 다음 절.
2. 왜 쪼개나 — 한 방 프롬프트의 문제와 쪼개기의 이점
심부름을 한 번에 열 가지 시키면 사람도 꼭 한두 개를 빠뜨린다. LLM 도 똑같다.
위 예시를 한 방으로 시켰다고 해보자 — "이 텍스트에서 사양 뽑아서 cpu/memory/storage JSON 으로 줘." 짧으면 통한다. 문제는 실무에서 지시·데이터·포맷 규칙·예외 상황(엣지케이스)을 하나의 거대 프롬프트(mega-prompt, 모든 걸 다 넣은 커다란 지시문)에 다 욱여넣을 때다:
"이 엑셀 거래내역을 읽어서, 카드사별로 분류하고, 이상 거래를 찾아내고,
한국어 요약 리포트를 마크다운 표로 만들어줘. 단 금액은 천단위 콤마,
날짜는 YYYY-MM-DD, 빈 셀은 '-' 로, 그리고 ..."
거의 항상 한두 개를 흘린다. 책(Gulli 1장) 그대로:
"For multifaceted tasks, using a single, complex prompt for an LLM can be inefficient, causing the model to struggle with constraints and instructions."
책은 이 현상을 instruction neglect(parts of the prompt are overlooked — 프롬프트의 일부가 통째로 무시됨)라고 부른다.
그리고 더 나쁜 건 — 어디서 깨졌는지 알 수 없다. 표가 틀렸을 때 그게 읽기에서 틀린 건지, 분류에서 틀린 건지, 포맷팅에서 틀린 건지 구분이 안 된다. 모델에게 한 번에 여러 가지를 동시에 시켰기 때문이다.
책의 핵심 명제: "Prompt chaining addresses these challenges by breaking the complex task into a focused, sequential workflow, which significantly improves reliability and control." 각 단계가 더 단순하고 덜 모호해(simpler and less ambiguous) 모델의 인지 부하가 줄고, "a more accurate and reliable final output" 로 이어진다.
쪼개면 좋은 이유 4가지 — 강의에서 이 넷만 기억하면 된다:
| 이점 | 뜻 | 거대 프롬프트는? |
|---|---|---|
| 인지 부하 ↓ | 단계가 단순·덜 모호해져 책 표현으로 "reduces the cognitive load on the model" | 전체 문제를 한 번에 다 들고 있어야 함 |
| 오류 가시성 | 3단계에서 실패하면 3단계가 문제라고 바로 안다 | 어디서 깨졌는지 모름 |
| 신뢰성 | 작은 단계 각각은 검증·재시도가 쉬움 | 한 방이라 통째로 다시 |
| 감사 가능성 | 중간 출력을 로깅·검토·승인 후 다음으로 | 중간이 없음 |
핵심은 하나다 — 단계 사이에 "끼어들 자리"를 만든다. 검증하고, 고치고, 캐시하고, 사람이 볼 수 있는 자리.
3. 우리 코드 ① — standard_utils 의 "추출 → LLM → 파싱" 체인
가장 작은 단위의 prompt chain 은 이미 standarda-core 안에 있다. PDF/엑셀/시트를 LLM 으로 읽는 함수들은 한 번의 LLM 호출이 아니라 3단계 파이프다.
standarda-core/standarda_core/standard_utils/pdf_text.py 의 send_pdf_to_text_llm():
① extract_pdf_text(pdf) # 18-60행: PDF → 순수 텍스트 (LLM 아님, 결정론적 추출)
→
② llm.invoke([HumanMessage(...)]) # 97-100행: 추출 텍스트 + 프롬프트 → LLM 응답
→
③ parse_json_from_llm(response) # 호출자 쪽: 응답에서 JSON 만 안전 추출
엑셀·시트도 같은 모양이다:
| 파일 | 함수 | 단계 |
|---|---|---|
standard_utils/excel_text.py |
send_excel_to_text_llm() (61-93행) |
openpyxl 추출 → llm.invoke → 파싱 |
standard_utils/sheets_text.py |
send_sheet_to_llm() (26-71행) |
_format_sheet_data(탭 구분) → llm.invoke → 파싱 |
standard_utils/excel_vision.py |
send_excel_to_vision() (41-82행) |
LibreOffice 로 PDF 변환 → send_pdf_to_vision(이미지+LLM) → 파싱 |
왜 한 호출로 "엑셀 읽고 JSON 줘" 라고 안 할까? 그게 바로 거대 프롬프트다. 쪼갰기 때문에:
- ① 추출은 LLM 이 아니라 정해진 규칙대로만 도는(결정론적) 코드다 (openpyxl·텍스트 추출). LLM 에게 "셀 값을 읽어줘" 라고 시켰다가 없는 값을 지어내는 일(환각) 을 아예 막는다.
- ② LLM 은 해석만 한다 — 이미 깔끔히 뽑아둔 텍스트를 받아 분류·요약에만 집중한다.
- ③ JSON 파싱을 따로 뗀 단계로 두어,
```json같은 군더더기를 붙이는 LLM 의 변덕을 한 곳(parse_json_from_llm)에서 받아낸다. (CLAUDE.md 규칙: 이 split/json.loads패턴을 여기저기 베껴 쓰지 말 것 → 정확히 "파싱은 별도 필터" 라는 chaining 사고다.)
excel_vision 의
엑셀 → PDF → 이미지 → LLM → JSON은 5단계 파이프다. 한 방 프롬프트로는 불가능한 일을, 각 단계가 앞 단계의 출력만 받아 처리하기 때문에 가능하다.
4. 우리 워크플로우 ② — /develop 는 워크플로우 레벨의 prompt chain
같은 패턴이 코드 한 줄이 아니라 개발 절차 전체로 확대된 게 /develop 다. (파이프라인 자체는 L03에서 다뤘으니 여기선 왜 이게 chaining 인지만.)
.claude/skills/develop/SKILL.md 의 단계들 — 각 단계는 별개의 Claude 호출이고, 앞 단계의 산출물이 뒤 단계의 입력이다:
3 Context(+lesson-finder) → 5 Plan → 5.5 Spec 작성 → 6 Code
→ 7 Verify(spec-verifier) → 8 Docs → 9 Merge(merge-master) → 9.5 Migrate → 10 Backlog
- 5.5 단계가 만든
docs/specs/{app}.md가 → 7 단계 검증의 입력이 된다. 한 단계의 출력이 다음의 입력 — 전형적 chain. - 서브에이전트(Claude Code 안에서 한 가지 일만 맡아 자기 창에서 도는 에이전트(Agent) — L12)도 같은 모양의 작은 chain 이다.
lesson-finder(.claude/agents/lesson-finder.md): INDEX 로드 → 도메인 필터 → trigger 매칭 → 본문 읽기 → 요약 의 5단계 필터 파이프.spec-verifier(.claude/agents/spec-verifier.md): 스펙 읽기 → 변경 파일 로드 → 절별 1:1 대조 → 불일치 리포트.
왜 이렇게 쪼갰나? 3절의 4가지 이점이 그대로 적용된다 — 오류 가시성(7단계에서 막히면 "코드가 스펙과 다르다"고 정확히 안다), 감사 가능성(5·5.5 단계는 사람 승인 게이트), 신뢰성(한 단계가 틀려도 그 단계만 되돌림). 거대 프롬프트("이 백로그 알아서 구현하고 머지해")로는 어느 것도 못 얻는다.
5. 반복 호출의 비용 — 체인의 prefix 를 캐시하라
체인을 돌리면(특히 tool-loop/ReAct 처럼 같은 system+tools 로 여러 번 invoke) 같은 앞부분(prefix)을 매번 다시 청구당한다. 책도 chaining 의 실무 비용으로 이걸 짚는다. 우리는 standarda-core/standarda_core/llm_cache.py 로 해결한다 (전모는 L14):
# llm_cache.py (32-71행)
cached_system_message(text, model) # claude* 일 때만 cache_control 마커 → 멀티 provider 안전
with_cache_control(message, model) # 임의 메시지 끝 블록에 캐시 마커(복사본, 원본 불변)
# track_usage() (118-126행): 체인 전체의 cache_read/write 토큰 누적 계측
핵심 숫자: CACHE_READ_MULT = 0.10 (175-176행) — 캐시 적중 시 입력가의 0.1배. 즉 체인을 잘게 쪼개도, 고정 prefix 를 캐시하면 반복 비용이 1/10 로 줄어든다. "쪼개면 LLM 호출이 늘어 비싸지 않나?" 의 답이 여기 있다 — 앞부분이 호출 때마다 글자 하나까지 똑같으면(바이트 동일) 그 부분은 거의 공짜다.
6. 그래서 언제 쪼개나 (그리고 어디까지)
- 포맷 규칙·제약이 3개 이상 얽히면 → 쪼개라. 한 방이면 하나는 흘린다.
- 중간 산출물을 검증/로깅/사람승인 하고 싶으면 → 그 경계가 곧 단계 분리선.
- LLM 이 굳이 안 해도 되는 일(셀 읽기, JSON 추출) → 별도 결정론적 단계로 빼라 (
extract_*/parse_json_from_llm). - 단, 무한히 잘게 쪼개진 마라. 단계가 늘면 단계 간 오차가 누적(compounding drift)되고 지연도 는다. 고정된 작업에선 적당히 큰 신뢰 단계 몇 개가 최선이다.
그리고 결정적인 경계 하나 — chaining 은 단계를 내가 미리 정한다. 추출→LLM→파싱, spec→code→verify 처럼 순서가 고정돼 있다. 그런데 어떤 단계를 밟을지 미리 알 수 없는 작업이라면? 거기서부터는 chaining 이 아니라 Planning 의 영역이다 → 다음 강의.
이 경계를 한 줄로: 단계를 미리 알면 chain, 모르면 plan. 다음 강의에서 보겠지만 책(6장)도 planning 을 "a specific tool, not a universal solution" 이라 못박는다 — 해법을 이미 잘 아는(=단계를 아는) 일엔 고정 체인이 낫다.
오늘 정리 + 다음
- 정리: Prompt Chaining 은 복잡한 작업을 작은 LLM 호출의 선형 연쇄(출력→입력, Pipe & Filter)로 쪼개는 패턴이다. 거대 프롬프트 한 방은 제약을 흘리고 어디서 깨졌는지도 모른다. 쪼개면 인지부하↓·오류 가시성·신뢰성·감사가능성을 얻는다. 우리 스택엔 이미 ①
standard_utils의 추출→LLM→파싱, ②/develop의 spec→code→verify→merge, ③ 서브에이전트 내부 필터 파이프로 박혀 있고, 반복 비용은llm_cache.py의 prefix 캐시(읽기 0.1x)로 누른다. - 흔한 함정: ① 거대 프롬프트로 시작 — "엑셀 읽고 분류하고 요약해 줘" 한 방. 하나는 꼭 흘린다. ② 반대로 과분해 — LLM 호출을 의미 없이 잘게 쪼개 오차 누적·지연·비용만 늘리는 것. 단계는 검증/승인 경계를 기준으로 가른다.
- 다음 시간: L22 Planning — 단계를 미리 못 정하는 작업에서, 에이전트가 스스로 목표를 하위작업으로 쪼개게 하는 패턴. plan 모드·
docs/plans/·/develop의 계획 게이트로 그 자율성을 어떻게 길들이는지. - 자습 권장: 최근 자기가 Claude 에게 던진 "한 방 프롬프트" 하나를 골라, 3절 표의 4가지 이점 기준으로 어디서 단계를 그었어야 했나를 적어 보라. 특히 "이게 LLM 이 할 일인가, 결정론적 코드가 할 일인가"를 가르는 연습.