일을 나눠 동시에 맡기고 다시 모으려면 어떻게 짤까요?

계획서 한 장을 보안, 비용, 일정 세 사람에게 검토받는다고 해 봅니다. 한 사람씩 차례로 받으면 세 번을 기다리고 세 사람에게 동시에 맡기면 한 번만 기다립니다. 세 검토는 서로의 결과를 기다릴 필요가 없기 때문입니다.

이렇게 서로 상관없는 일을 동시에 맡기고 결과를 모으는 방식을 병렬 처리(parallelization)라고 합니다. 두 방식의 시간이 어떻게 다른지 먼저 봅니다.

맡기는 방법

설명을 위한 예시 값입니다. 모델이 한 번 답하는 시간을 한 칸으로 놓았고 실제 시간은 요청마다 다릅니다.

병렬 분기는 시작점에서 길을 여러 개 냅니다

LangGraph에서는 같은 노드에서 엣지를 여러 개 내보내면 그 노드들이 같은 단계에서 동시에 실행됩니다. 다음 노드는 그 노드들이 모두 끝난 뒤에 실행됩니다. 앞에서 본 대로 노드는 State의 칸 하나를 바꿔 돌려주는 함수라서 세 노드가 서로 다른 칸에 쓰면 부딪히지 않습니다.

Anthropic은 병렬 처리를 두 가지로 나눕니다. 서로 독립된 하위 작업을 동시에 돌리는 방식(sectioning)과 같은 일을 여러 번 시켜 다양한 답을 모으는 방식(voting)입니다. 속도를 높이거나 여러 관점으로 확신을 얻고 싶을 때 씁니다.

parallel.pypython
for name, key in [("보안", "security"), ("비용", "cost"), ("일정", "schedule")]:
    b.add_node(key, view(name, key))   # 1 관점마다 노드 하나
    b.add_edge(START, key)             # 2 시작에서 세 노드로 동시에
    b.add_edge(key, "merge")           # 3 셋이 모두 끝나면 merge 실행
b.add_node("merge", merge)             # 4 세 칸을 하나로 합치는 노드
  1. 관점마다 노드입니다. view는 관점 이름을 받아 노드 함수를 만들어 주는 작은 함수입니다. 각 노드는 자기 전용 칸(security, cost, schedule) 하나에만 씁니다.
  2. 시작에서 갈라집니다. START에서 엣지가 셋 나가니 세 노드가 한 단계에서 함께 돌아갑니다. 모델 호출도 동시에 나갑니다.
  3. 모두 끝나야 합칩니다. merge는 세 노드가 끝난 다음 단계에서 실행되어 세 칸을 읽어 보고서 하나로 만듭니다.
  4. 같은 칸에 쓰려면 규칙이 필요합니다. 세 노드가 한 칸에 함께 쓰려면 앞서 본 리듀서(reducer)로 합치는 방법을 정해야 합니다. 전용 칸으로 나누면 그 걱정이 없습니다.

그래프를 draw_mermaid()로 그려 보면 __start__에서 세 노드로, 세 노드에서 merge로 화살표가 이어집니다. 코드는 langgraph 1.2를 모의 서버에 붙여 실행해 확인했습니다.

몇 갈래일지 입력이 정하면 Send가 워커를 만듭니다

문서 요약을 맡긴다고 해 봅니다. 문서가 2개 올지 5개 올지는 코딩할 때 알 수 없어서 노드를 미리 2개나 5개로 그려 둘 수 없습니다. 이럴 때 Send를 씁니다. 길 고르기 함수가 Send를 문서 수만큼 돌려주면 그 수만큼 워커(worker)가 실행됩니다.

Anthropic은 이 패턴을 Orchestrator-Worker라고 부릅니다. 중앙의 오케스트레이터(orchestrator)가 일을 쪼개 워커에게 맡기고 결과를 모으는 방식이고 하위 작업이 입력에 따라 달라서 미리 정해 둘 수 없는 복잡한 일에 맞습니다. 문서 수를 골라 놓고 스크롤하세요.

문서 수

문서 목록이 도착

State의 docs 칸에 문서가 들어옵니다. 몇 개인지는 이번 입력이 정했고 그래프는 아직 모릅니다.

길 고르기 함수가 Send를 만듦

fan_out이 문서 하나마다 Send를 하나씩 담아 목록으로 돌려줍니다. 돌려준 Send의 개수가 곧 워커의 수입니다.

워커가 문서 하나씩 맡음

같은 summarize 노드가 문서 수만큼 동시에 돌아갑니다. 워커는 자기 문서만 받아 서로 영향을 주지 않습니다.

결과가 한 칸에 쌓이고 합쳐짐

워커가 낸 요약은 summaries 칸에 이어 붙고 모두 끝나면 merge가 하나로 모읍니다.

