ドラフト エージェント設計パターン - ヒューマン・イン・ザ・ループ

ailangchainlanggraphhitltypescriptvercel
By sko X opus 4.19/21/202511 min read

必要な時に人間の助けを求めることができるインテリジェントエージェントの構築。このガイドでは、TypeScriptでLangGraphとLangChainを使用し、Vercelのサーバーレスプラットフォーム上でヒューマン・イン・ザ・ループ(HITL)パターンを実装する方法を示し、エージェントが重要な意思決定において人間とシームレスに協力できるようにします。

メンタルモデル:航空管制パターン

HITLを繁忙な空港の航空管制に例えて考えてみましょう。AIエージェントは自動操縦システムのようなもので、何千もの操作を管理しながら、ルーチンフライトを効率的に処理します。しかし、嵐が来たり、緊急事態が発生したり、複雑な状況が生じたりすると、人間の管制官が介入します。システムは停止することなく、すべてのコンテキストを維持しながら制御をシームレスに移行します。サーバーレス環境では、これがさらに重要になります。Vercelの実行制限などのプラットフォーム制約内で、ワークフローを一時停止し、状態を保存し、人間に通知し、実行を再開する必要があります。

基本例:承認ベースのカスタマーサポートエージェント

1. HITL依存関係のインストール

npm install @langchain/langgraph @langchain/core @langchain/google-genai
npm install @upstash/redis @upstash/qstash uuid
npm install zod es-toolkit

ワークフローオーケストレーション用のLangGraph、状態の永続化とキュー管理用のUpstash、ユーティリティ関数用のes-toolkitを追加します。

2. 中断機能を持つ基本的なHITLワークフローの作成

// lib/hitl-workflow.ts
import { MemorySaver, Annotation, interrupt, Command, StateGraph } from "@langchain/langgraph";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import { Redis } from "@upstash/redis";
import { debounce } from "es-toolkit";

const redis = Redis.fromEnv();

// ワークフロー状態の定義
const StateAnnotation = Annotation.Root({
  messages: Annotation<Array<HumanMessage | AIMessage>>({
    reducer: (curr, prev) => [...prev, ...curr],
    default: () => []
  }),
  customerQuery: Annotation<string>(),
  agentResponse: Annotation<string>(),
  humanFeedback: Annotation<string>().optional(),
  approved: Annotation<boolean>().optional()
});

// AI処理ノード
async function processQuery(state: typeof StateAnnotation.State) {
  const model = new ChatGoogleGenerativeAI({
    modelName: "gemini-2.5-flash",
    temperature: 0.3
  });

  const response = await model.invoke(state.messages);

  return {
    agentResponse: response.content as string,
    messages: [response]
  };
}

// 中断機能を持つ人間承認ノード
async function humanApproval(state: typeof StateAnnotation.State): Promise<Command> {
  // センシティブなアクションが含まれているかチェック
  const sensitiveKeywords = ["refund", "cancel", "delete", "payment"];
  const needsApproval = sensitiveKeywords.some(keyword =>
    state.agentResponse.toLowerCase().includes(keyword)
  );

  if (!needsApproval) {
    return new Command({
      goto: "send_response",
      update: { approved: true }
    });
  }

  // 人間のレビューのためにワークフローを中断
  const decision = await interrupt({
    question: "この応答を承認しますか?",
    agentResponse: state.agentResponse,
    customerQuery: state.customerQuery,
    requiresApproval: true
  });

  if (decision.approved) {
    return new Command({
      goto: "send_response",
      update: {
        approved: true,
        humanFeedback: decision.feedback
      }
    });
  }

  return new Command({
    goto: "revise_response",
    update: {
      approved: false,
      humanFeedback: decision.feedback
    }
  });
}

