ドラフト エージェント設計パターン - 探索と発見

agentslangchainlanggraphtypescriptnextjsaiexploration
By sko X opus 4.19/21/202515 min read

このガイドでは、TypeScript、Next.js 15、LangChain、LangGraphをVercelのサーバーレスプラットフォーム上で使用して、自律的な探索・発見エージェントを構築する方法を実演します。事前定義されたソリューション空間を超えて、新しい情報を積極的に探索し、隠れたパターンを発見し、新しい洞察を生成するエージェントを作成します。

メンタルモデル:科学研究ラボ

探索エージェントは、仮想の研究ラボを組み立てることに似ています。エージェントは主任研究者(オーケストレーター)として機能し、異なる仮説を探索するための専門的な研究助手(ワーカーエージェント)を生成します。LangGraphはラボのインフラ(状態管理とワークフロー)を提供し、LangChainツールは研究機器として機能します。探索プロセスは科学的方法を模倣します:仮説を生成し、探索によってテストし、発見を評価し、発見に基づいて反復します。Vercelのサーバーレスプラットフォームは、研究の需要に応じて拡大・縮小できるスケーラブルなラボスペースとして機能します。

基本例:仮説駆動型エクスプローラー

1. コア探索状態管理

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

export const ExplorationStateSchema = z.object({
  query: z.string(),
  currentHypothesis: z.string().optional(),
  explorationDepth: z.number().default(0),
  discoveredPaths: z.array(z.string()).default([]),
  findings: z.array(z.object({
    path: z.string(),
    content: z.string(),
    confidence: z.number(),
    timestamp: z.number()
  })).default([]),
  confidenceThreshold: z.number().default(0.7),
  maxDepth: z.number().default(5),
  status: z.enum(['exploring', 'evaluating', 'backtracking', 'complete']).default('exploring')
});

export type ExplorationState = z.infer<typeof ExplorationStateSchema>;

export interface ExplorationNode {
  id: string;
  hypothesis: string;
  score: number;
  children: ExplorationNode[];
  visited: boolean;
  metadata: Record<string, any>;
}

仮説追跡、発見パス、信頼スコア、ツリーベースの探索ノードを含む、探索エージェントのコア状態構造を定義します。

2. 基本探索エージェント

// lib/exploration/basic-explorer.ts
import { StateGraph, MessagesAnnotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import { pull } from 'langchain/hub';
import { filter, map, sortBy } from 'es-toolkit';

interface BasicExplorationState {
  messages: BaseMessage[];
  hypothesis: string;
  explorationCount: number;
  discoveries: string[];
}

export async function createBasicExplorer() {
  const model = new ChatGoogleGenerativeAI({
    temperature: 0.7,
    modelName: 'gemini-2.5-flash'
  });

  const searchTool = new TavilySearchResults({ maxResults: 3 });

  // ノード:仮説生成
  async function generateHypothesis(state: BasicExplorationState) {
    const prompt = await pull('exploration/hypothesis-generator');
    const response = await model.invoke([
      new HumanMessage(`Based on: ${state.messages.slice(-1)[0].content}
        Generate a testable hypothesis for exploration.`)
    ]);

    return {
      hypothesis: response.content,
      messages: [...state.messages, response]
    };
  }

  // ノード:仮説探索
  async function exploreHypothesis(state: BasicExplorationState) {
    const searchResults = await searchTool.invoke(state.hypothesis);
    const parsedResults = JSON.parse(searchResults);

    const findings = map(
      filter(parsedResults, (r: any) => r.score > 0.5),
      (result: any) => result.content
    );

    return {
      discoveries: [...state.discoveries, ...findings],
      explorationCount: state.explorationCount + 1,
      messages: [...state.messages, new AIMessage(`Explored: ${findings.length} findings`)]
    };
  }

  // ノード:発見評価
  async function evaluateFindings(state: BasicExplorationState) {
    const sortedDiscoveries = sortBy(
      state.discoveries,
      (d: string) => d.length
    );

    const evaluation = await model.invoke([
      new HumanMessage(`Evaluate these discoveries: ${sortedDiscoveries.join('\n')}
        Should we continue exploring or have we found sufficient insights?`)
    ]);

    return {
      messages: [...state.messages, evaluation]
    };
  }

  // 条件付きエッジ:探索を続けるべきか?
  function shouldContinue(state: BasicExplorationState) {
    if (state.explorationCount >= 5) return 'end';
    if (state.discoveries.length > 10) return 'evaluate';
    return 'explore';
  }

  // グラフを構築
  const workflow = new StateGraph<BasicExplorationState>({
    channels: {
      messages: { value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y], default: () => [] },
      hypothesis: { value: (x, y) => y || x, default: () => '' },
      explorationCount: { value: (x, y) => y || x, default: () => 0 },
      discoveries: { value: (x: string[], y: string[]) => [...x, ...y], default: () => [] }
    }
  });

  workflow.addNode('generate', generateHypothesis);
  workflow.addNode('explore', exploreHypothesis);
  workflow.addNode('evaluate', evaluateFindings);

  workflow.addEdge('__start__', 'generate');
  workflow.addEdge('generate', 'explore');
  workflow.addConditionalEdges('explore', shouldContinue, {
    'explore': 'generate',
    'evaluate': 'evaluate',
    'end': '__end__'
  });
  workflow.addEdge('evaluate', '__end__');

  return workflow.compile();
}

