day6_practice/final_scenario.py

6일차 종합: 코드를 따라가며 보기

RAG(2일차) + MCP(4일차) + Supervisor(6일차)를 한 파일에 합친 마지막 시나리오입니다. 이 페이지는 파일을 위에서 아래로 실행되는 순서대로 따라갑니다. 먼저 부품이 어떻게 조립되는지(1~8단계), 그다음 질문 하나가 들어와 Agent들이 일하는 과정(9~24단계)입니다. 호출 순서, 인자, 토큰, 터미널 출력은 모두 실제로 돌려서 기록한 값입니다(서브 Agent 안쪽까지 보려고 subgraphs=True로 따로 기록).

구조도: 누가 무엇을 들고 있나

위에서 아래로 진입 → 조율 → 전문 Agent → 자원 4층입니다. 각 상자의 L49-58은 final_scenario.py의 줄 번호입니다. 점선 상자는 코드 밖에 있는 외부 시스템입니다.

조율(Supervisor)전문 Agent도구외부 시스템

코드 실행 흐름: 파일은 이 순서로 돈다

python final_scenario.py를 치면 ① import 시점에 만들어지는 것, ② asyncio.run(main()) 안에서 만들어지는 것, ③ 질문을 처리하는 루프 순서로 진행됩니다. 소스에 보이는 순서와 실제 실행 순서가 다른 곳이 있습니다(build_app은 정의만 위에 있고 실제로는 맨 마지막 줄 asyncio.run이 불러야 실행됩니다).

Mermaid 소스 (flowchart) — mermaid.live에 붙여넣으면 그려집니다

      
    

시퀀스 다이어그램: 누가 누구를 불렀나

세로줄이 주체, 가로 화살표가 호출입니다. 🧠는 LLM을 부른 지점이고 번호 옆 숫자는 그 호출의 토큰입니다. 파란 배경은 main()의 print가 못 보는 서브 Agent 안쪽이고, 붉은 줄은 에러로 끝난 호출입니다.

Mermaid 소스 (sequenceDiagram)

      
    
조립 supervisor (LLM) handoff 서브 Agent (LLM) tools 끝
main() app.astream(...) supervisor 🧠 Sonnet 4.5 + handoff 도구 3개 🧠 LLM 호출! research_agent 🧠 worker LLM · 도구 1개 🧠 LLM 호출! data_agent 🧠 worker LLM · MCP 도구 4개 🧠 LLM 호출! general_agent 🧠 worker LLM · 도구 없음 · 이번엔 안 불림 ✗ transfer_back_to_supervisor Chroma ./chroma_db sds_policies + Titan embed v2 MCP 서버 mcp_server.py 자식 프로세스(stdio) json 3개 읽음 시작 ×0 END

🎒 State["messages"] 두 개

바깥과 안쪽은 따로 쌓입니다

바깥 (supervisor 그래프 · main()이 받는 것)

안쪽 (서브 Agent 그래프)

🖥️ 터미널 출력 (main()의 print)

터미널 출력 읽기: 왜 같은 줄이 이렇게 많이 나오나

main()은 stream_mode="updates"로 바깥 그래프 노드가 끝날 때마다 그 노드가 돌려준 메시지를 찍습니다. 바깥 업데이트는 5번 왔고, 이번 실행에서 찍힌 줄은 줄입니다. 그중 줄은 앞에서 이미 찍은 줄의 반복(주황색)입니다.

① 중복: supervisor는 메시지 전체를 돌려줌

supervisor 업데이트는 그때까지의 대화 전체를 담고 있습니다(3개 → 8개 → 12개로 늘어남). 그래서 앞에서 이미 찍은 줄이 매번 또 찍힙니다. State에는 중복이 없습니다. 같은 id의 메시지는 add_messages가 교체하므로 최종 State는 12개입니다.

② 라벨이 헷갈림 (132~133줄)

라벨은 m.name or node입니다. 사용자 질문(HumanMessage)은 name이 없어서 [supervisor] 출장 규정에서…처럼 supervisor가 말한 것처럼 찍힙니다. 도구 호출 줄은 항상 node를 쓰므로(131줄) research_agent가 만든 transfer_back_to_supervisor가 나중엔 [supervisor] 도구: …로 찍힙니다.

③ 200자에서 잘림 (134줄)

get_text(m)[:200] 때문에 research_agent의 긴 보고서와 Supervisor의 최종 보고서는 앞 200자만 보입니다. 터미널로는 최종 답의 결론(맨 끝)을 볼 수 없습니다. 전체를 보려면 마지막 업데이트의 마지막 메시지를 따로 전부 출력해야 합니다.

④ 서브 Agent 안쪽은 한 줄도 안 나옴

retrieve_docs, list_team_members, list_projects, data_agent의 LLM 호출 3번, 그리고 에러가 났던 호출까지 바깥 그래프에서는 보이지 않습니다. 안쪽 이벤트는 app.astream(..., subgraphs=True)로 받아야 나옵니다. 이 페이지의 안쪽 기록이 그렇게 뽑은 것입니다.

호출 횟수 (실제 실행)

누가무엇을횟수토큰 (호출별)main()에 보임?

핵심 개념 4개

🧱 부품은 두 시점에 만들어진다

research_agent와 general_agent는 모듈 맨 위(import 시점)에서 만들어지고, data_agent와 Supervisor는 async def build_app() 안에서 만들어집니다. MCP 도구를 가져오는 await client.get_tools()가 비동기라서 그 뒤에 오는 data_agent도 함수 안에 들어가야 하기 때문입니다.

🤝 handoff는 인자 없는 도구 호출

Supervisor LLM은 transfer_to_research_agent({})처럼 빈 인자로 담당을 고릅니다. "무엇을 해라"는 말이 없으니 서브 Agent는 원래 질문 전체와 지금까지의 대화를 그대로 받습니다. 서브 Agent는 돌려줄 때 마지막 메시지 하나만 내보냅니다(output_mode="last_message").

🔌 MCP 도구는 호출마다 새 프로세스

client.get_tools()를 세션 없이 부르면, 가져온 도구는 실행할 때마다 서버를 새로 띄웁니다. 이번 실행의 stderr에 Starting MCP server가 번 찍혔습니다. 도구 목록을 가져올 때 1번, list_team_members와 list_projects를 부를 때 2번입니다. 그래서 MCP 도구가 첫 호출에 1~2초 더 걸립니다.

🧭 "순서대로" 규칙과 한 번에 하나

Supervisor 프롬프트 107줄 "여러 요구가 섞인 질문은 각 Agent를 순서대로 호출" 때문인지, 이번엔 research → data 순으로 하나씩 넘겼습니다(2번 돌려 2번 다 그랬습니다). 앞서 verify_routing.py의 ④ 질문에서는 두 Agent를 한 응답에 같이 요청해 실패했습니다. 프롬프트로 유도할 뿐 보장은 아닙니다.

이번 실행에서 관찰한 점