以编程方式使用文档

并非所有智能体操作都应该无人监督地运行。当智能体即将发送 电子邮件、删除记录、执行金融交易或执行任何 不可逆操作时,您需要人工先审查并批准该操作。 人在回路(HITL)模式允许您的智能体暂停执行,呈现 待处理操作给用户,并在获得明确批准后才恢复执行。

由于HITL建立在LangGraph中断和检查点之上,暂停是 持久的。用户可以刷新页面,审核员可以从不同的 组件进行回复,而智能体仍会从执行 停止的确切位置恢复,而不是重放整个运行过程。

中断如何工作

LangGraph智能体支持 **中断**,即智能体的显式暂停点 将控制权归还给客户端。当智能体触发中断时:

  1. 智能体停止执行并发出中断负载
  2. useStream钩子通过以下方式呈现中断 stream.interrupt
  3. Your UI renders a review card with approve/reject/edit options
  4. 用户做出决定
  5. 您的代码调用 stream.submit() 并附带恢复命令
  6. 智能体从停止处继续

前端SDK将中断与线程状态的其他部分一起保存,因此 您的UI可以在任何有意义的地方呈现它:内联在记录中、 审核队列、管理员仪表板,或在阻止下一个用户 操作直到做出决定为止的模态框中。

设置 useStream

useStream连接到您的人工介入智能体。当图执行到 中断时,钩子在 stream.interrupt上暴露待处理的负载。在 设置该值时呈现批准卡片,然后用 stream.submit(null, { command: { resume: response } }) 在用户 批准、拒绝或编辑该操作后恢复运行。

const AGENT_URL = "http://localhost:2024";

  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "human_in_the_loop",
  });

  const interrupt = stream.interrupt;

  return (

      {stream.messages.map((msg) => (

      ))}
      {interrupt && (

            stream.submit(null, { command: { resume: response } })
          }
        />
      )}

  );
}
<script setup lang="ts">


const AGENT_URL = "http://localhost:2024";

const stream = useStream<typeof myAgent>({
  apiUrl: AGENT_URL,
  assistantId: "human_in_the_loop",
});

function handleRespond(response: HITLResponse) {
  stream.submit(null, { command: { resume: response } });
}
</script>

<template>




</template>
<script lang="ts">


  const AGENT_URL = "http://localhost:2024";

  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "human_in_the_loop",
  });

  function handleRespond(response: HITLResponse) {
    stream.submit(null, { command: { resume: response } });
  }
</script>


  {#each stream.messages as msg (msg.id)}

  {/each}

  {#if stream.interrupt}

  {/if}
const AGENT_URL = "http://localhost:2024";

@Component({
  selector: "app-chat",
  template: `
    @for (msg of stream.messages(); track msg.id) {
      <app-message [message]="msg" />
    }
    @if (stream.interrupt()) {
      <app-approval-card
        [interrupt]="stream.interrupt()"
        (respond)="handleRespond($event)"
      />
    }
  `,
})

  stream = injectStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "human_in_the_loop",
  });

  handleRespond(response: HITLResponse) {
    this.stream.submit(null, { command: { resume: response } });
  }
}

中断负载

当智能体暂停时, stream.interrupt 包含一个带有 的HITLRequest

interface HITLRequest {
  actionRequests: ActionRequest[];
  reviewConfigs: ReviewConfig[];
}

interface ActionRequest {
  name: string;
  args: Record<string, unknown>;
  description?: string;
}

interface ReviewConfig {
  allowedDecisions: ("approve" | "reject" | "edit" | "respond")[];
}
属性描述
actionRequests智能体想要执行的待处理操作数组
actionRequests[].name操作名称(例如。 "send_email", "delete_record")
actionRequests[].args操作的结构化参数
actionRequests[].description对操作功能的可选人类可读描述
reviewConfigs每个操作的配置,控制允许哪些决定
reviewConfigs[].allowedDecisions要显示的按钮: "approve", "reject", "edit", "respond"

决定类型

HITL模式支持四种决定类型:

批准

用户确认操作应按原样继续执行:

const response: HITLResponse = {
  decisions: [{ type: "approve" }],
};

stream.submit(null, { command: { resume: response } });

拒绝

用户拒绝该操作,可选择提供原因。工具不会被执行:

const response: HITLResponse = {
  decisions: [
    {
      type: "reject",
      message: "The email tone is too aggressive. Do not send it.",
    },
  ],
};

stream.submit(null, { command: { resume: response } });

编辑

用户在批准前修改操作的参数:

