콘텐츠로 이동

Parallelization: Sectioning과 Voting

Prompt Chaining은 각 단계가 이전 단계의 출력에 의존할 때 적합하다. 그러나 많은 실무 태스크에서 하위 작업들은 서로 독립적이다. “세 개 도시의 날씨를 동시에 조회하라”는 태스크에서 서울 날씨를 먼저 조회한 뒤 파리를 조회할 이유가 없다. 직렬로 처리하면 총 지연이 각 작업 지연의 합이 되지만, 병렬로 처리하면 가장 느린 작업 하나의 지연만 기다리면 된다.

Anthropic의 Building Effective AI Agents는 병렬화를 두 가지 하위 패턴으로 구분한다. Sectioning은 큰 작업을 독립된 조각으로 나눠 동시에 처리하고 결과를 합친다. Voting은 같은 작업을 여러 번 독립 실행해 다수결로 신뢰도를 높인다.

┌────────────────────────────────────────────────────────────────┐
│ Sectioning 흐름 │
│ │
│ 대형 작업 입력 │
│ │ │
│ ▼ │
│ 분할 (Splitter) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 섹션 A │ 섹션 B │ 섹션 C │ 섹션 D │ 섹션 E │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ [LLM] [LLM] [LLM] [LLM] [LLM] │
│ (병렬 실행 — 모두 동시 시작) │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ 결과 A 결과 B 결과 C 결과 D 결과 E │
│ │ │
│ ▼ │
│ 합병 (Aggregator): 결과 A~E를 하나로 종합 │
│ │ │
│ ▼ │
│ 최종 출력 │
└────────────────────────────────────────────────────────────────┘

Sectioning의 전형적 사용 사례는 긴 문서 처리다. 100페이지 보고서를 한 번에 요약하면 컨텍스트 한계에 부딪히고 중간 내용이 “중간에 묻히는” 현상이 생긴다. 대신 10페이지씩 10개 섹션으로 나눠 병렬로 요약한 뒤, 10개 요약을 합쳐 최종 요약을 만드면 더 균일한 품질을 얻을 수 있다.

중요한 전제: 섹션들이 실제로 독립적이어야 한다. 섹션 B의 요약이 섹션 A의 내용을 참조해야 한다면 병렬화가 불가능하다. 이 경우에는 Prompt Chaining이나 Orchestrator-Workers가 더 적합하다.

┌────────────────────────────────────────────────────────────────┐
│ Voting 흐름 │
│ │
│ 동일 태스크 입력 │
│ │ │
│ ├─────────────────┬─────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ [LLM #1] [LLM #2] [LLM #3] │
│ 답: "승인" 답: "거부" 답: "승인" │
│ │ │ │ │
│ └─────────────────┴─────────────────┘ │
│ ▼ │
│ 집계 (Aggregator): 승인 2 vs 거부 1 │
│ ▼ │
│ 최종 결정: "승인" │
└────────────────────────────────────────────────────────────────┘

Voting은 3장 섹션에서 다룬 Self-Consistency와 근본적으로 같은 아이디어를 워크플로 수준에서 구현한 것이다. Self-Consistency는 단일 에이전트가 같은 문제를 여러 번 샘플링해 다수결을 취한다. Workflow Voting은 여러 독립 LLM 호출(또는 다른 온도 설정, 다른 시스템 프롬프트)을 병렬로 실행해 결과를 집계한다.

Voting이 특히 효과적인 시나리오는 분류 및 이진 결정이다. 콘텐츠 안전성 판단, 승인/거부 결정, 품질 기준 충족 여부 같은 이진 출력에서 다수결은 단일 판단보다 일관적으로 신뢰할 수 있다.

개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다.

import asyncio
from collections import Counter
async def sectioning(model, task: str, sections: list[str]) -> str:
"""
독립 섹션을 병렬로 처리한 뒤 합병.
"""
async def process_section(section: str) -> str:
prompt = f"다음 섹션을 요약하라:\n\n{section}"
result = await model.generate_async(
[{"role": "user", "content": prompt}]
)
return result.text
# 모든 섹션을 동시에 처리
summaries = await asyncio.gather(*[
process_section(s) for s in sections
])
# 합병
aggregator_prompt = (
f"원래 태스크: {task}\n\n"
"다음 섹션별 요약들을 하나의 일관된 최종 요약으로 합쳐라:\n\n" +
"\n\n".join(f"섹션 {i+1}:\n{s}" for i, s in enumerate(summaries))
)
final = await model.generate_async(
[{"role": "user", "content": aggregator_prompt}]
)
return final.text
async def voting(
model,
question: str,
n_votes: int = 5,
temperature: float = 0.7,
) -> str:
"""
동일 질문을 N번 독립 실행해 다수결.
"""
async def single_vote() -> str:
result = await model.generate_async(
[{"role": "user", "content": question}],
temperature=temperature,
)
return result.text.strip()
votes = await asyncio.gather(*[single_vote() for _ in range(n_votes)])
counter = Counter(votes)
winner, count = counter.most_common(1)[0]
return winner

병렬화는 지연을 줄이지만 비용은 줄이지 않는다. N개 병렬 실행의 총 토큰 비용은 N배다. 따라서 병렬화는 다음 두 가지가 동시에 성립할 때 최적이다.

조건 이유
지연 감소가 비용보다 중요 실시간 처리, SLA 요건
하위 작업이 진정으로 독립적 섹션 간 또는 투표 간 상호 의존 없음

병렬화 결과를 합치는 Aggregator도 추가 LLM 호출이다. 합병 비용을 포함한 총 비용을 계산해야 한다.

다음 챕터에서는 Sectioning보다 더 동적인 분해를 다루는 Orchestrator-Workers 패턴을 살펴본다. 오케스트레이터가 실행 중에 서브태스크를 결정하고 워커에게 동적으로 할당하는 방식이다.

참고 자료