ドラフト "エージェンティック設計パターン - プロンプトチェーン"

ailangchainlangraphnextjsprompt-engineeringagents
By sko X opus 4.19/20/202513 min read

複雑なタスクを管理可能なステップに分解し、特化したプロンプト間で出力を受け渡すことで、Next.jsアプリケーションにおいて高度な推論と処理能力を実現する、本格的な連続AIワークフローを構築します。

メンタルモデル:AI向けの組み立てライン

プロンプトチェーンを、テスラ工場の現代的な組み立てラインのように考えてみてください。各ステーション(プロンプト)が特定の作業を実行します - 一つはホイールの取り付け、別のものは塗装、そして三つ目は品質検査を行います。各ステーションからの出力が次のステーションの入力となり、最終的に洗練された製品を作り上げます。AI分野では、車を作る代わりに情報を精製します:最初にデータを抽出し、次に分析し、そしてフォーマットし、最後に検証します。組み立てラインが製造効率を革命的に変えたように、プロンプトチェーンは複雑なAIタスクを専門化され管理可能な操作に分解することで、独立して最適化できるAIタスクの処理方法を革命的に変えます。

基本的な連続チェーンの実装

1. シンプルなテキスト処理チェーン

// lib/chains/basic-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RunnableSequence } from '@langchain/core/runnables';

export function createBasicChain() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-flash',
    temperature: 0,
    maxRetries: 3,
  });

  // ステップ1: 入力を要約する
  const summarizePrompt = PromptTemplate.fromTemplate(
    `以下のテキストを2-3文で要約してください:

    {text}`
  );

  // ステップ2: 重要なポイントを抽出する
  const extractPrompt = PromptTemplate.fromTemplate(
    `この要約から3-5個の重要なポイントを抽出してください:

    {summary}`
  );

  // ステップ3: アクションアイテムを生成する
  const actionPrompt = PromptTemplate.fromTemplate(
    `これらの重要なポイントに基づいて、実行可能な推奨事項を生成してください:

    {keyPoints}`
  );

  // LCELを使用してチェーンを構築
  const chain = RunnableSequence.from([
    // 最初のステップ: 要約
    summarizePrompt,
    model,
    new StringOutputParser(),
    // 次のステップに渡す
    (summary) => ({ summary }),
    extractPrompt,
    model,
    new StringOutputParser(),
    // 最終ステップに渡す
    (keyPoints) => ({ keyPoints }),
    actionPrompt,
    model,
    new StringOutputParser(),
  ]);

  return chain;
}

生のテキスト入力から実行可能な推奨事項まで、段階的にテキストを精製する3ステップのチェーンを作成します。

2. ストリーミング機能付きAPIルート

// app/api/basic-chain/route.ts
import { createBasicChain } from '@/lib/chains/basic-chain';
import { NextResponse } from 'next/server';

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

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

    if (!text || text.length < 50) {
      return NextResponse.json(
        { error: '最低50文字以上のテキストを提供してください' },
        { status: 400 }
      );
    }

    const chain = createBasicChain();
    const result = await chain.invoke({ text });

    return NextResponse.json({
      success: true,
      result,
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    console.error('チェーン実行エラー:', error);
    return NextResponse.json(
      { error: 'チェーンの処理に失敗しました' },
      { status: 500 }
    );
  }
}

サーバーレスデプロイメントに対応した適切なエラーハンドリングと検証を含むチェーン実行を処理します。

3. TanStack Queryを使用したフロントエンド統合

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

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

interface ChainResponse {
  success: boolean;
  result: string;
  timestamp: string;
}

