멀티 에이전트 시뮬레이션을 위한 RAG 변형 모델 설계: 설계 방식과 솔직한 트레이드오프
요약
멀티 에이전트 시뮬레이션 CivilizationOS를 위해 설계된 새로운 RAG 변형 모델 TCMF를 소개합니다. 단순 유사도 검색의 한계를 넘어 관련성, 최신성, 중요도를 결합한 다중 스트림 검색 방식을 제안합니다.
핵심 포인트
- 기존 코사인 유사도 기반 RAG의 인과관계 파악 한계 지적
- TCMF: 관련성, 최신성, 중요도를 결합한 점수 산출 방식
- CivilizationOS: 시민, 의회, LLM 라우터로 구성된 시뮬레이션 구조
- 에피소드 기억 관리를 위한 지수적 감쇠 및 정규화 적용
표준적인 RAG (Retrieval-Augmented Generation)는 정적인 지식 베이스(knowledge bases)에는 훌륭합니다. 문서를 임베딩(embed)하고, 쿼리(query)를 임베딩한 뒤, 코사인 유사도(cosine similarity)를 통해 상위 k개(top-k)를 반환합니다. 그것으로 충분하죠.
하지만 40명의 시민이 기억을 가지고 있고, 의회가 위기에 대해 심의하며, 과거의 결정이 미래로 파급되는 실행 중인 문명 속에 RAG를 배치한다면, 유사도(similarity)만으로는 금방 한계에 부딪힙니다.
문제는 간단합니다. 코사인 유사도는 지난달의 가뭄이 오늘날의 식량 폭동을 유발했다는 사실을 알지 못합니다. 3주 전에 비상 곡물 비축에 반대 투표를 했던 의회가 현재의 기근에 직접적인 책임이 있다는 것도 알지 못합니다. 유사도 기반 방식은 위기와 비슷하게 들리는 기억을 검색할 뿐, 위기로 이어진 기억을 검색하지 못합니다.
그 간극을 메우는 것이 제가 원했던 것입니다. 이 포스트에서는 제가 작업해 온 멀티 에이전트 시뮬레이션(multi-agent sim)인 CivilizationOS를 위해 구축한 검색(retrieval) 설계와 그에 따른 솔직한 트레이드오프(tradeoffs)에 대해 설명합니다.
컨텍스트: CivilizationOS란 무엇인가
CivilizationOS는 다음과 같은 멀티 에이전트 시뮬레이션(multi-agent simulation)입니다:
- 40명 이상의 시민이 시뮬레이션 틱(simulation ticks) 동안 거주하고, 일하며, 에피소드 기억(episodic memories)을 축적합니다 (AGORA 레이어)
- 전문 의회(군사, 보건, 재무, 상원)가 주입된 위기에 대해 심의합니다 (PANTHEON 레이어)
- 3단계 LLM 라우터(router)가 서로 다른 추론 부하를 처리합니다: 가벼운 호출을 위한 로컬 Ollama, 중간 단계의 Gemini Flash, 복잡한 심의를 위한 Claude Sonnet
예를 들어 80틱에 전염병이 발생하는 것과 같은 위기가 닥치면, 보건 의회는 심의를 해야 합니다. 컨텍스트(context)가 필요합니다. 문제는 '어떤 컨텍스트를, 어떤 순위로 배치할 것인가?'입니다.
순진한 답변은 "위기 질문을 임베딩하고, 유사한 상위 k개의 기억을 검색하라"는 것입니다. 더 나은 답변은 TCMF가 계산하는 방식입니다.
두 개의 스트림, 하나의 점수
TCMF는 두 개의 독립적인 정보 스트림을 융합합니다.
스트림 1: AGORA (에피소드적, 시민별)
이것은 Stanford 논문(Park et al., 2023)의 generative-agents 공식입니다. 각 시민은 타임스탬프가 찍힌 관찰 기록인 MemoryStream을 가집니다. 쿼리가 도착하면 모든 기억에 점수가 매겨집니다:
score = w_rel * relevance + w_rec * recency + w_imp * importance
- relevance (관련성): 쿼리 임베딩(query embedding)에 대한 메모리 임베딩(memory embedding)의 코사인 유사도(cosine similarity), [0, 1] 범위로 제한(clamped)
- recency (최신성): 메모리가 마지막으로 액세스된 이후 경과된 틱(ticks)에 따른 지수적 감쇠(exponential decay) -
exp(-decay * age) - importance (중요도): 1~10 사이의 강렬함 점수(LLM 또는 규칙에 의해 할당됨), [0, 1]로 정규화(normalized)
메모리를 검색(Retrieving)하면 해당 메모리의 last_access_tick이 업데이트됩니다. 이는 계속해서 드러나는 메모리들이 검색 풀(retrieval pool) 내에서 신선함을 유지한다는 것을 의미하며, 이는 매우 유용한 특성입니다. 즉, 진행 중인 위기에 관한 두드러진(salient) 메모리들이 지속적으로 유지됩니다.
def _recency(self, mem: Memory, now: int) -> float:
age = max(0, now - mem.last_access_tick)
return math.exp(-self.weights.decay * age)
...
이 방식 자체로도 견고합니다. 하지만 여전히 메모리가 얼마나 유사한지 또는 최신인지에 의해서만 순위가 매겨집니다. 인과관계(causality)에 대해서는 전혀 알지 못합니다.
스트림 2: PANTHEON (인과적, 사회 전체 규모)
NetworkX 유향 그래프(directed graph)가 문명적 규모에서 무엇이 무엇을 유발했는지를 추적합니다:
가뭄 (tick 20) -> 비상 배급 (tick 25) -> 암시장 급등 (tick 30) -> 시민 불안 (tick 45) -> 폭동 (tick 60)
노드(Nodes)는 사건(위기, 결정, 정책 결과)입니다. 유향 에지(Directed edges)는 인과적 선행 관계를 인코딩합니다. 에지 가중치(Edge weights)는 인과적 강도(0에서 1 사이)를 나타냅니다.
새로운 위기가 발생하면, TCMF는 위기 노드로부터 제한된 BFS(너비 우선 탐색)를 역방향으로 수행하여 인과적 조상(causal ancestors)을 찾습니다:
def predecessors(self, event_id: str, max_depth: int = 4) -> dict[str, int]:
visited: dict[str, int] = {}
queue: list[tuple[str, int]] = [(event_id, 0)]
...
깊이(Depth) 1은 직접적인 원인입니다. 깊이 4는 네 단계를 거슬러 올라간 것입니다. 결과물은 탐색 윈도우(lookback window) 내의 모든 인과적 조상에 대한 맵(map)입니다.
융합 공식 (The fusion formula)
위기 쿼리 q에 대해 점수가 매겨진 각 시민의 메모리 m에 대하여:
tcmf_score(m) = episodic_score(m, q) x (1 + lambda x causal_boost(m))
causal_boost(m)는 두 스트림이 연결되는 지점입니다. 그래프 내의 각 인과적 조상(causal ancestor)에 대해, TCMF는 메모리의 임베딩(embedding)과 조상의 임베딩 사이의 코사인 유사도(cosine similarity)를 계산합니다. 만약 해당 유사도가 임계값(기본값: 0.45)을 넘으면, 해당 메모리는 깊이 가중치 기반의 부스트(depth-weighted boost)를 받습니다:
def _causal_boost_for_memory(self, memory, ancestors, max_depth):
if not ancestors or memory.embedding is None:
return 0.0
...
직관적인 설명: 현재 위기의 _근본 원인(root cause)_에 직접 현장에 있었던 시민은, 비록 두 번째 시민의 메모리 텍스트가 위기 상황에 대한 설명과 더 유사하게 작성되었더라도 그보다 더 높은 순위를 차지합니다.
구체적인 예시
위기: "시장 구역의 전염병 발생"
순수 의미론적 RAG (Pure semantic RAG)의 경우 다음과 같은 결과가 나타납니다:
- "상인들이 우물 근처에서 이상 증상을 보고했다" - "전염병 발생"과 유사도가 높음
- "아이들이 아프고, 클리닉이 가득 찼다" - 유사도가 높음
- "2주 전 시가 격리 인프라 자금 지원을 거부했다" - 유사도가 낮아 순위가 낮음
격리 거부가 전염병 발생의 인과적 조상(causal ancestor)이라고 가정할 때, TCMF의 경우:
- "2주 전 시가 격리 인프라 자금 지원을 거부했다" - 인과적 부스트(causal boost)를 받아 순위가 상승함
- "상인들이 우물 근처에서 이상 증상을 보고했다" - 자체적인 가치에 따라 순위 결정
- "아이들이 아프고, 클리닉이 가득 찼다" - 동일함
이제 의회(council)의 컨텍스트에는 단순히 증상에 대한 설명뿐만 아니라, 전염병이 왜 그렇게 빠르게 퍼졌는지에 대한 _이유(reason)_가 포함됩니다. 이는 심의 과정을 변화시킵니다. 이것이 자초한 인프라 실패임을 인지하고 있는 의회는, 이를 무작위적인 발생으로 생각하는 의회와는 다른 정책을 권고할 것입니다.
의회 컨텍스트 블록(council context block)의 형태
검색(retrieval)이 끝나면, TCMF는 의회의 프롬프트(prompt)로 직접 전달될 구조화된 컨텍스트 블록을 구성합니다:
CRISIS: Plague outbreak in the market district
CITIZEN MEMORY EVIDENCE:
...
LLM은 두 가지를 전달받습니다: 순위가 매겨진 시민 기억 증거(citizen memory evidence)와 이곳에 이르게 된 과정을 보여주는 명시적인 인과 관계 체인(causal chain)입니다. 이를 통해 LLM은 컨텍스트를 단순히 유사한 문장들의 평면적인 집합(flat bag)으로 취급하는 대신, 두 가지 요소를 모두 바탕으로 추론할 수 있습니다.
전체 파이프라인 (The full pipeline)
async def retrieve(self, question, citizens, tick, institution_id,
crisis_event_id=None, k=12, router=None) -> TCMFContext:
...
구현 스택 (The implementation stack)
- NetworkX DiGraph: 인과 그래프(causal graph)를 위해 사용합니다. 자유로운 BFS/DFS, 에지 가중치(edge weights)를 지원하며 Python 네이티브입니다. 우리 규모에서는 별도의 그래프 데이터베이스가 필요하지 않습니다.
- NumPy 벡터 스토어 (vector store) (Chroma나 Pinecone 미사용): 에이전트가 약 10명이고 각 에이전트가 수백 개의 기억을 가지고 있는 상황에서는, 인메모리 행렬(in-memory matrix)에 대한 브루트 포스 코사인 유사도(brute-force cosine) 계산이 정확하고 빠릅니다. 쿼리당 단 한 번의 행렬-벡터 내적(matrix-vector dot)만 수행됩니다:
sims = matrix @ q_normalized. 데이터베이스를 임포트하는 대신 60줄의 코드를 직접 작성했으며, 이를 통해 점수 산정(scoring) 방식을 완전히 제어할 수 있습니다. - 전 과정에 Asyncio 적용: 임베딩(Embedding) 호출은 비동기(async)로 이루어지며, 검색(retrieval)은 논블로킹(non-blocking) 방식이고, 의회 오케스트레이션(council orchestration)은
await를 사용합니다. - 임베딩은 선택 사항: 임베딩을 사용할 수 없는 경우, 관련성 점수(relevance scores)는 0이 되며 공식은 최신성(recency) + 중요도(importance)로 대체됩니다. 시스템은 임베딩이 없는 모드에서도 중단 없이 테스트 및 실행이 가능합니다.
솔직한 트레이드오프 (The honest tradeoffs)
TCMF가 일반적인 에피소드 기반 RAG (episodic RAG) 대비 얻는 이점:
- 유사도만으로는 놓칠 수 있는 근본 원인(root-cause) 기억을 표면화합니다.
- 인과 관계 체인 요약은 LLM에게 추론할 수 있는 명시적인 역사적 구조를 제공합니다.
- 중복 제거(Deduplication)를 통해, 여러 시민이 우연히 동일한 기억을 보유하고 있더라도 단일 공유 기억이 전체를 지배하는 것을 방지합니다.
- 모든 단계에서 우아한 성능 저하(Graceful degradation)가 가능합니다: 임베딩이 없거나, 인과 그래프가 없거나, 위기 이벤트 ID가 없더라도 각 누락된 요소가 시스템을 중단시키지 않고 깔끔하게 성능만 낮추며 작동합니다.
TCMF의 비용:
인과 그래프를 유지 관리해야 합니다. 이벤트가 기록되어야 하고, 연결 관계(links)를 그려야 합니다. CivilizationOS에서는 시뮬레이션이 실행됨에 따라 위기 이벤트와 의회 결정이 자동으로 추가됩니다. 실제 시스템에서는 이벤트 로깅 파이프라인과 무엇이 무엇을 유발했는지 결정할 무언가가 필요할 것입니다.
auto_link_predecessors()는 명시적인 인과 관계(causal links)를 알 수 없는 경우를 처리합니다. 이 함수는 시간적 근접성(temporal proximity)과 의미적 유사성(semantic similarity)을 사용하여 약한 연결(weak links)을 추론합니다.
def auto_link_predecessors(self, new_event_id, window_ticks=48, semantic_threshold=0.5):
new_data = self._g.nodes[new_event_id]
new_tick = new_data["tick"]
...
하지만 추론된 인과 관계는 노이즈가 많습니다. "비슷한 시기에 발생했고 관련 있어 보이는 것들"은 "서로를 유발한 것들"에 대한 대리 지표(proxy)일 뿐입니다. 이는 희소한 그래프(sparse graph)를 채우는 데는 유용하지만, 명시적인 인과 모델링(explicit causal modeling)을 대체할 수는 없습니다.
또한 세 가지 조정 가능한 파라미터(tunable parameters)가 있습니다: causal_boost (lambda), causal_sim_threshold, 그리고 max_depth입니다. 이 값들을 잘못 설정하면 에피소드 신호(episodic signal)가 압도되거나 인과 부스트(causal boost)가 무의미해집니다. 기본값(lambda=0.6, threshold=0.45, depth=4)은 엄격한 스윕(sweep)을 통해 얻은 것이 아니라, 테스트 스위트를 실행하여 인과 부스트가 적용된 메모리가 관련 없는 메모리보다 상위에 랭크되는지 확인하여 결정되었습니다.
그리고 프로덕션 규모(production scale)에서는 밀집된 인과 그래프(dense causal graph)에 대한 BFS(너비 우선 탐색)가 지연 시간(latency)을 추가합니다. CivilizationOS의 현재 규모에서는 사소할 정도로 빠르지만, 규모가 커지면 실제적인 우려 사항이 됩니다.
TCMF와 일반 에피소드 RAG(plain episodic RAG) 중 언제 무엇을 사용할 것인가:
에이전트가 인과적으로 구조화된 환경(causally structured environment), 즉 과거의 이벤트가 후속 효과를 생성하고 그 체인이 의사 결정에 중요한 영향을 미치는 환경에서 작동할 때는 TCMF를 사용하십시오. 정적인 지식 베이스(static knowledge base)를 기반으로 지원 챗봇을 구축한다면 표준 RAG가 적절한 도구입니다. 만약 사물이 왜 발생했는지에 대해 추론해야 하는 에이전트를 구축하고 있다면, TCMF는 해당 컨텍스트를 프롬프트(prompt)에 넣는 하나의 방법이 될 수 있습니다.
v2에서 변경하고 싶은 점
부스트(boost)에 엣지 가중치(edge weights)를 사용하십시오. 현재 link()는 가중치를 저장하지만 _causal_boost_for_memory는 이를 무시합니다. 강력한 직접적 원인(weight=1.0)은 약한 추론된 연결(weight=0.3)보다 더 많이 기여해야 합니다. 해결 방법은 한 줄이면 됩니다: sim * normalized에 ev_weight를 곱하는 것입니다.
성찰(Reflection)로 생성된 메모리 추가. Stanford 논문의 에이전트들은 주기적으로 자신의 메모리를 "성찰(reflect)"하여, 개별적인 가공되지 않은 사건(raw events) 대신 "이번 달에 보건 분야에서 세 번의 위기를 목격했다"와 같은 더 높은 수준의 관찰(observations)을 생성합니다. CivilizationOS에 성찰을 추가하면 의회(councils)가 개별 사건뿐만 아니라 시간에 따른 패턴에 대해 추론할 수 있게 됩니다.
기관 간 인과 관계(Cross-institution causal links). 현재 기관 범위의 폴백(fallback) 방식은 최근 사건들을 고정된 약한 깊이(weak depth)로 추가합니다. 적절한 다기관 인과 그래프(multi-institution causal graph)는 재무부(Treasury)의 예산 결정이 어떻게 군사(Military) 대비 태세 위기로 이어지는지(cascade) 모델링할 것입니다. 그래프 구조는 이를 지원하지만, 검색(retrieval) 과정에서 아직 기관 간 조상(cross-institution ancestors)을 사용하지 않고 있을 뿐입니다.
소스 및 시사점
구현체는 CivilizationOS/api/memory/ 디렉토리 내 세 개의 파일에 걸쳐 존재합니다: tcmf.py (TCMFRetriever 및 TCMFContext), causal_graph.py (BFS 순회 및 자동 연결 기능이 포함된 CausalGraph), 그리고 stream.py (에피소드 점수 산정 공식이 포함된 MemoryStream). 전체 리포지토리: github.com/syzayd/CivilizationOS.
만약 여러분이 결정이 하류 효과(downstream effects)를 미치는 멀티 에이전트 시뮬레이션이나 에이전트 시스템(agentic systems)을 구축하고 있다면, 이 핵심 아이디어는 가져갈 만한 가치가 있습니다: 의미론적 유사성(semantic similarity)과 인과적 관련성(causal relevance)은 동일한 것이 아니며, 압박 속에서 결정을 내리는 에이전트에게는 그 차이가 매우 중요합니다.
에이전트 메모리나 인과적 검색(causal retrieval)에 대해 작업하고 계신다면, 어떻게 처리하고 계신지 진심으로 듣고 싶습니다. 여기에 답글을 남기거나 GitHub / LinkedIn에서 저를 찾아주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기