const response: HITLResponse = {
  decisions: [
    {
      type: "edit",
      editedAction: {
        name: actionRequest.name,
        args: {
          ...actionRequest.args,
          subject: "Updated subject line",
          body: "Revised email body with softer language.",
        },
      },
    },
  ],
};

stream.submit(null, { command: { resume: response } });

回复

用户为“询问用户”类型的工具提供直接回复。该 message 内容将成为工具结果,工具本身不会被执行:

const response: HITLResponse = {
  decisions: [{ type: "respond", message: "Blue." }],
};

stream.submit(null, { command: { resume: response } });

构建 ApprovalCard

以下是由审批卡片使用的决策连接方式。UI可以将每个 操作拆分到各自的卡片中,但恢复载荷是一个单一的 HITLResponse ,每个待处理操作对应一个决策:

async function approveAll() {
  const resume: HITLResponse = {
    decisions: actionRequests.map(() => ({ type: "approve" })),
  };
  await stream.submit(null, { command: { resume } });
}

async function rejectOne(index: number, message: string) {
  const resume: HITLResponse = {
    decisions: actionRequests.map((_, i) =>
      i === index
        ? { type: "reject", message }
        : { type: "reject", message: "Rejected along with other actions" },
    ),
  };
  await stream.submit(null, { command: { resume } });
}

async function editOne(index: number, editedArgs: Record<string, unknown>) {
  const originalAction = actionRequests[index];
  const resume: HITLResponse = {
    decisions: actionRequests.map((_, i) =>
      i === index
        ? {
            type: "edit",
            editedAction: { name: originalAction.name, args: editedArgs },
          }
        : { type: "approve" },
    ),
  };
  await stream.submit(null, { command: { resume } });
}

恢复流程

用户做出决策后,完整周期如下:

1. 调用 stream.submit(null, { command: { resume: hitlResponse } }) 2. The useStream hook 向 LangGraph 后端发送恢复命令 3. 代理收到 HITLResponse 并继续执行。中的每个条目可能是: decisions 可能是以下之一: - { type: "approve" }:代理继续执行该操作 - { type: "reject", message }:工具不执行,代理收到拒绝消息后再决定下一步 - { type: "edit", editedAction }:代理使用编辑后的参数运行工具 - { type: "respond", message }:人工消息直接作为工具结果返回,工具本身不执行 4. interrupt 属性重置为 null 当代理恢复流式传输时

处理多个待处理操作

一个中断可能包含多个 actionRequests 当代理想要 同时执行多个操作时。为每个操作渲染一张卡片,并收集所有 决策后再恢复:

function MultiActionReview({
  interrupt,
  onRespond,
}: {
  interrupt: { value: HITLRequest };
  onRespond: (response: HITLResponse) => void;
}) {
  const [decisions, setDecisions] = useState<Record<number, HITLResponse["decisions"][number]>>({});
  const request = interrupt.value;

  const allDecided =
    Object.keys(decisions).length === request.actionRequests.length;

  return (

      {request.actionRequests.map((action, i) => (

            setDecisions((prev) => ({ ...prev, [i]: response }))
          }
        />
      ))}
      {allDecided && (
        <button
          className="rounded bg-green-600 px-4 py-2 text-white"
          onClick={() =>
            onRespond({
              decisions: request.actionRequests.map((_, i) => decisions[i]),
            })
          }
        >
          Submit All Decisions
        </button>
      )}

  );
}

自定义中断表单

恢复流程 使用 humanInTheLoopMiddleware,它用以下内容包装一个工具 generic approve / reject / edit / respond card. Sometimes a single set of 仅按钮是不够的:预订航班、批准退款和审核 社交帖子每个都需要一个 *不同的* 表单,有各自的字段、验证和 副本。为此,引发 interrupt() **从工具内部** 并让 有效载荷描述UI应呈现的确切表单。每个工具可以展示一个 完全不同的界面。

在中断有效载荷中描述表单

interrupt() 接受任何JSON可序列化值,这允许你提供一个"卡片" 前端知道如何呈现的内容,例如,表单类型、标题、上下文 人类正在审核的内容,以及要收集的字段。 interrupt() 在其上是泛型的 输入和返回类型(interrupt<I, R>(value: I): R)所以你可以为两者添加类型 你发送的卡片(InterruptCard)和用户解决的值 (ReviewDecision)。导出这些类型以便前端可以导入它们并保持 同步:

  name: string;
  label: string;
  type: "select" | "checkbox" | "textarea" | "currency";
  options?: string[];
  default?: unknown;
}

/** What the user resolves the interrupt with. */

  approved: boolean;
  /** Edited / collected form values the tool should act on. */
  values?: Record<string, unknown>;
}