export default function BasicChainInterface() {
  const [text, setText] = useState('');
  const [charCount, setCharCount] = useState(0);

  const processChain = useMutation<ChainResponse, Error, string>({
    mutationFn: async (inputText: string) => {
      const response = await fetch('/api/basic-chain', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: inputText }),
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error || 'チェーン処理に失敗しました');
      }

      return response.json();
    },
    retry: 2,
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
  });

  const handleTextChange = debounce((value: string) => {
    setCharCount(value.length);
  }, 300);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.length >= 50) {
      processChain.mutate(text);
    }
  };

  return (
    <div className="card w-full bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">テキスト処理チェーン</h2>

        <form onSubmit={handleSubmit} className="space-y-4">
          <div className="form-control">
            <label className="label">
              <span className="label-text">入力テキスト</span>
              <span className="label-text-alt">{charCount} 文字</span>
            </label>
            <textarea
              className="textarea textarea-bordered h-32"
              placeholder="処理するテキストを入力してください(最低50文字)..."
              value={text}
              onChange={(e) => {
                setText(e.target.value);
                handleTextChange(e.target.value);
              }}
              disabled={processChain.isPending}
            />
          </div>

          <button
            type="submit"
            className="btn btn-primary"
            disabled={processChain.isPending || text.length < 50}
          >
            {processChain.isPending ? (
              <>
                <span className="loading loading-spinner"></span>
                チェーン処理中...
              </>
            ) : (
              'テキストを処理'
            )}
          </button>
        </form>

        {processChain.isError && (
          <div className="alert alert-error mt-4">
            <span>{processChain.error.message}</span>
          </div>
        )}

        {processChain.isSuccess && (
          <div className="card bg-base-200 mt-4">
            <div className="card-body">
              <h3 className="font-semibold">結果:</h3>
              <p className="whitespace-pre-wrap">{processChain.data.result}</p>
              <p className="text-xs text-base-content/60 mt-2">
                処理完了時刻: {new Date(processChain.data.timestamp).toLocaleString()}
              </p>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

TanStack Queryを使用したデバウンス文字カウントと包括的なエラーハンドリングを備えたReactコンポーネント。

状態管理を備えた高度なマルチステップチェーン

1. LangGraphを使用したドキュメント分析チェーン

// lib/chains/document-analyzer.ts
import { StateGraph, START, END, Annotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BaseMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
import { z } from 'zod';
import { StructuredOutputParser } from '@langchain/core/output_parsers';
import { isNil, chunk } from 'es-toolkit';

// Zodでスキーマを定義
const DocumentMetadata = z.object({
  title: z.string(),
  category: z.string(),
  confidence: z.number().min(0).max(1),
  keywords: z.array(z.string()),
});

// グラフ状態を定義
const GraphState = Annotation.Root({
  document: Annotation<string>(),
  chunks: Annotation<string[]>(),
  metadata: Annotation<z.infer<typeof DocumentMetadata>>(),
  summary: Annotation<string>(),
  insights: Annotation<string[]>(),
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
});

export function createDocumentAnalyzer() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-pro',
    temperature: 0.1,
    maxRetries: 3,
  });

  const metadataParser = StructuredOutputParser.fromZodSchema(DocumentMetadata);

  const workflow = new StateGraph(GraphState)
    // ノード1: ドキュメントを分割
    .addNode('chunk_document', async (state) => {
      const doc = state.document;
      // 50文字重複で500文字セグメントに分割
      const chunks = chunk(doc.split(' '), 100).map(words => words.join(' '));

      return {
        chunks,
        messages: [new AIMessage(`ドキュメントを${chunks.length}セグメントに分割しました`)],
      };
    })
    // ノード2: メタデータを抽出
    .addNode('extract_metadata', async (state) => {
      const prompt = `このドキュメントを分析してメタデータを抽出してください。

ドキュメント: ${state.chunks[0]}

${metadataParser.getFormatInstructions()}`;

      const response = await model.invoke([new HumanMessage(prompt)]);
      const metadata = await metadataParser.parse(response.content as string);

      return {
        metadata,
        messages: [new AIMessage(`メタデータを抽出しました: ${metadata.category}`)],
      };
    })
    // ノード3: 要約を生成
    .addNode('generate_summary', async (state) => {
      const combinedText = state.chunks.slice(0, 3).join('\n\n');
      const prompt = `このドキュメントの包括的な要約を作成してください。
      カテゴリ: ${state.metadata.category}

      ドキュメント抜粋:
      ${combinedText}`;

      const response = await model.invoke([new HumanMessage(prompt)]);

      return {
        summary: response.content as string,
        messages: [new AIMessage('要約を生成しました')],
      };
    })
    // ノード4: 洞察を抽出
    .addNode('extract_insights', async (state) => {
      const prompt = `この要約とメタデータに基づいて、3-5個の重要な洞察を提供してください:

      カテゴリ: ${state.metadata.category}
      キーワード: ${state.metadata.keywords.join(', ')}
      要約: ${state.summary}`;

      const response = await model.invoke([new HumanMessage(prompt)]);
      const insights = (response.content as string)
        .split('\n')
        .filter(line => line.trim().length > 0);

      return {
        insights,
        messages: [new AIMessage(`${insights.length}個の洞察を抽出しました`)],
      };
    })
    // エッジを定義
    .addEdge(START, 'chunk_document')
    .addEdge('chunk_document', 'extract_metadata')
    .addEdge('extract_metadata', 'generate_summary')
    .addEdge('generate_summary', 'extract_insights')
    .addEdge('extract_insights', END);

  return workflow.compile();
}

メタデータ抽出と洞察生成を備えたステートフルなドキュメント処理パイプラインを実装します。

2. ドキュメント分析用ストリーミングAPIルート

// app/api/analyze-document/route.ts
import { createDocumentAnalyzer } from '@/lib/chains/document-analyzer';
import { HumanMessage } from '@langchain/core/messages';

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

export async function POST(req: Request) {
  const { document } = await req.json();

  if (!document || document.length < 100) {
    return new Response(
      JSON.stringify({ error: 'ドキュメントは最低100文字以上である必要があります' }),
      { status: 400 }
    );
  }

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

  const analyzer = createDocumentAnalyzer();

  (async () => {
    try {
      const eventStream = await analyzer.stream({
        document,
        chunks: [],
        metadata: null,
        summary: '',
        insights: [],
        messages: [],
      });

      for await (const event of eventStream) {
        const update = {
          type: 'update',
          node: Object.keys(event)[0],
          data: event,
          timestamp: new Date().toISOString(),
        };

        await writer.write(
          encoder.encode(`data: ${JSON.stringify(update)}\n\n`)
        );
      }

      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: error instanceof Error ? error.message : '不明なエラー'
        })}\n\n`)
      );
    } finally {
      await writer.close();
    }
  })();

  return new Response(stream.readable, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

Server-Sent Eventsを使用して各ノードの実行結果をリアルタイムでストリーミングします。

3. エラー回復機能付きの高度なチェーン

// lib/chains/resilient-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { RunnableSequence, RunnableWithFallbacks } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { retry, delay } from 'es-toolkit';
import { kv } from '@vercel/kv';

interface ChainCache {
  get: (key: string) => Promise<string | null>;
  set: (key: string, value: string, ttl?: number) => Promise<void>;
}

class VercelKVCache implements ChainCache {
  async get(key: string): Promise<string | null> {
    try {
      return await kv.get(key);
    } catch {
      return null;
    }
  }

  async set(key: string, value: string, ttl = 3600): Promise<void> {
    try {
      await kv.set(key, value, { ex: ttl });
    } catch {
      // キャッシュ書き込みエラーは静かに無視
    }
  }
}

export function createResilientChain() {
  const cache = new VercelKVCache();

  // フォールバック付きプライマリモデル
  const primaryModel = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-pro',
    temperature: 0,
    maxRetries: 2,
  });

  const fallbackModel = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-flash',
    temperature: 0,
    maxRetries: 1,
  });

  const modelWithFallback = primaryModel.withFallbacks({
    fallbacks: [fallbackModel],
  });

  // キャッシュ付きプロンプト実行器を作成
  const cachedExecutor = async (prompt: string, input: any) => {
    const cacheKey = `chain:${prompt.substring(0, 20)}:${JSON.stringify(input)}`;

    // キャッシュをチェック
    const cached = await cache.get(cacheKey);
    if (cached) return cached;

    // リトライ論理で実行
    const result = await retry(
      async () => {
        const template = PromptTemplate.fromTemplate(prompt);
        const chain = template.pipe(modelWithFallback).pipe(new StringOutputParser());
        return await chain.invoke(input);
      },
      {
        times: 3,
        delay: 1000,
        onError: async (error, attemptNumber) => {
          console.error(`試行${attemptNumber}が失敗しました:`, error);
          await delay(attemptNumber * 1000); // 指数バックオフ
        },
      }
    );

    // 結果をキャッシュ
    await cache.set(cacheKey, result);
    return result;
  };

  // 回復力のあるチェーンを構築
  const chain = RunnableSequence.from([
    async (input: { query: string }) => {
      // ステップ1: キャッシュ付きクエリ理解
      const understanding = await cachedExecutor(
        'このクエリを明確化のために言い換えてください: {query}',
        input
      );
      return { understanding };
    },
    async (state: { understanding: string }) => {
      // ステップ2: フォールバック付きリサーチ
      const research = await cachedExecutor(
        '以下について詳細な調査を提供してください: {understanding}',
        state
      );
      return { ...state, research };
    },
    async (state: { understanding: string; research: string }) => {
      // ステップ3: 検証付き合成
      const synthesis = await cachedExecutor(
        `この調査を実行可能な洞察に合成してください:
        トピック: {understanding}
        調査: {research}`,
        state
      );

      // 出力を検証
      if (synthesis.length < 100) {
        throw new Error('合成が短すぎます、再試行中...');
      }

      return { ...state, synthesis };
    },
  ]);

  return chain;
}

es-toolkitユーティリティを使用したキャッシュ、フォールバック、リトライロジックを備えた本格的なチェーンを実装します。

4. 並列チェーン処理

// lib/chains/parallel-processor.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { RunnableParallel, RunnableSequence } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { chunk, uniqBy } from 'es-toolkit';

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

  // 並列分析ブランチを定義
  const sentimentAnalysis = PromptTemplate.fromTemplate(
    'このテキストの感情を分析してください(ポジティブ/ネガティブ/ニュートラル): {text}'
  ).pipe(model).pipe(new StringOutputParser());

  const entityExtraction = PromptTemplate.fromTemplate(
    '次のテキストからすべての固有名詞(人物、場所、組織)を抽出してください: {text}'
  ).pipe(model).pipe(new StringOutputParser());

  const topicClassification = PromptTemplate.fromTemplate(
    'このテキストの主要なトピックを分類してください: {text}'
  ).pipe(model).pipe(new StringOutputParser());

  const keywordExtraction = PromptTemplate.fromTemplate(
    'このテキストから5-10個のキーワードを抽出してください: {text}'
  ).pipe(model).pipe(new StringOutputParser());

  // 並列実行を作成
  const parallelAnalysis = RunnableParallel({
    sentiment: sentimentAnalysis,
    entities: entityExtraction,
    topic: topicClassification,
    keywords: keywordExtraction,
  });

  // 合成ステップ
  const synthesisPrompt = PromptTemplate.fromTemplate(`
    以下に基づいて包括的な分析レポートを作成してください:

    感情: {sentiment}
    固有名詞: {entities}
    トピック: {topic}
    キーワード: {keywords}

    セクション付きの構造化されたレポートとしてフォーマットしてください。
  `);

  // 完全なチェーン:並列分析後に合成
  const chain = RunnableSequence.from([
    parallelAnalysis,
    synthesisPrompt,
    model,
    new StringOutputParser(),
  ]);

  return chain;
}

// 複数ドキュメント用バッチプロセッサ
export async function processBatch(documents: string[]) {
  const processor = createParallelProcessor();
  const BATCH_SIZE = 5;

  // レート制限を避けるためにバッチで処理
  const batches = chunk(documents, BATCH_SIZE);
  const results = [];

  for (const batch of batches) {
    const batchResults = await Promise.all(
      batch.map(async (doc) => {
        try {
          const result = await processor.invoke({ text: doc });
          return { success: true, result, document: doc };
        } catch (error) {
          return {
            success: false,
            error: error instanceof Error ? error.message : '不明なエラー',
            document: doc
          };
        }
      })
    );
    results.push(...batchResults);

    // バッチ間のレート制限遅延
    if (batches.indexOf(batch) < batches.length - 1) {
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }

  return results;
}

複数の分析タスクを並列実行後に結果を合成し、バッチ処理サポートを提供します。

5. メモリ機能付きコンテキスト対応チェーン

// lib/chains/contextual-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BufferMemory } from 'langchain/memory';
import { ConversationChain } from 'langchain/chains';
import { PromptTemplate } from '@langchain/core/prompts';
import { MessagesPlaceholder } from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';

export class ContextualChain {
  private memory: BufferMemory;
  private chain: RunnableSequence;
  private model: ChatGoogleGenerativeAI;

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

    this.memory = new BufferMemory({
      returnMessages: true,
      memoryKey: 'history',
      inputKey: 'input',
      outputKey: 'output',
    });

    this.initializeChain();
  }

  private initializeChain() {
    const contextPrompt = PromptTemplate.fromTemplate(`
      あなたはドキュメントを段階的に分析しています。
      前のコンテキスト: {history}

      現在のタスク: {task}
      現在の入力: {input}

      前の分析に基づいて詳細な回答を提供してください。
    `);

    this.chain = RunnableSequence.from([
      async (input: { task: string; input: string }) => {
        const history = await this.memory.loadMemoryVariables({});
        return { ...input, history: history.history || '' };
      },
      contextPrompt,
      this.model,
      async (response) => {
        const content = response.content as string;
        // メモリに保存
        await this.memory.saveContext(
          { input: this.lastInput },
          { output: content }
        );
        return content;
      },
    ]);
  }

  private lastInput: string = '';

  async process(task: string, input: string): Promise<string> {
    this.lastInput = `${task}: ${input}`;
    return await this.chain.invoke({ task, input });
  }

  async reset() {
    await this.memory.clear();
  }

  async getHistory(): Promise<string> {
    const history = await this.memory.loadMemoryVariables({});
    return history.history || '履歴がありません';
  }
}

// APIルートでの使用方法
export async function processWithContext(
  sessionId: string,
  task: string,
  input: string
) {
  // セッションごとにチェーンを保存
  const chainStore = global as any;
  chainStore.contextChains = chainStore.contextChains || new Map();

  if (!chainStore.contextChains.has(sessionId)) {
    chainStore.contextChains.set(sessionId, new ContextualChain());
  }

  const chain = chainStore.contextChains.get(sessionId);
  return await chain.process(task, input);
}

ステートフルなドキュメント分析のために複数のチェーン実行にわたって会話コンテキストを維持します。

6. 監視機能付き本格的なチェーン

// lib/chains/monitored-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { RunnableSequence } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';

interface ChainMetrics {
  executionTime: number;
  tokenCount: number;
  cost: number;
  steps: Array<{
    name: string;
    duration: number;
    tokens: number;
  }>;
}

export class MonitoredChain {
  private metrics: ChainMetrics[] = [];

  async executeWithMonitoring(input: string): Promise<{
    result: string;
    metrics: ChainMetrics;
  }> {
    const startTime = Date.now();
    const stepMetrics: ChainMetrics['steps'] = [];

    const model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-flash',
      temperature: 0,
      callbacks: [
        {
          handleLLMStart: async (llm, prompts) => {
            console.log('LLM開始:', prompts[0].substring(0, 100));
          },
          handleLLMEnd: async (output) => {
            const tokens = output.llmOutput?.tokenUsage?.totalTokens || 0;
            stepMetrics[stepMetrics.length - 1].tokens = tokens;
          },
        },
      ],
    });

    // ステップ1: 分類
    const stepStart = Date.now();
    stepMetrics.push({ name: 'classification', duration: 0, tokens: 0 });

    const classificationPrompt = PromptTemplate.fromTemplate(
      'このテキストをカテゴリに分類してください: {text}'
    );

    const classification = await classificationPrompt
      .pipe(model)
      .pipe(new StringOutputParser())
      .invoke({ text: input });

    stepMetrics[0].duration = Date.now() - stepStart;

    // ステップ2: 強化
    const step2Start = Date.now();
    stepMetrics.push({ name: 'enhancement', duration: 0, tokens: 0 });

    const enhancementPrompt = PromptTemplate.fromTemplate(
      'この分類を詳細で強化してください: {classification}'
    );

    const enhancement = await enhancementPrompt
      .pipe(model)
      .pipe(new StringOutputParser())
      .invoke({ classification });

    stepMetrics[1].duration = Date.now() - step2Start;

    // 総メトリクスを計算
    const totalTokens = stepMetrics.reduce((sum, step) => sum + step.tokens, 0);
    const metrics: ChainMetrics = {
      executionTime: Date.now() - startTime,
      tokenCount: totalTokens,
      cost: totalTokens * 0.000001, // 価格例
      steps: stepMetrics,
    };

    this.metrics.push(metrics);

    return {
      result: enhancement,
      metrics,
    };
  }

  getAverageMetrics(): ChainMetrics | null {
    if (this.metrics.length === 0) return null;

    const avgTime = this.metrics.reduce((sum, m) => sum + m.executionTime, 0) / this.metrics.length;
    const avgTokens = this.metrics.reduce((sum, m) => sum + m.tokenCount, 0) / this.metrics.length;
    const avgCost = this.metrics.reduce((sum, m) => sum + m.cost, 0) / this.metrics.length;

    return {
      executionTime: Math.round(avgTime),
      tokenCount: Math.round(avgTokens),
      cost: avgCost,
      steps: [],
    };
  }
}

実行時間、トークン使用量、コストなど、各チェーン実行の詳細メトリクスを追跡します。

7. チェーン監視用フロントエンドダッシュボード

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

import { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { groupBy, mean, round } from 'es-toolkit';

interface ChainExecution {
  id: string;
  chainName: string;
  status: 'pending' | 'processing' | 'complete' | 'failed';
  startTime: string;
  endTime?: string;
  metrics?: {
    executionTime: number;
    tokenCount: number;
    cost: number;
  };
}

export default function ChainDashboard() {
  const [executions, setExecutions] = useState<ChainExecution[]>([]);

  // 実行履歴を取得
  const { data: history } = useQuery({
    queryKey: ['chain-history'],
    queryFn: async () => {
      const response = await fetch('/api/chain-history');
      return response.json();
    },
    refetchInterval: 5000,
  });

  // 新しいチェーンを実行
  const executeChain = useMutation({
    mutationFn: async (chainConfig: { name: string; input: string }) => {
      const response = await fetch('/api/execute-chain', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(chainConfig),
      });
      return response.json();
    },
    onSuccess: (data) => {
      setExecutions(prev => [...prev, data]);
    },
  });

  // 統計を計算
  const stats = executions.reduce(
    (acc, exec) => {
      if (exec.status === 'complete' && exec.metrics) {
        acc.totalExecutions++;
        acc.totalTokens += exec.metrics.tokenCount;
        acc.totalCost += exec.metrics.cost;
        acc.avgTime = mean([acc.avgTime, exec.metrics.executionTime]);
      } else if (exec.status === 'failed') {
        acc.failures++;
      }
      return acc;
    },
    {
      totalExecutions: 0,
      totalTokens: 0,
      totalCost: 0,
      avgTime: 0,
      failures: 0,
    }
  );

  return (
    <div className="p-6 space-y-6">
      {/* 統計グリッド */}
      <div className="stats shadow w-full">
        <div className="stat">
          <div className="stat-title">総実行数</div>
          <div className="stat-value">{stats.totalExecutions}</div>
          <div className="stat-desc">
            {stats.failures > 0 && `${stats.failures} 失敗`}
          </div>
        </div>

        <div className="stat">
          <div className="stat-title">平均実行時間</div>
          <div className="stat-value">{round(stats.avgTime / 1000, 2)}秒</div>
          <div className="stat-desc">チェーン実行あたり</div>
        </div>

        <div className="stat">
          <div className="stat-title">総トークン数</div>
          <div className="stat-value">{stats.totalTokens.toLocaleString()}</div>
          <div className="stat-desc">
            ¥{round(stats.totalCost * 150, 2)} 総コスト
          </div>
        </div>
      </div>

      {/* 実行タイムライン */}
      <div className="card bg-base-100 shadow-xl">
        <div className="card-body">
          <h2 className="card-title">最近の実行</h2>

          <div className="overflow-x-auto">
            <table className="table table-zebra">
              <thead>
                <tr>
                  <th>チェーン</th>
                  <th>ステータス</th>
                  <th>実行時間</th>
                  <th>トークン</th>
                  <th>コスト</th>
                </tr>
              </thead>
              <tbody>
                {executions.slice(-10).reverse().map((exec) => (
                  <tr key={exec.id}>
                    <td>{exec.chainName}</td>
                    <td>
                      <div className={`badge ${
                        exec.status === 'complete' ? 'badge-success' :
                        exec.status === 'failed' ? 'badge-error' :
                        exec.status === 'processing' ? 'badge-warning' :
                        'badge-ghost'
                      }`}>
                        {exec.status}
                      </div>
                    </td>
                    <td>
                      {exec.metrics
                        ? `${round(exec.metrics.executionTime / 1000, 2)}秒`
                        : '-'}
                    </td>
                    <td>{exec.metrics?.tokenCount || '-'}</td>
                    <td>
                      {exec.metrics
                        ? `¥${round(exec.metrics.cost * 150, 2)}`
                        : '-'}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>

      {/* 新しいチェーンを実行 */}
      <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>
            <select className="select select-bordered">
              <option>基本シーケンシャル</option>
              <option>ドキュメント分析器</option>
              <option>並列プロセッサ</option>
              <option>コンテキストチェーン</option>
            </select>
          </div>

          <button
            className="btn btn-primary"
            onClick={() => executeChain.mutate({
              name: '基本シーケンシャル',
              input: 'サンプル入力テキスト',
            })}
            disabled={executeChain.isPending}
          >
            {executeChain.isPending ? (
              <>
                <span className="loading loading-spinner"></span>
                実行中...
              </>
            ) : (
              'チェーンを実行'
            )}
          </button>
        </div>
      </div>
    </div>
  );
}

統計とコスト追跡機能を備えたチェーン実行のリアルタイム監視ダッシュボード。

まとめ

プロンプトチェーンは、複雑なAIタスクを管理可能で連続的な操作に変換し、独立して最適化、監視、スケールできるようにします。Vercelのサーバーレスプラットフォーム上でLangChainのLCELとLangGraphのステートフルワークフローを使用してこれらのパターンを実装することで、高度な推論タスクを確実に処理する本格的なAIアプリケーションを構築できます。重要なのは、シンプルなチェーンから始めて、アプリケーションのスケールに応じてキャッシュ、フォールバック、監視などの回復力機能を段階的に追加することです。