📚 Agentic AI - 기업용 자율 에이전트 개발 8장 · 멀티에이전트 아키텍처 Agentic AI란

따라하기 — 여러 보고를 하나의 판단으로 합치기

한 줄 요약

종합 단계는 보고를 모아 하나의 답으로 정리합니다. 도구가 없습니다. "조사된 것만 근거로" 라는 지시가 여기서도 핵심입니다.


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절의 그 문장입니다. 모델이 자기 지식을 보태지 못하게 막습니다.

이게 없으면 이런 게 나옵니다.

"…일반적으로 스마트워치는 배터리가 3~5일 지속되는 것이 표준이므로…"아무도 조사하지 않은 내용담당 넷의 보고 어디에도 없던 문장입니다. 종합 단계에도 '보고에 없는 것은 쓰지 않는다'를 명시해야 합니다.
담당 넷의 보고 어디에도 없던 문장이 최종 답에 섞였다

② 형식 지정

① 확인된 사실을 근거와 함께 정리 → ② 그래서 무엇을 해야 하는지 권고 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는 사실을 모아 정리했습니다. 무엇을 실행할지는 사람이 정합니다. 이 경계선을 형식으로 드러낸 것입니다.


스스로 해 보기

  1. SYNTH_SYSTEM 에서 "팀원들이 조사해 온 내용만 근거로" 를 지우고 실행해 보세요. 조사되지 않은 내용이 섞이나요?
  2. 형식 지정을 지우고 실행해 보세요. 사실과 권고가 구분되나요?
  3. 종합에 넘어가는 evidence 를 출력해 보세요.
    python print("=" * 40); print(evidence); print("=" * 40)
    담당자 보고가 어떻게 조립되는지 보세요.
  4. 형식을 바꿔 보세요. 예: "표 형식으로 정리한다" 또는 "세 문장 이내로 답한다".

핵심 정리

  • 종합 노드는 도구가 없습니다. 보고를 모아 모델을 한 번 부를 뿐입니다.
  • 6장 RAG와 구조가 같습니다 — 근거를 모아 프롬프트에 넣고 그것만 보고 답하게 합니다.
  • RAG의 R(검색)이 여기서는 "담당자들의 조사" 입니다.
  • 시스템 지시 셋 — 근거 제한 · 형식 지정(사실→권고) · 지어내지 않기.
  • 사실과 권고를 나누는 것이 중요합니다. 어디까지가 근거이고 어디부터가 판단인지 보여 줍니다.
  • 근거에 담당자 이름을 대괄호로 붙이면 최종 답변에 출처가 표시됩니다.
  • 결승선 — 리뷰 불만(CSV)과 제품 사양(RAG)을 붙여 "표기가 오해를 부른다" 는 판단이 나옵니다.
  • 종합도 틀릴 수 있습니다. 특히 권고가 과할 수 있어, 실행 여부는 사람이 정합니다.
← 이전 절06. 따라하기 각 담당에게 배분하고 실행하기다음 절 →08. 실행 스마트워치 Fit 5 어떻게 대응할까요 전체 데모