12. Interruption - Human-in-the-loop (HITL)

Nam

Nam Hoang / Sep 12, 2025

7 min read

Trong bài học trước, chúng ta đã học cách Agent giao tiếp với người dùng qua Streaming. Tuy nhiên, trong các hệ thống thực tế (như phê duyệt thanh toán hay gửi email), chúng ta cần hướng giao tiếp ngược lại: Agent phải dừng lại và chờ con người cho phép hoặc sửa đổi dữ liệu trước khi đi tiếp. Đây là mô hình Human-in-the-loop (HITL).

I. Khái niệm về Interrupts

Một Interrupt (Ngắt) là một lệnh dừng Graph một cách động. Khác với các điểm dừng cố định, interrupt() có thể được đặt ở bất kỳ đâu trong logic của Node và thường đi kèm với một điều kiện nghiệp vụ.

Khi gặp hàm interrupt():

  1. LangGraph ném ra một ngoại lệ đặc biệt khiến Node dừng lại ngay lập tức.
  2. Trạng thái hiện tại được lưu vào Checkpointer.
  3. Graph trả về kết quả cho người gọi kèm theo trường __interrupt__ chứa thông tin yêu cầu.
  4. Graph chờ đợi cho đến khi bạn gọi lại nó kèm theo một lệnh Command({ resume: ... }).

II. Cơ chế "Tỉnh dậy" của Node

Có một điểm quan trọng cần lưu ý: Khi bạn tiếp tục (resume) một Graph sau khi bị ngắt, LangGraph sẽ chạy lại Node đó từ đầu.

Tuy nhiên, lần này khi chạy đến dòng interrupt(), LangGraph sẽ không dừng lại nữa. Nó sẽ lấy giá trị bạn gửi vào qua thuộc tính resume và gán trực tiếp cho kết quả của hàm interrupt(). Do đó, các dòng code phía sau lệnh ngắt sẽ được thực thi với dữ liệu mới từ con người.

III. Mã nguồn: 12-interruption-hitl.ts

Ví dụ dưới đây minh họa một hệ thống duyệt chi ngân sách. Agent sẽ tự động duyệt nếu số tiền nhỏ, nhưng sẽ dừng lại chờ "quản lý" duyệt nếu số tiền lớn hơn $1000.

// path: 12-interruption-hitl.ts
import {
  Command,
  END,
  MemorySaver,
  START,
  StateGraph,
  interrupt,
} from '@langchain/langgraph';
import { generateImage } from '@workspace/util-langchain';
import z from 'zod';

const State = z.object({
  amount: z.number(),
  status: z.string().optional(),
});

async function budgetNode(state: any) {
  console.log('--- NODE: Checking budget ---');

  if (state.amount > 1000) {
    // Pause execution and request human approval
    // The message passed to interrupt() is sent to the UI/User
    const approval = interrupt(
      `Amount ${state.amount}$ is too large. Do you approve?`,
    );

    // After resume, 'approval' holds the value passed via Command({ resume: ... })
    if (approval === 'yes') {
      return { status: 'Approved by manager' };
    } else {
      return { status: 'Rejected by manager' };
    }
  }

  return { status: 'Auto-approved (Small amount)' };
}

const checkpointer = new MemorySaver();
const workflow = new StateGraph(State)
  .addNode('budget_check', budgetNode)
  .addEdge(START, 'budget_check')
  .addEdge('budget_check', END)
  .compile({ checkpointer });

// Execution scenario
async function run() {
  await generateImage(
    workflow,
    'graph-ignore/scripts-12-interruption-hitl.jpg',
  );
  const config = { configurable: { thread_id: 'thread-budget-1' } };

  // 1. First run with a large amount ($1500)
  console.log('--- Starting request for $1500 ---');
  const result = await workflow.invoke({ amount: 1500 }, config);

  // Check if execution was interrupted
  if (result.__interrupt__) {
    console.log('⚠️ SYSTEM PAUSED:', result.__interrupt__[0].value);

    // 2. Resume (Simulate human responding 'yes')
    console.log("\n--- Human response: 'yes' ---");
    const finalResult = await workflow.invoke(
      new Command({ resume: 'yes' }),
      config,
    );
    console.log('Final status:', finalResult.status);
  }
}

run();

IV. Các quy tắc quan trọng khi dùng HITL

Vì Node sẽ chạy lại từ đầu khi resume, bạn cần tuân thủ các quy tắc sau:

  1. Persistence là bắt buộc: Bạn phải cung cấp một checkpointer khi compile Graph, nếu không LangGraph sẽ không có nơi để lưu trạng thái tạm dừng.
  2. Tính lũy đẳng (Idempotency): Tránh thực hiện các hành động có tác động bên ngoài (như gửi email) trước lệnh interrupt(), vì chúng sẽ bị thực hiện lại khi Node resume. Hãy đặt các hành động đó sau lệnh ngắt.
  3. Dữ liệu thô: Chỉ truyền các dữ liệu có thể chuyển thành JSON (string, number, object đơn giản) vào hàm interrupt() để đảm bảo khả năng lưu trữ bền bỉ.

Sơ đồ hoạt động:

Sơ đồ HITL

V. Tổng kết

  • Interrupts cung cấp cơ chế bảo vệ an toàn cho Agent, đưa con người vào vòng lặp quyết định cuối cùng.
  • Command({ resume }) là "chìa khóa" để đánh thức Agent và truyền dữ liệu can thiệp từ bên ngoài vào State.
  • Việc kết hợp HITL và Persistence giúp xây dựng các Agent có thể chờ đợi sự phê duyệt của con người trong nhiều giờ hoặc nhiều ngày mà không mất tiến trình.

Ở bài học tới, chúng ta sẽ học về Time Travel - cách quay ngược thời gian để xem Agent đã làm gì tại thời điểm bị ngắt và sửa lỗi cho nó nếu cần thiết!

👉 Bài tiếp theo: 13. Time Travel - Kiểm soát và Chỉnh sửa lịch sử