仮説を生成し、検索ツールを使用してそれらを探索し、反復サイクルで発見を評価する基本的な探索エージェントを作成します。

3. 探索用APIルート

// app/api/explore/route.ts
import { createBasicExplorer } from '@/lib/exploration/basic-explorer';
import { HumanMessage } from '@langchain/core/messages';
import { NextResponse } from 'next/server';

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

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

    const explorer = await createBasicExplorer();

    const encoder = new TextEncoder();
    const stream = new TransformStream();
    const writer = stream.writable.getWriter();

    // バックグラウンドで探索を実行
    (async () => {
      try {
        const eventStream = await explorer.stream({
          messages: [new HumanMessage(query)],
          hypothesis: '',
          explorationCount: 0,
          discoveries: []
        });

        for await (const event of eventStream) {
          const update = {
            type: 'exploration_update',
            hypothesis: event.hypothesis,
            discoveries: event.discoveries?.length || 0,
            status: event.explorationCount < 5 ? 'exploring' : 'complete'
          };

          await writer.write(
            encoder.encode(`data: ${JSON.stringify(update)}\n\n`)
          );
        }
      } catch (error) {
        console.error('Exploration error:', error);
      } 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:', error);
    return NextResponse.json(
      { error: 'Failed to start exploration' },
      { status: 500 }
    );
  }
}

レスポンシブなユーザー体験のために、Server-Sent Eventsを使用してリアルタイムで探索の進捗をストリーミングするAPIエンドポイント。

4. フロントエンド探索インターフェース

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

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

interface ExplorationUpdate {
  type: string;
  hypothesis?: string;
  discoveries?: number;
  status?: string;
}

