G 스토리

[Side Project] 단일 AI의 한계를 넘어: LangGraph와 Streamlit으로 구축한 나만의 다중 에이전트 시스템(MAS) 개발기 본문

IT 이것저것 요모조모/AI

[Side Project] 단일 AI의 한계를 넘어: LangGraph와 Streamlit으로 구축한 나만의 다중 에이전트 시스템(MAS) 개발기

Jiione 2026. 4. 22. 23:04

들어가며 ...

왜 다중 에이전트(Multi-Agent)인가?

"AI에게 코드를 짜달라고 하고, 그 코드를 스스로 검증하라고 하면 왜 자꾸 같은 실수를 반복할까?"
이번 사이드 프로젝트는 이 단순한 질문에서 시작되었습니다. 사람이 무언가를 기획하고 스스로 검토할 때 확증 편향에 빠져 자신의 실수를 발견하지 못하는 것처럼, 단일 AI 모델 역시 자신이 생성한 결과물에 대해 비판적인 시각을 유지하기 어렵습니다. 하나의 프롬프트 안에서 계획, 실행, 검증을 모두 수행하려다 보면 역할 간의 경계가 모호해지고 결과물의 품질이 저하됩니다.
저는 최근 AI 엔지니어링에 관심을 가지며 얻게된 호기심 충족과 제 업무와 취미를 보조할 좀 더 완전한 AI를 만들어보고자

'다중 에이전트 시스템(Multi-Agent System, MAS)'을 설계했습니다.

 

이 글에서는 LangGraph 프레임워크를 기반으로 AI-native 검색 엔진인 Tavily, 그리고 Streamlit 웹 인터페이스를 결합하여 어떻게 이 시스템을 밑바닥부터 구축했는지 상세한 과정을 공유하고자 합니다.


시스템 아키텍처 및 핵심 도구

제가 구축한 시스템은 크게 4가지 역할을 가진 에이전트와 최종 결정권자인 사람(Human)으로 구성됩니다.
  • Planner (계획 수립자): 사용자의 요청을 분석하고 구체적인 실행 계획을 세웁니다.
  • Executor (실행자): 계획을 바탕으로 실제 코드 작성, 파일 시스템 접근, 검색 등의 도구를 사용해 작업을 수행합니다.
  • Skeptic (회의론자): Executor와 병렬로 실행되며, Executor의 결과물이 아닌 Planner의 계획 자체를 보고 독립적인 위협 모델과 검토 기준을 세웁니다.
  • Executive (관리자): 실행 로그와 리뷰 코멘트를 취합하여 최종 판단(승인, 수정, 거절)을 내립니다.
  • Human-in-the-Loop (사람): 최종 실행 전 개입하여 승인하거나 수정 지시를 내립니다.
이 아키텍처를 구현하기 위해 다음과 같은 핵심 기술 스택을 채택했습니다.
  • 오케스트레이션: LangGraph (에이전트 간의 상태와 흐름 제어)
  • 웹 인터페이스: Streamlit (기존 디스코드 봇의 메시지 제한 한계를 극복하기 위한 세련된 UI)
  • 검색 엔진: Tavily API (AI 에이전트에 최적화된 정보 검색 및 추출)
  • 코딩 보조 및 자동화: Claude Code CLI (터미널 기반의 에이전틱 코딩 도구로 전체 스캐폴딩 및 테스트 자동화 수행)

단계별 구축 프로세스 및 핵심 구현 로직

프로젝트는 복잡한 시스템을 한 번에 뭉뚱그려 짜는 대신, 총 9개의 페이즈(Phase)로 나누어 점증적으로 구축하고 검증하는 방식을 택했습니다

 

Phase 1 & 2: 기초 스캐폴딩과 상태(State) 설계

가장 먼저 uv를 사용해 가상환경을 초기화하고, LangGraph, LangChain, Pydantic 등의 필수 패키지를 설치했습니다. 모든 과정은 터미널에서 Claude Code CLI를 통해 자연어로 지시하여 자동화했습니다.
다중 에이전트 시스템에서 가장 중요한 것은 에이전트들이 정보를 주고받을 '칠판(State)'의 설계입니다. 특히 병렬로 실행되는 노드들이 동시에 값을 쓸 때 데이터가 덮어써지는(Overwrite) 문제를 막아야 했습니다.
class AgentState(TypedDict):
    # 누적 필드: 병렬 노드가 동시에 쓰더라도 덮어쓰지 않고 리스트에 추가됨
    messages: Annotated[list[BaseMessage], operator.add]
    review_comments: Annotated[list[str], operator.add]
    execution_logs: Annotated[list[str], operator.add]
    
    # 단일값 필드: 마지막으로 쓴 노드의 값이 최종값
    task: str
    current_agent: str
    executive_verdict: str

