유스케이스 평가 (Evaluating Use Cases)¶
CrewAI로 AI 애플리케이션을 만들다 보면 가장 먼저 마주치는 결정이 있어요. 지금 만들려는 것에 Crew를 쓸지, Flow를 쓸지, 아니면 둘을 합칠지 고르는 거죠. 정답은 요구사항을 얼마나 정확히 파악했느냐에서 나와요. 이 가이드는 그 요구사항을 평가해서 합리적인 아키텍처를 고르는 판단 기준을 정리한 거예요. 판단의 중심에는 애플리케이션의 복잡성(complexity) 과 정밀도(precision) 사이의 관계를 이해하는 게 자리해요.
복잡성-정밀도 매트릭스 이해하기¶
CrewAI 애플리케이션을 설계할 때, 각 접근 방식이 서로 다른 요구사항에 어떻게 맞아떨어지는지를 보여주는 매트릭스가 있어요. 네 구간이 각각 무엇을 뜻하는지, 그리고 아키텍처 선택을 어떻게 안내하는지 함께 볼게요.
복잡성(Complexity)이란¶
CrewAI 맥락에서 복잡성은 이런 요소들을 가리켜요.
- 필요한 고유 단계(steps)나 연산의 수
- 수행해야 할 작업(task)의 다양성
- 서로 다른 컴포넌트 사이의 의존 관계
- 조건부 로직과 분기(branching)의 필요성
- 전체 워크플로의 정교함
정밀도(Precision)란¶
정밀도는 이쪽을 가리켜요.
- 최종 출력에 요구되는 정확도
- 구조화되고 예측 가능한 결과의 필요성
- 재현 가능성(reproducibility) 의 중요성
- 각 단계를 통제해야 하는 수준
- 출력 변동에 대한 허용 범위
네 구간 (The Four Quadrants)¶
1. 낮은 복잡성, 낮은 정밀도¶
특징:
- 단순하고 직관적인 작업
- 출력에 어느 정도 변동이 생겨도 괜찮음
- 단계 수가 제한적
- 창의적이거나 탐색적인 용도
권장 접근 방식: 최소한의 에이전트로 구성한 단순한 Crew
사용 예시:
- 기본 콘텐츠 생성
- 아이디어 브레인스토밍
- 단순 요약 작업
- 창의적인 글쓰기 지원
2. 낮은 복잡성, 높은 정밀도¶
특징:
- 정확하고 구조화된 출력이 필요한 단순 워크플로
- 재현 가능한 결과가 필요
- 단계는 적지만 높은 정확도 요구
- 데이터 처리나 변환과 관련된 경우가 많음
권장 접근 방식: 직접 LLM 호출을 쓰는 Flow, 또는 구조화된 출력을 내는 단순한 Crew
사용 예시:
- 데이터 추출 및 변환
- 폼 작성과 검증
- 구조화된 콘텐츠 생성 (JSON, XML)
- 단순 분류 작업
3. 높은 복잡성, 낮은 정밀도¶
특징:
- 여러 단계로 이뤄진 다단계 프로세스
- 창의적이거나 탐색적인 출력
- 컴포넌트 사이의 복잡한 상호작용
- 최종 결과의 변동을 어느 정도 허용
권장 접근 방식: 여러 전문 에이전트로 구성한 복잡한 Crew
사용 예시:
- 리서치와 분석
- 콘텐츠 제작 파이프라인
- 탐색적 데이터 분석
- 창의적인 문제 해결
4. 높은 복잡성, 높은 정밀도¶
특징:
- 구조화된 출력이 필요한 복잡한 워크플로
- 엄격한 정확도 요구와 함께 얽힌 여러 의존 단계
- 정교한 처리와 정밀한 결과가 모두 필요
- 보통 미션 크리티컬한 애플리케이션
권장 접근 방식: 여러 Crew를 검증 단계와 함께 오케스트레이션하는 Flow
사용 예시:
- 엔터프라이즈 의사결정 지원 시스템
- 복잡한 데이터 처리 파이프라인
- 다단계 문서 처리
- 규제 산업 애플리케이션
Crew와 Flow 중에서 고르기¶
Crew를 고를 때¶
Crew는 이런 상황에 잘 맞아요.
- 협업 지능(collaborative intelligence)이 필요할 때 — 서로 다른 전문 분야를 가진 여러 에이전트가 함께 일해야 할 때
- 창발적 사고(emergent thinking)가 필요한 문제일 때 — 다양한 관점과 접근 방식이 답을 더 좋게 만들 때
- 작업이 주로 창의적이거나 분석적일 때 — 리서치, 콘텐츠 제작, 분석이 주된 작업일 때
- 엄격한 구조보다 적응성을 중시할 때 — 에이전트의 자율성이 워크플로에 도움이 될 때
- 출력 형식이 어느 정도 유연해도 될 때 — 출력 구조의 변동을 수용할 수 있을 때
# 예시: 시장 분석용 리서치 Crew
from crewai import Agent, Crew, Process, Task
# 전문 에이전트 만들기
researcher = Agent(
role="Market Research Specialist",
goal="Find comprehensive market data on emerging technologies",
backstory="You are an expert at discovering market trends and gathering data.",
)
analyst = Agent(
role="Market Analyst",
goal="Analyze market data and identify key opportunities",
backstory="You excel at interpreting market data and spotting valuable insights.",
)
# 작업 정의
research_task = Task(
description="Research the current market landscape for AI-powered healthcare solutions",
expected_output="Comprehensive market data including key players, market size, and growth trends",
agent=researcher,
)
analysis_task = Task(
description="Analyze the market data and identify the top 3 investment opportunities",
expected_output="Analysis report with 3 recommended investment opportunities and rationale",
agent=analyst,
context=[research_task],
)
# 크루 생성
market_analysis_crew = Crew(
agents=[researcher, analyst],
tasks=[research_task, analysis_task],
process=Process.sequential,
verbose=True,
)
# 실행
result = market_analysis_crew.kickoff()
이 코드에서 핵심은 context=[research_task]예요. analyst가 researcher의 결과를 이어받아 분석하도록 연결해 주죠. Process.sequential이라 작업이 순서대로 진행돼요.
Flow를 고를 때¶
Flow는 이런 상황에 잘 맞아요.
- 실행에 대한 정밀한 제어가 필요할 때 — 워크플로가 정확한 순서와 상태 관리를 요구할 때
- 복잡한 상태 요구사항이 있을 때 — 여러 단계에 걸쳐 상태를 유지하고 변환해야 할 때
- 구조화되고 예측 가능한 출력이 필요할 때 — 일관되고 형식화된 결과가 요구될 때
- 조건부 로직이 포함될 때 — 중간 결과에 따라 서로 다른 경로를 타야 할 때
- AI와 절차적 코드를 결합해야 할 때 — AI 기능과 전통적인 프로그래밍이 모두 필요할 때
# 예시: 구조화된 처리를 하는 고객 지원 Flow
from crewai.flow.flow import Flow, listen, or_, router, start
from pydantic import BaseModel
from typing import List, Dict
# 구조화된 상태 정의
class SupportTicketState(BaseModel):
ticket_id: str = ""
customer_name: str = ""
issue_description: str = ""
category: str = ""
priority: str = "medium"
resolution: str = ""
satisfaction_score: int = 0
class CustomerSupportFlow(Flow[SupportTicketState]):
@start()
def receive_ticket(self):
# 실제 앱에서는 API에서 이 값이 들어올 수 있어요
self.state.ticket_id = "TKT-12345"
self.state.customer_name = "Alex Johnson"
self.state.issue_description = "Unable to access premium features after payment"
return "Ticket received"
@listen(receive_ticket)
def categorize_ticket(self, _):
# 분류는 직접 LLM 호출로 처리
from crewai import LLM
llm = LLM(model="openai/gpt-4o-mini")
prompt = f"""
Categorize the following customer support issue into one of these categories:
- Billing
- Account Access
- Technical Issue
- Feature Request
- Other
Issue: {self.state.issue_description}
Return only the category name.
"""
self.state.category = llm.call(prompt).strip()
return self.state.category
@router(categorize_ticket)
def route_by_category(self, category):
# 카테고리에 따라 서로 다른 핸들러로 라우팅
return category.lower().replace(" ", "_")
@listen("billing")
def handle_billing_issue(self):
# 청구 관련 로직 처리
self.state.priority = "high"
# 추가 청구 처리...
return "Billing issue handled"
@listen("account_access")
def handle_access_issue(self):
# 접근 관련 로직 처리
self.state.priority = "high"
# 추가 접근 처리...
return "Access issue handled"
# 기타 카테고리 핸들러...
@listen(or_("billing", "account_access", "technical_issue",
"feature_request", "other"))
def resolve_ticket(self, resolution_info):
# 최종 해결 단계
self.state.resolution = f"Issue resolved: {resolution_info}"
return self.state.resolution
# 실행
support_flow = CustomerSupportFlow()
result = support_flow.kickoff()
이 Flow에서 눈여겨볼 부분은 세 가지예요.
class CustomerSupportFlow(Flow[SupportTicketState])— 상태를 PydanticBaseModel로 정의해서 Flow 전체에서self.state로 주고받아요.@router(categorize_ticket)— 반환값을 라우팅 키로 써서,@listen("billing")처럼 이름으로 핸들러를 연결해요.- 직접
llm.call(prompt)— 분류처럼 단순한 단계는 크루를 만들지 않고 LLM 호출로 바로 처리해요.
Crew와 Flow를 함께 쓸 때¶
가장 정교한 애플리케이션은 종종 Crew와 Flow를 결합할 때 빛을 발해요.
- 복잡한 다단계 프로세스 — 전체 프로세스는 Flow가 오케스트레이션하고, 복잡한 하위 작업은 Crew가 맡아요
- 창의성과 구조가 모두 필요한 애플리케이션 — 창의적인 작업엔 Crew, 구조화된 처리는 Flow
- 엔터프라이즈급 AI 애플리케이션 — Flow가 상태와 흐름을 관리하면서 Crew가 전문 작업을 처리하게 해요
# 예시: Crew와 Flow를 결합한 콘텐츠 제작 파이프라인
from crewai.flow.flow import Flow, listen, start
from crewai import Agent, Crew, Process, Task
from pydantic import BaseModel
from typing import List, Dict
class ContentState(BaseModel):
topic: str = ""
target_audience: str = ""
content_type: str = ""
outline: Dict = {}
draft_content: str = ""
final_content: str = ""
seo_score: int = 0
class ContentProductionFlow(Flow[ContentState]):
@start()
def initialize_project(self):
# 초기 파라미터 설정
self.state.topic = "Sustainable Investing"
self.state.target_audience = "Millennial Investors"
self.state.content_type = "Blog Post"
return "Project initialized"
@listen(initialize_project)
def create_outline(self, _):
# 리서치 Crew로 아웃라인 만들기
researcher = Agent(
role="Content Researcher",
goal=f"Research {self.state.topic} for {self.state.target_audience}",
backstory="You are an expert researcher with deep knowledge of content creation.",
)
outliner = Agent(
role="Content Strategist",
goal=f"Create an engaging outline for a {self.state.content_type}",
backstory="You excel at structuring content for maximum engagement.",
)
research_task = Task(
description=f"Research {self.state.topic} focusing on what would interest {self.state.target_audience}",
expected_output="Comprehensive research notes with key points and statistics",
agent=researcher,
)
outline_task = Task(
description=f"Create an outline for a {self.state.content_type} about {self.state.topic}",
expected_output="Detailed content outline with sections and key points",
agent=outliner,
context=[research_task],
)
outline_crew = Crew(
agents=[researcher, outliner],
tasks=[research_task, outline_task],
process=Process.sequential,
verbose=True,
)
# Crew를 실행하고 결과 저장
result = outline_crew.kickoff()
# 아웃라인 파싱 (실제 앱에서는 더 견고한 파싱을 쓸 수 있어요)
import json
try:
self.state.outline = json.loads(result.raw)
except:
# JSON이 아니면 폴백
self.state.outline = {"sections": result.raw}
return "Outline created"
@listen(create_outline)
def write_content(self, _):
# 작성 Crew로 본문 만들기
writer = Agent(
role="Content Writer",
goal=f"Write engaging content for {self.state.target_audience}",
backstory="You are a skilled writer who creates compelling content.",
)
editor = Agent(
role="Content Editor",
goal="Ensure content is polished, accurate, and engaging",
backstory="You have a keen eye for detail and a talent for improving content.",
)
writing_task = Task(
description=f"Write a {self.state.content_type} about {self.state.topic} following this outline: {self.state.outline}",
expected_output="Complete draft content in markdown format",
agent=writer,
)
editing_task = Task(
description="Edit and improve the draft content for clarity, engagement, and accuracy",
expected_output="Polished final content in markdown format",
agent=editor,
context=[writing_task],
)
writing_crew = Crew(
agents=[writer, editor],
tasks=[writing_task, editing_task],
process=Process.sequential,
verbose=True,
)
# Crew를 실행하고 결과 저장
result = writing_crew.kickoff()
self.state.final_content = result.raw
return "Content created"
@listen(write_content)
def optimize_for_seo(self, _):
# SEO 최적화는 직접 LLM 호출로
from crewai import LLM
llm = LLM(model="openai/gpt-4o-mini")
prompt = f"""
Analyze this content for SEO effectiveness for the keyword "{self.state.topic}".
Rate it on a scale of 1-100 and provide 3 specific recommendations for improvement.
Content: {self.state.final_content[:1000]}...
Format your response as JSON with the following structure:
{{"score": 85, "recommendations": ["Recommendation 1", "Recommendation 2", "Recommendation 3"]}}
"""
seo_analysis = llm.call(prompt)
# SEO 분석 파싱
import json
try:
analysis = json.loads(seo_analysis)
self.state.seo_score = analysis.get("score", 0)
return analysis
except:
self.state.seo_score = 50
return {"score": 50, "recommendations": ["Unable to parse SEO analysis"]}
# 실행
content_flow = ContentProductionFlow()
result = content_flow.kickoff()
이 예시가 결합 패턴의 감을 잡아주는 핵심이에요. Flow가 initialize → create_outline → write_content → optimize_for_seo 단계를 차례로 이끌면서, 각 단계의 무거운 작업(리서치, 작성)은 Crew가 맡고, 가벼운 단계(SEO 평가)는 직접 LLM 호출로 처리해요. 상태는 ContentState라는 한 곳에 모여 있어서 파이프라인 전체가 깔끔하게 이어져요.
실용적인 평가 프레임워크¶
지금 만들려는 유스케이스에 어떤 접근이 맞는지는 이 단계별 프레임워크로 정리할 수 있어요.
1단계: 복잡성 평가하기¶
애플리케이션의 복잡성을 1~10 척도로 매겨요. 기준은 이 네 가지예요.
- 단계 수(Number of steps) — 몇 개의 고유 연산이 필요한가요?
- 1~3단계: 낮음 (1-3)
- 4~7단계: 중간 (4-7)
- 8단계 이상: 높음 (8-10)
- 의존 관계(Interdependencies) — 서로 다른 부분이 얼마나 얽혀 있나요?
- 의존이 적음: 낮음 (1-3)
- 의존이 조금 있음: 중간 (4-7)
- 복잡한 의존이 많음: 높음 (8-10)
- 조건부 로직(Conditional logic) — 분기와 의사결정이 얼마나 필요한가요?
- 선형 프로세스: 낮음 (1-3)
- 분기가 조금 있음: 중간 (4-7)
- 복잡한 의사결정 트리: 높음 (8-10)
- 도메인 지식(Domain knowledge) — 필요한 지식이 얼마나 전문적인가요?
- 일반 지식: 낮음 (1-3)
- 특정 분야 지식이 조금 있음: 중간 (4-7)
- 여러 분야의 깊은 전문성: 높음 (8-10)
평균 점수를 내면 전체 복잡성이 나와요.
2단계: 정밀도 요구사항 평가하기¶
정밀도 요구사항을 1~10 척도로 매겨요.
- 출력 구조(Output structure) — 출력이 얼마나 구조화되어야 하나요?
- 자유 형식 텍스트: 낮음 (1-3)
- 반구조화: 중간 (4-7)
- 엄격한 형식 (JSON, XML): 높음 (8-10)
- 정확도 요구(Accuracy needs) — 사실적 정확성이 얼마나 중요한가요?
- 창의적 콘텐츠: 낮음 (1-3)
- 정보성 콘텐츠: 중간 (4-7)
- 중요 정보: 높음 (8-10)
- 재현 가능성(Reproducibility) — 실행마다 결과가 얼마나 일관되어야 하나요?
- 변동 허용: 낮음 (1-3)
- 어느 정도 일관성 필요: 중간 (4-7)
- 정확한 재현 필요: 높음 (8-10)
- 오류 허용 범위(Error tolerance) — 오류가 미치는 영향은 어떤가요?
- 영향이 적음: 낮음 (1-3)
- 영향이 보통: 중간 (4-7)
- 영향이 큼: 높음 (8-10)
평균 점수를 내면 전체 정밀도 요구사항이 나와요.
3단계: 매트릭스에 매핑하기¶
복잡성과 정밀도 점수를 매트릭스에 올려보면 선택지가 보여요.
- 낮은 복잡성 (1-4), 낮은 정밀도 (1-4): 단순한 Crew
- 낮은 복잡성 (1-4), 높은 정밀도 (5-10): 직접 LLM 호출을 쓰는 Flow
- 높은 복잡성 (5-10), 낮은 정밀도 (1-4): 복잡한 Crew
- 높은 복잡성 (5-10), 높은 정밀도 (5-10): Crew를 오케스트레이션하는 Flow
4단계: 추가 요소 고려하기¶
복잡성과 정밀도 말고도 이런 것들을 함께 봐요.
- 개발 시간(Development time) — Crew는 보통 프로토타이핑이 더 빨라요
- 유지보수 요구(Maintenance needs) — Flow는 장기적인 유지보수성이 더 좋아요
- 팀의 전문성(Team expertise) — 팀이 각 접근 방식에 얼마나 익숙한지
- 확장성 요구(Scalability requirements) — Flow는 복잡한 애플리케이션에서 대체로 더 잘 확장돼요
- 통합 요구(Integration needs) — 기존 시스템과 어떻게 통합될지
결론¶
Crew와 Flow 중 무엇을 고르든, 아니면 둘을 합치든 이는 애플리케이션의 효과성, 유지보수성, 확장성을 결정하는 중요한 아키텍처 결정이에요. 복잡성과 정밀도라는 두 축으로 유스케이스를 평가하면, 요구사항에 맞는 합리적인 선택을 할 수 있어요. 다만 기억할 점은, 최선의 접근 방식은 애플리케이션이 성숙해감에 따라 진화한다는 거예요. 가장 단순한 해법부터 시작하고, 경험이 쌓이고 요구사항이 분명해질수록 아키텍처를 다듬을 준비를 하세요.
더 알아보기¶
- 효과적인 에이전트 만들기 (crafting effective agents) — 에이전트를 잘 설계하는 방법
- 첫 번째 크루 만들기 (building your first crew) — Crew의 실제 동작 살펴보기
- Flow 상태 관리 마스터하기 (mastering flow state management) — Flow의 상태 관리 심화
- 핵심 개념 (core concepts) — 더 깊은 이해를 위한 개념 살펴보기