Xây Dựng Giao Diện Người Dùng Hiện Đại Cho LangChain Deep Agents Bằng CopilotKit: Hướng Dẫn Toàn Diện

Trong kỷ nguyên của Trí tuệ Nhân tạo (AI), việc phát triển các hệ thống agent thông minh, có khả năng thực hiện các nhiệm vụ phức tạp, đa bước đang trở thành trọng tâm. LangChain Deep Agents, một sáng kiến đột phá từ LangChain, đã mở ra một phương pháp mới để xây dựng các hệ thống đa agent có cấu trúc, có thể lập kế hoạch, ủy quyền và suy luận qua nhiều bước. Tuy nhiên, việc kết nối những agent mạnh mẽ này với một giao diện người dùng (UI) trực quan, phản hồi theo thời gian thực vẫn là một thách thức không nhỏ. Bài viết này sẽ hướng dẫn bạn cách vượt qua rào cản đó bằng cách tích hợp LangChain Deep Agents với CopilotKit để tạo ra một trải nghiệm người dùng liền mạch và mạnh mẽ.

Chúng ta sẽ cùng nhau xây dựng một trợ lý tìm việc làm được cung cấp bởi Deep Agents và kết nối nó với một giao diện Next.js sống động bằng cách sử dụng CopilotKit. Mục tiêu là đảm bảo giao diện người dùng luôn đồng bộ với trạng thái của agent theo thời gian thực. Bạn sẽ khám phá kiến trúc tổng thể, các mẫu thiết kế quan trọng, cách dữ liệu luân chuyển giữa UI và agent, cùng với hướng dẫn từng bước để xây dựng ứng dụng này từ đầu.

Hãy bắt đầu hành trình xây dựng ứng dụng AI thế hệ mới này!

Bạn có thể tham khảo thêm tại GitHub của CopilotKit ⭐️ để biết thêm chi tiết.

1. LangChain Deep Agents: Sự Tiến Hóa Của Hệ Thống AI Agent

Để xây dựng một giao diện người dùng hiệu quả cho hệ thống AI của chúng ta, trước tiên cần hiểu rõ về nền tảng Deep Agents. Đây không chỉ là một công cụ, mà là một triết lý thiết kế agent tiên tiến, khắc phục những hạn chế của các mô hình “LLM trong vòng lặp” truyền thống.

Deep Agents Là Gì và Tại Sao Chúng Lại Quan Trọng?

Hầu hết các hệ thống agent AI hiện tại thường chỉ dừng lại ở mô hình “LLM trong vòng lặp + công cụ”, một cấu trúc có giới hạn đáng kể khi xử lý các nhiệm vụ phức tạp. Các agent này thường thiếu khả năng lập kế hoạch rõ ràng, thực thi yếu trong dài hạn và dễ gặp tình trạng quản lý trạng thái lộn xộn khi các lần chạy kéo dài. Điều này dẫn đến hiệu suất không ổn định và khó khăn trong việc mở rộng quy mô.

Để giải quyết vấn đề này, các agent hàng đầu như Claude Code, Deep ResearchManus đã áp dụng một mô hình chung: chúng lập kế hoạch trước, ngoại hóa ngữ cảnh làm việc (thường qua tệp hoặc shell) và ủy quyền các phần công việc riêng lẻ cho các sub-agent chuyên biệt. Deep Agents của LangChain đã đóng gói những nguyên tắc cơ bản này vào một môi trường runtime agent có thể tái sử dụng.

Thay vì phải tự thiết kế vòng lặp agent từ đầu, bạn chỉ cần gọi hàm create_deep_agent(...) và nhận được một biểu đồ thực thi được cấu hình sẵn, có khả năng lập kế hoạch, ủy quyền và quản lý trạng thái qua nhiều bước. Điều này giúp đơn giản hóa đáng kể quá trình phát triển các ứng dụng agent phức tạp.

Về mặt kỹ thuật, một Deep Agent được tạo thông qua create_deep_agent thực chất là một biểu đồ LangGraph. Không có môi trường runtime riêng biệt hay lớp điều phối ẩn nào. Điều này có nghĩa là các tính năng tiêu chuẩn của LangGraph đều hoạt động nguyên vẹn:

  • Streaming: Truyền dữ liệu theo thời gian thực.
  • Checkpoints và Interrupts: Lưu trạng thái và tạm dừng/tiếp tục thực thi.
  • Human-in-the-loop Controls: Cho phép con người can thiệp vào quá trình hoạt động của agent.

Mô Hình Hoạt Động Của Deep Agents

Về mặt khái niệm, luồng thực thi của Deep Agents diễn ra như sau:

User goal
  ↓
Deep Agent (LangGraph StateGraph)
  ├─ Plan: write_todos → cập nhật "todos" trong trạng thái
  ├─ Delegate: task(...) → chạy một subagent với vòng lặp công cụ riêng
  ├─ Context: ls/read_file/write_file/edit_file → lưu trữ ghi chú/tạo tác làm việc trung gian
  ↓
Final answer

Cấu trúc này cung cấp một khuôn khổ hữu ích cho quy trình “lập kế hoạch → thực hiện công việc → lưu trữ các tạo tác trung gian → tiếp tục” mà không cần phải tự mình phát minh định dạng kế hoạch, lớp bộ nhớ hay giao thức ủy quyền. Để tìm hiểu sâu hơn, bạn có thể tham khảo blog.langchain.com/deep-agentstài liệu chính thức của LangChain.

2. Vai Trò Của CopilotKit Trong Kiến Trúc Deep Agents

Deep Agents đẩy các phần quan trọng vào trạng thái rõ ràng (ví dụ: todos, tệp tin, tin nhắn), giúp việc kiểm tra các lần chạy dễ dàng hơn. Chính trạng thái rõ ràng này là yếu tố then chốt giúp việc tích hợp CopilotKit trở nên khả thi và hiệu quả.