export default function ExplorationInterface() {
  const [query, setQuery] = useState('');
  const [updates, setUpdates] = useState<ExplorationUpdate[]>([]);
  const [isExploring, setIsExploring] = useState(false);

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

      if (!response.ok) throw new Error('Exploration failed');

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

      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;

        const text = decoder.decode(value);
        const lines = text.split('\n');

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = JSON.parse(line.slice(6));
            setUpdates(prev => [...prev, data]);
          }
        }
      }
    },
    onSuccess: () => {
      setIsExploring(false);
    },
    onError: (error) => {
      console.error('Exploration error:', error);
      setIsExploring(false);
    }
  });

  const handleExplore = useCallback(
    debounce(() => {
      if (query.trim()) {
        setIsExploring(true);
        setUpdates([]);
        startExploration.mutate(query);
      }
    }, 500),
    [query]
  );

  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">探索・発見エージェント</h2>

        <div className="form-control">
          <label className="label">
            <span className="label-text">何を探索しますか?</span>
          </label>
          <textarea
            className="textarea textarea-bordered h-24"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="深い探索のためのトピックや質問を入力..."
          />
        </div>

        <div className="card-actions justify-end">
          <button
            className="btn btn-primary"
            onClick={handleExplore}
            disabled={isExploring || !query.trim()}
          >
            {isExploring ? (
              <>
                <span className="loading loading-spinner"></span>
                探索中...
              </>
            ) : '探索開始'}
          </button>
        </div>

        {updates.length > 0 && (
          <div className="mt-4">
            <h3 className="font-semibold mb-2">探索進捗:</h3>
            <div className="space-y-2 max-h-64 overflow-y-auto">
              {updates.map((update, idx) => (
                <div key={idx} className="alert alert-info">
                  <div>
                    <span className="font-semibold">仮説:</span> {update.hypothesis}
                    <br />
                    <span className="font-semibold">発見:</span> {update.discoveries}
                    <br />
                    <span className="badge badge-sm">{update.status}</span>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

仮説生成と発見追跡を使用して探索プロセスのリアルタイム視覚化を提供するReactコンポーネント。

高度な例:マルチエージェント科学発見システム

1. モンテカルロ木探索エクスプローラー

// lib/exploration/mcts-explorer.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { mean, sum, maxBy, sample } from 'es-toolkit';
import { v4 as uuidv4 } from 'uuid';

interface MCTSNode {
  id: string;
  state: string;
  value: number;
  visits: number;
  parent?: MCTSNode;
  children: MCTSNode[];
  untried_actions: string[];
}

export class MonteCarloTreeSearchExplorer {
  private model: ChatGoogleGenerativeAI;
  private explorationConstant: number = Math.sqrt(2);
  private maxIterations: number = 100;

  constructor() {
    this.model = new ChatGoogleGenerativeAI({
      temperature: 0.9,
      modelName: 'gemini-2.5-pro'
    });
  }

  async explore(problem: string, iterations: number = 100): Promise<any> {
    this.maxIterations = iterations;
    const root = this.createNode(problem);

    for (let i = 0; i < this.maxIterations; i++) {
      // 選択
      const selectedNode = await this.select(root);

      // 拡張
      const expandedNode = await this.expand(selectedNode);

      // シミュレーション(セルフリファイン)
      const reward = await this.simulate(expandedNode);

      // バックプロパゲーション
      await this.backpropagate(expandedNode, reward);
    }

    return this.getBestPath(root);
  }

  private createNode(state: string, parent?: MCTSNode): MCTSNode {
    return {
      id: uuidv4(),
      state,
      value: 0,
      visits: 0,
      parent,
      children: [],
      untried_actions: []
    };
  }

  private async select(node: MCTSNode): Promise<MCTSNode> {
    while (node.children.length > 0) {
      const ucbValues = node.children.map(child => this.calculateUCB(child));
      const maxIndex = ucbValues.indexOf(Math.max(...ucbValues));
      node = node.children[maxIndex];
    }
    return node;
  }

  private calculateUCB(node: MCTSNode): number {
    if (node.visits === 0) return Infinity;

    const exploitation = node.value / node.visits;
    const exploration = this.explorationConstant *
      Math.sqrt(Math.log(node.parent!.visits) / node.visits);

    return exploitation + exploration;
  }

  private async expand(node: MCTSNode): Promise<MCTSNode> {
    // LLMを使用して可能なアクションを生成
    const response = await this.model.invoke([{
      role: 'system',
      content: 'ソリューション空間を探索しています。3つの多様な次のステップを生成してください。'
    }, {
      role: 'user',
      content: `現在の状態: ${node.state}\n次の探索ステップをJSON配列として生成してください。`
    }]);

    try {
      const actions = JSON.parse(response.content as string);
      node.untried_actions = actions;

      if (actions.length > 0) {
        const action = sample(actions);
        const childNode = this.createNode(action, node);
        node.children.push(childNode);
        return childNode;
      }
    } catch (e) {
      console.error('アクションの解析に失敗しました:', e);
    }

    return node;
  }

  private async simulate(node: MCTSNode): Promise<number> {
    // セルフリファイン:LLMを使用して現在のパスを評価し改善
    const response = await this.model.invoke([{
      role: 'system',
      content: 'この探索パスを評価し、改善を提案してください。品質を0-1で評価してください。'
    }, {
      role: 'user',
      content: `パス: ${this.getPath(node).join(' -> ')}\n評価とスコア。`
    }]);

    // レスポンスからスコアを抽出
    const scoreMatch = response.content.toString().match(/\d\.\d+/);
    return scoreMatch ? parseFloat(scoreMatch[0]) : 0.5;
  }

  private async backpropagate(node: MCTSNode, reward: number) {
    let current: MCTSNode | undefined = node;
    while (current) {
      current.visits++;
      current.value += reward;
      current = current.parent;
    }
  }

  private getPath(node: MCTSNode): string[] {
    const path: string[] = [];
    let current: MCTSNode | undefined = node;

    while (current) {
      path.unshift(current.state);
      current = current.parent;
    }

    return path;
  }

  private getBestPath(root: MCTSNode): any {
    const bestChild = maxBy(root.children, child => child.value / child.visits);

    if (!bestChild) return { path: [root.state], score: 0 };

    return {
      path: this.getPath(bestChild),
      score: bestChild.value / bestChild.visits,
      explorations: this.countTotalNodes(root)
    };
  }

  private countTotalNodes(node: MCTSNode): number {
    return 1 + sum(node.children.map(child => this.countTotalNodes(child)));
  }
}

UCBベースの選択、LLMによる拡張、セルフリファインメントを使用したインテリジェント探索のためのモンテカルロ木探索を実装します。

2. マルチエージェント研究ラボ

// lib/exploration/research-laboratory.ts
import { StateGraph, MessagesAnnotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BaseMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
import { groupBy, flatten, uniqBy } from 'es-toolkit';
import { MonteCarloTreeSearchExplorer } from './mcts-explorer';

interface ResearchState {
  messages: BaseMessage[];
  research_topic: string;
  hypotheses: Array<{id: string; content: string; score: number}>;
  literature_review: string;
  experiments: Array<{id: string; design: string; results?: string}>;
  synthesis: string;
  phase: 'planning' | 'literature' | 'hypothesis' | 'experimentation' | 'synthesis';
}

export class ResearchLaboratory {
  private leadModel: ChatGoogleGenerativeAI;
  private workerModel: ChatGoogleGenerativeAI;
  private mctsExplorer: MonteCarloTreeSearchExplorer;

  constructor() {
    this.leadModel = new ChatGoogleGenerativeAI({
      temperature: 0.3,
      modelName: 'gemini-2.5-pro'
    });

    this.workerModel = new ChatGoogleGenerativeAI({
      temperature: 0.7,
      modelName: 'gemini-2.5-flash'
    });

    this.mctsExplorer = new MonteCarloTreeSearchExplorer();
  }

  async createResearchWorkflow() {
    const workflow = new StateGraph<ResearchState>({
      channels: {
        messages: {
          value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y],
          default: () => []
        },
        research_topic: {
          value: (x, y) => y || x,
          default: () => ''
        },
        hypotheses: {
          value: (x, y) => y || x,
          default: () => []
        },
        literature_review: {
          value: (x, y) => y || x,
          default: () => ''
        },
        experiments: {
          value: (x, y) => y || x,
          default: () => []
        },
        synthesis: {
          value: (x, y) => y || x,
          default: () => ''
        },
        phase: {
          value: (x, y) => y || x,
          default: () => 'planning'
        }
      }
    });

    // 計画エージェント
    workflow.addNode('planner', async (state) => {
      const plan = await this.leadModel.invoke([
        new HumanMessage(`研究計画を作成: ${state.research_topic}
          含める: 1) 調査する主要分野 2) 方法論 3) 成功指標`)
      ]);

      return {
        messages: [...state.messages, plan],
        phase: 'literature' as const
      };
    });

    // 文献レビューエージェント
    workflow.addNode('literature_reviewer', async (state) => {
      // 複数のエージェントによる並列文献レビューをシミュレート
      const reviewPromises = Array.from({ length: 3 }, async (_, i) => {
        const review = await this.workerModel.invoke([
          new HumanMessage(`${state.research_topic}に関する文献をレビュー
            フォーカスエリア ${i + 1}: ${['理論的基礎', '最近の進歩', '未解決の問題'][i]}`)
        ]);
        return review.content;
      });

      const reviews = await Promise.all(reviewPromises);
      const combinedReview = reviews.join('\n\n');

      return {
        literature_review: combinedReview,
        messages: [...state.messages, new AIMessage(combinedReview)],
        phase: 'hypothesis' as const
      };
    });

    // 仮説生成エージェント(MCTSを使用)
    workflow.addNode('hypothesis_generator', async (state) => {
      const explorationResult = await this.mctsExplorer.explore(
        `仮説を生成: ${state.research_topic}\n基づく: ${state.literature_review}`,
        50
      );

      const hypotheses = explorationResult.path.map((h: string, idx: number) => ({
        id: `hyp_${idx}`,
        content: h,
        score: Math.random() * (1 - 0.5) + 0.5 // スコアリングをシミュレート
      }));

      return {
        hypotheses: hypotheses,
        messages: [...state.messages, new AIMessage(`${hypotheses.length}個の仮説を生成しました`)],
        phase: 'experimentation' as const
      };
    });

    // 実験設計エージェント
    workflow.addNode('experimenter', async (state) => {
      const topHypotheses = state.hypotheses
        .sort((a, b) => b.score - a.score)
        .slice(0, 3);

      const experiments = await Promise.all(
        topHypotheses.map(async (hyp) => {
          const design = await this.workerModel.invoke([
            new HumanMessage(`テストする実験を設計: ${hyp.content}`)
          ]);

          return {
            id: hyp.id,
            design: design.content as string,
            results: `${hyp.id}のシミュレート結果`
          };
        })
      );

      return {
        experiments: experiments,
        messages: [...state.messages, new AIMessage(`${experiments.length}個の実験を設計しました`)],
        phase: 'synthesis' as const
      };
    });

    // 統合エージェント
    workflow.addNode('synthesizer', async (state) => {
      const synthesis = await this.leadModel.invoke([
        new HumanMessage(`研究結果を統合:
          トピック: ${state.research_topic}
          文献: ${state.literature_review}
          仮説: ${JSON.stringify(state.hypotheses)}
          実験: ${JSON.stringify(state.experiments)}

          包括的な洞察と結論を提供してください。`)
      ]);

      return {
        synthesis: synthesis.content as string,
        messages: [...state.messages, synthesis]
      };
    });

    // ワークフローエッジを定義
    workflow.addEdge('__start__', 'planner');
    workflow.addEdge('planner', 'literature_reviewer');
    workflow.addEdge('literature_reviewer', 'hypothesis_generator');
    workflow.addEdge('hypothesis_generator', 'experimenter');
    workflow.addEdge('experimenter', 'synthesizer');
    workflow.addEdge('synthesizer', '__end__');

    return workflow.compile();
  }
}

計画、文献レビュー、MCTSを使用した仮説生成、実験、統合のための専門エージェントを備えた完全なマルチエージェント研究ラボを実装します。

3. 状態永続化を持つサーバーレス探索

// lib/exploration/serverless-explorer.ts
import { kv } from '@vercel/kv';
import { Queue } from 'bullmq';
import { chunk, throttle } from 'es-toolkit';

interface ExplorationChunk {
  id: string;
  sessionId: string;
  chunkIndex: number;
  totalChunks: number;
  state: any;
  timestamp: number;
}

export class ServerlessExplorer {
  private maxExecutionTime = 777; // 秒(安全バッファ付き)
  private checkpointInterval = 60; // 秒

  async executeWithCheckpoints(
    explorationFn: () => Promise<any>,
    sessionId: string
  ) {
    const startTime = Date.now();
    const checkpointKey = `exploration:${sessionId}`;

    // 前の状態を復元しようとする
    const previousState = await kv.get<ExplorationChunk>(checkpointKey);
    let currentState = previousState?.state || {};

    const executeWithTimeout = async () => {
      const elapsedSeconds = (Date.now() - startTime) / 1000;

      if (elapsedSeconds >= this.maxExecutionTime - 30) {
        // 状態を保存して継続をスケジュール
        await this.saveCheckpoint(sessionId, currentState);
        await this.scheduleContinuation(sessionId);
        return { status: 'paused', state: currentState };
      }

      // 探索チャンクを実行
      const result = await explorationFn();
      currentState = { ...currentState, ...result };

      // 定期的なチェックポイント
      if (elapsedSeconds % this.checkpointInterval < 1) {
        await this.saveCheckpoint(sessionId, currentState);
      }

      return { status: 'continuing', state: currentState };
    };

    // レート制限を防ぐために実行をスロットル
    const throttledExecute = throttle(executeWithTimeout, 1000);

    let status = 'continuing';
    while (status === 'continuing') {
      const result = await throttledExecute();
      status = result.status;
      currentState = result.state;
    }

    return currentState;
  }

  private async saveCheckpoint(sessionId: string, state: any) {
    const checkpoint: ExplorationChunk = {
      id: `checkpoint_${Date.now()}`,
      sessionId,
      chunkIndex: state.chunkIndex || 0,
      totalChunks: state.totalChunks || 1,
      state,
      timestamp: Date.now()
    };

    await kv.set(
      `exploration:${sessionId}`,
      checkpoint,
      { ex: 3600 } // 1時間の有効期限
    );
  }

  private async scheduleContinuation(sessionId: string) {
    // スケジューリングのためにInngestまたは類似のものを使用
    await fetch('/api/schedule', {
      method: 'POST',
      body: JSON.stringify({
        event: 'exploration.continue',
        data: { sessionId },
        delay: '5s'
      })
    });
  }
}

// チャンク化された探索用APIルート
export async function POST(req: Request) {
  const { sessionId, query } = await req.json();
  const explorer = new ServerlessExplorer();

  const explorationTasks = chunk(
    Array.from({ length: 20 }, (_, i) => i),
    5
  );

  const result = await explorer.executeWithCheckpoints(
    async () => {
      // 探索の1チャンクを実行
      const tasks = explorationTasks.shift();
      if (!tasks) return { complete: true };

      const results = await Promise.all(
        tasks.map(async (taskId) => {
          // 探索タスクをシミュレート
          return { taskId, result: `発見 ${taskId}` };
        })
      );

      return {
        chunkIndex: (explorationTasks.length || 0) + 1,
        discoveries: results
      };
    },
    sessionId
  );

  return new Response(JSON.stringify(result), {
    headers: { 'Content-Type': 'application/json' }
  });
}

長時間実行される探索のための自動チェックポイント、状態永続化、タスク継続を備えたサーバーレス対応の探索を実装します。

4. ベクトルメモリ統合

// lib/exploration/memory-system.ts
import { Pinecone } from '@pinecone-database/pinecone';
import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
import { Document } from '@langchain/core/documents';
import { groupBy, sortBy, take } from 'es-toolkit';

export class ExplorationMemorySystem {
  private pinecone: Pinecone;
  private embeddings: GoogleGenerativeAIEmbeddings;
  private indexName = 'exploration-memory';

  constructor() {
    this.pinecone = new Pinecone({
      apiKey: process.env.PINECONE_API_KEY!
    });

    this.embeddings = new GoogleGenerativeAIEmbeddings({
      modelName: 'embedding-001'
    });
  }

  async storeDiscovery(
    content: string,
    metadata: {
      sessionId: string;
      hypothesis: string;
      confidence: number;
      timestamp: number;
      explorationPath: string[];
    }
  ) {
    const embedding = await this.embeddings.embedQuery(content);
    const index = this.pinecone.index(this.indexName);

    await index.upsert([{
      id: `discovery_${metadata.sessionId}_${metadata.timestamp}`,
      values: embedding,
      metadata: {
        ...metadata,
        content: content.slice(0, 1000) // プレビューを保存
      }
    }]);
  }

  async querySemanticMemory(
    query: string,
    filters?: Record<string, any>
  ): Promise<Document[]> {
    const queryEmbedding = await this.embeddings.embedQuery(query);
    const index = this.pinecone.index(this.indexName);

    const results = await index.query({
      vector: queryEmbedding,
      topK: 20,
      includeMetadata: true,
      filter: filters
    });

    // 仮説でグループ化し、各グループから最高のものを取得
    const grouped = groupBy(
      results.matches,
      (match: any) => match.metadata.hypothesis
    );

    const diverseResults = Object.values(grouped).map(group => {
      const sorted = sortBy(group, (m: any) => -m.score);
      return sorted[0];
    });

    return take(diverseResults, 10).map(match =>
      new Document({
        pageContent: match.metadata.content,
        metadata: match.metadata
      })
    );
  }

  async getExplorationPattern(sessionId: string) {
    const index = this.pinecone.index(this.indexName);

    const results = await index.query({
      vector: new Array(768).fill(0), // Gemini埋め込み用のダミーベクトル
      topK: 100,
      includeMetadata: true,
      filter: { sessionId }
    });

    // 探索パターンを分析
    const pathFrequency = new Map<string, number>();
    results.matches.forEach(match => {
      const path = match.metadata.explorationPath?.join(' -> ') || '';
      pathFrequency.set(path, (pathFrequency.get(path) || 0) + 1);
    });

    return {
      totalDiscoveries: results.matches.length,
      avgConfidence: results.matches.reduce(
        (sum, m) => sum + (m.metadata.confidence || 0), 0
      ) / results.matches.length,
      mostFrequentPaths: Array.from(pathFrequency.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, 5)
    };
  }
}

長期的な探索パターン学習のためのPineconeベクトルデータベースを使用したセマンティックメモリの保存と取得を実装します。

5. リアルタイム視覚化を備えた高度な探索UI

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

import { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { debounce } from 'es-toolkit';

interface ExplorationMetrics {
  timestamp: number;
  depth: number;
  discoveries: number;
  confidence: number;
}

export default function AdvancedExplorationUI() {
  const [topic, setTopic] = useState('');
  const [metrics, setMetrics] = useState<ExplorationMetrics[]>([]);
  const [currentPhase, setCurrentPhase] = useState('idle');
  const [hypotheses, setHypotheses] = useState<Array<{id: string; content: string; score: number}>>([]);

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

      if (!response.ok) throw new Error('研究が失敗しました');

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

      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;

        const text = decoder.decode(value);
        const lines = text.split('\n');

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = JSON.parse(line.slice(6));

            // フェーズを更新
            if (data.phase) setCurrentPhase(data.phase);

            // 仮説を更新
            if (data.hypotheses) setHypotheses(data.hypotheses);

            // メトリクスを更新
            if (data.metrics) {
              setMetrics(prev => [...prev, {
                timestamp: Date.now(),
                depth: data.metrics.depth,
                discoveries: data.metrics.discoveries,
                confidence: data.metrics.confidence
              }]);
            }
          }
        }
      }
    }
  });

  const memoryStats = useQuery({
    queryKey: ['memory-stats', topic],
    queryFn: async () => {
      const response = await fetch(`/api/memory-stats?topic=${encodeURIComponent(topic)}`);
      return response.json();
    },
    enabled: !!topic,
    refetchInterval: 5000
  });

  return (
    <div className="min-h-screen bg-base-200 p-4">
      <div className="max-w-7xl mx-auto">
        <div className="text-center mb-8">
          <h1 className="text-5xl font-bold mb-2">研究ラボ</h1>
          <p className="text-xl">マルチエージェント科学発見システム</p>
        </div>

        <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
          {/* コントロールパネル */}
          <div className="lg:col-span-1">
            <div className="card bg-base-100 shadow-xl">
              <div className="card-body">
                <h2 className="card-title">研究コントロール</h2>

                <div className="form-control">
                  <label className="label">
                    <span className="label-text">研究トピック</span>
                  </label>
                  <input
                    type="text"
                    className="input input-bordered"
                    value={topic}
                    onChange={(e) => setTopic(e.target.value)}
                    placeholder="研究トピックを入力..."
                  />
                </div>

                <button
                  className="btn btn-primary mt-4"
                  onClick={() => startResearch.mutate(topic)}
                  disabled={!topic || startResearch.isPending}
                >
                  {startResearch.isPending ? (
                    <>
                      <span className="loading loading-spinner"></span>
                      研究中...
                    </>
                  ) : '研究開始'}
                </button>

                <div className="divider"></div>

                <div className="stats stats-vertical shadow">
                  <div className="stat">
                    <div className="stat-title">現在のフェーズ</div>
                    <div className="stat-value text-primary">{currentPhase}</div>
                  </div>
                  <div className="stat">
                    <div className="stat-title">仮説</div>
                    <div className="stat-value">{hypotheses.length}</div>
                  </div>
                  <div className="stat">
                    <div className="stat-title">メモリエントリ</div>
                    <div className="stat-value">{memoryStats.data?.totalEntries || 0}</div>
                  </div>
                </div>
              </div>
            </div>
          </div>

          {/* 視覚化パネル */}
          <div className="lg:col-span-2">
            <div className="card bg-base-100 shadow-xl">
              <div className="card-body">
                <h2 className="card-title">探索メトリクス</h2>

                <ResponsiveContainer width="100%" height={300}>
                  <LineChart data={metrics}>
                    <CartesianGrid strokeDasharray="3 3" />
                    <XAxis dataKey="timestamp" />
                    <YAxis />
                    <Tooltip />
                    <Line
                      type="monotone"
                      dataKey="confidence"
                      stroke="#8884d8"
                      name="信頼度"
                    />
                    <Line
                      type="monotone"
                      dataKey="discoveries"
                      stroke="#82ca9d"
                      name="発見"
                    />
                    <Line
                      type="monotone"
                      dataKey="depth"
                      stroke="#ffc658"
                      name="深さ"
                    />
                  </LineChart>
                </ResponsiveContainer>

                <div className="divider"></div>

                {/* 仮説リスト */}
                <h3 className="font-semibold mb-2">生成された仮説</h3>
                <div className="space-y-2 max-h-64 overflow-y-auto">
                  {hypotheses.map((hyp) => (
                    <div key={hyp.id} className="alert">
                      <div className="flex-1">
                        <p className="font-semibold">{hyp.content}</p>
                        <div className="flex items-center gap-2 mt-1">
                          <span className="badge badge-sm">スコア: {hyp.score.toFixed(2)}</span>
                          <progress
                            className="progress progress-primary w-24"
                            value={hyp.score}
                            max="1"
                          ></progress>
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

包括的な探索モニタリングのためのリアルタイムメトリクス視覚化、仮説追跡、メモリ統計を備えた高度なUIコンポーネント。

まとめ

この実装では、TypeScript、LangChain、LangGraphをVercelのサーバーレスプラットフォーム上で使用して、複雑な問題空間を自律的にナビゲートする本番環境対応の探索・発見エージェントを実証しました。示されたパターンにより、エージェントは仮説を生成し、モンテカルロ木探索を使用してソリューション空間を探索し、マルチエージェント研究チームを調整し、継続的な学習のためのセマンティックメモリを維持できます。主要なアーキテクチャ上の決定には、リアルタイムストリーミングのためのServer-Sent Eventsの使用、サーバーレス制約内での長時間実行される探索のためのチェックポイント実装、パターン認識のためのベクトルデータベースの活用、効率的なデータ操作のためのes-toolkitの使用が含まれます。これらの探索エージェントは、AIシステムを反応的なツールから、新しい洞察を発見し、自身の能力を拡張できる積極的な研究パートナーへと変革します。