// ワークフローの作成とコンパイル
export function createHITLWorkflow() {
  const workflow = new StateGraph(StateAnnotation)
    .addNode("process_query", processQuery)
    .addNode("human_approval", humanApproval)
    .addNode("send_response", sendResponse)
    .addNode("revise_response", reviseResponse)
    .addEdge("process_query", "human_approval")
    .addConditionalEdges("human_approval", (state) => {
      return state.approved ? "send_response" : "revise_response";
    })
    .addEdge("revise_response", "human_approval");

  const checkpointer = new MemorySaver();
  return workflow.compile({ checkpointer });
}

センシティブなアクションを含むAI応答が人間のレビューをトリガーし、非同期承認のための状態永続化を持つワークフローを作成します。

3. HITL用のサーバーレスAPIルート

// app/api/hitl/chat/route.ts
import { NextRequest } from "next/server";
import { createHITLWorkflow } from "@/lib/hitl-workflow";
import { QStash } from "@upstash/qstash";
import { v4 as uuidv4 } from "uuid";

export const maxDuration = 10; // Hobbyプランの制限

const qstash = new QStash({ token: process.env.QSTASH_TOKEN! });

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

  const workflow = createHITLWorkflow();
  const config = {
    configurable: {
      thread_id: sessionId,
      checkpoint_ns: "hitl"
    }
  };

  // ワークフロー実行の開始
  const initialState = {
    customerQuery: message,
    messages: [{ role: "human", content: message }]
  };

  // ワークフロー実行をキューに入れる
  await qstash.publishJSON({
    url: `${process.env.VERCEL_URL}/api/hitl/process`,
    body: {
      sessionId,
      state: initialState,
      config
    },
    retries: 3
  });

  return Response.json({
    sessionId,
    status: "processing",
    message: "リクエストを処理中です"
  });
}

// app/api/hitl/resume/route.ts
export async function POST(req: NextRequest) {
  const { sessionId, decision } = await req.json();

  const workflow = createHITLWorkflow();
  const config = {
    configurable: {
      thread_id: sessionId,
      checkpoint_ns: "hitl"
    }
  };

  // 中断から再開
  const result = await workflow.invoke(
    new Command({ resume: decision }),
    config
  );

  return Response.json(result);
}

Vercelの実行制限に対処するためのキューベースの処理を実装し、ワークフローの開始と再開のための個別のエンドポイントを提供します。

4. SSEによるリアルタイムステータス更新

// app/api/hitl/status/[sessionId]/route.ts
import { NextRequest } from "next/server";
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

export async function GET(
  req: NextRequest,
  { params }: { params: { sessionId: string }}
) {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      const sendUpdate = async () => {
        const state = await redis.hgetall(`session:${params.sessionId}`);

        if (state?.interrupt) {
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({
              type: "approval_needed",
              data: state.interrupt
            })}\n\n`)
          );
        }

        if (state?.status === "completed") {
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({
              type: "completed",
              response: state.response
            })}\n\n`)
          );
          controller.close();
        } else {
          setTimeout(sendUpdate, 1000);
        }
      };

      await sendUpdate();
    }
  });

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

Server-Sent Eventsを使用して、ワークフローのステータスと承認リクエストについてクライアントにリアルタイム更新を提供します。

5. 人間承認用のReact UI

// components/HITLInterface.tsx
"use client";

import { useState, useEffect } from "react";
import { useMutation } from "@tanstack/react-query";

