day6_practice · 최종 정리 (verify_routing.py 기준)

6일차: 멀티 Agent Supervisor 총정리

도구 8개를 든 만능 Agent가 헷갈리는 문제에서 출발해, 전문 Agent 3개 + Supervisor로 나누고, 마지막에 verify_routing.py로 단일 질문 3개와 복합 질문 1개가 제대로 배분되는지 검증했습니다. 아래 결과는 모두 실제로 돌려서 기록한 값입니다(서브 Agent 안쪽까지 subgraphs=True로 기록, 모델은 지금 파일 설정인 Sonnet 4.6).

6일차 흐름: 왜 여기까지 왔나

1단계single_agent_problem.py만능 Agent의 혼동

도구 8개(문서 검색 2, DB 조회 4, 보고서, 메신저)를 한 Agent에 몰아주고 같은 질문을 5번 반복해 도구 선택이 흔들리는지 봅니다. 문제 제기 단계.

2단계router_only.py라우팅만 떼어 보기

Supervisor의 핵심인 "누구에게 보낼까"만 구조화 출력(Route.next_agent)으로 분리해, 질문 3개가 맞는 Agent 이름으로 나오는지 봅니다.

3단계two_agent_supervisor.py최소 2-Agent

create_supervisor로 data/research 2개만 묶어 handoff가 어떻게 오가는지 관찰합니다. 서브 Agent에 name이 필수인 이유.

4단계specialist_agents.py전문 Agent 3종 단독 테스트

조립 전에 각 Agent를 따로 불러 담당 범위와 거절을 확인합니다.

→ 콜 맵 보기
5단계supervisor_assembled.pySupervisor 조립

3종을 Supervisor 프롬프트(배분 기준, 규칙)와 함께 묶습니다. run() 출력 읽는 법도 여기.

→ 콜 흐름 보기
6단계 · 지금verify_routing.py배분 검증

단일 질문 3개(Agent별 1개) + 복합 질문 1개(data + research)를 넣어 기대한 Agent가 불렸는지 확인합니다.

1~3단계는 이번에 다시 돌리지 않았고 코드 기준으로 정리했습니다. 4~6단계는 실제 실행 결과입니다.

전체 구조: 누가 누구를 부를 수 있나

run(질문) verify_routing → 4번 supervisor 🧠 supervisor_llm (Sonnet 4.6) transfer_to_data_agent transfer_to_research_agent transfer_to_general_agent handoff data_agent 임직원·자산·프로젝트 조회 research_agent 연차·재택·출장 규정 검색 general_agent 인사·잡담·범위 밖 안내 search_employee(name) list_team_members(team) get_asset(owner) ← 사람 이름 list_projects(team) retrieve_docs(query) 키워드 도구 없음 마지막 메시지만 들고 복귀 (transfer_back_to_supervisor)

Supervisor는 "누구에게 넘길지"만 도구 호출로 정하고, 실제 조회는 서브 Agent가 자기 도구로 합니다. 서브 Agent 안쪽의 도구 호출은 Supervisor에게 보이지 않고, 마지막 보고 한 문단만 돌아옵니다.

검증 결과: 기대 vs 실제

#질문기대실제로 실행된 Agent (이번 실행)판정LLM 호출 · 토큰

"실제로 실행된 Agent"는 Supervisor가 요청한 handoff가 아니라 서브 그래프가 실제로 돈 순서입니다. ④는 Supervisor가 매번 둘을 동시에 요청했지만 한 번에 하나씩만 실행됐습니다.

질문별 콜 시퀀스 (누가 뭘 호출했나)

행을 누르면 실제 메시지가 여기 나옵니다


      
🟣 LLM 호출(자기 자신 화살표)🟠 handoff (요청 → 실제 실행)✗ 빨간 점선 = 요청했지만 그 차례에 실행 안 됨🔵 도구 호출🟢 결과 반환

복합 질문 해부: 왜 ④가 매번 어긋나나

같은 질문 "클라우드운영팀 GPU 서버 자산 현황을 알려주고, 출장 규정상 식비 한도도 함께 알려줘"를 오늘 5번 돌린 결과입니다(Bedrock 응답 시간 초과로 중간에 끊긴 1번은 제외). 정답은 "이서준 GPU 서버 1대(NVIDIA A100 80GB) + 국내 식비 하루 5만원"입니다.

시도실행된 순서끝GPU 답
A (모델 전환 시점)supervisor(둘 요청) → research만 → supervisor정상 종료지어냄 GPU-SVR-001/002, 판교 IDC 표 (어디에도 없는 데이터)
Bsupervisor(둘 요청) → data → supervisor(둘 요청) → data → … 반복GraphRecursionError (limit 25)답 없음
C(둘 요청) → data → (둘 요청) → data → (둘 요청) → research → (둘 요청) → research → supervisor정상 종료틀림 "GPU 서버 자산이 확인되지 않습니다"
D(둘 요청) → data ×8번 왕복도중에 Bedrock 요청 한도(Throttling)답 없음
E (페이지 탭 ④)(둘 요청) → data → (둘 요청) → research → supervisor정상 종료틀림 "확인되지 않습니다"
원인 1: 동시 handoff를 막지 못함

