実践ガイド:オープンソースエージェントフレームワークとElevenAgents
- 公開日
- 最終更新日
聴くこの記事を聴く
前回のElevenLabsの音声オーケストレーションと外部エージェントの統合に関する記事では、既存のテキストベースのエージェントオーケストレーションをカスタムLLMを介してElevenLabsに接続する方法を紹介しました。このガイドでは、その基盤をもとに、主要なオープンソースのエージェントフレームワークをカスタムLLMインターフェースの背後に適応・デプロイする方法を解説します。その結果、状態管理、ツールオーケストレーション、アプリケーション固有の制御を損なうことなく、成熟したエージェントシステムに音声を重ねられる柔軟なアーキテクチャが実現します。フレームワークにかかわらず、共通して次の3ステップに従います。生成リクエストを作成し、最終的なテキスト応答を抽出して、OpenAI互換のServer-Sent Events(SSE)形式に再構成します。ElevenLabsはChat CompletionsとResponsesの両形式をサポートしています。このガイドでは広く利用されている4つのフレームワークを扱いますが、このパターンはOpenAI互換のストリーミング出力を生成できるあらゆるランタイムに応用できます。
.webp&w=3840&q=80)
共通セットアップ
このセクションの例ではPythonとFastAPIを使用しますが、HTTP POSTリクエストとSSEレスポンスのストリーミングを処理できるスタックであれば使用できます。ElevenLabsの音声オーケストレーションが発話終了の可能性を検出すると、設定済みのカスタムLLMエンドポイントに生成リクエストを送信します。このセクションでは、音声オーケストレーションとエージェントフレームワークを同じ言語でつなぐブリッジ、つまりプロキシとなる変換レイヤーの主要コンポーネントを説明します。
当然ながら、各フレームワークは一般的な知名度や特定の目的を果たす能力を理由に選ばれる場合があります。たとえばLlamaIndexは、もともと検索拡張生成(RAG)のセットアップを簡単にするために開発され、CrewAIはエージェント時代における定義済みタスクの自動化を目的として構築されました。設計目標が異なれば応答構造も異なり、それぞれに固有の処理が必要です。ターン全体の完了を待つのではなく、LLMが生成したチャンクをその都度ストリーミングすることは重要です。これにより、テキスト読み上げ(TTS)モデルがより早く音声の生成を開始でき、体感レイテンシーを低減できます。ここでは、LangGraph、Google ADK、CrewAI、LlamaIndexという代表的な4つのフレームワークに焦点を当てます。
共有コードについて
各フレームワークは、OpenAI互換のSSEチャンクとして応答をストリーミングする必要があります。ここでは、これらのチャンクを構築するために各例で使用する小さなヘルパー関数を紹介します。
準備ができたので、LangGraphから始めましょう。
LangGraph
LangGraphでは、エージェントをグラフとしてモデル化します。ノードは個々のステップを表し、エッジはノード間の制御フローを定義します。最小限のセットアップはシンプルです。チャットモデルを初期化し、エージェントツールを定義して、エージェントグラフのランタイムを作成します。
LangGraph Agentは生成リクエストごとに会話履歴全体を受け取るため、必要な状態を内部で維持できます。LangGraphはチェックポイントによるサーバー側の永続化をサポートしていますが、実装を最小限に抑えるため、ここでは扱いません。
状態管理ができたら、次にLangGraph固有の判断ポイントとなるのがストリーミングモードです。LangGraphには、用途に応じた次の2つの選択肢があります。
- stream_mode="values"はグラフ状態のスナップショットを提供します。実装は簡単ですが、各応答により完全なメッセージ状態が含まれるため、リアルタイムの会話フローではレイテンシーが増加します。
- stream_mode="messages"は、モデルから増分メッセージチャンクをストリーミングします。ElevenLabsのオーケストレーションレイヤーで音声の最初の出力までの時間を短縮できるため、一般にリアルタイム音声インタラクションにはこちらが推奨されます。
より具体的には、エージェントループのmessages実装には、音声として読み上げるべきではないツール呼び出しの更新など、中間ステップが含まれます。プロキシはこれらを除外し、ユーザー向けの応答テキストだけをTTSレイヤーに渡します。ツールを使用するターンの例を示します。
[1] モデルがツールを呼び出すと判断する(tool_calls=["get_price"])[2] ツールが実行され、データを返す(result="$24.99") [3] モデルが結果を使って応答を生成する(content="価格は$24.99です")
当然、SSEストリームで転送すべきなのはステップ3のチャンクだけです。実際には、ストリーミングループ内の2つのガードチェックでこのフィルタリングを行います。1つはlanggraph_node == "model"イベントだけを保持し、もう1つは空のコンテンツをスキップします。これらのチェックにより、ユーザー向けアシスタントテキストだけがSSEとしてElevenLabsに転送されます。これらの概念を組み合わせた、軽量なリクエストプロキシの実装を紹介します。
これにより、ユーザー向けのモデルチャンクだけがElevenLabsに転送されます。LangGraphでは内部ツールの実行が状態ストリームを通じて可視化されるため、フィルタリングは明示的に行われ、プロキシによって制御されます。
次に、GoogleのAgent Development Kit(ADK)を扱う際のポイントを見ていきます。
Google ADK
GoogleのADKは、ランタイムループをAgent、Runner、SessionServiceという少数のコアプリミティブの背後に抽象化します。ADKのRunnerは、HTTPレイヤーとエージェント定義の間に位置します。メッセージルーティング、ツールオーケストレーション、セッションライフサイクル、イベントストリーミングを処理します。
エージェント、セッションバックエンド、Runnerを初期化したら、プロキシは受信リクエストごとにADKセッションを取得または作成します。ADKでは、session_idがメモリの永続化を制御します。同じsession_idをターン間で再利用すると、履歴、ツール呼び出し、過去の応答が自動的に引き継がれます。会話IDはElevenLabsの上流にあるため、プロキシがこのマッピングを明示的に処理します。生成リクエストに正しいIDを渡すことで、SDKは過去のコンテキストを内部で処理できます。会話の開始時には、追加パラメータをリクエスト本文に渡して任意のIDを指定します。
メッセージとセッションを準備すれば、Runnerを呼び出せます。実行中、ツール呼び出しとツール結果は内部ADKイベントとして引き続き表示されますが、ユーザー向け出力ではなく中間的なオーケストレーションステップとして扱われます。そのため、ツール呼び出しがユーザーに見えるテキストとして現れるフレームワークと比べ、手動フィルターは不要です。
以下のハンドラーは、セッションの解決と取得・作成ロジックをインラインで含む簡略化した実装です。
次に、設計上よりタスク中心のCrewAIを見ていきます。
CrewAI
CrewAIは、自由形式の対話ループではなく、構造化されたタスク(調査、執筆、要約)を中心にマルチエージェントワークフローをオーケストレーションするために設計されています。エージェントはロール、目標、バックストーリーによって定義されます。実行は、それぞれ明確な説明と期待する出力を持つTaskオブジェクトを中心に行われます。
LangGraphやADKで使われるエージェントループモデルとは異なり、CrewAIでは通常、会話のそのターンにおける作業単位を定義するため、リクエストごとにTaskとCrewを構築します。プレースホルダーを介して前のターンを次のタスクに挿入し、会話コンテキストを引き継ぎます。{crew_chat_messages}変数にはリクエストごとに現在の会話履歴が設定され、実行時にタスクの説明へ補間されます。また、中間トレースパターン(Thought、Action、Action Input、Observation)を明示的に除外し、最終回答のテキストだけを出力することで、音声出力に適したクリーンなテキストを生成します。
以下のハンドラーは、リクエストごとのタスク構築、履歴の補間、Crewレベルのストリーミング、トレースのフィルタリング、出力フォーマットをまとめたものです。
次に、ネイティブのイベント駆動型ストリーミングモデルに焦点を当てた、異なるアプローチのLlamaIndexを見ていきます。
LlamaIndex
この投稿で扱うほかのフレームワークとは異なり、LlamaIndexはLLMを外部データソース(ドキュメントストア、インデックス、検索パイプライン)に接続するために設計されています。そのエージェントレイヤーであるFunctionAgentは、この基盤の上で構造化されたコンテキストを取得・推論するものであり、自由な対話やタスク実行を目的とするものではありません。
会話の連続性を保つため、プロキシは受信メッセージをLlamaIndexのチャットメッセージに変換し、最新のユーザーターン(user_msg)と過去のターン(chat_history)に分割します。各AgentStreamイベントのevent.deltaフィールドには次のテキスト断片が含まれ、OpenAI形式のdelta.contentチャンクに直接マッピングされます。空でないdeltaはそのまま転送できるため、これが本ガイドで最もシンプルなストリーミングブリッジです。ストリームには、オーケストレーションイベント(ツール呼び出し、結果)と音声イベント(アシスタントのテキストdelta)の両方が含まれます。音声出力をクリーンに保つため、プロキシはAgentStreamイベントだけを保持し、空のdeltaをスキップします。
[1] AgentStream(delta='') ← 無視[2] ToolCall ← 無視[3] ToolCallResult ← 無視[4] AgentStream(delta='It') ← 転送 ✓[5] AgentStream(delta=' costs')← 転送 ✓[6] AgentStream(delta=' $49.99')← 転送 ✓
この分離により、中間的なツールの仕組みを読み上げ出力から除外しながら、低レイテンシーの増分音声生成を維持できます。以下のそのまま使えるハンドラーは、これらの手順をまとめたものです。
LlamaIndexは、より強力な組み込みオーケストレーションレイヤーを持つフレームワークと比べ、エンドツーエンドの会話ランタイムパターンについて規定が少なめです。本番デプロイでは通常、セッション処理、応答ガードレール、ツールオーケストレーション、トレーシングを実装する必要があります。
まとめ
このガイドの各フレームワークは、同じ契約を通じてElevenLabsに接続します。OpenAI形式のCompletionsまたはResponsesリクエストを受け取り、SSEチャンクをストリーミングで返します。これにより、既存のエージェント実装に最小限の変更で音声オーケストレーションを重ねられます。すでに構築したものを維持しつつ、リアルタイムの会話型AIを実現できます。このモジュール性はElevenAgentsプラットフォームの中核となる考え方です。既存のエージェントを拡張する場合でも、最初から音声ネイティブで構築する場合でも、ElevenAgentの音声オーケストレーションは、それぞれの状況に合わせて利用できます。
すでにオープンソースのフレームワークでエージェントを運用していて、音声を有効にしたい場合は、ぜひこのアプローチを試して感想をお聞かせください。