CopilotKit: Cầu Nối Giữa UI và Agent

CopilotKit là một môi trường runtime frontend được thiết kế để giữ trạng thái UI đồng bộ với quá trình thực thi của agent. Nó thực hiện điều này bằng cách truyền trực tiếp (streaming) các sự kiện của agent và các cập nhật trạng thái theo thời gian thực, sử dụng giao thức AG-UI làm nền tảng.

Lớp middleware CopilotKitMiddleware chính là thành phần cho phép frontend luôn đồng bộ hoàn hảo với agent khi nó hoạt động. Điều này đảm bảo rằng người dùng luôn nhìn thấy thông tin cập nhật nhất từ agent mà không có độ trễ đáng kể. Bạn có thể đọc tài liệu chi tiết tại docs.copilotkit.ai/langgraph/deep-agents.

agent = create_deep_agent(
    model="openai:gpt-4o",
    tools=[get_weather],
    middleware=[CopilotKitMiddleware()], # để sử dụng công cụ và ngữ cảnh frontend
    system_prompt="Bạn là một trợ lý nghiên cứu hữu ích."
)

Sơ đồ dưới đây minh họa cách một hành động của người dùng trong UI được gửi qua AG-UI đến bất kỳ backend agent nào, và các phản hồi sẽ được truyền ngược lại dưới dạng các sự kiện tiêu chuẩn hóa:

Sơ đồ giao thức CopilotKit

3. Các Thành Phần Cốt Lõi Trong Hệ Thống

Để xây dựng ứng dụng trợ lý tìm việc làm, chúng ta sẽ sử dụng các thành phần cốt lõi sau đây:

3.1. Công Cụ Lập Kế Hoạch (Planning Tools)

Được tích hợp sẵn thông qua Deep Agents, các công cụ này cung cấp khả năng lập kế hoạch và tạo danh sách việc cần làm (to-do) mà không cần bạn phải tự viết công cụ lập kế hoạch riêng. Điều này giúp agent tự động chia nhỏ quy trình làm việc thành các bước có thể quản lý được.

# Ví dụ minh họa (không yêu cầu trong mã nguồn)
@tool
def todo_write(tasks: List[str]) -> str:
    formatted = "\n".join([f"- {task}" for task in tasks])
    return f"Danh sách việc cần làm đã tạo:\n{formatted}"

3.2. Subagents

Subagents cho phép agent chính ủy quyền các nhiệm vụ tập trung vào các vòng lặp thực thi riêng biệt. Mỗi sub-agent có lời nhắc (prompt), bộ công cụ (tools) và ngữ cảnh (context) riêng, giúp xử lý các phần công việc một cách cô lập và hiệu quả.

subagents = [
    {
        "name": "job-search-agent",
        "description": "Tìm kiếm các công việc liên quan và xuất ra các ứng viên công việc có cấu trúc.",
        "system_prompt": JOB_SEARCH_PROMPT,
        "tools": [internet_search],
    }
]

3.3. Công Cụ (Tools)

Đây là cách agent thực sự thực hiện các hành động trong thế giới thực hoặc tương tác với UI. Ví dụ, công cụ finalize() sẽ báo hiệu rằng agent đã hoàn thành nhiệm vụ.

@tool
def finalize() -> dict:
    """Báo hiệu rằng agent đã hoàn thành."""
    return {"status": "done"}

3.4. Cách Deep Agents Được Triển Khai (Middleware)

Nếu bạn thắc mắc làm thế nào hàm create_deep_agent() có thể “tiêm” khả năng lập kế hoạch, quản lý tệp và subagents vào một agent LangGraph thông thường, câu trả lời nằm ở middleware. Mỗi tính năng được triển khai dưới dạng một middleware riêng biệt. Theo mặc định, ba middleware sau được gắn vào:

  • To-do list middleware: Bổ sung công cụ write_todos và các chỉ dẫn thúc đẩy agent lập kế hoạch rõ ràng và cập nhật danh sách việc cần làm trực tiếp trong các nhiệm vụ đa bước.
  • Filesystem middleware: Thêm các công cụ quản lý tệp (ls, read_file, write_file, edit_file) để agent có thể ngoại hóa ghi chú và tạo tác thay vì nhồi nhét mọi thứ vào lịch sử trò chuyện.
  • Subagent middleware: Bổ sung công cụ task, cho phép agent chính ủy quyền công việc cho các subagent với ngữ cảnh cô lập và lời nhắc/công cụ riêng của chúng.

Đây chính là lý do Deep Agents mang lại cảm giác “được cấu hình sẵn” mà không cần giới thiệu một môi trường runtime hoàn toàn mới. Nếu bạn muốn tìm hiểu sâu hơn, tài liệu middleware được liên kết sẽ cung cấp chi tiết triển khai chính xác.

Các thành phần liên quan trong hệ thống

Chúng Ta Sẽ Xây Dựng Gì?

Với những kiến thức nền tảng trên, chúng ta sẽ tạo ra một agent có khả năng:

  • Chấp nhận CV (PDF) và trích xuất kỹ năng + ngữ cảnh.
  • Sử dụng Deep Agents để lập kế hoạch và điều phối các sub-agent.
  • Tìm kiếm việc làm liên quan trên web bằng các công cụ (ví dụ: Tavily).
  • Truyền kết quả của công cụ về UI qua CopilotKit (AG-UI) theo thời gian thực.

Chúng ta sẽ thấy những khái niệm này hoạt động như thế nào khi xây dựng agent.

4. Frontend: Kết Nối Agent Với Giao Diện Người Dùng (Next.js)

Phần frontend sẽ được xây dựng trên Next.js, đóng vai trò là cầu nối trực quan giữa người dùng và sức mạnh của Deep Agents. Dưới đây là cấu trúc thư mục chính của frontend:

