ドラフト エージェント設計パターン - 学習と適応

agentic-designlangchainlanggraphaitypescriptmachine-learning
By sko X opus 4.19/20/202510 min read

TypeScript、LangGraph、Vercelのサーバーレスプラットフォーム向けに最適化された最新のメモリアーキテクチャを使用して、経験から本当に改善するエージェントを構築する方法を学びます。

メンタルモデル:進化するレストラン

学習エージェントは、顧客に適応するレストランのようなものだと考えてください。最初は、一般的な料理(ベースラインレスポンス)を提供します。時間が経つにつれて、常連客の好み(ユーザー固有の適応)を学習し、人気の組み合わせ(パターン認識)を発見し、フィードバックに基づいてレシピを調整し(強化学習)、過去の類似した顧客に基づいて新規顧客が何を楽しむかを予測することさえできます(転移学習)。成功したレストランが静的なメニューから動的で顧客を意識した体験に進化するように、エージェントは単純な応答者から、相互作用のたびに改善するインテリジェントなシステムに変化します。

基本例:メモリ付き適応チャットエージェント

ユーザーの好みを記憶し、それに応じて応答を適応させる簡単なエージェントを構築しましょう。

// app/api/adaptive-agent/route.ts
import { StateGraph, MemorySaver } from "@langchain/langgraph";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import { Redis } from "@upstash/redis";
import { groupBy, maxBy, sortBy } from "es-toolkit";
import { z } from "zod";

// 適応状態スキーマの定義
const AdaptiveStateSchema = z.object({
  messages: z.array(z.any()),
  userPreferences: z.object({
    style: z.enum(['concise', 'detailed', 'technical']).optional(),
    topics: z.array(z.string()).optional(),
  }).default({}),
  interactionCount: z.number().default(0),
  feedbackScores: z.array(z.number()).default([]),
});

type AdaptiveState = z.infer<typeof AdaptiveStateSchema>;

// 永続メモリ用のRedisの初期化
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});

// 適応エージェントの作成
async function createAdaptiveAgent() {
  const model = new ChatGoogleGenerativeAI({
    temperature: 0.7,
    modelName: "gemini-pro",
  });

  // 会話の永続性のためのメモリセーバー
  const checkpointer = new MemorySaver();

  // エージェントロジックの定義
  async function agentNode(state: AdaptiveState) {
    const { messages, userPreferences } = state;

    // 学習した好みに基づいてプロンプトを適応
    const systemPrompt = `あなたは役立つアシスタントです。
      ${userPreferences.style ? `${userPreferences.style}な方法で応答してください。` : ''}
      ${userPreferences.topics?.length ? `ユーザーは以下に興味があります: ${userPreferences.topics.join(', ')}` : ''}
      相互作用回数: ${state.interactionCount}`;

    const response = await model.invoke([
      { role: "system", content: systemPrompt },
      ...messages
    ]);

    return {
      messages: [...messages, response],
      interactionCount: state.interactionCount + 1,
    };
  }

  // グラフの構築
  const workflow = new StateGraph<AdaptiveState>({
    channels: AdaptiveStateSchema.shape,
  })
    .addNode("agent", agentNode)
    .setEntryPoint("agent")
    .setFinishPoint("agent");

  return workflow.compile({ checkpointer });
}

// APIハンドラー
export async function POST(req: Request) {
  const { message, sessionId, feedback } = await req.json();

  // ユーザー状態の取得または初期化
  const storedState = await redis.get(`session:${sessionId}`) as AdaptiveState | null;
  const initialState: AdaptiveState = storedState || {
    messages: [],
    userPreferences: {},
    interactionCount: 0,
    feedbackScores: [],
  };

  // フィードバックが提供された場合の処理
  if (feedback) {
    initialState.feedbackScores.push(feedback);

    // シンプルな好み学習
    if (initialState.feedbackScores.length > 3) {
      const avgScore = initialState.feedbackScores.reduce((a, b) => a + b) / initialState.feedbackScores.length;
      if (avgScore < 3) {
        initialState.userPreferences.style = 'detailed';
      } else if (avgScore > 4) {
        initialState.userPreferences.style = 'concise';
      }
    }
  }

  // エージェントの実行
  const agent = await createAdaptiveAgent();
  const result = await agent.invoke(
    {
      ...initialState,
      messages: [...initialState.messages, new HumanMessage(message)],
    },
    {
      configurable: { thread_id: sessionId },
    }
  );

  // 更新された状態の保存
  await redis.set(`session:${sessionId}`, result, {
    ex: 86400 * 7, // 7日間のTTL
  });

  return Response.json({
    response: result.messages[result.messages.length - 1].content,
    preferences: result.userPreferences,
    interactionCount: result.interactionCount,
  });
}

