17. Project: Email Assistant - Thực thi & HITL

Nam

Nam Hoang / Sep 17, 2025

12 min read

Trong bài học trước, chúng ta đã xây dựng phần khung phân loại và tra cứu. Bây giờ, chúng ta sẽ thực hiện phần quan trọng nhất: Soạn thảo phản hồiGiao tiếp với con người thông qua cơ chế Interrupt để hoàn thiện dự án cuối khóa.

I. Soạn thảo và Điều phối bằng Command

Agent sẽ soạn thảo email dựa trên kết quả tra cứu. Một điểm đặc biệt là chúng ta sử dụng đối tượng Command để rẽ nhánh: Nếu email có mức độ khẩn cấp là critical, Agent sẽ chuyển sang node chờ người duyệt (human_review), ngược lại sẽ đi thẳng tới node gửi email.

async function draftNode(state: EmailState) {
  console.log('--- NODE: Draft Response ---');

  const response = await llm.invoke(`
    Use the following information to write a customer reply:
    Docs: ${state.searchResults.join(', ')}
    Issue summary: ${state.classification?.summary}
  `);

  const draft = response.content as string;
  const needsHuman = state.classification?.urgency === 'critical';
  const nextNode = needsHuman ? 'human_review' : 'send_email';

  return new Command({
    update: { draftResponse: draft },
    goto: nextNode,
  });
}

II. Tích hợp Human-in-the-loop (HITL)

Node human_review sử dụng hàm interrupt() để tạm dừng Graph. Khi bạn "đánh thức" Agent bằng lệnh resume, nó sẽ lấy dữ liệu phê duyệt của bạn để quyết định gửi email hay hủy bỏ.

async function humanReviewNode(state: EmailState) {
  console.log('--- NODE: Waiting for Human Review ---');

  const decision = interrupt({
    message: 'Please review this email draft',
    draft: state.draftResponse,
  });

  if (decision.approved === 'yes') {
    return new Command({
      update: { draftResponse: decision.newDraft || state.draftResponse },
      goto: 'send_email',
    });
  }

  return new Command({
    update: { finalStatus: 'Rejected by human' },
    goto: END,
  });
}

III. Mã nguồn hoàn chỉnh: 17-project-implementation.ts

Dưới đây là toàn bộ mã nguồn tích hợp tất cả các kỹ năng: State phức tạp, Xử lý song song, Command Routing, Persistence và HITL.

// path: 17-project-implementation.ts
import {
  Command,
  END,
  START,
  StateGraph,
  interrupt,
  MemorySaver,
} from '@langchain/langgraph';
import { createGeminiModel, generateImage } from '@workspace/util-langchain';
import { z } from 'zod';

// 1. SCHEMAS & STATE
const ClassificationSchema = z.object({
  intent: z.enum(['billing', 'technical', 'feature_request', 'general']),
  urgency: z.enum(['low', 'medium', 'high', 'critical']),
  summary: z.string(),
});

const EmailStateDefinition = z.object({
  emailContent: z.string(),
  senderEmail: z.string(),
  classification: ClassificationSchema.optional(),
  searchResults: z.array(z.string()).default([]),
  ticketId: z.string().optional(),
  draftResponse: z.string().optional(),
  finalStatus: z.string().optional(),
});

type EmailState = z.infer<typeof EmailStateDefinition>;
const llm = createGeminiModel();

// 2. NODES (Classify, Search, Bug report, Draft, Review, Send)
async function classifyIntent(state: EmailState) {
  const structured = llm.withStructuredOutput(ClassificationSchema);
  const result = await structured.invoke(`Analyze: ${state.emailContent}`);
  return { classification: result };
}

async function searchDocs(state: EmailState) {
  return { searchResults: ['Standard Support Procedure v1.0'] };
}

async function bugTracking(state: EmailState) {
  return state.classification?.intent === 'technical'
    ? { ticketId: 'JIRA-101' }
    : {};
}

async function draftNode(state: EmailState) {
  const response = await llm.invoke(
    `Write reply for: ${state.classification?.summary}`,
  );
  const draft = response.content as string;
  const nextNode =
    state.classification?.urgency === 'critical'
      ? 'human_review'
      : 'send_email';
  return new Command({ update: { draftResponse: draft }, goto: nextNode });
}

async function humanReviewNode(state: EmailState) {
  const decision = interrupt({
    message: 'Review needed',
    draft: state.draftResponse,
  });
  return decision.approved === 'yes'
    ? new Command({
        update: { draftResponse: decision.newDraft || state.draftResponse },
        goto: 'send_email',
      })
    : new Command({ update: { finalStatus: 'Rejected' }, goto: END });
}

async function sendEmailNode(state: EmailState) {
  console.log('\n--- EMAIL SENT ---\n', state.draftResponse);
  return { finalStatus: 'Sent' };
}

// 3. BUILD GRAPH
const workflow = new StateGraph(EmailStateDefinition)
  .addNode('classify', classifyIntent)
  .addNode('search', searchDocs)
  .addNode('bug_report', bugTracking)
  .addNode('draft', draftNode, { ends: ['human_review', 'send_email'] })
  .addNode('human_review', humanReviewNode, { ends: ['send_email', END] })
  .addNode('send_email', sendEmailNode)
  .addEdge(START, 'classify')
  .addEdge('classify', 'search')
  .addEdge('classify', 'bug_report')
  .addEdge('search', 'draft')
  .addEdge('bug_report', 'draft')
  .addEdge('send_email', END)
  .compile({ checkpointer: new MemorySaver() });

// 4. RUN SCENARIOS
async function run() {
  await generateImage(
    workflow,
    'graph-ignore/scripts-17-project-email-implementation.jpg',
  );
  const config = { configurable: { thread_id: 'project-thread' } };

  console.log('--- Scenario: Critical Issue ---');
  const result = await workflow.invoke(
    {
      emailContent: 'System is down! Emergency!',
      senderEmail: 'ceo@client.com',
    },
    config,
  );

  if (result.__interrupt__) {
    console.log('⚠️ Status: Interrupted for Review');
    const final = await workflow.invoke(
      new Command({
        resume: { approved: 'yes', newDraft: 'Our team is on it right now!' },
      }),
      config,
    );
    console.log('Final Status:', final.finalStatus);
  }
}

run();

IV. Sơ đồ hoạt động của Project

Dưới đây là sơ đồ hoàn chỉnh của AI Email Assistant:

Sơ đồ AI Email Assistant

V. Tổng kết khóa học

Chúc mừng bạn! Bạn đã hoàn thành hành trình LangGraph Essentials. Bạn hiện đã sở hữu những kỹ năng:

  • Xây dựng Agent có trạng thái và khả năng tự phục hồi.
  • Điều phối luồng công việc phức tạp, chạy song song và rẽ nhánh thông minh bằng Command.
  • Kết hợp con người vào quy trình AI một cách an toàn thông qua interrupt.
  • Quản lý bộ nhớ ngắn hạn và dài hạn cho Agent chuyên nghiệp.

Hành trình của bạn chỉ mới bắt đầu. Hãy thử áp dụng các kiến thức này để xây dựng những trợ lý AI thực thụ cho công việc của mình. Hẹn gặp lại bạn ở các khóa học nâng cao tiếp theo!