/** The form spec ("card") an interrupt hands to the frontend. */

  formType: "flight-booking" | "refund-approval" | "content-review";
  tool: string;
  title: string;
  context: Record<string, unknown>;
  fields: FormField[];
  /** Populated by the frontend when it commits the resolved card to state. */
  resolved?: boolean;
  decision?: ReviewDecision;
}

const bookFlight = tool(
  async ({ origin, destination, date, passengers }) => {
    // Pause the tool and hand the frontend a typed form spec; the typed return
    // is whatever the UI resolves the interrupt with.
    const decision = interrupt<InterruptCard, ReviewDecision>({
      formType: "flight-booking",
      tool: "book_flight",
      title: "Confirm flight booking",
      context: { origin, destination, date, passengers },
      fields: [
        {
          name: "seatClass",
          label: "Seat class",
          type: "select",
          options: ["Economy", "Premium Economy", "Business"],
          default: "Economy",
        },
        { name: "insurance", label: "Add trip insurance", type: "checkbox", default: false },
      ],
    });

    if (!decision.approved) {
      return `Booking cancelled. No flight from ${origin} to ${destination} was reserved.`;
    }

    // Run the real (possibly slow) work with the values the human confirmed.
    const seatClass = String(decision.values?.seatClass ?? "Economy");
    return `Flight booked from ${origin} to ${destination} in ${seatClass}.`;
  },
  {
    name: "book_flight",
    description: "Book a flight. Requires human confirmation of trip details.",
    schema: z.object({
      origin: z.string(),
      destination: z.string(),
      date: z.string(),
      passengers: z.number().int().min(1),
    }),
  },
);

为每个工具提供不同的 formType (e.g. "refund-approval", "content-review")以便前端可以切换它并呈现匹配的 form.

为每个工具渲染不同的表单

在客户端,卡片以以下形式到达 stream.interrupt.value。导入 InterruptCardReviewDecision 类型从您的代理模块,以便表单 和负载保持同步,切换到 formType 以选择正确的表单,然后 传入 fields 到您的输入中:

function Chat() {
  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "hitl_interrupt_forms",
  });

  const card = stream.interrupt?.value as InterruptCard | undefined;

  return (

      {stream.messages.map((msg) => (

      ))}
      {card && }

  );
}

// `InterruptForm` renders a flight / refund / content card based on
// `card.formType`, collects `card.fields`, and calls `onResolve` with the
// user's decision and edited values.

使用以下方式让卡片保持在屏幕上 respond(decision, { update })

当您解决一个普通中断时,卡片会在中断清除的瞬间消失,只有 工具结果返回。这意味着一个丰富的审核卡片 会在运行中途消失。为了让它保持在屏幕上,请解决中断 **并** 提交 一条携带卡片的消息到状态中,在 *同一* 超步骤中使用 useStream's respond:

function handleResolve(decision: ReviewDecision) {
  // Snapshot the card with the decision baked in so it renders read-only.
  const resolvedCard = { ...card, resolved: true, decision };
  const cardMessage = new AIMessage({
    content: `Review ${decision.approved ? "approved" : "declined"}.`,
    response_metadata: { cards: resolvedCard },
  });

  // Resume the interrupt AND push the card into state atomically. Maps to
  // LangGraph's `Command(resume, update)`: one checkpoint, no extra state write.
  stream.respond(decision, { update: { messages: [cardMessage] } });
}

respond(response, { update }) 应用 update **乐观地**: 卡片立即绘制,一旦恢复的运行回显相同的消息,就按ID进行协调 后端从不重新发出卡片,因此它保持渲染 在(可能较慢的)工具运行期间不会闪烁。渲染已解决 的卡片,通过从消息中读取它:

{stream.messages.map((msg) => {
  const card = (msg.response_metadata as { cards?: InterruptCard })?.cards;
  if (card) return ;
  return ;
})}

最佳实践

在实现 HITL 工作流时请牢记以下准则:

- **显示清晰的上下文**。始终显示 *什么* 代理想要做什么以及 *为什么*。包括操作描述和完整参数。 - **使批准成为最简单的路径**。如果操作看起来正确,批准 should be a single click. Reserve multi-step flows for reject/edit. - **验证编辑后的参数**。当用户编辑操作参数时,验证 JSON 结构后再发送。对于格式错误的输入显示内联错误。 - **持久化中断状态**。如果用户刷新页面, 中断应该仍然可见。useStream 通过线程的 checkpoint. - **记录所有决策**. For audit trails, log every approve/reject/edit 带有时间戳和做出决策的用户的决策。 - **深思熟虑地设置超时**。长时间运行的代理不应阻塞 无限期地等待人工审查。考虑显示代理已运行了多长时间 waiting.