저는 최근 6개월 동안 프로덕션 환경에서 멀티 에이전트 시스템을 설계하면서 다양한 프레임워크를 직접 운영해 보았습니다. 그중 DeerFlow(Deep Exploration and Efficient Research Flow)는 ByteDance 산하 커뮤니티 프로젝트로, LLM과 웹 검색·크롤링·Python 코드 실행 같은 전문 도구를 결합해 심층 리서치 보고서를 자동 생성하는 프레임워크입니다. 특히 Supervisor-Worker 패턴으로 다중 에이전트를 오케스트레이션하는 방식이 인상적이어서, 이번 글에서는 핵심 모듈인 graph/와 agents/의 내부 동작과 실제 워크플로우 스케줄링 아키텍처를 깊이 파헤쳐 보겠습니다.
1. DeerFlow의 핵심 아키텍처 개요
DeerFlow는 LangGraph 기반의 상태 머신(state machine) 위에 다음 네 가지 역할을 계층적으로 배치합니다.
- Coordinator: 사용자 질의 의도 분류와 작업 라우팅
- Planner: LLM을 호출해 실행 가능한 research plan 생성
- Supervisor: 노드 간 상태 전이 및 재계획(re-planning) 결정
- Worker Team: Researcher(웹 검색), Coder(Python 실행), Reporter(리포트 합성)
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.5 | 3.00 | 15.00 | $2.85 |
| GPT-4.1 | 3.00 | 8.00 | $1.92 |
| DeepSeek V3.2 | 0.21 | 0.42 | $0.12 |
| Gemini 2.5 Flash | 0.30 | 2.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% 통과율을 보입니다. 다음 조합으로 품질을 끌어올렸습니다.
- Planner: Claude Sonnet 4.5
- Researcher Worker: DeepSeek V3.2 + Tavily Hybrid Search
- Coder: Claude Sonnet 4.5 (정확도 우선)
- Reporter: Claude Sonnet 4.5 (출력 품질 우선)
이 구성에서 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.18 | 34.2s |
| 2-tier (현재 권장) | 71.4% | $0.83 | 21.7s |
| DeepSeek V3.2 단일 | 55.8% | $0.12 | 14.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. 프로덕션 배포 체크리스트
- 🔍 관측성: LangSmith + OpenTelemetry 트레이싱 연결
- 💸 비용 가드: 일일 토큰 상한 5M 설정, 80% 도달 시 Slack 알림
- 🔁 멱등성:
thread_id를 사용자 ID 기반으로 결정론적 생성 - 🧪 회귀 테스트: 50문항 골든셋으로 주 1회 CI 실행
- 🛡️ 프롬프트 인젝션 방어: 검색 결과 → Reporter 입력 직전 sanitize
마무리하며
DeerFlow는 멀티 에이전트 워크플로우의 좋은 출발점이지만, 실제 운영에서는 모델 선택의 유연성과 게이트웨이의 안정성이 곧 비용과 품질을 좌우합니다. 저는 지난 3개월간 HolySheep AI를 DeerFlow의 LLM 백엔드로 사용해 왔는데, 단일 키로 DeepSeek V3.2부터 Claude Sonnet 4.5까지 자유롭게 스위칭하면서 월 LLM 비용을 약 58% 절감했습니다. 특히 2-tier 라우팅을 자동화하면 품질을 타협하지 않고도 이런 절감 효과가 가능합니다.
오늘介绍的 프레임워크는 무료로 시작할 수 있습니다. 👉 HolySheep AI 가입하고 무료 크레딧 받기