위 코드처럼 Annotated[list[str], operator.add]를 사용해 리스트가 안전하게 누적(Append)되도록 설계했습니다. 이 리듀서(Reducer) 기능이 없었다면 Skeptic의 비판적 리뷰가 Executor의 로그에 조용히 덮어써져 사라지는 문제를 겪었을 수도 있습니다.

 

 

 

Phase 3 & 4: 병렬 팬아웃(Fan-out) 설계

Planner의 계획이 수립된 후, Executor와 Skeptic 노드는 직렬이 아닌 병렬(Parallel)로 실행되도록 add_edge를 구성했습니다.

만약 직렬(Executor 완료 후 Skeptic 실행)로 구성했다면, Skeptic은 Executor의 결과물 프레임에 갇혀 수동적인 리뷰어에 머물렀을 것입니다. 두 노드를 병렬로 분기(Fan-out)시켜 동일한 계획을 보게 함으로써, Skeptic이 완전히 독립적인 시각에서 발생할 수 있는 문제점(위협 모델)을 사전에 짚어낼 수 있도록 의도했습니다.
 

 

Phase 5: 무한 루프 방지와 Executive 피드백 루프

Executor와 Skeptic의 작업이 끝나면 팬인(Fan-in)되어 Executive 노드로 모입니다. Executive는 두 결과를 분석하여 approve, revise, reject 중 하나를 판정합니다.
여기서 핵심은 자율성 시스템이 가진 '무한 루프'의 위험을 통제하는 것이었습니다.
if has_error:
    if revision_count >= MAX_REVISIONS: # 안전장치: 상한선 도달 시 강제 종료
        verdict = "reject"
    else:
        verdict = "revise"
        revision_count = revision_count + 1
위와 같이 상태(State)에 revision_count 필드를 두고 최대 3회까지만 수정을 허용하도록 안전장치를 구현했습니다. 이 제약이 없으면 Skeptic이 사소한 트집을 잡을 때마다 시스템이 무한히 재작업을 수행하여 엄청난 API 비용이 청구될 수 있습니다.
 

 

Phase 6: Docker 기반의 안전한 코드 실행 샌드박스

에이전트(Executor)에게 시스템 제어권을 부여하는 것은 매우 위험합니다. LLM이 생성한 코드가 제 로컬 파일 시스템을 망치거나, 보안을 우회할 수 있기 때문입니다. 이를 방지하기 위해 파이썬의 subprocess를 활용하여 Docker 컨테이너 내부에서만 코드가 실행되는 샌드박스 툴을 만들었습니다.
docker run --rm --network=none --memory=512m --cpus=1 python:3.11-slim python -c "실행할코드"
외부 해킹이나 악의적인 API 호출을 막기 위해 --network=none으로 네트워크를 원천 차단하고, 메모리 폭탄을 막기 위해 --memory=512m 제약을 걸었습니다.

 

Phase 7: 권한의 물리적 분리와 Tavily 지능형 검색

에이전트의 역할을 프롬프트(글)로만 나누는 것은 불완전합니다. 따라서 '도구(Tool) 바인딩'을 통해 권한을 물리적으로 분리했습니다.
  • Executor 툴: read_file, write_file, run_code_in_sandbox, web_search
  • Skeptic 툴: grep_logs (읽기 전용), web_search
Skeptic 에이전트에게는 의도적으로 파일 쓰기나 코드 실행 권한을 주지 않았습니다. 만약 부여했다면 검토자가 코드를 직접 수정해버리는 월권 행위가 발생하여 다중 에이전트의 밸런스가 무너졌을 것입니다.
특히 웹 검색 툴로는 Tavily를 선택했습니다. 전통적인 검색 API가 정리되지 않은 HTML이나 단순 링크만 반환하여 LLM의 토큰을 낭비하는 반면, Tavily는 에이전트 시스템을 위해 설계되어 복잡한 정보 추출과 정제된 클린 텍스트/마크다운을 제공해주어 RAG 및 에이전트 워크플로우에 최적화되어 있기 때문입니다.
 

 

Phase 8: Streamlit을 활용한 Human-in-the-Loop (인간 개입)

