ドラフト エージェント設計パターン - プランニング

aiエージェントプランニングlangchainlanggraphtypescript
By sko X opus 4.19/20/202517 min read

TypeScript、LangChain、LangGraph、Google Geminiモデルを使用して、Vercelのサーバーレスプラットフォーム上で、複雑なタスクを分解し、多段階戦略を作成し、洗練されたワークフローを実行するAIエージェントのプランニングパターンの実装方法を学びます。

メンタルモデル:建設現場の現場監督

プランニングエージェントを、建設プロジェクトを監督する現場監督のように考えてみてください。現場監督は個人的にレンガを積んだり配管を設置したりはしません。代わりに包括的な計画を作成し、専門作業員にタスクを委任し、進捗を監視し、問題が発生したときに戦略を調整します。同様に、プランニングエージェントは複雑な問題を管理可能なステップに分解し、専門ツールやサブエージェントを通じて実行をオーケストレートし、中間結果に基づいて計画を適応させます。この計画と実行の分離により、単一パスアプローチでは対応できない複雑さを扱えるようになります。

基本例:プランアンドエグゼキュートエージェント

1. エージェント状態タイプの定義

// lib/planning/types.ts
import { z } from 'zod';
import { Annotation } from '@langchain/langgraph';
import { BaseMessage } from '@langchain/core/messages';

// 単一プランステップのスキーマ
export const PlanStepSchema = z.object({
  step: z.number(),
  action: z.string(),
  reasoning: z.string(),
  dependencies: z.array(z.number()).default([]),
  status: z.enum(['pending', 'in_progress', 'completed', 'failed']).default('pending'),
  result: z.string().optional(),
});

export type PlanStep = z.infer<typeof PlanStepSchema>;

// LangGraphアノテーションを使用した状態定義
export const PlanningAgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
  plan: Annotation<PlanStep[]>({
    reducer: (current, update) => update,
    default: () => [],
  }),
  currentStep: Annotation<number>({
    default: () => 0
  }),
  executionResults: Annotation<Record<number, any>>({
    reducer: (current, update) => ({ ...current, ...update }),
    default: () => ({}),
  }),
  finalOutput: Annotation<string>({
    default: () => ''
  }),
});

export type AgentState = typeof PlanningAgentState.State;

状態管理にLangGraphのAnnotationシステムを使用して、プランステップ、実行追跡、メッセージ履歴を含む強く型付けされた状態構造を定義します。

2. プランニングノードの作成

// lib/planning/nodes/planner.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
import { map, filter, sortBy } from 'es-toolkit';
import { AgentState, PlanStep } from '../types';

const plannerModel = new ChatGoogleGenerativeAI({
  model: 'gemini-2.5-pro',
  temperature: 0.1,
  maxOutputTokens: 8192,
}).withStructuredOutput({
  name: 'plan',
  description: 'タスク実行のための構造化プラン',
  parameters: {
    type: 'object',
    properties: {
      steps: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            step: { type: 'number' },
            action: { type: 'string' },
            reasoning: { type: 'string' },
            dependencies: {
              type: 'array',
              items: { type: 'number' }
            },
          },
          required: ['step', 'action', 'reasoning'],
        },
      },
    },
    required: ['steps'],
  },
});

export async function plannerNode(state: AgentState): Promise<Partial<AgentState>> {
  const userMessage = state.messages[state.messages.length - 1];

  const systemPrompt = `あなたはプランニングエージェントです。ユーザーのリクエストを明確で実行可能なステップに分解してください。
  各ステップは:
  1. 実行する明確なアクションを持つ
  2. なぜそのステップが必要かの理由を含む
  3. 前のステップへの依存関係をリストアップする(ステップ番号を使用)

  依存関係を尊重して、ステップが論理的に順序付けられるようにしてください。`;

  const response = await plannerModel.invoke([
    new SystemMessage(systemPrompt),
    userMessage,
  ]);

  // es-toolkitを使用してステップを処理および検証
  const processedSteps = map(
    response.steps,
    (step: any, index: number) => ({
      ...step,
      step: index + 1,
      status: 'pending' as const,
      dependencies: step.dependencies || [],
    })
  );

  // 正しい実行順序を確保するために依存関係でソート
  const sortedSteps = sortBy(
    processedSteps,
    [(step: PlanStep) => step.dependencies.length, 'step']
  );

  return {
    plan: sortedSteps,
    currentStep: 0,
  };
}