export default function HITLInterface({ sessionId }: { sessionId: string }) {
  const [approvalRequest, setApprovalRequest] = useState(null);
  const [status, setStatus] = useState("waiting");

  // 承認リクエストをリッスン
  useEffect(() => {
    const eventSource = new EventSource(`/api/hitl/status/${sessionId}`);

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);

      if (data.type === "approval_needed") {
        setApprovalRequest(data.data);
        setStatus("pending_approval");
      } else if (data.type === "completed") {
        setStatus("completed");
      }
    };

    return () => eventSource.close();
  }, [sessionId]);

  const approveMutation = useMutation({
    mutationFn: async (decision: any) => {
      const response = await fetch("/api/hitl/resume", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ sessionId, decision })
      });
      return response.json();
    }
  });

  if (status === "pending_approval" && approvalRequest) {
    return (
      <div className="card bg-base-100 shadow-xl">
        <div className="card-body">
          <h2 className="card-title">人間の承認が必要です</h2>

          <div className="divider">顧客の問い合わせ</div>
          <p className="text-sm">{approvalRequest.customerQuery}</p>

          <div className="divider">エージェントの応答</div>
          <div className="bg-base-200 p-4 rounded">
            <p>{approvalRequest.agentResponse}</p>
          </div>

          <div className="form-control">
            <label className="label">
              <span className="label-text">フィードバック(オプション)</span>
            </label>
            <textarea
              className="textarea textarea-bordered"
              placeholder="修正のためのフィードバックを追加..."
              id="feedback"
            />
          </div>

          <div className="card-actions justify-end mt-4">
            <button
              className="btn btn-error"
              onClick={() => {
                const feedback = document.getElementById("feedback").value;
                approveMutation.mutate({
                  approved: false,
                  feedback
                });
              }}
            >
              却下して修正
            </button>
            <button
              className="btn btn-success"
              onClick={() => approveMutation.mutate({ approved: true })}
            >
              承認
            </button>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="alert alert-info">
      <span>ステータス: {status}</span>
    </div>
  );
}

人間のレビュアーがエージェントの応答を承認または却下するための直感的なインターフェースを提供するReactコンポーネント。

高度な例:エスカレーションパターンを持つマルチエージェントシステム

1. フォールバック付き複雑なHITLアーキテクチャ

// lib/advanced-hitl-system.ts
import { StateGraph, Annotation, Command, interrupt } from "@langchain/langgraph";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { Redis } from "@upstash/redis";
import { QStash } from "@upstash/qstash";
import { groupBy, sortBy, pick } from "es-toolkit";
import { z } from "zod";

const redis = Redis.fromEnv();
const qstash = new QStash({ token: process.env.QSTASH_TOKEN! });

// 信頼度とエスカレーション追跡を含む拡張状態
const EnhancedStateAnnotation = Annotation.Root({
  query: Annotation<string>(),
  context: Annotation<Record<string, any>>(),
  agentResponse: Annotation<string>(),
  confidence: Annotation<number>(),
  escalationLevel: Annotation<number>({ default: () => 0 }),
  reviewHistory: Annotation<Array<{
    level: number;
    reviewer: string;
    decision: string;
    feedback?: string;
    timestamp: Date;
  }>>({ default: () => [] }),
  finalResponse: Annotation<string>().optional()
});

// 信頼度評価ノード
async function assessConfidence(state: typeof EnhancedStateAnnotation.State) {
  const model = new ChatGoogleGenerativeAI({
    modelName: "gemini-2.5-pro",
    temperature: 0
  });

  const prompt = `
    この応答の信頼度レベルを評価してください。
    クエリ: ${state.query}
    応答: ${state.agentResponse}

    0-1の信頼度スコアと、リスクを特定してください。
    形式: { "confidence": 0.X, "risks": ["risk1", "risk2"] }
  `;

  const result = await model.invoke(prompt);
  const assessment = JSON.parse(result.content as string);

  return {
    confidence: assessment.confidence,
    context: { ...state.context, risks: assessment.risks }
  };
}

// 階層的エスカレーションシステム
class EscalationManager {
  private levels = [
    { threshold: 0.8, handler: "auto_approve", timeout: 0 },
    { threshold: 0.6, handler: "peer_review", timeout: 300000 }, // 5分
    { threshold: 0.4, handler: "supervisor_review", timeout: 600000 }, // 10分
    { threshold: 0.0, handler: "expert_panel", timeout: 1800000 } // 30分
  ];

  async determineEscalation(confidence: number, currentLevel: number) {
    const appropriateLevel = sortBy(
      this.levels.filter(l => confidence <= l.threshold),
      l => -l.threshold
    )[0];

    if (!appropriateLevel) return "auto_approve";

    // さらにエスカレーションが必要かチェック
    const levelIndex = this.levels.findIndex(
      l => l.handler === appropriateLevel.handler
    );

    if (levelIndex > currentLevel) {
      await this.notifyReviewers(appropriateLevel.handler);
      return appropriateLevel.handler;
    }

    return appropriateLevel.handler;
  }