초기 계획은 디스코드 봇을 연동하는 것이었으나, 긴 코드 스니펫이나 리서치 결과를 2,000자 제한이 있는 메신저로 보는 것은 비효율적이었습니다. 그래서 Streamlit을 활용해 직관적인 웹 기반 상호작용 UI를 구축했습니다.
가장 중요한 기술적 성취는 LangGraph의 interrupt_before=["executive"] 설정을 통한 Human-in-the-Loop(HITL) 구현입니다. 시스템이 위험하거나 비가역적인 작업을 최종 승인하기 직전, 그래프 실행이 멈추고(Pause) Streamlit 화면에 Executor의 결과와 Skeptic의 비판적 리뷰가 나란히 렌더링됩니다.
사용자(저)는 이 리뷰를 읽고 UI의 [승인] 버튼을 누르거나 [수정 지시] 텍스트를 입력해 에이전트를 다시 루프에 태울 수 있습니다. 사용자의 피드백은 graph.update_state를 통해 반영되며, graph.invoke(None, config)가 호출되어 멈췄던 지점부터 시스템이 재개(Resume)됩니다.
 
 


프로젝트 회고 및 개선점(희망 사항)

1. 사람의 개입에서 완전한 A2A 시스템으로

이번 시스템에서 의도적으로 사람의 개입을 넣은 이유는 두 가지 이유가 있었습니다.

  •  AI의 추론 과정에서 제 의견을 주입하여 방향을 틀 수 있고, 완벽하다면 입력창을 비워 둔채 통과시켜 유연하게 저의 개입을 정하려고 했습니다.
  • 가장 큰 이유는 Claude API 가격이 너무 많이 나올까봐 였습니다... Executor(실행자)와 Skeptic(검토자) 사이에서 무한 루프를 돌며 API 토큰 비용을 기하급수적으로 소모할 수 있기 때문에 제어가 필요하다고 생각했습니다.

하지만 저의 기존 목표는 완전 자율형 A2A 였기 때문에 향후, 에이전트의 자체 검증 로직을 좀 더 고도화하여 사람의 개입이 없이 문제를 해결하는 아키텍처를 만들고자 합니다.

 

2. 에이전트 프레임워크 다원화 와 멀티 모델

이번 프로젝트에서는 에이전트 간의 '상태(State)'와 '제어 흐름(Control Flow)'을 명시적인 그래프 구조로 정밀하게 제어하기 위해 LangGraph를 채택했고 시스템의 모든 에이전트는 Claude 단일 모델을 사용하였습니다. 

 

그래서 첫 번째로는 정확히 동일한 조건과 목표(과제)를 부여한 상태에서 CrewAI AutoGen(AG2) 같은 다른 에이전트 프레임워크를 사용해 시스템을 재구축
LangGraph와 비교했을 때 개발자 경험(DX), 토큰 소모량, 결과물의 품질 면에서 어떤 차이를 보이는지 직접 벤치마킹 해보고 싶습니다.

 

두 번째는 Gemini 3.1 pro나 다른 오픈소스 모델 등 AI 모델들의 각기 다른 특화된 강점을 살려 멀티 모델 시스템을 구축할 예정입니다.

 

3. 홈 서버와 결합

현재는 로컬 환경에서 가동하여, 집에 있는 데스크탑에서만 사용이 가능하지만, 조금 더 고도화를 한 이후에 웹 페이지와 에이전트 시스템 전체를 제 홈 서버의 Proxmox VM에 배포하여 상시 가동을 하며, 외부에서도 핸드폰이나 노트북으로 사용할 수 있게 만들 예정입니다.

 

4. Claude Code와 NotebookLM을 통한 '프롬프트 엔지니어링'의 재발견

번 프로젝트의 뼈대를 세우고 문서화하는 과정에서 터미널 기반의 Claude Code와 구글의 NotebookLM을 처음으로 적극 활용해 보았습니다. 이 도구들이 프로젝트 스캐폴딩과 코드 실행을 자동화해주어 개발 속도가 비약적으로 상승했습니다.

 

또한, 프롬프트의 미세한 차이가 전체 시스템의 결과물에 얼마나 영향을 미치는가 를 느꼈습니다. 단순히 "~ 해 줘"라고 묻는 것을 넘어서 에 이전트의 페르소나, 사용 가능한 도구의 제약 조건 등을 어떻게 걸어두냐에 따라서 결과물이 많이 달랐습니다.

 

이후에도 계속해서 프롬프트 엔지니어링에 대해서 공부하고 제 방향에 맞는 프롬프트를 찾아볼 예정입니다.