ユーザーリクエストを分析し、高品質な推論のためにGemini 2.5 Proを使用して構造化された依存関係対応の実行プランを生成するプランニングノードを作成します。

3. 実行ノードの作成

// lib/planning/nodes/executor.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { SystemMessage, HumanMessage } from '@langchain/core/messages';
import { filter, find, every, map as mapArray } from 'es-toolkit';
import { AgentState, PlanStep } from '../types';

const executorModel = new ChatGoogleGenerativeAI({
  model: 'gemini-2.5-flash',  // 実行用の高速で安価なモデル
  temperature: 0,
  maxOutputTokens: 2048,
});

export async function executorNode(state: AgentState): Promise<Partial<AgentState>> {
  const { plan, currentStep, executionResults } = state;

  // 次の実行可能ステップを見つける
  const executableStep = find(plan, (step: PlanStep) => {
    // ステップは保留中で、すべての依存関係が完了している必要がある
    if (step.status !== 'pending') return false;

    const dependenciesMet = every(
      step.dependencies,
      (depId: number) => {
        const depStep = find(plan, (s: PlanStep) => s.step === depId);
        return depStep?.status === 'completed';
      }
    );

    return dependenciesMet;
  });

  if (!executableStep) {
    // 実行するステップがもうない
    return {
      finalOutput: generateSummary(plan, executionResults),
    };
  }

  // ステップを実行
  const contextFromDeps = executableStep.dependencies
    .map(depId => {
      const result = executionResults[depId];
      return result ? `ステップ ${depId} の結果: ${result}` : '';
    })
    .filter(Boolean)
    .join('\n');

  const executionPrompt = `以下のアクションを実行してください:
  アクション: ${executableStep.action}
  理由: ${executableStep.reasoning}
  ${contextFromDeps ? `\n前のステップからのコンテキスト:\n${contextFromDeps}` : ''}

  アクション実行の簡潔な結果を提供してください。`;

  const result = await executorModel.invoke([
    new SystemMessage('あなたは実行エージェントです。要求されたアクションを実行し、結果を返してください。'),
    new HumanMessage(executionPrompt),
  ]);

  // es-toolkitを使用してプランと結果を更新
  const updatedPlan = mapArray(plan, (step: PlanStep) =>
    step.step === executableStep.step
      ? { ...step, status: 'completed' as const, result: result.content as string }
      : step
  );

  return {
    plan: updatedPlan,
    executionResults: {
      [executableStep.step]: result.content,
    },
    currentStep: currentStep + 1,
  };
}

function generateSummary(plan: PlanStep[], results: Record<number, any>): string {
  const completedSteps = filter(plan, (s: PlanStep) => s.status === 'completed');
  return `${completedSteps.length}ステップを正常に完了しました。
最終結果: ${JSON.stringify(results, null, 2)}`;
}

コスト効率のためにGemini 2.5 Flashを使用して個別プランステップを実行し、依存関係を管理し、後続ステップのための結果を蓄積します。

4. プランニンググラフの構築

// lib/planning/graph.ts
import { StateGraph, END, MemorySaver } from '@langchain/langgraph';
import { AgentState } from './types';
import { plannerNode } from './nodes/planner';
import { executorNode } from './nodes/executor';
import { every } from 'es-toolkit';

export function createPlanningGraph() {
  const workflow = new StateGraph<AgentState>({
    stateSchema: AgentState,
  });

  // ノードを追加
  workflow.addNode('planner', plannerNode);
  workflow.addNode('executor', executorNode);

  // 条件付きエッジを定義
  workflow.addConditionalEdges(
    'executor',
    (state: AgentState) => {
      // es-toolkitを使用してすべてのステップが完了したかチェック
      const allCompleted = every(
        state.plan,
        step => step.status === 'completed' || step.status === 'failed'
      );

      return allCompleted ? 'end' : 'continue';
    },
    {
      end: END,
      continue: 'executor',
    }
  );

  // フローを設定
  workflow.setEntryPoint('planner');
  workflow.addEdge('planner', 'executor');

  return workflow.compile({
    checkpointer: new MemorySaver(), // サーバーレス用。本番では外部ストレージを使用
  });
}

すべてのタスクが完了するまで反復的なステップ実行を可能にする条件付き実行ロジックを持つプランニングワークフローを組み立てます。

5. プランニングエージェント用APIルート

