導入
AIエージェントを構築するのは一つのことです。しかし、実際のアプリケーションで「本当に連携して動く」エージェントを作るとなると、話は一気に難しくなります。
本日は、3つの専門エージェント――要約担当(Summarizer)、Q&Aエンジン、コード生成担当(Code Generator)――を1つの統合された協調ワークフローに組み込む、コンポーザブルなマルチエージェントシステムを作っていきます。フロントエンドにはNext.js、エージェントのオーケストレーションにはLangGraph、そしてCopilotKitを使って、すばらしいリアルタイムUIでそれらを結び付けます。
アーキテクチャ、重要なパターン、エージェント間での状態の流れ、そしてゼロからこれを構築するためのステップバイステップのガイドまで解説します。
さっそく作りましょう。
GitHubの完全なソースコードと、CopilotKitのGitHub ⭐️ をご覧ください
CopilotKitとは?
CopilotKitは、開発者がWebアプリケーションにAIコパイロットを簡単に組み込めるようにするオープンソースのフレームワークです。LLM(OpenAIモデルなど)をReactまたはNext.jsのコンポーネントに接続するためのツールが用意されており、ユーザーはチャット、フォーム、あるいはライブダッシュボード上で、あなたのアプリケーションの中でAIと直接やり取りできます。
CopilotKitにより実現できます:
- ユーザーの状態やアプリケーションのコンテキストを理解する、状況に応じたコパイロット。
- フロントエンドとAIモデル間でのリアルタイムのストリーミング。
- コンポーザブルなエージェントアーキテクチャ。要約、分析、生成などを行うためのさまざまなAIモジュールを、差し替え可能な部品として組み込めます。
- ReactおよびNext.jsとの簡単でカスタマイズ可能な連携のための、開発者最優先のAPI。 要するに、CopilotKitはフロントエンドUIとバックエンドのAIをつなぎ、現代的なWebアプリケーションのためのインテリジェントなレイヤーを作り出します。 公式ドキュメントを参照してください
何を作っているのか?
私たちは、エージェントシステムの完全なスタックを構築します(つまり、複数のエージェントが特定の能力を持ち、それらが連鎖[またはパイプライン]の形で協働できる、フルスタックのコンポーザブルなエージェントシステムです)。
以下は、各エージェントがどのようにパイプライン全体のシーケンスの一部として動作するかを示す図です。ユーザーがシステムに対してアクションを要求すると、そのアクションは次のような簡略化された流れに従います。
[ユーザーがプロンプトを入力]
↓
Next.js UI(CopilotChat)
↓
(POST /api/copilotkit → GraphQL)
Next.js API
↓
(LangGraphHttpAgentがFastAPIへリクエストを転送)
FastAPI(copilotkit + LangGraph)
↓
(LangGraphAgentのワークフロー:Summarizer → Q&A → CodeGen)
↓
(OpenAI GPT-4o-miniを呼び出す)
↓
フロントエンドUIへストリーミングされ、リアルタイムの更新が生成されます。
コンポーザブル設計の素晴らしさは、エージェント同士が完全に独立していることです。したがって、アーキテクチャ上の他のエージェントに影響を与えることなく、エージェントを簡単に差し替えたり、並び順を変更したり、追加したりできます。
アーキテクチャとテックスタック
中心となるのは、次のスタックです:
- Next.js 14:フロントエンドにはApp RouterとTypeScript
- CopilotKitのSDK:UIにエージェントを埋め込む:@copilotkit/react-core、@copilotkit/runtime、@copilotkit/react-ui
- FastAPI & Uvicorn:エージェントを提供するためのバックエンドフレームワーク
- LangGraph:ステートレスなエージェントワークフローを構築するために
- LangChain:LLMのC/APIをオーケストレーションし、クライアントからLLMへのメッセージを扱うために
- OpenAI GPT-4o-mini:対応する入力リクエストに対するエージェントの出力を提供するために使用するLLM。さらに推論を行い、それらを生成します。;そして
- LangServe:LangChainのrunableをRESTスタイルのAPIとして提供するために
- Tailwind CSS & Framer Motion:グラスモーフィックなUIを開発し、アニメーションを取り込むために
全体のアーキテクチャ。
コンポーザブルなエージェントを使う動機
ソースコードに入る前に、なぜこのパターンが必要になるのかを話しましょう。
モノリシックなエージェントは良くない
モノリシックなエージェントシステムは、従来次のように見えます。
def handle_request(user_input):
# すべてを1つの巨大な関数でやる
summary = summarize(user_input)
analysis = analyze(summary)
code = generate_code(analysis)
return code
このアプローチはデモ目的には十分かもしれませんが、次のような多くの制約のため本番環境では機能しません:
- 個々の関数をテストすることが難しい。
- 何かがうまくいかないときにデバッグするのが難しい。
- 複数のワークフローでエージェントを活用できない。
- スケールやアップグレードを行いたい場合、現在の構成を維持したままではなく、システム全体を丸ごと書き直す必要がある。
問題解決におけるコンポーザブルなアプローチ
LangGraphは、各エージェントを独立したノードとして定義します。
workflow = StateGraph(AgentState)
返却形式: {"translated": "翻訳されたHTML"}# 各エージェントは別個の、テスト可能なノードです
workflow.add_node("summarizer", summarizer_node)
workflow.add_node("qna", qna_node)
workflow.add_node("codegen", codegen_node)
# フローを定義します
workflow.set_entry_point("summarizer")
workflow.add_edge("summarizer", "qna")
workflow.add_edge("qna", "codegen")
workflow.add_edge("codegen", END)
これは、次のことを意味します:
- 各エージェントは独立して検証できます。
- エラーが発生した場合、ワークフローの任意の時点から順序の流れを簡単に追跡できます。
- 多数の異なるワークフローでエージェントを利用できます。
- 既存のエージェントに影響を与えずに、追加のエージェントを組み込めます。
4. プロジェクトセットアップ
前提条件
- Node.js 18+ と npm
- Python 3.9+
- OpenAI API キー
インストール
フロントエンドのセットアップ
# Next.js アプリを作成します
npx create-next-app@latest composable-copilotkit-app --typescript --tailwind --app
cd composable-copilotkit-app
# CopilotKit の依存関係をインストールします
npm install@copilotkit/react-core@^1.51.3\
@copilotkit/react-ui@^1.51.3\
@copilotkit/runtime@^1.51.3\
framer-motion@^11.0.3
バックエンドのセットアップ
# エージェント用のディレクトリを作成します
mkdir agent
cd agent
# 仮想環境を作成します
python -m venv venv
# 有効化(Windows)
venv\Scripts\activate
# 有効化(macOS/Linux)
source venv/bin/activate
# 依存関係をインストールします
pip install \
langchain==0.3.13\
langchain-openai==0.2.14\
langgraph==0.2.62\
copilotkit>=0.1.39\
fastapi==0.115.6\
uvicorn==0.34.0\
python-dotenv==1.0.1
プロジェクト構成
composable-copilotkit-app/
├── app/
│ ├── api/
│ │ └── copilotkit/
│ │ └── route.ts # CopilotKit API エンドポイント
│ ├── globals.css # Glassmorphic スタイル
│ ├── layout.tsx # CopilotKit プロバイダを含むルートレイアウト
│ └── page.tsx # メイン UI
├── components/
│ └── LangGraphAgent.tsx # エージェントのラッパー
├── agent/
│ ├── agent.py # LangGraph ワークフロー
│ ├── server.py # FastAPI サーバー
│ └── requirements.txt
└── package.json
環境変数を追加する
ルートに .env.local を作成します:
env
OPENAI_API_KEY=your_openai_api_key_here
LANGGRAPH_URL=http://127.0.0.1:8000/copilotkit
agent/.env を作成します:
env
OPENAI_API_KEY=your_openai_api_key_here
5. フロントエンド:エージェントを UI に接続する
ステップ 1. app/layout.tsx に CopilotKit プロバイダを次のようにインストールして設定します:
import type { Metadata } from 'next'
import { Outfit } from 'next/font/google'
import './globals.css'
import "@copilotkit/react-ui/styles.css";
import { CopilotKit } from "@copilotkit/react-core";
const outfit = Outfit({
subsets: ['latin'],
display: 'swap',
})
返却形式: {"translated": "翻訳されたHTML"}export const metadata: Metadata = {
title: 'Composable CopilotKit: モジュール化されたエージェントアプリ',
description: 'CopilotKit & LangGraph を使用したマルチエージェントシステム',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={outfit.className}>
<CopilotKit
runtimeUrl="/api/copilotkit"
agent="researcher"
>
{children}
</CopilotKit>
</body>
</html>
)
}
重要な注意:
- ここでは、runtimeUrl="/api/copilotkit" として Next.js の API ルートを指定します。
- 使用したいエージェント名は、API ルート内で作成したエージェントを示すものです。agent="researcher" プロパティを通じて指定します。
手順 2: Next.js API 統合: FastAPI へのルート
これを行うには、app/api/copilotkit/route.ts に Next.js の Next API Route を作成します:
import {
CopilotRuntime,
copilotRuntimeNextJSAppRouterEndpoint,
EmptyAdapter,
} from "@copilotkit/runtime";
import { LangGraphHttpAgent } from "@copilotkit/runtime/langgraph";
import { NextRequest } from "next/server";
export const dynamic = "force-dynamic";
const runtime = new CopilotRuntime({
remoteEndpoints: [
copilotKitEndpoint({ url: "http://127.0.0.1:8000/copilotkit" }),
],
});
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter: new EmptyAdapter(),
endpoint: "/api/copilotkit",
});
export const GET = handleRequest;
export const POST = handleRequest;
このルートにより、次のことが可能になります:
- LangGraph エージェントを使用して、新しい CopilotRuntime を作成します。
- LangGraphHttpAgent を使って、FastAPI バックエンドと通信します。
- ストリーミング目的のために、GET と POST の両方のリクエストを処理します。
手順 3: チャット UI の構築
app/page.tsx にメインページを作成します:
'use client';
import React from 'react';
import {CopilotChat }from"@copilotkit/react-ui";
import {motion }from'framer-motion';
import"@copilotkit/react-ui/styles.css";
返却形式: {"translated": "翻訳されたHTML"}export default function Home() {
return (
<div className="flex h-screen w-full overflow-hidden">
{/* アニメーション背景 */}
<divclassName="bg-mesh"/>
{/* グラスモーフィックなサイドバー */}
<motion.aside
initial={{x:-20,opacity:0 }}
animate={{x:0,opacity:1 }}
className="w-80 glass-sidebar flex flex-col hidden md:flex z-20 m-4 rounded-[2rem]"
>
<divclassName="p-8 border-b border-white/40">
<h1className="font-extrabold text-xl">Composable</h1>
<pclassName="text-xs text-violet-600 font-bold">COPILOTKIT</p>
</div>
<divclassName="p-8 flex-1">
<h2className="text-xs font-bold text-slate-400 uppercase">
ActiveNeuralNodes
</h2>
<divclassName="space-y-4 mt-4">
<AgentCardicon=""name="Summarizer"/>
<AgentCardicon=""name="Q&A Engine"/>
<AgentCardicon="⚙️"name="Code Generator"/>
</div>
</div>
</motion.aside>
{/* メインのチャット領域 */}
<mainclassName="flex-1 flex flex-col relative z-10 m-4">
<headerclassName="h-20 flex items-center px-8">
<div>
<h2className="text-2xl font-black">AgentWorkspace</h2>
<pclassName="text-xs text-slate-500">
複数の-エージェントワークフローを統括
</p>
</div>
</header>
<divclassName="flex-1 flex items-center justify-center p-4">
<motion.div
initial={{y:30,opacity:0 }}
animate={{y:0,opacity:1 }}
className="w-full max-w-6xl h-full glass-card-ultra"
>
<CopilotChat
className="h-full ultra-chat"
instructions="あなたはマルチエージェントシステムです。正確で、役に立つようにしてください。"
labels={{
title:"ニューラル出力",
initial:"初期化を待っています...",
placeholder:"作りたいものを説明してください..."
}}
/>
</motion.div>
</div>
</main>
</div>
);
}
functionAgentCard({icon,name }: {icon:string,name:string }) {
return (
<motion.div
whileHover={{x:5 }}
className="p-4 rounded-2xl bg-white/40 border border-white/40"
>
<divclassName="flex items-center space-x-4">
<divclassName="text-xl">{icon}</div>
<h3className="text-sm font-bold">{name}</h3>
</div>
</motion.div>
);
}
ステップ 4: グラスモーフィックのスタイリング
以下のスタイルを app/globals.css に追加してください:
@tailwind base;
@tailwind components;
@tailwind utilities;
/* アニメーションするメッシュ背景 */
.bg-mesh {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background-color: #ffffff;
}
.bg-mesh::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background-image:
radial-gradient(at 10% 10%, hsla(250, 100%, 95%, 1) 0%, transparent 40%),
radial-gradient(at 90% 10%, hsla(280, 100%, 95%, 1) 0%, transparent 40%),
radial-gradient(at 50% 50%, hsla(220, 100%, 97%, 1) 0%, transparent 50%);
filter: blur(80px) saturate(180%);
opacity: 0.8;
animation: mesh-drift 15s ease-in-out infinite alternate;
}
返却形式: {"translated": "翻訳されたHTML"}@keyframes mesh-drift {
0% {
transform: scale(1) translate(0, 0);
}
100% {
transform: scale(1.15) translate(30px, -20px);
}
}
/* ガラスモーフィズム */
.glass-sidebar {
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.4);
}
.glass-card-ultra {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(30px);
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 2.5rem;
box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.08);
}
6. バックエンド:エージェントサービスの構築(FastAPI + LangGraph)
ステップ 1:エージェントの状態を定義する
agent/agent.py を作成:
from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
class AgentState(TypedDict):
"""ノード間で受け渡される状態"""
messages: Annotated[list, operator.add]
summary: str
qna_result: str
code_result: str
重要なポイント: Annotated[list, operator.add] は、messages が置き換えられるのではなく、ノード間で蓄積されることを意味します。
ステップ 2:エージェントノードを実装する
要約エージェント
def summarizer_node(state: AgentState) -> AgentState:
"""
要約エージェントのノード
ユーザー入力から重要な洞察を抽出する
"""
print(" 要約エージェントノードを実行中")
messages= state.get("messages", [])
ifnot messages:
return state
user_message=next((msg.contentfor msginreversed(messages)
ifisinstance(msg, HumanMessage)),"")
llm= ChatOpenAI(
model="gpt-4o-mini",
temperature=0.3,
max_tokens=150
)
返却形式: {"translated": "翻訳されたHTML"}system_msg= SystemMessage(
content="あなたはプロの要約者です。簡潔な要約を作成してください。"
)
human_msg= HumanMessage(content=f"要約:{user_message}")
response= llm.invoke([system_msg, human_msg])
summary= response.content
print(f"✅ 要約:{summary}")
return {
**state,
"summary": summary,
"messages": [AIMessage(content=f" **要約**:{summary}")]
}
Q&A エージェント
defqna_node(state: AgentState) -> AgentState:
"""
Q&A エージェントノード
要約を使って文脈に沿った回答を提供します
"""
print(" Q&A ノードを実行中")
messages= state.get("messages", [])
summary= state.get("summary",")
user_message=next((msg.contentfor msgin messages
ifisinstance(msg, HumanMessage)),")
llm= ChatOpenAI(
model="gpt-4o-mini",
temperature=0.5,
max_tokens=300
)
system_msg= SystemMessage(
content="あなたは Q&A アシスタントです。包括的な回答を提供してください。"
)
context=f"質問:{user_message}
コンテキスト:{summary}"
human_msg= HumanMessage(content=f"次をもとに回答してください:
{context}")
response= llm.invoke([system_msg, human_msg])
qna_result= response.content
return {
**state,
"qna_result": qna_result,
"messages": [AIMessage(content=f"❓ **分析**:{qna_result}")]
}
CodeGen エージェント
defcodegen_node(state: AgentState) -> AgentState:
"""
CodeGen エージェントノード
TypeScript/React のコードを生成します
"""
print(" CodeGen ノードを実行中")
返却形式: {"translated": "翻訳されたHTML"}messages= state.get("messages", [])
summary= state.get("summary",")
qna_result= state.get("qna_result",")
user_message=next((msg.contentfor msgin messages
ifisinstance(msg, HumanMessage)),")
llm= ChatOpenAI(
model="gpt-4o-mini",
temperature=0.2,
max_tokens=500
)
system_msg= SystemMessage(
content="You are an expert TypeScript/React developer."
)
context=f"Request:{user_message}
Summary:{summary}
Analysis:{qna_result}"
human_msg= HumanMessage(content=f"Generate code for:
{context}")
response= llm.invoke([system_msg, human_msg])
code_result= response.content
return {
**state,
"code_result": code_result,
"messages": [AIMessage(content=f" **Code**:
{code_result}")]
}
Step 3: Build the LangGraph Workflow
# Build the workflow
workflow= StateGraph(AgentState)
# Add nodes
workflow.add_node("summarizer", summarizer_node)
workflow.add_node("qna", qna_node)
workflow.add_node("codegen", codegen_node)
# Define the flow
workflow.set_entry_point("summarizer")
workflow.add_edge("summarizer","qna")
workflow.add_edge("qna","codegen")
workflow.add_edge("codegen", END)
# Compile
agent= workflow.compile()
print("✅ LangGraph agent compiled successfully!")
Step 4: FastAPI Server Setup
agent/server.pyを作成します:
返却形式: {"translated": "翻訳されたHTML"}from fastapi import FastAPI
from fastapi.middleware.corsimport CORSMiddleware
from langserveimport add_routes
from agentimport agent
from dotenvimport load_dotenv
load_dotenv()
app= FastAPI(
title="LangGraph エージェントサーバー",
description="構成可能なマルチエージェントシステム",
version="1.0.0"
)
# フロントエンド通信のための CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# LangServe のルートを追加
add_routes(
app,
agent,
path="/copilotkit",
enabled_endpoints=["invoke", "stream", "playground"],
)
@app.get("/")
asyncdefroot():
return {
"message":"LangGraph エージェントサーバー",
"status":"running",
"endpoints": {
"stream":"/copilotkit/stream",
"playground":"/copilotkit/playground"
}
}
if__name__="__main__":
import uvicorn
print(" LangGraph エージェントサーバーを http://localhost:8000 で起動します")
uvicorn.run("server:app",host="0.0.0.0",port=8000,reload=True)
7. アプリケーションの実行
バックエンドを開始
cd agent
source venv/bin/activate# または Windows では venv\Scripts\activate
python server.py
次が表示されるはずです:
LangGraph エージェントサーバーを http://localhost:8000 で起動します
✅ LangGraph エージェントは正常にコンパイルされました!
INFO: Uvicorn は http://0.0.0.0:8000 で実行中
フロントエンドを開始
新しいターミナルで:
npm run dev
ブラウザで http://localhost:3000 を開きます。
8. すべての部品がどのように連携するか
処理されるリクエストを順を追って説明します:
ユーザー入力
React で todo リストを作成してほしいと依頼しています。
1. 要約する
ユーザーの入力: ユーザーからの生のリクエスト
出力:
要約:ユーザーは、CRUD の todo を作成、読み取り、更新、削除するための基本的な React コンポーネントを探しています。
2. 元の依頼に基づくQA回答
ユーザー入力: 元のユーザーリクエスト + 要約
出力:
要件の分析:todoリストアプリには、状態管理の方法(useState)を用意する必要があります。新しいtodoを送信する方法(フォーム送信)が必要で、todoを削除できます(キーとしてidを使用)。また、Reactでコンポーネントを構築する際のベストプラクティスに従っています。
3. コードの生成
ユーザー入力: 元のユーザーリクエスト + 要約 + 分析
出力:
import React, { useState } from 'react';
interface Todo {
id: number;
text: string;
}
export function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, { id: Date.now(), text: input }]);
setInput('');
}
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div className="p-4">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="todoを追加..."
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => deleteTodo(todo.id)}>削除</button>
</li>
))}
</ul>
</div>
);
}
9. 重要なパターン & ベストプラクティス
1. 状態管理のための演算子
addのような演算子は、エージェントの現在の状態に対するアキュムレータとして用いるべきです。
classAgentState(TypedDict):
messages: Annotated[list, operator.add]# 追加して蓄積する
summary:str# 置き換える
2. エラーハンドリング
本番環境で try-except を使用する場合は、エージェントのロジックを try-except ブロックで包んでください。
defsummarizer_node(state: AgentState) -> AgentState:
try:
# ... エージェントのロジック
exceptExceptionas e:
print(f"❌ 要約エラー:{e}")
return {
**state,
"messages": [AIMessage(content=f"Error:{str(e)}")]
}
3. 温度設定
各エージェントタイプには、それぞれ出力の創造性を制御するための温度設定があります:
Summarizer: temperature=0.3(つまり、焦点を当てる/決定論的にする)
Q&A: temperature=0.5(つまり、バランス型)
CodeGen: temperature=0.2(つまり、決定論的で、信頼できるコード出力のために構造化されている)
4. ストリーミング対応
add_routes に enabled_endpoints=["stream"] を有効にして使用すると、LangServe がストリーミングの振る舞いを自動的に管理します。
10. システムの拡張
新しいエージェントを追加する
ノード関数を定義します:
defreviewer_node(state: AgentState) -> AgentState:
# コードをレビューして改善する
code=state.get("code_result","")
# ... レビューロジック
return {**state,"reviewed_code": improved_code}
ワークフローに追加:
workflow.add_node("reviewer", reviewer_node)
workflow.add_edge("codegen","reviewer")
workflow.add_edge("reviewer", END)
条件付きルーティング
状態に基づいてルートする:
defshould_review(state: AgentState) ->str:
code=state.get("code_result","")
return"reviewer"iflen(code)>100else END
workflow.add_conditional_edges("codegen", should_review)
並列処理
エージェントを並列で実行する:
workflow.add_edge("summarizer","qna")
workflow.add_edge("summarizer","codegen")# 両方とも並列に実行される
次は?
ここまでで、完全に動作する合成可能なマルチエージェントシステムが手に入りました。では、次にそれを使ってできることのアイデアをいくつか紹介します。
- 追加の専門エージェントを組み込む(例:画像生成、Web検索、データベースクエリ)
- ユーザーの意図に基づいて、異なるエージェントへ条件付きルーティングを実装する
- human-in-the-loop(人の承認)ステップを追加する
- 会話履歴をデータベースに保存する 返却形式: {"translated": "翻訳されたHTML"}
- 本番環境にデプロイ(例:Vercel + Railway/Render)
コンポーザブルのパターンにより、追加のエージェントを現在の数以上に増やしても、複雑さを増やすことなく、システムがより良く動作し、スケールアウトできるようになります。
つながりを保つ
もしこれを気に入っていただけたなら、GitHub で私の仕事をぜひご覧ください。また、さらなる洞察についてはTwitterをチェックしてください。さらに、TwitterでCopilotKitもフォローして、ぜひ気軽にご挨拶ください。コミュニティはとても活発で、常にワクワクする何かを作り続けています!