Supervisor LLM은 매번 transfer_to_data_agent와 transfer_to_research_agent를 한 응답에 같이 냈습니다. langgraph_supervisor는 이걸 막으려고 LLM에 parallel_tool_calls=False를 넘기는데, 모델의 bind_tools가 그 인자를 받을 때만 넘깁니다(supervisor.py의 _supports_disable_parallel_tool_calls). Bedrock의 ChatBedrockConverse.bind_tools에는 그 인자가 없어서 막는 장치가 꺼진 상태입니다.

원인 2: 둘을 요청해도 하나만 실행됨

동시 요청이 오면 라이브러리는 둘 다 Send로 보내도록 만들어져 있지만, 오늘 5번 모두 그 차례에 실제로 돈 Agent는 하나뿐이었습니다. 돌아온 Supervisor는 "아직 하나가 안 됐네"라며 또 둘을 요청하고, 같은 Agent가 또 불리면 핑퐁이 됩니다(B, D). 왜 하나만 실행되는지는 라이브러리 내부 동작이라 여기서 단정하지 않습니다. 관찰된 사실로만 적습니다.

원인 3: data_agent의 도구 오용

get_asset(owner)는 사람 이름을 받는데, data_agent가 get_asset(owner='클라우드운영팀')으로 팀 이름을 넣어 "자산이 없습니다"를 받고 그대로 "GPU 서버 없음"이라 보고했습니다. 단독 질문("클라우드운영팀 자산 목록", 5단계 페이지)에서는 list_team_members로 팀원을 먼저 찾았는데, 복합 질문에선 그 단계를 건너뛰었습니다.

원인 4: Supervisor가 빈칸을 채움

시도 A에서 data_agent는 한 번도 안 불렸는데 Supervisor의 최종 답에 GPU 서버 표가 들어 있었습니다. 프롬프트 21줄 "직접 답을 지어내지 말고"가 있어도, Supervisor LLM이 빠진 결과를 그럴듯하게 채운 사례입니다. 서브 Agent의 도구 결과가 Supervisor에게 안 보이는 구조(마지막 메시지만 전달)라 검증할 근거도 없습니다.

고칠 방향 (제안 · 아직 적용, 검증 안 함)

① 한 번에 한 Agent만 넘기게

Supervisor 프롬프트 규칙에 명시합니다. 22줄 "순서대로 호출"만으로는 동시 요청을 막지 못했습니다.

"- handoff 도구는 한 번에 하나만 호출하라.
  결과를 받은 뒤 다음 Agent를 호출하라."
② data_agent에 팀 자산 조회 순서를

시스템 프롬프트에 순서를 적거나, get_asset이 팀도 받도록 도구를 고칩니다.

"팀 자산은 list_team_members로 팀원을 확인한 뒤
 팀원별로 get_asset을 호출한다."
③ 근거 없는 내용 금지

Supervisor 규칙에 "Agent 보고에 없는 수치나 목록은 쓰지 말고, 못 받은 항목은 '확인하지 못했다'고 답하라"를 추가합니다. 필요하면 output_mode="full_history"로 도구 결과까지 넘겨 Supervisor가 확인할 수 있게 합니다(대신 토큰 증가).

④ 무한 핑퐁 대비

recursion_limit=25가 B에서 실제로 루프를 끊었습니다. 지금 run()은 이 에러를 잡지 않아 프로그램이 죽으므로, 잡아서 "처리하지 못했다"고 안내하는 코드가 필요합니다.

from langgraph.errors import GraphRecursionError
try: ...app.stream(...)
except GraphRecursionError: print("  => 배분 실패(핑퐁)")

agent_checklist.md로 3종 점검

항목data_agentresearch_agentgeneral_agent
담당을 한 문장으로 말할 수 있나✅ 임직원·자산·프로젝트 조회✅ 규정 문서 검색✅ 인사·잡담·범위 밖 안내
다른 도메인 도구가 섞이지 않았나✅ DB 조회 4개만✅ retrieve_docs만✅ 도구 없음
시스템 프롬프트 5줄 이내✅ 4줄✅ 3줄✅ 3줄
Supervisor 배분 기준에 담당이 명사로✅ "임직원, 자산, 프로젝트"✅ "연차, 재택근무, 출장"⚠ "위 두 가지에 해당하지 않는 질문" (명사 아님)
범위 밖 행동(거절 후 복귀)✅ "담당 범위가 아닙니다"✅ 같음✅ 안내 문구

체크리스트는 다 통과에 가깝지만 ④가 틀렸습니다. 체크리스트는 Agent 하나하나의 설계를 보는 것이고, 복합 질문의 실패는 Supervisor의 동시 handoff와 도구 인자 설계(get_asset의 owner) 쪽에서 났습니다. 검증 질문을 실제로 돌려 봐야 보이는 문제입니다. 그래서 verify_routing.py 같은 검증 단계가 필요합니다.

운영 메모 (오늘 실제로 겪은 것)