  private async notifyReviewers(level: string) {
    // 緊急度に基づいて異なるチャネルで通知を送信
    const notifications = {
      peer_review: { channel: "slack", urgency: "normal" },
      supervisor_review: { channel: "slack", urgency: "high" },
      expert_panel: { channel: "pagerduty", urgency: "critical" }
    };

    const config = notifications[level];
    if (config) {
      await qstash.publishJSON({
        url: `${process.env.NOTIFICATION_WEBHOOK}`,
        body: {
          level,
          channel: config.channel,
          urgency: config.urgency,
          timestamp: new Date()
        }
      });
    }
  }
}

// タイムアウトとフォールバックを持つ人間レビュー
async function humanReviewWithFallback(
  state: typeof EnhancedStateAnnotation.State
): Promise<Command> {
  const escalationManager = new EscalationManager();
  const handler = await escalationManager.determineEscalation(
    state.confidence,
    state.escalationLevel
  );

  if (handler === "auto_approve") {
    return new Command({
      goto: "finalize_response",
      update: { finalResponse: state.agentResponse }
    });
  }

  // 人間の応答のタイムアウトを設定
  const timeoutMs = 300000; // デフォルト5分
  const startTime = Date.now();

  try {
    const decision = await Promise.race([
      interrupt({
        level: handler,
        query: state.query,
        response: state.agentResponse,
        confidence: state.confidence,
        risks: state.context.risks
      }),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error("Timeout")), timeoutMs)
      )
    ]);

    // レビュー履歴を記録
    const reviewRecord = {
      level: state.escalationLevel,
      reviewer: decision.reviewer || handler,
      decision: decision.approved ? "approved" : "rejected",
      feedback: decision.feedback,
      timestamp: new Date()
    };

    if (decision.approved) {
      return new Command({
        goto: "finalize_response",
        update: {
          finalResponse: decision.editedResponse || state.agentResponse,
          reviewHistory: [...state.reviewHistory, reviewRecord]
        }
      });
    } else {
      return new Command({
        goto: "revise_with_feedback",
        update: {
          reviewHistory: [...state.reviewHistory, reviewRecord],
          escalationLevel: state.escalationLevel + 1
        }
      });
    }
  } catch (error) {
    // タイムアウトが発生 - フォールバックを実装
    console.warn(`レベル${handler}でレビュータイムアウト`);

    if (state.escalationLevel < 2) {
      // 次のレベルにエスカレート
      return new Command({
        goto: "human_review",
        update: { escalationLevel: state.escalationLevel + 1 }
      });
    } else {
      // 最終フォールバック - 安全なデフォルトを使用
      return new Command({
        goto: "apply_safe_default",
        update: {
          finalResponse: "正確な応答を提供するためにもう少し時間が必要です。専門家が間もなくご連絡します。"
        }
      });
    }
  }
}

// 高度なワークフローの作成
export function createAdvancedHITLWorkflow() {
  const workflow = new StateGraph(EnhancedStateAnnotation)
    .addNode("generate_response", generateResponse)
    .addNode("assess_confidence", assessConfidence)
    .addNode("human_review", humanReviewWithFallback)
    .addNode("revise_with_feedback", reviseWithFeedback)
    .addNode("apply_safe_default", applySafeDefault)
    .addNode("finalize_response", finalizeResponse)
    .addEdge("generate_response", "assess_confidence")
    .addEdge("assess_confidence", "human_review")
    .addEdge("revise_with_feedback", "assess_confidence")
    .addConditionalEdges("human_review", (state) => {
      if (state.finalResponse) return "finalize_response";
      if (state.escalationLevel > 2) return "apply_safe_default";
      return "human_review";
    });

  return workflow.compile({
    checkpointer: new MemorySaver()
  });
}

