15. Xử lý quyết định của con người (Command Resume)

Nam

Nam Hoang / Feb 15, 2026

8 min read

Trong bài học trước, chúng ta đã học cách sử dụng Interrupts để "đóng băng" Agent ngay trước khi nó thực hiện một hành động nhạy cảm. Tuy nhiên, một Agent chuyên nghiệp không thể đứng đợi mãi mãi. Bài học này sẽ hướng dẫn bạn cách sử dụng đối tượng Command để gửi các tín hiệu điều khiển, giúp Agent tiếp tục thực thi dựa trên quyết định của con người.

I. Đối tượng Command là gì?

Thay vì gửi một tin nhắn văn bản thông thường, để "đánh thức" một Agent đang bị tạm dừng, chúng ta gửi một đối tượng Command của LangGraph. Đối tượng này chứa thuộc tính resume, nơi bạn truyền vào các quyết định (decisions) cụ thể cho từng yêu cầu hành động đang chờ xử lý.

II. Ba loại Quyết định (Decisions)

Khi phản hồi một Interrupt, bạn có 3 lựa chọn chính:

  1. approve: Cho phép thực thi công cụ với đúng các tham số mà AI đã đề xuất ban đầu.
  2. edit: Bạn không đồng ý với tham số AI đưa ra và cung cấp một editedAction chứa tên công cụ và bộ tham số mới đã được sửa.
  3. reject: Hủy bỏ hoàn toàn việc gọi công cụ và gửi một lời giải thích cho AI để nó biết tại sao hành động đó không được thực hiện.

III. Mã nguồn: 15-resuming-execution-with-commands.ts

Chúng ta sẽ mô phỏng lại quy trình: Agent xin phép chuyển tiền -> Bị tạm dừng -> Con người gửi lệnh Duyệt (Approve). Toàn bộ mã nguồn sử dụng hệ thống nạp môi trường env.ts và tiện ích util-langchain đã thiết lập từ Bài 1.

// apps/langchain/scripts/15-resuming-execution-with-commands.ts
// pnpm --filter=ai-notes-langchain run tsx scripts/15-resuming-execution-with-commands.ts

import './env'; // Load professional environment configuration
import { createGeminiModel, generateImage } from '@workspace/util-langchain';
import { createAgent, tool, humanInTheLoopMiddleware } from 'langchain';
import { MemorySaver, Command } from '@langchain/langgraph';
import { z } from 'zod';

// 1. Define a high-risk payment tool
const sendPayment = tool(
  async ({ amount, recipient }) => {
    return `[BANK]: Successfully transferred ${amount} VNĐ to ${recipient}.`;
  },
  {
    name: 'send_payment',
    description: 'Transfer money to another user. Requires human approval.',
    schema: z.object({
      amount: z.number(),
      recipient: z.string(),
    }),
  },
);

async function main() {
  const model = createGeminiModel();
  const checkpointer = new MemorySaver();

  const agent = createAgent({
    model,
    tools: [sendPayment],
    checkpointer,
    middleware: [
      humanInTheLoopMiddleware({
        interruptOn: { send_payment: true },
      }),
    ],
  });

  // Use a fixed thread_id to track the state across multiple calls
  const config = { configurable: { thread_id: 'tx-branch-999' } };

  // --- STEP 1: INITIAL INVOCATION (Will trigger Interrupt) ---
  console.log('--- STEP 1: AI proposes a payment ---');
  const firstResult = await agent.invoke(
    {
      messages: [{ role: 'user', content: 'Please transfer 500,000đ to Nam.' }],
    },
    config,
  );

  if (firstResult.__interrupt__) {
    console.log('🛑 Agent paused. Context saved to Checkpointer.');
    console.log('Waiting for human decision...');

    // --- STEP 2: RESUMING WITH COMMAND ---
    console.log('\n--- STEP 2: Human sends APPROVE Command ---');

    // We generate a visual diagram of the resumption state
    await generateImage(
      agent,
      'images/courses/langchain-course/scripts-15-resume.jpg',
    );

    // Instead of sending messages, we send a Command object
    const finalResult = await agent.invoke(
      new Command({
        resume: {
          // Provide decisions for each actionRequest in order
          decisions: [{ type: 'approve' }],
        },
      }),
      config, // CRITICAL: Must use the same thread_id
    );

    console.log('\nFinal Outcome after Approval:');
    const lastMsg = finalResult.messages[finalResult.messages.length - 1];
    console.log(lastMsg.content);
  }
}

main().catch(console.error);

IV. Phân tích kỹ thuật

  • Mảng decisions: Tại sao lại là một mảng? Vì AI có thể yêu cầu thực hiện nhiều hành động cùng lúc (ví dụ: chuyển tiền cho 3 người khác nhau). Bạn cần cung cấp quyết định cho từng hành động theo đúng thứ tự mà AI đã đề xuất trong actionRequests.
  • Sự quan trọng của thread_id: Nếu bạn sử dụng một ID khác ở bước resume, Agent sẽ không tìm thấy trạng thái "đang đứng đợi" trong MemorySaver và sẽ khởi tạo một cuộc hội thoại mới hoàn toàn.
  • Logic Tự sửa lỗi: Nếu bạn chọn type: "edit", bạn phải truyền thêm thuộc tính editedAction. Agent sẽ bỏ qua đề xuất cũ và thực thi công cụ với dữ liệu mới mà bạn cung cấp, giúp con người có quyền can thiệp sâu vào dữ liệu đầu ra của AI.

Sơ đồ hoạt động:

Sơ đồ Resume Agent

V. Tổng kết

  • new Command({ resume: ... }) là "chìa khóa" duy nhất để đánh thức Agent từ trạng thái Interrupt.
  • Việc nạp môi trường qua env.ts đảm bảo các API nhạy cảm luôn được bảo mật trong suốt quá trình Pause/Resume.
  • Kiến trúc này giúp bạn xây dựng các hệ thống AI cấp độ doanh nghiệp, nơi sự an toàn và quyền kiểm soát của con người được đặt lên hàng đầu.

Trong bài học tiếp theo, chúng ta sẽ học về Context Engineering — kỹ thuật đỉnh cao để thay đổi "tính cách" và "khả năng" của Agent một cách linh hoạt dựa trên từng đối tượng người dùng cụ thể!

👉 Bài tiếp theo: 16. Kỹ thuật Ngữ cảnh động (Context Engineering)