따라하기 — 여러 보고를 하나의 판단으로 합치기
한 줄 요약
종합 단계는 보고를 모아 하나의 답으로 정리합니다. 도구가 없습니다. "조사된 것만 근거로" 라는 지시가 여기서도 핵심입니다.
1. 종합 노드
아래는 code/ch08_multi_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
def synthesize_node(state: State) -> dict:
"""보고를 모아 하나의 답으로 정리한다."""
evidence = "\n\n".join(
f"[{r['worker']}] {r['task']}\n{r['report']}" for r in state["results"])
question = state["messages"][-1].text
answer = llm.invoke([
{"role": "system", "content": SYNTH_SYSTEM},
{"role": "user", "content": f"[질문]\n{question}\n\n[팀원 보고]\n{evidence}"},
]).text.strip()
return {"messages": [{"role": "assistant", "content": answer}]}
여기서도 도구가 없습니다. 모델을 한 번 부를 뿐입니다.
2. 6장 RAG와 구조가 같습니다
나란히 놓아 보세요.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
# 6장 — 문서 조각을 근거로
docs = retriever.invoke(question)
context = "\n\n".join(d.page_content for d in docs)
body = llm.invoke(PROMPT.format(context=context, question=question)).text
# 8장 — 담당자 보고를 근거로
evidence = "\n\n".join(f"[{r['worker']}] {r['task']}\n{r['report']}" for r in state["results"])
answer = llm.invoke([{"role": "system", ...}, {"role": "user", "content": f"...{evidence}"}]).text
"근거를 모아 프롬프트에 넣고, 그것만 보고 답하게 한다" — 완전히 같은 패턴입니다.
| 6장 | 8장 | |
|---|---|---|
| 근거의 출처 | 문서 조각 4개 | 담당 보고 여러 개 (작업 수만큼) |
| 근거를 모으는 법 | 벡터 검색 | 담당이 조사 |
| 지시 | "문서에 적힌 것만" | "조사된 것만" |
| 출처 표시 | "환불교환정책.pdf 1쪽" | "(데이터분석가)" |
RAG의 R(Retrieval)이 여기서는 "담당자들의 조사" 입니다. 무엇으로 근거를 모으든, 모아서 근거로 쓴다는 구조는 같습니다.
3. 종합의 시스템 지시
아래는 code/ch08_multi_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
SYNTH_SYSTEM = (
"너는 승승장구몰 대응팀의 팀장이다. 팀원들이 조사해 온 내용만 근거로 최종 답을 쓴다.\n"
"형식: ① 확인된 사실을 근거와 함께 정리 → ② 그래서 무엇을 해야 하는지 권고 2~3개.\n"
"조사되지 않은 내용은 지어내지 않는다. 한국어로 간결하게 쓴다."
)
세 부분입니다.
① "팀원들이 조사해 온 내용만 근거로"
6장 6절의 그 문장입니다. 모델이 자기 지식을 보태지 못하게 막습니다.
이게 없으면 이런 게 나옵니다.
② 형식 지정
① 확인된 사실을 근거와 함께 정리 → ② 그래서 무엇을 해야 하는지 권고 2~3개
형식을 정해 주면 답이 일관됩니다. 그리고 "사실"과 "권고"를 분리하는 것이 중요합니다.
[사실] 재고가 3개다 ← 조사된 것. 확실하다.
[권고] 27개를 발주하십시오 ← 판단. 사람이 검토해야 한다.
둘을 섞어 쓰면 어디까지가 사실이고 어디부터가 판단인지 알 수 없습니다.
이게 실무에서 중요합니다. AI가 낸 답을 사람이 검토할 때, "사실은 맞는데 권고가 과하다" 같은 판단을 하려면 둘이 나뉘어 있어야 합니다.
③ "조사되지 않은 내용은 지어내지 않는다"
①을 반복한 것이지만 관점은 다릅니다. ①은 "근거를 제한"하고, ③은 "빈칸을 메우지 말라"입니다.
6장 6절에서 본 그 조합이죠 — 근거 제한 + 모를 때 할 말.
4. 근거를 조립하는 방식
아래는 code/ch08_multi_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
evidence = "\n\n".join(
f"[{r['worker']}] {r['task']}\n{r['report']}" for r in state["results"])
[데이터분석가] 최근 판매 추이, 재고 수량, 리뷰 평점과 부정 리뷰 내용을 분석해주세요.
승승 스마트워치 Fit 5(P0003)의 정보는 다음과 같습니다.
* 재고 수량: 3개
* 리뷰 평균 평점: 3.8점
* 부정 리뷰 주요 내용: "GPS를 켜면 하루도 못 갑니다..."
[규정담당] 제품 사양(배터리·방수 등)을 알려주세요.
* 배터리(일반): 약 7일
* 배터리(GPS): 약 18시간
...
담당자 이름을 대괄호로 표시했습니다. 그래서 최종 답변에 이런 게 나옵니다.
1. **제품 현황 및 매출 추이**: ... 재고가 3개로 매우 적습니다. (데이터분석가)
출처가 담당자 이름으로 표시됩니다. 6장에서 "환불교환정책.pdf 1쪽"이라고 했던 자리죠.
5. 질문을 꺼내는 방법
아래는 code/ch08_multi_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
question = state["messages"][-1].text
messages 의 마지막이 이번 질문입니다.
왜 마지막인가 — 종합 노드가 실행되는 시점에는 아직 답변이 안 들어갔기 때문입니다.
messages = [
...(지난 대화),
HumanMessage("승승 스마트워치 Fit 5, 지금 어떻게 대응해야 할까요?") ← [-1]
]
그리고 종합 노드가 답변을 넣습니다.
아래는 code/ch08_multi_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
return {"messages": [{"role": "assistant", "content": answer}]}
add_messages 가 뒤에 이어 붙입니다. 7장에서 배운 그것이죠.
6. 최종 답변
[최종 답변]
**① 확인된 사실을 근거와 함께 정리**
1. **제품 현황 및 매출 추이**: '승승 스마트워치 Fit 5(P0003)'는 현재 재고가 3개로
매우 적습니다. 2026년 5월에 1,876,200원의 일시적인 매출 급증이 있었습니다. (데이터분석가)
2. **고객 불만 및 제품 사양 불일치**: 평균 리뷰 평점은 3.8점이며, 부정 리뷰의 주요
내용은 "GPS 사용 시 배터리 지속 시간이 짧아 제품 설명(일주일)과 다르다"는 불만입니다.
제품 사양상 일반 사용 시 배터리는 약 7일, GPS 사용 시 약 18시간으로 명시되어 있습니다.
(데이터분석가, 규정담당)
3. **환불 규정**: 단순 변심은 7일, 상품 불량·하자는 30일 이내입니다. (규정담당)
4. **경쟁 환경**: 스마트워치 시장은 성장세이며, 긴 배터리 수명이 중요한 경쟁 요소로
작용하고 있습니다. (시장조사원)
**② 그래서 무엇을 해야 하는지 권고**
1. **배터리 성능 관련 고객 소통 강화**: GPS 사용 시 배터리 지속 시간에 대한 고객 불만이
제품 사양과 큰 차이를 보이므로, 상세 페이지에 GPS 사용 시 배터리 소모량(약 18시간)을
명확히 고지해야 합니다.
2. **재고 확보**: 현재 재고가 3개로 매우 적고 최근 판매가 급증했으므로 즉시 발주가
필요합니다.
3. **불만 고객 응대 기준 적용**: 불량·하자 기준(30일)에 따라 반품을 안내합니다.
표현은 실행할 때마다 다릅니다. 구조와 근거가 같으면 성공입니다.
7. 여기가 이 과정의 결승선입니다
2번 항목을 다시 보세요.
부정 리뷰: "GPS 사용 시 배터리가 짧아 제품 설명(일주일)과 다르다" ← CSV (5장)
제품 사양: 일반 7일 / GPS 18시간 ← RAG (6장)
↓
"고객 불만 및 제품 사양 불일치"
↓
권고: 상세 페이지에 GPS 18시간을 명확히 고지하라
서로 다른 곳에서 온 두 사실을 붙여 만든 판단입니다.
- CSV만 봤다면 → "평점이 낮다"에서 끝
- 문서만 봤다면 → "사양은 이렇다"에서 끝
- 둘을 붙이니 → "표기가 오해를 부른다"
이것이 멀티에이전트를 쓰는 이유의 전부입니다.
1장 1절에서 보여 준 화면을 기억하시나요? 그때는 "지금 이해할 필요 없다"고 했습니다. 지금은 한 줄 한 줄 어디서 왔는지 아실 겁니다.
8. 종합도 틀릴 수 있습니다
한계도 짚고 넘어갑니다.
| 위험 | 무슨 일이 |
|---|---|
| 보고가 틀렸으면 | 종합도 틀린다. 잘못된 정보를 넣으면 잘못된 답이 나온다 |
| 보고가 서로 모순되면 | 모델이 한쪽을 고르거나 얼버무린다 |
| 권고가 과할 수 있음 | "단종을 검토하십시오" 같은 큰 판단을 쉽게 한다 |
세 번째가 실전에서 문제입니다. 실제로 실행해 보면 모델이 이런 권고를 하기도 합니다.
"재고를 소진한 후 단종을 검토하십시오"
재고 3개와 평점 3.8점만 보고 단종을 권하는 것은 과합니다. 사람이 검토해야 할 판단이죠.
그래서 형식을 "사실 → 권고"로 나눈 것입니다. 사실은 근거가 있고, 권고는 사람이 검토하라는 표시입니다.
AI가 한 것과 사람이 판단할 것 — AI는 사실을 모아 정리했습니다. 무엇을 실행할지는 사람이 정합니다. 이 경계선을 형식으로 드러낸 것입니다.
스스로 해 보기
SYNTH_SYSTEM에서 "팀원들이 조사해 온 내용만 근거로" 를 지우고 실행해 보세요. 조사되지 않은 내용이 섞이나요?- 형식 지정을 지우고 실행해 보세요. 사실과 권고가 구분되나요?
- 종합에 넘어가는
evidence를 출력해 보세요.
python print("=" * 40); print(evidence); print("=" * 40)
담당자 보고가 어떻게 조립되는지 보세요. - 형식을 바꿔 보세요. 예:
"표 형식으로 정리한다"또는"세 문장 이내로 답한다".
핵심 정리
- 종합 노드는 도구가 없습니다. 보고를 모아 모델을 한 번 부를 뿐입니다.
- 6장 RAG와 구조가 같습니다 — 근거를 모아 프롬프트에 넣고 그것만 보고 답하게 합니다.
- RAG의 R(검색)이 여기서는 "담당자들의 조사" 입니다.
- 시스템 지시 셋 — 근거 제한 · 형식 지정(사실→권고) · 지어내지 않기.
- 사실과 권고를 나누는 것이 중요합니다. 어디까지가 근거이고 어디부터가 판단인지 보여 줍니다.
- 근거에 담당자 이름을 대괄호로 붙이면 최종 답변에 출처가 표시됩니다.
- 결승선 — 리뷰 불만(CSV)과 제품 사양(RAG)을 붙여 "표기가 오해를 부른다" 는 판단이 나옵니다.
- 종합도 틀릴 수 있습니다. 특히 권고가 과할 수 있어, 실행 여부는 사람이 정합니다.