信頼度スコアに基づいたタイムアウト、フォールバック、およびマルチレベルレビューを含む洗練されたエスカレーション階層を実装します。

2. スケール用の分散HITL処理

// lib/distributed-hitl.ts
import { QStash } from "@upstash/qstash";
import { Redis } from "@upstash/redis";
import { chunk, partition, map } from "es-toolkit";

const redis = Redis.fromEnv();
const qstash = new QStash({ token: process.env.QSTASH_TOKEN! });

export class DistributedHITLProcessor {
  private maxBatchSize = 10;
  private maxConcurrent = 5;

  async processLargeWorkload(tasks: Array<any>, workflowId: string) {
    // 優先度でタスクを分割
    const [highPriority, normalPriority] = partition(
      tasks,
      t => t.priority === "high"
    );

    // 高優先度を即座に処理
    await this.processBatch(highPriority, workflowId, "high");

    // 通常優先度をチャンクでキューに入れる
    const chunks = chunk(normalPriority, this.maxBatchSize);

    for (let i = 0; i < chunks.length; i++) {
      await qstash.publishJSON({
        url: `${process.env.VERCEL_URL}/api/hitl/batch-process`,
        body: {
          workflowId,
          batchId: i,
          tasks: chunks[i]
        },
        delay: i * 5 // 5秒ずつずらす
      });
    }

    // ワークフローメタデータを保存
    await redis.hset(`workflow:${workflowId}`, {
      totalTasks: tasks.length,
      totalBatches: chunks.length,
      startTime: Date.now(),
      status: "processing"
    });
  }

  private async processBatch(
    tasks: Array<any>,
    workflowId: string,
    priority: string
  ) {
    const results = await Promise.allSettled(
      tasks.map(task => this.processTask(task, priority))
    );

    // 結果を保存
    await redis.hset(`workflow:${workflowId}:results`, {
      [`batch_${Date.now()}`]: JSON.stringify(results)
    });

    // 進捗を更新
    await redis.hincrby(`workflow:${workflowId}`, "completed", tasks.length);
  }

  private async processTask(task: any, priority: string) {
    // 優先度に基づいて適切なHITLでタスク処理を実装
    if (priority === "high") {
      // 直接人間レビュー
      return await this.requestImmediateReview(task);
    } else {
      // オプションのエスカレーション付きAI
      return await this.processWithOptionalReview(task);
    }
  }
}

// バッチ処理用APIルート
// app/api/hitl/batch-process/route.ts
export const maxDuration = 300; // Fluid Compute付きProプラン

export async function POST(req: NextRequest) {
  const { workflowId, batchId, tasks } = await req.json();

  const processor = new DistributedHITLProcessor();

  try {
    await processor.processBatch(tasks, workflowId, "normal");

    // すべてのバッチが完了したかチェック
    const workflow = await redis.hgetall(`workflow:${workflowId}`);
    const completed = parseInt(workflow.completed || "0");
    const total = parseInt(workflow.totalTasks || "0");

    if (completed >= total) {
      await redis.hset(`workflow:${workflowId}`, {
        status: "completed",
        endTime: Date.now()
      });

      // 完了を通知
      await notifyCompletion(workflowId);
    }

    return Response.json({ success: true, batchId });
  } catch (error) {
    console.error(`バッチ${batchId}失敗:`, error);

    // リトライロジック
    await qstash.publishJSON({
      url: `${process.env.VERCEL_URL}/api/hitl/batch-process`,
      body: { workflowId, batchId, tasks },
      delay: 60 // 1分後にリトライ
    });

    return Response.json({ error: "処理失敗、リトライ中" });
  }
}

優先度キューイングとバッチ管理を持つ複数のサーバーレス関数にわたってHITL処理をスケールします。

3. HITL管理のためのUIダッシュボード

// components/HITLDashboard.tsx
"use client";

import { useQuery, useMutation } from "@tanstack/react-query";
import { useState, useEffect } from "react";
import { groupBy, sortBy } from "es-toolkit";