send.pypython
def fan_out(s: State):                          # 1 문서마다 Send 하나
    return [Send("summarize", {"doc": d}) for d in s["docs"]]

class State(TypedDict):
    docs: list[str]
    summaries: Annotated[list[str], operator.add]   # 2 워커 결과가 쌓이는 칸

c.add_conditional_edges(START, fan_out, ["summarize"])  # 3 시작에서 길을 나눈다
c.add_edge("summarize", "merge")
터미널
$ python send.py 문서 2개 → 요약 2개 문서 5개 → 요약 5개

출력은 예시입니다. 모의 서버로 확인한 결과이고 요약 내용은 실제 모델마다 다릅니다. 코드는 langgraph 1.2 기준입니다.

  1. Send 하나가 워커 하나입니다. Send("summarize", {"doc": d})는 "summarize 노드를 이 입력으로 한 번 실행하라"는 뜻입니다. 두 번째 값은 그래프 State가 아니라 워커 하나가 받을 입력입니다.
  2. 쌓이는 칸입니다. 워커 여럿이 같은 칸에 쓰므로 operator.add로 목록을 이어 붙이는 규칙을 붙입니다. 규칙이 없으면 마지막 워커의 결과만 남습니다.
  3. 시작에서 길을 나눕니다. 조건부 엣지의 세 번째 값은 fan_out이 보낼 수 있는 노드 목록입니다. 이 예시는 문서 목록이 입력으로 오지만 오케스트레이터가 모델이라면 모델이 계획한 하위 작업 목록으로 Send를 만들 수 있습니다.

평가자가 불합격을 돌려보내면 생성자가 고칩니다

Evaluator-Optimizer는 한 모델이 초안을 쓰고 다른 모델이 평가해 피드백을 주는 일을 반복합니다. Anthropic은 평가 기준이 분명하고 반복해서 고치면 눈에 띄게 나아지는 일에 맞다고 말합니다. 사람이 피드백을 주면 글이 나아지는 일이라면 모델 평가자도 비슷하게 도울 수 있습니다.

이 반복에는 끝이 있어야 합니다. 평가자가 계속 불합격을 주면 호출이 끝없이 늘기 때문에 시도 횟수의 상한을 코드로 둡니다. 세 경우를 눌러 라운드가 어떻게 쌓이는지 보세요.

evaluator.pypython
class Feedback(BaseModel):                  # 1 평가 결과의 모양
    grade: Literal["pass", "fail"]
    feedback: str
evaluator = model.with_structured_output(Feedback, method="json_schema")

def route(s: State) -> Literal["generate", "__end__"]:    # 2 끝낼지 되돌릴지
    return "__end__" if s["grade"] == "pass" or s["tries"] >= 3 else "generate"

d.add_edge("generate", "evaluate")
d.add_conditional_edges("evaluate", route)  # 3 불합격이면 generate로 돌아감
터미널
$ python evaluator.py 시도 1: fail · 근거를 한 줄 더 넣으세요 시도 2: pass 최종 시도 횟수 2

출력은 예시입니다. 평가 결과는 모의 서버로 확인했고 실제 모델의 평가는 달라질 수 있습니다.

  1. 평가를 정해진 모양으로 받습니다. 앞서 본 구조화 출력입니다. grade가 pass와 fail 중 하나라서 코드가 안전하게 읽고 길을 정합니다. feedback은 다음 초안에 넘길 말입니다.
  2. 끝낼지 정하는 함수입니다. 합격이거나 시도가 3번에 이르면 끝으로 가고 아니면 generate로 돌아갑니다. 이 상한은 공식 예제에 들어 있지 않아서 직접 넣어야 합니다.
  3. 돌아가는 길입니다. evaluate 뒤의 조건부 엣지가 generate를 가리키므로 그래프에 고리가 생깁니다. 앞에서 만든 고리와 같은 모양입니다.

어떤 일에 어떤 방식이 맞을까요

세 방식 모두 코드가 길을 그려 둔 워크플로입니다. 다만 나누는 기준이 다릅니다. 갈래가 미리 정해져 있으면 병렬 분기, 갈래의 수를 입력이 정하면 Send, 기준에 맞을 때까지 고쳐야 하면 평가 반복입니다.

어느 방식이든 속도나 품질을 얻는 대신 호출을 더 씁니다. Anthropic도 이런 시스템은 더 나은 성능을 얻으려고 지연과 비용을 맞바꾸는 경우가 많다고 말합니다. 일을 하나 골라 어느 방식이 맞는지 보세요.

다음 글에서는 Agent를 중간에 멈춰 두고 사람의 확인을 받은 뒤 이어 가는 방법을 알아보겠습니다.

참고 자료

출처 4개 펼치기