멀티 에이전트 타운홀 시뮬레이션 구축하기
요약
Redis Streams를 메시지 버스로 활용하여 비동기 통신을 구현한 멀티 에이전트 타운홀 시뮬레이션 구축 가이드입니다. 슬라이딩 윈도우 방식의 컨텍스트 관리와 벡터 검색을 이용한 메모리 파이프라인 구축 방법을 다룹니다.
핵심 포인트
- Redis Streams를 활용한 확장 가능한 비동기 에이전트 통신 구현
- 슬라이딩 윈도우 기법을 통한 동적 컨텍스트 윈도우 관리
- Embeddings, Faiss, SQLite를 결합한 에이전트 메모리 파이프라인 구축
- Multi-Agent Debate(MAD) 프레임워크 기반의 출력 최적화
저는 멀티 에이전트 시스템 (multi-agent systems)이 실제로 어떻게 작동하는지 튜토리얼을 통해서가 아니라, 직접 구성 요소들을 연결하며 이해하고 싶었습니다. 그래서 세 명의 AI 캐릭터(프랑스 억양을 사용하는 시장 (mayor), 판사 (judge), 그리고 마을 주민 (villager))가 중세 타운홀에서 주제에 대해 토론하는 중세 타운홀 시뮬레이션을 구축했습니다. 이들은 모두 실시간으로 서로의 말을 들을 수 있으며 대화에 참여할 수 있습니다.
이 프로젝트는 비동기 에이전트 통신 (asynchronous agent communication)을 구현하는 방법, 변화하는 컨텍스트 윈도우 (context window)를 전달하는 방법, 벡터 검색 (vector search, 코사인 유사도 검색 (cosine similarity search))을 사용하여 메모리를 저장하고 검색하는 방법, 그리고 마지막으로 Multi-Agent Debate (MAD) 프레임워크를 기반으로 에이전트로부터 최상의 출력을 얻는 방법에 대해 제가 배운 교훈들을 다룹니다.
저장소는 여기에서 확인하실 수 있습니다.
아키텍처 (The Architecture)
어떻게 비동기 통신을 구현할까요?
요약 (TLDR); Redis Streams
에이전트들이 API를 통해 서로 직접 통신하게 만드는 것은 지루하고 확장성이 없습니다. 에이전트가 자신의 코멘트를 다른 모든 에이전트에게 보내야 하며, 이는 각 에이전트가 이미 다른 모든 엔티티를 알고 있고 그들과 어떻게 인터페이스해야 하는지 알아야 함을 의미합니다.
이를 해결하기 위해 메시지 버스 (message bus)가 필요합니다. 권한이 있는 어떤 엔티티든 언제든지 데이터를 푸시하고 읽을 수 있는 중앙 집중식 진실의 원천 (central source of truth)이 필요합니다. 우리의 사용 사례를 위해 저는 Redis Streams를 구현하기로 선택했습니다.
Redis Streams는 추가 전용 로그 (append-only log) 역할을 하는 Redis의 내장 데이터 구조입니다. 각 엔트리는 자동으로 생성된 타임스탬프 ID와 일련의 키-값 (key-value) 필드를 가집니다. 멀티 에이전트 작업에 유용한 점은 **컨슈머 그룹 (consumer groups)**입니다. 여러 독립적인 리더가 스트림 내에서 각자의 위치를 추적할 수 있으므로, 동일한 메시지가 중복 없이 모든 그룹에 전달됩니다.
결정적으로 Redis Streams는 로컬에 호스팅하기 쉽습니다.
어떻게 컨텍스트 윈도우를 확보할까요?
Redis Streams 설정이 완료되고 데이터를 삽입할 토픽(topic)을 결정했다면, 컨텍스트 윈도우 (context window)를 구현할 수 있습니다. 이는 에이전트의 프롬프트 (prompt)에 동적으로 공급할 수 있는 마지막 n개의 메시지로 구성된 슬라이딩 윈도우 (sliding window)입니다.
메모리 파이프라인 (Memory Pipeline): Embeddings → Faiss → SQLite
컨텍스트 윈도우는 훌륭하지만, 더 의미 있는 에이전트 개발을 가능하게 하지는 않습니다. 에이전트가 단순히 과거 n개의 메시지에 반응하는 대신, 과거의 경험이나 축적된 지식을 활용하여 답변을 풍부하게 만들 수 있습니다.
이를 위해서는 두 가지가 필요합니다: 메모리 삽입 (memory insertion)과 메모리 검색 (memory retrieval)입니다.
1단계: ChronicleAgent가 요약본 생성
전용 ChronicleAgent가 토론 에이전트들과 함께 실행됩니다. 이 에이전트는 Redis 스트림을 폴링 (polling)하며, 매 10개의 메시지마다 Mistral에게 간결한 요약을 생성하도록 요청합니다:
prompt = f"""
당신은 타운홀 미팅의 다음 이벤트들을 요약하는 임무를 맡은 AI 에이전트입니다.
...
이것이 바로 마을의 순환 메모리 (rolling memory)입니다 — 압축되어 있고, 검색 가능하며, 지속적입니다. 메모리 역할을 하나의 에이전트에게 전담시킴으로써 중복된 메모리나 개인적 편향 (personal biases)을 줄일 수 있습니다. 또한 쿼리 실행의 복잡성과 비용도 줄일 수 있습니다.
만약 에이전트마다 서로 다른 메모리 저장소를 가졌다면, 아마도 이 방식을 포기하고 대신 모든 에이전트가 각자의 메모리를 생성하도록 책임지게 만들고 싶었을 것입니다.
2단계: 요약본 임베딩 및 저장
각 요약본은 CPU에서 로컬로 실행되는 sentence-transformers (all-mpnet-base-v2)를 사용하여 768차원 벡터 (vector)로 변환됩니다. 요약 텍스트와 임베딩 (embedding)은 SQLite (신뢰할 수 있는 단일 원천, source of truth)에 저장되며, 벡터는 빠른 유사도 검색 (similarity search)을 위해 Faiss 인덱스 (index)에 저장됩니다:
embedding: np.ndarray = generate_embedding(summary)
embedding_bytes: bytes = embedding.tobytes()
...
3단계: 에이전트가 응답 전 관련 메모리 검색
에이전트가 응답해야 할 때, 최근 대화 컨텍스트 (context)를 임베딩하고 Faiss에 쿼리하여 가장 유사한 과거 요약본을 찾아냅니다:
memories = await asyncio.to_thread(retrieve, formatted_context, 2)
formatted_memories = self.format_memories(memories)
persona = self.generate_prompt(formatted_context, formatted_memories)
The retrieve() 함수는 전체 파이프라인을 연결합니다:
def retrieve(query: str, k: int) -> list[RetrievalResult]:
query_embedding = generate_embedding(query)
...
이는 마을 주민(Villager)이 이전 실행에서 발생했던 대화(
보시다시피 대화는 흥미롭지만 한계가 있습니다. 에이전트(Agent)들에게는 주제가 주어지며, 각 에이전트가 응답 횟수의 하드 리밋 (hard limit)에 도달할 때까지 해당 주제로 토론을 벌입니다. 대화는 종종 극적인 상황으로 치닫게 되며, 에이전트들은 모두가 죽임을 당할 것이라고 비명을 지를 때까지 상황을 악화시키기만 하는 것처럼 보입니다.
여기서 중요한 점은 제가 갈등 해결 메커니즘 (conflict resolution mechanism)을 추가하지 않았다는 것입니다. 에이전트들은 토론하도록 프롬프트 (prompt)를 받지만, 판사 (judge)는 결코 명령을 내려 대화를 종료할 수 없습니다.
한계점: 순환 토론 문제와 사고의 퇴화 (Degeneration of Thought)
제가 맞닥뜨린 명확한 한계는 다음과 같습니다. 메시지가 약 30개 정도 지나면, 대화는 루프 (loop)로 퇴화합니다:
- 주민 (Villager): "우리는 굶주리고 있어요!"
- 판사 (Judge): "영주들이 곡물을 독점하고 있습니다!"
- 시장 (Mayor): "하지만 우리는 그것을 나눠줄 여유가 없습니다!"
- 주민 (Villager): "우리는 굶주리고 있어요!"
- ...무한 반복
각 에이전트는 주제에 대한 자신의 우려 사항을 응답하도록 프롬프트됩니다. 이들에게 해결책을 제시하라고 지시하지는 않습니다. 또한, 합리적인 해결책이 식별되었는지 판단하거나 토론자들의 기여를 통해 해결책을 합성 (synthesize)할 수 있는 메커니즘도 마련되어 있지 않습니다.
설상가상으로, 검색된 메모리 (retrieved memories)가 이 루프를 강화합니다. 과거의 요약본들은 모두 "결론에 도달하지 못함; 추가 토론 필요"라고 말합니다. 매번 그런 일이 발생했기 때문입니다. 에이전트들은 이전에 결정을 내리는 데 실패했다는 사실을 문자 그대로 상기받은 뒤, 다시 더 많은 우려 사항을 표현하라는 요청을 받는 셈입니다.
이 문제에 대한 최선의 해결책을 찾기 위해 조사를 진행했고, 이것이 이미 잘 문서화된 문제라는 것을 발견했습니다.
**Liang et al. (2023)**은 이를 **"사고의 퇴화 (Degeneration of Thought)"**라고 부릅니다. LLM (Large Language Model)이 한 입장에 대해 확신을 갖게 되면, 자기 성찰 (self-reflection)로도 이를 깨뜨릴 수 없다는 것입니다. 그들의 Multi-Agent Debate (MAD) 프레임워크는 입장이 수렴될 때 토론을 종료하는 **"적응형 중단 (adaptive break)" 기능이 있는 판사 (judge)**를 도입하며, 진정한 불일치를 강제하는 별도의 찬성/반대 역할을 부여합니다.
그들은 토론을 라운드(round)별로 나누어 더욱 구조화하고, 판사(Judge)가 토론 과정에서 해결책을 식별할 수 있는 더 많은 기회를 제공합니다.
다음 단계 계획
저는 MAD(Multi-Agent Debate) 방식이 현재 문제에 대한 최선의 해결책이라고 믿으며, 다음과 같은 작업을 수행할 계획입니다:
- 토론자들에게 아이디어를 제안하도록 독려하는 프롬프트(prompt) 제공 (아마도 찬성 측과 반대 측을 나누어 진행)
- 합의(consensus)에 도달했는지 평가하고, 도달하지 못했다면 토론자들의 답변으로부터 합리적인 해결책을 추출할 수 있는지 판단하는 능력을 판사에게 부여
- 집계(aggregation)와 투표(voting)가 가능하도록 LLM을 위한 구조화된 출력(structured outputs) 추가
- 에이전트들이 서로 얼마나 자주 의견이 일치하거나 불일치하는지, 판사가 누구의 편을 더 자주 드는지, 그리고 기타 핵심 지표(core metrics)를 확인하기 위한 테스트 수행
핵심 요약 (Key Takeaways)
- 프롬프트가 행동의 한계치(behaviour ceiling)를 결정합니다. 모든 에이전트는 지시받은 대로 정확히 우려 사항을 표현했습니다. 아무리 아키텍처가 정교하더라도 잘못된 것을 요구하는 프롬프트를 극복할 수는 없습니다.
- 메모리는 실패를 강화할 수 있습니다. 만약 요약(summary)에 "합의에 도달하지 못함"이라고 기록되고 에이전트들이 해당 요약을 검색(retrieve)한다면, 당신은 결정을 내릴 수 없다는 사실을 기억하고 그에 따라 행동하는 시스템을 구축한 것입니다.
- 메시지 버스(message bus)가 기반입니다. 컨슈머 그룹(consumer groups)을 활용한 Redis Streams는 확인 응답(acknowledgment)이 포함된 팬아웃(fan-out) 메시징을 제공하여, 에이전트들을 서로 분리(decouple)하고 새로운 역할(role)을 추가하여 전체 시스템을 쉽게 확장할 수 있게 해주었습니다.
- 정규화된 내적(Normalised inner product) = 코사인 유사도(cosine similarity). 표준적인 기법이지만, 제1원리(first principles)부터 이해할 가치가 있습니다. 벡터를 Faiss
IndexFlatIP인덱스에 삽입하기 전에 정규화하면 코사인 유사도를 무료로 얻을 수 있습니다. - 문헌을 읽으세요. 사람들이 에이전트 간의 대화를 어떻게 개선하는지 찾아보던 중 MAD 프레임워크와 Generative Agents (Park et al) 등 다양한 연구 논문을 접하게 되었습니다. 이는 제가 이전에는 사용하지 않았을 새로운 정보원을 접하는 계기가 되었습니다.
이 논문들은 매우 훌륭하며, 앞으로 연구 논문 (research papers)을 활용해 보실 것을 강력히 추천합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기