ドラフト エージェント設計パターン - リフレクション
LangChain、LangGraph、TypeScriptを使用して、Vercelのサーバーレスプラットフォーム上で自己改善型AIエージェントを実装する方法を学びます。
メンタルモデル:コードレビューのアナロジー
リフレクションパターンは、プルリクエストのレビュープロセスのように考えることができます。コードを提出すると、レビュアー(批評家エージェント)がそれを調べ、フィードバックを提供し、あなた(プロデューサーエージェント)はそのフィードバックに基づいて修正を行います。このサイクルは、コードが品質基準を満たすか、マージ期限に達するまで続きます。AIエージェントでは、この同じ原理により、構造化されたフィードバックループを通じて反復的な自己改善が可能になります。コードレビューがバグを見つけ品質を向上させるように、リフレクションパターンはエージェントが自身のミスを特定し修正するのに役立ち、より正確で信頼性の高い出力をもたらします。
基本例:自己反省エージェント
1. リフレクション状態グラフの作成
// lib/agents/reflection-basic.ts
import { StateGraph, END, START, Annotation } from "@langchain/langgraph";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { take } from "es-toolkit";
const ReflectionState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
reflectionCount: Annotation<number>({
reducer: (x, y) => y ?? x,
default: () => 0,
}),
});
const model = new ChatGoogleGenerativeAI({
modelName: "gemini-2.5-flash",
temperature: 0.7,
});
const generatePrompt = ChatPromptTemplate.fromMessages([
["system", "You are an expert essay writer. Generate a response to the user's request."],
new MessagesPlaceholder("messages"),
]);
const reflectPrompt = ChatPromptTemplate.fromMessages([
["system", `You are a writing critic. Review the essay and provide specific, actionable feedback.
If the essay is excellent, respond with only "APPROVED".
Otherwise, list 2-3 specific improvements needed.`],
new MessagesPlaceholder("messages"),
]);
メッセージ履歴とリフレクション回数を持つ基本的な状態構造を作成します。状態は会話と何回のリフレクションサイクルが発生したかの両方を追跡します。
2. 生成と反省ノードの実装
// lib/agents/reflection-basic.ts (続き)
async function generateNode(state: typeof ReflectionState.State) {
const chain = generatePrompt.pipe(model);
const response = await chain.invoke({
messages: state.messages
});
return {
messages: [response],
};
}
async function reflectNode(state: typeof ReflectionState.State) {
const chain = reflectPrompt.pipe(model);
const lastMessages = take(state.messages, -2); // 最後のユーザーメッセージとAIレスポンスを取得
const critique = await chain.invoke({
messages: lastMessages
});
return {
messages: [new HumanMessage(`Feedback: ${critique.content}`)],
reflectionCount: state.reflectionCount + 1,
};
}
function shouldContinue(state: typeof ReflectionState.State) {
const lastMessage = state.messages[state.messages.length - 1];
// 承認されたか最大反射回数に達したら停止
if (lastMessage.content?.toString().includes("APPROVED") ||
state.reflectionCount >= 3) {
return END;
}
return "reflect";
}
生成ノードは初期コンテンツを作成し、反省ノードはそれを批評します。shouldContinue関数は品質承認または反復制限に基づいて停止ロジックを実装します。
3. ワークフローグラフの構築
// lib/agents/reflection-basic.ts (続き)
export function createReflectionAgent() {
const workflow = new StateGraph(ReflectionState)
.addNode("generate", generateNode)
.addNode("reflect", reflectNode)
.addEdge(START, "generate")
.addConditionalEdges("generate", shouldContinue, {
reflect: "reflect",
[END]: END,
})
.addEdge("reflect", "generate");
return workflow.compile();
}
リフレクションループフローを制御する条件付きエッジでワークフローを組み立てます。
4. APIルートの作成
// app/api/reflection/route.ts
import { createReflectionAgent } from "@/lib/agents/reflection-basic";
import { HumanMessage } from "@langchain/core/messages";
import { NextResponse } from "next/server";
export const runtime = "nodejs";
export const maxDuration = 60;
export async function POST(req: Request) {
try {
const { prompt } = await req.json();
const agent = createReflectionAgent();
const result = await agent.invoke({
messages: [new HumanMessage(prompt)],
reflectionCount: 0,
});
// 最終的に洗練された出力を抽出
const finalOutput = result.messages
.filter((m: any) => m._getType() === "ai")
.pop()?.content;
return NextResponse.json({
output: finalOutput,
iterations: result.reflectionCount,
messages: result.messages.map((m: any) => ({
type: m._getType(),
content: m.content,
})),
});
} catch (error) {
console.error("Reflection error:", error);
return NextResponse.json(
{ error: "Reflection process failed" },
{ status: 500 }
);
}
}
HTTPリクエストを処理し、適切なエラー処理でリフレクションエージェントの実行を管理します。
5. フロントエンドコンポーネントの構築
// components/ReflectionDemo.tsx
"use client";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { groupBy } from "es-toolkit";
interface ReflectionResult {
output: string;
iterations: number;
messages: Array<{ type: string; content: string }>;
}
export default function ReflectionDemo() {
const [prompt, setPrompt] = useState("");
const [showProcess, setShowProcess] = useState(false);
const reflection = useMutation({
mutationFn: async (userPrompt: string) => {
const res = await fetch("/api/reflection", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: userPrompt }),
});
if (!res.ok) throw new Error("Reflection failed");
return res.json() as Promise<ReflectionResult>;
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (prompt.trim()) {
reflection.mutate(prompt);
}
};
// 表示のためにメッセージを反復ごとにグループ化
const messagesByIteration = reflection.data?.messages
? groupBy(reflection.data.messages, (_, index) =>
Math.floor(index / 2).toString()
)
: {};
return (
<div className="card w-full bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">リフレクションエージェントデモ</h2>
<form onSubmit={handleSubmit}>
<textarea
className="textarea textarea-bordered w-full"
placeholder="ライティングプロンプトを入力..."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
rows={3}
disabled={reflection.isPending}
/>
<div className="card-actions justify-between mt-4">
<label className="label cursor-pointer">
<span className="label-text mr-2">プロセス表示</span>
<input
type="checkbox"
className="checkbox"
checked={showProcess}
onChange={(e) => setShowProcess(e.target.checked)}
/>
</label>
<button
type="submit"
className="btn btn-primary"
disabled={reflection.isPending || !prompt.trim()}
>
{reflection.isPending ? (
<>
<span className="loading loading-spinner"></span>
反省中...
</>
) : "生成"}
</button>
</div>
</form>
{reflection.data && (
<div className="mt-6 space-y-4">
<div className="stats shadow">
<div className="stat">
<div className="stat-title">リフレクション反復回数</div>
<div className="stat-value">{reflection.data.iterations}</div>
</div>
</div>
{showProcess && (
<div className="space-y-4">
{Object.entries(messagesByIteration).map(([iter, msgs]) => (
<div key={iter} className="collapse collapse-arrow bg-base-200">
<input type="checkbox" />
<div className="collapse-title font-medium">
反復 {parseInt(iter) + 1}
</div>
<div className="collapse-content">
{msgs.map((msg, idx) => (
<div key={idx} className={`chat chat-${msg.type === "ai" ? "end" : "start"}`}>
<div className={`chat-bubble ${msg.type === "human" ? "chat-bubble-primary" : ""}`}>
{msg.content}
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
<div className="divider">最終出力</div>
<div className="prose max-w-none">
{reflection.data.output}
</div>
</div>
)}
</div>
</div>
);
}
折りたたみ可能な反復ビューでリフレクションプロセスをデモンストレーションするインタラクティブUIを提供します。
高度な例:ストリーミング付きプロデューサー批評家アーキテクチャ
1. プロデューサーと批評家エージェントの定義
// lib/agents/producer-critic.ts
import { StateGraph, END, START, Annotation } from "@langchain/langgraph";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { z } from "zod";
import { minBy, maxBy } from "es-toolkit";
import { StructuredOutputParser } from "@langchain/core/output_parsers";
const CritiqueSchema = z.object({
score: z.number().min(0).max(100),
approved: z.boolean(),
issues: z.array(z.object({
category: z.enum(["accuracy", "clarity", "completeness", "style"]),
description: z.string(),
severity: z.enum(["minor", "major", "critical"]),
})),
suggestions: z.array(z.string()),
});
const ProducerCriticState = Annotation.Root({
task: Annotation<string>(),
drafts: Annotation<string[]>({
reducer: (x, y) => x.concat(y),
default: () => [],
}),
critiques: Annotation<typeof CritiqueSchema._type[]>({
reducer: (x, y) => x.concat(y),
default: () => [],
}),
iteration: Annotation<number>({
reducer: (_, y) => y,
default: () => 0,
}),
});
const producer = new ChatGoogleGenerativeAI({
modelName: "gemini-2.5-pro",
temperature: 0.7,
maxOutputTokens: 2048,
});
const critic = new ChatGoogleGenerativeAI({
modelName: "gemini-2.5-flash",
temperature: 0.3,
});
構造化された批評出力スキーマを使用して、プロデューサーと批評家の役割のために別々のモデルを定義します。
2. コンテキスト付きプロデューサーノードの実装
// lib/agents/producer-critic.ts (続き)
async function producerNode(state: typeof ProducerCriticState.State) {
const lastCritique = state.critiques[state.critiques.length - 1];
let prompt = `タスク: ${state.task}`;
if (lastCritique) {
const criticalIssues = lastCritique.issues
.filter(i => i.severity === "critical")
.map(i => `- ${i.description}`)
.join("\n");
prompt += `\n\n前のドラフトがフィードバックを受けました。対処すべき重要な問題:\n${criticalIssues}`;
prompt += `\n\n改善のための提案:\n${lastCritique.suggestions.join("\n")}`;
prompt += `\n\nすべてのフィードバックに対処した改良版を生成してください。`;
} else {
prompt += "\n\n高品質な応答を生成してください。";
}
const response = await producer.invoke(prompt);
return {
drafts: [response.content as string],
iteration: state.iteration + 1,
};
}
プロデューサーノードは、以前の批評フィードバックを組み込んで改善されたドラフトを生成します。
3. 構造化出力付き批評家ノードの実装
// lib/agents/producer-critic.ts (続き)
async function criticNode(state: typeof ProducerCriticState.State) {
const latestDraft = state.drafts[state.drafts.length - 1];
const parser = StructuredOutputParser.fromZodSchema(CritiqueSchema);
const prompt = `あなたは専門の批評家です。タスク「${state.task}」に対するこの応答を評価してください
評価する応答:
${latestDraft}
このJSONスキーマに従って詳細な批評を提供してください:
${parser.getFormatInstructions()}
スコア90以上は応答が優れていて承認されたことを意味します。
フィードバックは徹底的でありながら建設的であるべきです。`;
const response = await critic.invoke(prompt);
const critique = await parser.parse(response.content as string);
return {
critiques: [critique],
};
}
批評家はスコア、問題の分類、改善提案を含む構造化されたフィードバックを提供します。
4. 高度なルーティングロジック
// lib/agents/producer-critic.ts (続き)
function routingLogic(state: typeof ProducerCriticState.State) {
const lastCritique = state.critiques[state.critiques.length - 1];
// 早期終了条件
if (!lastCritique) return "critic";
if (lastCritique.approved || state.iteration >= 5) {
return END;
}
// 批評の深刻度に基づく適応的ルーティング
const criticalCount = lastCritique.issues.filter(i => i.severity === "critical").length;
if (criticalCount > 2 && state.iteration < 3) {
// 大幅な書き直しが必要
return "producer";
} else if (lastCritique.score > 75) {
// 小さな改善のみ
return "producer";
} else {
// 標準的な反復
return "producer";
}
}
export function createProducerCriticAgent() {
const workflow = new StateGraph(ProducerCriticState)
.addNode("producer", producerNode)
.addNode("critic", criticNode)
.addEdge(START, "producer")
.addEdge("producer", "critic")
.addConditionalEdges("critic", routingLogic, {
producer: "producer",
[END]: END,
});
return workflow.compile();
}
批評の深刻度と反復回数に基づいて洗練されたルーティングを実装します。
5. サーバー送信イベントによるストリーミングAPI
// app/api/producer-critic/route.ts
import { createProducerCriticAgent } from "@/lib/agents/producer-critic";
import { debounce } from "es-toolkit";
export const runtime = "nodejs";
export const maxDuration = 300;
export async function POST(req: Request) {
const { task } = await req.json();
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
// クライアントを圧倒しないようにデバウンスされた書き込み
const debouncedWrite = debounce(async (data: any) => {
await writer.write(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
);
}, 100);
const agent = createProducerCriticAgent();
(async () => {
try {
const eventStream = await agent.streamEvents(
{ task, drafts: [], critiques: [], iteration: 0 },
{ version: "v2" }
);
for await (const event of eventStream) {
if (event.event === "on_chain_end" && event.name === "producer") {
await debouncedWrite({
type: "draft",
iteration: event.data.output.iteration,
content: event.data.output.drafts[event.data.output.drafts.length - 1],
});
}
if (event.event === "on_chain_end" && event.name === "critic") {
const critique = event.data.output.critiques[event.data.output.critiques.length - 1];
await debouncedWrite({
type: "critique",
iteration: event.data.output.iteration,
score: critique.score,
approved: critique.approved,
issues: critique.issues,
});
}
}
await writer.write(encoder.encode(`data: ${JSON.stringify({ type: "complete" })}\n\n`));
} catch (error) {
await writer.write(
encoder.encode(`data: ${JSON.stringify({ type: "error", error: String(error) })}\n\n`)
);
} finally {
await writer.close();
}
})();
return new Response(stream.readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
}
プログレッシブUI更新のためにサーバー送信イベントを使用してリフレクションイベントをリアルタイムでストリーミングします。
6. リアルタイム可視化を備えた高度なフロントエンド
// components/ProducerCriticDemo.tsx
"use client";
import { useState, useCallback } from "react";
import { useMutation } from "@tanstack/react-query";
import { partition, groupBy } from "es-toolkit";
interface StreamEvent {
type: "draft" | "critique" | "complete" | "error";
iteration?: number;
content?: string;
score?: number;
approved?: boolean;
issues?: Array<{
category: string;
description: string;
severity: string;
}>;
error?: string;
}
export default function ProducerCriticDemo() {
const [task, setTask] = useState("");
const [events, setEvents] = useState<StreamEvent[]>([]);
const [isStreaming, setIsStreaming] = useState(false);
const startReflection = useCallback(async () => {
setEvents([]);
setIsStreaming(true);
try {
const response = await fetch("/api/producer-critic", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ task }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
try {
const event = JSON.parse(line.slice(6));
setEvents(prev => [...prev, event]);
if (event.type === "complete" || event.type === "error") {
setIsStreaming(false);
}
} catch {}
}
}
}
} catch (error) {
console.error("Stream error:", error);
setIsStreaming(false);
}
}, [task]);
// イテレーションごとにイベントをグループ化
const [drafts, critiques] = partition(
events.filter(e => e.type === "draft" || e.type === "critique"),
e => e.type === "draft"
);
const iterations = groupBy(
[...drafts, ...critiques],
e => e.iteration?.toString() || "0"
);
const finalDraft = drafts[drafts.length - 1];
const finalCritique = critiques[critiques.length - 1];
return (
<div className="container mx-auto p-4">
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">プロデューサー批評家リフレクションシステム</h2>
<div className="form-control">
<textarea
className="textarea textarea-bordered"
placeholder="タスクを説明してください..."
value={task}
onChange={(e) => setTask(e.target.value)}
rows={3}
disabled={isStreaming}
/>
</div>
<div className="card-actions justify-end mt-4">
<button
className="btn btn-primary"
onClick={startReflection}
disabled={isStreaming || !task.trim()}
>
{isStreaming ? (
<>
<span className="loading loading-spinner"></span>
処理中...
</>
) : "リフレクション開始"}
</button>
</div>
</div>
</div>
{events.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-6">
{/* 反復タイムライン */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title text-lg">リフレクションプロセス</h3>
<ul className="timeline timeline-vertical">
{Object.entries(iterations).map(([iter, iterEvents]) => {
const draft = iterEvents.find(e => e.type === "draft");
const critique = iterEvents.find(e => e.type === "critique");
return (
<li key={iter}>
<div className="timeline-middle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="h-5 w-5"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="timeline-end timeline-box">
<div className="text-lg font-black">反復 {iter}</div>
{critique && (
<div className="stats stats-horizontal shadow mt-2">
<div className="stat">
<div className="stat-title">スコア</div>
<div className="stat-value text-2xl">{critique.score}</div>
</div>
<div className="stat">
<div className="stat-title">ステータス</div>
<div className={`stat-value text-2xl ${critique.approved ? "text-success" : "text-warning"}`}>
{critique.approved ? "✓" : "↻"}
</div>
</div>
</div>
)}
{critique?.issues && (
<div className="mt-2">
<p className="font-semibold">発見された問題:</p>
{critique.issues.map((issue, idx) => (
<div key={idx} className={`badge badge-${issue.severity === "critical" ? "error" : "warning"} gap-2 mr-1`}>
{issue.category}
</div>
))}
</div>
)}
</div>
<hr />
</li>
);
})}
</ul>
</div>
</div>
{/* 最終出力 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title text-lg">最終出力</h3>
{finalCritique?.approved && (
<div className="alert alert-success">
<span>出力が承認されました。スコア: {finalCritique.score}/100</span>
</div>
)}
{finalDraft && (
<div className="prose max-w-none">
<div className="mockup-code">
<pre><code>{finalDraft.content}</code></pre>
</div>
</div>
)}
{isStreaming && (
<div className="flex justify-center">
<span className="loading loading-dots loading-lg"></span>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}
タイムライン可視化とリアルタイムストリーミング更新を備えた洗練されたUIを作成します。
7. キャッシングによるパフォーマンス最適化
// lib/cache/reflection-cache.ts
import { kv } from "@vercel/kv";
import { hash } from "es-toolkit/compat";
interface CacheEntry {
task: string;
output: string;
score: number;
timestamp: number;
}
export class ReflectionCache {
private readonly ttl = 3600; // 1時間
async get(task: string): Promise<CacheEntry | null> {
const key = `reflection:${hash(task)}`;
const cached = await kv.get<CacheEntry>(key);
if (cached && Date.now() - cached.timestamp < this.ttl * 1000) {
return cached;
}
return null;
}
async set(task: string, output: string, score: number): Promise<void> {
const key = `reflection:${hash(task)}`;
const entry: CacheEntry = {
task,
output,
score,
timestamp: Date.now(),
};
await kv.set(key, entry, { ex: this.ttl });
}
async getSimilar(task: string, threshold = 0.8): Promise<CacheEntry[]> {
// セマンティック類似性検索を実装
const allKeys = await kv.keys("reflection:*");
const similar: CacheEntry[] = [];
for (const key of allKeys) {
const entry = await kv.get<CacheEntry>(key);
if (entry) {
// シンプルな類似性チェック(適切なセマンティック類似性を実装)
const similarity = this.calculateSimilarity(task, entry.task);
if (similarity > threshold) {
similar.push(entry);
}
}
}
return similar;
}
private calculateSimilarity(a: string, b: string): number {
// 簡略化された類似性計算
const wordsA = new Set(a.toLowerCase().split(" "));
const wordsB = new Set(b.toLowerCase().split(" "));
const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
const union = new Set([...wordsA, ...wordsB]);
return intersection.size / union.size;
}
}
類似タスクの冗長なリフレクションサイクルを削減するキャッシングを実装します。
8. コスト追跡と最適化
// lib/monitoring/cost-tracker.ts
interface ReflectionMetrics {
totalTokens: number;
inputTokens: number;
outputTokens: number;
iterations: number;
duration: number;
estimatedCost: number;
}
export class CostTracker {
private metrics: ReflectionMetrics = {
totalTokens: 0,
inputTokens: 0,
outputTokens: 0,
iterations: 0,
duration: 0,
estimatedCost: 0,
};
private readonly costPerToken = {
"gemini-2.5-pro": { input: 0.00125, output: 0.005 },
"gemini-2.5-flash": { input: 0.00015, output: 0.0006 },
};
trackIteration(model: string, inputTokens: number, outputTokens: number): void {
this.metrics.inputTokens += inputTokens;
this.metrics.outputTokens += outputTokens;
this.metrics.totalTokens += inputTokens + outputTokens;
this.metrics.iterations += 1;
const modelCost = this.costPerToken[model as keyof typeof this.costPerToken];
if (modelCost) {
this.metrics.estimatedCost +=
(inputTokens * modelCost.input + outputTokens * modelCost.output) / 1000;
}
}
shouldContinue(maxCost: number = 0.10): boolean {
return this.metrics.estimatedCost < maxCost;
}
getMetrics(): ReflectionMetrics {
return { ...this.metrics };
}
}
予算を意識したリフレクションサイクルを実装するためにトークン使用量とコストを追跡します。
まとめ
リフレクションパターンは、AIエージェントを単一応答システムから自己改善が可能な反復学習者に変換します。適切な状態管理、ストリーミング機能、コスト最適化を備えたプロデューサー批評家アーキテクチャを実装することで、Vercelのサーバーレスプラットフォーム上で洗練されたリフレクションシステムを展開できます。鍵は、インテリジェントなキャッシング、早期停止、適応的ルーティング戦略を通じて、品質改善と計算コストのバランスを取ることです。単純なタスクには基本的な自己反省から始め、より高品質な出力を必要とする複雑なシナリオにはマルチエージェントプロデューサー批評家システムにスケールアップします。