feat: adopt directory-based docs versioning with Edge channel

Switch docs.crewai.com from navigation-only versioning (every version
selector entry rendered the same docs/<lang>/* source files) to
Mintlify's directory-based versioning so each version selector entry
renders its own snapshot. Add an "Edge" channel under docs/edge/<lang>/*
that always reflects main HEAD for unreleased work, eliminating
pre-release leakage onto frozen release labels. External links to
canonical /<lang>/* URLs are preserved via wildcard redirects that
always land on the current default version.

Layout:
- docs/edge/<lang>/*         rolling source (you edit here)
- docs/edge/enterprise-api.*.yaml
- docs/v<X.Y.Z>/<lang>/*     frozen, immutable snapshots
- docs/v<X.Y.Z>/enterprise-api.*.yaml
- docs/images/               shared, append-only
- docs/docs.json             nav + redirects

URLs follow the Mintlify-idiomatic shape: /edge/<lang>/<page> for
Edge, /v<X.Y.Z>/<lang>/<page> for every frozen snapshot. The wildcard
redirects /<lang>/:slug* -> /<default>/<lang>/:slug* keep stale links
working, and every freeze rewrites them (plus all per-section/per-page
redirects) so destinations always resolve to the current default
without depending on a second redirect hop.

Release flow integration (devtools release):
- New module crewai_devtools.docs_versioning.freeze() materialises
  docs/v<X.Y.Z>/ from docs/edge/, rewrites openapi: refs inside the
  snapshot, inserts the version into every language block in
  docs.json, and refreshes all redirect destinations.
- _update_docs_and_create_pr() in cli.py now calls that freeze during
  Phase 2 of devtools release. Edge changelogs are updated first (so
  the snapshot freeze picks them up), then the snapshot is staged
  alongside docs.json, branched as docs/freeze-v<X.Y.Z>, and the PR
  is titled [docs-freeze] docs: snapshot and changelog for v<X.Y.Z>
  — the title prefix the new CI guard reads.
- The PR still gates tag, GitHub release, PyPI publish, and the
  enterprise release as before; no new PRs are added.
- Pre-releases (1.X.YaN, 1.X.YbN, ...) skip the snapshot — they ride
  Edge — and the docs PR title omits the [docs-freeze] prefix.
- docs_check (AI-generated docs scaffolding) writes to
  docs/edge/<lang>/* so newly-generated unreleased docs land in Edge
  and never accidentally touch a frozen snapshot.

Migration scripts (one-shot):
- scripts/docs/freeze_historical_versions.py reconstructs all 16
  historical snapshots (v1.10.0 .. v1.14.7) from git tags via
  git archive | tar, rewriting openapi: MDX refs so each snapshot
  reads its own enterprise-api YAML rather than the live one.
- scripts/docs/prefix_version_paths.py one-shot-migrates docs.json:
  rewrites every page path in 16 versioned blocks to point under
  docs/v<X.Y.Z>/, inserts a new Edge entry per language, tags
  v1.14.7 as Latest (default), prunes pages whose target file
  doesn't exist in the snapshot (e.g. docs/ar/ didn't exist before
  v1.12.0), and writes the wildcard + per-section redirects.
- scripts/docs/freeze_current_edge.py is now a thin CLI wrapper
  around docs_versioning.freeze for manual one-off freezes (e.g.
  retroactively snapshotting a forgotten release).

CI guards (.github/workflows/docs-snapshots.yml):
- Frozen snapshots under docs/v[0-9]*/ are immutable; only PRs whose
  title contains [docs-freeze] (i.e. release-cut PRs generated by
  devtools release or the manual wrapper) may modify them.
- Images under docs/images/ are append-only since snapshots share a
  single image directory. Deleting or renaming an image breaks every
  historical snapshot that still references it.

Restored docs/images/crewai-otel-export.png from PR #3673; it was
deleted in PR #4908 but v1.10.0 / v1.10.1 snapshots still reference
it. Restoring instead of editing the snapshots preserves historical
rendering fidelity and validates the new append-only rule
retroactively.

Tests:
- lib/devtools/tests/test_docs_versioning.py covers the freeze: file
  copy, openapi rewrite, version insertion, default demotion, redirect
  upserts, per-section redirect rewriting, idempotency, and invalid
  inputs.

Verified locally with mintlify broken-links: 0 broken links across
the full site (Edge + 16 frozen versions, 4 locales).

AGENTS.md (repo root) is the contributor guide for the new model;
RELEASING.md is the release-cut runbook; README's Contribution
section links to both.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lucas Gomide
2026-06-17 09:33:56 -03:00
parent 7bb9bc7e1a
commit 93dafe2637
15793 changed files with 3237032 additions and 16873 deletions

View File

@@ -0,0 +1,331 @@
---
title: 프롬프트 커스터마이징
description: CrewAI를 위한 저수준 프롬프트 커스터마이징에 대해 자세히 알아보고, 다양한 모델과 언어에 대해 매우 맞춤화되고 복잡한 사용 사례를 구현할 수 있습니다.
icon: message-pen
mode: "wide"
---
## 프롬프트를 커스터마이즈해야 하는 이유
CrewAI의 기본 프롬프트는 많은 시나리오에서 잘 작동하지만, 저수준 커스터마이징은 훨씬 더 유연하고 강력한 에이전트 행동으로 이어집니다. 더 깊은 제어를 통해 얻을 수 있는 이점은 다음과 같습니다:
1. **특정 LLM에 맞게 최적화** GPT-4, Claude, Llama와 같은 다양한 모델은 각자의 고유한 아키텍처에 맞는 프롬프트 형식에서 최고의 성능을 발휘합니다.
2. **언어 변경** 영어를 넘어서는 언어로만 작동하는 에이전트를 구축하여 미묘한 뉘앙스도 정확하게 처리할 수 있습니다.
3. **복잡한 도메인에 특화** 헬스케어, 금융, 법률 등 매우 전문적인 산업군에 맞춰 프롬프트를 조정할 수 있습니다.
4. **톤과 스타일 조정** 에이전트의 톤과 스타일을 좀 더 형식적, 캐주얼, 창의적, 혹은 분석적으로 만들 수 있습니다.
5. **초고도 커스텀 사례 지원** 복잡하고 프로젝트에 특화된 요구사항을 충족하기 위해 고급 프롬프트 구조 및 포맷을 활용할 수 있습니다.
이 가이드에서는 CrewAI의 프롬프트를 더 낮은 레벨에서 활용하여, 에이전트의 사고 및 상호작용 방식을 세밀하게 제어하는 방법을 다룹니다.
## CrewAI의 Prompt 시스템 이해하기
내부적으로 CrewAI는 광범위하게 커스터마이즈할 수 있는 모듈식 prompt 시스템을 사용합니다:
- **Agent 템플릿** 각 agent가 할당된 역할을 수행하는 방식을 결정합니다.
- **Prompt 슬라이스** 작업, 도구 사용, 출력 구조와 같은 특수한 동작을 제어합니다.
- **오류 처리** agent가 실패, 예외, 또는 타임아웃에 어떻게 반응할지 지정합니다.
- **도구별 prompt** 도구가 호출되거나 사용되는 방법에 대한 상세 지침을 정의합니다.
이 요소들이 어떻게 구성되어 있는지 보려면 [CrewAI 저장소의 원본 prompt 템플릿](https://github.com/crewAIInc/crewAI/blob/main/src/crewai/translations/en.json)을 확인하세요. 여기서 필요에 따라 오버라이드하거나 수정하여 고급 동작을 구현할 수 있습니다.
## 기본 시스템 지침 이해하기
<Warning>
**프로덕션 투명성 문제**: CrewAI는 여러분이 인지하지 못하는 사이에 기본 지침을 프롬프트에 자동으로 삽입합니다. 이 섹션에서는 내부적으로 어떤 일이 일어나고 있는지와 완전한 제어권을 얻는 방법을 설명합니다.
</Warning>
여러분이 `role`, `goal`, `backstory`로 에이전트를 정의할 때, CrewAI는 형식 및 동작을 제어하는 추가 시스템 지침을 자동으로 추가합니다. 이러한 기본 삽입을 이해하는 것은 완전한 프롬프트 투명성이 필요한 프로덕션 시스템에서 매우 중요합니다.
### CrewAI가 자동으로 삽입하는 내용
에이전트 구성에 따라 CrewAI는 다양한 기본 지침을 추가합니다:
#### 도구가 없는 에이전트를 위한 안내
```text
"I MUST use these formats, my job depends on it!"
```
#### 도구가 있는 에이전트를 위한 안내
```text
"IMPORTANT: Use the following format in your response:
Thought: you should always think about what to do
Action: the action to take, only one name of [tool_names]
Action Input: the input to the action, just a simple JSON object...
```
#### 구조화된 출력(JSON/Pydantic)의 경우
```text
"Ensure your final answer contains only the content in the following format: {output_format}
Ensure the final output does not include any code block markers like ```json or ```python."
```
### 전체 시스템 프롬프트 보기
LLM에 전달되는 프롬프트가 정확히 무엇인지 확인하려면, 생성된 프롬프트를 확인할 수 있습니다:
```python
from crewai import Agent, Crew, Task
from crewai.utilities.prompts import Prompts
# 에이전트 생성
agent = Agent(
role="Data Analyst",
goal="Analyze data and provide insights",
backstory="You are an expert data analyst with 10 years of experience.",
verbose=True
)
# 샘플 태스크 생성
task = Task(
description="Analyze the sales data and identify trends",
expected_output="A detailed analysis with key insights and trends",
agent=agent
)
# 프롬프트 생성기 생성
prompt_generator = Prompts(
agent=agent,
has_tools=len(agent.tools) > 0,
use_system_prompt=agent.use_system_prompt
)
# 실제 프롬프트 생성 및 확인
generated_prompt = prompt_generator.task_execution()
# LLM에 전달될 전체 시스템 프롬프트 출력
if "system" in generated_prompt:
print("=== SYSTEM PROMPT ===")
print(generated_prompt["system"])
print("\n=== USER PROMPT ===")
print(generated_prompt["user"])
else:
print("=== COMPLETE PROMPT ===")
print(generated_prompt["prompt"])
# 태스크 설명이 어떻게 포맷되는지도 확인할 수 있습니다
print("\n=== TASK CONTEXT ===")
print(f"Task Description: {task.description}")
print(f"Expected Output: {task.expected_output}")
```
### 기본 지침 재정의
프롬프트에 대한 완전한 제어를 얻기 위해 여러 가지 옵션이 있습니다:
#### 옵션 1: 커스텀 템플릿 (권장)
```python
from crewai import Agent
# Define your own system template without default instructions
custom_system_template = """You are {role}. {backstory}
Your goal is: {goal}
Respond naturally and conversationally. Focus on providing helpful, accurate information."""
custom_prompt_template = """Task: {input}
Please complete this task thoughtfully."""
agent = Agent(
role="Research Assistant",
goal="Help users find accurate information",
backstory="You are a helpful research assistant.",
system_template=custom_system_template,
prompt_template=custom_prompt_template,
use_system_prompt=True # Use separate system/user messages
)
```
#### 옵션 2: 사용자 지정 프롬프트 파일
특정 프롬프트 슬라이스를 오버라이드하려면 `custom_prompts.json` 파일을 생성하세요:
```json
{
"slices": {
"no_tools": "\nProvide your best answer in a natural, conversational way.",
"tools": "\nYou have access to these tools: {tools}\n\nUse them when helpful, but respond naturally.",
"formatted_task_instructions": "Format your response as: {output_format}"
}
}
```
그런 다음 crew에서 사용하세요:
```python
crew = Crew(
agents=[agent],
tasks=[task],
prompt_file="custom_prompts.json",
verbose=True
)
```
<Note>
`agent.i18n`은 이전 버전과의 호환성을 위해서만 유지되며 사용이 중단될 예정입니다. 런타임 프롬프트 커스터마이징에는 `Crew`에 `prompt_file`을 전달하세요. 프롬프트 슬라이스를 코드에서 직접 읽어야 한다면 i18n 유틸리티를 직접 사용하세요:
</Note>
```python
from crewai.utilities.i18n import get_i18n
i18n = get_i18n("custom_prompts.json")
format_slice = i18n.slice("format")
tool_prompt = i18n.tools("ask_question")
```
#### 옵션 3: o1 모델에 대한 시스템 프롬프트 비활성화
```python
agent = Agent(
role="Analyst",
goal="Analyze data",
backstory="Expert analyst",
use_system_prompt=False # Disables system prompt separation
)
```
### 관측 도구를 활용한 디버깅
프로덕션 투명성을 위해 관측 플랫폼과 통합하여 모든 prompt 및 LLM 상호작용을 모니터링하세요. 이를 통해 LLM에 어떤 prompt(기본 지침 포함)가 전송되고 있는지 정확히 확인할 수 있습니다.
다양한 플랫폼(Langfuse, MLflow, Weights & Biases, 커스텀 로깅 솔루션 등)과의 통합에 대한 자세한 가이드는 [관측 문서](/ko/observability/overview)를 참고하세요.
### 프로덕션을 위한 모범 사례
1. **프로덕션에 배포하기 전에 반드시 생성된 prompt를 점검하세요**
2. **prompt 내용을 완전히 제어해야 할 경우에는 커스텀 템플릿을 사용하세요**
3. **지속적인 prompt 모니터링을 위해 관측 도구를 통합하세요** ([Observability 문서](/ko/observability/overview) 참고)
4. **서로 다른 LLM으로 테스트하세요**. 기본 instruction은 모델마다 다르게 작동할 수 있습니다
5. **팀 투명성을 위해 prompt 커스터마이징을 문서화하세요**
<Tip>
기본 instruction은 일관된 agent 동작을 보장하기 위해 존재하지만, 도메인 특화 요구사항과 충돌할 수 있습니다. 위의 커스터마이징 옵션을 사용하여 프로덕션 시스템에서 agent의 동작을 완전히 제어할 수 있습니다.
</Tip>
## 프롬프트 파일 관리 모범 사례
저수준 프롬프트 커스터마이징을 수행할 때는 다음 지침을 따라 조직적이고 유지 관리가 용이하도록 하세요:
1. **파일 분리** 커스터마이징한 프롬프트는 메인 코드베이스 외부의 전용 JSON 파일에 저장하세요.
2. **버전 관리** 리포지토리 내에서 변경 사항을 추적하여 프롬프트 조정 내역이 명확히 문서화되도록 하세요.
3. **모델 또는 언어별 정리** `prompts_llama.json` 또는 `prompts_es.json`과 같이 네이밍 스킴을 사용해 특화된 구성을 빠르게 식별할 수 있도록 하세요.
4. **변경 사항 문서화** 주석을 추가하거나 README를 유지 관리하여 커스터마이징의 목적과 범위를 상세히 기술하세요.
5. **수정 최소화** 실제로 조정이 필요한 특정 부분만 오버라이드하고, 나머지 부분은 기본 기능을 유지하세요.
## 프롬프트를 커스터마이즈하는 가장 간단한 방법
가장 간단한 접근 방법 중 하나는 오버라이드하려는 프롬프트에 대한 JSON 파일을 생성한 다음, 해당 파일을 Crew에 지정하는 것입니다.
1. 업데이트된 프롬프트 슬라이스로 JSON 파일을 만드세요.
2. Crew의 `prompt_file` 파라미터를 통해 그 파일을 참조하세요.
그러면 CrewAI가 기본값과 사용자가 지정한 내용을 병합하므로, 모든 프롬프트를 다시 정의할 필요가 없습니다. 방법은 다음과 같습니다:
프롬프트 슬라이스를 코드에서 직접 읽어야 하는 경우에는 `agent.i18n`을 읽는 대신 동일한 프롬프트 파일로 `crewai.utilities.i18n.get_i18n()`을 사용하세요.
### 예시: 기본 프롬프트 커스터마이징
수정하고 싶은 프롬프트를 포함하는 `custom_prompts.json` 파일을 생성하세요. 변경 사항만이 아니라 포함해야 하는 모든 최상위 프롬프트를 반드시 나열해야 합니다:
```json
{
"slices": {
"format": "When responding, follow this structure:\n\nTHOUGHTS: Your step-by-step thinking\nACTION: Any tool you're using\nRESULT: Your final answer or conclusion"
}
}
```
그 다음 아래와 같이 통합하세요:
```python
from crewai import Agent, Crew, Task, Process
# 평소와 같이 에이전트와 태스크를 생성
researcher = Agent(
role="Research Specialist",
goal="Find information on quantum computing",
backstory="You are a quantum physics expert",
verbose=True
)
research_task = Task(
description="Research quantum computing applications",
expected_output="A summary of practical applications",
agent=researcher
)
# 커스텀 프롬프트 파일로 crew를 생성
crew = Crew(
agents=[researcher],
tasks=[research_task],
prompt_file="path/to/custom_prompts.json",
verbose=True
)
# crew 실행
result = crew.kickoff()
```
이 몇 가지 간단한 수정으로, agent가 소통하고 태스크를 해결하는 방식을 세밀하게 제어할 수 있습니다.
## 특정 모델에 맞춘 최적화
모델마다 잘 동작하는 프롬프트의 구조가 다릅니다. 프롬프트를 모델의 뉘앙스에 맞게 더욱 깊이 있게 조정하면 성능이 크게 향상될 수 있습니다.
### 예시: Llama 3.3 프롬프트 템플릿
예를 들어, Meta의 Llama 3.3과 작업할 때는 더 깊은 수준의 커스터마이징이 다음에 설명된 권장 구조를 반영할 수 있습니다:
https://www.llama.com/docs/model-cards-and-prompt-formats/llama3_1/#prompt-template
다음은 Llama 3.3을 코드에서 활용하도록 Agent를 세밀하게 튜닝하는 방법을 보여주는 예시입니다:
```python
from crewai import Agent, Crew, Task, Process
from crewai_tools import DirectoryReadTool, FileReadTool
# Define templates for system, user (prompt), and assistant (response) messages
system_template = """<|begin_of_text|><|start_header_id|>system<|end_header_id|>{{ .System }}<|eot_id|>"""
prompt_template = """<|start_header_id|>user<|end_header_id|>{{ .Prompt }}<|eot_id|>"""
response_template = """<|start_header_id|>assistant<|end_header_id|>{{ .Response }}<|eot_id|>"""
# Create an Agent using Llama-specific layouts
principal_engineer = Agent(
role="Principal Engineer",
goal="Oversee AI architecture and make high-level decisions",
backstory="You are the lead engineer responsible for critical AI systems",
verbose=True,
llm="groq/llama-3.3-70b-versatile", # Using the Llama 3 model
system_template=system_template,
prompt_template=prompt_template,
response_template=response_template,
tools=[DirectoryReadTool(), FileReadTool()]
)
# Define a sample task
engineering_task = Task(
description="Review AI implementation files for potential improvements",
expected_output="A summary of key findings and recommendations",
agent=principal_engineer
)
# Create a Crew for the task
llama_crew = Crew(
agents=[principal_engineer],
tasks=[engineering_task],
process=Process.sequential,
verbose=True
)
# Execute the crew
result = llama_crew.kickoff()
print(result.raw)
```
이와 같이 더 심도 있는 설정을 통해 별도의 JSON 파일 없이도 Llama 기반 워크플로에 대해 포괄적이고 저수준의 제어를 할 수 있습니다.
## 결론
CrewAI에서의 저수준 prompt 커스터마이제이션은 매우 맞춤화되고 복잡한 사용 사례에 대한 문을 엽니다. 잘 구성된 prompt 파일(또는 직접 작성한 인라인 템플릿)을 구축함으로써 다양한 모델, 언어, 특화된 도메인을 수용할 수 있습니다. 이러한 수준의 유연성 덕분에 원하는 AI 동작을 정확하게 설계할 수 있으며, override하지 않을 경우에도 CrewAI가 신뢰할 수 있는 기본값을 제공한다는 점에서 안심할 수 있습니다.
<Check>
이제 CrewAI에서 고급 prompt 커스터마이징을 위한 기초를 갖추었습니다. 모델별 구조나 도메인별 제약에 맞춰 적용하든, 이러한 저수준 접근 방식은 agent 상호작용을 매우 전문적으로 조정할 수 있게 해줍니다.
</Check>

View File

@@ -0,0 +1,134 @@
---
title: 지문 인식
description: CrewAI의 지문 인식 시스템을 사용하여 컴포넌트를 전체 라이프사이클 동안 고유하게 식별하고 추적하는 방법을 알아보세요.
icon: fingerprint
mode: "wide"
---
## 개요
CrewAI의 Fingerprints는 컴포넌트를 고유하게 식별하고 그 생애주기를 추적할 수 있는 방법을 제공합니다. 각 `Agent`, `Crew`, `Task`는 생성 시 자동으로 고유한 fingerprint를 부여받으며, 이는 수동으로 변경할 수 없습니다.
이러한 fingerprints는 다음과 같은 용도로 사용할 수 있습니다:
- 컴포넌트 사용 감사 및 추적
- 컴포넌트 식별 무결성 보장
- 컴포넌트에 메타데이터 첨부
- 추적 가능한 작업 체인 생성
## 지문(Fingerprints)의 작동 방식
지문(fingerprint)은 `crewai.security` 모듈의 `Fingerprint` 클래스의 인스턴스입니다. 각 지문에는 다음과 같은 정보가 포함되어 있습니다:
- UUID 문자열: 컴포넌트의 고유 식별자로, 자동으로 생성되며 수동으로 설정할 수 없습니다.
- 생성 타임스탬프: 지문이 생성된 시점을 나타내며, 자동으로 설정되고 수동으로 수정할 수 없습니다.
- 메타데이터: 추가 정보를 담은 사전(dictionary)으로, 사용자 정의가 가능합니다.
지문은 컴포넌트가 생성될 때 자동으로 생성되어 할당됩니다. 각 컴포넌트는 읽기 전용 속성을 통해 자신의 지문을 제공합니다.
## 기본 사용법
### 지문 접근하기
```python
from crewai import Agent, Crew, Task
# Create components - fingerprints are automatically generated
agent = Agent(
role="Data Scientist",
goal="Analyze data",
backstory="Expert in data analysis"
)
crew = Crew(
agents=[agent],
tasks=[]
)
task = Task(
description="Analyze customer data",
expected_output="Insights from data analysis",
agent=agent
)
# Access the fingerprints
agent_fingerprint = agent.fingerprint
crew_fingerprint = crew.fingerprint
task_fingerprint = task.fingerprint
# Print the UUID strings
print(f"Agent fingerprint: {agent_fingerprint.uuid_str}")
print(f"Crew fingerprint: {crew_fingerprint.uuid_str}")
print(f"Task fingerprint: {task_fingerprint.uuid_str}")
```
### 지문 메타데이터 작업
지문에 추가적인 맥락 정보를 제공하기 위해 메타데이터를 추가할 수 있습니다:
```python
# Add metadata to the agent's fingerprint
agent.security_config.fingerprint.metadata = {
"version": "1.0",
"department": "Data Science",
"project": "Customer Analysis"
}
# Access the metadata
print(f"Agent metadata: {agent.fingerprint.metadata}")
```
## 지문(Fingerprint) 지속성
지문은 컴포넌트의 생애 주기 전체에 걸쳐 지속되고 변하지 않도록 설계되었습니다. 컴포넌트를 수정하더라도 지문은 동일하게 유지됩니다:
```python
original_fingerprint = agent.fingerprint.uuid_str
# Modify the agent
agent.goal = "New goal for analysis"
# The fingerprint remains unchanged
assert agent.fingerprint.uuid_str == original_fingerprint
```
## 결정론적 지문
UUID와 생성 타임스탬프를 직접 설정할 수는 없지만, `generate` 메서드와 시드(seed)를 사용하여 결정론적 지문을 만들 수 있습니다:
```python
from crewai.security import Fingerprint
# 시드 문자열을 사용하여 결정론적 지문 생성
deterministic_fingerprint = Fingerprint.generate(seed="my-agent-id")
# 동일한 시드로 항상 동일한 지문이 생성됨
same_fingerprint = Fingerprint.generate(seed="my-agent-id")
assert deterministic_fingerprint.uuid_str == same_fingerprint.uuid_str
# 메타데이터도 설정할 수 있음
custom_fingerprint = Fingerprint.generate(
seed="my-agent-id",
metadata={"version": "1.0"}
)
```
## 고급 사용
### Fingerprint 구조
각 fingerprint는 다음과 같은 구조를 가지고 있습니다:
```python
from crewai.security import Fingerprint
fingerprint = agent.fingerprint
# UUID 문자열 - 고유 식별자 (자동 생성)
uuid_str = fingerprint.uuid_str # e.g., "123e4567-e89b-12d3-a456-426614174000"
# 생성 타임스탬프 (자동 생성)
created_at = fingerprint.created_at # datetime 객체
# 메타데이터 - 추가 정보용 (사용자 지정 가능)
metadata = fingerprint.metadata # 딕셔너리, 기본값은 {}
```

View File

@@ -0,0 +1,454 @@
---
title: 효과적인 에이전트 제작
description: 복잡한 문제를 효과적으로 해결하기 위해 협업하는 강력하고 전문화된 AI 에이전트를 설계하는 모범 사례를 배워보세요.
icon: robot
mode: "wide"
---
## 에이전트 설계의 예술과 과학
CrewAI의 핵심에는 에이전트가 있습니다. 에이전트는 협업 프레임워크 내에서 특정 역할을 수행하도록 설계된 전문화된 AI 엔터티입니다. 기본적인 에이전트를 만드는 것은 간단하지만, 진정으로 효과적이고 탁월한 결과를 만들어내는 에이전트를 설계하려면 주요 설계 원칙과 모범 사례를 이해해야 합니다.
이 가이드는 여러분이 에이전트 설계의 예술을 마스터할 수 있도록 도와줍니다. 이를 통해 효과적으로 협업하고, 비판적으로 사고하며, 특정 요구에 맞춤화된 고품질 결과물을 만들어내는 전문화된 AI 페르소나를 설계할 수 있게 됩니다.
### 에이전트 설계가 중요한 이유
에이전트를 정의하는 방식은 다음에 중대한 영향을 미칩니다:
1. **출력 품질**: 잘 설계된 에이전트는 더 관련성 높고, 품질이 뛰어난 결과를 생성합니다
2. **협업 효율성**: 상호 보완적인 역량을 가진 에이전트들이 함께 더 효율적으로 작업합니다
3. **작업 성과**: 명확한 역할과 목표를 가진 에이전트가 작업을 더 효과적으로 수행합니다
4. **시스템 확장성**: 신중하게 설계된 에이전트는 여러 crew와 다양한 컨텍스트에서 재사용될 수 있습니다
이러한 측면에서 뛰어난 에이전트를 만들기 위한 모범 사례를 함께 살펴보겠습니다.
## 80/20 법칙: 에이전트보다 작업에 집중하세요
효과적인 AI 시스템을 구축할 때 이 중요한 원칙을 기억하세요: **노력의 80%는 작업 설계에, 20%만 에이전트 정의에 투자해야 합니다**.
왜일까요? 아무리 완벽하게 정의된 에이전트라도 잘못된 작업 설계에서는 실패하지만, 잘 설계된 작업은 단순한 에이전트까지도 뛰어나게 만들 수 있기 때문입니다. 즉,
- 대부분의 시간을 명확한 작업 지침 작성에 할애하세요
- 상세한 입력과 예상 결과를 정의하세요
- 실행을 안내할 예시와 컨텍스트를 추가하세요
- 남은 시간에는 에이전트 역할, 목표, 배경에 집중하세요
이는 에이전트 설계가 중요하지 않다는 의미가 아닙니다. 분명히 중요합니다. 하지만 실행 실패의 대부분은 작업 설계에서 발생하므로, 그에 따라 우선순위를 두어야 합니다.
## 효과적인 에이전트 설계의 핵심 원칙
### 1. 역할-목표-배경 이야기 프레임워크
CrewAI에서 가장 강력한 에이전트는 세 가지 핵심 요소의 탄탄한 기반 위에 구축됩니다:
#### 역할: 에이전트의 전문화된 기능
역할은 에이전트가 수행하는 일과 전문 분야를 정의합니다. 역할을 설계할 때는 다음을 준수하세요:
- **구체적이고 전문적으로 작성하세요**: "작가" 대신 "기술 문서 전문가"나 "창의적 스토리텔러"처럼 명확하게 표현하세요.
- **현실 세계의 직업과 일치시키세요**: 역할을 잘 알려진 직업 유형에 기반하세요.
- **도메인 전문성을 포함하세요**: 에이전트의 지식 분야를 명확히 하세요 (예: "시장 동향에 특화된 금융 분석가").
**효과적인 역할 예시:**
```yaml
role: "Senior UX Researcher specializing in user interview analysis"
role: "Full-Stack Software Architect with expertise in distributed systems"
role: "Corporate Communications Director specializing in crisis management"
```
#### 목표: 에이전트의 목적과 동기
목표는 에이전트의 노력을 이끌고 의사 결정 과정을 형성합니다. 효과적인 목표는 다음과 같아야 합니다:
- **명확하고 결과 중심적이어야 함**: 에이전트가 달성하려는 것이 무엇인지 정의합니다.
- **품질 기준을 강조해야 함**: 작업의 품질에 대한 기대치를 포함합니다.
- **성공 기준을 통합해야 함**: "좋음"이 무엇인지 에이전트가 이해할 수 있도록 도와줍니다.
**효과적인 목표의 예시:**
```yaml
goal: "Uncover actionable user insights by analyzing interview data and identifying recurring patterns, unmet needs, and improvement opportunities"
goal: "Design robust, scalable system architectures that balance performance, maintainability, and cost-effectiveness"
goal: "Craft clear, empathetic crisis communications that address stakeholder concerns while protecting organizational reputation"
```
#### 배경 이야기: 에이전트의 경험과 관점
배경 이야기는 에이전트에게 깊이를 부여하며, 문제를 해결하고 타인과 상호작용하는 방식에 영향을 미칩니다. 좋은 배경 이야기는 다음과 같습니다:
- **전문성과 경험을 확립**: 에이전트가 어떻게 자신의 기술을 습득했는지 설명합니다.
- **업무 스타일 및 가치를 정의**: 에이전트가 일에 어떻게 접근하는지 설명합니다.
- **통합된 페르소나 생성**: 배경 이야기의 모든 요소가 역할과 목표에 부합하는지 확인합니다.
**효과적인 배경 이야기 예시:**
```yaml
backstory: "You have spent 15 years conducting and analyzing user research for top tech companies. You have a talent for reading between the lines and identifying patterns that others miss. You believe that good UX is invisible and that the best insights come from listening to what users don't say as much as what they do say."
backstory: "With 20+ years of experience building distributed systems at scale, you've developed a pragmatic approach to software architecture. You've seen both successful and failed systems and have learned valuable lessons from each. You balance theoretical best practices with practical constraints and always consider the maintenance and operational aspects of your designs."
backstory: "As a seasoned communications professional who has guided multiple organizations through high-profile crises, you understand the importance of transparency, speed, and empathy in crisis response. You have a methodical approach to crafting messages that address concerns while maintaining organizational credibility."
```
### 2. 전문가가 일반가보다 우수함
에이전트는 일반적인 역할보다 전문화된 역할을 부여할 때 훨씬 더 우수한 성능을 보입니다. 고도로 집중된 에이전트는 더 정확하고 관련성 높은 결과물을 제공합니다:
**일반적 (효과 적음):**
```yaml
role: "Writer"
```
**전문화 (효과 좋음):**
```yaml
role: "Technical Blog Writer specializing in explaining complex AI concepts to non-technical audiences"
```
**전문가의 이점:**
- 기대되는 출력물에 대한 더 명확한 이해
- 더 일관된 성과
- 특정 작업과의 더 나은 정렬
- 도메인별 판단능력 향상
### 3. 전문화와 다재다능성의 균형
효과적인 에이전트는 전문성(한 가지를 매우 잘하는 것)과 다재다능성(다양한 상황에 적응할 수 있는 것) 사이에서 적절한 균형을 이룹니다:
- **역할에 전문화하고, 적용에는 다재다능하게**: 여러 맥락에서 적용할 수 있는 전문 기술을 가진 에이전트를 만드세요
- **지나치게 좁은 정의는 피하기**: 에이전트가 자신이 전문으로 하는 영역 내에서 다양한 변형을 처리할 수 있도록 하세요
- **협업 맥락을 고려하기**: 함께 일하게 될 다른 에이전트들과 전문 분야가 상호 보완될 수 있도록 에이전트를 설계하세요
### 4. 적절한 전문성 수준 설정
에이전트에게 할당하는 전문성 수준은 작업 접근 방식에 영향을 미칩니다:
- **초급 에이전트**: 단순한 작업, 브레인스토밍, 초기 초안에 적합
- **중급 에이전트**: 대부분의 표준 작업에서 신뢰성 있는 실행에 적합
- **전문가 에이전트**: 깊이와 세밀함이 요구되는 복잡하고 전문적인 작업에 최적
- **월드 클래스 에이전트**: 예외적인 품질이 필요한 중요한 작업에 할당
작업의 복잡성과 품질 요구 사항에 따라 적합한 전문성 수준을 선택하세요. 대부분의 협업 crew에서는 다양한 전문성 수준이 어우러지는 것이 가장 효과적이며, 핵심 전문 기능에는 더 높은 전문성을 배정하는 것이 좋습니다.
## 실제 예시: 적용 전과 적용 후
이러한 모범 사례를 적용하기 전과 후의 agent 정의 예시를 살펴보겠습니다:
### 예시 1: 콘텐츠 제작 에이전트
**이전:**
```yaml
role: "Writer"
goal: "Write good content"
backstory: "You are a writer who creates content for websites."
```
**이후:**
```yaml
role: "B2B Technology Content Strategist"
goal: "Create compelling, technically accurate content that explains complex topics in accessible language while driving reader engagement and supporting business objectives"
backstory: "You have spent a decade creating content for leading technology companies, specializing in translating technical concepts for business audiences. You excel at research, interviewing subject matter experts, and structuring information for maximum clarity and impact. You believe that the best B2B content educates first and sells second, building trust through genuine expertise rather than marketing hype."
```
### 예시 2: 리서치 에이전트
**변경 전:**
```yaml
role: "Researcher"
goal: "Find information"
backstory: "You are good at finding information online."
```
**변경 후:**
```yaml
role: "Academic Research Specialist in Emerging Technologies"
goal: "Discover and synthesize cutting-edge research, identifying key trends, methodologies, and findings while evaluating the quality and reliability of sources"
backstory: "With a background in both computer science and library science, you've mastered the art of digital research. You've worked with research teams at prestigious universities and know how to navigate academic databases, evaluate research quality, and synthesize findings across disciplines. You're methodical in your approach, always cross-referencing information and tracing claims to primary sources before drawing conclusions."
```
## 에이전트를 위한 효과적인 작업 설계하기
에이전트 설계도 중요하지만, 작업 설계는 성공적인 실행을 위해 매우 중요합니다. 에이전트가 성공할 수 있도록 작업을 설계할 때 참고할 수 있는 모범 사례는 다음과 같습니다:
### 효과적인 작업의 구조
잘 설계된 작업은 서로 다른 목적을 가진 두 가지 주요 구성 요소를 가지고 있습니다:
#### 작업 설명: 프로세스
설명은 무엇을 어떻게 해야 하는지에 초점을 맞춰야 하며, 아래를 포함해야 합니다:
- 실행을 위한 상세 지침
- 맥락 및 배경 정보
- 범위 및 제약 조건
- 따라야 할 프로세스 단계
#### 예상 산출물: 결과물
예상 산출물은 최종 결과가 어떻게 보여야 하는지 정의해야 합니다:
- 형식 명세(마크다운, JSON 등)
- 구조 요구사항
- 품질 기준
- 좋은 결과물의 예시(가능할 경우)
### 작업 설계 모범 사례
#### 1. 단일 목적, 단일 산출물
작업은 하나의 명확한 목표에 집중할 때 가장 좋은 성과를 냅니다:
**나쁜 예시(너무 광범위함):**
```yaml
task_description: "Research market trends, analyze the data, and create a visualization."
```
**좋은 예시(집중됨):**
```yaml
# Task 1
research_task:
description: "Research the top 5 market trends in the AI industry for 2024."
expected_output: "A markdown list of the 5 trends with supporting evidence."
# Task 2
analysis_task:
description: "Analyze the identified trends to determine potential business impacts."
expected_output: "A structured analysis with impact ratings (High/Medium/Low)."
# Task 3
visualization_task:
description: "Create a visual representation of the analyzed trends."
expected_output: "A description of a chart showing trends and their impact ratings."
```
#### 2. 입력 및 출력 명시
항상 작업에 사용할 입력값과 출력이 어떻게 보여야 하는지 명확하게 지정하세요:
**예시:**
```yaml
analysis_task:
description: >
Analyze the customer feedback data from the CSV file.
Focus on identifying recurring themes related to product usability.
Consider sentiment and frequency when determining importance.
expected_output: >
A markdown report with the following sections:
1. Executive summary (3-5 bullet points)
2. Top 3 usability issues with supporting data
3. Recommendations for improvement
```
#### 3. 목적 및 맥락 포함
작업이 왜 중요한지, 더 큰 워크플로우에서 어떻게 맞물리는지 설명하세요:
**예시:**
```yaml
competitor_analysis_task:
description: >
Analyze our three main competitors' pricing strategies.
This analysis will inform our upcoming pricing model revision.
Focus on identifying patterns in how they price premium features
and how they structure their tiered offerings.
```
#### 4. 구조화된 출력 도구 사용하기
기계가 읽을 수 있는 출력을 위해서, 포맷을 명확히 지정하세요:
**예시:**
```yaml
data_extraction_task:
description: "Extract key metrics from the quarterly report."
expected_output: "JSON object with the following keys: revenue, growth_rate, customer_acquisition_cost, and retention_rate."
```
## 피해야 할 일반적인 실수
실제 구현에서 얻은 교훈을 바탕으로, 에이전트 및 태스크 설계에서 가장 흔한 실수는 다음과 같습니다:
### 1. 불명확한 작업 지시
**문제:** 작업에 충분한 세부 정보가 없어 에이전트가 효과적으로 실행하기 어렵습니다.
**잘못 설계된 예시:**
```yaml
research_task:
description: "Research AI trends."
expected_output: "A report on AI trends."
```
**개선된 버전:**
```yaml
research_task:
description: >
Research the top emerging AI trends for 2024 with a focus on:
1. Enterprise adoption patterns
2. Technical breakthroughs in the past 6 months
3. Regulatory developments affecting implementation
For each trend, identify key companies, technologies, and potential business impacts.
expected_output: >
A comprehensive markdown report with:
- Executive summary (5 bullet points)
- 5-7 major trends with supporting evidence
- For each trend: definition, examples, and business implications
- References to authoritative sources
```
### 2. 너무 많은 작업을 시도하는 "God Tasks"
**문제:** 여러 복잡한 작업을 하나의 지시 세트로 결합하는 태스크.
**잘못된 설계 예시:**
```yaml
comprehensive_task:
description: "Research market trends, analyze competitor strategies, create a marketing plan, and design a launch timeline."
```
**개선된 버전:**
이 작업을 순차적이고 집중된 태스크로 분리하세요:
```yaml
# Task 1: Research
market_research_task:
description: "Research current market trends in the SaaS project management space."
expected_output: "A markdown summary of key market trends."
# Task 2: Competitive Analysis
competitor_analysis_task:
description: "Analyze strategies of the top 3 competitors based on the market research."
expected_output: "A comparison table of competitor strategies."
context: [market_research_task]
# Continue with additional focused tasks...
```
### 3. 설명과 기대 출력 불일치
**문제:** 작업 설명에서 요구하는 내용과 기대 출력이 서로 다릅니다.
**설계가 미흡한 예시:**
```yaml
analysis_task:
description: "Analyze customer feedback to find areas of improvement."
expected_output: "A marketing plan for the next quarter."
```
**개선된 버전:**
```yaml
analysis_task:
description: "Analyze customer feedback to identify the top 3 areas for product improvement."
expected_output: "A report listing the 3 priority improvement areas with supporting customer quotes and data points."
```
### 4. 당신이 직접 프로세스를 이해하지 못함
**문제:** 당신이 완전히 이해하지 못하는 작업을 에이전트에게 수행하도록 요청함.
**해결책:**
1. 먼저 직접 작업을 수동으로 수행해보세요
2. 프로세스, 의사결정 지점, 정보 출처를 문서화하세요
3. 이 문서를 작업 설명의 기초로 사용하세요
### 5. 계층 구조의 조기 사용
**문제:** 순차적인 프로세스만으로도 충분한 경우에 불필요하게 복잡한 에이전트 계층 구조를 만드는 것.
**해결 방법:** 우선 순차적 프로세스부터 시작하고, 워크플로우의 복잡성이 정말로 필요할 때만 계층적 모델로 전환하세요.
### 6. 모호하거나 일반적인 에이전트 정의
**문제:** 일반적인 에이전트 정의는 일반적인 결과로 이어집니다.
**잘못된 설계 예시:**
```yaml
agent:
role: "Business Analyst"
goal: "Analyze business data"
backstory: "You are good at business analysis."
```
**개선된 버전:**
```yaml
agent:
role: "SaaS Metrics Specialist focusing on growth-stage startups"
goal: "Identify actionable insights from business data that can directly impact customer retention and revenue growth"
backstory: "With 10+ years analyzing SaaS business models, you've developed a keen eye for the metrics that truly matter for sustainable growth. You've helped numerous companies identify the leverage points that turned around their business trajectory. You believe in connecting data to specific, actionable recommendations rather than general observations."
```
## 고급 에이전트 설계 전략
### 협업을 위한 설계
여러 agent가 crew 내에서 함께 작업할 때 다음 사항을 고려하십시오:
- **상호 보완적인 스킬**: 각기 다르지만 상호 보완되는 능력을 가진 agent를 설계하세요.
- **업무 인계 시점**: agent 간에 작업이 어떻게 전달될지 명확한 인터페이스를 정의하세요.
- **건설적인 긴장감**: 때때로 약간씩 다른 관점을 가진 agent를 만들면 생산적인 대화를 통해 더 나은 결과를 이끌어낼 수 있습니다.
예를 들어, 콘텐츠 제작 crew는 다음과 같이 구성될 수 있습니다:
```yaml
# Research Agent
role: "Research Specialist for technical topics"
goal: "Gather comprehensive, accurate information from authoritative sources"
backstory: "You are a meticulous researcher with a background in library science..."
# Writer Agent
role: "Technical Content Writer"
goal: "Transform research into engaging, clear content that educates and informs"
backstory: "You are an experienced writer who excels at explaining complex concepts..."
# Editor Agent
role: "Content Quality Editor"
goal: "Ensure content is accurate, well-structured, and polished while maintaining consistency"
backstory: "With years of experience in publishing, you have a keen eye for detail..."
```
### 전문화된 도구 사용자 생성
일부 agent는 특정 도구를 효과적으로 활용하도록 특별히 설계될 수 있습니다:
```yaml
role: "Data Analysis Specialist"
goal: "Derive meaningful insights from complex datasets through statistical analysis"
backstory: "With a background in data science, you excel at working with structured and unstructured data..."
tools: [PythonREPLTool, DataVisualizationTool, CSVAnalysisTool]
```
### LLM 기능에 맞춘 에이전트 맞춤화
다양한 LLM은 서로 다른 강점을 가지고 있습니다. 이러한 기능을 염두에 두고 에이전트를 설계하세요:
```yaml
# For complex reasoning tasks
analyst:
role: "Data Insights Analyst"
goal: "..."
backstory: "..."
llm: openai/gpt-4o
# For creative content
writer:
role: "Creative Content Writer"
goal: "..."
backstory: "..."
llm: anthropic/claude-3-opus
```
## 에이전트 설계 테스트 및 반복
에이전트 설계는 종종 반복적인 과정입니다. 다음은 실용적인 접근 방식입니다:
1. **프로토타입으로 시작하기**: 초기 에이전트 정의 생성
2. **샘플 작업으로 테스트하기**: 대표적인 작업에서 성능 평가
3. **출력물 분석**: 강점과 약점 파악
4. **정의 수정**: 관찰에 따라 역할, 목표, 백스토리 조정
5. **협업 테스트**: 에이전트가 crew 환경에서 어떻게 작동하는지 평가
## 결론
효과적인 agent를 만드는 것은 예술이자 과학입니다. 여러분의 특정 요구에 맞춘 역할, 목표, 그리고 backstory를 신중하게 정의하고, 잘 설계된 task와 결합함으로써 뛰어난 결과를 만들어내는 전문화된 AI 협업자를 만들 수 있습니다.
agent와 task의 설계는 반복적인 과정임을 기억하세요. 이러한 모범 사례로 시작하여 agent가 실제로 동작하는 모습을 관찰하고, 배운 점을 바탕으로 접근 방식을 개선하세요. 그리고 항상 80/20 법칙을 명심하세요. agent로부터 최고의 결과를 얻기 위해서는 명확하고 집중된 task를 만드는 데 대부분의 노력을 집중하는 것이 중요합니다.
<Check>
축하합니다! 이제 효과적인 agent 설계의 원칙과 실천법을 이해하셨습니다. 이 기술들을 적용하여 강력하고 전문화된 agent들이 복잡한 task를 매끄럽게 협력하여 완수할 수 있도록 만드세요.
</Check>
## 다음 단계
- 특정 사용 사례에 맞는 다양한 agent 구성을 실험해 보세요
- [첫 crew 만들기](/ko/guides/crews/first-crew)에 대해 배우며 agent들이 어떻게 함께 작동하는지 확인해 보세요
- 더 발전된 오케스트레이션을 위해 [CrewAI Flows](/ko/guides/flows/first-flow)를 탐색해 보세요

View File

@@ -0,0 +1,61 @@
---
title: 코딩 도구
description: AGENTS.md를 사용하여 CrewAI 프로젝트 전반에서 코딩 에이전트와 IDE를 안내합니다.
icon: terminal
mode: "wide"
---
## AGENTS.md를 사용하는 이유
`AGENTS.md`는 가벼운 저장소 로컬 지침 파일로, 코딩 에이전트에게 일관되고 프로젝트별 안내를 제공합니다. 프로젝트 루트에 배치하고 어시스턴트가 작업하는 방식(컨벤션, 명령어, 아키텍처 노트, 가드레일)에 대한 신뢰할 수 있는 소스로 활용하세요.
## CLI로 프로젝트 생성
CrewAI CLI를 사용하여 프로젝트를 스캐폴딩하면, `AGENTS.md`가 루트에 자동으로 추가됩니다.
```bash
# Crew
crewai create crew my_crew
# Flow
crewai create flow my_flow
# Tool repository
crewai tool create my_tool
```
## 도구 설정: 어시스턴트에 AGENTS.md 연결
### Codex
Codex는 저장소에 배치된 `AGENTS.md` 파일로 안내할 수 있습니다. 컨벤션, 명령어, 워크플로우 기대치 등 지속적인 프로젝트 컨텍스트를 제공하는 데 사용하세요.
### Claude Code
Claude Code는 프로젝트 메모리를 `CLAUDE.md`에 저장합니다. `/init`으로 부트스트랩하고 `/memory`로 편집할 수 있습니다. Claude Code는 `CLAUDE.md` 내에서 임포트도 지원하므로, `@AGENTS.md`와 같은 한 줄을 추가하여 공유 지침을 중복 없이 가져올 수 있습니다.
간단하게 다음과 같이 사용할 수 있습니다:
```bash
mv AGENTS.md CLAUDE.md
```
### Gemini CLI와 Google Antigravity
Gemini CLI와 Antigravity는 저장소 루트 및 상위 디렉토리에서 프로젝트 컨텍스트 파일(기본값: `GEMINI.md`)을 로드합니다. Gemini CLI 설정에서 `context.fileName`을 설정하여 `AGENTS.md`를 대신(또는 추가로) 읽도록 구성할 수 있습니다. 예를 들어, `AGENTS.md`만 설정하거나 각 도구의 형식을 유지하고 싶다면 `AGENTS.md`와 `GEMINI.md`를 모두 포함할 수 있습니다.
간단하게 다음과 같이 사용할 수 있습니다:
```bash
mv AGENTS.md GEMINI.md
```
### Cursor
Cursor는 `AGENTS.md`를 프로젝트 지침 파일로 지원합니다. 프로젝트 루트에 배치하여 Cursor의 코딩 어시스턴트에 안내를 제공하세요.
### Windsurf
Claude Code는 Windsurf와의 공식 통합을 제공합니다. Windsurf 내에서 Claude Code를 사용하는 경우, 위의 Claude Code 안내를 따르고 `CLAUDE.md`에서 `AGENTS.md`를 임포트하세요.
Windsurf의 네이티브 어시스턴트를 사용하는 경우, 프로젝트 규칙 또는 지침 기능(사용 가능한 경우)을 구성하여 `AGENTS.md`에서 읽거나 내용을 직접 붙여넣으세요.

View File

@@ -0,0 +1,214 @@
---
title: "AI와 함께 빌드하기"
description: "CrewAI로 빌드·배포·확장하는 데 필요한 모든 것 — 스킬, 기계가 읽을 수 있는 문서, 배포, 엔터프라이즈 기능을 AI 코딩 에이전트용으로 정리했습니다."
icon: robot
mode: "wide"
---
# AI와 함께 빌드하기
CrewAI는 AI 네이티브입니다. 이 페이지는 Claude Code, Codex, Cursor, Gemini CLI 등 개발자가 crew와 flow를 배포하도록 돕는 코딩 에이전트가 CrewAI로 빌드할 때 필요한 내용을 한곳에 모았습니다.
### 지원 코딩 에이전트
<CardGroup cols={5}>
<Card title="Claude Code" icon="message-bot" color="#D97706" />
<Card title="Cursor" icon="arrow-pointer" color="#3B82F6" />
<Card title="Codex" icon="terminal" color="#10B981" />
<Card title="Windsurf" icon="wind" color="#06B6D4" />
<Card title="Gemini CLI" icon="sparkles" color="#8B5CF6" />
</CardGroup>
<Note>
이 페이지는 사람과 AI 어시스턴트 모두를 위해 작성되었습니다. 코딩 에이전트라면 CrewAI 맥락은 **Skills**부터, 전체 문서 접근은 **llms.txt**를 사용하세요.
</Note>
---
## 1. Skills — 에이전트에게 CrewAI 가르치기
**Skills**는 코딩 에이전트에게 Flow 스캐폴딩, Crew 구성, 도구 사용, 프레임워크 관례 등 CrewAI에 대한 깊은 지식을 담은 지침 묶음입니다.
<Tabs>
<Tab title="Claude Code (플러그인 마켓플레이스)">
<img src="https://cdn.simpleicons.org/anthropic/D97706" alt="Anthropic" width="28" style={{display: "inline", verticalAlign: "middle", marginRight: "8px"}} />
CrewAI 스킬은 **Claude Code 플러그인 마켓플레이스**에서 제공됩니다. AI 네이티브 기업들이 쓰는 것과 같은 배포 채널입니다.
```shell
/plugin marketplace add crewAIInc/skills
/plugin install crewai-skills@crewai-plugins
/reload-plugins
```
CrewAI와 관련된 질문을 하면 다음 네 가지 스킬이 자동으로 활성화됩니다.
| 스킬 | 실행 시점 |
|------|-------------|
| `getting-started` | 새 프로젝트 스캐폴딩, `LLM.call()` / `Agent` / `Crew` / `Flow` 선택, `crew.jsonc` / `main.py` 연결 |
| `design-agent` | 에이전트 구성 — 역할, 목표, 배경 이야기, 도구, LLM, 메모리, 가드레일 |
| `design-task` | 태스크 설명, 의존성, 구조화된 출력(`output_pydantic`, `output_json`), 사람 검토 |
| `ask-docs` | 최신 API 정보를 위해 [CrewAI 문서 MCP 서버](https://docs.crewai.com/mcp) 조회 |
</Tab>
<Tab title="npx (모든 에이전트)">
Claude Code, Codex, Cursor, Gemini CLI 등 모든 코딩 에이전트에서 사용할 수 있습니다.
```shell
npx skills add crewaiinc/skills
```
[skills.sh 레지스트리](https://skills.sh/crewaiinc/skills)에서 가져옵니다.
</Tab>
</Tabs>
<Steps>
<Step title="공식 스킬 팩 설치">
위 방법 중 하나를 사용하세요 — Claude Code 플러그인 마켓플레이스 또는 `npx skills add`. 둘 다 공식 [crewAIInc/skills](https://github.com/crewAIInc/skills) 팩을 설치합니다.
</Step>
<Step title="에이전트가 즉시 CrewAI 전문성을 갖춤">
스킬 팩이 에이전트에게 알려 주는 내용:
- **Flow** — 상태ful 앱, 단계, crew 킥오프
- **Crew 및 에이전트** — JSON-first 패턴(`crew.jsonc`, `agents/*.jsonc`), 역할, 태스크, 위임
- **도구 및 통합** — 검색, API, MCP 서버, 일반적인 CrewAI 도구
- **프로젝트 레이아웃** — CLI 스캐폴드와 저장소 관례
- **최신 패턴** — 현재 CrewAI 문서와 모범 사례 반영
</Step>
<Step title="빌드 시작">
매 세션마다 프레임워크를 다시 설명하지 않아도 에이전트가 CrewAI 프로젝트를 스캐폴딩하고 빌드할 수 있습니다.
</Step>
</Steps>
<CardGroup cols={2}>
<Card title="Skills 개념" icon="bolt" href="/ko/concepts/skills">
CrewAI 에이전트에서 스킬이 동작하는 방식 — 주입, 활성화, 패턴.
</Card>
<Card title="Skills 랜딩 페이지" icon="wand-magic-sparkles" href="/ko/skills">
crewAIInc/skills 팩 개요와 포함 내용.
</Card>
<Card title="AGENTS.md 및 코딩 도구" icon="terminal" href="/ko/guides/coding-tools/agents-md">
Claude Code, Codex, Cursor, Gemini CLI용 AGENTS.md 설정.
</Card>
<Card title="Skills 레지스트리 (skills.sh)" icon="globe" href="https://skills.sh/crewaiinc/skills">
공식 목록 — 스킬, 설치 통계, 감사 정보.
</Card>
</CardGroup>
---
## 2. llms.txt — 기계가 읽을 수 있는 문서
CrewAI는 AI 어시스턴트가 전체 문서에 기계가 읽을 수 있는 형태로 바로 접근할 수 있도록 `llms.txt` 파일을 제공합니다.
```
https://docs.crewai.com/llms.txt
```
<Tabs>
<Tab title="llms.txt란?">
[`llms.txt`](https://llmstxt.org/)는 문서를 대규모 언어 모델이 소비하기 쉽게 만드는 새로운 표준입니다. HTML을 스크래핑하는 대신, 필요한 내용이 담긴 하나의 구조화된 텍스트 파일을 가져올 수 있습니다.
CrewAI의 `llms.txt`는 **이미 제공 중**이며, 에이전트가 바로 사용할 수 있습니다.
</Tab>
<Tab title="사용 방법">
CrewAI 참고 문서가 필요할 때 코딩 에이전트에 URL을 알려 주세요.
```
Fetch https://docs.crewai.com/llms.txt for CrewAI documentation.
```
Claude Code, Cursor 등 많은 코딩 에이전트가 URL을 직접 가져올 수 있습니다. 파일에는 CrewAI 개념, API, 가이드를 아우르는 구조화된 문서가 포함되어 있습니다.
</Tab>
<Tab title="왜 중요한가">
- **스크래핑 불필요** — 한 번의 요청으로 깔끔한 구조화 콘텐츠
- **항상 최신** — docs.crewai.com에서 직접 제공
- **LLM에 최적화** — 브라우저가 아니라 컨텍스트 윈도우에 맞게 포맷
- **스킬과 상호 보완** — 스킬은 패턴을, llms.txt는 참조를 제공
</Tab>
</Tabs>
---
## 3. 엔터프라이즈에 배포
로컬 crew를 몇 분 안에 **CrewAI AMP**(Agent Management Platform) 프로덕션으로 가져가세요.
<Steps>
<Step title="로컬에서 빌드">
crew 또는 flow를 스캐폴딩하고 테스트합니다.
```bash
crewai create crew my_crew
cd my_crew
crewai run
```
</Step>
<Step title="배포 준비">
프로젝트 구조가 준비되었는지 확인합니다.
```bash
crewai deploy --prepare
```
구조와 요구 사항은 [준비 가이드](/ko/enterprise/guides/prepare-for-deployment)를 참고하세요.
</Step>
<Step title="AMP에 배포">
CrewAI AMP 플랫폼으로 푸시합니다.
```bash
crewai deploy
```
[GitHub 연동](/ko/enterprise/guides/deploy-to-amp) 또는 [Crew Studio](/ko/enterprise/guides/enable-crew-studio)로도 배포할 수 있습니다.
</Step>
<Step title="API로 접근">
배포된 crew는 REST API 엔드포인트를 받습니다. 모든 애플리케이션에 통합할 수 있습니다.
```bash
curl -X POST https://app.crewai.com/api/v1/crews/<crew-id>/kickoff \
-H "Authorization: Bearer $CREWAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"inputs": {"topic": "AI agents"}}'
```
</Step>
</Steps>
<CardGroup cols={2}>
<Card title="AMP에 배포" icon="rocket" href="/ko/enterprise/guides/deploy-to-amp">
전체 배포 가이드 — CLI, GitHub, Crew Studio 방법.
</Card>
<Card title="AMP 소개" icon="globe" href="/ko/enterprise/introduction">
플랫폼 개요 — 프로덕션 crew에 AMP가 제공하는 것.
</Card>
</CardGroup>
---
## 4. 엔터프라이즈 기능
CrewAI AMP는 프로덕션 팀을 위해 만들어졌습니다. 배포 외에 제공되는 것은 다음과 같습니다.
<CardGroup cols={2}>
<Card title="관측 가능성" icon="chart-line">
모든 crew 실행에 대한 상세 실행 추적, 로그, 성능 지표. 에이전트 결정, 도구 호출, 태스크 완료를 실시간으로 모니터링합니다.
</Card>
<Card title="Crew Studio" icon="paintbrush">
시각적으로 crew를 만들고, 맞춤 설정하고, 배포하는 노코드/로코드 인터페이스 — 코드로 보내거나 바로 배포할 수 있습니다.
</Card>
<Card title="웹훅 스트리밍" icon="webhook">
crew 실행에서 실시간 이벤트를 시스템으로 스트리밍합니다. Slack, Zapier 등 웹훅 소비자와 연동할 수 있습니다.
</Card>
<Card title="팀 관리" icon="users">
SSO, RBAC, 조직 단위 제어. 팀 전체에서 crew 생성·배포·접근 권한을 관리합니다.
</Card>
<Card title="도구 저장소" icon="toolbox">
조직 전체에 맞춤 도구를 게시하고 공유합니다. 레지스트리에서 커뮤니티 도구를 설치합니다.
</Card>
<Card title="Factory(셀프 호스팅)" icon="server">
자체 인프라에서 CrewAI AMP를 실행합니다. 데이터 상주와 규정 준수 제어와 함께 플랫폼 전체 기능을 사용할 수 있습니다.
</Card>
</CardGroup>
<AccordionGroup>
<Accordion title="AMP는 누구를 위한 것인가요?">
AI 에이전트 워크플로를 프로토타입에서 프로덕션으로 옮겨야 하는 팀을 위한 제품입니다. 관측 가능성, 접근 제어, 확장 가능한 인프라를 제공합니다. 스타트업이든 대기업이든 운영 복잡도는 AMP가 맡고, 에이전트 구축에 집중할 수 있습니다.
</Accordion>
<Accordion title="배포 옵션은 무엇이 있나요?">
- **클라우드 (app.crewai.com)** — CrewAI가 관리, 프로덕션까지 가장 빠른 경로
- **Factory(셀프 호스팅)** — 데이터 통제를 위해 자체 인프라에서 실행
- **하이브리드** — 민감도에 따라 클라우드와 셀프 호스팅을 혼합
</Accordion>
</AccordionGroup>
<Card title="CrewAI AMP 살펴보기 →" icon="arrow-right" href="https://app.crewai.com">
가입하고 첫 crew를 프로덕션에 배포해 보세요.
</Card>

View File

@@ -0,0 +1,504 @@
---
title: CrewAI 사용 사례 평가
description: AI 애플리케이션 요구 사항을 평가하고 복잡성과 정밀도 요구 사항에 따라 Crews와 Flows 중 올바른 접근 방식을 선택하는 방법을 알아보세요.
icon: scale-balanced
mode: "wide"
---
## 의사결정 프레임워크 이해하기
CrewAI로 AI 애플리케이션을 구축할 때 가장 중요한 결정 중 하나는 특정 사용 사례에 적합한 방식을 선택하는 것입니다. Crew를 사용할까요? Flow를 사용할까요? 아니면 둘의 조합을 사용할까요? 이 가이드는 요구 사항을 평가하고 정보에 기반한 아키텍처 결정을 내리는 데 도움이 됩니다.
이 결정의 핵심은 애플리케이션에서의 **복잡성**과 **정밀성**의 관계를 이해하는 것입니다:
<Frame caption="CrewAI 애플리케이션을 위한 복잡성 vs. 정밀성 매트릭스">
<img src="/images/complexity_precision.png" alt="복잡성 vs. 정밀성 매트릭스" />
</Frame>
이 매트릭스를 통해 다양한 방식이 복잡성과 정밀성에 대한 요구 사항과 어떻게 일치하는지 시각적으로 확인할 수 있습니다. 각 사분면이 의미하는 바와 그것이 아키텍처 선택에 어떻게 도움이 되는지 함께 살펴보겠습니다.
## 복잡성-정밀도 행렬 설명
### 복잡성이란 무엇인가?
CrewAI 애플리케이션의 맥락에서 **복잡성**은 다음을 의미합니다:
- 요구되는 뚜렷한 단계 또는 작업 수
- 수행해야 할 작업의 다양성
- 서로 다른 구성 요소 간의 상호 의존성
- 조건부 로직과 분기의 필요성
- 전체 워크플로우의 정교함
### 정밀성이란 무엇인가?
**정밀성**은 이 맥락에서 다음을 의미합니다:
- 최종 결과물에 요구되는 정확성
- 구조화되고 예측 가능한 결과의 필요성
- 재현성의 중요성
- 각 단계에 대한 통제 수준
- 출력의 변동 허용치
### 네 가지 사분면
#### 1. 낮은 복잡도, 낮은 정밀도
**특징:**
- 단순하고 직관적인 작업
- 출력 결과의 일부 변형 허용
- 제한된 단계 수
- 창의적이거나 탐색적인 응용
**권장 접근법:** 최소한의 에이전트를 가진 Simple Crews
**예시 사용 사례:**
- 기본 콘텐츠 생성
- 아이디어 브레인스토밍
- 간단한 요약 작업
- 창의적 글쓰기 보조
#### 2. 낮은 복잡성, 높은 정밀도
**특징:**
- 정확하고 구조화된 결과물이 요구되는 단순한 워크플로우
- 재현 가능한 결과가 필요한 경우
- 단계는 제한적이지만, 높은 정확도가 요구됨
- 주로 데이터 처리 또는 변환이 포함됨
**권장 방식:** 직접적인 LLM 호출이나 구조화된 출력이 있는 간단한 Crew 사용
**예시 활용 사례:**
- 데이터 추출 및 변환
- 양식 작성 및 검증
- 구조화된 콘텐츠 생성(JSON, XML)
- 단순 분류 작업
#### 3. 높은 복잡성, 낮은 정밀도
**특징:**
- 여러 단계로 이루어진 다단계 프로세스
- 창의적이거나 탐색적인 출력물
- 구성 요소 간의 복잡한 상호작용
- 최종 결과의 변동성 허용
**권장 접근 방식:** 여러 전문화된 agent가 포함된 Complex Crew
**예시 사용 사례:**
- 연구 및 분석
- 콘텐츠 생성 파이프라인
- 탐색적 데이터 분석
- 창의적 문제 해결
#### 4. 높은 복잡성, 높은 정밀도
**특징:**
- 구조화된 산출물이 요구되는 복잡한 워크플로
- 엄격한 정확성 요구사항을 가진 여러 상호 의존적인 단계
- 정교한 처리와 정밀한 결과 모두 필요
- 종종 임무에 중요한 애플리케이션
**권장 접근 방식:** 검증 단계를 포함한 여러 Crew를 오케스트레이션하는 Flows
**예시 사용 사례:**
- 엔터프라이즈 의사결정 지원 시스템
- 복잡한 데이터 처리 파이프라인
- 다단계 문서 처리
- 규제 산업 애플리케이션
## 크루와 플로우 중에서 선택하기
### Crews를 선택해야 할 때
Crews는 다음과 같은 경우에 이상적입니다:
1. **협업 지능이 필요할 때** - 서로 다른 전문성을 가진 여러 agent들이 함께 작업해야 할 때
2. **문제가 창발적 사고를 요구할 때** - 다양한 관점과 접근 방식에서의 해결책이 이득이 될 때
3. **작업이 주로 창의적이거나 분석적일 때** - 작업이 리서치, 콘텐츠 제작, 분석을 포함할 때
4. **엄격한 구조보다는 적응력을 중시할 때** - agent의 자율성이 workflow에 도움이 될 때
5. **출력 형식이 다소 유연할 수 있을 때** - 출력 구조에 약간의 변동이 허용될 때
```python
# Example: Research Crew for market analysis
from crewai import Agent, Crew, Process, Task
# Create specialized agents
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."
)
# Define their tasks
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]
)
# Create the crew
market_analysis_crew = Crew(
agents=[researcher, analyst],
tasks=[research_task, analysis_task],
process=Process.sequential,
verbose=True
)
# Run the crew
result = market_analysis_crew.kickoff()
```
### 플로우를 선택해야 할 때
플로우는 다음과 같은 경우에 이상적입니다:
1. **실행에 대한 정밀한 제어가 필요할 때** - 워크플로우에 정확한 순서 지정과 상태 관리가 필요한 경우
2. **애플리케이션에 복잡한 상태 요구사항이 있을 때** - 여러 단계에 걸쳐 상태를 유지하고 변환해야 하는 경우
3. **구조화되고 예측 가능한 출력이 필요할 때** - 애플리케이션에서 일관되고 포맷된 결과가 필요한 경우
4. **워크플로우에 조건부 로직이 포함될 때** - 중간 결과에 따라 다른 경로를 선택해야 하는 경우
5. **AI와 절차적 코드를 결합해야 할 때** - 솔루션에 AI 기능과 전통적인 프로그래밍이 모두 필요한 경우
```python
# Example: Customer Support Flow with structured processing
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
from typing import List, Dict
# Define structured state
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):
# In a real app, this might come from an 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, _):
# Use a direct LLM call for categorization
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):
# Route to different handlers based on category
return category.lower().replace(" ", "_")
@listen("billing")
def handle_billing_issue(self):
# Handle billing-specific logic
self.state.priority = "high"
# More billing-specific processing...
return "Billing issue handled"
@listen("account_access")
def handle_access_issue(self):
# Handle access-specific logic
self.state.priority = "high"
# More access-specific processing...
return "Access issue handled"
# Additional category handlers...
@listen("billing", "account_access", "technical_issue", "feature_request", "other")
def resolve_ticket(self, resolution_info):
# Final resolution step
self.state.resolution = f"Issue resolved: {resolution_info}"
return self.state.resolution
# Run the flow
support_flow = CustomerSupportFlow()
result = support_flow.kickoff()
```
### 크루와 플로우를 결합해야 할 때
가장 정교한 애플리케이션은 종종 크루와 플로우를 결합할 때 이점을 얻습니다:
1. **복잡한 다단계 프로세스** - 플로우를 사용해 전체 프로세스를 오케스트레이션하고, 크루를 통해 복잡한 하위 작업을 처리합니다.
2. **창의성과 구조가 모두 필요한 애플리케이션** - 창의적인 작업에는 크루를 사용하고, 구조적인 처리는 플로우로 처리합니다.
3. **엔터프라이즈급 AI 애플리케이션** - 플로우로 상태 및 프로세스 흐름을 관리하면서, 크루를 활용해 특화된 작업을 수행합니다.
```python
# Example: Content Production Pipeline combining Crews and Flows
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):
# Set initial parameters
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, _):
# Use a research crew to create an outline
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
)
# Run the crew and store the result
result = outline_crew.kickoff()
# Parse the outline (in a real app, you might use a more robust parsing approach)
import json
try:
self.state.outline = json.loads(result.raw)
except:
# Fallback if not valid JSON
self.state.outline = {"sections": result.raw}
return "Outline created"
@listen(create_outline)
def write_content(self, _):
# Use a writing crew to create the content
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
)
# Run the crew and store the result
result = writing_crew.kickoff()
self.state.final_content = result.raw
return "Content created"
@listen(write_content)
def optimize_for_seo(self, _):
# Use a direct LLM call for SEO optimization
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]}... (truncated for brevity)
Format your response as JSON with the following structure:
{{
"score": 85,
"recommendations": [
"Recommendation 1",
"Recommendation 2",
"Recommendation 3"
]
}}
"""
seo_analysis = llm.call(prompt)
# Parse the SEO analysis
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"]}
# Run the flow
content_flow = ContentProductionFlow()
result = content_flow.kickoff()
```
## 실용적인 평가 프레임워크
특정 사용 사례에 맞는 올바른 접근 방식을 결정하려면 다음 단계별 평가 프레임워크를 따르세요:
### 1단계: 복잡성 평가
아래와 같은 기준으로 애플리케이션의 복잡성을 1~10점 척도로 평가하세요:
1. **단계 수**: 얼마나 많은 개별 작업이 필요한가요?
- 1-3단계: 낮은 복잡성 (1-3)
- 4-7단계: 중간 복잡성 (4-7)
- 8단계 이상: 높은 복잡성 (8-10)
2. **상호 의존성**: 서로 다른 부분 간의 연결성은 어느 정도인가요?
- 의존성이 거의 없음: 낮은 복잡성 (1-3)
- 다소 의존성 있음: 중간 복잡성 (4-7)
- 복잡한 다중 의존성: 높은 복잡성 (8-10)
3. **조건부 논리**: 얼마나 많은 분기 및 의사결정이 필요한가요?
- 선형 프로세스: 낮은 복잡성 (1-3)
- 분기가 일부 있음: 중간 복잡성 (4-7)
- 복잡한 결정 트리: 높은 복잡성 (8-10)
4. **도메인 지식**: 요구되는 지식의 전문성은 어느 정도인가요?
- 일반적인 지식: 낮은 복잡성 (1-3)
- 일부 전문 지식 필요: 중간 복잡성 (4-7)
- 여러 도메인에 대한 깊은 전문성 필요: 높은 복잡성 (8-10)
평균 점수를 계산하여 전체 복잡성을 결정하세요.
### 2단계: 정밀도 요구사항 평가
정밀도 요구사항을 1-10점 척도로 평가하세요. 다음을 고려합니다:
1. **출력 구조**: 출력이 얼마나 구조화되어야 합니까?
- 자유형 텍스트: 낮은 정밀도 (1-3)
- 반구조화: 중간 정밀도 (4-7)
- 엄격한 포맷(JSON, XML): 높은 정밀도 (8-10)
2. **정확성 필요성**: 사실적 정확성이 얼마나 중요합니까?
- 창의적 콘텐츠: 낮은 정밀도 (1-3)
- 정보성 콘텐츠: 중간 정밀도 (4-7)
- 중요한 정보: 높은 정밀도 (8-10)
3. **재현성**: 실행마다 결과가 얼마나 일관되어야 합니까?
- 변동 허용: 낮은 정밀도 (1-3)
- 어느 정도 일관성 필요: 중간 정밀도 (4-7)
- 정확한 재현성 필요: 높은 정밀도 (8-10)
4. **오류 허용도**: 오류의 영향은 어느 정도입니까?
- 영향 적음: 낮은 정밀도 (1-3)
- 영향 보통: 중간 정밀도 (4-7)
- 영향 큼: 높은 정밀도 (8-10)
평균 점수를 계산하여 전체 정밀도 요구사항을 결정하세요.
### 3단계: 매트릭스에 매핑하기
복잡도와 정밀도 점수를 매트릭스에 표시하세요:
- **낮은 복잡도(1-4), 낮은 정밀도(1-4)**: Simple Crews
- **낮은 복잡도(1-4), 높은 정밀도(5-10)**: 직접적인 LLM 호출이 있는 Flows
- **높은 복잡도(5-10), 낮은 정밀도(1-4)**: Complex Crews
- **높은 복잡도(5-10), 높은 정밀도(5-10)**: Crews를 오케스트레이션하는 Flows
### 4단계: 추가 요소 고려
복잡성과 정밀성 외에도 다음을 고려하세요:
1. **개발 시간**: crew는 프로토타입을 더 빠르게 만들 수 있습니다
2. **유지보수 필요**: flow는 장기적인 유지보수에 더 적합합니다
3. **팀 전문성**: 팀이 다양한 접근법에 얼마나 익숙한지 고려하세요
4. **확장성 요구 사항**: flow는 일반적으로 복잡한 애플리케이션에 더 잘 확장됩니다
5. **통합 필요**: 솔루션이 기존 시스템과 어떻게 통합될지 고려하세요
## 결론
Crews와 Flows 중에서 선택하거나 결합하는 것은 CrewAI 애플리케이션의 효과성, 유지 관리성, 확장성에 영향을 미치는 중요한 아키텍처적 결정입니다. 복잡성과 정밀성이라는 차원에서 사용 사례를 평가함으로써, 귀하의 특정 요구 사항에 부합하는 정보에 기반한 결정을 내릴 수 있습니다.
가장 좋은 접근방식은 애플리케이션이 성숙해지면서 종종 진화한다는 점을 기억하세요. 귀하의 요구를 충족하는 가장 간단한 해결책으로 시작하고, 경험이 쌓이고 요구 사항이 명확해지면 아키텍처를 개선할 준비를 하세요.
<Check>
이제 CrewAI 사용 사례를 평가하고, 복잡성과 정밀성 요구 사항에 따라 올바른 접근법을 선택할 수 있는 프레임워크를 갖추게 되었습니다. 이를 통해 보다 효과적이고 유지 관리 가능하며 확장성 있는 AI 애플리케이션을 구축할 수 있습니다.
</Check>
## 다음 단계
- [효과적인 에이전트 만들기](/ko/guides/agents/crafting-effective-agents)에 대해 더 알아보기
- [처음으로 crew 만들기](/ko/guides/crews/first-crew) 살펴보기
- [flow 상태 관리 마스터하기](/ko/guides/flows/mastering-flow-state)에 깊이 파고들기
- 더 깊은 이해를 위해 [핵심 개념](/ko/concepts/agents) 확인하기

View File

@@ -0,0 +1,140 @@
---
title: 첫 번째 Crew 만들기
description: JSON-first crew 설정으로 협업 AI 팀을 만드는 단계별 튜토리얼입니다.
icon: users-gear
mode: "wide"
---
## 리서치 Crew 만들기
이 가이드에서는 두 에이전트가 주제를 조사하고 markdown 보고서를 작성하는 crew를 만듭니다. 새 crew 프로젝트는 JSON-first입니다. 에이전트는 `agents/*.jsonc`, 태스크와 crew 설정은 `crew.jsonc`에 두며, `crewai run`이 이 정의를 직접 로드합니다.
### 준비 사항
1. [설치 가이드](/ko/installation)에 따라 CrewAI 설치
2. [LLM 설정](/ko/concepts/llms#setting-up-your-llm)에 따라 모델 API 키 설정
3. 웹 검색을 사용할 경우 [Serper.dev](https://serper.dev/) API 키 준비
## 1단계: 새 Crew 만들기
```bash
crewai create crew research_crew
cd research_crew
```
생성되는 구조:
```text
research_crew/
├── .gitignore
├── .env
├── agents/
│ └── researcher.jsonc
├── crew.jsonc
├── knowledge/
├── pyproject.toml
├── README.md
├── skills/
└── tools/
```
<Tip>
`crew.py`, `config/agents.yaml`, `config/tasks.yaml`을 쓰는 기존 레이아웃이 필요하면 `crewai create crew research_crew --classic`을 사용하세요.
</Tip>
## 2단계: 에이전트 정의
생성된 `agents/researcher.jsonc` 파일을 교체하고 `agents/analyst.jsonc`를 추가합니다. 파일 이름이 `crew.jsonc`에서 참조하는 에이전트 이름입니다.
```jsonc agents/researcher.jsonc
{
"role": "Senior Research Specialist for {topic}",
"goal": "Find comprehensive and accurate information about {topic}, with a focus on recent developments and key insights.",
"backstory": "You are an experienced research specialist who organizes complex information into clear, useful notes.",
// 사용하는 모델로 바꾸세요. 예: "openai/gpt-4o".
"llm": "provider/model-id",
"tools": ["SerperDevTool"],
"settings": {
"verbose": true,
"allow_delegation": false
}
}
```
```jsonc agents/analyst.jsonc
{
"role": "Report Analyst for {topic}",
"goal": "Turn research findings into a clear, well-structured report.",
"backstory": "You are a careful analyst with strong technical writing skills and a talent for extracting useful insights.",
// 사용하는 모델로 바꾸세요. 예: "openai/gpt-4o".
"llm": "provider/model-id",
"settings": {
"verbose": true,
"allow_delegation": false
}
}
```
`provider/model-id`를 `openai/gpt-4o`, `anthropic/claude-sonnet-4-6`, `gemini/gemini-2.0-flash-001` 같은 모델로 바꾸세요.
## 3단계: 태스크와 Crew 설정
`crew.jsonc`를 다음으로 교체합니다:
```jsonc crew.jsonc
{
"name": "Research Crew",
"agents": ["researcher", "analyst"],
"tasks": [
{
"name": "research_task",
"description": "Conduct thorough research on {topic}. Focus on key concepts, recent developments, major challenges, notable applications, and future outlook.",
"expected_output": "A comprehensive research document with organized sections, specific facts, and useful examples about {topic}.",
"agent": "researcher"
},
{
"name": "analysis_task",
"description": "Analyze the research findings and create a polished report on {topic}. Include an executive summary, key insights, trend analysis, and recommendations.",
"expected_output": "A professional markdown report with clear headings, a concise summary, main findings, and recommendations.",
"agent": "analyst",
"context": ["research_task"],
"output_file": "output/report.md",
"markdown": true
}
],
"process": "sequential",
"verbose": true,
"memory": true,
"inputs": {
"topic": "Artificial Intelligence in Healthcare"
}
}
```
`context`는 이전 태스크 이름을 가리키므로 analyst가 research 태스크 출력을 받습니다. `inputs`는 `{topic}`의 기본값을 제공합니다. 기본값이 없으면 `crewai run`이 실행 중에 물어봅니다.
## 4단계: 환경 변수 설정
`.env`를 편집합니다:
```sh
SERPER_API_KEY=your_serper_api_key
# 모델 제공자 API 키도 추가하세요.
```
## 5단계: 설치 및 실행
```bash
crewai install
crewai run
```
실행이 끝나면 `output/report.md`를 확인하세요.
<Warning>
신뢰하는 출처의 JSON crew 프로젝트만 실행하세요. `custom:<name>` 도구와 `{"python": "module.attribute"}` 참조는 crew 로드 시 로컬 Python 코드를 실행합니다.
</Warning>
<Check>
주제를 조사하고 보고서를 작성하는 JSON-first crew를 만들었습니다.
</Check>

View File

@@ -0,0 +1,474 @@
---
title: 대화형 Flow
description: 턴마다 kickoff, 메시지 기록, 의도 라우팅, 트레이싱, WebSocket 브리지로 멀티턴 채팅 앱을 만듭니다.
icon: comments
mode: "wide"
---
## 개요
대화형 앱은 각 사용자 입력을 **동일한 세션 id**로 **새 flow 실행**으로 처리합니다. CrewAI는 메시지 기록, 선택적 의도 분류, 지연 트레이싱, UI 브리지, 그리고 대화형 flow용 로컬 `flow.chat()` REPL을 제공합니다.
| 개념 | 구현 |
|------|------|
| 세션 id | `handle_turn(..., session_id=...)` → `kickoff(inputs={"id": ...})` → `state.id` |
| 사용자 입력 | `handle_turn(message)`가 그래프 실행 전 `state.messages`에 추가 |
| 턴 완료 | `FlowFinished`는 **이번 실행**만 의미; 다음 `handle_turn`로 대화 계속 |
| 세션 전체 트레이스 | `ConversationConfig(defer_trace_finalization=True)` + `finalize_session_traces()` |
## 턴 API
REST, WebSocket, 테스트, 커스텀 UI에서 오는 모든 사용자 메시지에는 **`flow.handle_turn(message, session_id=...)`**를 사용하세요. 대화형 `Flow`를 로컬 터미널 채팅 루프로 실행하고 싶을 때는 **`flow.chat()`**을 사용하세요.
`Flow.kickoff()`는 `user_message=` 또는 `session_id=` 키워드 인자를 받지 않습니다. 대화형 flow에서는 `handle_turn()`이 보류 중인 메시지를 저장하고 내부적으로 `kickoff(inputs={"id": session_id})`를 호출합니다.
| API | 용도 |
|-----|------|
| `handle_turn(message, session_id=...)` | 대화형 `Flow`용 한 턴 편의 래퍼 |
| `chat()` | 대화형 `Flow`용 로컬 터미널 REPL |
| `kickoff(inputs={...})` | 대화형 턴 처리 없이 flow를 직접 실행 |
| `ask()` | 한 스텝 **내부** 블로킹 프롬프트 (마법사, 확인) |
| `@human_feedback` | **스텝 출력** 승인/거부 — 다음 채팅 줄이 아님 |
| `ChatSession.handle_turn(...)` | `handle_turn` 위의 전송 계층 (SSE / WebSocket) |
## 빠른 시작
```python
from uuid import uuid4
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
ConversationConfig,
ConversationState,
)
@ConversationConfig(defer_trace_finalization=True)
class SupportFlow(Flow[ConversationState]):
conversational = True
def route_turn(self, context):
message = self.state.current_user_message or ""
if "주문" in message or "order" in message.lower():
return "order"
if "안녕" in message or "goodbye" in message.lower():
return "goodbye"
return "help"
@listen("order")
def handle_order(self):
reply = "주문이 배송 중입니다."
self.append_assistant_message(reply)
return reply
@listen("help")
def handle_help(self):
reply = "무엇을 도와드릴까요?"
self.append_assistant_message(reply)
return reply
@listen("goodbye")
def handle_goodbye(self):
reply = "안녕히 가세요!"
self.append_assistant_message(reply)
return reply
session_id = str(uuid4())
flow = SupportFlow()
try:
flow.handle_turn("주문 어디까지 왔나요?", session_id=session_id)
flow.handle_turn("반품은 어떻게 하나요?", session_id=session_id)
finally:
flow.finalize_session_traces() # 전체 대화에 대한 단일 trace 링크
```
## 턴 생명주기
각 `handle_turn`은 다음 파이프라인을 실행합니다:
1. **`_configure_conversational_kickoff`** — `session_id` / `user_message`를 `inputs`에 병합, `ConversationalConfig` 적용, 설정 시 지연 트레이싱 활성화.
2. **상태 복원** — `inputs["id"]`가 있고 `@persist`가 설정되면 최신 스냅샷 로드.
3. **`FlowStarted`** — 지연 세션의 첫 턴에서만 발생.
4. **`prepare_conversational_turn`** — 사용자 메시지를 `state.messages`에 추가, `last_user_message` 설정, `last_intent` 초기화, `intents` / `default_intents` + `intent_llm` 설정 시 분류.
5. **그래프 실행** — `@start` → `@router` → `@listen` 핸들러.
6. **실행 종료** — 지연 활성화 시 턴별 `flow_finished` 및 trace 종료 **건너뜀**; 중첩 `Agent.kickoff()` / crew도 부모 batch를 닫지 않음.
핸들러는 **`append_assistant_message(reply)`**를 호출해 다음 턴의 `conversation_messages`에 어시스턴트 응답이 포함되게 하세요. 사용자 입력은 `handle_turn`이 이미 저장합니다 — 핸들러에서 다시 추가하지 마세요.
## `ConversationalConfig` (클래스 수준 기본값)
`Flow` 서브클래스에 `conversational_config: ClassVar[ConversationalConfig | None]`로 설정합니다.
| 필드 | 기본값 | 목적 |
|------|--------|------|
| `default_intents` | `None` | kickoff 전 자동 분류용 outcome 라벨 |
| `intent_llm` | `None` | 분류용 모델 (intent 사용 시 필수) |
| `interactive_prompt` | `"You: "` | `kickoff(interactive=True)` 프롬프트 |
| `interactive_timeout` | `None` | 대화형 모드 줄 단위 타임아웃 |
| `exit_commands` | `exit`, `quit` | 대화형 모드 종료 단어 |
| `defer_trace_finalization` | `True` | 턴 간 하나의 trace batch 유지 |
`intents=` 및 `intent_llm=` 키워드로 kickoff마다 재정의할 수 있습니다.
## `ChatState` (권장 persist 형태)
```python
from crewai.flow import ChatState
class MyChatState(ChatState):
# 상속: id, messages, last_user_message, last_intent, session_ready
research_turn_count: int = 0
custom_flag: bool = False
```
| 필드 | 역할 |
|------|------|
| `id` | 세션 UUID (`session_id` / `inputs["id"]`와 동일) |
| `messages` | LLM 기록용 `{role, content}` 리스트 |
| `last_user_message` | 이번 턴의 최신 사용자 입력 |
| `last_intent` | 분류 후 라우트 라벨 (사용 시) |
| `session_ready` | 일회성 bootstrap 플래그 |
`ConversationalInputs`는 `kickoff(inputs={...})`용 `TypedDict`: `id`, `user_message`, `last_intent`.
## `Flow` 대화 API
### `kickoff` / `kickoff_async` 파라미터
| 파라미터 | 목적 |
|----------|------|
| `user_message` | 이번 턴 텍스트 (또는 `{"role": "user", "content": "..."}`) |
| `session_id` | 대화 UUID → `inputs["id"]` / `state.id` |
| `intents` | kickoff 전 `classify_intent`용 outcome 라벨 |
| `intent_llm` | 분류 LLM (`intents`와 함께 필수) |
| `interactive` | `ask()` CLI 루프 (로컬 데모 전용) |
| `interactive_prompt` | 대화형 모드 프롬프트 |
| `interactive_timeout` | 줄 단위 `ask()` 타임아웃 |
| `exit_commands` | 대화형 모드 종료 단어 |
| `inputs` | 추가 상태 필드 |
| `restore_from_state_id` | 다른 persist flow에서 fork 복원 |
### 인스턴스 속성
| 속성 | 목적 |
|------|------|
| `conversational_config` | 클래스 수준 `ConversationalConfig` |
| `defer_trace_finalization` | 인스턴스 플래그; kickoff 시 config에서 자동 설정 |
| `suppress_flow_events` | 콘솔 flow 패널 숨김; **트레이싱은 계속 기록** |
| `stream` | 스트리밍; `ChatSession.handle_turn(..., stream=True)`와 함께 |
### 메서드 및 프로퍼티
| 이름 | 설명 |
|------|------|
| `append_message(role, content, **extra)` | `state.messages`에 추가 |
| `conversation_messages` | LLM 호출용 읽기 전용 기록 |
| `classify_intent(text, outcomes, *, llm, context=None)` | outcome 매핑 (`@human_feedback`와 동일 collapse) |
| `receive_user_message(text, *, outcomes=None, llm=None)` | 사용자 메시지 추가; 선택적 `last_intent` |
| `finalize_session_traces()` | 지연 `flow_finished` 발생 및 세션 trace batch 종료 |
| `_should_defer_trace_finalization()` | 턴별 trace 종료 지연 여부 |
| `input_history` | `ask()` 프롬프트/응답 감사 기록 |
### 모듈 헬퍼 (`crewai.flow.conversation`)
테스트 또는 커스텀 오케스트레이션용:
| 함수 | 설명 |
|------|------|
| `normalize_kickoff_inputs(...)` | 대화 kwargs를 `inputs`에 병합 |
| `get_conversation_messages(flow)` | 상태 또는 내부 버퍼에서 메시지 읽기 |
| `append_message(flow, ...)` | 인스턴스 메서드와 동일 |
| `prepare_conversational_turn(flow, ...)` | 턴 수화 (보통 kickoff가 호출) |
| `receive_user_message(flow, ...)` | 인스턴스 메서드와 동일 |
| `set_state_field(flow, name, value)` | dict 또는 Pydantic 상태 필드 설정 |
| `get_conversational_config(flow)` | 클래스 `conversational_config` 읽기 |
| `input_history_to_messages(entries)` | `input_history`를 LLM 메시지 형식으로 |
## 의도 라우팅 패턴
### A. `ConversationalConfig`로 사전 분류 (가장 단순)
`default_intents`와 `intent_llm` 설정. 각 kickoff가 `@router` 전에 분류; `route()`에서 `self.state.last_intent` 읽기.
### B. `@router` 내부에서 분류 (풍부한 프롬프트)
`default_intents=None`으로 kickoff는 메시지만 추가. `route()`에서 커스텀 프롬프트로 `classify_intent` 호출:
```python
@router(bootstrap)
def route(self):
intent = self.classify_intent(
self._routing_prompt(self.state.last_user_message),
("GREETING", "ORDER", "RESEARCH", "GOODBYE"),
llm=self.conversational_config.intent_llm or "gpt-4o-mini",
)
self.state.last_intent = intent
return intent
```
웹 리서치나 다단계 tool이 필요하면 **`@listen("RESEARCH")`** 등에서 `Agent.kickoff()`와 tool 사용 — 단순 `LLM.call()` 대신.
## flow가 끝났지만 사용자는 계속 대화할 때
`FlowFinished`는 **이번 그래프 실행**이 완료됨을 의미합니다. 같은 `session_id`로 또 다른 `kickoff`로 대화가 이어집니다. `@persist`가 `messages`, 플래그, 컨텍스트를 복원합니다.
**Persist 패턴:** 전체 `Flow` 클래스보다 **단일 종료 스텝**(예: `finalize`)에 `@persist`를 두는 것이 좋습니다. 클래스 수준 persist는 매 메서드 후 저장하며, `load_state`는 최신 행을 사용해 같은 턴의 핸들러 업데이트를 놓칠 수 있습니다.
후속 채팅 줄에 `@human_feedback`를 쓰지 마세요. 특정 스텝 출력을 사람이 승인해야 할 때만 사용하세요.
## 대화형 `Flow` (실험적)
<Warning>
**실험적 기능입니다.** 대화형 `Flow`의 API 표면(`conversational = True`,
`handle_turn`, `ConversationConfig`, `RouterConfig`, `ConversationState`,
내장 그래프와 헬퍼)은 `crewai.experimental` 하위에 있으며 정식 출시
전까지 변경될 수 있습니다. 특정 동작에 의존한다면 CrewAI 버전을 고정하고
변경 사항이 있는지 changelog를 확인하세요. 피드백과 이슈 환영합니다.
</Warning>
`Flow` 서브클래스에 `conversational = True`를 지정하면 대화형 챗 그래프가 활성화됩니다. 베이스 `Flow`가 `@start` / `@router` / `converse_turn` / `end_conversation` 그래프를 노출하고, `state.messages`를 관리하며, router LLM을 구동하고, 턴 간 trace 배치를 열린 상태로 유지합니다. 여러분은 **커스텀 라우트**만 작성하면 되고, 나머지는 프레임워크가 담당합니다.
LLM 기반 라우터와 라우트별 핸들러로 멀티턴 챗을 만들고 싶지만 라이프사이클을 직접 배선하고 싶지 않을 때 사용하세요. 완전한 제어가 필요하면 위의 `Flow[ChatState]`로 내려가세요.
### 빠른 예제
```python
from crewai import LLM, Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
ConversationConfig,
ConversationState,
RouterConfig,
)
ROUTER_LLM = LLM(model="gpt-4o-mini")
@ConversationConfig(
system_prompt="A multi-agent assistant for ordinary chat and tool-backed tasks.",
llm=ROUTER_LLM,
router=RouterConfig(), # 라우트 + 설명은 @listen 핸들러에서 자동 발견
)
class SupportFlow(Flow[ConversationState]):
conversational = True
@listen("INTERNET_SEARCH")
def handle_internet_search(self) -> str:
"""Fresh web research, current news, real-time lookups."""
...
self.append_assistant_message(reply)
return reply
@listen("CREWAI_DOCS")
def handle_crewai_docs(self) -> str:
"""Look up the CrewAI documentation for framework/API questions."""
...
self.append_assistant_message(reply)
return reply
flow = SupportFlow()
try:
flow.handle_turn("뭘 할 수 있어?") # converse(빌트인)로 라우팅
flow.handle_turn("AI 뉴스를 웹에서 찾아줘.") # INTERNET_SEARCH로 라우팅
flow.handle_turn("첫 번째 결과를 요약해줘.") # 다시 converse로 라우팅
finally:
flow.finalize_session_traces()
```
로컬 터미널 채팅에는 `chat()`을 사용하세요:
```python
def kickoff() -> None:
SupportFlow().chat()
```
`chat()`은 `handle_turn()`을 REPL로 감싸고, `exit` / `quit`에서 종료하며, 기본적으로 빈 줄을 건너뛰고, 세션이 끝날 때 `finalize_session_traces()`를 호출합니다.
### `ConversationConfig`
클래스 단위의 챗 기본값을 부착하는 클래스 데코레이터입니다.
| 필드 | 기본값 | 목적 |
|------|--------|------|
| `system_prompt` | i18n `slices.conversational_system_prompt` | 빌트인 `converse_turn`이 사용하는 system 메시지. 빈 문자열(`""`)을 전달하면 system 메시지를 끕니다. |
| `llm` | `None` | 대화용 LLM (빌트인 `converse_turn`이 사용하고 router 폴백도 됨). |
| `router` | `None` | LLM 기반 라우팅을 위한 `RouterConfig`. 없으면 항상 `converse`로 떨어집니다. |
| `answer_from_history_prompt` | 프레임워크 기본값 | 선택적인 `answer_from_history` 라우트용 system 메시지. |
| `answer_from_history_llm` | `None` | 설정되면 `answer_from_history` 단축 경로가 활성화됩니다. |
| `intent_llm` | `None` | 레거시 `intents=`/`default_intents` 사전 분류용 LLM. |
| `default_intents` | `None` | 레거시 사전 분류용 outcome 레이블. |
| `visible_agent_outputs` | `None` | `"all"` 또는 `append_agent_result()` 결과를 사용자에게 공개로 승격할 에이전트 이름 목록. |
| `defer_trace_finalization` | `True` | `handle_turn()` 호출들 사이에서 하나의 trace 배치를 열어 둡니다. |
### `RouterConfig`와 자동 생성되는 라우트 카탈로그
```python
RouterConfig(
prompt="선택적인 도메인 프레이밍 (정책, 톤, 페르소나).",
response_format=MyRoute, # 선택; 없으면 자동 생성
llm=ROUTER_LLM, # ConversationConfig.llm으로 폴백
routes=["INTERNET_SEARCH", "CREWAI_DOCS"], # 선택; 리스너에서 추론
route_descriptions={
"INTERNET_SEARCH": "이 라우트만 docstring 대신 사용할 설명.",
},
default_intent="converse", # LLM 호출 실패 또는 LLM 없음일 때 사용
fallback_intent="converse", # LLM이 잘못된 라우트를 반환할 때 사용
intent_field="intent",
)
```
router에 전달되는 프롬프트는 자동으로 만들어집니다. 각 라우트의 설명은 다음 우선순위로 결정됩니다:
1. `RouterConfig.route_descriptions[label]` — 명시적 오버라이드.
2. `Flow.builtin_route_descriptions[label]` — `converse`, `end`, `answer_from_history`용 프레임워크 캐닝 텍스트 (router LLM용으로 다듬어진 문구).
3. `@listen(label)` 핸들러 docstring의 첫 줄(비어있지 않은 줄).
4. 빈 문자열 (라우트만 카탈로그에 등장하고 설명은 없음).
실제 사용에서 **새 라우트를 추가하는 방법은 `@listen("X")` + 한 줄짜리 docstring**입니다:
```python
@listen("INTERNET_SEARCH")
def handle_internet_search(self) -> str:
"""Fresh web research, current news, real-time lookups."""
...
```
…그러면 router LLM은 다음을 봅니다:
```
Routes:
- CREWAI_DOCS: Look up the CrewAI documentation for framework/API questions.
- INTERNET_SEARCH: Fresh web research, current news, real-time lookups.
- converse: Ordinary chat, follow-ups, summaries, clarifications…
- end: User signals the conversation is finished (goodbye, exit, done).
```
`RouterConfig.prompt`는 **도메인 프레이밍** (어시스턴트 페르소나, 비즈니스 규칙, 톤)을 위한 자리입니다. 라우트 카탈로그는 자동 생성되니 `prompt` 안에 라우트 목록을 넣지 마세요. 핸들러를 추가하는 순간 동기화가 깨집니다.
### 빌트인 라우트
| 라우트 | 핸들러 | 목적 |
|--------|--------|------|
| `converse` | `converse_turn` | 기본 챗 핸들러. system prompt + 정식 메시지 히스토리와 함께 `ConversationConfig.llm`을 호출합니다. |
| `end` | `end_conversation` | `state.ended = True`로 설정하고 종료 응답을 보냅니다. |
| `answer_from_history` | `answer_from_history_turn` | 선택적. `ConversationConfig.answer_from_history_llm`이 설정되어 있고 메시지를 히스토리만으로 답할 수 있을 때 라우팅됩니다. |
서브클래스에 같은 이름의 핸들러를 정의하면 어떤 것이든 오버라이드할 수 있습니다.
### `handle_turn()` 시맨틱
`flow.handle_turn(message)`는 한 턴을 실행합니다:
1. 그래프가 다시 실행되도록 턴 단위 실행 추적(`_completed_methods`, `_method_outputs`)을 초기화합니다 — 이게 없으면 동일 인스턴스에서 반복 `kickoff` 호출 시 `Flow.kickoff_async`가 `inputs={"id": ...}`를 체크포인트 복원으로 간주해 2번째 턴부터 단락 회로가 발생합니다.
2. 사용자 메시지를 `state.messages`에 추가하고 `current_user_message` / `last_user_message`를 설정합니다. `last_intent`는 **이전 턴 값이 유지**되어 router LLM이 신호로 활용할 수 있습니다.
3. `conversation_start` → `route_conversation` → 선택된 `@listen` 핸들러 순으로 실행됩니다.
4. router는 결정을 `state.last_intent`에 저장합니다 (다음 턴의 router 컨텍스트에서 보입니다).
5. 핸들러가 문자열을 반환했지만 `append_assistant_message`를 직접 호출하지 않았다면, `handle_turn`이 대신 추가해 줍니다.
채팅 메시지에는 `handle_turn()`을 호출하세요. `kickoff(inputs={"id": ...})`를 직접 호출하면 대화형 턴 래퍼 없이 flow 그래프가 실행됩니다.
### 로컬 REPL용 `chat()`
`flow.chat()`은 `handle_turn()` 위에 얹은 바로 쓸 수 있는 터미널 래퍼입니다:
```python
flow = SupportFlow()
flow.chat()
```
일반적인 로컬 루프를 처리합니다:
1. 사용자 메시지를 입력받습니다.
2. `exit` / `quit`, `EOFError`, `KeyboardInterrupt`에서 멈춥니다.
3. `handle_turn(message, session_id=...)`를 호출합니다.
4. 어시스턴트 결과를 출력합니다.
5. `finally` 블록에서 지연된 세션 trace를 finalize합니다.
주입 가능한 I/O로 터미널 동작을 커스터마이즈할 수 있습니다:
```python
flow.chat(
session_id="demo-session",
prompt="You: ",
assistant_prefix="Assistant: ",
exit_commands=("exit", "quit", "bye"),
)
```
웹 앱, 백그라운드 worker, 테스트, 커스텀 transport에서는 계속 `handle_turn()`을 직접 사용하세요.
### 커스텀 router 동작
매 라우팅 결정마다 사이드 이펙트(이벤트 버스 셋업, 텔레메트리)를 실행하려면 `route_turn`을 오버라이드하세요:
```python
class SupportFlow(Flow[ConversationState]):
conversational = True
def route_turn(self, context: dict[str, Any]) -> str | None:
self.event_bus = MyBus(self)
return super().route_turn(context)
```
LLM router를 우회해 프로그램적으로 라우트를 선택하려면 `route_turn`에서 문자열을 반환하세요. `None`을 반환하면 `_route_with_config(...)`로 떨어집니다.
### `append_assistant_message`와 `append_agent_result`
`@listen(label)` 핸들러 안에서 두 가지 중 선택하세요:
- `self.append_assistant_message(text)` — 사용자에게 보이는 어시스턴트 턴을 `state.messages`에 추가합니다. 다음 턴의 `converse_turn`이 이 내용을 보게 됩니다.
- `self.append_agent_result(agent_name, result, visibility="private")` — 구조화된 이벤트를 `state.events`에, 스레드를 `state.agent_threads[agent_name]`에 기록합니다. public 가시성은 자동으로 `append_assistant_message`도 호출합니다. 정식 히스토리를 더럽히지 말아야 할 임시 작업에는 private을 쓰세요.
`ConversationConfig.visible_agent_outputs`로 특정 에이전트의 private 결과를 전역적으로 public으로 승격할 수 있습니다 (`"all"` 또는 이름 리스트).
## 턴 간 트레이싱
`defer_trace_finalization=True` (`ConversationalConfig` 기본값):
- 채팅 세션 전체에 **하나의 trace batch**.
- 첫 턴에만 **`flow_started`**; `finalize_session_traces()`에서 **`flow_finished`** 한 번.
- 턴별 `kickoff`는 “Trace batch finalized”를 출력하지 않음.
- **중첩 작업** (`Agent.kickoff()`, crew, Exa tool)은 **부모** batch에 추가; 내부 `AgentExecutor` flow가 세션 batch를 조기 종료하지 않음.
```python
flow.chat(session_id=session_id)
```
`flow.chat()`이 `finalize_session_traces()`를 대신 호출합니다. `handle_turn()`이나 `kickoff(...)`로 직접 루프를 소유하는 경우, 세션이 끝날 때 `finalize_session_traces()`를 호출하세요.
`suppress_flow_events=True`는 Rich 콘솔 패널만 숨깁니다. trace 및 method 이벤트는 계속 발생합니다.
### 대화형 `Flow` trace 수명 주기
실험적 [대화형 `Flow`](#대화형-flow-실험적)는 동일한 tracing 수명 주기를 따릅니다. `defer_trace_finalization` 기본값이 `True`이므로 각 `handle_turn()`이 세션 trace를 열어 둡니다. 세션 끝에서 항상 finalize하세요 — REPL/루프를 `try/finally`로 감싸고 종료 시 `flow.finalize_session_traces()`를 호출하세요. 호출하지 않으면 batch가 열린 채 남아 마지막 대화가 export되지 않을 수 있습니다.
## 스트리밍
`Flow` 클래스에 `stream = True`. `kickoff(...)`가 표준 이벤트 버스를 통해 `assistant_delta` 등 이벤트를 발생시킵니다.
## import
```python
from crewai.flow import (
ChatState,
ConversationalConfig,
ConversationalInputs,
Flow,
listen,
persist,
router,
start,
)
```
## 참고
- [Flow 상태 관리 마스터하기](/ko/guides/flows/mastering-flow-state)
- [첫 Flow 만들기](/ko/guides/flows/first-flow)
- 데모: `lib/crewai/runner_conversational_flow_simple.py`

View File

@@ -0,0 +1,550 @@
---
title: 첫 Flow 빌드하기
description: 정밀한 실행 제어가 가능한 구조화된 이벤트 기반 워크플로우를 만드는 방법을 배웁니다.
icon: diagram-project
mode: "wide"
---
## Flows로 AI 워크플로우 제어하기
CrewAI Flows는 AI 오케스트레이션의 새로운 수준을 제공합니다. 즉, AI agent crew의 협업 능력과 절차적 프로그래밍의 정밀성 및 유연성을 결합합니다. crew가 agent 협업에서 탁월하다면, flow는 AI 시스템의 다양한 구성요소가 어떻게 그리고 언제 상호작용하는지에 대해 세밀하게 제어할 수 있게 해줍니다.
이 가이드에서는 원하는 주제에 대한 포괄적인 학습 가이드를 생성하는 강력한 CrewAI Flow를 만드는 과정을 소개합니다. 이 튜토리얼을 통해 Flow가 일반 코드, 직접적인 LLM 호출, crew 기반 처리 등을 결합하여 AI 워크플로우에 구조적이고 이벤트 기반의 제어를 제공하는 방법을 시연할 것입니다.
### 플로우의 강력한 점
플로우를 통해 다음과 같은 작업을 할 수 있습니다:
1. **다양한 AI 상호작용 패턴 결합** - 복잡한 협업 작업에는 crew를 사용하고, 더 단순한 작업에는 직접적인 LLM 호출과 절차적 논리에는 일반 코드를 사용하세요.
2. **이벤트 기반 시스템 구축** - 구성 요소가 특정 이벤트와 데이터 변경에 어떻게 반응할지 정의할 수 있습니다.
3. **구성 요소 간 상태 유지** - 애플리케이션의 다양한 부분 간에 데이터를 공유하고 변환할 수 있습니다.
4. **외부 시스템과 통합** - 데이터베이스, API, 사용자 인터페이스와 같은 외부 시스템과 AI 워크플로우를 원활하게 연동할 수 있습니다.
5. **복잡한 실행 경로 생성** - 조건부 분기, 병렬 처리 및 동적인 워크플로우를 설계할 수 있습니다.
### 무엇을 구축하고 배우게 될까요
이 가이드가 끝나면 여러분은 다음을 달성할 수 있습니다:
1. **사용자 입력, AI 계획, 그리고 멀티 에이전트 콘텐츠 생성이 결합된 정교한 콘텐츠 생성 시스템을 구축**했습니다.
2. **시스템의 다양한 구성 요소 간 정보 흐름을 오케스트레이션(조율)**했습니다.
3. **이전 단계의 완료에 따라 각 단계가 반응하는 이벤트 기반 아키텍처를 구현**했습니다.
4. **더 복잡한 AI 애플리케이션을 확장하고 맞춤화할 수 있는 기반을 구축**했습니다.
이번 가이드의 creator flow는 다음과 같은 훨씬 더 발전된 애플리케이션에 적용할 수 있는 기본 패턴을 보여줍니다:
- 여러 전문화된 하위 시스템을 결합하는 대화형 AI assistant
- AI 기반 변환을 포함한 복잡한 데이터 처리 파이프라인
- 외부 서비스 및 API와 통합되는 자율적 에이전트
- 인간이 개입하는 프로세스를 포함한 다단계 의사결정 시스템
함께 여러분의 첫 번째 flow를 만들어 봅시다!
## 사전 준비 사항
시작하기 전에 다음을 확인하세요:
1. [설치 가이드](/ko/installation)에 따라 CrewAI를 설치했는지 확인하십시오.
2. [LLM 설정 가이드](/ko/concepts/llms#setting-up-your-llm)에 따라 환경에 LLM API 키를 설정했는지 확인하십시오.
3. Python에 대한 기본적인 이해
## 1단계: 새로운 CrewAI Flow 프로젝트 생성
먼저, CLI를 사용하여 새로운 CrewAI Flow 프로젝트를 생성해봅시다. 이 명령어는 필요한 모든 디렉터리와 템플릿 파일이 포함된 기본 프로젝트 구조를 만들어줍니다.
```bash
crewai create flow guide_creator_flow
cd guide_creator_flow
```
이렇게 하면 flow에 필요한 기본 구조를 가진 프로젝트가 생성됩니다.
<Frame caption="CrewAI Framework 개요">
<img src="/images/flows.png" alt="CrewAI Framework 개요" />
</Frame>
## 2단계: 프로젝트 구조 이해하기
생성된 프로젝트는 다음과 같은 구조를 가지고 있습니다. 시작용 embedded crew는 클래식 Python/YAML 레이아웃을 사용합니다. Flow 안에서 JSON-first crew를 사용하려면 crew 폴더에 `crew.jsonc`와 `agents/*.jsonc`를 만들고 `crewai.project.load_crew`로 로드하세요. 예시는 [Flows](/ko/concepts/flows#building-your-crews)를 참고하세요.
```
guide_creator_flow/
├── .gitignore
├── pyproject.toml
├── README.md
├── .env
└── src/
└── guide_creator_flow/
├── __init__.py
├── main.py
├── crews/
│ └── poem_crew/
│ ├── config/
│ │ ├── agents.yaml
│ │ └── tasks.yaml
│ └── poem_crew.py
└── tools/
└── custom_tool.py
```
이 구조는 flow의 다양한 구성 요소를 명확하게 분리해줍니다:
- `src/guide_creator_flow/main.py` 파일의 main flow 로직
- `src/guide_creator_flow/crews` 디렉터리의 특화된 crew들
- `src/guide_creator_flow/tools` 디렉터리의 custom tool들
이제 이 구조를 수정하여 guide creator flow를 만들 것입니다. 이 flow는 포괄적인 학습 가이드 생성을 조직하는 역할을 합니다.
## 3단계: Content Writer Crew 추가
우리 flow에는 콘텐츠 생성 프로세스를 처리할 전문화된 crew가 필요합니다. CrewAI CLI를 사용하여 content writer crew를 추가해봅시다:
```bash
crewai flow add-crew content-crew
```
이 명령어는 자동으로 crew에 필요한 디렉터리와 템플릿 파일을 생성합니다. content writer crew는 가이드의 각 섹션을 작성하고 검토하는 역할을 담당하며, 메인 애플리케이션에 의해 조율되는 전체 flow 내에서 작업하게 됩니다.
## 4단계: 콘텐츠 작가 Crew 구성
이제 콘텐츠 작가 crew를 JSONC로 구성합니다. 가이드의 고품질 콘텐츠를 만들기 위해 협업하는 두 명의 전문 에이전트 - 작가와 리뷰어 - 를 설정합니다.
1. `src/guide_creator_flow/crews/content_crew/agents/content_writer.jsonc`를 만듭니다:
```jsonc
{
"role": "Educational Content Writer",
"goal": "Create engaging, informative content that thoroughly explains the assigned topic and provides valuable insights to the reader.",
"backstory": "You are a talented educational writer who explains complex concepts in accessible language and organizes information clearly.",
"llm": "provider/model-id",
"settings": {
"verbose": true
}
}
```
2. `src/guide_creator_flow/crews/content_crew/agents/content_reviewer.jsonc`를 만듭니다:
```jsonc
{
"role": "Educational Content Reviewer and Editor",
"goal": "Ensure content is accurate, comprehensive, well-structured, and consistent with previously written sections.",
"backstory": "You are a meticulous editor with an eye for detail, clarity, and coherence.",
"llm": "provider/model-id",
"settings": {
"verbose": true
}
}
```
`provider/model-id`를 사용하는 모델로 바꾸세요. 예: `openai/gpt-4o`, `gemini/gemini-2.0-flash-001`, `anthropic/claude-sonnet-4-6`.
3. `src/guide_creator_flow/crews/content_crew/crew.jsonc`를 만듭니다:
```jsonc
{
"name": "Content Crew",
"agents": ["content_writer", "content_reviewer"],
"tasks": [
{
"name": "write_section_task",
"description": "Write a comprehensive section on the topic: \"{section_title}\".\n\nSection description: {section_description}\nTarget audience: {audience_level} level learners\n\nYour content should begin with a brief introduction, explain key concepts clearly with examples, include practical applications where appropriate, end with a summary, and be approximately 500-800 words.\n\nPreviously written sections:\n{previous_sections}",
"expected_output": "A well-structured, comprehensive section in Markdown format that thoroughly explains the topic and is appropriate for the target audience.",
"agent": "content_writer",
"markdown": true
},
{
"name": "review_section_task",
"description": "Review and improve this section on \"{section_title}\":\n\n{draft_content}\n\nTarget audience: {audience_level} level learners\nPreviously written sections:\n{previous_sections}\n\nFix errors, improve clarity, verify consistency, enhance structure, and add missing key information.",
"expected_output": "An improved, polished version of the section that maintains the original structure but enhances clarity, accuracy, and consistency.",
"agent": "content_reviewer",
"context": ["write_section_task"],
"markdown": true
}
],
"process": "sequential",
"verbose": true
}
```
`context` 필드를 통해 리뷰어가 작가의 출력을 사용할 수 있습니다.
4. `src/guide_creator_flow/crews/content_crew/content_crew.py`를 작은 loader로 교체합니다:
```python
from pathlib import Path
from crewai.project import load_crew
def kickoff_content_crew(inputs: dict):
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
return crew.kickoff(inputs={**default_inputs, **inputs})
```
이 loader는 런타임에 `crew.jsonc`를 `Crew`로 바꿉니다. 이 crew는 독립적으로도 작동할 수 있지만, 우리의 플로우에서는 더 큰 시스템의 일부로 오케스트레이션됩니다.
## 5단계: 플로우(Flow) 생성
이제 가장 흥미로운 부분입니다 - 전체 가이드 생성 과정을 오케스트레이션할 플로우를 만드는 단계입니다. 이곳에서 우리는 일반 Python 코드, 직접적인 LLM 호출, 그리고 우리의 컨텐츠 제작 crew를 결합하여 일관된 시스템으로 만듭니다.
우리의 플로우는 다음과 같은 일을 수행합니다:
1. 주제와 대상 독자 수준에 대한 사용자 입력을 받습니다.
2. 구조화된 가이드 개요를 만들기 위해 직접 LLM 호출을 합니다.
3. 컨텐츠 writer crew를 사용하여 각 섹션을 순차적으로 처리합니다.
4. 모든 내용을 결합하여 최종 종합 문서를 완성합니다.
`main.py` 파일에 우리의 플로우를 생성해봅시다:
```python
#!/usr/bin/env python
import json
import os
from typing import List, Dict
from pydantic import BaseModel, Field
from crewai import LLM
from crewai.flow.flow import Flow, listen, start
from guide_creator_flow.crews.content_crew.content_crew import kickoff_content_crew
# Define our models for structured data
class Section(BaseModel):
title: str = Field(description="Title of the section")
description: str = Field(description="Brief description of what the section should cover")
class GuideOutline(BaseModel):
title: str = Field(description="Title of the guide")
introduction: str = Field(description="Introduction to the topic")
target_audience: str = Field(description="Description of the target audience")
sections: List[Section] = Field(description="List of sections in the guide")
conclusion: str = Field(description="Conclusion or summary of the guide")
# Define our flow state
class GuideCreatorState(BaseModel):
topic: str = ""
audience_level: str = ""
guide_outline: GuideOutline = None
sections_content: Dict[str, str] = {}
class GuideCreatorFlow(Flow[GuideCreatorState]):
"""Flow for creating a comprehensive guide on any topic"""
@start()
def get_user_input(self):
"""Get input from the user about the guide topic and audience"""
print("\n=== Create Your Comprehensive Guide ===\n")
# Get user input
self.state.topic = input("What topic would you like to create a guide for? ")
# Get audience level with validation
while True:
audience = input("Who is your target audience? (beginner/intermediate/advanced) ").lower()
if audience in ["beginner", "intermediate", "advanced"]:
self.state.audience_level = audience
break
print("Please enter 'beginner', 'intermediate', or 'advanced'")
print(f"\nCreating a guide on {self.state.topic} for {self.state.audience_level} audience...\n")
return self.state
@listen(get_user_input)
def create_guide_outline(self, state):
"""Create a structured outline for the guide using a direct LLM call"""
print("Creating guide outline...")
# Initialize the LLM
llm = LLM(model="openai/gpt-4o-mini", response_format=GuideOutline)
# Create the messages for the outline
messages = [
{"role": "system", "content": "You are a helpful assistant designed to output JSON."},
{"role": "user", "content": f"""
Create a detailed outline for a comprehensive guide on "{state.topic}" for {state.audience_level} level learners.
The outline should include:
1. A compelling title for the guide
2. An introduction to the topic
3. 4-6 main sections that cover the most important aspects of the topic
4. A conclusion or summary
For each section, provide a clear title and a brief description of what it should cover.
"""}
]
# Make the LLM call with JSON response format
response = llm.call(messages=messages)
# Parse the JSON response
outline_dict = json.loads(response)
self.state.guide_outline = GuideOutline(**outline_dict)
# Ensure output directory exists before saving
os.makedirs("output", exist_ok=True)
# Save the outline to a file
with open("output/guide_outline.json", "w") as f:
json.dump(outline_dict, f, indent=2)
print(f"Guide outline created with {len(self.state.guide_outline.sections)} sections")
return self.state.guide_outline
@listen(create_guide_outline)
def write_and_compile_guide(self, outline):
"""Write all sections and compile the guide"""
print("Writing guide sections and compiling...")
completed_sections = []
# Process sections one by one to maintain context flow
for section in outline.sections:
print(f"Processing section: {section.title}")
# Build context from previous sections
previous_sections_text = ""
if completed_sections:
previous_sections_text = "# Previously Written Sections\n\n"
for title in completed_sections:
previous_sections_text += f"## {title}\n\n"
previous_sections_text += self.state.sections_content.get(title, "") + "\n\n"
else:
previous_sections_text = "No previous sections written yet."
# Run the content crew for this section
result = kickoff_content_crew(inputs={
"section_title": section.title,
"section_description": section.description,
"audience_level": self.state.audience_level,
"previous_sections": previous_sections_text,
"draft_content": ""
})
# Store the content
self.state.sections_content[section.title] = result.raw
completed_sections.append(section.title)
print(f"Section completed: {section.title}")
# Compile the final guide
guide_content = f"# {outline.title}\n\n"
guide_content += f"## Introduction\n\n{outline.introduction}\n\n"
# Add each section in order
for section in outline.sections:
section_content = self.state.sections_content.get(section.title, "")
guide_content += f"\n\n{section_content}\n\n"
# Add conclusion
guide_content += f"## Conclusion\n\n{outline.conclusion}\n\n"
# Save the guide
with open("output/complete_guide.md", "w") as f:
f.write(guide_content)
print("\nComplete guide compiled and saved to output/complete_guide.md")
return "Guide creation completed successfully"
def kickoff():
"""Run the guide creator flow"""
GuideCreatorFlow().kickoff()
print("\n=== Flow Complete ===")
print("Your comprehensive guide is ready in the output directory.")
print("Open output/complete_guide.md to view it.")
def plot():
"""Generate a visualization of the flow"""
flow = GuideCreatorFlow()
flow.plot("guide_creator_flow")
print("Flow visualization saved to guide_creator_flow.html")
if __name__ == "__main__":
kickoff()
```
이 플로우에서 일어나는 과정을 분석해봅시다:
1. 구조화된 데이터에 대한 Pydantic 모델을 정의하여 타입 안전성과 명확한 데이터 표현을 보장합니다.
2. 플로우 단계별로 데이터를 유지하기 위한 state 클래스를 생성합니다.
3. 세 가지 주요 플로우 단계를 구현합니다:
- `@start()` 데코레이터로 사용자 입력을 받습니다.
- 직접 LLM 호출로 가이드 개요를 생성합니다.
- content crew로 각 섹션을 처리합니다.
4. `@listen()` 데코레이터를 활용해 단계 간 이벤트 기반 관계를 설정합니다.
이것이 바로 flows의 힘입니다 - 다양한 처리 유형(사용자 상호작용, 직접적인 LLM 호출, crew 기반 작업)을 하나의 일관된 이벤트 기반 시스템으로 결합할 수 있습니다.
## 6단계: 환경 변수 설정하기
프로젝트 루트에 `.env` 파일을 생성하고 API 키를 입력하세요. 공급자 구성에 대한 자세한 내용은 [LLM 설정 가이드](/ko/concepts/llms#setting-up-your-llm)를 참고하세요.
```sh .env
OPENAI_API_KEY=your_openai_api_key
# or
GEMINI_API_KEY=your_gemini_api_key
# or
ANTHROPIC_API_KEY=your_anthropic_api_key
```
## 7단계: 의존성 설치
필수 의존성을 설치합니다:
```bash
crewai install
```
## 8단계: Flow 실행하기
이제 여러분의 flow가 실제로 작동하는 모습을 볼 차례입니다! CrewAI CLI를 사용하여 flow를 실행하세요:
```bash
crewai flow kickoff
```
이 명령어를 실행하면 flow가 다음과 같이 작동하는 것을 확인할 수 있습니다:
1. 주제와 대상 수준을 입력하라는 메시지가 표시됩니다.
2. 가이드의 체계적인 개요를 생성합니다.
3. 각 섹션을 처리할 때 content writer와 reviewer가 협업합니다.
4. 마지막으로 모든 내용을 종합하여 완성도 높은 가이드를 만듭니다.
이는 여러 구성요소(인공지능 및 비인공지능 모두)가 포함된 복잡한 프로세스를 flows가 어떻게 조정할 수 있는지 보여줍니다.
## 9단계: Flow 시각화하기
flow의 강력한 기능 중 하나는 구조를 시각화할 수 있다는 점입니다.
```bash
crewai flow plot
```
이 명령은 flow의 구조를 보여주는 HTML 파일을 생성하며, 각 단계 간의 관계와 그 사이에 흐르는 데이터를 확인할 수 있습니다. 이러한 시각화는 복잡한 flow를 이해하고 디버깅하는 데 매우 유용합니다.
## 10단계: 출력물 검토하기
flow가 완료되면 `output` 디렉토리에서 두 개의 파일을 찾을 수 있습니다:
1. `guide_outline.json`: 가이드의 구조화된 개요가 포함되어 있습니다
2. `complete_guide.md`: 모든 섹션이 포함된 종합적인 가이드입니다
이 파일들을 잠시 검토하고 여러분이 구축한 시스템을 되돌아보세요. 이 시스템은 사용자 입력, 직접적인 AI 상호작용, 협업 에이전트 작업을 결합하여 복잡하고 고품질의 결과물을 만들어냅니다.
## 가능한 것의 예술: 첫 번째 Flow 그 이상
이 가이드에서 배운 내용은 훨씬 더 정교한 AI 시스템을 만드는 데 기반이 됩니다. 다음은 이 기본 flow를 확장할 수 있는 몇 가지 방법입니다:
### 사용자 상호작용 향상
더욱 인터랙티브한 플로우를 만들 수 있습니다:
- 입력 및 출력을 위한 웹 인터페이스
- 실시간 진행 상황 업데이트
- 인터랙티브한 피드백 및 개선 루프
- 다단계 사용자 상호작용
### 추가 처리 단계 추가하기
다음과 같은 추가 단계로 flow를 확장할 수 있습니다:
- 개요 작성 전 사전 리서치
- 일러스트를 위한 이미지 생성
- 기술 가이드용 코드 스니펫 생성
- 최종 품질 보증 및 사실 확인
### 더 복잡한 Flows 생성하기
더 정교한 flow 패턴을 구현할 수 있습니다:
- 사용자 선호도나 콘텐츠 유형에 따른 조건 분기
- 독립적인 섹션의 병렬 처리
- 피드백과 함께하는 반복적 개선 루프
- 외부 API 및 서비스와의 통합
### 다양한 도메인에 적용하기
동일한 패턴을 사용하여 다음과 같은 flow를 만들 수 있습니다:
- **대화형 스토리텔링**: 사용자 입력을 바탕으로 개인화된 이야기를 생성
- **비즈니스 인텔리전스**: 데이터를 처리하고, 인사이트를 도출하며, 리포트를 생성
- **제품 개발**: 아이디어 구상, 디자인, 기획을 지원
- **교육 시스템**: 개인화된 학습 경험을 제공
## 주요 특징 시연
이 guide creator flow에서는 CrewAI의 여러 강력한 기능을 시연합니다:
1. **사용자 상호작용**: flow는 사용자로부터 직접 입력을 수집합니다
2. **직접적인 LLM 호출**: 효율적이고 단일 목적의 AI 상호작용을 위해 LLM 클래스를 사용합니다
3. **Pydantic을 통한 구조화된 데이터**: 타입 안정성을 보장하기 위해 Pydantic 모델을 사용합니다
4. **컨텍스트를 활용한 순차 처리**: 섹션을 순서대로 작성하면서 이전 섹션을 컨텍스트로 제공합니다
5. **멀티 에이전트 crew**: 콘텐츠 생성을 위해 특화된 에이전트(writer 및 reviewer)를 활용합니다
6. **상태 관리**: 프로세스의 다양한 단계에 걸쳐 상태를 유지합니다
7. **이벤트 기반 아키텍처**: 이벤트에 응답하기 위해 `@listen` 데코레이터를 사용합니다
## 플로우 구조 이해하기
플로우의 주요 구성 요소를 분해하여 자신만의 플로우를 만드는 방법을 이해할 수 있도록 도와드리겠습니다:
### 1. 직접 LLM 호출
Flow를 사용하면 간단하고 구조화된 응답이 필요할 때 언어 모델에 직접 호출할 수 있습니다:
```python
llm = LLM(
model="model-id-here", # gpt-4o, gemini-2.0-flash, anthropic/claude...
response_format=GuideOutline
)
response = llm.call(messages=messages)
```
특정하고 구조화된 출력이 필요할 때 crew를 사용하는 것보다 더 효율적입니다.
### 2. 이벤트 기반 아키텍처
Flows는 데코레이터를 사용하여 컴포넌트 간의 관계를 설정합니다:
```python
@start()
def get_user_input(self):
# First step in the flow
# ...
@listen(get_user_input)
def create_guide_outline(self, state):
# This runs when get_user_input completes
# ...
```
이렇게 하면 애플리케이션에 명확하고 선언적인 구조가 만들어집니다.
### 3. 상태 관리
flow는 단계 간 상태를 유지하여 데이터를 쉽게 공유할 수 있습니다:
```python
class GuideCreatorState(BaseModel):
topic: str = ""
audience_level: str = ""
guide_outline: GuideOutline = None
sections_content: Dict[str, str] = {}
```
이 방식은 flow 전반에 걸쳐 데이터를 추적하고 변환하는 타입 안전(type-safe)한 방법을 제공합니다.
### 4. Crew 통합
Flow는 복잡한 협업 작업을 위해 crew와 원활하게 통합될 수 있습니다:
```python
result = kickoff_content_crew(inputs={
"section_title": section.title,
# ...
})
```
이를 통해 애플리케이션의 각 부분에 적합한 도구를 사용할 수 있습니다. 단순한 작업에는 직접적인 LLM 호출을, 복잡한 협업에는 crew를 사용할 수 있습니다.
## 다음 단계
이제 첫 번째 flow를 구축했으니 다음을 시도해 볼 수 있습니다:
1. 더 복잡한 flow 구조와 패턴을 실험해 보세요.
2. `@router()`를 사용하여 flow에서 조건부 분기를 만들어 보세요.
3. 더 복잡한 병렬 실행을 위해 `and_` 및 `or_` 함수를 탐색해 보세요.
4. flow를 외부 API, 데이터베이스 또는 사용자 인터페이스에 연결해 보세요.
5. 여러 전문화된 crew를 하나의 flow에서 결합해 보세요.
6. [대화형 Flow](/ko/guides/flows/conversational-flows)로 멀티턴 채팅 앱 구축 (`kickoff` per message, `ChatSession`, 지연 트레이싱)
<Check>
축하합니다! 정규 코드, 직접적인 LLM 호출, crew 기반 처리를 결합하여 포괄적인 가이드를 생성하는 첫 번째 CrewAI Flow를 성공적으로 구축하셨습니다. 이러한 기초적인 역량을 바탕으로 절차적 제어와 협업적 인텔리전스를 결합하여 복잡하고 다단계의 문제를 해결할 수 있는 점점 더 정교한 AI 애플리케이션을 만들 수 있습니다.
</Check>

View File

@@ -0,0 +1,125 @@
---
title: "inputs.id에서 restore_from_state_id로 마이그레이션"
description: "더 이상 지원되지 않는 inputs.id 하이드레이션에서 지원되는 restore_from_state_id 필드로 @persist 흐름을 이동"
icon: "arrow-right-arrow-left"
---
<Warning>
`inputs` 내에서 `id`를 전달하여 `@persist` 흐름을 하이드레이트하는 것은 **더 이상 지원되지 않으며**
향후 릴리스에서 제거될 예정입니다. 대체품인 `restore_from_state_id`는 CrewAI **v1.14.5 이상**에서 사용할 수 있으며,
아래 단계는 업그레이드 후 적용됩니다.
</Warning>
## 개요
이전 실행에서 `@persist` 흐름을 하이드레이트하는 문서화된 방법은
해당 실행의 UUID를 `inputs.id`로 전달하는 것입니다. CrewAI는 이제
`inputs` 페이로드를 과부하하지 않고 동일한 하이드레이션을 수행하는 전용 필드인
`restore_from_state_id`를 제공합니다 — 그리고 하이드레이션 키를 새로운 실행의
정체성과 결합하지 않습니다.
## 마이그레이션
현재 `inputs={"id": ...}`로 `@persist` 흐름을 시작하는 경우:
```python
# 더 이상 지원되지 않음
flow = CounterFlow()
flow.kickoff(inputs={"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv"})
```
`restore_from_state_id`로 전환하십시오:
```python
# 지원됨
flow = CounterFlow()
flow.kickoff(restore_from_state_id="abcd1234-5678-90ef-ghij-klmnopqrstuv")
```
두 모드는 서로 다른 계보 의미론을 가지고 있습니다:
- `inputs={"id": <uuid>}` (더 이상 지원되지 않음) — **재개**: 제공된
id 아래에 기록이 작성되어 동일한 `flow_uuid` 이력이 확장됩니다.
- `restore_from_state_id=<uuid>` — **분기**: 스냅샷에서 상태를 하이드레이트한 후
새로운 `state.id` 아래에 기록합니다. 원본 흐름의 이력은 보존됩니다.
대부분의 프로덕션 시나리오에서는 — 이전 상태에서 시드된 흐름을 다시 실행하는 경우 — 분기가
필요합니다. 전체 정신 모델은 [Flow State 마스터링](/ko/guides/flows/mastering-flow-state)을 참조하십시오.
CrewAI AMP REST API를 통해 흐름을 시작하는 경우, 아래 [AMP](#amp)에서
동일한 페이로드 마이그레이션을 참조하십시오.
## 왜 `@persist`에 대해 `inputs.id`를 더 이상 지원하지 않습니까?
`inputs.id`는 현재 이전 실행에서 `@persist` 흐름을 재개하는 문서화된 방법입니다. 문제는
동일한 UUID가 두 가지 작업을 동시에 수행한다는 것입니다:
1. **어떤 스냅샷에서 `@persist`가 하이드레이트되는지를 선택합니다** — 해당 UUID 아래에 저장된 상태를 로드합니다.
2. **새 실행의 흐름 실행 ID가 됩니다** (`state.id`는 SDK에서; 일부 컨텍스트에서는 `flow_id`로 표시됨) — 이
시작에서의 모든 `@persist` 기록도 동일한 UUID 아래에 작성됩니다.
이 이중 역할이 이 가이드에서 설명하는 문제의 근본 원인입니다. 제공된 UUID가 새 실행의 id이기도 하므로,
동일한 `inputs.id`를 전달하는 두 번의 시작은 두 개의 별도 실행이 아닙니다 — 그들은 id를 공유하고,
지속성 기록을 공유하며, (AMP에서) 실행 목록에서 행을 공유합니다. "이 스냅샷에서 하이드레이트하지만,
이 실행을 별도로 기록하십시오"라고 말할 방법이 없습니다.
`restore_from_state_id`가 그 분리입니다. 이는 `@persist`에 어떤 스냅샷에서 하이드레이트할지를 알려주며,
새 실행이 새로운 `state.id`를 받을 수 있도록 합니다. 하이드레이션 소스와 기록된 실행은 더 이상 동일한 UUID가 아닙니다 — 이는 대부분의 프로덕션 시나리오에서 실제로 원하는 것입니다.
## 제거 일정
`@persist` 하이드레이션을 위한 `inputs.id`는 CrewAI의 향후 릴리스에서 제거될 예정입니다. 즉각적인 강제 종료는 없으며 — 기존 흐름은 계속 작동합니다 — 하지만 v1.14.5 이상으로 업그레이드하면,
새 코드에서는 `restore_from_state_id`를 사용해야 하며, 기존 흐름은 다음 편리한 기회에 마이그레이션해야 합니다.
## AMP
흐름을 CrewAI AMP에 배포하는 경우, 마이그레이션은 배포된 팀에 전송되는 시작 페이로드로 확장되며,
`inputs.id`를 재사용하는 가시적인 증상은 배포 대시보드에 나타납니다. 아래 두 개의 하위 섹션이 이를 다룹니다.
### 시작 페이로드 마이그레이션
현재 `inputs`에 `id`를 포함하여 배포된 흐름을 시작하는 경우:
```bash
# 더 이상 지원되지 않음
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{"inputs": {"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv", "topic": "AI Agent Frameworks"}}' \
https://your-crew-url.crewai.com/kickoff
```
UUID를 최상위 `restoreFromStateId` 필드로 이동하십시오:
```bash
# 지원됨
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{
"inputs": {"topic": "AI Agent Frameworks"},
"restoreFromStateId": "abcd1234-5678-90ef-ghij-klmnopqrstuv"
}' \
https://your-crew-url.crewai.com/kickoff
```
`restoreFromStateId`는 시작 페이로드에서 `inputs` 옆에 위치하며, 내부에 있지 않습니다.
`inputs` 객체는 이제 흐름이 실제로 소비하는 값만 포함합니다.
### `inputs.id`가 재사용될 때 발생하는 일
AMP가 기존 실행과 `inputs.id`가 일치하는 흐름의 시작을 수신하면,
새로운 기록을 생성하는 대신 기존 기록으로 해결됩니다. 배포 대시보드에서 다음을 확인할 수 있습니다:
- **실행 상태** — 새로운 실행의 상태가 이전 실행의 상태를 덮어씁니다. 완료된 실행은
다시 `실행 중`으로 전환되거나, `완료`된 실행은 새로운 시작이 실패할 경우 `오류`로 전환될 수 있습니다 — 어쨌든 대시보드는 더 이상
원래 실행을 반영하지 않습니다.
- **추적** — OTel 추적이 시작 간에 쌓이기 때문에 동일한 실행 id를 공유합니다; 이전 실행의 추적은
새로운 실행의 추적과 교체되거나 혼합됩니다. 단계별 재생은 더 이상 단일 실행에 해당하지 않습니다.
- **실행 목록** — 별도의 행으로 나타나야 할 시작이 단일 항목으로 축소되어 이력을 숨깁니다.
`restoreFromStateId`로 마이그레이션하면 모든 시작이 자체 실행으로 유지됩니다 — 각자의 상태, 추적 및 목록의 행을 가지며 — 여전히 이전 실행에서 상태를 하이드레이트합니다.
<Card title="도움이 필요하신가요?" icon="headset" href="mailto:support@crewai.com">
흐름이 어떤 모드가 필요한지 확실하지 않거나 마이그레이션 중 문제가 발생하면 지원 팀에 문의하십시오.
</Card>

View File

@@ -0,0 +1,815 @@
---
title: 플로우 상태 관리 마스터하기
description: 견고한 AI 애플리케이션 구축을 위한 CrewAI 플로우에서 상태를 관리, 유지 및 활용하는 종합 가이드입니다.
icon: diagram-project
mode: "wide"
---
## 플로우에서 State의 힘 이해하기
State 관리는 모든 고급 AI 워크플로우의 중추입니다. CrewAI Flows에서 state 시스템은 컨텍스트를 유지하고, 단계 간 데이터를 공유하며, 복잡한 애플리케이션 로직을 구축할 수 있도록 해줍니다. State 관리에 능숙해지는 것은 신뢰할 수 있고, 유지보수가 용이하며, 강력한 AI 애플리케이션을 만들기 위해 필수적입니다.
이 가이드는 CrewAI Flows에서 state를 관리하는 데 꼭 알아야 할 기본 개념부터 고급 기법까지, 실용적인 코드 예제와 함께 단계별로 안내합니다.
### 상태 관리가 중요한 이유
효과적인 상태 관리는 다음을 가능하게 합니다:
1. **실행 단계 간의 컨텍스트 유지** - 워크플로의 다양한 단계 간에 정보를 원활하게 전달할 수 있습니다.
2. **복잡한 조건부 논리 구성** - 누적된 데이터를 기반으로 의사 결정을 내릴 수 있습니다.
3. **지속적인 애플리케이션 생성** - 워크플로 진행 상황을 저장하고 복원할 수 있습니다.
4. **에러를 우아하게 처리** - 더 견고한 애플리케이션을 위한 복구 패턴을 구현할 수 있습니다.
5. **애플리케이션 확장** - 적절한 데이터 조직을 통해 복잡한 워크플로를 지원할 수 있습니다.
6. **대화형 애플리케이션 활성화** - 컨텍스트 기반 AI 상호작용을 위해 대화 내역을 저장하고 접근할 수 있습니다.
멀티턴 채팅(`kickoff` per user line, `ChatState`, 의도 라우팅, 지연 트레이싱, `ChatSession`)은 [대화형 Flow](/ko/guides/flows/conversational-flows)를 참고하세요.
이러한 기능을 효과적으로 활용하는 방법을 살펴보겠습니다.
## 상태 관리 기본 사항
### Flow 상태 라이프사이클
CrewAI Flow에서 상태는 예측 가능한 라이프사이클을 따릅니다:
1. **초기화** - flow가 생성될 때, 상태는 초기화됩니다(빈 딕셔너리 또는 Pydantic 모델 인스턴스로)
2. **수정** - flow 메서드는 실행되는 동안 상태에 접근하고 이를 수정합니다
3. **전달** - 상태는 flow 메서드들 사이에 자동으로 전달됩니다
4. **영속화** (선택 사항) - 상태는 스토리지에 저장될 수 있고 나중에 다시 불러올 수 있습니다
5. **완료** - 최종 상태는 모든 실행된 메서드의 누적 변경 사항을 반영합니다
이 라이프사이클을 이해하는 것은 효과적인 flow를 설계하는 데 매우 중요합니다.
### 상태 관리의 두 가지 접근 방식
CrewAI에서는 흐름에서 상태를 관리하는 두 가지 방법을 제공합니다:
1. **비구조적 상태** - 유연성을 위해 딕셔너리와 유사한 객체 사용
2. **구조적 상태** - 타입 안전성과 검증을 위해 Pydantic 모델 사용
각 접근 방식을 자세히 살펴보겠습니다.
## 비구조적 상태 관리
비구조적 상태는 사전(dictionary)과 유사한 방식을 사용하여, 단순한 애플리케이션에 유연성과 단순성을 제공합니다.
### 작동 방식
비구조화된 상태의 경우:
- `self.state`를 통해 상태에 접근하며, 이는 딕셔너리처럼 동작합니다
- 언제든지 키를 자유롭게 추가, 수정, 삭제할 수 있습니다
- 모든 상태는 모든 flow 메서드에서 자동으로 사용할 수 있습니다
### 기본 예제
다음은 비구조적 상태 관리를 보여주는 간단한 예제입니다:
```python
from crewai.flow.flow import Flow, listen, start
class UnstructuredStateFlow(Flow):
@start()
def initialize_data(self):
print("Initializing flow data")
# Add key-value pairs to state
self.state["user_name"] = "Alex"
self.state["preferences"] = {
"theme": "dark",
"language": "English"
}
self.state["items"] = []
# The flow state automatically gets a unique ID
print(f"Flow ID: {self.state['id']}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Previous step returned: {previous_result}")
# Access and modify state
user = self.state["user_name"]
print(f"Processing data for {user}")
# Add items to a list in state
self.state["items"].append("item1")
self.state["items"].append("item2")
# Add a new key-value pair
self.state["processed"] = True
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Access multiple state values
user = self.state["user_name"]
theme = self.state["preferences"]["theme"]
items = self.state["items"]
processed = self.state.get("processed", False)
summary = f"User {user} has {len(items)} items with {theme} theme. "
summary += "Data is processed." if processed else "Data is not processed."
return summary
# Run the flow
flow = UnstructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
```
### 비구조적 상태를 사용할 때
비구조적 상태는 다음과 같은 경우에 이상적입니다:
- 빠른 프로토타이핑 및 간단한 플로우
- 동적으로 변화하는 상태 요구
- 구조가 사전에 알려지지 않을 수 있는 경우
- 간단한 상태 요구가 있는 플로우
비구조적 상태는 유연하지만, 타입 검사 및 스키마 검증이 없기 때문에 복잡한 애플리케이션에서 오류가 발생할 수 있습니다.
## 구조화된 상태 관리
구조화된 상태는 Pydantic 모델을 사용하여 flow의 상태에 대한 스키마를 정의함으로써 타입 안전성, 검증, 그리고 더 나은 개발자 경험을 제공합니다.
### 작동 방식
구조화된 상태에서는:
- 상태 구조를 나타내는 Pydantic 모델을 정의합니다.
- 이 모델 타입을 유형 매개변수로 Flow 클래스에 전달합니다.
- `self.state`를 통해 상태에 접근할 수 있으며, 이는 Pydantic 모델 인스턴스처럼 동작합니다.
- 모든 필드는 정의된 타입에 따라 검증됩니다.
- IDE 자동 완성 및 타입 체크 지원을 받을 수 있습니다.
### 기본 예제
구조화된 상태 관리를 구현하는 방법은 다음과 같습니다:
```python
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel, Field
from typing import List, Dict, Optional
# Define your state model
class UserPreferences(BaseModel):
theme: str = "light"
language: str = "English"
class AppState(BaseModel):
user_name: str = ""
preferences: UserPreferences = UserPreferences()
items: List[str] = []
processed: bool = False
completion_percentage: float = 0.0
# Create a flow with typed state
class StructuredStateFlow(Flow[AppState]):
@start()
def initialize_data(self):
print("Initializing flow data")
# Set state values (type-checked)
self.state.user_name = "Taylor"
self.state.preferences.theme = "dark"
# The ID field is automatically available
print(f"Flow ID: {self.state.id}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Processing data for {self.state.user_name}")
# Modify state (with type checking)
self.state.items.append("item1")
self.state.items.append("item2")
self.state.processed = True
self.state.completion_percentage = 50.0
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Access state (with autocompletion)
summary = f"User {self.state.user_name} has {len(self.state.items)} items "
summary += f"with {self.state.preferences.theme} theme. "
summary += "Data is processed." if self.state.processed else "Data is not processed."
summary += f" Completion: {self.state.completion_percentage}%"
return summary
# Run the flow
flow = StructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
```
### 구조화된 상태의 이점
구조화된 상태를 사용하면 여러 가지 장점이 있습니다:
1. **타입 안정성** - 개발 단계에서 타입 오류를 잡을 수 있습니다
2. **자체 문서화** - 상태 모델이 어떤 데이터가 사용 가능한지 명확히 문서화합니다
3. **검증** - 데이터 타입과 제약 조건을 자동으로 검증합니다
4. **IDE 지원** - 자동 완성과 인라인 문서화를 받을 수 있습니다
5. **기본값** - 누락된 데이터에 대한 대체값을 쉽게 정의할 수 있습니다
### 구조화된 상태를 사용할 때
구조화된 상태는 다음과 같은 경우에 권장됩니다:
- 명확하게 정의된 데이터 스키마를 가진 복잡한 플로우
- 여러 개발자가 동일한 코드를 작업하는 팀 프로젝트
- 데이터 검증이 중요한 애플리케이션
- 특정 데이터 타입 및 제약 조건을 강제로 적용해야 하는 플로우
## 자동 상태 ID
비구조화 상태와 구조화 상태 모두 상태 인스턴스를 추적하고 관리하는 데 도움이 되는 고유한 식별자(UUID)를 자동으로 부여받습니다.
### 작동 방식
- 비구조화 state의 경우, ID는 `self.state["id"]`로 접근할 수 있습니다.
- 구조화 state의 경우, ID는 `self.state.id`로 접근할 수 있습니다.
- 이 ID는 flow가 생성될 때 자동으로 생성됩니다.
- ID는 flow의 생명주기 동안 동일하게 유지됩니다.
- ID는 추적, 로깅, 저장된 state의 조회에 사용할 수 있습니다.
이 UUID는 persistence를 구현하거나 여러 flow 실행을 추적할 때 특히 유용합니다.
## 동적 상태 업데이트
구조화된 상태를 사용하든 비구조화된 상태를 사용하든, flow의 실행 중 언제든지 상태를 동적으로 업데이트할 수 있습니다.
### 단계 간 데이터 전달
Flow 메서드는 값을 반환할 수 있으며, 이러한 반환값은 리스닝 메서드의 인자로 전달됩니다:
```python
from crewai.flow.flow import Flow, listen, start
class DataPassingFlow(Flow):
@start()
def generate_data(self):
# This return value will be passed to listening methods
return "Generated data"
@listen(generate_data)
def process_data(self, data_from_previous_step):
print(f"Received: {data_from_previous_step}")
# You can modify the data and pass it along
processed_data = f"{data_from_previous_step} - processed"
# Also update state
self.state["last_processed"] = processed_data
return processed_data
@listen(process_data)
def finalize_data(self, processed_data):
print(f"Received processed data: {processed_data}")
# Access both the passed data and state
last_processed = self.state.get("last_processed", "")
return f"Final: {processed_data} (from state: {last_processed})"
```
이 패턴을 사용하면 직접적인 데이터 전달과 state 업데이트를 결합하여 최대한 유연하게 작업할 수 있습니다.
## 플로우 상태 지속
CrewAI의 가장 강력한 기능 중 하나는 실행 간에 플로우 상태를 지속할 수 있다는 점입니다. 이를 통해 중단, 재개, 심지어 실패 후에도 복구할 수 있는 워크플로우를 구현할 수 있습니다.
### @persist() 데코레이터
`@persist()` 데코레이터는 상태 지속을 자동화하여 flow의 상태를 실행의 주요 지점마다 저장합니다.
#### 클래스 수준 지속성
클래스 수준에서 `@persist()`를 적용하면 모든 메서드 실행 후 상태가 저장됩니다:
```python
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
from pydantic import BaseModel
class CounterState(BaseModel):
value: int = 0
@persist() # Apply to the entire flow class
class PersistentCounterFlow(Flow[CounterState]):
@start()
def increment(self):
self.state.value += 1
print(f"Incremented to {self.state.value}")
return self.state.value
@listen(increment)
def double(self, value):
self.state.value = value * 2
print(f"Doubled to {self.state.value}")
return self.state.value
# First run
flow1 = PersistentCounterFlow()
result1 = flow1.kickoff()
print(f"First run result: {result1}")
# Second run - state is automatically loaded
flow2 = PersistentCounterFlow()
result2 = flow2.kickoff()
print(f"Second run result: {result2}") # Will be higher due to persisted state
```
#### 메서드 수준 지속성
더 세밀한 제어를 위해 `@persist()`를 특정 메서드에 적용할 수 있습니다:
```python
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
class SelectivePersistFlow(Flow):
@start()
def first_step(self):
self.state["count"] = 1
return "First step"
@persist() # Only persist after this method
@listen(first_step)
def important_step(self, prev_result):
self.state["count"] += 1
self.state["important_data"] = "This will be persisted"
return "Important step completed"
@listen(important_step)
def final_step(self, prev_result):
self.state["count"] += 1
return f"Complete with count {self.state['count']}"
```
#### 영속 상태 포크하기
`@persist`는 `kickoff` / `kickoff_async`에서 두 가지 별개의 하이드레이션 모드를 지원합니다. 동일한 계보를 계속하려면 **재개**(`inputs["id"]`)를 사용하고, 스냅샷에서 시작하는 새 계보를 시작하려면 **포크**(`restore_from_state_id`)를 사용하세요:
| | kickoff 후 `state.id` | `@persist` 기록 위치 |
|---|---|---|
| `inputs["id"]` (재개) | 제공된 id | 제공된 id (기록 확장) |
| `restore_from_state_id` (포크) | 새 id, 또는 고정 시 `inputs["id"]` | 새 id (원본 보존) |
```python
from crewai.flow.flow import Flow, start
from crewai.flow.persistence import persist
from pydantic import BaseModel
class CounterState(BaseModel):
id: str = ""
counter: int = 0
@persist
class CounterFlow(Flow[CounterState]):
@start()
def step(self):
self.state.counter += 1
# 실행 1: 새 상태, counter 0 -> 1
flow_1 = CounterFlow()
flow_1.kickoff()
# 포크: flow_1의 최신 스냅샷에서 하이드레이트, 단 새 state.id에 기록
flow_2 = CounterFlow()
flow_2.kickoff(restore_from_state_id=flow_1.state.id)
# flow_2는 counter=1(하이드레이트)로 시작하고, step()이 2로 증가시킵니다.
# flow_1의 flow_uuid 기록은 변경되지 않습니다.
```
동작 노트:
- `restore_from_state_id`가 영속에서 발견되지 않음 → kickoff는 조용히 기본 동작으로 폴백됩니다 (기존 `inputs["id"]`의 미발견 동작 미러링). 예외는 발생하지 않습니다.
- `restore_from_state_id`를 `from_checkpoint`와 결합하면 `ValueError`가 발생합니다 — 서로 다른 상태 시스템(`@persist` 대 Checkpointing)을 대상으로 하므로 결합할 수 없습니다.
- `restore_from_state_id=None`(기본값)은 매개변수 없는 kickoff와 바이트 단위로 동일합니다.
- 포크 중 `inputs["id"]`를 고정하면 새 실행이 다른 플로우와 영속 키를 공유함을 의미합니다 — 일반적으로 `restore_from_state_id`만 사용하는 것이 좋습니다.
## 고급 상태 패턴
### 상태 기반 조건부 로직
state를 사용하여 flow에서 복잡한 조건부 로직을 구현할 수 있습니다:
```python
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
class PaymentState(BaseModel):
amount: float = 0.0
is_approved: bool = False
retry_count: int = 0
class PaymentFlow(Flow[PaymentState]):
@start()
def process_payment(self):
# Simulate payment processing
self.state.amount = 100.0
self.state.is_approved = self.state.amount < 1000
return "Payment processed"
@router(process_payment)
def check_approval(self, previous_result):
if self.state.is_approved:
return "approved"
elif self.state.retry_count < 3:
return "retry"
else:
return "rejected"
@listen("approved")
def handle_approval(self):
return f"Payment of ${self.state.amount} approved!"
@listen("retry")
def handle_retry(self):
self.state.retry_count += 1
print(f"Retrying payment (attempt {self.state.retry_count})...")
# Could implement retry logic here
return "Retry initiated"
@listen("rejected")
def handle_rejection(self):
return f"Payment of ${self.state.amount} rejected after {self.state.retry_count} retries."
```
### 복잡한 상태 변환 처리
복잡한 상태 변환의 경우, 전용 메서드를 만들어 처리할 수 있습니다.
```python
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
from typing import List, Dict
class UserData(BaseModel):
name: str
active: bool = True
login_count: int = 0
class ComplexState(BaseModel):
users: Dict[str, UserData] = {}
active_user_count: int = 0
class TransformationFlow(Flow[ComplexState]):
@start()
def initialize(self):
# Add some users
self.add_user("alice", "Alice")
self.add_user("bob", "Bob")
self.add_user("charlie", "Charlie")
return "Initialized"
@listen(initialize)
def process_users(self, _):
# Increment login counts
for user_id in self.state.users:
self.increment_login(user_id)
# Deactivate one user
self.deactivate_user("bob")
# Update active count
self.update_active_count()
return f"Processed {len(self.state.users)} users"
# Helper methods for state transformations
def add_user(self, user_id: str, name: str):
self.state.users[user_id] = UserData(name=name)
self.update_active_count()
def increment_login(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].login_count += 1
def deactivate_user(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].active = False
self.update_active_count()
def update_active_count(self):
self.state.active_user_count = sum(
1 for user in self.state.users.values() if user.active
)
```
이와 같은 헬퍼 메서드 생성 패턴은 flow 메서드를 깔끔하게 유지하면서 복잡한 상태 조작을 가능하게 해줍니다.
## Crews로 상태 관리하기
CrewAI에서 가장 강력한 패턴 중 하나는 flow 상태 관리와 crew 실행을 결합하는 것입니다.
### 크루에 상태 전달하기
플로우 상태를 사용하여 크루에 매개변수를 전달할 수 있습니다:
```python
from crewai.flow.flow import Flow, listen, start
from crewai import Agent, Crew, Process, Task
from pydantic import BaseModel
class ResearchState(BaseModel):
topic: str = ""
depth: str = "medium"
results: str = ""
class ResearchFlow(Flow[ResearchState]):
@start()
def get_parameters(self):
# In a real app, this might come from user input
self.state.topic = "Artificial Intelligence Ethics"
self.state.depth = "deep"
return "Parameters set"
@listen(get_parameters)
def execute_research(self, _):
# Create agents
researcher = Agent(
role="Research Specialist",
goal=f"Research {self.state.topic} in {self.state.depth} detail",
backstory="You are an expert researcher with a talent for finding accurate information."
)
writer = Agent(
role="Content Writer",
goal="Transform research into clear, engaging content",
backstory="You excel at communicating complex ideas clearly and concisely."
)
# Create tasks
research_task = Task(
description=f"Research {self.state.topic} with {self.state.depth} analysis",
expected_output="Comprehensive research notes in markdown format",
agent=researcher
)
writing_task = Task(
description=f"Create a summary on {self.state.topic} based on the research",
expected_output="Well-written article in markdown format",
agent=writer,
context=[research_task]
)
# Create and run crew
research_crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process=Process.sequential,
verbose=True
)
# Run crew and store result in state
result = research_crew.kickoff()
self.state.results = result.raw
return "Research completed"
@listen(execute_research)
def summarize_results(self, _):
# Access the stored results
result_length = len(self.state.results)
return f"Research on {self.state.topic} completed with {result_length} characters of results."
```
### State에서 Crew 출력 처리하기
Crew가 완료되면, 해당 출력을 처리하여 flow state에 저장할 수 있습니다:
```python
@listen(execute_crew)
def process_crew_results(self, _):
# Parse the raw results (assuming JSON output)
import json
try:
results_dict = json.loads(self.state.raw_results)
self.state.processed_results = {
"title": results_dict.get("title", ""),
"main_points": results_dict.get("main_points", []),
"conclusion": results_dict.get("conclusion", "")
}
return "Results processed successfully"
except json.JSONDecodeError:
self.state.error = "Failed to parse crew results as JSON"
return "Error processing results"
```
## 상태 관리 모범 사례
### 1. 상태를 집중적으로 유지하세요
상태를 설계할 때 꼭 필요한 내용만 포함하도록 하세요:
```python
# Too broad
class BloatedState(BaseModel):
user_data: Dict = {}
system_settings: Dict = {}
temporary_calculations: List = []
debug_info: Dict = {}
# ...many more fields
# Better: Focused state
class FocusedState(BaseModel):
user_id: str
preferences: Dict[str, str]
completion_status: Dict[str, bool]
```
### 2. 복잡한 플로우를 위한 구조화된 상태 사용
플로우의 복잡도가 증가할수록 구조화된 상태의 가치는 점점 커집니다:
```python
# Simple flow can use unstructured state
class SimpleGreetingFlow(Flow):
@start()
def greet(self):
self.state["name"] = "World"
return f"Hello, {self.state['name']}!"
# Complex flow benefits from structured state
class UserRegistrationState(BaseModel):
username: str
email: str
verification_status: bool = False
registration_date: datetime = Field(default_factory=datetime.now)
last_login: Optional[datetime] = None
class RegistrationFlow(Flow[UserRegistrationState]):
# Methods with strongly-typed state access
```
### 3. 문서 상태 전이
복잡한 흐름의 경우, 실행 중에 상태가 어떻게 변하는지 문서화하세요:
```python
@start()
def initialize_order(self):
"""
Initialize order state with empty values.
State before: {}
State after: {order_id: str, items: [], status: 'new'}
"""
self.state.order_id = str(uuid.uuid4())
self.state.items = []
self.state.status = "new"
return "Order initialized"
```
### 4. 상태 오류를 정상적으로 처리하기
상태 접근에 대한 오류 처리를 구현하세요:
```python
@listen(previous_step)
def process_data(self, _):
try:
# Try to access a value that might not exist
user_preference = self.state.preferences.get("theme", "default")
except (AttributeError, KeyError):
# Handle the error gracefully
self.state.errors = self.state.get("errors", [])
self.state.errors.append("Failed to access preferences")
user_preference = "default"
return f"Used preference: {user_preference}"
```
### 5. 상태를 사용하여 진행 상황 추적
긴 실행 흐름에서 진행 상황을 추적하기 위해 상태를 활용하세요:
```python
class ProgressTrackingFlow(Flow):
@start()
def initialize(self):
self.state["total_steps"] = 3
self.state["current_step"] = 0
self.state["progress"] = 0.0
self.update_progress()
return "Initialized"
def update_progress(self):
"""Helper method to calculate and update progress"""
if self.state.get("total_steps", 0) > 0:
self.state["progress"] = (self.state.get("current_step", 0) /
self.state["total_steps"]) * 100
print(f"Progress: {self.state['progress']:.1f}%")
@listen(initialize)
def step_one(self, _):
# Do work...
self.state["current_step"] = 1
self.update_progress()
return "Step 1 complete"
# Additional steps...
```
### 6. 가능한 경우 불변(Immutable) 연산 사용하기
특히 구조화된 상태에서는 명확성을 위해 불변 연산을 선호하세요:
```python
# 리스트를 즉시 수정하는 대신:
self.state.items.append(new_item) # 변경 가능한 연산
# 새로운 상태를 생성하는 것을 고려하세요:
from pydantic import BaseModel
from typing import List
class ItemState(BaseModel):
items: List[str] = []
class ImmutableFlow(Flow[ItemState]):
@start()
def add_item(self):
# 추가된 항목과 함께 새로운 리스트 생성
self.state.items = [*self.state.items, "new item"]
return "Item added"
```
## 플로우 상태 디버깅
### 상태 변경 로깅
개발할 때 상태 변화를 추적하기 위해 로깅을 추가하세요:
```python
import logging
logging.basicConfig(level=logging.INFO)
class LoggingFlow(Flow):
def log_state(self, step_name):
logging.info(f"State after {step_name}: {self.state}")
@start()
def initialize(self):
self.state["counter"] = 0
self.log_state("initialize")
return "Initialized"
@listen(initialize)
def increment(self, _):
self.state["counter"] += 1
self.log_state("increment")
return f"Incremented to {self.state['counter']}"
```
### 상태 시각화
디버깅을 위해 상태를 시각화하는 메서드를 추가할 수 있습니다:
```python
def visualize_state(self):
"""Create a simple visualization of the current state"""
import json
from rich.console import Console
from rich.panel import Panel
console = Console()
if hasattr(self.state, "model_dump"):
# Pydantic v2
state_dict = self.state.model_dump()
elif hasattr(self.state, "dict"):
# Pydantic v1
state_dict = self.state.dict()
else:
# Unstructured state
state_dict = dict(self.state)
# Remove id for cleaner output
if "id" in state_dict:
state_dict.pop("id")
state_json = json.dumps(state_dict, indent=2, default=str)
console.print(Panel(state_json, title="Current Flow State"))
```
## 결론
CrewAI Flows에서 상태 관리를 마스터하면 컨텍스트를 유지하고, 복잡한 결정을 내리며, 일관된 결과를 제공하는 정교하고 견고한 AI 애플리케이션을 구축할 수 있는 힘을 얻게 됩니다.
비구조화 상태든 구조화 상태든 적절한 상태 관리 방식을 구현하면 유지 관리가 용이하고, 확장 가능하며, 실제 문제를 효과적으로 해결할 수 있는 플로우를 만들 수 있습니다.
더 복잡한 플로우를 개발할수록 좋은 상태 관리는 유연성과 구조성 사이의 올바른 균형을 찾는 것임을 기억하세요. 이를 통해 코드가 강력하면서도 이해하기 쉬워집니다.
<Check>
이제 CrewAI Flows에서 상태 관리의 개념과 실습을 마스터하셨습니다! 이 지식을 통해 컨텍스트를 효과적으로 유지하고, 단계 간 데이터를 공유하며, 정교한 애플리케이션 로직을 구현하는 견고한 AI 워크플로우를 만들 수 있습니다.
</Check>
## 다음 단계
- flow에서 구조화된 state와 비구조화된 state를 모두 실험해 보세요
- 장기 실행 워크플로를 위해 state 영속성을 구현해 보세요
- [첫 crew 만들기](/ko/guides/crews/first-crew)를 탐색하여 crew와 flow가 어떻게 함께 작동하는지 확인해 보세요
- 더 고급 기능을 원한다면 [Flow 참고 문서](/ko/concepts/flows)를 확인해 보세요

View File

@@ -0,0 +1,518 @@
---
title: "LangGraph에서 CrewAI로 옮기기: 엔지니어를 위한 실전 가이드"
description: LangGraph로 이미 구축했다면, 프로젝트를 CrewAI로 빠르게 옮기는 방법을 알아보세요
icon: switch
mode: "wide"
---
LangGraph로 에이전트를 구축해 왔습니다. `StateGraph`와 씨름하고, 조건부 에지를 연결하고, 새벽 2시에 상태 딕셔너리를 디버깅해 본 적도 있죠. 동작은 하지만 — 어느 순간부터 프로덕션으로 가는 더 나은 길이 없을까 고민하게 됩니다.
있습니다. **CrewAI Flows**는 이벤트 기반 오케스트레이션, 조건부 라우팅, 공유 상태라는 동일한 힘을 훨씬 적은 보일러플레이트와 실제로 다단계 AI 워크플로우를 생각하는 방식에 잘 맞는 정신적 모델로 제공합니다.
이 글은 핵심 개념을 나란히 비교하고 실제 코드 비교를 보여주며, 다음으로 손이 갈 프레임워크가 왜 CrewAI Flows인지 설명합니다.
---
## 정신적 모델의 전환
LangGraph는 **그래프**로 생각하라고 요구합니다: 노드, 에지, 그리고 상태 딕셔너리. 모든 워크플로우는 계산 단계 사이의 전이를 명시적으로 연결하는 방향 그래프입니다. 강력하지만, 특히 워크플로우가 몇 개의 결정 지점이 있는 순차적 흐름일 때 이 추상화는 오버헤드를 가져옵니다.
CrewAI Flows는 **이벤트**로 생각하라고 요구합니다: 시작하는 메서드, 결과를 듣는 메서드, 실행을 라우팅하는 메서드. 워크플로우의 토폴로지는 명시적 그래프 구성 대신 데코레이터 어노테이션에서 드러납니다. 이것은 단순한 문법 설탕이 아니라 — 파이프라인을 설계하고 읽고 유지하는 방식을 바꿉니다.
핵심 매핑은 다음과 같습니다:
| LangGraph 개념 | CrewAI Flows 대응 |
| --- | --- |
| `StateGraph` class | `Flow` class |
| `add_node()` | Methods decorated with `@start`, `@listen` |
| `add_edge()` / `add_conditional_edges()` | `@listen()` / `@router()` decorators |
| `TypedDict` state | Pydantic `BaseModel` state |
| `START` / `END` constants | `@start()` decorator / natural method return |
| `graph.compile()` | `flow.kickoff()` |
| Checkpointer / persistence | Built-in memory (LanceDB-backed) |
실제로 어떻게 보이는지 살펴보겠습니다.
---
## 데모 1: 간단한 순차 파이프라인
주제를 받아 조사하고, 요약을 작성한 뒤, 결과를 포맷팅하는 파이프라인을 만든다고 해봅시다. 각 프레임워크는 이렇게 처리합니다.
### LangGraph 방식
```python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class ResearchState(TypedDict):
topic: str
raw_research: str
summary: str
formatted_output: str
def research_topic(state: ResearchState) -> dict:
# Call an LLM or search API
result = llm.invoke(f"Research the topic: {state['topic']}")
return {"raw_research": result}
def write_summary(state: ResearchState) -> dict:
result = llm.invoke(
f"Summarize this research:\n{state['raw_research']}"
)
return {"summary": result}
def format_output(state: ResearchState) -> dict:
result = llm.invoke(
f"Format this summary as a polished article section:\n{state['summary']}"
)
return {"formatted_output": result}
# Build the graph
graph = StateGraph(ResearchState)
graph.add_node("research", research_topic)
graph.add_node("summarize", write_summary)
graph.add_node("format", format_output)
graph.add_edge(START, "research")
graph.add_edge("research", "summarize")
graph.add_edge("summarize", "format")
graph.add_edge("format", END)
# Compile and run
app = graph.compile()
result = app.invoke({"topic": "quantum computing advances in 2026"})
print(result["formatted_output"])
```
함수를 정의하고 노드로 등록한 다음, 모든 전이를 수동으로 연결합니다. 이렇게 단순한 순서인데도 의례처럼 해야 할 작업이 많습니다.
### CrewAI Flows 방식
```python
from crewai import LLM, Agent, Crew, Process, Task
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
llm = LLM(model="openai/gpt-5.2")
class ResearchState(BaseModel):
topic: str = ""
raw_research: str = ""
summary: str = ""
formatted_output: str = ""
class ResearchFlow(Flow[ResearchState]):
@start()
def research_topic(self):
# Option 1: Direct LLM call
result = llm.call(f"Research the topic: {self.state.topic}")
self.state.raw_research = result
return result
@listen(research_topic)
def write_summary(self, research_output):
# Option 2: A single agent
summarizer = Agent(
role="Research Summarizer",
goal="Produce concise, accurate summaries of research content",
backstory="You are an expert at distilling complex research into clear, "
"digestible summaries.",
llm=llm,
verbose=True,
)
result = summarizer.kickoff(
f"Summarize this research:\n{self.state.raw_research}"
)
self.state.summary = str(result)
return self.state.summary
@listen(write_summary)
def format_output(self, summary_output):
# Option 3: a complete crew (with one or more agents)
formatter = Agent(
role="Content Formatter",
goal="Transform research summaries into polished, publication-ready article sections",
backstory="You are a skilled editor with expertise in structuring and "
"presenting technical content for a general audience.",
llm=llm,
verbose=True,
)
format_task = Task(
description=f"Format this summary as a polished article section:\n{self.state.summary}",
expected_output="A well-structured, polished article section ready for publication.",
agent=formatter,
)
crew = Crew(
agents=[formatter],
tasks=[format_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
self.state.formatted_output = str(result)
return self.state.formatted_output
# Run the flow
flow = ResearchFlow()
flow.state.topic = "quantum computing advances in 2026"
result = flow.kickoff()
print(flow.state.formatted_output)
```
눈에 띄는 차이점이 있습니다: 그래프 구성 없음, 에지 연결 없음, 컴파일 단계 없음. 실행 순서는 로직이 있는 곳에서 바로 선언됩니다. `@start()`는 진입점을 표시하고, `@listen(method_name)`은 단계들을 연결합니다. 상태는 타입 안전성, 검증, IDE 자동 완성까지 제공하는 제대로 된 Pydantic 모델입니다.
---
## 데모 2: 조건부 라우팅
여기서 흥미로워집니다. 콘텐츠 유형에 따라 서로 다른 처리 경로로 라우팅하는 파이프라인을 만든다고 해봅시다.
### LangGraph 방식
```python
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
class ContentState(TypedDict):
input_text: str
content_type: str
result: str
def classify_content(state: ContentState) -> dict:
content_type = llm.invoke(
f"Classify this content as 'technical', 'creative', or 'business':\n{state['input_text']}"
)
return {"content_type": content_type.strip().lower()}
def process_technical(state: ContentState) -> dict:
result = llm.invoke(f"Process as technical doc:\n{state['input_text']}")
return {"result": result}
def process_creative(state: ContentState) -> dict:
result = llm.invoke(f"Process as creative writing:\n{state['input_text']}")
return {"result": result}
def process_business(state: ContentState) -> dict:
result = llm.invoke(f"Process as business content:\n{state['input_text']}")
return {"result": result}
# Routing function
def route_content(state: ContentState) -> Literal["technical", "creative", "business"]:
return state["content_type"]
# Build the graph
graph = StateGraph(ContentState)
graph.add_node("classify", classify_content)
graph.add_node("technical", process_technical)
graph.add_node("creative", process_creative)
graph.add_node("business", process_business)
graph.add_edge(START, "classify")
graph.add_conditional_edges(
"classify",
route_content,
{
"technical": "technical",
"creative": "creative",
"business": "business",
}
)
graph.add_edge("technical", END)
graph.add_edge("creative", END)
graph.add_edge("business", END)
app = graph.compile()
result = app.invoke({"input_text": "Explain how TCP handshakes work"})
```
별도의 라우팅 함수, 명시적 조건부 에지 매핑, 그리고 모든 분기에 대한 종료 에지가 필요합니다. 라우팅 결정 로직이 그 결정을 만들어 내는 노드와 분리됩니다.
### CrewAI Flows 방식
```python
from crewai import LLM, Agent
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
llm = LLM(model="openai/gpt-5.2")
class ContentState(BaseModel):
input_text: str = ""
content_type: str = ""
result: str = ""
class ContentFlow(Flow[ContentState]):
@start()
def classify_content(self):
self.state.content_type = (
llm.call(
f"Classify this content as 'technical', 'creative', or 'business':\n"
f"{self.state.input_text}"
)
.strip()
.lower()
)
return self.state.content_type
@router(classify_content)
def route_content(self, classification):
if classification == "technical":
return "process_technical"
elif classification == "creative":
return "process_creative"
else:
return "process_business"
@listen("process_technical")
def handle_technical(self):
agent = Agent(
role="Technical Writer",
goal="Produce clear, accurate technical documentation",
backstory="You are an expert technical writer who specializes in "
"explaining complex technical concepts precisely.",
llm=llm,
verbose=True,
)
self.state.result = str(
agent.kickoff(f"Process as technical doc:\n{self.state.input_text}")
)
@listen("process_creative")
def handle_creative(self):
agent = Agent(
role="Creative Writer",
goal="Craft engaging and imaginative creative content",
backstory="You are a talented creative writer with a flair for "
"compelling storytelling and vivid expression.",
llm=llm,
verbose=True,
)
self.state.result = str(
agent.kickoff(f"Process as creative writing:\n{self.state.input_text}")
)
@listen("process_business")
def handle_business(self):
agent = Agent(
role="Business Writer",
goal="Produce professional, results-oriented business content",
backstory="You are an experienced business writer who communicates "
"strategy and value clearly to professional audiences.",
llm=llm,
verbose=True,
)
self.state.result = str(
agent.kickoff(f"Process as business content:\n{self.state.input_text}")
)
flow = ContentFlow()
flow.state.input_text = "Explain how TCP handshakes work"
flow.kickoff()
print(flow.state.result)
```
`@router()` 데코레이터는 메서드를 결정 지점으로 만듭니다. 리스너와 매칭되는 문자열을 반환하므로, 매핑 딕셔너리도, 별도의 라우팅 함수도 필요 없습니다. 분기 로직이 Python `if` 문처럼 읽히는 이유는, 실제로 `if` 문이기 때문입니다.
---
## 데모 3: AI 에이전트 Crew를 Flow에 통합하기
여기서 CrewAI의 진짜 힘이 드러납니다. Flows는 LLM 호출을 연결하는 것에 그치지 않고 자율적인 에이전트 **Crew** 전체를 오케스트레이션합니다. 이는 LangGraph에 기본으로 대응되는 개념이 없습니다.
```python
from crewai import Agent, Task, Crew
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
class ArticleState(BaseModel):
topic: str = ""
research: str = ""
draft: str = ""
final_article: str = ""
class ArticleFlow(Flow[ArticleState]):
@start()
def run_research_crew(self):
"""A full Crew of agents handles research."""
researcher = Agent(
role="Senior Research Analyst",
goal=f"Produce comprehensive research on: {self.state.topic}",
backstory="You're a veteran analyst known for thorough, "
"well-sourced research reports.",
llm="gpt-4o"
)
research_task = Task(
description=f"Research '{self.state.topic}' thoroughly. "
"Cover key trends, data points, and expert opinions.",
expected_output="A detailed research brief with sources.",
agent=researcher
)
crew = Crew(agents=[researcher], tasks=[research_task])
result = crew.kickoff()
self.state.research = result.raw
return result.raw
@listen(run_research_crew)
def run_writing_crew(self, research_output):
"""A different Crew handles writing."""
writer = Agent(
role="Technical Writer",
goal="Write a compelling article based on provided research.",
backstory="You turn complex research into engaging, clear prose.",
llm="gpt-4o"
)
editor = Agent(
role="Senior Editor",
goal="Review and polish articles for publication quality.",
backstory="20 years of editorial experience at top tech publications.",
llm="gpt-4o"
)
write_task = Task(
description=f"Write an article based on this research:\n{self.state.research}",
expected_output="A well-structured draft article.",
agent=writer
)
edit_task = Task(
description="Review, fact-check, and polish the draft article.",
expected_output="A publication-ready article.",
agent=editor
)
crew = Crew(agents=[writer, editor], tasks=[write_task, edit_task])
result = crew.kickoff()
self.state.final_article = result.raw
return result.raw
# Run the full pipeline
flow = ArticleFlow()
flow.state.topic = "The Future of Edge AI"
flow.kickoff()
print(flow.state.final_article)
```
핵심 인사이트는 다음과 같습니다: **Flows는 오케스트레이션 레이어를, Crews는 지능 레이어를 제공합니다.** Flow의 각 단계는 각자의 역할, 목표, 도구를 가진 협업 에이전트 팀을 띄울 수 있습니다. 구조화되고 예측 가능한 제어 흐름 *그리고* 자율적 에이전트 협업 — 두 세계의 장점을 모두 얻습니다.
LangGraph에서 비슷한 것을 하려면 노드 함수 안에 에이전트 통신 프로토콜, 도구 호출 루프, 위임 로직을 직접 구현해야 합니다. 가능하긴 하지만, 매번 처음부터 배관을 만드는 셈입니다.
---
## 데모 4: 병렬 실행과 동기화
실제 파이프라인은 종종 작업을 병렬로 분기하고 결과를 합쳐야 합니다. CrewAI Flows는 `and_`와 `or_` 연산자로 이를 우아하게 처리합니다.
```python
from crewai import LLM
from crewai.flow.flow import Flow, and_, listen, start
from pydantic import BaseModel
llm = LLM(model="openai/gpt-5.2")
class AnalysisState(BaseModel):
topic: str = ""
market_data: str = ""
tech_analysis: str = ""
competitor_intel: str = ""
final_report: str = ""
class ParallelAnalysisFlow(Flow[AnalysisState]):
@start()
def start_method(self):
pass
@listen(start_method)
def gather_market_data(self):
# Your agentic or deterministic code
pass
@listen(start_method)
def run_tech_analysis(self):
# Your agentic or deterministic code
pass
@listen(start_method)
def gather_competitor_intel(self):
# Your agentic or deterministic code
pass
@listen(and_(gather_market_data, run_tech_analysis, gather_competitor_intel))
def synthesize_report(self):
# Your agentic or deterministic code
pass
flow = ParallelAnalysisFlow()
flow.state.topic = "AI-powered developer tools"
flow.kickoff()
```
여러 `@start()` 데코레이터는 병렬로 실행됩니다. `@listen` 데코레이터의 `and_()` 결합자는 `synthesize_report`가 *세 가지* 상위 메서드가 모두 완료된 뒤에만 실행되도록 보장합니다. *어떤* 상위 작업이든 끝나는 즉시 진행하고 싶다면 `or_()`도 사용할 수 있습니다.
LangGraph에서는 병렬 분기, 동기화 노드, 신중한 상태 병합이 포함된 fan-out/fan-in 패턴을 만들어야 하며 — 모든 것을 에지로 명시적으로 연결해야 합니다.
---
## 프로덕션에서 CrewAI Flows를 쓰는 이유
깔끔한 문법을 넘어, Flows는 여러 프로덕션 핵심 이점을 제공합니다:
**내장 상태 지속성.** Flow 상태는 LanceDB에 의해 백업되므로 워크플로우가 크래시에서 살아남고, 재개될 수 있으며, 실행 간에 지식을 축적할 수 있습니다. LangGraph는 별도의 체크포인터를 구성해야 합니다.
**타입 안전한 상태 관리.** Pydantic 모델은 즉시 검증, 직렬화, IDE 지원을 제공합니다. LangGraph의 `TypedDict` 상태는 런타임 검증을 하지 않습니다.
**일급 에이전트 오케스트레이션.** Crews는 기본 프리미티브입니다. 역할, 목표, 배경, 도구를 가진 에이전트를 정의하고, Flow의 구조적 틀 안에서 자율적으로 협업하게 합니다. 다중 에이전트 조율을 다시 만들 필요가 없습니다.
**더 단순한 정신적 모델.** 데코레이터는 의도를 선언합니다. `@start`는 "여기서 시작", `@listen(x)`는 "x 이후 실행", `@router(x)`는 "x 이후 어디로 갈지 결정"을 의미합니다. 코드는 자신이 설명하는 워크플로우처럼 읽힙니다.
**CLI 통합.** `crewai run`으로 Flows를 실행합니다. 별도의 컴파일 단계나 그래프 직렬화가 없습니다. Flow는 Python 클래스이며, 그대로 실행됩니다.
---
## 마이그레이션 치트 시트
LangGraph 코드베이스를 CrewAI Flows로 옮기고 싶다면, 다음의 실전 변환 가이드를 참고하세요:
1. **상태를 매핑하세요.** `TypedDict`를 Pydantic `BaseModel`로 변환하고 모든 필드에 기본값을 추가하세요.
2. **노드를 메서드로 변환하세요.** 각 `add_node` 함수는 `Flow` 서브클래스의 메서드가 됩니다. `state["field"]` 읽기는 `self.state.field`로 바꾸세요.
3. **에지를 데코레이터로 교체하세요.** `add_edge(START, "first_node")`는 첫 메서드의 `@start()`가 됩니다. 순차적인 `add_edge("a", "b")`는 `b` 메서드의 `@listen(a)`가 됩니다.
4. **조건부 에지는 `@router`로 교체하세요.** 라우팅 함수와 `add_conditional_edges()` 매핑은 하나의 `@router()` 메서드로 통합하고, 라우트 문자열을 반환하세요.
5. **compile + invoke를 kickoff으로 교체하세요.** `graph.compile()`를 제거하고 `flow.kickoff()`를 호출하세요.
6. **Crew가 들어갈 지점을 고려하세요.** 복잡한 다단계 에이전트 로직이 있는 노드는 Crew로 분리할 후보입니다. 이 부분에서 가장 큰 품질 향상을 체감할 수 있습니다.
---
## 시작하기
CrewAI를 설치하고 새 Flow 프로젝트를 스캐폴딩하세요:
```bash
pip install crewai
crewai create flow my_first_flow
cd my_first_flow
```
이렇게 하면 바로 편집 가능한 Flow 클래스, 설정 파일, 그리고 `type = "flow"`가 이미 설정된 `pyproject.toml`이 포함된 프로젝트 구조가 생성됩니다. 다음으로 실행하세요:
```bash
crewai run
```
그 다음부터는 에이전트를 추가하고 리스너를 연결한 뒤, 배포하면 됩니다.
---
## 마무리
LangGraph는 AI 워크플로우에 구조가 필요하다는 사실을 생태계에 일깨워 주었습니다. 중요한 교훈이었습니다. 하지만 CrewAI Flows는 그 교훈을 더 빠르게 쓰고, 더 쉽게 읽으며, 프로덕션에서 더 강력한 형태로 제공합니다 — 특히 워크플로우에 여러 에이전트의 협업이 포함될 때 그렇습니다.
단일 에이전트 체인을 넘는 무엇인가를 만들고 있다면, Flows를 진지하게 검토해 보세요. 데코레이터 기반 모델, Crews의 네이티브 통합, 내장 상태 관리를 통해 배관 작업에 쓰는 시간을 줄이고, 중요한 문제에 더 많은 시간을 쓸 수 있습니다.
`crewai create flow`로 시작하세요. 후회하지 않을 겁니다.

View File

@@ -0,0 +1,190 @@
---
title: "CrewAI 업그레이드"
description: "프로젝트에서 CrewAI를 업그레이드하고 버전 간 브레이킹 체인지에 적응하는 방법."
icon: "arrow-up-circle"
---
## 개요
CrewAI 릴리스는 정기적으로 새로운 기능을 제공합니다. 이 가이드는 CLI와 프로젝트의 가상 환경을 모두 최신 상태로 유지하기 위한 실용적인 단계를 안내합니다.
새로 시작한다면 [설치](/ko/installation)를 참고하세요. 다른 프레임워크에서 옮겨오는 경우라면 [LangGraph에서 마이그레이션](/ko/guides/migration/migrating-from-langgraph)을 참고하세요.
---
## 업그레이드할 수 있는 두 가지
CrewAI는 사용자의 머신에 두 곳에 존재하며, 각각 독립적으로 업그레이드됩니다:
| 무엇 | 설치 방법 | 업그레이드 방법 |
|---|---|---|
| **전역 `crewai` CLI** | `uv tool install crewai` | `uv tool install crewai --upgrade` |
| **프로젝트 venv** (코드가 실행되는 곳) | `crewai install` / `uv sync` | `uv add "crewai[...]>=X.Y.Z"` 후 `crewai install` |
이 둘은 — 그리고 자주 — 동기화가 어긋날 수 있습니다. `crewai --version`은 CLI 버전을 알려줍니다. 프로젝트 안에서 `uv pip show crewai`를 실행하면 venv 버전을 알려줍니다. 둘이 다른 것은 정상이며, 실행 중인 코드에 중요한 것은 venv 버전입니다.
## 왜 `crewai install`만으로는 업그레이드되지 않는가
`crewai install`은 `uv sync`를 감싼 얇은 래퍼입니다. 현재 `uv.lock` 파일이 지시하는 것 그대로를 설치할 뿐이며 — 어떤 버전 제약도 올리지 **않습니다**.
`pyproject.toml`이 `crewai>=1.11.1`이라 적혀 있고 lock 파일이 `1.11.1`로 해소되었다면, `crewai install`을 실행해도 `1.14.4`가 사용 가능하더라도 영원히 `1.11.1`에 머무릅니다.
실제로 업그레이드하려면 다음을 해야 합니다:
1. `pyproject.toml`의 버전 제약 업데이트
2. lock 파일 재해소
3. venv 동기화
`uv add`는 이 세 가지를 한 번에 처리합니다.
## 프로젝트 업그레이드 방법
```bash
# 제약을 올리고 lock을 다시 만드는 한 번의 명령
uv add "crewai[tools]>=1.14.4"
# venv 동기화 (crewai install은 내부적으로 uv sync를 호출)
crewai install
# 확인
uv pip show crewai
# → Version: 1.14.4
```
`[tools]`를 프로젝트에서 사용하는 extras로 바꾸세요 (예: `[tools,anthropic]`). 잘 모르겠다면 `pyproject.toml`의 `dependencies` 목록을 확인하세요.
<Note>
`uv add`는 `pyproject.toml`과 `uv.lock`을 **둘 다** 원자적으로 업데이트합니다. `pyproject.toml`을 수동으로 편집하는 경우, `crewai install`이 새 버전을 가져가도록 하기 전에 `uv lock --upgrade-package crewai`를 실행해 lock 파일을 다시 해소해야 합니다.
</Note>
## 전역 CLI 업그레이드
전역 CLI는 프로젝트와 분리되어 있습니다. 다음 명령으로 업그레이드하세요:
```bash
uv tool install crewai --upgrade
```
업그레이드 후 셸이 `PATH`에 대해 경고하면 새로고침하세요:
```bash
uv tool update-shell
```
이 명령은 프로젝트의 venv를 **건드리지 않습니다** — 프로젝트 내부에서는 여전히 `uv add` + `crewai install`이 필요합니다.
## 둘이 동기화되었는지 확인
```bash
# 전역 CLI 버전
crewai --version
# 프로젝트 venv 버전
uv pip show crewai | grep Version
```
둘이 일치할 필요는 없지만 — 런타임 동작에 중요한 것은 프로젝트 venv 버전입니다.
<Note>
CrewAI는 `Python >=3.10, <3.14`를 요구합니다. `uv`가 더 오래된 인터프리터로 설치되어 있다면, `crewai install`을 실행하기 전에 지원되는 Python으로 프로젝트 venv를 다시 만드세요.
</Note>
---
## 브레이킹 체인지 및 마이그레이션 노트
대부분의 업그레이드는 작은 조정만 필요합니다. 아래 항목들은 조용히 깨지거나 헷갈리는 트레이스백을 내는 영역들입니다.
### Import 경로: tools와 `BaseTool`
tools의 정식 import 위치는 `crewai.tools`입니다. 옛 경로들이 아직 튜토리얼에 등장하지만 업데이트해야 합니다.
```python
# 이전
from crewai_tools import BaseTool
from crewai.agents.tools import tool
# 이후
from crewai.tools import BaseTool, tool
```
`@tool` 데코레이터와 `BaseTool` 서브클래스는 모두 `crewai.tools`에 있습니다. `AgentFinish` 등 내부 에이전트 심볼들은 더 이상 공개 표면이 아닙니다 — import 중이었다면 event listener나 `Task` 콜백으로 전환하세요.
### `Agent` 파라미터 변경
```python
from crewai import Agent
agent = Agent(
role="Researcher",
goal="Find authoritative sources on {topic}",
backstory="You are a careful, source-driven researcher.",
llm="gpt-4o-mini", # 모델명 문자열 또는 LLM 객체
verbose=True, # 정수 레벨이 아닌 bool
max_iter=15, # 버전마다 기본값이 바뀌었음 — 명시적으로 지정
allow_delegation=False,
)
```
- `llm`은 문자열 모델명(설정된 provider를 통해 해소)이나 세밀한 제어를 위한 `LLM` 객체를 받습니다.
- `verbose`는 일반 `bool`입니다. 정수를 전달해도 더 이상 로그 레벨을 토글하지 않습니다.
- `max_iter`의 기본값은 릴리스 사이에 변경되었습니다. 첫 tool 호출 후 에이전트가 조용히 반복을 멈춘다면 `max_iter`를 명시적으로 지정하세요.
### `Crew` 파라미터
```python
from crewai import Crew, Process
crew = Crew(
agents=[...],
tasks=[...],
process=Process.sequential, # 또는 Process.hierarchical
memory=True,
cache=True,
embedder={"provider": "openai", "config": {"model": "text-embedding-3-small"}},
)
```
- `process=Process.hierarchical`은 `manager_llm=` 또는 `manager_agent=` 중 하나가 필요합니다. 둘 다 없으면 kickoff 시 검증 단계에서 오류가 발생합니다.
- 기본이 아닌 임베딩 provider와 함께 `memory=True`를 쓰려면 `embedder` dict가 필요합니다 — 아래의 [메모리와 embedder 설정](#memory-embedder-config)을 참고하세요.
### `Task` 구조화된 출력
`output_pydantic`, `output_json`, 또는 `output_file`을 사용해 task 결과를 타입이 지정된 형태로 강제할 수 있습니다:
```python
from pydantic import BaseModel
from crewai import Task
class Article(BaseModel):
title: str
body: str
write = Task(
description="Write an article about {topic}",
expected_output="A short article with a title and body",
agent=writer,
output_pydantic=Article, # 인스턴스가 아닌 클래스
output_file="output/article.md",
)
```
`output_pydantic`은 **클래스** 자체를 받습니다. `Article(title="", body="")`을 전달하는 것은 흔한 실수이며 헷갈리는 검증 오류로 실패합니다.
### 메모리와 embedder 설정 {#memory-embedder-config}
`memory=True`이고 OpenAI의 기본 임베딩을 사용하지 않는다면, `embedder`를 반드시 전달해야 합니다:
```python
crew = Crew(
agents=[...],
tasks=[...],
memory=True,
embedder={
"provider": "ollama",
"config": {"model": "nomic-embed-text"},
},
)
```
해당 provider의 자격 증명(`OPENAI_API_KEY`, `OLLAMA_HOST` 등)을 `.env` 파일에 설정하세요. 메모리 저장 경로는 기본적으로 프로젝트-로컬입니다 — embedder를 바꾸면 차원이 호환되지 않으므로 프로젝트의 메모리 디렉터리를 삭제하세요.

View File

@@ -0,0 +1,244 @@
---
title: 커스텀 도구 배포하기
description: PyPI에 게시할 수 있는 CrewAI 호환 도구를 빌드, 패키징, 배포하는 방법을 안내합니다.
icon: box-open
mode: "wide"
---
## 개요
CrewAI의 도구 시스템은 확장 가능하도록 설계되었습니다. 다른 사용자에게도 유용한 도구를 만들었다면, 독립적인 Python 라이브러리로 패키징하여 PyPI에 게시하고 모든 CrewAI 사용자가 사용할 수 있도록 할 수 있습니다. CrewAI 저장소에 PR을 보낼 필요가 없습니다.
이 가이드에서는 도구 계약 구현, 패키지 구조화, PyPI 게시까지의 전체 과정을 안내합니다.
<Note type="info" title="배포할 계획이 없으신가요?">
프로젝트 내에서만 사용할 커스텀 도구가 필요하다면 [커스텀 도구 생성](/ko/learn/create-custom-tools) 가이드를 참고하세요.
</Note>
## 도구 계약
모든 CrewAI 도구는 다음 두 가지 인터페이스 중 하나를 충족해야 합니다:
### 옵션 1: `BaseTool` 서브클래싱
`crewai.tools.BaseTool`을 서브클래싱하고 `_run` 메서드를 구현합니다. `name`, `description`, 그리고 선택적으로 입력 검증을 위한 `args_schema`를 정의합니다.
```python
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
class GeolocateInput(BaseModel):
"""GeolocateTool의 입력 스키마."""
address: str = Field(..., description="지오코딩할 도로명 주소.")
class GeolocateTool(BaseTool):
name: str = "Geolocate"
description: str = "도로명 주소를 위도/경도 좌표로 변환합니다."
args_schema: type[BaseModel] = GeolocateInput
def _run(self, address: str) -> str:
# 구현 로직
return f"40.7128, -74.0060"
```
### 옵션 2: `@tool` 데코레이터 사용
간단한 도구의 경우, `@tool` 데코레이터로 함수를 CrewAI 도구로 변환할 수 있습니다. 함수에는 반드시 독스트링(도구 설명으로 사용됨)과 타입 어노테이션이 있어야 합니다.
```python
from crewai.tools import tool
@tool("Geolocate")
def geolocate(address: str) -> str:
"""도로명 주소를 위도/경도 좌표로 변환합니다."""
return "40.7128, -74.0060"
```
### 핵심 요구사항
어떤 방식을 사용하든, 도구는 다음을 충족해야 합니다:
- **`name`** — 짧고 설명적인 식별자.
- **`description`** — 에이전트에게 도구를 언제, 어떻게 사용할지 알려줍니다. 에이전트가 도구를 얼마나 잘 활용하는지에 직접적으로 영향을 미치므로 명확하고 구체적으로 작성하세요.
- **`_run`** (BaseTool) 또는 **함수 본문** (@tool) 구현 — 동기 실행 로직.
- 모든 매개변수와 반환 값에 **타입 어노테이션** 사용.
- **문자열** 결과를 반환 (또는 의미 있게 문자열로 변환 가능한 값).
### 선택사항: 비동기 지원
I/O 바운드 작업을 수행하는 도구의 경우 비동기 실행을 위해 `_arun`을 구현합니다:
```python
class GeolocateTool(BaseTool):
name: str = "Geolocate"
description: str = "도로명 주소를 위도/경도 좌표로 변환합니다."
def _run(self, address: str) -> str:
# 동기 구현
...
async def _arun(self, address: str) -> str:
# 비동기 구현
...
```
### 선택사항: `args_schema`를 통한 입력 검증
Pydantic 모델을 `args_schema`로 정의하면 자동 입력 검증과 명확한 에러 메시지를 받을 수 있습니다. 제공하지 않으면 CrewAI가 `_run` 메서드의 시그니처에서 추론합니다.
```python
from pydantic import BaseModel, Field
class TranslateInput(BaseModel):
"""TranslateTool의 입력 스키마."""
text: str = Field(..., description="번역할 텍스트.")
target_language: str = Field(
default="en",
description="대상 언어의 ISO 639-1 언어 코드.",
)
```
배포용 도구에는 명시적 스키마를 권장합니다 — 에이전트 동작이 개선되고 사용자에게 더 명확한 문서를 제공합니다.
### 선택사항: 환경 변수
도구에 API 키나 기타 설정이 필요한 경우, `env_vars`로 선언하여 사용자가 무엇을 설정해야 하는지 알 수 있도록 합니다:
```python
from crewai.tools import BaseTool, EnvVar
class GeolocateTool(BaseTool):
name: str = "Geolocate"
description: str = "도로명 주소를 위도/경도 좌표로 변환합니다."
env_vars: list[EnvVar] = [
EnvVar(
name="GEOCODING_API_KEY",
description="지오코딩 서비스 API 키.",
required=True,
),
]
def _run(self, address: str) -> str:
...
```
## 패키지 구조
프로젝트를 표준 Python 패키지로 구성합니다. 권장 레이아웃:
```
crewai-geolocate/
├── pyproject.toml
├── LICENSE
├── README.md
└── src/
└── crewai_geolocate/
├── __init__.py
└── tools.py
```
### `pyproject.toml`
```toml
[project]
name = "crewai-geolocate"
version = "0.1.0"
description = "도로명 주소를 지오코딩하는 CrewAI 도구."
requires-python = ">=3.10"
dependencies = [
"crewai",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```
사용자가 자동으로 호환 버전을 받을 수 있도록 `crewai`를 의존성으로 선언합니다.
### `__init__.py`
사용자가 직접 import할 수 있도록 도구 클래스를 re-export합니다:
```python
from crewai_geolocate.tools import GeolocateTool
__all__ = ["GeolocateTool"]
```
### 명명 규칙
- **패키지 이름**: `crewai-` 접두사를 사용합니다 (예: `crewai-geolocate`). PyPI에서 검색할 때 도구를 쉽게 찾을 수 있습니다.
- **모듈 이름**: 밑줄을 사용합니다 (예: `crewai_geolocate`).
- **도구 클래스 이름**: `Tool`로 끝나는 PascalCase를 사용합니다 (예: `GeolocateTool`).
## 도구 테스트
게시 전에 도구가 크루 내에서 작동하는지 확인합니다:
```python
from crewai import Agent, Crew, Task
from crewai_geolocate import GeolocateTool
agent = Agent(
role="Location Analyst",
goal="주어진 주소의 좌표를 찾습니다.",
backstory="지리공간 데이터 전문가.",
tools=[GeolocateTool()],
)
task = Task(
description="1600 Pennsylvania Avenue, Washington, DC의 좌표를 찾으세요.",
expected_output="해당 주소의 위도와 경도.",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task])
result = crew.kickoff()
print(result)
```
## PyPI에 게시하기
도구 테스트를 완료하고 준비가 되면:
```bash
# 패키지 빌드
uv build
# PyPI에 게시
uv publish
```
처음 게시하는 경우 [PyPI 계정](https://pypi.org/account/register/)과 [API 토큰](https://pypi.org/help/#apitoken)이 필요합니다.
### 게시 후
사용자는 다음과 같이 도구를 설치할 수 있습니다:
```bash
pip install crewai-geolocate
```
또는 uv를 사용하여:
```bash
uv add crewai-geolocate
```
그런 다음 크루에서 사용합니다:
```python
from crewai_geolocate import GeolocateTool
agent = Agent(
role="Location Analyst",
tools=[GeolocateTool()],
# ...
)
```