この基本的なエージェントは、ユーザーの相互作用を追跡し、フィードバックスコアから学習し、応答スタイルを適応させます。サーバーレス互換の永続ストレージにはUpstash Redisを使用しています。

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

interface ChatResponse {
  response: string;
  preferences: any;
  interactionCount: number;
}

export function AdaptiveChatInterface() {
  const [message, setMessage] = useState('');
  const [sessionId] = useState(() => crypto.randomUUID());
  const [lastResponse, setLastResponse] = useState<string>('');

  const chatMutation = useMutation({
    mutationFn: async (params: { message: string; feedback?: number }) => {
      const response = await fetch('/api/adaptive-agent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ...params, sessionId }),
      });
      return response.json() as Promise<ChatResponse>;
    },
    onSuccess: (data) => {
      setLastResponse(data.response);
    },
  });

  // デバウンスされたフィードバックハンドラー
  const handleFeedback = debounce((score: number) => {
    chatMutation.mutate({ message: '', feedback: score });
  }, 500);

  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">適応チャットエージェント</h2>

        {lastResponse && (
          <div className="alert alert-info">
            <span>{lastResponse}</span>
          </div>
        )}

        <div className="form-control">
          <input
            type="text"
            placeholder="何でも聞いてください..."
            className="input input-bordered"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            onKeyPress={(e) => {
              if (e.key === 'Enter') {
                chatMutation.mutate({ message });
                setMessage('');
              }
            }}
          />
        </div>

        {lastResponse && (
          <div className="rating rating-lg">
            {[1, 2, 3, 4, 5].map((score) => (
              <input
                key={score}
                type="radio"
                name="rating"
                className="mask mask-star-2 bg-orange-400"
                onClick={() => handleFeedback(score)}
              />
            ))}
          </div>
        )}

        <div className="stats shadow">
          <div className="stat">
            <div className="stat-title">相互作用</div>
            <div className="stat-value text-primary">
              {chatMutation.data?.interactionCount || 0}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

フロントエンドは星評価システムを通じてユーザーフィードバックを収集し、相互作用統計を表示して、完全なフィードバックループを作成します。

高度な例:経験再生を備えたマルチエージェント学習システム

次に、共有経験から学習する複数の専門エージェントを備えた洗練されたシステムを構築しましょう。

// lib/memory/experience-store.ts
import { PineconeStore } from "@langchain/pinecone";
import { GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
import { Pinecone } from "@pinecone-database/pinecone";
import { chunk, sortBy, take, groupBy, maxBy } from "es-toolkit";
import { z } from "zod";

// 経験スキーマ
const ExperienceSchema = z.object({
  id: z.string(),
  interaction: z.object({
    input: z.string(),
    context: z.record(z.any()),
    agentType: z.string(),
  }),
  outcome: z.object({
    response: z.string(),
    success: z.boolean(),
    metrics: z.object({
      latency: z.number(),
      tokenCount: z.number(),
      userSatisfaction: z.number().optional(),
    }),
  }),
  timestamp: z.string(),
  embedding: z.array(z.number()).optional(),
});

type Experience = z.infer<typeof ExperienceSchema>;

export class ExperienceReplayBuffer {
  private vectorStore: PineconeStore;
  private embeddings: GoogleGenerativeAIEmbeddings;
  private bufferSize = 1000;
  private priorityAlpha = 0.6; // 優先度指数

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

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

    this.vectorStore = new PineconeStore(this.embeddings, {
      pineconeIndex: pinecone.index(process.env.PINECONE_INDEX!),
      namespace: "experiences",
    });
  }

  async store(experience: Experience): Promise<void> {
    // TDエラーまたは結果メトリックに基づく優先度の計算
    const priority = this.calculatePriority(experience);

    await this.vectorStore.addDocuments([
      {
        pageContent: JSON.stringify({
          interaction: experience.interaction,
          outcome: experience.outcome,
        }),
        metadata: {
          id: experience.id,
          timestamp: experience.timestamp,
          priority,
          agentType: experience.interaction.agentType,
          success: experience.outcome.success,
          userSatisfaction: experience.outcome.metrics.userSatisfaction,
        },
      },
    ]);

    // バッファサイズの維持
    await this.pruneOldExperiences();
  }

  async sample(
    context: any,
    k: number = 5,
    strategy: 'uniform' | 'prioritized' = 'prioritized'
  ): Promise<Experience[]> {
    const query = JSON.stringify(context);
    const results = await this.vectorStore.similaritySearch(query, k * 2);

    if (strategy === 'prioritized') {
      // 優先経験再生
      const experiences = results.map(doc => ({
        ...JSON.parse(doc.pageContent),
        priority: doc.metadata.priority || 1,
      }));

      // 優先度に基づくサンプル
      const sorted = sortBy(experiences, exp => -exp.priority);
      return take(sorted, k);
    }

    return take(results.map(doc => JSON.parse(doc.pageContent)), k);
  }

  private calculatePriority(experience: Experience): number {
    // 予想外の結果に高い優先度
    const successWeight = experience.outcome.success ? 0.3 : 0.7;
    const satisfactionWeight = experience.outcome.metrics.userSatisfaction
      ? (5 - experience.outcome.metrics.userSatisfaction) / 5
      : 0.5;

    return Math.pow(successWeight + satisfactionWeight, this.priorityAlpha);
  }

  private async pruneOldExperiences(): Promise<void> {
    // バッファサイズを維持するための実装
    // 制限を超えた場合、最も古い経験を削除
  }
}

