Langchain을 이용한 Multi-agent system 설계 방법 리서치

Multi-agent system

랭체인에서의 Multi-agent system

다중 에이전트 시스템(Multi-agent system)은 복잡한 작업을 여러 개의 전문화된 에이전트로 나누어 문제를 처리하는 것. 하나의 에이전트가 모든 단계를 처리하는 대신, 다중 에이전트 아키텍처는 작고 전문적인 에이전트들을 종합하여 워크플로우를 구성한다.

다중 에이전트가 유용한 경우:

  • 하나의 에이전트에 너무 많은 도구가 주어져 적절한 선택을 못 하는 경우
  • 컨텍스트나 메모리 소요가 너무 커 한 에이전트가 효율적으로 추적하기 어려운 경우
  • 작업에 전문화가 필요한 경우(예: 플래너, 연구, 수학 전문가)

다중 에이전트 패턴

패턴 동작 방식 제어 흐름 사용례
도구 호출 Tool Calling 관리자(supervisor) 에이전트가 다른 에이전트를 "도구"처럼 호출. "도구" 에이전트는 사용자와 직접 대화하지 않고 결과만 반환. 중앙집중식: 모든 라우팅이 호출 에이전트를 통과 작업 편성, 구조화된 워크플로우
핸드오프 Handoffs 현재 에이전트가 다른 에이전트로 제어를 넘김. 활성화된 에이전트가 바뀌며, 사용자는 새 에이전트와 직접 상호작용. 탈중앙화: 에이전트가 활성 주체를 변경 다중 도메인 대화, 전문가 인수

패턴 선택 가이드

  • 워크플로우에 대한 중앙집중 제어가 필요하면: Tool Calling이 적합
  • 에이전트가 사용자와 직접 상호작용해야 하면: Handoffs가 적합
  • 전문 에이전트 사이에 복잡한 대화가 필요하면: Handoffs가 강점

둘을 혼합할 수도 있음. 에이전트 전환에는 핸드오프, 각 에이전트 내부의 특화 작업에는 툴 호출을 사용하는 식.


에이전트 컨텍스트 커스토마이징

다중 에이전트 설계의 핵심은 컨텍스트 엔지니어링. 컨텍스트 엔지니어링이란 각 에이전트가 볼 정보 범위를 정하는 것을 말한다. 랭체인에서는 아래와 같은 사항들을 개발자가 통제할 수 있음

  • 어떤 대화나 상태를 어떤 에이전트에 전달할지
  • 서브에이전트 전용 프롬프트
  • 중간 추론 포함 여부
  • 에이전트별 입력과 출력 양식

시스템의 성능은 컨텍스트 설계에 크게 좌우됨. 각 에이전트가 도구 역할이든, 활성화된 에이전트든 자신의 작업을 수행하는 데 필요한 정확한 데이터를 보도록 하는 것이 우리의 목표.


핸드오프 Handoffs

에이전트 사이에 통제권이 직접 넘어가는 구조. 활성화된 에이전트가 바뀌면 사용자는 새 에이전트와 직접 상호작용.

image.png

흐름:

  1. 현재 에이전트가 다른 에이전트의 도움이 필요하다고 판단
  2. 상태와 함께 다음 에이전트로 통제권을 전달
  3. 새 에이전트가 사용자를 직접 상대하며, 다시 핸드오프하거나 종료

핸드오프는 구현 파트가 아직 작성되지 않음


도구 호출 (Tool Calling)

툴 호출에서는 하나의 컨트롤러 에이전트가 다른 에이전트를 도구로 호출함. 컨트롤러는 작업 편성(orchestration)을 담당하고, 도구 에이전트는 특정 작업을 수행해 결과를 반환.

image.png

흐름:

  1. 컨트롤러가 입력을 받고 호출할 도구(서브에이전트)를 결정
  2. 도구 에이전트가 지시에 따라 작업을 수행
  3. 도구 에이전트가 결과를 반환
  4. 컨트롤러가 다음 단계를 결정하거나 종료