.
├── src/                               ← Frontend Next.js
│   ├── app/
│   │   ├── page.tsx                      
│   │   ├── layout.tsx                 ← CopilotKit provider
│   │   └── api/
│   │       ├── upload-resume/route.ts ← Endpoint tải lên
│   │       └── copilotkit/route.ts    ← CopilotKit AG-UI runtime
│   ├── components/
│   │   ├── ChatPanel.tsx              ← Chat + thu thập công cụ
│   │   ├── ResumeUpload.tsx           ← UI tải lên PDF
│   │   ├── JobsResults.tsx            ← Trình hiển thị bảng công việc
│   │   └── LivePreviewPanel.tsx          
│   └── lib/
│       └── types.ts   
├── package.json                     
├── next.config.ts                   
└── README.md

Cài đặt frontend Next.js

4.1. Bước 1: CopilotKit Provider & Cấu Trúc Layout

Để bắt đầu, chúng ta cần cài đặt các gói CopilotKit cần thiết:

npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime
  • @copilotkit/react-core: Cung cấp các React hooks và context cốt lõi để kết nối UI của bạn với một backend agent tương thích AG-UI.
  • @copilotkit/react-ui: Cung cấp các thành phần UI sẵn có như <CopilotChat /> để xây dựng giao diện chat hoặc trợ lý AI một cách nhanh chóng.
  • @copilotkit/runtime: Là môi trường runtime phía máy chủ, cung cấp một API endpoint và cầu nối giữa frontend với một backend agent tương thích AG-UI bên ngoài sử dụng HTTP và SSE.

Các gói CopilotKit

Thành phần <CopilotKit> phải bao bọc các phần của ứng dụng có nhận biết Copilot. Trong hầu hết các trường hợp, tốt nhất là đặt nó xung quanh toàn bộ ứng dụng, như trong tệp layout.tsx:

import type { Metadata } from "next";

import { CopilotKit } from "@copilotkit/react-core";
import "./globals.css";
import "@copilotkit/react-ui/styles.css";

export const metadata: Metadata = {
  title: "Job Finder | Deep Agents with CopilotKit",
  description: "A job search assistant powered by Deep Agents and CopilotKit",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={"antialiased"}>
        <CopilotKit runtimeUrl="/api/copilotkit" agent="job_application_assistant">
          {children}
        </CopilotKit>
      </body>
    </html>
  );
}

Tại đây, runtimeUrl="/api/copilotkit" trỏ đến API route của Next.js mà CopilotKit sử dụng để giao tiếp với backend agent. Mỗi trang sẽ được bao bọc trong ngữ cảnh này, cho phép các thành phần UI biết agent nào sẽ gọi và nơi gửi yêu cầu.

4.2. Bước 2: Next.js API Route: Proxy đến FastAPI

API route này của Next.js đóng vai trò như một proxy mỏng giữa trình duyệt và Deep Agents. Nó thực hiện các chức năng sau:

  • Chấp nhận các yêu cầu CopilotKit từ UI.
  • Chuyển tiếp chúng đến agent qua AG-UI.
  • Truyền trực tiếp trạng thái agent và các sự kiện trở lại frontend.

Thay vì để frontend giao tiếp trực tiếp với agent FastAPI, tất cả các yêu cầu sẽ đi qua một endpoint duy nhất là /api/copilotkit.

import {
  CopilotRuntime,
  ExperimentalEmptyAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphHttpAgent } from "@copilotkit/runtime/langgraph";
import { NextRequest } from "next/server";

const serviceAdapter = new ExperimentalEmptyAdapter();

const runtime = new CopilotRuntime({
  agents: {
    job_application_assistant: new LangGraphHttpAgent({
      url: process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123",
    }),
  },
});

export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: "/api/copilotkit",
  });

  return handleRequest(req);
};

Giải thích ngắn gọn về đoạn mã trên:

  • Đoạn mã đăng ký agent job_application_assistant.
  • LangGraphHttpAgent: Định nghĩa một endpoint agent LangGraph từ xa. Nó trỏ đến backend Deep Agents đang chạy trên FastAPI.
  • ExperimentalEmptyAdapter: Một adapter đơn giản, không làm gì (no-op), được sử dụng khi backend agent tự xử lý các cuộc gọi LLM và điều phối của nó.
  • copilotRuntimeNextJSAppRouterEndpoint: Một hàm trợ giúp nhỏ thích ứng môi trường runtime của Copilot với một API route của Next.js App Router và trả về hàm handleRequest.

4.3. Bước 3: Endpoint API Tải Lên CV

API route này (src\app\api\upload-resume\route.ts) xử lý việc tải lên CV từ frontend và chuyển tiếp chúng đến backend FastAPI. Nó thực hiện các bước sau:

  • Chấp nhận các tệp tải lên dạng multipart từ trình duyệt.
  • Proxy tệp đến trình phân tích CV ở backend.
  • Trả về văn bản được trích xuất và các kỹ năng cho UI.

Việc giữ logic phân tích CV ở backend cho phép agent tái sử dụng cùng logic và giữ cho frontend nhẹ nhàng.

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  try {
    const formData = await req.formData();
    const file = formData.get("file") as File;

    if (!file) {
      return NextResponse.json({ error: "No file provided" }, { status: 400 });
    }

    const backendFormData = new FormData();
    backendFormData.append("file", file);

    const backendUrl = process.env.BACKEND_URL || "http://localhost:8123";
    const response = await fetch(`${backendUrl}/api/upload-resume`, {
      method: "POST",
      body: backendFormData,
    });

    if (!response.ok) {
      throw new Error("Backend upload failed");
    }

    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: error instanceof Error ? error.message : "Upload failed" },
      { status: 500 }
    );
  }
}

4.4. Bước 4: Xây Dựng Các Thành Phần Chính