// lib/agents/specialized-agents.ts
import { StateGraph } from "@langchain/langgraph";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { ExperienceReplayBuffer } from "./memory/experience-store";
import { filter, map, reduce } from "es-toolkit";

interface LearningAgentState {
  messages: any[];
  experiences: any[];
  adaptationMetrics: {
    successRate: number;
    avgLatency: number;
    confidenceScore: number;
  };
}

class SpecializedLearningAgent {
  private model: ChatGoogleGenerativeAI;
  private experienceBuffer: ExperienceReplayBuffer;
  private agentType: string;
  private learningRate = 0.1;

  constructor(agentType: 'researcher' | 'coder' | 'analyst') {
    this.agentType = agentType;
    this.model = new ChatGoogleGenerativeAI({
      temperature: 0.3,
      modelName: "gemini-pro",
    });
    this.experienceBuffer = new ExperienceReplayBuffer();
  }

  async createWorkflow() {
    const workflow = new StateGraph<LearningAgentState>({
      channels: {
        messages: { value: (x: any[], y: any[]) => [...x, ...y] },
        experiences: { value: (x: any[], y: any[]) => [...x, ...y] },
        adaptationMetrics: {
          value: (x: any, y: any) => ({ ...x, ...y })
        },
      },
    });

    // 関連する経験の取得
    workflow.addNode("retrieve_experiences", async (state) => {
      const context = state.messages[state.messages.length - 1];
      const experiences = await this.experienceBuffer.sample(context, 5);

      return { experiences };
    });

    // 経験に基づく動作の適応
    workflow.addNode("adapt_strategy", async (state) => {
      const successfulExperiences = filter(
        state.experiences,
        exp => exp.outcome.success
      );

      // 適応メトリックの計算
      const successRate = successfulExperiences.length / state.experiences.length;
      const avgLatency = reduce(
        state.experiences,
        (acc, exp) => acc + exp.outcome.metrics.latency,
        0
      ) / state.experiences.length;

      // 最適な戦略の決定
      const strategy = this.determineStrategy(successfulExperiences);

      return {
        adaptationMetrics: {
          successRate,
          avgLatency,
          confidenceScore: this.calculateConfidence(state.experiences),
        },
      };
    });

    // 学習した動作での実行
    workflow.addNode("execute", async (state) => {
      const systemPrompt = this.buildAdaptivePrompt(
        state.experiences,
        state.adaptationMetrics
      );

      const startTime = Date.now();
      const response = await this.model.invoke([
        { role: "system", content: systemPrompt },
        ...state.messages,
      ]);
      const latency = Date.now() - startTime;

      // この相互作用を新しい経験として保存
      const experience = {
        id: crypto.randomUUID(),
        interaction: {
          input: state.messages[state.messages.length - 1].content,
          context: state.adaptationMetrics,
          agentType: this.agentType,
        },
        outcome: {
          response: response.content,
          success: true, // フィードバックに基づいて更新される
          metrics: {
            latency,
            tokenCount: response.usage?.total_tokens || 0,
          },
        },
        timestamp: new Date().toISOString(),
      };

      await this.experienceBuffer.store(experience);

      return {
        messages: [response],
      };
    });

    // ノードの接続
    workflow
      .addEdge("retrieve_experiences", "adapt_strategy")
      .addEdge("adapt_strategy", "execute")
      .setEntryPoint("retrieve_experiences")
      .setFinishPoint("execute");

    return workflow.compile();
  }

