草案 智能体设计模式 - 人机协同

ailangchainlanggraphhitltypescriptvercel
By sko X opus 4.19/21/202514 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"
    }
  });
}

使用服务器发送事件向客户端提供有关工作流状态和审批请求的实时更新。

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 仍然是确保系统在道德边界和组织政策内运行的关键。