20장. 복합 요청 분해 처리
Worker와 결과 이어 주기 — 앞의 보고가 뒤의 재료가 된다
한 줄 요약
Worker는 작업 유형 하나를 맡는 함수입니다. 19장의 갈래 노드처럼 자기 일에 필요한 도구만 가집니다. Worker들은 서로를 모르므로, 우리 코드가 앞 Worker의 보고를 문자열로 쌓아 다음 Worker에게 넘겨 줍니다. 마지막에 모든 보고를 합쳐 답 하나를 만듭니다.
1. Worker 네 개
Worker는 모두 같은 모양입니다. 지시문과 지금까지의 결과를 받아, 보고를 문자열로 돌려줍니다.
def worker_order(instruction: str, context: str) -> str:
| Worker | 가진 것 | LLM 호출 |
|---|---|---|
worker_order |
get_my_orders, get_order_status, get_my_membership |
있음 |
worker_policy |
정책 문서 검색 (chroma_db/) |
있음 |
worker_stock |
search_products, check_stock |
있음 |
worker_ticket |
create_ticket |
없음 |
19장의 네 갈래 노드와 거의 같은 구성입니다. 달라진 점은 하나만 실행되는 것이 아니라, 계획에 적힌 만큼 차례로 실행된다는 것입니다.
어느 Worker를 부를지는 딕셔너리 하나가 정합니다.
WORKERS = {
TaskType.ORDER_LOOKUP: worker_order,
TaskType.POLICY_CHECK: worker_policy,
TaskType.STOCK_CHECK: worker_stock,
TaskType.TICKET: worker_ticket,
}
19장의 라우팅 표와 같은 역할입니다. 계획에 적힌 작업 유형을 보고 함수를 고르는 것은 코드입니다.
2. 결과를 이어 준다
세 번째 Worker(재고 확인)가 일하려면 어떤 상품의 재고를 볼지 알아야 합니다. 그 정보는 첫 번째 Worker(주문 조회)가 찾아냅니다. 그런데 Worker들은 서로 다른 LLM 호출이라 서로의 결과를 모릅니다.
그래서 우리 코드가 사이에서 이어 줍니다.
results, context = [], f"[고객 문의] {question}"
for i, task in enumerate(plan.tasks, 1):
out = WORKERS[task.task_type](task.instruction, context)
print(f" [Worker {i} 완료] {out}")
results.append(f"({task.task_type.value}) {out}")
context += f"\n[작업{i} 결과] {out}"
context는 처음에 고객 문의 한 줄로 시작합니다. Worker 하나가 끝날 때마다 그 보고가 context 뒤에 붙고, 다음 Worker는 길어진 context 전체를 받습니다.
| 순서 | 이 Worker가 받는 context |
|---|---|
| Worker 1 | 고객 문의 |
| Worker 2 | 고객 문의 + 작업1 결과 |
| Worker 3 | 고객 문의 + 작업1 결과 + 작업2 결과 |
9장에서 도구 실행 결과를 대화에 계속 덧붙였고, 16장에서 대화 이력을 매번 통째로 다시 보냈습니다. 앞의 결과를 뒤의 입력에 넣어 준다는 같은 방법이 작업 단위로 쓰이고 있습니다. 모델은 호출과 호출 사이에 아무것도 기억하지 못하므로, 이어 주는 일은 언제나 우리 코드의 몫입니다.
3. Worker의 지시 — 무엇을 알고 무엇을 모르는가
worker_order의 시스템 프롬프트는 짧습니다.
주문 조회 담당. 도구로 조회한 사실만 간결히 보고. 고객 등급·포인트는 get_my_membership 으로 확인.
"저 VIP인데요"처럼 고객의 등급이 관련된 문의에서는 계획의 첫 작업이 "등급 확인"으로 나옵니다. 그 일을 맡을 수 있도록 이 Worker에게 등급·포인트 조회 도구도 함께 주었습니다. 각 작업 유형에 해당하는 일을 Worker가 실제로 할 수 있도록 도구를 갖춰 두는 것까지가 개발자의 몫입니다.
worker_stock의 시스템 프롬프트는 훨씬 깁니다.
재고 확인 담당. 반드시 search_products 로 상품을 찾고 check_stock 을 호출한 결과로만 보고한다.
재고 데이터는 상품 단위다. 색상·사이즈 등 옵션별 재고는 시스템이 관리하지 않으므로,
'상품 전체 재고 N개, 요청하신 옵션의 재고는 별도 확인 필요' 형식으로 보고한다.
조회 없이 있다/없다를 단정하지 않는다.
길어진 까닭은 데이터의 생김새에 있습니다. 하루마켓의 상품 데이터에는 재고가 상품마다 숫자 하나로 적혀 있습니다. 쿠션 운동화는 70개입니다. "검정 270이 몇 개"인지는 데이터에 없습니다.
고객은 "검정 270 재고"를 물었습니다. 이 물음에 정확한 보고는 "상품 전체 재고는 70개이고, 검정 270만의 수량은 따로 확인이 필요하다" 입니다. 프롬프트의 보고 형식이 바로 그 문장입니다.
"모른다"와 "없다"는 다른 사실입니다. 확인이 필요한 것은 확인이 필요하다고 보고해야, 교환할 수 있는 고객을 돌려보내지 않습니다.
그래서 프롬프트에 이 Worker가 알 수 있는 것과 알 수 없는 것의 경계를 적었습니다. 이 Worker가 다루는 데이터가 어떻게 생겼는지는 모델이 알 수 없고, 개발자만 압니다.
4. 취합 — 보고를 답으로 바꾼다
Worker의 보고는 고객에게 보낼 글이 아닙니다. 도구 이름이 섞여 있고, 조항이 나열되어 있고, 셋이 따로 놉니다. 마지막 호출이 이것을 답변 하나로 만듭니다.
r = client.models.generate_content(
model=MODEL,
contents=f"고객 문의: {question}\n\n작업 결과:\n" + "\n".join(results)
+ "\n\n위 결과를 종합해 고객에게 하나의 답변을 작성하라.",
config=types.GenerateContentConfig(
system_instruction="하루마켓 상담원 '하루'. 존댓말, 5문장 이내.\n"
"- 작업 결과에 없는 사실을 만들지 않는다. "
"'확인 불가'로 보고된 것은 없음으로 바꾸지 말고 "
"확인이 필요하다고 그대로 안내한다.\n"
"- 확인된 주문(상품명·옵션)과 작업 결과의 숫자(금액·"
"재고 수량)는 빠뜨리지 않고 그대로 옮긴다.\n"
"- 접수·신청을 고객에게 떠넘기지 않는다. "
"원하시면 채팅에서 바로 접수를 도와드리겠다고 안내한다."))
취합에는 도구가 없습니다. 주어진 보고만 가지고 글을 씁니다. 시스템 프롬프트의 규칙 세 개를 봅니다.
| 규칙 | 답변에서 지켜지는 것 |
|---|---|
| "'확인 불가'로 보고된 것은 없음으로 바꾸지 말고" | Worker가 "확인이 필요하다"고 보고한 것은 답변에서도 "확인이 필요하다" 로 안내된다 |
| "확인된 주문과 작업 결과의 숫자는 빠뜨리지 않고 그대로" | 어떤 주문인지와 배송비 금액이 보고에 적힌 그대로 답변에 들어간다 |
| "접수·신청을 고객에게 떠넘기지 않는다" | 답변이 "채팅에서 바로 접수를 도와드리겠다" 로 끝난다 |
여러 보고를 한 답으로 합치는 자리에는 "보고에 있는 것만, 있는 그대로"라는 규칙이 함께 있어야 합니다. 3장에서 말한 대로, 무엇을 근거로 쓸지는 프롬프트가 정합니다.
5. 단계마다 규칙을 두고, 단계마다 출력한다
고객이 받는 답은 계획, Worker 셋, 취합을 차례로 거쳐 나옵니다. 그래서 단계마다 그 단계가 알 수 있는 것의 범위를 적어 두었고, 단계마다 결과를 화면에 찍습니다.
| 단계 | 그 단계에 적어 둔 것 | 화면에서 확인하는 곳 |
|---|---|---|
| 계획 | 작업 유형 네 가지, 1~4개, 최소 작업으로 | [Plan] |
| Worker | 자기 도구로 조회한 사실만. 재고는 상품 단위 | [Worker N 완료] |
| 취합 | 보고에 있는 것만, 있는 그대로 | 하루: |
답을 고치고 싶을 때는 이 세 곳을 위에서부터 읽습니다. 계획에 필요한 작업이 들어 있는지, 그 Worker의 보고에 필요한 사실이 있는지, 답변이 보고를 그대로 옮겼는지를 차례로 보면 손댈 곳이 한 단계로 좁혀집니다.
핵심 정리
- Worker는 작업 유형 하나를 맡는 함수이고, 자기 일에 필요한 도구만 가집니다.
- 어느 Worker를 부를지는 딕셔너리(코드) 가 정합니다.
- Worker는 서로를 모릅니다. 앞의 보고를 문자열로 쌓아 뒤에 넘기는 것은 우리 코드입니다.
- 데이터가 어떻게 생겼는지(재고는 상품 단위)는 개발자가 프롬프트에 적어 줘야 합니다.
- "모른다"와 "없다"는 다릅니다. Worker와 취합 양쪽 프롬프트에 그 구분을 적습니다.
- 단계마다 규칙을 두고, 단계마다 출력합니다. 답을 고칠 때 손댈 곳이 한 단계로 좁혀집니다.