Chúng ta sẽ đi sâu vào logic cốt lõi của từng thành phần quan trọng, bỏ qua các chi tiết UI/styling để tập trung vào chức năng. Bạn có thể tìm thấy toàn bộ mã nguồn của các thành phần tại repository trong thư mục src\components.

Các thành phần này sử dụng CopilotKit hooks (như useCopilotReadable) để kết nối mọi thứ lại với nhau.

✅ Thành Phần Tải Lên CV (Resume Upload Component)

Thành phần client này xử lý việc chọn CV và chuyển tiếp tệp đến backend để phân tích cú pháp. Nó chấp nhận tệp PDF/TXT, gửi yêu cầu POST đến /api/upload-resume và đẩy văn bản cùng các kỹ năng được trích xuất lên thành phần cha.

"use client";
import { useRef, useState } from "react";

type ResumeUploadResponse = { success: boolean; text: string; skills: string[]; filename: string };

export function ResumeUpload({ onUploadSuccess }: { onUploadSuccess(d: ResumeUploadResponse): void }) {
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  const onSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
    setError(null);
    const f = e.target.files?.[0] ?? null;
    if (f && !["application/pdf", "text/plain"].includes(f.type)) {
      setSelectedFile(null);
      setError("Please upload a PDF or TXT file");
      e.target.value = ""; // cho phép chọn lại cùng một tệp
      return;
    }
    setSelectedFile(f);
  };

  const onSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!selectedFile) return;

    setIsLoading(true);
    setError(null);

    try {
      const fd = new FormData();
      fd.append("file", selectedFile);

      const res = await fetch("/api/upload-resume", { method: "POST", body: fd });
      if (!res.ok) throw new Error("Upload failed");

      onUploadSuccess((await res.json()) as ResumeUploadResponse);

      setSelectedFile(null);
      if (inputRef.current) inputRef.current.value = "";
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to upload resume");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={onSubmit}>
      <input ref={inputRef} type="file" accept=".pdf,.txt" onChange={onSelect} />
      <button disabled={!selectedFile || isLoading}>{isLoading ? "Uploading..." : "Upload Resume"}</button>
      {error && <p>{error}</p>}
      {/* ... UI/styling được lược bỏ ... */}
    </form>
  );
}

Giải thích ngắn gọn:

  • Chấp nhận tệp PDF/TXT từ người dùng.
  • Gửi tệp đến /api/upload-resume bằng FormData.
  • Nhận văn bản được trích xuất + kỹ năng từ backend.
  • Truyền dữ liệu đó qua onUploadSuccess để có thể được đưa vào agent sau này.

Xem mã đầy đủ tại src/components/ResumeUpload.tsx.

✅ Thành Phần Bảng Chat (Chat Panel Component)

Đây là UI cốt lõi kết nối người dùng, agent và đầu ra công cụ. Bảng Chat thực hiện các chức năng sau:

  • Nhúng CopilotChat để xử lý đầu vào hội thoại và truyền trực tiếp các phản hồi của agent.
  • Sử dụng useCopilotReadable để liên tục đồng bộ văn bản CV, các kỹ năng được phát hiện và sở thích của người dùng vào ngữ cảnh của agent.
  • Chặn các cuộc gọi công cụ (như update_jobs_list) để cập nhật trạng thái UI cục bộ mà không làm đổ JSON công việc vào cuộc trò chuyện.

Chúng ta cũng xây dựng UI hội thoại bằng cách sử dụng <CopilotChat /> trong thành phần này.

"use client";

import { useState, useRef } from "react";
import { useDefaultTool, useCopilotReadable } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";
import { ResumeUpload } from "./ResumeUpload";
import { JobsResults } from "./JobsResults";

export function ChatPanel() {
  // trạng thái form + CV (tiêu đề, vị trí, kỹ năng, văn bản CV…)
  const [jobs, setJobs] = useState<JobPosting[]>([]);
  const processedKeyRef = useRef<string | null>(null); // chống trùng lặp các cuộc gọi công cụ

  // Thu thập đầu ra công cụ
  useDefaultTool({
    render: ({ name, status, args, result }) => {
      if (name === "update_jobs_list" && status === "complete" && result?.jobs_list) {
        const key = JSON.stringify({
          len: result.jobs_list.length,
          first: result.jobs_list[0]?.url,
        });

        if (processedKeyRef.current !== key) {
          processedKeyRef.current = key;

          // Tránh setState trong quá trình render
          queueMicrotask(() => {
            setJobs(result.jobs_list);
          });
        }
      }

      // Render các cuộc gọi công cụ nội tuyến
      return (
        <details>
          ...
        </details>
      );
    },
  });

  // Gửi trạng thái UI + dữ liệu CV vào ngữ cảnh agent
  useCopilotReadable({
    description: "Job search preferences",
    value: {
      targetTitle,
      targetLocation,
      skillsHint,
      resumeText,
      detectedSkills,
    },
  });

  return (
    <div>
      {/* UI tải lên CV + kỹ năng được trích xuất */}
      {!resumeUploaded && <ResumeUpload onUploadSuccess={handleUploadSuccess} />}

      {/* Các input tìm kiếm việc làm (tiêu đề / vị trí / kỹ năng) */}

      {/* UI chat của CopilotKit */}
      <CopilotChat />

      {/* Đầu ra có cấu trúc được render bên ngoài chat */}
      <JobsResults jobs={jobs} />
    </div>
  );
}

Xem mã đầy đủ tại src/components/ChatPanel.tsx.

✅ Thành Phần Hiển Thị Kết Quả Công Việc (Jobs Results Component)

Đây là một thành phần trình bày thuần túy. Nó nhận mảng jobs (được điền khi update_jobs_list hoàn thành) và hiển thị nó dưới dạng bảng, giữ cho đầu ra chat luôn sạch sẽ và dễ đọc.

