실무 Multi-Agent 오케스트레이션 20장 · 복합 요청 분해 처리 3 / 7 ← 이전목차다음 → TechLead Cro

20장. 복합 요청 분해 처리

계획도 구조화 출력이다 — 작업 목록을 정해진 모양으로 받는다

한 줄 요약

Planner가 내놓는 계획은 코드가 읽고 실행할 데이터입니다. 그래서 자유로운 문장이 아니라 4장에서 배운 구조화 출력으로 받습니다. 작업의 종류를 네 가지로 못 박아 두면, 모델은 우리가 처리할 수 있는 작업으로만 계획을 세울 수 있습니다.


1. 계획을 문장으로 받으면

모델에게 "이 문의를 처리할 계획을 세워라"고만 하면 이런 답이 올 수 있습니다.

먼저 고객님의 주문 내역을 살펴본 뒤, 반품 규정을 확인하고, 필요하면 재고도 봐야겠습니다.

사람이 읽기에는 충분합니다. 그런데 코드는 이 문장에서 "작업이 몇 개이고 각각 누구에게 맡길지"를 꺼낼 수 없습니다. 4장에서 문의 유형을 구조화 출력으로 받은 것과 같은 이유로, 계획도 정해진 모양으로 받아야 합니다.


2. 계획의 모양 — 클래스 세 개

class TaskType(str, enum.Enum):
    ORDER_LOOKUP = "주문조회"
    POLICY_CHECK = "정책확인"
    STOCK_CHECK = "재고확인"
    TICKET = "티켓생성"


class PlanTask(BaseModel):
    task_type: TaskType
    instruction: str = Field(description="이 작업이 알아내야 할 것 (한 문장)")
    depends_on_previous: bool = Field(
        description="이전 작업 결과가 있어야 실행 가능한가")


class Plan(BaseModel):
    tasks: list[PlanTask] = Field(description="실행 순서대로 정렬된 작업 목록 (1~4개)")
클래스 뜻 담는 것
TaskType 작업의 종류 네 값 가운데 하나
PlanTask 작업 하나 종류, 무엇을 알아낼지, 앞 작업의 결과가 필요한지
Plan 계획 전체 작업의 목록

4장에서 문의 유형을 Intent라는 열거형(enum)으로 정했던 것과 같은 방식입니다. 이번에는 그 안에 목록이 들어 있다는 점만 다릅니다.


3. 작업 유형이 곧 계획의 테두리다

TaskType의 값은 네 개입니다. 구조화 출력으로 받으므로 모델은 이 네 가지 말고는 계획에 넣을 수 없습니다. "배송지 변경"이나 "환불 실행" 같은 작업은 계획에 등장할 수 없습니다. 이 장의 Worker는 확인하고 접수하는 데까지만 합니다. 배송지 변경이나 교환 접수처럼 직접 처리할 수 있는 일은 21장에서 처리 도구를 따로 만들어 붙입니다.

그리고 이 네 가지는 뒤에서 볼 Worker 함수 네 개와 하나씩 짝을 이룹니다.

작업 유형 맡는 Worker
주문조회 worker_order
정책확인 worker_policy
재고확인 worker_stock
티켓생성 worker_ticket

5장에서 "모델은 우리가 준 도구 목록 안에서만 고를 수 있다"고 했습니다. 같은 원리입니다. 할 수 있는 일의 목록은 개발자가 정하고, 모델은 그 안에서 고릅니다.


4. 나머지 칸들 — 설명이 곧 지시다

4장에서 Field(description=...)에 적은 글이 모델에게 주는 지시가 된다고 했습니다. 여기서도 같습니다.

칸 설명에 적은 것 왜 그렇게 적었나
instruction "이 작업이 알아내야 할 것 (한 문장)" Worker에게 그대로 넘길 지시문입니다. 길면 Worker가 헷갈립니다
depends_on_previous "이전 작업 결과가 있어야 실행 가능한가" 작업 사이의 의존 관계를 적게 합니다
tasks "실행 순서대로 정렬된 작업 목록 (1~4개)" 순서와 개수의 범위를 알려 줍니다

depends_on_previous는 이런 구분을 담으려는 칸입니다. "검정 270의 재고 확인"은 앞에서 어떤 운동화인지 찾은 다음에야 뜻이 있습니다(의존 있음). 서로 상관없는 작업들은 동시에 실행해도 됩니다(의존 없음).

다만 이번 장의 코드는 이 값을 화면에 표시만 하고, 실행에는 쓰지 않습니다. 모든 작업을 계획에 적힌 순서대로 하나씩 실행합니다. 계획에 정보를 담아 두는 일과 그 정보를 활용하는 일은 따로입니다.

"1~4개"라는 범위도 지시의 일부입니다. 작업 하나하나가 LLM 호출이므로, 계획이 잘게 쪼개질수록 시간과 비용이 늘어납니다. 좋은 계획은 정밀하게 쪼갠 계획이 아니라 필요한 만큼만 쪼갠 계획입니다.


5. Planner — 호출 한 번

def make_plan(question: str) -> Plan:
    r = client.models.generate_content(
        model=MODEL,
        contents=f"고객 문의를 처리하기 위한 작업 계획을 세워라.\n문의: {question}",
        config=types.GenerateContentConfig(
            system_instruction="하루마켓 상담 Planner. 문의를 최소 작업으로 분해한다. "
                               "불필요한 작업을 만들지 않는다.",
            response_mime_type="application/json",
            response_schema=Plan,
        ),
    )
    return r.parsed

4장의 분류기와 뼈대가 같습니다. response_mime_type으로 JSON 응답을 켜고 response_schema에 Plan을 넘기면, r.parsed에 Plan 객체가 들어옵니다. 그 뒤로는 plan.tasks를 반복문으로 돌면 됩니다.

Planner에게는 도구를 주지 않았습니다. Planner는 주문을 조회하지도 문서를 찾지도 않습니다. 문의를 읽고 할 일의 목록만 적습니다.


핵심 정리

  • 계획은 코드가 실행할 데이터이므로 구조화 출력으로 받습니다.
  • TaskType의 네 값이 계획의 테두리입니다. 모델은 그 밖의 작업을 계획에 넣을 수 없습니다.
  • 작업 유형은 Worker 함수와 하나씩 짝을 이룹니다.
  • Field의 설명으로 지시문의 길이, 순서, 개수 범위, 의존 관계를 알려 줍니다.
  • 이번 장의 코드는 depends_on_previous를 표시만 하고 모든 작업을 순서대로 실행합니다.
  • Planner는 도구 없이 계획만 세웁니다.
← 이전 절Planner-Worker — 계획하고, 실행하고, 합친다다음 절 →Worker와 결과 이어 주기 — 앞의 보고가 뒤의 재료가 된다
오명운 · macro@prag-ai.com