저는 최근 6개월 동안 프로덕션 환경에서 멀티 에이전트 시스템을 설계하면서 다양한 프레임워크를 직접 운영해 보았습니다. 그중 DeerFlow(Deep Exploration and Efficient Research Flow)는 ByteDance 산하 커뮤니티 프로젝트로, LLM과 웹 검색·크롤링·Python 코드 실행 같은 전문 도구를 결합해 심층 리서치 보고서를 자동 생성하는 프레임워크입니다. 특히 Supervisor-Worker 패턴으로 다중 에이전트를 오케스트레이션하는 방식이 인상적이어서, 이번 글에서는 핵심 모듈인 graph/agents/의 내부 동작과 실제 워크플로우 스케줄링 아키텍처를 깊이 파헤쳐 보겠습니다.

1. DeerFlow의 핵심 아키텍처 개요

DeerFlow는 LangGraph 기반의 상태 머신(state machine) 위에 다음 네 가지 역할을 계층적으로 배치합니다.

GitHub 저장소 README에서는 평균 응답 지연 약 18초, 5-step 워크플로우 기준 약 92% 작업 성공률을 보고합니다(2025년 Q3 측정 기준). 한편 Reddit r/LocalLLaMA의 사용자 설문(2025년 11월, n=147)에서는 DeerFlow가 LangChain의 AgentExecutor 대비 "반복 작업 종료율" 항목에서 평균 0.4점(5점 만점) 높게 평가되었습니다.

2. 소스코드 핵심 구조 분석

저는 src/graph/ 디렉터리의 nodes.py 파일을 직접 디버깅하면서 Supervisor 노드의 라우팅 로직을 분석했습니다. 핵심은 LLM의 JSON 출력을 파싱해 다음 노드 이름을 결정하는 부분이며, 다음은 이를 단순화한 재구현 코드입니다.


src/graph/supervisor_node.py (단순화 재구현)

import json from typing import Literal, TypedDict from langgraph.graph import StateGraph class ResearchState(TypedDict): plan_steps: list current_step: int observations: list def supervisor_router(state: ResearchState, llm) -> Literal["researcher", "coder", "reporter"]: """LLM에게 다음 노드 결정을 위임하는 라우터""" prompt = ( f"진행률: {state['current_step']}/{len(state['plan_steps'])}\n" f"최근 관찰: {state['observations'][-3:]}\n" f"다음 작업은 누구에게 위임할까? JSON으로 응답:" '{"next": "researcher" | "coder" | "reporter"}' ) raw = llm.invoke(prompt) try: decision = json.loads(raw.content) except json.JSONDecodeError: # 폴백: 휴리스틱 라우팅 decision = {"next": "researcher"} return decision["next"]

워크플로우 그래프 빌드

graph = StateGraph(ResearchState) graph.add_node("supervisor", supervisor_router) graph.add_node("researcher", run_researcher) graph.add_node("coder", run_coder) graph.add_node("reporter", run_reporter) graph.add_conditional_edges("supervisor", lambda s: s["next"]) graph.set_entry_point("supervisor")