도구로 사용되는 에이전트는 일반적으로 사용자와 대화하지 않는다. 사용자와의 대화가 필요하면 핸드오프 방식을 사용

Python 구현

아래는 메인 에이전트가 도구 정의를 통해서 하나의 서브에이전트에 접근하는 예시

from [langchain.tools](http://langchain.tools) import tool
from langchain.agents import create_agent

subagent1 = create_agent(model="...", tools=[...])

@tool(
    "subagent1_name",
    description="subagent1_description"
)
def call_subagent1(query: str):
    result = subagent1.invoke({
        "messages": [{"role": "user", "content": query}]
    })
    return result["messages"][-1].content

agent = create_agent(model="...", tools=[call_subagent1])

이 방식에서:

  1. 주어진 작업이 서브에이전트 설명("subagent1_description")에 부합한다고 판단되면, 메인 에이전트는 call_subagent1을 호출.
  2. 서브에이전트는 독립적으로 실행되어 결과를 반환.
  3. 메인 에이전트는 결과를 받아 다음 단계를 진행.

커스토마이징 할 수 있는 부분

  1. 서브에이전트 이름("subagent1_name"): 프롬프트에 영향을 주므로 이름을 잘 고르기를 권장.
  2. 서브에이전트 설명("subagent1_description"): 메인 에이전트가 언제 호출할지 판단하는 핵심 단서. 메인 에이전트가 서브에이전트를 호출하는 것에 직접적인 영향을 끼침
  3. 서브에이전트 인풋: 위의 예시처럼 메인 에이전트가 생성한 query를 그대로 넘겨도 되지만, 서브에이전트가 작업을 해석하기 용이하도록 적절한 가공도 가능.
  4. 서브에이전트 아웃풋: 위의 예시처럼 최종 텍스트만 반환해도 되지만, 메인 에이전트의 이해를 도울 수 있는 상태/메타데이터를 함께 반환하도록 조정 가능.

서브에이전트에 전달하는 인풋 조정

메인 에이전트에서 서브 에이전트로 인풋을 넘길 때, 다음과 같은 2가지 지점에서 통제를 가할 수 있다

  • 프롬프트 수정: 메인 프롬프트나 도구 메타데이터(이름, 설명)를 조정해 호출 타이밍과 방식을 유도
  • 컨텍스트 주입: 대화 이력, 선행 결과, 작업 메타데이터 등을 고정된 프롬프트에서는 얻어내기 힘든 정보를 컨텍스트로 추가
from langchain.agents import AgentState
from [langchain.tools](http://langchain.tools) import tool, ToolRuntime

class CustomState(AgentState):
    example_state_key: str

@tool(
    "subagent1_name",
    description="subagent1_description"
)
def call_subagent1(query: str, runtime: ToolRuntime[None, CustomState]):
    # 쿼리 메세지를 적절한 인풋으로 수정하는 어떤 로직
    subagent_input = some_logic(query, runtime.state["messages"])
    result = subagent1.invoke({
        "messages": subagent_input,
        # 필요한 경우 다른 상태 키도 전달 가능
        # 상태 키의 경우 메인 에이전트와 서브에이전트의 스키마에 추가하는 것 잊지 말 것
        "example_state_key": runtime.state["example_state_key"]
    })
    return result["messages"][-1].content

서브에이전트의 아웃풋 조정

서브에이전트의 아웃풋을 메인 에이전트에게 넘겨줄 때, 두 가지 전략을 사용한다

  • 프롬프트 수정으로 최종 메시지에 필요한 정보가 누락되지 않게 요구
    • 아웃풋이 불완전하거나, 너무 길거나, 중요한 세부사항을 놓치기 쉬울 때 사용
    • 예를 들어, 서브 에이전트가 도구를 호출하거나 추론을 한 뒤에 정작 메시지에 결과값을 포함시키지 않는 경우가 있음
  • 커스텀 아웃풋 포맷팅으로 텍스트 외 상태 키를 함께 반환하도록 감싸기
    • 예를 들어, 최종 메시지와 함께 특정한 상태값을 반환하도록 함
    • 랭체인에서는 Command라는 구조를 통해 state와 메시지를 함께 반환할 수 있음
from typing import Annotated
from langchain.agents import AgentState
from [langchain.tools](http://langchain.tools) import InjectedToolCallId
from langgraph.types import Command

@tool(
    "subagent1_name",
    description="subagent1_description"
)
# 서브에이전트가 도구 호출 결과로 응답할 수 있도록 tool_call_id 전달

def call_subagent1(
    query: str,
    tool_call_id: Annotated[str, InjectedToolCallId],
    # 텍스트 이상의 내용을 반환하려면 Command 객체를 반환해야 함
) -> Command:
    result = subagent1.invoke({
        "messages": [{"role": "user", "content": query}]
    })
    return Command(update={
        # 메인 에이전트에 반환할 상태 키 예시
        "example_state_key": result["example_state_key"],
        "messages": [
            ToolMessage(
                content=result["messages"][-1].content,
                # 올바른 도구 호출과 매칭을 위해 tool_call_id를 포함
                tool_call_id=tool_call_id
            )
        ]
    })

Multi-agent Debate Strategy (정리 중…)

여러 개의 에이전트들이 토론과 합의를 통해 결정을 내리는 전략.

일종의 '집단 지혜'로, 개별 LLM이 가질 수 있는 무작위성이나 환각 현상이 완화되며, 합의 값의 분산(Variance)이 감소하고 시스템의 결과가 초기 평균값에 더 가까워지는 등 결과의 안정성이 높아짐

핵심 원칙과 아키텍처 패턴

MAD 시스템은 크게 3개의 구성 요소로 이루어진다

  • 에이전트(토론자): 둘 이상의 LLM 에이전트가 독립적으로 주장이나 해법을 생성하고, 반복적으로 서로의 출력을 비평하며 개선. 역할은 찬성/반대, 천사/악마 페르소나, 도메인 전문가 등으로 명시될 수 있습니다.
  • 심판/중재자: 토론을 관리하며 정답성 평가, 최종 해법 추출, 불일치가 계속될 시 판정
  • 상호작용 프로토콜: 순차 교대, 비동기 동시 상호작용, 혹은 찬반·배우-비평가 등의 규칙. 알고리즘은 라운드별로 토론 기록을 갱신하며 적응적 정지 조건에 도달할 때까지 반복

다양한 의견과 교정적인 추론 과정

MAD에서는 다양한 의견과 robust한 에러 교정이 필요.

  • 티키타카식(tit-for-tat) 불일치 전략: 의도적이되 통제된 수준의 불일치를 유도. 서로 다른 추론 경로를 탐색하여 편향을 교정
  • 에이전트 이기종성(Agent Heterogeneity): 서로 다른 기반 모델이나 아키텍처를 조합하면 동종 조합 대비 정확도가 크게 향상하고, 교사‑학생과 같은 상호작용이 나타남.
  • 외부 지식 통합: MADKE 같은 프레임워크는 위키피디아·검색 등 외부 증거를 검색·공유하고, 각 에이전트가 증거 섭취를 적응적으로 선택해 다단계 추론의 일관성을 높임.
  • 점진적 경계와 역할 스펙트럼: 위험 태도를 저경계↔고경계로 점진 배치하고, 간헐적 상호교신을 더해 유용성과 안전성의 균형을 개선합니다.

토론 동역학, 토폴로지, 효율성

  • 희소 통신 토폴로지: 완전 연결 대신 이웃 연결 등 희소 구조를 사용해 누가 누구의 출력을 받는지 제한함으로써 컨텍스트 길이와 토큰 비용을 줄이면서도 정확도를 유지합니다.
  • 동적 토론 그래프: 신경과학 영감을 받은 CortexDebate는 이전 라운드에 가장 유익했던 상호작용만 유지하도록 그래프를 재구성하며, 신뢰 공식을 통해 최적화합니다.
  • 희소화·조건부 참여(S²‑MAD): 유사도 계산과 중복 제거, 선택적 참여를 통해 불필요한 교환을 걸러내 토큰 비용을 크게 절감하면서 정확도 손실을 최소화합니다.[1]

안전성, 정렬, 적대적 강건성

  • 안전 정렬: 에이전트 간 상호 검토와 레드팀식 토론으로 안전성 이슈를 자가 식별·완화할 수 있으며, 단기·장기 메모리 결합 시 세션 간 안전 피드백 전이가 가능합니다.
  • 취약점: 역할 기반 다중 라운드 대화 구조는 탈옥 공격 표면을 넓힐 수 있어, 해로움 증폭과 높은 공격 성공률이 보고됩니다. 대응으로 토론 중 모니터링, 앙상블 가드레일, 프롬프트 보정이 권장됩니다.[

TradingAgents에서는…

TradingAgents에서는 다음과 같은 역할들이 존재

  • Analyst team: 데이터 수집 및 1차 분석을 담당하는 전문가 그룹. Fundamentals, Sentiment, News, Technical analyst의 4가지 종류
  • Researcher team: 분석가 팀의 보고서를 비판적으로 검토하고, 투자 가설을 심화시키는 역할. Bullish와 Bearish researcher가 토론
  • Trader agent: 분석가 팀의 보고서와 연구자 팀의 토론을 바탕으로 최종적인 거래 제안
  • Risk Manager: 트레이더 에이전트의 거래에 대한 위험을 평가하여, 최종 승인을 내림
def research_manager_node(state) -> dict:
        history = state["investment_debate_state"].get("history", "")
        market_research_report = state["market_report"]
        sentiment_report = state["sentiment_report"]
        news_report = state["news_report"]
        fundamentals_report = state["fundamentals_report"]

        investment_debate_state = state["investment_debate_state"]

        curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
        past_memories = memory.get_memories(curr_situation, n_matches=2)

        past_memory_str = ""
        for i, rec in enumerate(past_memories, 1):
            past_memory_str += rec["recommendation"] + "\n\n"

        prompt = f"""As the portfolio manager and debate facilitator, your role is to critically evaluate this round of debate and make a definitive decision: align with the bear analyst, the bull analyst, or choose Hold only if it is strongly justified based on the arguments presented.

                                           Summarize the key points from both sides concisely, focusing on the most compelling evidence or reasoning. Your recommendation—Buy, Sell, or Hold—must be clear and actionable. Avoid defaulting to Hold simply because both sides have valid points; commit to a stance grounded in the debate's strongest arguments.
                                           
                                           Additionally, develop a detailed investment plan for the trader. This should include:
                                           
                                           Your Recommendation: A decisive stance supported by the most convincing arguments.
                                           Rationale: An explanation of why these arguments lead to your conclusion.
                                           Strategic Actions: Concrete steps for implementing the recommendation.
                                           Take into account your past mistakes on similar situations. Use these insights to refine your decision-making and ensure you are learning and improving. Present your analysis conversationally, as if speaking naturally, without special formatting. 
                                           
                                           Here are your past reflections on mistakes:
                                           \"{past_memory_str}\"
                                           
                                           Here is the debate:
                                           Debate History:
                                           {history}"""
        response = llm.invoke(prompt)

        new_investment_debate_state = {
            "judge_decision": response.content,
            "history": investment_debate_state.get("history", ""),
            "bear_history": investment_debate_state.get("bear_history", ""),
            "bull_history": investment_debate_state.get("bull_history", ""),
            "current_response": response.content,
            "count": investment_debate_state["count"],
        }

        return {
            "investment_debate_state": new_investment_debate_state,
            "investment_plan": response.content,
        }

image.png


참고 자료

Multi-Agent Debate Strategies

TauricResearch/TradingAgents | DeepWiki

Multi-agent - Docs by LangChain