  private determineStrategy(experiences: any[]): string {
    // パターンごとに経験をグループ化
    const patterns = groupBy(experiences, exp =>
      exp.interaction.input.split(' ')[0].toLowerCase()
    );

    // 最も成功したパターンを見つける
    const bestPattern = maxBy(
      Object.entries(patterns),
      ([_, exps]) => filter(exps, e => e.outcome.success).length
    );

    return bestPattern ? bestPattern[0] : 'default';
  }

  private calculateConfidence(experiences: any[]): number {
    if (experiences.length === 0) return 0.5;

    const weights = experiences.map((_, idx) =>
      Math.exp(-idx * 0.5) // 最近性のための指数減衰
    );

    const weightedSuccess = reduce(
      experiences,
      (acc, exp, idx) => acc + (exp.outcome.success ? weights[idx] : 0),
      0
    );

    return weightedSuccess / reduce(weights, (a, b) => a + b, 0);
  }

  private buildAdaptivePrompt(
    experiences: any[],
    metrics: any
  ): string {
    const successfulPatterns = filter(
      experiences,
      exp => exp.outcome.success
    ).map(exp => exp.interaction.input);

    return `あなたは専門の${this.agentType}エージェントです。

      過去の相互作用に基づいて:
      - 成功率: ${(metrics.successRate * 100).toFixed(1)}%
      - 平均応答時間: ${metrics.avgLatency}ms
      - 信頼レベル: ${(metrics.confidenceScore * 100).toFixed(1)}%

      観察された成功パターン:
      ${successfulPatterns.slice(0, 3).join('\n')}

      これらの学習に基づいて、応答スタイルとアプローチを適応させてください。`;
  }
}

// lib/agents/multi-agent-coordinator.ts
export class MultiAgentLearningCoordinator {
  private agents: Map<string, SpecializedLearningAgent>;
  private routingModel: ChatGoogleGenerativeAI;

  constructor() {
    this.agents = new Map([
      ['researcher', new SpecializedLearningAgent('researcher')],
      ['coder', new SpecializedLearningAgent('coder')],
      ['analyst', new SpecializedLearningAgent('analyst')],
    ]);

    this.routingModel = new ChatGoogleGenerativeAI({
      temperature: 0,
      modelName: "gemini-pro",
    });
  }

  async route(input: string): Promise<string> {
    const routingPrompt = `次のリクエストを処理すべき専門エージェントを決定してください:
      - researcher: 情報収集、事実確認、研究タスク用
      - coder: プログラミング、デバッグ、コード生成用
      - analyst: データ分析、洞察、戦略計画用

      入力: "${input}"

      エージェント名のみで応答してください。`;

    const response = await this.routingModel.invoke(routingPrompt);
    return response.content.trim().toLowerCase();
  }

  async process(input: string, sessionId: string): Promise<any> {
    // 使用するエージェントの決定
    const agentType = await this.route(input);
    const agent = this.agents.get(agentType);

    if (!agent) {
      throw new Error(`不明なエージェントタイプ: ${agentType}`);
    }

    const workflow = await agent.createWorkflow();
    const result = await workflow.invoke({
      messages: [{ role: "user", content: input }],
      experiences: [],
      adaptationMetrics: {
        successRate: 0.5,
        avgLatency: 0,
        confidenceScore: 0.5,
      },
    });

    return {
      response: result.messages[result.messages.length - 1].content,
      agentType,
      metrics: result.adaptationMetrics,
    };
  }
}

// app/api/learning-system/route.ts
import { MultiAgentLearningCoordinator } from "@/lib/agents/multi-agent-coordinator";
import { NextRequest } from "next/server";

const coordinator = new MultiAgentLearningCoordinator();

export async function POST(req: NextRequest) {
  // サーバーレス用のバックグラウンドコールバックを無効化
  process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";

  const { message, sessionId, feedback } = await req.json();

  try {
    const result = await coordinator.process(message, sessionId);

    return Response.json({
      success: true,
      ...result,
    });
  } catch (error) {
    console.error("学習システムエラー:", error);
    return Response.json(
      { success: false, error: "処理に失敗しました" },
      { status: 500 }
    );
  }
}

この高度なシステムは、優先経験再生、マルチエージェント調整、および履歴パフォーマンスに基づく適応戦略選択を実装しています。