interface ReviewTask {
  id: string;
  query: string;
  response: string;
  confidence: number;
  level: string;
  timestamp: Date;
  deadline?: Date;
}

export default function HITLDashboard() {
  const [selectedTask, setSelectedTask] = useState<ReviewTask | null>(null);
  const [filter, setFilter] = useState("all");

  // 保留中のレビューを取得
  const { data: tasks = [], refetch } = useQuery({
    queryKey: ["pending-reviews"],
    queryFn: async () => {
      const response = await fetch("/api/hitl/pending");
      return response.json();
    },
    refetchInterval: 5000 // 5秒ごとにポーリング
  });

  // 緊急度でタスクをグループ化
  const groupedTasks = groupBy(
    sortBy(tasks, t => t.confidence),
    t => {
      if (t.confidence < 0.4) return "critical";
      if (t.confidence < 0.6) return "high";
      if (t.confidence < 0.8) return "medium";
      return "low";
    }
  );

  const reviewMutation = useMutation({
    mutationFn: async (decision: any) => {
      const response = await fetch("/api/hitl/review", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(decision)
      });
      return response.json();
    },
    onSuccess: () => {
      setSelectedTask(null);
      refetch();
    }
  });

  return (
    <div className="container mx-auto p-4">
      <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">
            <div className="card-body">
              <h2 className="card-title">保留中のレビュー</h2>

              {/* フィルタータブ */}
              <div className="tabs tabs-boxed mb-4">
                <button
                  className={`tab ${filter === "all" ? "tab-active" : ""}`}
                  onClick={() => setFilter("all")}
                >
                  すべて ({tasks.length})
                </button>
                <button
                  className={`tab ${filter === "critical" ? "tab-active" : ""}`}
                  onClick={() => setFilter("critical")}
                >
                  重要 ({groupedTasks.critical?.length || 0})
                </button>
                <button
                  className={`tab ${filter === "high" ? "tab-active" : ""}`}
                  onClick={() => setFilter("high")}
                >
                  高 ({groupedTasks.high?.length || 0})
                </button>
              </div>

              {/* タスクアイテム */}
              <div className="space-y-2">
                {(filter === "all" ? tasks : groupedTasks[filter] || [])
                  .map(task => (
                    <div
                      key={task.id}
                      className={`card card-compact bg-base-200 cursor-pointer hover:bg-base-300 ${
                        selectedTask?.id === task.id ? "ring-2 ring-primary" : ""
                      }`}
                      onClick={() => setSelectedTask(task)}
                    >
                      <div className="card-body">
                        <div className="flex justify-between items-start">
                          <div className="flex-1">
                            <p className="text-sm truncate">{task.query}</p>
                            <div className="flex gap-2 mt-1">
                              <span className={`badge badge-sm ${
                                task.confidence < 0.4 ? "badge-error" :
                                task.confidence < 0.6 ? "badge-warning" :
                                "badge-success"
                              }`}>
                                {(task.confidence * 100).toFixed(0)}%
                              </span>
                              <span className="badge badge-sm badge-outline">
                                {task.level}
                              </span>
                            </div>
                          </div>
                          {task.deadline && (
                            <span className="text-xs text-warning">
                              {new Date(task.deadline).toLocaleTimeString()}
                            </span>
                          )}
                        </div>
                      </div>
                    </div>
                  ))}
              </div>
            </div>
          </div>
        </div>

        {/* レビューパネル */}
        <div className="lg:col-span-2">
          {selectedTask ? (
            <div className="card bg-base-100 shadow">
              <div className="card-body">
                <h2 className="card-title">タスクのレビュー</h2>

                {/* タスクの詳細 */}
                <div className="space-y-4">
                  <div>
                    <label className="label">
                      <span className="label-text font-semibold">顧客の問い合わせ</span>
                    </label>
                    <div className="bg-base-200 p-3 rounded">
                      {selectedTask.query}
                    </div>
                  </div>

                  <div>
                    <label className="label">
                      <span className="label-text font-semibold">AI応答</span>
                      <span className="label-text-alt">
                        信頼度: {(selectedTask.confidence * 100).toFixed(1)}%
                      </span>
                    </label>
                    <div className="bg-base-200 p-3 rounded">
                      <textarea
                        className="textarea w-full h-32"
                        defaultValue={selectedTask.response}
                        id="edited-response"
                      />
                    </div>
                  </div>

                  <div>
                    <label className="label">
                      <span className="label-text font-semibold">レビューフィードバック</span>
                    </label>
                    <textarea
                      className="textarea textarea-bordered w-full"
                      placeholder="エージェントへのフィードバックを提供..."
                      id="feedback"
                    />
                  </div>

                  {/* クイックアクション */}
                  <div className="flex gap-2">
                    <button className="btn btn-sm btn-outline">
                      追加コンテキストをリクエスト
                    </button>
                    <button className="btn btn-sm btn-outline">
                      履歴を表示
                    </button>
                    <button className="btn btn-sm btn-outline">
                      エスカレート
                    </button>
                  </div>
                </div>

                {/* アクションボタン */}
                <div className="card-actions justify-end mt-6">
                  <button
                    className="btn btn-error"
                    onClick={() => {
                      reviewMutation.mutate({
                        taskId: selectedTask.id,
                        approved: false,
                        feedback: document.getElementById("feedback").value
                      });
                    }}
                  >
                    却下
                  </button>
                  <button
                    className="btn btn-warning"
                    onClick={() => {
                      reviewMutation.mutate({
                        taskId: selectedTask.id,
                        approved: true,
                        editedResponse: document.getElementById("edited-response").value,
                        feedback: document.getElementById("feedback").value
                      });
                    }}
                  >
                    編集して承認
                  </button>
                  <button
                    className="btn btn-success"
                    onClick={() => {
                      reviewMutation.mutate({
                        taskId: selectedTask.id,
                        approved: true
                      });
                    }}
                  >
                    承認
                  </button>
                </div>
              </div>
            </div>
          ) : (
            <div className="card bg-base-100 shadow">
              <div className="card-body">
                <div className="text-center py-8">
                  <p className="text-base-content/60">
                    レビューするタスクを選択してください
                  </p>
                </div>
              </div>
            </div>
          )}
        </div>
      </div>

      {/* 統計 */}
      <div className="stats shadow mt-6">
        <div className="stat">
          <div className="stat-title">保留中の合計</div>
          <div className="stat-value">{tasks.length}</div>
        </div>
        <div className="stat">
          <div className="stat-title">重要</div>
          <div className="stat-value text-error">
            {groupedTasks.critical?.length || 0}
          </div>
        </div>
        <div className="stat">
          <div className="stat-title">平均応答時間</div>
          <div className="stat-value">2.3分</div>
        </div>
        <div className="stat">
          <div className="stat-title">承認率</div>
          <div className="stat-value">87%</div>
        </div>
      </div>
    </div>
  );
}

優先度フィルタリング、インライン編集、およびパフォーマンスメトリクスを備えたHITLタスク管理のための包括的なダッシュボード。

まとめ

ヒューマン・イン・ザ・ループパターンは、自律エージェントをAIの効率性と人間の判断の両方を活用する協調システムに変換します。サーバーレス環境での成功したHITL実装の鍵は、状態の永続化の管理、非同期ワークフローの処理、および人間のレビュアーのための直感的なインターフェースの提供にあります。LangGraphの中断機能をVercelのサーバーレスインフラストラクチャとモダンなUIパターンと組み合わせることで、強力で信頼できるシステムを作成します。

ここで示されたパターン(基本的な承認ワークフローから洗練されたマルチレベルのエスカレーションシステムまで)は、HITLが制限ではなく強化であることを示しています。純粋な自動化が無責任である重要なドメインでAIエージェントの展開を可能にしながら、サーバーレスアーキテクチャのスケーラビリティとコスト効率を維持します。AI機能が進歩し続ける中でも、HITLは、システムが倫理的境界と組織のポリシー内で動作することを保証するために不可欠です。