CrewAI入門:実践ガイド
私は3時間かけて、APIエンドポイントを幻覚し続けるCrewAIエージェントのデバッグを行い、そこで重要なことを学びました。CrewAIの強力さは、同時に最大の落とし穴でもあるということです。LangChainから移行してきた方、あるいはマルチエージェントシステムを始めたばかりの方なら、複数のAIエージェントを調整することが、単にプロンプトをつなげるだけではないこと、つまり依存関係、コンテキスト、障害モードの管理が重要であることにすぐに気づくでしょう。このガイドでは、自動ブログコンテンツ生成のための実際のCrewAIシステムを構築する中で学んだこと、具体的には壊れたコードとその修正方法について説明します。
課題:なぜ単一エージェントではダメなのか?
おそらく、単一のLLM呼び出しでブログ記事を生成しようとしたことがあるでしょう。それはうまくいきます。ただし、ファクトチェック、SEO最適化、フォーマットが必要になるまでは。単一エージェントはコンテキストを忘れたり、自己矛盾を起こしたり、無意味な内容を生成したりします。CrewAIは、人間のチームのように、専門化されたエージェントを定義してタスクを相互に渡すことで、この問題を解決します。しかし、ここに落とし穴があります。CrewAIのシンプルさは複雑さを隠しています。エージェントの役割とタスクを慎重に設計しないと、循環依存、無限ループ、あるいは作業の引き継ぎを拒否するエージェントが発生します。
ステップ1:CrewAIのインストール(そして落とし穴)
pip install crewai
これで必要なものがすべてインストールされると思っていました。間違いでした。 CrewAIはlangchainとopenaiに依存していますが、最新バージョンではありません。Python 3.12を使用している場合、pydanticの競合が発生します。以下が私が使用した正確な修正方法です。
pip install crewai langchain==0.1.0 openai==1.6.1 pydantic==2.5.0
これらのバージョンを固定しないと、ImportError: cannot import name 'BaseModel' from 'pydantic'というエラーが発生します。これで30分無駄にしました。
ステップ2:エージェントを定義する(具体的にしないと苦労する)
CrewAIのエージェントは、role、goal、backstoryを持つPythonクラスとして定義されます。backstoryはオプションですが、エージェントのトーンと動作を制御するため重要です。以下が私が最初に使用したものです。
from crewai import Agent
class Researcher(Agent):
role = "リサーチャー"
goal = "医療におけるAIに関する最近のニュースを見つける"
backstory = "あなたは情報源を検証する几帳面な研究者です。"
これは動作しますが、漠然としすぎています。エージェントは一般的な応答を生成します。テスト後、制約を追加することを学びました。
class Researcher(Agent):
role = "シニアヘルスケアAIリサーチャー"
goal = "腫瘍学におけるAIに関する最近(2024年)の査読付き論文を3件見つける"
backstory = """あなたは医療AI分野で10年の経験があります。
常に具体的なPMIDまたはDOIリンクを引用します。
情報源を捏造することは決してありません。"""
情報源を引用する明示的な指示と年の制約に注目してください。これらがないと、私のエージェントは偽の論文を作り出しました。CrewAIは事実を検証しません。LLMを信頼するのです。
ステップ3:正しく連鎖するタスクを作成する
タスクは、ほとんどの人が失敗する部分です。タスクにはdescription、expected_output、agentがあります。ポイントは、タスクを以前の出力に依存させることです。私は2つのエージェントからなるパイプラインを構築しました。
from crewai import Task
research_task = Task(
description="AIヘルスケアに関する最近の論文を3件見つける。タイトルとリンクのリストを出力する。",
expected_output="タイトル、年、URLを含む3件の論文の箇条書きリスト",
agent=researcher
)
writing_task = Task(
description="""リサーチの出力に基づいて、調査結果を要約した500語のブログ記事を書く。
提供された論文への引用を含めること。""",
expected_output="見出しと引用元を含むマークダウン形式のブログ記事",
agent=writer
)
ここにバグがあります。writing_taskはresearch_taskの出力を明示的に参照していません。CrewAIはエージェントのメモリを通じて暗黙的にコンテキストを渡しますが、これは信頼性が低いです。私はタスク依存関係を使用して修正しました。
writing_task = Task(
description="""前のタスクのリサーチ出力に基づいて、調査結果を要約した500語のブログ記事を書く。
リサーチ出力は次の通りです:{research_output}""",
expected_output="見出しと引用元を含むマークダウン形式のブログ記事",
agent=writer,
context=[research_task] # 明示的な依存関係
)
contextパラメータは、出力が説明に注入されるタスクのリストです。これがないと、私のライターエージェントは独自のリサーチを幻覚していました。
ステップ4:Crewを実行する(そして障害に対処する)
次にCrewを作成して実行します。
from crewai import Crew
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
verbose=True # デバッグに不可欠
)
result = crew.kickoff()
print(result)
初めて実行したときは動作しましたが、45秒かかり、API呼び出しで0.12ドルの費用がかかりました。詳細出力を見ると、リサーチャーエージェントが論文を見つけるために3回のAPI呼び出しを行い、その後ライターエージェントが記事を書くためにさらに2回の呼び出しを行っていることがわかりました。CrewAIのデフォルトは逐次実行で、各タスクは前のタスクが完了するのを待ちます。
欠点: 最初のタスクが失敗した場合(例:APIがエラーを返す)、Crew全体がクラッシュします。CrewAIには組み込みのリトライロジックがありません。私は簡単なリトライラッパーを追加しました。
import time
def safe_kickoff(crew, max_retries=3):
for attempt in range(max_retries):
try:
return crew.kickoff()
except Exception as e:
print(f"試行 {attempt+1} が失敗しました:{e}")
time.sleep(2 ** attempt) # 指数バックオフ
raise Exception("3回試行後もCrewは失敗しました")
ステップ5:実践的な最適化のヒント
1週間のテストの後、実際に信頼性を向上させたものを紹介します。
エージェントのメモリを制限する:デフォルトでは、エージェントは会話全体を記憶します。長いタスクでは、コンテキストウィンドウが大きくなりすぎます。履歴が必要ないエージェントには
memory=Falseを設定します。researcher = Researcher(memory=False)allow_delegation=Falseを使用する:デフォルトでは、エージェントは他のエージェントにタスクを委任できます。これによりループが発生します。複雑な階層を構築していない限り、無効にします。researcher = Researcher(allow_delegation=False)結果をキャッシュする:CrewAIはデフォルトでLLM呼び出しをキャッシュしますが、エージェントごとです。同じCrewを2回実行すると、応答を再利用します。これはデバッグには便利ですが、本番環境では危険です。古いデータを提供する可能性があります。キャッシュを無効にするには:
crew = Crew(agents=[...], tasks=[...], cache=False)トークン使用量を監視する:CrewAIはトークン数を公開しません。私は簡単なコールバックを追加しました。
from langchain.callbacks import get_openai_callback with get_openai_callback() as cb: result = crew.kickoff() print(f"総トークン数:{cb.total_tokens}、費用:${cb.total_cost}")
CrewAIが教えてくれない最大の制限
コンテンツ生成のための5エージェントシステムを構築した後、壁にぶつかりました。CrewAIにはエージェント障害に対する組み込みのエラー回復機能がありません。 リサーチャーエージェントが無意味な出力を返した場合でも、ライターエージェントはそれを使用しようとします。唯一の修正方法は、タスクの説明で出力を検証することです。
research_task = Task(
description="""3件の論文を見つける。3件見つけられない場合は、'NO_RESULTS'を出力し、
理由を説明する。論文を捏造しないこと。""",
...
)
その後、ライティングタスクでこのセンチネル値をチェックします。ハッキーな方法ですが、機能します。
次のステップ:実際のプロジェクトを構築する
理論から始めないでください。私の壊れたサンプルをgithub.com/your-repo/crewai-blog-generatorからクローンし、意図的なバグを修正してください。READMEには、私が意図的に壊した3つのことが書かれています。
- ライタータスクの
contextパラメータの欠落 - API障害に対するリトライロジックの欠如
allow_delegation=Trueによるエージェントの無限ループ
これらを修正したら、ライターの引用を検証するFactCheckerエージェントを追加してシステムを拡張してください。ドキュメントを1時間読むよりも、30分のデバッグでより多くを学べます。そして、何かを壊してしまったときは、詳細出力が最良の友であることを忘れないでください。Trueに設定して、エージェントのすべての決定を確認しましょう。