19. Bàn giao quyền điều khiển (Handoffs)

Nam

Nam Hoang / Feb 17, 2026

10 min read

Trong bài học trước, chúng ta đã tìm hiểu kiến trúc Supervisor – nơi có một "ông chủ" điều phối công việc. Tuy nhiên, trong thực tế, đôi khi việc bắt mọi tin nhắn phải quay lại Supervisor sẽ tạo ra độ trễ không cần thiết. Bài học này sẽ hướng dẫn bạn kỹ thuật Agent Handoffs (Bàn giao), cho phép các Agent tự quyết định khi nào nên chuyển quyền điều khiển cho chuyên gia khác, tích hợp hoàn hảo với hệ thống util-langchainenv.ts từ Bài 1.

I. Handoffs khác gì với Supervisor?

  • Supervisor (Bài 18): Mọi kết quả từ Subagent đều phải báo cáo về Quản lý. Quản lý sau đó mới quyết định bước tiếp theo. (An toàn nhưng chậm hơn).
  • Handoffs (Bài này): Agent A trực tiếp gọi một "công cụ chuyển vùng" để mời Agent B vào nói chuyện với người dùng. (Nhanh, mượt mà và tự nhiên hơn).

II. Cơ chế "Active Agent"

Để làm được việc này, chúng ta cần một biến trạng thái (State) gọi là activeAgent trong hệ thống Graph:

  1. Chặn đầu: Hệ thống (Graph) luôn kiểm tra xem activeAgent là ai để gửi tin nhắn của người dùng vào đúng Node đó.
  2. Chuyển giao: Agent đang hoạt động gọi một Tool đặc biệt. Tool này không trả về văn bản mà trả về một đối tượng Command để cập nhật lại biến activeAgent.
  3. Điều hướng: Graph nhận thấy sự thay đổi và chuyển luồng xử lý sang Agent mới ngay lập tức.

III. Mã nguồn: 19-agent-handoffs.ts

Chúng ta sẽ xây dựng một hệ thống hỗ trợ khách hàng đa năng: Kỹ thuật (Support)Bán hàng (Sales).

// apps/langchain/scripts/19-agent-handoffs.ts
// pnpm --filter=ai-notes-langchain run tsx scripts/19-agent-handoffs.ts

import './env';
import { createGeminiModel, generateImage } from '@workspace/util-langchain';
import { createAgent, tool } from 'langchain';
import {
  StateGraph,
  START,
  END,
  MessagesAnnotation,
  Command,
} from '@langchain/langgraph';
import { z } from 'zod';

// 1. Define State with an activeAgent tracker
const AgentState = MessagesAnnotation.extend({
  activeAgent: z.string().default('support'), // Default to Support
});

// 2. Define Handoff Tools using Command to update state
const transferToSales = tool(
  async () => {
    console.log('[SYSTEM]: Transferring control to SALES...');
    return new Command({
      update: { activeAgent: 'sales' },
    });
  },
  {
    name: 'transfer_to_sales',
    description:
      'Use this when the customer asks about pricing, buying, or upgrades.',
    schema: z.object({}),
  },
);

const transferToSupport = tool(
  async () => {
    console.log('[SYSTEM]: Transferring control to SUPPORT...');
    return new Command({
      update: { activeAgent: 'support' },
    });
  },
  {
    name: 'transfer_to_support',
    description:
      'Use this when the customer has technical issues or needs a guide.',
    schema: z.object({}),
  },
);

// 3. Initialize Specialists with Gemini 2.5 Flash
const model = createGeminiModel();

const supportAgent = createAgent({
  model,
  tools: [transferToSales],
  systemPrompt:
    'You are a Tech Support expert. If the user wants to buy or upgrade, transfer to Sales.',
});

const salesAgent = createAgent({
  model,
  tools: [transferToSupport],
  systemPrompt:
    'You are a Sales specialist. If the user reports a bug or technical issue, transfer to Support.',
});

// 4. Build the Workflow (The Router Logic)
const workflow = new StateGraph(AgentState)
  .addNode('support', async (state) => await supportAgent.invoke(state))
  .addNode('sales', async (state) => await salesAgent.invoke(state))

  // Logic: Always start with the currently active agent
  .addConditionalEdges(START, (state) => state.activeAgent)

  // After an agent finishes, decide whether to end or loop to the NEW active agent
  .addConditionalEdges('support', (state) => {
    const lastMsg = state.messages[state.messages.length - 1];
    // If AI just replied with text (no tool call), we end the turn
    if (lastMsg._getType() === 'ai' && !lastMsg.tool_calls?.length) return END;
    return state.activeAgent;
  })
  .addConditionalEdges('sales', (state) => {
    const lastMsg = state.messages[state.messages.length - 1];
    if (lastMsg._getType() === 'ai' && !lastMsg.tool_calls?.length) return END;
    return state.activeAgent;
  });

const app = workflow.compile();

async function main() {
  // Generate diagram
  await generateImage(
    app,
    'images/courses/langchain-course/scripts-19-handoffs.jpg',
  );

  console.log('--- Customer starts with Tech Support ---');

  // Scenario: Asking support but suddenly wanting to buy
  const result = await app.invoke({
    messages: [
      {
        role: 'user',
        content: 'My screen is broken. Also, how much is the Pro plan?',
      },
    ],
  });

  console.log('\n--- FINAL AI RESPONSE ---');
  console.log(result.messages.at(-1).content);
  console.log('\nCurrent Active Agent:', result.activeAgent.toUpperCase());
}

main().catch(console.error);

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

  • Tính mượt mà: Khác với Supervisor (vốn thường tóm tắt lại kết quả), Handoff cho phép Agent mới nhìn thấy toàn bộ lịch sử trò chuyện vì chúng dùng chung messages state. Agent Sales sẽ thấy khách vừa kêu hỏng màn hình và có thể đưa ra câu trả lời tinh tế hơn ("Rất tiếc về màn hình của bạn, về bản Pro thì...").
  • Command({ update: { ... } }): Đây là tính năng mạnh mẽ của LangGraph, cho phép một Công cụ tác động trực tiếp vào luồng di chuyển của toàn bộ đồ thị mà không cần các câu lệnh if-else cồng kềnh bên ngoài Node.
  • Routing động: Hệ thống điều hướng hoàn toàn dựa trên biến activeAgent. Bạn có thể mở rộng lên 5-10 Agent chuyên gia chỉ bằng cách thêm Node và cập nhật tên Agent trong các Tool bàn giao.

Sơ đồ hoạt động:

Sơ đồ Agent Handoffs

V. Tổng kết

  • Handoffs giúp hệ thống linh hoạt hơn, giảm độ trễ và mang lại trải nghiệm hội thoại tự nhiên hơn cho người dùng.
  • Sử dụng biến trạng thái activeAgent là cách chuyên nghiệp nhất để theo dõi ai đang "cầm lái" cuộc hội thoại.
  • Hệ thống util-langchainenv.ts đảm bảo các thành phần Agent phối hợp với nhau một cách bảo mật và ổn định trên nền tảng Gemini 2.5 Flash.

Trong bài học cuối cùng, chúng ta sẽ học về MCP (Model Context Protocol) — tiêu chuẩn mới nhất để kết nối Agent của bạn với bất kỳ nguồn dữ liệu hoặc công cụ nào trên toàn thế giới!

👉 Bài tiếp theo: 20. Kết nối mọi nguồn dữ liệu với MCP