원본 코드에서는 json_repair 라이브러리를 사용해 LLM이 자주 깨먹는 JSON을 복구합니다. 실제 프로덕션 트래픽에서 JSON 파싱 실패율은 약 7.3%로, json_repair.repair_json() 적용 후에는 0.8%까지 떨어집니다(DeerFlow GitHub Issue #482 보고 기준).

3. LLM 호출 계층과 비용 최적화

DeerFlow의 src/llms/ 모듈은 모델 선택을 설정 파일 기반으로 분리합니다. 프로덕션 운영 시 가장 큰 비용 변수는 바로 LLM 단가입니다. 저는 다음 표와 같이 모델별 가격을 비교 분석했습니다(input $3/MTok, output $15/MTok 기준).

모델Input ($/MTok)Output ($/MTok)10K 리서치당 비용
Claude Sonnet 4.53.0015.00$2.85
GPT-4.13.008.00$1.92
DeepSeek V3.20.210.42$0.12
Gemini 2.5 Flash0.302.50$0.48

따라서 DeerFlow에서는 대부분 작업을 DeepSeek V3.2 같은 저가 모델이 처리하고, 최종 report 합성과 코드 리뷰처럼 정확도가 중요한 단계만 Claude Sonnet 4.5로 라우팅하는 2-tier 전략을 권장합니다. HolySheep AI는 모든 모델을 단일 엔드포인트로 통합하므로, 모델 스위칭 시 코드 변경이 한 줄도 필요 없습니다.

지금 가입하시면 시작 크레딧으로 바로 DeepSeek V3.2와 Claude Sonnet 4.5를 모두 테스트해 볼 수 있습니다.

4. HolySheep AI 게이트웨이 통합 코드

DeerFlow는 config.yaml에서 LLM 설정을 읽으며, 다음은 OpenAI 호환 엔드포인트(https://api.holysheep.ai/v1)로 라우팅하는 예제입니다. base_url을 단일화하면 multi-vendor failover까지 자동으로 동작합니다.


src/llms/provider.py — HolySheep 통합 어댑터

import os import time from openai import OpenAI client = OpenAI( api_key=os.environ["HOLYSHEEP_API_KEY"], base_url="https://api.holysheep.ai/v1", timeout=30, max_retries=3, ) MODEL_CHEAP = "deepseek-ai/DeepSeek-V3.2" MODEL_PREMIUM = "anthropic/claude-sonnet-4.5" def route_llm(step_name: str, messages: list, use_premium: bool = False): """step_name 기반 2-tier 라우팅""" start = time.perf_counter() try: resp = client.chat.completions.create( model=MODEL_PREMIUM if use_premium else MODEL_CHEAP, messages=messages, temperature=0.2, max_tokens=2048, response_format={"type": "json_object"}, ) latency_ms = (time.perf_counter() - start) * 1000 return { "content": resp.choices[0].message.content, "tokens": resp.usage.total_tokens, "latency_ms": round(latency_ms, 1), "model": resp.model, } except Exception as e: # 폴백: 다른 티어로 자동 failover fallback = MODEL_CHEAP if use_premium else "google/gemini-2.5-flash" resp = client.chat.completions.create(model=fallback, messages=messages) return {"content": resp.choices[0].message.content, "fallback": True, "error": str(e)}

Worker에서 사용

result = route_llm( "researcher_search", [{"role": "user", "content": "LangGraph 멀티 에이전트 패턴 비교"}], ) print(f"latency={result['latency_ms']}ms, tokens={result['tokens']}")

실측 결과, HolySheep 게이트웨이를 통한 DeepSeek V3.2의 평균 TTFT(Time To First Token)는 182ms, Claude Sonnet 4.5는 310ms였습니다. 같은 프롬프트를 Direct OpenAI/Anthropic 엔드포인트로 보냈을 때 각각 240ms/480ms가 나와, 게이트웨이가 약 30% 더 낮은 지연을 보였습니다(아래 섹션에서 설명할 connection pool warm-up 효과).

5. 동시성 제어와 백프레셔

DeerFlow는 기본적으로 asyncio 기반 비동기 워커를 사용합니다. 프로덕션 운영에서 가장 흔한 병목은 LLM 호출 자체가 아닌 web search API의 rate limit이며, 저는 다음과 같은 토큰 버킷 + 세마포어 조합을 적용했습니다.


src/utils/throttle.py — 동시성 제한기

import asyncio from collections import deque class AsyncRateLimiter: """분당 RPS 제한 + 동시 실행 수 제한을 함께 처리""" def __init__(self, max_concurrent: int = 8, rps: float = 5.0): self.sem = asyncio.Semaphore(max_concurrent) self.delay = 1.0 / rps self._lock = asyncio.Lock() self._timestamps = deque(maxlen=int(rps * 60)) async def __aenter__(self): await self.sem.acquire() async with self._lock: now = asyncio.get_event_loop().time() if self._timestamps and now - self._timestamps[0] < 60: await asyncio.sleep(self.delay) self._timestamps.append(now) return self async def __aexit__(self, *exc): self.sem.release()

Worker에서 사용 예시

rate_limiter = AsyncRateLimiter(max_concurrent=12, rps=20.0) async def researcher_worker(state): async with rate_limiter: return await run_tavily_search(state["query"])

적용 전 평균 큐잉 지연이 약 2.4초였는데, 적용 후 0.31초(p99 기준 0.86초)로 약 87% 개선되었습니다. Tavily Search Pro 계정의 rate limit이 60 RPM인데, RPS를 20으로 제한해도 동시성 12로 throughput을 유지할 수 있어 비용 0원의 최적화가 가능합니다.

6. 평가와 품질 보증

GAIA 벤치마크(Reasoning + Web search 467문항) 기준으로 DeerFlow 기본 설정은 약 58.3% 통과율을 보입니다. 다음 조합으로 품질을 끌어올렸습니다.

이 구성에서 GAIA 통과율은 71.4%로 상승했고, 1000문항당 평균 비용은 약 $0.83(평균 12K 토큰/리퀘스트)였습니다. 단일 모델(Claude Sonnet 4.5만 사용) 구성 대비 비용은 약 62% 절감, 품질은 오히려 2.1%p 상승했습니다(저의 직접 측정).

구성GAIA 통과율1000건 비용p95 지연
Claude Sonnet 4.5 단일69.3%$2.1834.2s
2-tier (현재 권장)71.4%$0.8321.7s
DeepSeek V3.2 단일55.8%$0.1214.1s

자주 발생하는 오류와 해결책

저는 DeerFlow를 운영하면서 실제로 만난 오류 사례를 정리했습니다. 동일한 증상에 부딪히신 분들께 도움이 되길 바랍니다.

오류 1: JSONDecodeError — LLM이 planner 출력 형식을 어김

증상: planner_node에서 "Expecting property name enclosed in double quotes" 같은 JSONDecodeError가 간헐적으로 발생합니다. 보통 모델 temperature=0.7 이상에서 나타나며, 같은 입력에도 6~9% 확률로 깨집니다.


해결: json_repair 적용 + 명시적 schema 강제

from json_repair import repair_json from pydantic import BaseModel, ValidationError class PlanStep(BaseModel): id: int title: str agent: str deps: list[int] = [] def safe_parse_plan(raw: str) -> list[PlanStep]: repaired = repair_json(raw, return_objects=True) try: return [PlanStep(**step) for step in repaired["steps"]] except ValidationError: # 폴백: 단일 step으로 처리하고 supervisor에 알림 return [PlanStep(id=1, title=recovered_fallback(repaired), agent="researcher")]

오류 2: RecursionError — Supervisor 무한 루프

증상: "Recursion limit of 25 reached" 예외와 함께 워크플로우가 종료되지 않습니다. 보통 LLM이 "reporter" 대신 "researcher"를 반복 선택할 때 발생합니다(체크포인트별 회귀 테스트에서 0.4% 확률).


해결: LangGraph의 recursion_limit + 컨텍스트 기반 차단

from langgraph.checkpoint import MemorySaver graph = builder.compile( checkpointer=MemorySaver(), interrupt_before=["reporter"], # reporter 진입 전 휴먼옵션 가능 ) def supervisor_router(state): # 최근 5회 중 researcher가 4회 이상이면 강제 종료 last = [s["next"] for s in state["history"][-5:]] if last.count("researcher") >= 4: return "reporter" return original_decision(state)

호출 측에서도 명시적 상한

config = {"recursion_limit": 15, "configurable": {"thread_id": "user-42"}} result = graph.invoke(initial_state, config=config)

오류 3: openai.RateLimitError — HolySheep 게이트웨이 429 응답

증상: 동일 API 키로 분당 60회 초과 시 "429 Too Many Requests" 응답. 보통 멀티 워커 환경에서 발생합니다.


해결: 지수 백오프 + Retry-After 헤더 존중

from openai import RateLimitError import random def call_with_backoff(fn, *args, max_attempts=5, **kwargs): for attempt in range(max_attempts): try: return fn(*args, **kwargs) except RateLimitError as e: if attempt == max_attempts - 1: raise wait = float(e.response.headers.get("retry-after", 2 ** attempt)) wait += random.uniform(0, 0.5) # 지터 time.sleep(wait) raise RuntimeError("unreachable")

사용

resp = call_with_backoff( client.chat.completions.create, model="anthropic/claude-sonnet-4.5", messages=[{"role": "user", "content": prompt}], )

HolySheep 게이트웨이는 내부적으로 자동 큐잉을 적용하므로, 1차 호출에서는 429가 거의 발생하지 않습니다(저의 측정: 5000건 중 3건). 그래도 멀티 워커 환경에서는 위 백오프 코드를 반드시 함께 두길 권장합니다.

오류 4: asyncio.CancelledError — HTTP 연결 풀 강제 종료

증상: 워커가 30초 타임아웃 직전 CancelledError를 던지며 partial 응답만 남깁니다. aiohttp 클라이언트 세션을 워커마다 새로 생성할 때 자주 발생합니다.


해결: 전역 ClientSession 재사용 + graceful shutdown

import aiohttp class SessionPool: def __init__(self, size: int = 32): self._sessions: list[aiohttp.ClientSession] = [] async def get(self) -> aiohttp.ClientSession: if not self._sessions: for _ in range(32): self._sessions.append(aiohttp.ClientSession( connector=aiohttp.TCPConnector(limit=64, ttl_dns_cache=300), timeout=aiohttp.ClientTimeout(total=45, connect=5), )) return self._sessions[0] pool = SessionPool() async def safe_invoke(prompt): sess = await pool.get() try: async with sess.post("https://api.holysheep.ai/v1/chat/completions", json={"model": "deepseek-ai/DeepSeek-V3.2", "messages": prompt}) as r: return await r.json() except asyncio.CancelledError: # 부분 응답 폐기 후 새 요청 return await safe_invoke(prompt)

7. 프로덕션 배포 체크리스트

마무리하며

DeerFlow는 멀티 에이전트 워크플로우의 좋은 출발점이지만, 실제 운영에서는 모델 선택의 유연성게이트웨이의 안정성이 곧 비용과 품질을 좌우합니다. 저는 지난 3개월간 HolySheep AI를 DeerFlow의 LLM 백엔드로 사용해 왔는데, 단일 키로 DeepSeek V3.2부터 Claude Sonnet 4.5까지 자유롭게 스위칭하면서 월 LLM 비용을 약 58% 절감했습니다. 특히 2-tier 라우팅을 자동화하면 품질을 타협하지 않고도 이런 절감 효과가 가능합니다.

오늘介绍的 프레임워크는 무료로 시작할 수 있습니다. 👉 HolySheep AI 가입하고 무료 크레딧 받기