"use client";
import { JobPosting } from "@/lib/types";

export function JobsResults({ jobs }: { jobs: JobPosting[] }) {
  if (!jobs.length) return null;

  return (
    <div className="mt-4 bg-white border border-slate-200 rounded-lg shadow-sm overflow-hidden">
      <div className="px-4 py-3 border-b border-slate-200">
        <h3 className="font-semibold text-slate-900">Jobs</h3>
      </div>

      <div className="overflow-x-auto">
        <table className="w-full text-sm">
          <thead>{/* Company | Title | Location | Link | Good match */}</thead>
          <tbody>
            {jobs.map((j, idx) => (
              <tr key={idx} className="border-t border-slate-100 text-black">
                <td className="px-4 py-2">{j.company}</td>
                <td className="px-4 py-2">{j.title}</td>
                <td className="px-4 py-2">{j.location}</td>
                <td className="px-4 py-2">
                  <a className="text-blue-600 hover:underline" href={j.url} target="_blank" rel="noreferrer">
                    Open
                  </a>
                </td>
                <td className="px-4 py-2">{j.goodMatch || "Yes"}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

Xem mã đầy đủ tại src/components/JobsResults.tsx.

4.5. Bước 5: Kết Nối UI Chat với Agent

Tại thời điểm này, tất cả các mảnh ghép đã ở đúng vị trí. Trang này chỉ đơn giản là hiển thị ChatPanel, đã được kết nối hoàn toàn với backend Deep Agents thông qua CopilotKit. Một LivePreviewPanel thứ cấp được gắn song song với nó. Vì các cuộc gọi công cụ đã được hiển thị nội tuyến trong CopilotChat, bảng điều khiển này là tùy chọn và hiện đóng vai trò là không gian làm việc thử nghiệm cho việc gỡ lỗi và trực quan hóa phong phú hơn.

"use client";

import { ChatPanel } from "@/components/ChatPanel";
import { LivePreviewPanel } from "@/components/LivePreviewPanel";

export default function Page() {
  return (
    <main className="min-h-screen flex flex-col">
      {/* Tiêu đề ứng dụng (thương hiệu + mô tả) */}
      <header>
        <h1>Trợ Lý Tìm Việc Làm</h1>
        <p>Tìm kiếm việc làm cá nhân hóa với AI.</p>
        {/* ... huy hiệu / styling được lược bỏ ... */}
      </header>

      <div className="grid lg:grid-cols-3 gap-6">
        <section className="lg:col-span-2">
          <ChatPanel />
        </section>

        <aside className="lg:col-span-1">
          <LivePreviewPanel />
          {/* Các cuộc gọi công cụ đã được render bên trong CopilotChat */}
          {/* Bảng điều khiển này là tùy chọn và hiện được sử dụng để thử nghiệm */}
        </aside>
      </div>

      {/* Footer */}
      {/* ... nội dung footer được lược bỏ ... */}
    </main>
  );
}

5. Backend: Xây Dựng Dịch Vụ Agent (FastAPI + Deep Agents + AG-UI)

Bây giờ chúng ta sẽ xây dựng backend FastAPI, nơi sẽ lưu trữ Deep Agent của chúng ta. Dưới thư mục /agent là một máy chủ FastAPI chạy agent Ứng Dụng Việc Làm. Dưới đây là cấu trúc dự án của backend:

.
├── agent/                             ← Backend Deep Agents
│   ├── main.py                        ← FastAPI + AG-UI endpoint
│   ├── agent.py                       ← Biểu đồ & công cụ Deep Agents
│   ├── pyproject.toml                 ← Python deps (uv)
│   └── uv.lock
...

Ở cấp độ cao, backend chịu trách nhiệm cho các tác vụ sau:

  • Cung cấp một endpoint agent tương thích CopilotKit (để truyền trực tiếp trạng thái agent và các cuộc gọi công cụ).
  • Cung cấp một endpoint /api/upload-resume để phân tích CV.
  • Xây dựng một biểu đồ Deep Agents có khả năng lập kế hoạch, ủy quyền cho các sub-agent và tìm kiếm trên web các công việc phù hợp.

Backend sử dụng uv để quản lý các phần phụ thuộc. Cài đặt nó nếu bạn chưa có trong hệ thống:

pip install uv

Phiên bản uv

Khởi tạo một dự án uv mới bằng lệnh sau. Điều này sẽ tạo ra một tệp pyproject.toml mới:

cd agent
uv init

Hầu hết các công cụ AI được sử dụng trong backend này (đặc biệt là AG-UI Strands) hiện yêu cầu Python 3.12+ trở lên, vì vậy hãy đảm bảo chỉ định uv sử dụng phiên bản Python tương thích bằng lệnh này:

uv python pin 3.12

uv pin

Sau đó cài đặt các phần phụ thuộc. Điều này cũng sẽ tạo môi trường ảo của dự án:

uv add copilotkit deepagents fastapi langchain langchain-openai pypdf python-dotenv python-multipart tavily-python "uvicorn[standard]"
  • copilotkit: Kết nối các agent với frontend bằng streaming, công cụ và trạng thái chia sẻ.
  • deepagents: Framework agent ưu tiên lập kế hoạch cho việc thực thi đa bước.
  • fastapi: Web framework cung cấp API của agent.
  • langchain: Lớp điều phối agent và công cụ.
  • langchain-openai: Tích hợp mô hình OpenAI cho LangChain.
  • pypdf: Trích xuất văn bản từ các tệp PDF.
  • python-dotenv: Tải các biến môi trường từ .env.
  • python-multipart: Cho phép tải lên tệp trong FastAPI.
  • tavily-python: Công cụ tìm kiếm web cho nghiên cứu agent thời gian thực.
  • uvicorn[standard]: Máy chủ ASGI để chạy FastAPI.

Các gói Python

Bây giờ, chạy lệnh sau để tạo một tệp uv.lock được ghim với các phiên bản chính xác:

uv sync

5.1. Thêm Khóa API Cần Thiết

Tạo một tệp .env trong thư mục agent và thêm Khóa API OpenAIKhóa API Tavily của bạn vào tệp. Tôi đã đính kèm các liên kết tài liệu để dễ dàng làm theo.

OPENAI_API_KEY=sk-proj-...
TAVILY_API_KEY=tvly-dev-...
OPENAI_MODEL=gpt-4-turbo

Khóa API OpenAI

Khóa API Tavily

5.2. Bước 1: Định Nghĩa Hành Vi Của Agent

Chúng ta bắt đầu bằng cách định nghĩa hành vi của agent bằng một lời nhắc hệ thống duy nhất, nghiêm ngặt trong tệp agent.py. Trong Deep Agents, lời nhắc hệ thống hoạt động như lớp điều khiển cho quy trình làm việc, kết hợp lập kế hoạch và ủy quyền để phân tách các nhiệm vụ phức tạp thành các bước có thứ tự.

Biến MAIN_SYSTEM_PROMPT điều phối các công cụ và sub-agent bằng cách thực thi một chuỗi thực thi cố định. Lời nhắc này đảm bảo:

  • Các hành động bên ngoài luôn xảy ra thông qua các công cụ.
  • Trạng thái UI được cập nhật một cách có kiểm soát.
  • Thực thi kết thúc một cách xác định bằng finalize().
MAIN_SYSTEM_PROMPT = """
Bạn là một agent sử dụng công cụ.

Các quy tắc nghiêm ngặt:
- Không bao giờ bao gồm chi tiết công việc, URL hoặc JSON trong tin nhắn trợ lý.
- Chỉ xuất ra công việc qua update_jobs_list(jobs_json).
- Một công việc hợp lệ phải là một trang chi tiết công việc duy nhất trên ATS hoặc trang tuyển dụng của công ty.
- KHÔNG sử dụng các trang tổng hợp việc làm hoặc trang danh sách/tìm kiếm.
- company PHẢI là công ty tuyển dụng (không bao giờ là Lever/Greenhouse/Ashby/Workday/Talent.com/etc).

Schema (khóa chính xác):
- company, title, location, url, goodMatch

Các bước:
1) Gọi internet_search(query) chính xác một lần.
2) Từ các kết quả trả về, chọn tối đa 5 vị trí tuyển dụng cá nhân hợp lệ.
3) Gọi update_jobs_list(jobs_json) một lần.
4) Gọi finalize().
5) Đầu ra: Tìm thấy N công việc.

Nếu không tìm thấy 5 công việc hợp lệ, hãy trả về càng nhiều công việc hợp lệ càng tốt.
"""

JOB_SEARCH_PROMPT định nghĩa hành vi của một sub-agent chuyên biệt. Trách nhiệm của nó giới hạn trong việc tìm kiếm các công việc liên quan và trả về kết quả có cấu trúc theo một định dạng được kiểm soát.

JOB_SEARCH_PROMPT = (
    "Tìm kiếm và chọn 5 vị trí tuyển dụng thực phù hợp với chức danh, địa điểm và kỹ năng của người dùng. "
    "Chỉ xuất ra định dạng khối này (không có văn bản bổ sung trước/sau wrapper):\n"
    "<JOBS>\n"
    '[{"company":"...","title":"...","location":"...","link":"https://...","Good Match":"một câu"},'
    ' {"company":"...","title":"...","location":"...","link":"https://...","Good Match":"một câu"},'
    ' {"company":"...","title":"...","location":"...","link":"https://...","Good Match":"một câu"},'
    ' {"company":"...","title":"...","location":"...","link":"https://...","Good Match":"một câu"},'
    ' {"company":"...","title":"...","location":"...","link":"https://...","Good Match":"một câu"}]'
    "\n</JOBS>"
    "Mỗi công việc PHẢI:"
    "- Là một vị trí mở duy nhất (không phải một trang tổng hợp việc làm, trang lọc hoặc chỉ mục công việc của công ty)"
    "- Thuộc về một công ty cụ thể với một trang mô tả công việc chuyên dụng"
    "Bạn PHẢI:"
    "- Sử dụng internet_search để tìm các công việc liên quan."
    "- KHÔNG xuất danh sách việc làm, JSON hoặc URL trong tin nhắn."
    "- Trả về mọi thứ CHỈ bằng cách gọi công cụ cha `update_jobs_list` với một chuỗi JSON."
)

5.3. Bước 2: Thêm Tiện Ích Phân Tích CV và Trích Xuất Kỹ Năng

Chúng ta trích xuất văn bản thô từ các tệp PDF được tải lên bằng pypdf. Chức năng này được endpoint tải lên của FastAPI sử dụng để chuyển CV thành văn bản thuần túy.

def parse_pdf_resume(file_path: str) -> str:
    with open(file_path, "rb") as file:
        reader = PdfReader(file)
        return "".join(page.extract_text() for page in reader.pages)

Tiếp theo, chúng ta trích xuất các tín hiệu có cấu trúc nhẹ (ngôn ngữ, frameworks, công cụ) từ CV. Điều này ảnh hưởng đến các truy vấn tìm kiếm việc làm và chất lượng phù hợp.

def extract_skills_from_resume(resume_text: str) -> List[str]:
    skills_db = {
        "languages": ["Python", "JavaScript", "Go"],
        "frameworks": ["React", "FastAPI", "Django"],
        "cloud": ["AWS", "Docker", "Kubernetes"],
    }

    found = set()
    text = resume_text.lower()

    for skills in skills_db.values():
        for skill in skills:
            if skill.lower() in text:
                found.add(skill)

    return list(found)

5.4. Bước 3: Định Nghĩa Công Cụ Để Tìm Kiếm, Cập Nhật UI và Chấm Dứt

Các công cụ là bề mặt tích hợp giữa agent và thế giới bên ngoài / UI.

Công cụ internet_search chịu trách nhiệm khám phá các vị trí tuyển dụng thực. Nó cố tình lấy thêm kết quả tìm kiếm, lọc ra bất kỳ URL nào chứa chuỗi con “xấu” (các trang tổng hợp việc làm/tìm kiếm) qua BAD_URL_SUBSTRINGS và chỉ trả về max_results lượt truy cập sạch đầu tiên.

BAD_URL_SUBSTRINGS = [
    "linkedin.com/jobs/search",
    "linkedin.com/jobs/",
    "builtin.com/jobs",
    "naukri.com",
    "glassdoor.",
    "/jobs/search",
    "/search?",
]

def _is_bad(url: str) -> bool:
    u = (url or "").lower()
    return any(p in u for p in BAD_URL_SUBSTRINGS)

@tool
def internet_search(query: str, max_results: int = 10) -> List[Dict[str, Any]]:
    """
    Tìm kiếm việc làm bằng API Tavily. Luôn trả về tối đa 5 kết quả.
    """
    tavily_key = os.environ.get("TAVILY_API_KEY")
    if not tavily_key:
        raise RuntimeError("TAVILY_API_KEY chưa được đặt")

    client = TavilyClient(api_key=tavily_key)
    res = client.search(
        query=query,
        max_results=max_results * 3,  # lấy nhiều hơn, sau đó lọc
        include_raw_content=False,
        topic="general",
    )

    trimmed = []
    for r in res.get("results", []):
        url = r.get("url") or ""
        if _is_bad(url):
            continue
        trimmed.append(
            {
                "title": r.get("title"),
                "url": url,
                "content": (r.get("content") or "")[:400],
            }
        )
        if len(trimmed) == max_results:
            break

    print(f"[SEARCH] Trả về {len(trimmed)} kết quả đã lọc")
    print(trimmed)
    return trimmed

Công cụ update_jobs_list là cách duy nhất dữ liệu công việc có cấu trúc đến được frontend, giữ cho các cập nhật UI rõ ràng và JSON không bị đổ vào tin nhắn chat.

@tool
def update_jobs_list(jobs_json: str) -> Dict[str, Any]:
    """Gửi danh sách công việc đến trạng thái UI."""
    jobs = json.loads(jobs_json)
    print(f"[TOOL] update_jobs_list: {len(jobs)} công việc")
    return {"jobs_list": jobs}

Công cụ finalize báo hiệu rằng agent đã hoàn thành quy trình làm việc của nó.

@tool
def finalize() -> dict:
    """Báo hiệu hoàn thành."""
    print("[TOOL] finalize: Tìm kiếm việc làm hoàn tất")
    return {"status": "done"}

5.5. Bước 4: Tập Hợp Biểu Đồ Deep Agents Với Sub-agents

Bây giờ chúng ta kết nối mọi thứ và xây dựng biểu đồ Deep Agents với hàm build_agent().

def build_agent():
    """Xây dựng biểu đồ Deep Agents với giới hạn đệ quy phù hợp"""
    api_key = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        raise RuntimeError("Thiếu OPENAI_API_KEY")

    llm = ChatOpenAI(
        model=os.environ.get("OPENAI_MODEL", "gpt-4-turbo"),
        temperature=0.7,
        api_key=api_key,
    )

    tools = [
        internet_search,
        update_jobs_list,
        finalize,
    ]

    subagents = [
        {
            "name": "job-search-agent",
            "description": "Tìm kiếm các công việc liên quan và xuất ra JSON <JOBS>.",
            "system_prompt": JOB_SEARCH_PROMPT,
            "tools": [internet_search],
        },
    ]

    agent_graph = create_deep_agent(
        model=llm,
        system_prompt=MAIN_SYSTEM_PROMPT,
        tools=tools,
        subagents=subagents,
        middleware=[CopilotKitMiddleware()],
        checkpointer=MemorySaver(),
    )

    print("[AGENT] Biểu đồ Deep Agents đã được tạo")
    print(agent_graph)

    return agent_graph

5.6. Bước 5: Thiết Lập FastAPI

Bước cuối cùng là khởi tạo backend và công khai nó dưới dạng một ứng dụng FastAPI. Nó cũng xử lý việc tải lên CV và phân tích PDF, biến các tệp thô thành văn bản và kỹ năng sạch trước khi chúng được gửi đến agent.

import os
from fastapi import FastAPI, HTTPException, File, UploadFile
import uvicorn
from dotenv import load_dotenv
from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from copilotkit import LangGraphAGUIAgent
from agent import build_agent, parse_pdf_resume, extract_skills_from_resume
import tempfile

load_dotenv()

app = FastAPI(
    title="Job Application Assistant",
    description="Tìm kiếm cơ hội việc làm cá nhân hóa dựa trên kỹ năng và sở thích",
    version="1.0.0",
)

try:
    agent_graph = build_agent()
    print(agent_graph)
    add_langgraph_fastapi_endpoint(
        app=app,
        agent=LangGraphAGUIAgent(
            name="job_application_assistant",
            description="Tìm việc làm",
            graph=agent_graph,
        ),
        path="/",
    )
    print("[MAIN] Agent đã đăng ký")
except Exception as e:
    print(f"[ERROR] Không thể xây dựng agent: {str(e)}")
    raise

@app.get("/healthz")
async def health_check():
    """Kiểm tra sức khỏe"""
    return {
        "status": "healthy",
        "service": "job-application-assistant",
        "version": "1.0.0",
    }

@app.post("/api/upload-resume")
async def upload_resume(file: UploadFile = File(...)):
    """
    Tải lên và phân tích CV (PDF, DOCX, TXT).
    Trả về văn bản và kỹ năng đã trích xuất.
    """
    if not file:
        raise HTTPException(status_code=400, detail="Không có tệp nào được cung cấp")

    try:
        with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
            content = await file.read()
            tmp.write(content)
            tmp_path = tmp.name

        if file.filename.endswith(".pdf"):
            resume_text = parse_pdf_resume(tmp_path)
        else:
            # đối với các định dạng khác, chỉ đọc dưới dạng văn bản
            resume_text = content.decode("utf-8", errors="ignore")

        skills = extract_skills_from_resume(resume_text)

        os.unlink(tmp_path)

        return {
            "success": True,
            "text": resume_text[:1000],
            "skills": skills,
            "filename": file.filename,
        }

    except Exception as e:
        print(f"[ERROR] Tải lên CV thất bại: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

def main():
    """Chạy máy chủ"""
    host = os.getenv("SERVER_HOST", "0.0.0.0")
    port = int(os.getenv("SERVER_PORT", 8123))

    uvicorn.run(
        "main:app",
        host=host,
        port=port,
        reload=True,
        log_level="info",
    )

if __name__ == "__main__":
    main()

6. Chạy Ứng Dụng

Sau khi hoàn thành tất cả các phần của mã, đã đến lúc chạy ứng dụng cục bộ. Vui lòng đảm bảo rằng bạn đã thêm thông tin xác thực vào tệp agent/.env.

Từ thư mục gốc của dự án, điều hướng đến thư mục agent và khởi động máy chủ FastAPI:

cd agent
uv run python main.py

Backend sẽ khởi động trên http://localhost:8123.

Backend đang chạy

Trong một cửa sổ terminal mới, khởi động máy chủ phát triển frontend bằng cách sử dụng:

npm run dev

Frontend đang chạy

Khi cả hai máy chủ đang chạy, mở frontend trong trình duyệt của bạn tại http://localhost:3000/ để xem nó cục bộ.

Frontend

Sau đó, bạn tải lên CV của mình và tìm kiếm một truy vấn việc làm.

CV đã tải lên

Các cuộc gọi công cụ

Đầu ra

Dựa trên truy vấn việc làm, nó có thể tìm nạp một số lượng kết quả khác nhau. Dưới đây là một đầu ra khác!

Đầu ra

CopilotKit cũng cung cấp Agent Inspector, là một chế độ xem runtime AG-UI trực tiếp cho phép bạn kiểm tra các lần chạy agent, ảnh chụp nhanh trạng thái, tin nhắn và các cuộc gọi công cụ khi chúng được truyền từ backend. Nó có thể truy cập được từ một nút copilotkit được phủ lên ứng dụng của bạn.

Agent Inspector

Agent Inspector

7. Luồng Dữ Liệu Trong Hệ Thống

Sau khi xây dựng cả frontend và dịch vụ agent, đây là cách dữ liệu thực sự luân chuyển giữa chúng. Bạn sẽ dễ dàng theo dõi nếu đã xây dựng cùng chúng tôi cho đến nay.

[Người dùng tải lên CV & gửi truy vấn việc làm]
        ↓
Next.js UI (ResumeUpload + CopilotChat)
        ↓
useCopilotReadable đồng bộ CV + sở thích
        ↓
POST /api/copilotkit (giao thức AG-UI)
        ↓
FastAPI + Deep Agents (endpoint /copilotkit)
        ↓
Ngữ cảnh CV + kỹ năng được đưa vào agent
        ↓
Điều phối Deep Agents
   ├─ internet_search (Tavily)
   ├─ lọc & chuẩn hóa công việc
   └─ update_jobs_list (gọi công cụ)
        ↓
AG-UI streaming (SSE)
        ↓
CopilotKit runtime nhận kết quả công cụ
        ↓
Frontend thu thập đầu ra công cụ
        ↓
Các công việc được hiển thị trong bảng + chat vẫn sạch sẽ

Kết Luận

Chúc mừng! Bạn đã thành công trong việc xây dựng một trợ lý tìm kiếm việc làm mạnh mẽ được cung cấp bởi LangChain Deep Agents, với CopilotKit đóng vai trò là lớp frontend trực quan và tương tác theo thời gian thực. Từ việc hiểu sâu về kiến trúc Deep Agents, tích hợp CopilotKit, đến triển khai các thành phần frontend bằng Next.js và xây dựng backend FastAPI mạnh mẽ, bạn đã trang bị cho mình những kiến thức và kỹ năng cần thiết để phát triển các ứng dụng AI thế hệ mới.

Hệ thống này không chỉ thể hiện sức mạnh của các agent AI có khả năng lập kế hoạch và ủy quyền phức tạp mà còn minh họa cách CopilotKit đơn giản hóa việc đồng bộ hóa trạng thái giữa agent và UI, mang lại trải nghiệm người dùng mượt mà và hiệu quả. Hy vọng rằng bạn đã học được nhiều điều giá trị từ hướng dẫn toàn diện này. Chúc bạn có một ngày tuyệt vời và tiếp tục khám phá thế giới rộng lớn của AI!

Bạn có thể kiểm tra công việc của tôi tại anmolbaranwal.com. Cảm ơn bạn đã đọc! 🥰

Theo dõi CopilotKit trên Twitter và chào hỏi, và nếu bạn muốn xây dựng điều gì đó thú vị, hãy tham gia cộng đồng Discord.

CopilotKit: UI React + cơ sở hạ tầng thanh lịch cho các Copilots AI, agent AI trong ứng dụng, chatbot AI và Textareas hỗ trợ AI 🪁.

Tìm hiểu cách xây dựng giao diện người dùng mạnh mẽ cho LangChain Deep Agents với CopilotKit để tạo ra các ứng dụng AI thông minh, có khả năng tương tác theo thời gian thực.

Chỉ mục