ContextVar로 Multi-Agent 상태관리 하기
Multi-Agent 시스템에서 가장 까다로운 문제 중 하나가 Agent 간 상태 공유다. 특히 파킹통장 추천 AI 프로젝트에서는 QuestionAgent 내부의 5개 Tool이 순차적으로 실행되면서 특정 데이터에 선택적으로 접근해야 하는 복잡한 상황이 발생했다.
핵심 문제는 이렇다.
QuestionAgent Tool 체인
🔄 Tool 실행 순서 및 역할
1. ConditionExtractorTool
- 역할: 우대조건 및 금리정보 청크 데이터 추출
- 입력: EligibilityAgent에서 전달받은 적격 통장 목록
- 처리: MongoDB에서 각 통장의 우대조건 상세 정보 조회
- 출력: 구조화된 청크 데이터 (basic_rate_info, preferential_details)
2. PatternAnalyzerTool
- 역할: LLM 기반 우대조건 패턴 분석 및 RAG 쿼리 생성
- 입력: 추출된 청크 데이터
- 처리: 우대조건 텍스트를 분석하여 공통 패턴 식별
- 출력: 표준화된 패턴 목록 + RAG 검색용 쿼리
3. QuestionGeneratorTool
- 역할: 패턴 분석 결과 기반으로 RAG 검색하여 사용자 질문 생성
- 입력: 분석된 패턴과 RAG 쿼리
- 처리: RAG 시스템에서 관련 정보 검색 후 Yes/No 질문 생성
- 출력: 카테고리별 사용자 질문 리스트
4. UserInputTool
- 역할: 환경별 적응형 사용자 입력 처리 (콘솔/API 전환)
- 입력: 생성된 사용자 질문 리스트
- 처리: 테스트 모드에 따라 콘솔 입력 또는 API 응답 처리
- 출력: 사용자 응답 데이터 (질문별 Yes/No 답변)
5. ResponseFormatterTool
- 역할: QuestionAgent 최종 출력 포맷팅 (StrategyAgent 입력용)
- 입력: 사용자 응답 데이터
- 처리: Context에서 eligible_products와 user_conditions 조회
- 출력: QuestionSuccessResponse (다음 Agent로 전달할 표준 포맷)
🎯 핵심 문제점
Context 접근이 필요한 Tool
- ConditionExtractorTool: eligible_products 필요
- ResponseFormatterTool: eligible_products + user_conditions 필요
Context 접근이 불필요한 Tool
- PatternAnalyzerTool: 이전 Tool 결과만 사용
- QuestionGeneratorTool: 이전 Tool 결과만 사용
- UserInputTool: 이전 Tool 결과만 사용
기존 파이프라인 방식의 한계: 모든 Tool에 eligible_products와 user_conditions를 파라미터로 전달해야 하는데, 실제로는 2개 Tool만 필요함 → 불필요한 파라미터 전달과 복잡한 의존성 발생
파이프라인 구조상 모든 Tool에 데이터를 전달해야 하니 불필요한 파라미터 전달이 강제되고, Tool 간 의존성이 복잡하게 얽히면서 디버깅이 사실상 불가능한 상태가 되었다.
더 큰 문제는 멀티 세션 환경이다. 동시에 여러 사용자가 시스템을 사용할 때 각자의 eligible_products와 user_conditions 데이터가 섞이거나 덮어쓰여지는 상황이 발생했다.
수많은 구글링 결과 Python의 ContextVar를 도입하여 이 모든 문제를 구조적으로 해결했다.
문제 정의: 기존 접근법의 구조적 한계
시스템 구성
5개 Agent로 구성된 파이프라인
- EligibilityAgent: 자격 조건 필터링
- QuestionAgent: 우대조건 역질문 생성 <- 여기서 전역 데이터 필요함
- StrategyAgent: 전략 시나리오 생성
- ComparatorAgent: 전략 비교 분석
- FormatterAgent: 최종 출력 포맷팅
기존 방식의 실패 분석
문제코드: 파라미터 체인 전달
def execute_pipeline(user_input):
eligible_products = eligibility_agent.execute(user_input)
questions = question_agent.execute(eligible_products, user_input)
strategies = strategy_agent.execute(eligible_products, questions, user_input)
결과: Agent 증가 시 파라미터 폭증으로 유지보수성 급격히 저하
해결책: ContextVar 기반 상태 관리
선택 근거
ContextVar는 다음 요구사항을 완전히 충족했다
- Thread-safe 보장
- 비동기 환경 지원
- Context별 완전 격리
- 타입 안전성
핵심 구현
Context 설계의 핵심은 각 데이터 타입별로 독립적인 ContextVar 인스턴스를 생성하는 것이다. 각 ContextVar는 고유한 식별자와 기본값을 가지며, 타입 힌팅을 통해 런타임 안전성을 보장한다.
from contextvars import ContextVar
from typing import Any
from schemas.agent_responses import SimpleProduct
from schemas.eligibility_conditions import EligibilityConditions
class QuestionAgentContext:
def __init__(self):
# 각 ContextVar는 독립적인 네임스페이스를 가짐
self.eligible_products_ctx: ContextVar[list[SimpleProduct]] = ContextVar(
"eligible_products", default=[]
)
self.user_conditions_ctx: ContextVar[EligibilityConditions | None] = ContextVar(
"user_conditions", default=None
)
self.session_id_ctx: ContextVar[str] = ContextVar("session_id", default="")
def set_eligible_products(self, products: list[SimpleProduct]) -> None:
"""
EligibilityAgent에서 필터링된 통장 목록을 Context에 저장
이후 QuestionAgent의 모든 Tool에서 접근 가능해짐
"""
self.eligible_products_ctx.set(products)
def get_eligible_products(self) -> list[SimpleProduct]:
"""
현재 실행 컨텍스트의 통장 목록 조회
멀티 세션 환경에서도 각 세션의 데이터만 반환됨
"""
return self.eligible_products_ctx.get()
def set_user_conditions(self, conditions: EligibilityConditions) -> None:
"""
사용자 입력 조건을 Context에 저장
Tool 체인 전반에서 참조 가능
"""
self.user_conditions_ctx.set(conditions)
def get_user_conditions(self) -> EligibilityConditions | None:
"""
저장된 사용자 조건 조회
None 반환 시 조건이 설정되지 않은 상태
"""
return self.user_conditions_ctx.get()
Agent 통합
QuestionAgent 초기화 시점에 Context 인스턴스를 생성하고, 이를 모든 Tool에 전달한다. 이렇게 하면 각 Tool이 필요한 시점에 Context에서 데이터를 조회할 수 있다.
class QuestionAgent:
def __init__(self, llm: BaseLanguageModel, test_mode: bool = True):
# Agent별 독립적인 Context 생성
self.agent_ctx = QuestionAgentContext()
# 모든 Tool에 동일한 Context 인스턴스 전달
self.tools = QuestionTools.get_tools(llm, test_mode, self.agent_ctx)
Tool 레벨 데이터 접근
각 Tool은 초기화 시점에 Context 참조를 받아 저장하고, 실행 시점에 필요한 데이터를 Context에서 조회한다. 파라미터로 데이터를 전달받을 필요가 없어진다.
class ResponseFormatterTool:
def __init__(self, agent_context: QuestionAgentContext):
# Tool 생성 시 Context 참조 저장
self.context = agent_context
def _format_response(self, user_input_result: UserInputResult) -> QuestionSuccessResponse:
# 실행 시점에 Context에서 필요한 데이터 조회
# 다른 Tool의 파라미터와 무관하게 독립적으로 접근
eligible_products = self.context.get_eligible_products()
user_conditions = self.context.get_user_conditions()
return QuestionSuccessResponse(
result_products=eligible_products,
user_responses=user_input_result.responses,
user_conditions=user_conditions
)
기술적 분석: Thread-Safe 매커니즘
ContextVar 격리 원리
ContextVar의 핵심은 각 실행 컨텍스트(execution context)별로 독립적인 값을 유지한다는 점이다. 같은 ContextVar 인스턴스라도 다른 실행 컨텍스트에서는 완전히 다른 값을 가질 수 있다.
# 세션 A에서 실행
context.set_eligible_products([product_a1, product_a2])
# 세션 B에서 동시 실행
context.set_eligible_products([product_b1, product_b2])
# 각 세션에서 get 호출 시:
# 세션 A: [product_a1, product_a2] 반환
# 세션 B: [product_b1, product_b2] 반환
# 데이터 충돌 없음
이는 전역 변수나 클래스 속성과는 완전히 다른 동작이다. 전역 변수를 사용했다면 마지막에 설정된 값으로 모든 세션이 덮어쓰여졌을 것이다.
타입 안전성 구현
ContextVar 생성 시 Generic 타입을 명시하면 IDE와 타입 체커가 완전한 지원을 제공한다. 이는 런타임 에러를 개발 단계에서 사전 차단하는 효과가 있다.
# 타입 명시로 안전성 확보
self.eligible_products_ctx: ContextVar[list[SimpleProduct]] = ContextVar(
"eligible_products", default=[]
)
# IDE에서 자동완성과 타입 검증 지원
products = self.eligible_products_ctx.get() # list[SimpleProduct] 타입으로 추론
모니터링 및 디버깅
Context 상태를 실시간으로 추적할 수 있는 디버깅 메서드를 구현했다. 이는 개발 과정에서 데이터 흐름을 파악하고 문제를 진단하는 데 필수적이다.
def get_context_info(self) -> dict[str, Any]:
"""
현재 Context 상태 정보 조회
개발/디버깅 시 데이터 흐름 추적용
"""
return {
"eligible_products_count": len(self.eligible_products_ctx.get()),
"has_user_conditions": self.user_conditions_ctx.get() is not None,
"session_id": self.session_id_ctx.get(),
"context_status": "active" if self.session_id_ctx.get() else "empty",
}
실제 운영에서는 이 정보를 로깅하여 Agent 간 데이터 전달 상태를 모니터링한다.
성과 측정
정량적 개선 지표
동시 사용자 10명 환경 테스트 결과:
- 데이터 충돌: 0건 (이전 방식 대비 100% 개선)
- 메모리 사용량: 15% 감소
- 응답 시간: 2.3초 → 2.1초 (9% 개선)
- 코드 복잡도: Tool 간 파라미터 전달 완전 제거
아키텍처 개선
이전: Agent → Tool 파라미터 체인 (N² 복잡도) 현재: Context 중앙집중 관리 (O(1) 접근)
중앙 집중형 상태관리
ContextVar 기반 Multi-Agent 상태 관리는 복잡한 시스템에서 데이터 공유 문제의 근본적 해결책이다. Thread-safe 보장과 동시에 코드 복잡도를 대폭 줄일 수 있었고, 확장성 있는 아키텍처 구축이 가능했다. Multi-Agent 시스템 구축 시 기존 방식의 한계를 인식하고 있다면, ContextVar 도입을 적극 검토하라. 초기 설정 비용 대비 얻는 안정성과 성능 이득이 명확하다.
알파코캠퍼스에서 진행한 이 프로젝트에서 많은걸 배울수 있어서 소중한 시간이었다!
이 글은 실제 파킹통장 추천 AI 프로젝트를 진행하면서 겪은 시행착오를 바탕으로 작성되었습니다.
전체 코드는 GitHub에서 확인할 수 있습니다.
'📂 AI 실무 개발 > 트러블슈팅 일지' 카테고리의 다른 글
| Claude Code safe mode, 진짜 문제는 모델 밖에 있다 (0) | 2026.06.09 |
|---|