// components/LearningSystemDashboard.tsx
import { useState } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { groupBy, sortBy } from 'es-toolkit';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';

interface SystemMetrics {
  agentType: string;
  metrics: {
    successRate: number;
    avgLatency: number;
    confidenceScore: number;
  };
}

export function LearningSystemDashboard() {
  const [sessionId] = useState(() => crypto.randomUUID());
  const [history, setHistory] = useState<SystemMetrics[]>([]);

  const sendMessage = useMutation({
    mutationFn: async (message: string) => {
      const response = await fetch('/api/learning-system', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message, sessionId }),
      });
      return response.json();
    },
    onSuccess: (data) => {
      setHistory(prev => [...prev, {
        agentType: data.agentType,
        metrics: data.metrics,
      }]);
    },
  });

  // パフォーマンストレンドの計算
  const performanceData = history.map((item, index) => ({
    interaction: index + 1,
    confidence: item.metrics.confidenceScore * 100,
    success: item.metrics.successRate * 100,
  }));

  // エージェント使用状況の分布
  const agentUsage = Object.entries(
    groupBy(history, item => item.agentType)
  ).map(([agent, items]) => ({
    agent,
    count: items.length,
    avgConfidence: items.reduce((acc, item) =>
      acc + item.metrics.confidenceScore, 0
    ) / items.length * 100,
  }));

  return (
    <div className="container mx-auto p-4">
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {/* チャットインターフェース */}
        <div className="card bg-base-100 shadow-xl">
          <div className="card-body">
            <h2 className="card-title">マルチエージェント学習システム</h2>

            <div className="form-control">
              <div className="input-group">
                <input
                  type="text"
                  placeholder="何でも聞いてください..."
                  className="input input-bordered flex-1"
                  onKeyPress={(e) => {
                    if (e.key === 'Enter') {
                      sendMessage.mutate((e.target as HTMLInputElement).value);
                      (e.target as HTMLInputElement).value = '';
                    }
                  }}
                />
                <button
                  className="btn btn-primary"
                  onClick={() => {
                    const input = document.querySelector('input');
                    if (input?.value) {
                      sendMessage.mutate(input.value);
                      input.value = '';
                    }
                  }}
                >
                  送信
                </button>
              </div>
            </div>

            {sendMessage.data && (
              <div className="alert alert-info mt-4">
                <div>
                  <span className="badge badge-secondary mr-2">
                    {sendMessage.data.agentType}
                  </span>
                  {sendMessage.data.response}
                </div>
              </div>
            )}
          </div>
        </div>

        {/* パフォーマンスメトリック */}
        <div className="card bg-base-100 shadow-xl">
          <div className="card-body">
            <h2 className="card-title">学習の進捗</h2>

            {performanceData.length > 0 && (
              <LineChart width={400} height={200} data={performanceData}>
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="interaction" />
                <YAxis />
                <Tooltip />
                <Legend />
                <Line
                  type="monotone"
                  dataKey="confidence"
                  stroke="#8884d8"
                  name="信頼度 %"
                />
                <Line
                  type="monotone"
                  dataKey="success"
                  stroke="#82ca9d"
                  name="成功率 %"
                />
              </LineChart>
            )}
          </div>
        </div>

        {/* エージェント統計 */}
        <div className="card bg-base-100 shadow-xl lg:col-span-2">
          <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>
                  </tr>
                </thead>
                <tbody>
                  {agentUsage.map(agent => (
                    <tr key={agent.agent}>
                      <td className="font-bold">{agent.agent}</td>
                      <td>{agent.count}</td>
                      <td>
                        <progress
                          className="progress progress-primary w-32"
                          value={agent.avgConfidence}
                          max="100"
                        />
                        <span className="ml-2">{agent.avgConfidence.toFixed(1)}%</span>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

ダッシュボードは、学習の進捗、エージェントパフォーマンスメトリック、およびシステム信頼度トレンドのリアルタイム可視化を提供します。

結論

学習と適応により、静的なLLMアプリケーションが相互作用のたびに改善する動的なシステムに変わります。LangGraphの状態的オーケストレーション、ベクターベースの経験再生、軽量な強化学習技術を組み合わせることで、Vercelのようなサーバーレスプラットフォームで優れたパフォーマンスを維持しながら、経験から本当に学習するエージェントを構築できます。シンプルな好み学習から始めて、システムが成熟するにつれて、優先経験再生やマルチエージェント調整などの洗練された機能を段階的に追加してください。