// app/api/planning/route.ts
import { createPlanningGraph } from '@/lib/planning/graph';
import { HumanMessage } from '@langchain/core/messages';
import { NextResponse } from 'next/server';
import { v4 as uuidv4 } from 'uuid';

export const runtime = 'nodejs';
export const maxDuration = 300;

export async function POST(req: Request) {
  try {
    const { message, sessionId = uuidv4() } = await req.json();

    const graph = createPlanningGraph();

    // リアルタイム更新のためのイベントストリーム
    const encoder = new TextEncoder();
    const stream = new TransformStream();
    const writer = stream.writable.getWriter();

    (async () => {
      try {
        const events = graph.stream(
          {
            messages: [new HumanMessage(message)],
          },
          {
            configurable: { thread_id: sessionId },
            streamMode: 'values',
          }
        );

        for await (const event of events) {
          // フロントエンドにプラン更新を送信
          if (event.plan) {
            await writer.write(
              encoder.encode(`data: ${JSON.stringify({
                type: 'plan_update',
                plan: event.plan,
                currentStep: event.currentStep,
              })}\n\n`)
            );
          }

          // 最終出力を送信
          if (event.finalOutput) {
            await writer.write(
              encoder.encode(`data: ${JSON.stringify({
                type: 'complete',
                output: event.finalOutput,
              })}\n\n`)
            );
          }
        }
      } catch (error) {
        console.error('ストリーミングエラー:', error);
        await writer.write(
          encoder.encode(`data: ${JSON.stringify({
            type: 'error',
            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',
      },
    });
  } catch (error) {
    console.error('プランニングAPIエラー:', error);
    return NextResponse.json(
      { error: 'リクエストの処理に失敗しました' },
      { status: 500 }
    );
  }
}

エージェントがプラン実行を進めるにつれてリアルタイム更新を提供するストリーミングAPIエンドポイントを通じてプランニングエージェントを公開します。

6. React Queryとのフロントエンドコンポーネント

// components/PlanningInterface.tsx
'use client';

import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { map } from 'es-toolkit';

interface PlanStep {
  step: number;
  action: string;
  reasoning: string;
  status: 'pending' | 'in_progress' | 'completed' | 'failed';
  result?: string;
}

export default function PlanningInterface() {
  const [input, setInput] = useState('');
  const [plan, setPlan] = useState<PlanStep[]>([]);
  const [output, setOutput] = useState('');

  const executePlan = useMutation({
    mutationFn: async (message: string) => {
      const response = await fetch('/api/planning', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message }),
      });

      if (!response.ok) throw new Error('プランの実行に失敗しました');
      if (!response.body) throw new Error('レスポンスボディがありません');

      const reader = response.body.getReader();
      const decoder = new TextDecoder();

      while (true) {
        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 data = JSON.parse(line.slice(6));

              if (data.type === 'plan_update') {
                setPlan(data.plan);
              } else if (data.type === 'complete') {
                setOutput(data.output);
              }
            } catch (e) {
              console.error('パースエラー:', e);
            }
          }
        }
      }
    },
  });

  const getStatusBadge = (status: string) => {
    const badges = {
      pending: 'badge-ghost',
      in_progress: 'badge-info',
      completed: 'badge-success',
      failed: 'badge-error',
    };
    return badges[status as keyof typeof badges] || 'badge-ghost';
  };

  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">プランニングエージェント</h2>

        <form onSubmit={(e) => {
          e.preventDefault();
          executePlan.mutate(input);
        }}>
          <div className="form-control">
            <textarea
              className="textarea textarea-bordered h-24"
              placeholder="複雑なタスクを説明してください..."
              value={input}
              onChange={(e) => setInput(e.target.value)}
              disabled={executePlan.isPending}
            />
          </div>

          <div className="card-actions justify-end mt-4">
            <button
              type="submit"
              className="btn btn-primary"
              disabled={!input || executePlan.isPending}
            >
              {executePlan.isPending ? (
                <>
                  <span className="loading loading-spinner"></span>
                  プランニング・実行中...
                </>
              ) : 'プランを作成・実行'}
            </button>
          </div>
        </form>

        {plan.length > 0 && (
          <div className="mt-6">
            <h3 className="font-bold mb-2">実行プラン:</h3>
            <ul className="steps steps-vertical">
              {map(plan, (step) => (
                <li
                  key={step.step}
                  className={`step ${step.status === 'completed' ? 'step-primary' : ''}`}
                >
                  <div className="text-left">
                    <div className="flex items-center gap-2">
                      <span className="font-semibold">{step.action}</span>
                      <span className={`badge badge-sm ${getStatusBadge(step.status)}`}>
                        {step.status}
                      </span>
                    </div>
                    <p className="text-sm opacity-70">{step.reasoning}</p>
                    {step.result && (
                      <div className="mt-2 p-2 bg-base-200 rounded text-sm">
                        {step.result}
                      </div>
                    )}
                  </div>
                </li>
              ))}
            </ul>
          </div>
        )}

        {output && (
          <div className="alert alert-success mt-4">
            <span>{output}</span>
          </div>
        )}

        {executePlan.isError && (
          <div className="alert alert-error mt-4">
            <span>プランの実行に失敗しました。もう一度お試しください。</span>
          </div>
        )}
      </div>
    </div>
  );
}

DaisyUIコンポーネントを使用して、プラン作成と実行をリアルタイムで視覚化し、ステップの進捗と結果を表示するReactコンポーネント。

高度な例:思考の木を使ったReActエージェント

1. すでにインストール済みの追加依存関係

// プロジェクトセットアップからすべての依存関係がすでに利用可能:
// @langchain/google-genai - Google AIモデル
// @langchain/langgraph - ステートフルワークフロー
// es-toolkit, es-toolkit/compat - 関数型ユーティリティ
// zod - スキーマ検証
// @tanstack/react-query - データフェッチング

追加インストールは不要 - プロジェクトセットアップで必要なパッケージはすべて事前インストール済みです。

// lib/advanced-planning/types.ts
import { z } from 'zod';
import { Annotation } from '@langchain/langgraph';
import { BaseMessage } from '@langchain/core/messages';

// 木構造の思考ノード
export const ThoughtNodeSchema = z.object({
  id: z.string(),
  content: z.string(),
  score: z.number(),
  depth: z.number(),
  parentId: z.string().nullable(),
  children: z.array(z.string()).default([]),
  isTerminal: z.boolean().default(false),
  metadata: z.record(z.any()).optional(),
});

export type ThoughtNode = z.infer<typeof ThoughtNodeSchema>;

// 木探索による拡張状態
export const TreeAgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
  thoughtTree: Annotation<Map<string, ThoughtNode>>({
    reducer: (current, update) => new Map([...current, ...update]),
    default: () => new Map(),
  }),
  currentNodeId: Annotation<string | null>({
    default: () => null
  }),
  bestPath: Annotation<string[]>({
    reducer: (current, update) => update,
    default: () => [],
  }),
  explorationBudget: Annotation<number>({
    default: () => 10
  }),
  iterationCount: Annotation<number>({
    reducer: (current, update) => current + update,
    default: () => 0,
  }),
});

export type TreeState = typeof TreeAgentState.State;

洗練された推論のためのスコアリングと深度追跡により、分岐思考プロセスをサポートする木ベースの探索状態を定義します。

2. 木探索を使ったReActパターンの実装

// lib/advanced-planning/nodes/react-tree.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { SystemMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
import { maxBy, filter, map, sortBy, take } from 'es-toolkit';
import { v4 as uuidv4 } from 'uuid';
import { TreeState, ThoughtNode } from '../types';

const reasoningModel = new ChatGoogleGenerativeAI({
  model: 'gemini-2.5-pro',
  temperature: 0.7,
  maxOutputTokens: 8192,
}).withStructuredOutput({
  name: 'thought_expansion',
  description: '複数の推論パスを生成',
  parameters: {
    type: 'object',
    properties: {
      thoughts: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            reasoning: { type: 'string' },
            action: { type: 'string' },
            confidence: { type: 'number' },
          },
        },
      },
    },
  },
});

export async function reactTreeNode(state: TreeState): Promise<Partial<TreeState>> {
  const { thoughtTree, currentNodeId, explorationBudget, iterationCount } = state;

  // 探索予算をチェック
  if (iterationCount >= explorationBudget) {
    return selectBestPath(state);
  }

  // 現在のコンテキストを取得
  const currentNode = currentNodeId ? thoughtTree.get(currentNodeId) : null;
  const context = buildContext(state, currentNode);

  // 複数の思考分岐を生成
  const thoughtPrompt = `現在のコンテキストを考慮して、3つの異なる推論パスを生成してください。
  各パスは以下を含む必要があります:
  1. 独自の推論アプローチを提供
  2. 具体的なアクションを提案
  3. アプローチへの信頼度(0-1)を推定

  コンテキスト: ${context}

  現在の思考: ${currentNode?.content || '初期状態'}`;

  const response = await reasoningModel.invoke([
    new SystemMessage('あなたは複数のソリューションパスを探索する推論エージェントです。'),
    new HumanMessage(thoughtPrompt),
  ]);

  // 新しい思考ノードを作成
  const newNodes = new Map<string, ThoughtNode>();
  const parentDepth = currentNode?.depth || 0;

  for (const thought of response.thoughts) {
    const nodeId = uuidv4();
    const node: ThoughtNode = {
      id: nodeId,
      content: `${thought.reasoning} → ${thought.action}`,
      score: thought.confidence * (1 / (parentDepth + 1)), // 深度とともにスコアを減衰
      depth: parentDepth + 1,
      parentId: currentNodeId,
      children: [],
      isTerminal: false,
      metadata: { action: thought.action },
    };

    newNodes.set(nodeId, node);

    // 親の子供を更新
    if (currentNode) {
      currentNode.children.push(nodeId);
      thoughtTree.set(currentNodeId!, {
        ...currentNode,
        children: currentNode.children,
      });
    }
  }

  // es-toolkitを使用して次に探索するノードを選択(最高スコア)
  const nextNode = maxBy(
    Array.from(newNodes.values()),
    (node: ThoughtNode) => node.score
  );

  return {
    thoughtTree: new Map([...thoughtTree, ...newNodes]),
    currentNodeId: nextNode?.id || null,
    iterationCount: 1,
  };
}

function buildContext(state: TreeState, currentNode: ThoughtNode | null): string {
  if (!currentNode) {
    return state.messages[state.messages.length - 1]?.content as string || '';
  }

  // es-toolkitを使用してルートから現在までのパスを構築
  const path: ThoughtNode[] = [];
  let node: ThoughtNode | null = currentNode;

  while (node) {
    path.unshift(node);
    node = node.parentId ? state.thoughtTree.get(node.parentId) || null : null;
  }

  return map(path, (n: ThoughtNode) => n.content).join(' → ');
}

function selectBestPath(state: TreeState): Partial<TreeState> {
  const { thoughtTree } = state;

  // es-toolkitを使用して終端ノードまたは最深ノードを見つける
  const allNodes = Array.from(thoughtTree.values());
  const terminalNodes = filter(allNodes, (n: ThoughtNode) =>
    n.isTerminal || n.children.length === 0
  );

  // 最良の終端ノードを選択
  const bestNode = maxBy(terminalNodes, (n: ThoughtNode) => n.score);

  if (!bestNode) {
    return { bestPath: [] };
  }

  // パスを再構築
  const path: string[] = [];
  let current: ThoughtNode | null = bestNode;

  while (current) {
    path.unshift(current.id);
    current = current.parentId ? thoughtTree.get(current.parentId) || null : null;
  }

  return { bestPath: path };
}

複数の推論パスの体系的探索を可能にするGemini 2.5 Proを使用して、ReActパターンと思考の木を組み合わせた木ベース探索を実装します。

3. 反省機能付きアクション実行

// lib/advanced-planning/nodes/reflective-executor.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { find } from 'es-toolkit';
import { TreeState, ThoughtNode } from '../types';

// スキーマ付きツールを定義
const searchTool = new DynamicStructuredTool({
  name: 'search',
  description: '情報を検索',
  schema: z.object({
    query: z.string(),
  }),
  func: async ({ query }) => {
    // 検索をシミュレート
    return `"${query}"の検索結果: [関連情報]`;
  },
});

const calculateTool = new DynamicStructuredTool({
  name: 'calculate',
  description: '計算を実行',
  schema: z.object({
    expression: z.string(),
  }),
  func: async ({ expression }) => {
    // 実際の計算にはmath.jsなどを使用
    try {
      // デモ用の安全な評価
      const result = Function('"use strict"; return (' + expression + ')')();
      return `計算結果: ${result}`;
    } catch (e) {
      return `計算エラー: ${e}`;
    }
  },
});

const tools = [searchTool, calculateTool];

export async function reflectiveExecutor(state: TreeState): Promise<Partial<TreeState>> {
  const { thoughtTree, currentNodeId } = state;

  if (!currentNodeId) return {};

  const currentNode = thoughtTree.get(currentNodeId);
  if (!currentNode?.metadata?.action) return {};

  const action = currentNode.metadata.action as string;

  // アクションを実行
  let result: string;
  try {
    // アクションを解析して適切なツールを実行
    const toolMatch = action.match(/(\w+)\((.*)\)/);
    if (toolMatch) {
      const [, toolName, args] = toolMatch;
      const tool = find(tools, (t) => t.name === toolName);

      if (tool) {
        // 引数を安全に解析
        const parsedArgs = args ? { [toolName === 'search' ? 'query' : 'expression']: args.replace(/['"]/g, '') } : {};
        result = await tool.func(parsedArgs);
      } else {
        result = `不明なツール: ${toolName}`;
      }
    } else {
      result = '実行する有効なアクションがありません';
    }
  } catch (error) {
    result = `実行エラー: ${error}`;
  }

  // Gemini Flashを使用して結果を反省
  const reflectionModel = new ChatGoogleGenerativeAI({
    model: 'gemini-2.5-flash',
    temperature: 0,
    maxOutputTokens: 1024,
  });

  const reflection = await reflectionModel.invoke([
    new SystemMessage('この結果が目標を成功裏に達成しているかを評価してください。'),
    new HumanMessage(`アクション: ${action}\n結果: ${result}\nこれは我々の目的を達成していますか?`),
  ]);

  // 反省でノードを更新
  const updatedNode: ThoughtNode = {
    ...currentNode,
    isTerminal: reflection.content?.includes('成功') || reflection.content?.includes('達成') || false,
    metadata: {
      ...currentNode.metadata,
      result,
      reflection: reflection.content,
    },
  };

  thoughtTree.set(currentNodeId, updatedNode);

  return {
    thoughtTree,
  };
}

成功を評価するためにGemini 2.5 Flashで反省機能を持つ思考ノードからのアクションを実行し、エージェントが実行結果から学習できるようにします。

4. 並列探索を使ったオーケストレーショングラフ

// lib/advanced-planning/graph.ts
import { StateGraph, END } from '@langchain/langgraph';
import { filter } from 'es-toolkit';
import { TreeState, ThoughtNode } from './types';
import { reactTreeNode } from './nodes/react-tree';
import { reflectiveExecutor } from './nodes/reflective-executor';

export function createAdvancedPlanningGraph() {
  const workflow = new StateGraph<TreeState>({
    stateSchema: TreeState,
  });

  // ノードを追加
  workflow.addNode('think', reactTreeNode);
  workflow.addNode('act', reflectiveExecutor);
  workflow.addNode('select_best', selectBestPathNode);

  // 条件付きルーティング
  workflow.addConditionalEdges(
    'think',
    (state: TreeState) => {
      // 探索を続けるか実行するかをチェック
      const { iterationCount, explorationBudget } = state;

      if (iterationCount >= explorationBudget) {
        return 'select';
      }

      // 現在のノードがアクションを必要とするかチェック
      const currentNode = state.currentNodeId
        ? state.thoughtTree.get(state.currentNodeId)
        : null;

      return currentNode?.metadata?.action ? 'execute' : 'explore';
    },
    {
      explore: 'think',
      execute: 'act',
      select: 'select_best',
    }
  );

  workflow.addConditionalEdges(
    'act',
    (state: TreeState) => {
      const currentNode = state.currentNodeId
        ? state.thoughtTree.get(state.currentNodeId)
        : null;

      return currentNode?.isTerminal ? 'end' : 'continue';
    },
    {
      end: END,
      continue: 'think',
    }
  );

  workflow.addEdge('select_best', END);
  workflow.setEntryPoint('think');

  return workflow.compile();
}

async function selectBestPathNode(state: TreeState): Promise<Partial<TreeState>> {
  const { thoughtTree, bestPath } = state;

  if (bestPath.length === 0) {
    return { finalOutput: '探索予算内でソリューションが見つかりませんでした' };
  }

  // es-toolkitを使用して最良パスから結果をコンパイル
  const pathNodes = filter(
    bestPath.map(id => thoughtTree.get(id)),
    (node): node is ThoughtNode => node !== undefined
  );

  const solution = pathNodes
    .map(node => ({
      step: node.content,
      result: node.metadata?.result || 'N/A',
    }));

  return {
    finalOutput: JSON.stringify(solution, null, 2),
  };
}

最適な問題解決のための思考、行動、パス選択フェーズ間の条件付き分岐により複雑な推論をオーケストレートします。

5. キャッシングによるパフォーマンス最適化

// lib/advanced-planning/cache.ts
import { hash } from 'es-toolkit/compat';
import { isEqual } from 'es-toolkit';

interface CacheEntry {
  result: any;
  timestamp: number;
  ttl: number;
}

// Vercelサーバーレス用、インメモリキャッシュまたはRedisなどの外部サービスを使用
// これはデモンストレーション用の簡易インメモリバージョンです
class InMemoryCache {
  private cache = new Map<string, CacheEntry>();

  get(key: string): any | null {
    const entry = this.cache.get(key);
    if (!entry) return null;

    if (Date.now() - entry.timestamp > entry.ttl * 1000) {
      this.cache.delete(key);
      return null;
    }

    return entry.result;
  }

  set(key: string, value: any, ttl: number): void {
    this.cache.set(key, {
      result: value,
      timestamp: Date.now(),
      ttl,
    });
  }

  delete(key: string): void {
    this.cache.delete(key);
  }
}

export class PlanningCache {
  private readonly prefix = 'planning:';
  private readonly defaultTTL = 3600; // 1時間
  private store = new InMemoryCache();

  async get(key: string): Promise<any | null> {
    try {
      const cacheKey = this.generateKey(key);
      return this.store.get(cacheKey);
    } catch (error) {
      console.error('キャッシュ取得エラー:', error);
      return null;
    }
  }

  async set(key: string, value: any, ttl?: number): Promise<void> {
    try {
      const cacheKey = this.generateKey(key);
      this.store.set(cacheKey, value, ttl || this.defaultTTL);
    } catch (error) {
      console.error('キャッシュ設定エラー:', error);
    }
  }

  private generateKey(input: string): string {
    // 一貫したキー生成のためにes-toolkit/compatのhashを使用
    const hashed = hash(input);
    return `${this.prefix}${hashed}`;
  }

  // 類似クエリのための意味的類似性キャッシング
  async findSimilar(
    query: string,
    threshold: number = 0.85
  ): Promise<any | null> {
    try {
      // 本番では、埋め込みを使用したベクトル類似性検索を使用
      // これは簡素化されたトークンオーバーラップバージョンです
      const queryTokens = new Set(query.toLowerCase().split(' '));

      // 類似性のためにキャッシュされたクエリをチェック
      for (const [key, entry] of (this.store as any).cache) {
        if (!key.startsWith(this.prefix)) continue;

        // 保存されている場合、メタデータから元のクエリを抽出
        const similarity = this.calculateSimilarity(query, key);
        if (similarity > threshold) {
          return entry.result;
        }
      }

      return null;
    } catch (error) {
      console.error('類似性検索エラー:', error);
      return null;
    }
  }

  private calculateSimilarity(query1: string, query2: string): number {
    // デモンストレーション用の簡単なJaccard類似性
    const set1 = new Set(query1.toLowerCase().split(' '));
    const set2 = new Set(query2.toLowerCase().split(' '));

    const intersection = new Set([...set1].filter(x => set2.has(x)));
    const union = new Set([...set1, ...set2]);

    return intersection.size / union.size;
  }
}

// サーバーレス用シングルトンインスタンスをエクスポート
export const planningCache = new PlanningCache();

冗長なLLM呼び出しを減らし、応答時間を向上させるために、es-toolkitユーティリティを使用してTTLと意味的類似性マッチングを備えたインテリジェントキャッシングを実装します。

6. 木探索のフロントエンド視覚化

// components/TreeVisualization.tsx
'use client';

import { useEffect, useRef } from 'react';
import { ThoughtNode } from '@/lib/advanced-planning/types';

interface TreeVisualizationProps {
  thoughtTree: Map<string, ThoughtNode>;
  currentNodeId: string | null;
  bestPath: string[];
}

export default function TreeVisualization({
  thoughtTree,
  currentNodeId,
  bestPath,
}: TreeVisualizationProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    if (!canvasRef.current) return;

    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    // キャンバスをクリア
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // ノードの位置を計算
    const positions = calculateTreeLayout(thoughtTree);

    // エッジを描画
    thoughtTree.forEach((node) => {
      const nodePos = positions.get(node.id);
      if (!nodePos) return;

      node.children.forEach((childId) => {
        const childPos = positions.get(childId);
        if (!childPos) return;

        ctx.beginPath();
        ctx.moveTo(nodePos.x, nodePos.y);
        ctx.lineTo(childPos.x, childPos.y);

        // 最良パスをハイライト
        if (bestPath.includes(node.id) && bestPath.includes(childId)) {
          ctx.strokeStyle = '#10b981';
          ctx.lineWidth = 3;
        } else {
          ctx.strokeStyle = '#6b7280';
          ctx.lineWidth = 1;
        }

        ctx.stroke();
      });
    });

    // ノードを描画
    thoughtTree.forEach((node) => {
      const pos = positions.get(node.id);
      if (!pos) return;

      ctx.beginPath();
      ctx.arc(pos.x, pos.y, 20, 0, 2 * Math.PI);

      // 状態に基づく色分け
      if (node.id === currentNodeId) {
        ctx.fillStyle = '#3b82f6'; // 現在のノード - 青
      } else if (bestPath.includes(node.id)) {
        ctx.fillStyle = '#10b981'; // 最良パス - 緑
      } else if (node.isTerminal) {
        ctx.fillStyle = '#f59e0b'; // 終端 - オレンジ
      } else {
        ctx.fillStyle = '#e5e7eb'; // デフォルト - グレー
      }

      ctx.fill();
      ctx.strokeStyle = '#1f2937';
      ctx.lineWidth = 2;
      ctx.stroke();

      // スコアを描画
      ctx.fillStyle = '#1f2937';
      ctx.font = '12px sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(node.score.toFixed(2), pos.x, pos.y);
    });
  }, [thoughtTree, currentNodeId, bestPath]);

  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h3 className="card-title">思考の木探索</h3>
        <canvas
          ref={canvasRef}
          width={800}
          height={400}
          className="border border-base-300 rounded-lg"
        />
        <div className="flex gap-4 mt-4 text-sm">
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-blue-500 rounded-full"></div>
            <span>現在</span>
          </div>
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-green-500 rounded-full"></div>
            <span>最良パス</span>
          </div>
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-orange-500 rounded-full"></div>
            <span>終端</span>
          </div>
        </div>
      </div>
    </div>
  );
}

function calculateTreeLayout(
  thoughtTree: Map<string, ThoughtNode>
): Map<string, { x: number; y: number }> {
  const positions = new Map<string, { x: number; y: number }>();
  const levelCounts = new Map<number, number>();

  // レベルごとのノード数をカウント
  thoughtTree.forEach((node) => {
    const count = levelCounts.get(node.depth) || 0;
    levelCounts.set(node.depth, count + 1);
  });

  // 位置を計算
  const levelIndices = new Map<number, number>();
  thoughtTree.forEach((node) => {
    const levelIndex = levelIndices.get(node.depth) || 0;
    const levelCount = levelCounts.get(node.depth) || 1;

    const x = (800 / (levelCount + 1)) * (levelIndex + 1);
    const y = 50 + node.depth * 100;

    positions.set(node.id, { x, y });
    levelIndices.set(node.depth, levelIndex + 1);
  });

  return positions;
}

思考の木探索をリアルタイムで視覚化し、インタラクティブなキャンバスレンダリングで現在のノード、最良パス、終端ノードを表示します。

結論

プランニングパターンは、AIエージェントを単純な応答者から複雑な多段階タスクを処理できる洗練された問題解決者に変換します。構造化ワークフローのためのプランアンドエグゼキュートを実装し、探索的推論のためにReActと思考の木を組み合わせることで、人間レベルの計画能力に匹敵またはそれを超えるエージェントを構築できます。

これらの実装からの主要な洞察:

  • プランニングと実行の関心の分離により、各フェーズに最適なモデルを使用可能(プランニングにGemini 2.5 Pro、実行にGemini 2.5 Flash)
  • スコアリングメカニズムを持つ木探索は線形アプローチよりも優れたソリューションを見つけます
  • キャッシングと最適化により洗練されたプランニングが本番環境で経済的に実行可能になります
  • リアルタイム視覚化によりユーザーがエージェントの意思決定を理解し信頼できます
  • 全体を通じたes-toolkitの使用により、サーバーレス環境に最適化されたクリーンで関数型のコードを確保します

これらのパターンは、TypeScript、LangGraph、Google GeminiモデルでVercelのサーバーレスプラットフォーム上で実行され、777秒の実行ウィンドウ内で複雑さを分解し、代替案を探索し、信頼性の高い結果をスケールで提供できる本番対応プランニングエージェントを構築するための基盤を提供します。