Hôm nay, chúng ta sẽ xây dựng một trợ lý tìm kiếm việc làm dựa trên Deep Agents (LangChain đã giới thiệuDeep Agents : một cách thức mới để xây dựng các hệ thống đa tác tử có cấu trúc, có khả năng lập kế hoạch, phân công và suy luận qua nhiều bước.Nó đi kèm với tính năng lập kế hoạch tích hợp, hệ thống tập tin để lưu trữ ngữ cảnh và khả năng tạo ra các tác nhân phụ. Nhưng việc kết nối tác nhân đó với giao diện người dùng thực sự vẫn khó khăn một cách đáng ngạc nhiên), sử dụng sơ yếu lý lịch của bạn (dưới dạng PDF) để tìm các vị trí tuyển dụng phù hợp và kết nối nó với giao diện người dùng Next.js trực tiếp bằng CopilotKit , để giao diện người dùng luôn đồng bộ với trợ lý trong thời gian thực.
Deep Agents là gì?
Hầu hết các tác nhân hiện nay chỉ là “LLM trong một vòng lặp + các công cụ“. Điều đó có thể hoạt động, nhưng nó thường khá hời hợt: không có kế hoạch rõ ràng, khả năng thực thi yếu trong thời gian dài và trạng thái lộn xộn khi quá trình chạy kéo dài.
Các tác nhân phổ biến như Claude Code , Deep Research và Manus giải quyết vấn đề này bằng cách tuân theo một mô hình chung: chúng lập kế hoạch trước, chuyển ngữ cảnh làm việc ra bên ngoài (thường thông qua các tệp hoặc giao diện dòng lệnh), và giao phó các phần việc riêng lẻ cho các tác nhân con.
Deep Agents đóng gói các thành phần cơ bản đó vào một môi trường chạy tác nhân có thể tái sử dụng.
Thay vì tự thiết kế vòng lặp tác nhân từ đầu, bạn chỉ cần gọi create_deep_agent(…)và nhận một biểu đồ thực thi được kết nối sẵn, biểu đồ này đã biết cách lập kế hoạch, phân công và quản lý trạng thái qua nhiều bước.

Về mặt thực tế, một Deep Agent được tạo ra create_deep_agentchỉ đơn giản là một đồ thị LangGraph. Không có lớp thực thi riêng biệt hoặc lớp điều phối ẩn nào.
Điều đó có nghĩa là các tính năng tiêu chuẩn của LangGraph hoạt động bình thường:
- phát trực tuyến
- điểm kiểm tra và gián đoạn
- kiểm soát có sự tham gia của con người
Mô hình tư duy (cách thức hoạt động)
Về mặt khái niệm, quy trình thực thi diễn ra như sau:
Mục tiêu người dùng
↓
Deep Agent (LangGraph StateGraph)
├─ Kế hoạch: write_todos → cập nhật “todos” trong trạng thái
├─ Ủy quyền: task(…) → chạy một subagent với vòng lặp công cụ riêng
├─ Ngữ cảnh: ls /read_file/write_file/edit_file → lưu trữ các ghi chú/tệp đang làm việc
↓
Câu trả lời cuối cùng
Điều đó cung cấp cho bạn một cấu trúc 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 sản phẩm trung gian → tiếp tục” mà không cần phải tự tạo ra định dạng kế hoạch, lớp bộ nhớ hoặc giao thức ủy quyền của riêng mình.
Bạn có thể đọc thêm tạiblog.langchain.com/deep-agentsvà xemtài liệu chính thức.
Vị trí của CopilotKit
Deep Agents đưa các thành phần quan trọng vào trạng thái tường minh (ví dụ todos: + tập tin + thông báo), giúp việc kiểm tra quá trình chạy dễ dàng hơn. Trạng thái tường minh đó cũng là điều giúp cho việc tích hợp với Copilotkit trở nên khả thi.
CopilotKit là một môi trường chạy giao diện người dùng giúp đồng bộ hóa trạng thái giao diện người dùng với quá trình thực thi tác nhân bằng cách truyền phát các sự kiện tác nhân và cập nhật trạng thái theo thời gian thực (sử dụng AG-UI ở phía sau).
Phần mềm trung gian này ( CopilotKitMiddleware) cho phép giao diện người dùng duy trì sự đồng bộ với tác nhân khi nó hoạt động. Bạn có thể đọc tài liệu tạidocs.copilotkit.ai/langgraph/deep-agents.
agent = create_deep_agent(
model= “openai:gpt-4o” ,
tools=[get_weather],
middleware=[CopilotKitMiddleware()], # cho các công cụ và ngữ cảnh giao diện người dùng
system_prompt= “Bạn là một trợ lý nghiên cứu hữu ích.”
)
Sơ đồ bên dưới minh họa cách một thao tác của người dùng trên giao diện người dùng được gửi qua AG-UI đến bất kỳ hệ thống phụ trợ nào của tác nhân và các phản hồi được truyền trở lại dưới dạng các sự kiện tiêu chuẩn.

2. Các thành phần cốt lõi
Dưới đây là các thành phần cốt lõi mà chúng ta sẽ sử dụng sau này:
1) Công cụ lập kế hoạch (được tích hợp sẵn thông qua Deep Agents) — chức năng lập kế hoạch/việc cần làm được tích hợp sẵn, cho phép tác nhân chia nhỏ quy trình làm việc thành các bước mà không cần bạn phải tự viết một công cụ lập kế hoạch riêng biệt.
# Ví dụ minh họa (không bắt buộc 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 đã được tạo:\n{formatted}”
2) Các tác nhân phụ — cho phép tác nhân chính phân công các nhiệm vụ cụ thể vào các vòng lặp thực thi riêng biệt. Mỗi tác nhân phụ có lời nhắc, công cụ và ngữ cảnh riêng.
subagents = [
{
“name” : “job-search-agent” ,
“description” : “Tìm kiếm các công việc phù hợp và xuất ra các ứng viên việc làm có cấu trúc.” ,
“system_prompt” : JOB_SEARCH_PROMPT,
“tools” : [internet_search],
}
]
3) Công cụ — đây là cách mà tác nhân thực sự thực hiện mọi việc. Ở đây, finalize()nó báo hiệu sự hoàn thành.
@tool
def finalize () -> dict:
“” “Báo hiệu rằng tác nhân đã hoàn thành.” “”
return { “status” : “done” }
Cách thức triển khai Deep Agents (Middleware)
Nếu bạn đang thắc mắc làm thế nào mà create_deep_agent()phần mềm trung gian thực sự tích hợp việc lập kế hoạch, các tập tin và các tác nhân con vào một tác nhân LangGraph thông thường, thì câu trả lời là thông qua phần mềm trung gian.
Mỗi tính năng được triển khai như một middleware riêng biệt. Theo mặc định, có ba middleware được đính kèm:
Phần mềm trung gian danh sách việc cần làm : bổ sung write_todoscông cụ và hướng dẫn giúp tác nhân lập kế hoạch và cập nhật danh sách việc cần làm trực tiếp trong quá trình thực hiện các tác vụ nhiều bước.
Phần mềm trung gian hệ thống tập tin : bổ sung các công cụ tập tin ( ls, read_file, write_file, edit_file) để tác nhân có thể xuất các ghi chú và hiện vật ra bên ngoài thay vì nhét tất cả vào lịch sử trò chuyện.
Phần mềm trung gian cho tác nhân phụ : bổ sung taskcông cụ cho phép tác nhân chính ủy thác công việc cho các tác nhân phụ với ngữ cảnh riêng biệt và các lời nhắc/công cụ riêng.
Đây chính là điều khiến Deep Agents có cảm giác như được “lập trình sẵn” mà không cần giới thiệu một runtime mới. Nếu bạn muốn tìm hiểu sâu hơn,tài liệu về middlewaređược liên kết sẽ trình bày chi tiết cách triển khai cụ thể.

Chúng ta đang xây dựng cái gì vậy?
Hãy tạo một tác nhân:
Chấp nhận sơ yếu lý lịch (PDF) và trích xuất kỹ năng + bối cảnh.
Sử dụng Deep Agents để lập kế hoạch và điều phối các tác nhân phụ.
Tìm kiếm trên web các công việc phù hợp bằng các công cụ (Tavily)
Công cụ Streams trả về kết quả cho giao diện người dùng thông qua CopilotKit (AG-UI).Đây làkho lưu trữ GitHub.
Chúng ta sẽ thấy một số khái niệm này được áp dụng trong thực tế khi xây dựng tác nhân.
3. Giao diện người dùng: kết nối tác nhân với giao diện người dùng
Trước tiên, hãy xây dựng phần giao diện người dùng. Đây là cấu trúc thư mục của chúng ta.
Thư mục này srcchứa giao diện người dùng Next.js, bao gồm UI, các thành phần dùng chung và tuyến API CopilotKit ( /api/copilotkit) được sử dụng để liên lạc với tác nhân.
.
├── src/ ← Giao diện người dùng Next.js
│ ├── app/
│ │ ├── page.tsx
│ │ ├── layout.tsx ← Nhà cung cấp CopilotKit
│ │ └── api/
│ │ ├── upload-resume/route.ts ← Điểm cuối tải lên
│ │ └── copilotkit/route.ts ← Môi trường chạy CopilotKit AG-UI
│ ├── components/
│ │ ├── ChatPanel.tsx ← Trò chuyện + chụp công cụ
│ │ ├── ResumeUpload.tsx ← Giao diện người dùng tải lên PDF
│ │ ├── JobsResults.tsx ← Trình hiển thị bảng việc làm
│ │ └── LivePreviewPanel.tsx
│ └── lib/
│ └── types.ts
├── package.json
├── next.config.ts
└── README.md

Bước 1: Nhà cung cấp và bố cục CopilotKit
Cài đặt các gói CopilotKit cần thiết.
npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime
@copilotkit/react-coreCung cấp các hook và ngữ cảnh cốt lõi của React để kết nối giao diện người dùng của bạn với hệ thống phụ trợ của tác nhân tương thích với AG-UI.
@copilotkit/react-uiCung cấp các thành phần giao diện người dùng có sẵn, giúp <CopilotChat />xây dựng giao diện trò chuyện hoặc trợ lý AI một cách nhanh chóng.
@copilotkit/runtimeĐây là môi trường chạy phía máy chủ, cung cấp điểm cuối API và kết nối giao diện người dùng với hệ thống phụ trợ tác nhân tương thích AG-UI bên ngoài bằng giao thức HTTP và SSE.
Thành phần này <CopilotKit>phải bao bọc các phần hỗ trợ Copilot trong ứng dụng của bạn. Trong hầu hết các trường hợp, tốt nhất là nên đặt nó bao quanh toàn bộ ứng dụng, như trong 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 : “Tìm việc làm | Deep Agents với CopilotKit” ,
description : “Trợ lý tìm việc làm được hỗ trợ bởi Deep Agents và 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”con trỏ trỏ đến tuyến API Next.js mà CopilotKit sử dụng để giao tiếp với máy chủ phụ trợ của agent.
Mỗi trang đều được bao bọc trong ngữ cảnh này để các thành phần giao diện người dùng biết cần gọi tác nhân nào và gửi yêu cầu đến đâu.
Bước 2: Định tuyến API Next.js: Chuyển hướng đến FastAPI
Tuyến API Next.js này hoạt động như một máy chủ proxy đơn giản giữa trình duyệt và Deep Agents. Nó:
Chấp nhận các yêu cầu CopilotKit từ giao diện người dùng.
Chuyển tiếp chúng đến đại lý thông qua AG-UI.
Truyền trạng thái và sự kiện của tác nhân về giao diện người dùng.
Thay vì để giao diện người dùng (frontend) giao tiếp trực tiếp với tác nhân FastAPI, tất cả các yêu cầu đều được xử lý thông qua một điểm cuối duy nhất /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 || ” //localhost:8123″ , } ), }, } ); export const POST = async ( req: NextRequest ) => { const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint ({ runtime, serviceAdapter, endpoint : “/api/copilotkit” , }); return handleRequest (req); };
Dưới đây là lời giải thích đơn giản về đoạn mã trên:
Đoạn mã trên đăng ký job_application_assistanttác nhân.
LangGraphHttpAgent: Định nghĩa một điểm cuối tác nhân LangGraph từ xa. Nó trỏ đến phần phụ trợ Deep Agents đang chạy trên FastAPI.
ExperimentalEmptyAdapter: Bộ chuyển đổi đơn giản không làm gì cả, được sử dụng khi phần phụ trợ của tác nhân tự xử lý các cuộc gọi LLM và điều phối.
copilotRuntimeNextJSAppRouterEndpoint: một hàm hỗ trợ nhỏ giúp điều chỉnh runtime Copilot cho phù hợp với tuyến API của Next.js App Router và trả về một handleRequesthàm.
Bước 3: Khôi phục điểm cuối API tải lên
Tuyến API này ( src\app\api\upload-resume\route.ts) xử lý việc tải lên hồ sơ xin việc từ giao diện người dùng và chuyển tiếp chúng đến phần phụ trợ FastAPI. Nó:
Chấp nhận tải lên tập tin nhiều phần từ trình duyệt.
Chuyển tiếp tệp đến trình phân tích cú pháp tiếp tục ở phía máy chủ.
Trả về văn bản và kỹ năng đã trích xuất cho giao diện người dùng.
Việc giữ nguyên quá trình phân tích sơ yếu lý lịch ở phần máy chủ cho phép nhân viên tái sử dụng cùng một logic và giữ cho giao diện người dùng 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 : “Không có tệp được cung cấp” }, { status : 400 });
}
const backendFormData = new FormData ();
backendFormData. append ( “file” , file);
const backendUrl = process. env . BACKEND_URL || “//localhost:8123” ;
const response = await fetch ( ` ${backendUrl} /api/upload-resume` , {
method : “POST” ,
body : backendFormData,
});
if (!response. ok ) {
throw new Error ( “Tải lên máy chủ phụ trợ thất bại” );
}
const data = await response. json ();
return NextResponse.json (data); } catch ( error ) { return NextResponse.json ( { error : error instanceof Error ? error.message : “Tải lên thất bại” } , { status : 500 } ) ; } }
Bước 4: Xây dựng các thành phần chính
Tôi chỉ trình bày phần logic cốt lõi của từng thành phần vì mã nguồn tổng thể rất lớn. Bạn có thể tìm thấy tất cả các thành phần trong kho lưu trữ tại địa chỉ này src\components.
Các thành phần này sử dụng các hook của CopilotKit (như useCopilotReadable) để kết nối mọi thứ lại với nhau.
Thành phần tải lên sơ yếu lý lịch
Thành phần phía máy khách này xử lý việc chọn sơ yếu lý lịch và chuyển tiếp tệp đến máy chủ để phân tích cú pháp.
Nó chấp nhận một tệp PDF/TXT, gửi tệp đó bằng phương thức POST /api/upload-resumevà chuyển văn bản cùng các kỹ năng đã trích xuất trở lại 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 ( “Vui lòng tải lên tệp PDF hoặc TXT” );
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 ( “Tải lên thất bại” );
onUploadSuccess (( await res. json ()) as ResumeUploadResponse );
setSelectedFile ( null );
if (inputRef. current ) inputRef. current . value = “” ;
} catch (err) {
setError (err instanceof Error ? err. message : “Không thể tải lên sơ yếu lý lịch” );
} finally {
setIsLoading ( false );
}
};
return (
<form onSubmit={onSubmit}>
<input ref={inputRef} type=”file” accept=”.pdf,.txt” onChange={onSelect} />
<button disabled={!selectedFile || isLoading}>{isLoading ? “Đang tải lên…” : “Tiếp tục tải lên”}</button>
{error && <p>{error}</p>}
{/* … Giao diện/kiểu dáng đã được lược bỏ … */}
</form>
);
}
Dưới đây là lời 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 tin bằng cách /api/upload-resumesử dụngFormData
Nhận văn bản đã trích xuất + kỹ năng từ máy chủ phụ trợ.
Nó lấy dữ liệu đó onUploadSuccessđể có thể đưa vào tác nhân sau này.
Xem toàn bộ mã nguồn tạisrc/components/ResumeUpload.tsx.
Thành phần bảng trò chuyện
Đây là giao diện người dùng cốt lõi kết nối người dùng, nhân viên hỗ trợ và kết quả đầu ra của công cụ. Bảng trò chuyện:
Tích hợp CopilotChat để xử lý đầu vào hội thoại và phản hồi trực tuyến từ nhân viên hỗ trợ.
Được sử dụng useCopilotReadableđể liên tục đồng bộ hóa văn bản sơ yếu lý lịch, kỹ năng được phát hiện và tùy chọn người dùng vào ngữ cảnh của tác nhân.
Chặn các lệnh gọi công cụ (như update_jobs_list) để cập nhật trạng thái giao diện người dùng cục bộ mà không cần xuất JSON công việc vào khung chat.
Chúng tôi cũng xây dựng giao diện người dùng đàm thoại <CopilotChat />bằng cách sử dụng 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 ( ) {
// form + resume state (title, location, skills, resume text…)
const [jobs, setJobs] = useState< JobPosting []>([]);
const processedKeyRef = useRef<string | null >( null ); // Loại bỏ các lệnh gọi công cụ trùng lặp
// Ghi lại đầu ra của 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 sử dụng setState trong quá trình render
queueMicrotask ( () => {
setJobs (result. jobs_list );
});
}
}
// Hiển thị các lệnh gọi công cụ nội tuyến
return (
<details>
…
</details>
);
},
});
// Gửi trạng thái giao diện người dùng + dữ liệu sơ yếu lý lịch vào ngữ cảnh của tác nhân
useCopilotReadable ({
description : “Tùy chọn tìm kiếm việc làm” ,
value : {
targetTitle,
targetLocation,
skillsHint,
resumeText,
detectedSkills,
},
});
trở lại(
<div>
{/* Giao diện tải lên CV + trích xuất kỹ năng */}
{!resumeUploaded && <ResumeUpload onUploadSuccess={handleUploadSuccess} />}
{/* Ô nhập tìm kiếm việc làm (chức danh / địa điểm / kỹ năng) */}
{/* Giao diện trò chuyện CopilotKit */}
<CopilotChat />
{/* Kết quả có cấu trúc được hiển thị bên ngoài khung trò chuyện */}
<JobsResults jobs={jobs} />
</div>
);
}
Xem toàn bộ mã nguồn tại src/components/ChatPanel.tsx .
✅ Thành phần kết quả việc làm
Đây là một thành phần thuần túy dùng để trình bày. Nó nhận jobsmảng dữ liệu (được điền đầy khi update_jobs_listhoàn tất) và hiển thị nó dưới dạng bảng, giúp cho đầu ra trò chuyện được gọn gàng.
“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* Công ty | Chức danh | Địa điểm | Liên kết | Phù hợp */}</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”>
Mở
</a>
</td>
<td className=”px-4 py-2″>{j.goodMatch || “Có”}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
Xem toàn bộ mã nguồn tại src/components/JobsResults.tsx .
Bước 5: Kết nối giao diện trò chuyện với nhân viên hỗ trợ
Đến thời điểm này, mọi thứ đã sẵn sàng. Trang này chỉ đơn giản là hiển thị nội dung ChatPanel, được kết nối hoàn toàn với hệ thống phụ trợ Deep Agents thông qua CopilotKit.
Một bảng điều khiển phụ LivePreviewPanelđược gắn kèm bên cạnh. Vì các lệnh gọi công cụ đã được hiển thị nội tuyến bên trong CopilotChat, nên bảng điều khiển này hiện tại là tùy chọn và đóng vai trò là không gian đang được phát triển để gỡ lỗi và trực quan hóa chi tiết 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ý ứng tuyển việc làm</h1>
<p>Tìm việc làm phù hợp với AI.</p>
{/* … huy hiệu / kiểu dáng đã đượ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 lệnh gọi công cụ đã được hiển thị bên trong CopilotChat */}
{/* Bảng này là tùy chọn và hiện đang được sử dụng để thử nghiệm */}
</aside>
</div>
{/* Chân trang */}
{/* … nội dung chân trang đã được lược bỏ … */}
</main>
);
}
4. Phần Backend: Xây dựng dịch vụ Agent (FastAPI + Deep Agents + AG-UI)
Tiếp theo, chúng ta sẽ xây dựng phần backend FastAPI để lưu trữ Deep Agent.
Trong /agentthư mục đó có một máy chủ FastAPI chạy tác nhân Ứng dụng Việc làm. Đây là cấu trúc dự án của phần backend.
.
├── agent/ ← Phần phụ trợ của Deep Agents
│ ├── main.py ← Điểm cuối FastAPI + AG-UI
│ ├── agent.py ← Đồ thị và công cụ của Deep Agents
│ ├── pyproject.toml ← Các phụ thuộc Python (uv)
│ └── uv.lock
…
Nhìn chung, phần backend chịu trách nhiệm về:
Cung cấp một điểm cuối tác nhân tương thích với CopilotKit (để truyền trạng thái tác nhân và các lệnh gọi công cụ).
Cung cấp /api/upload-resumeđiểm cuối để phân tích sơ yếu lý lịch.
Xây dựng đồ thị Deep Agents để lập kế hoạch, phân công nhiệm vụ cho các tác nhân con và tìm kiếm trên web các công việc phù hợp.
Phần backend sử dụng uv để quản lý các thư viện phụ thuộc. Hãy cài đặt nó nếu hệ thống của bạn chưa có.
pip install uv
Khởi tạo một dự án UV mới bằng lệnh sau. Thao tác này sẽ tạo ra một tệp . mới pyproject.toml.
cd agent
uv init
Hầu hết các công cụ AI được sử dụng trong hệ thống phụ trợ 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 thông báo cho uv sử dụng phiên bản Python tương thích bằng lệnh này:
uv python pin 3.12
Tiếp theo, hãy cài đặt các thư viện phụ thuộc. Thao tác này cũng sẽ tạo môi trường ảo cho 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 tác nhân với giao diện người dùng thông qua tính năng truyền dữ liệu, công cụ và chia sẻ trạng thái.
deepagentsKhung tác nhân lập kế hoạch trước cho việc thực thi nhiều bước.
fastapi: Khung web cung cấp API cho tác nhân.
langchainLớp điều phối tác nhân và công cụ.
langchain-openaiTích hợp mô hình OpenAI cho LangChain.
pypdfTrí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-multipartCho phép tải lên tập tin trong FastAPI.
tavily-pythonTìm kiếm trên web để nghiên cứu thông tin đại lý theo thời gian thực.
uvicorn[standard]Máy chủ ASGI để chạy FastAPI.
Bây giờ hãy chạy lệnh sau để tạo một uv.locktệp được ghim với các phiên bản chính xác.
đồng bộ tia cực tím
Thêm các khóa API cần thiết
Tạo một .envtệp trong cả hai agentthư mục và thêm Khóa API OpenAI và Khóa API Tavily của bạn vào tệp đó. Tôi đã đính kèm liên kết tài liệu để bạn dễ dàng làm theo.
OPENAI_API_KEY=sk-proj-…
TAVILY_API_KEY=tvly-dev-…
OPENAI_MODEL=gpt-4-turbo
Bước 1: Xác định hành vi của tác nhân
Chúng ta bắt đầu bằng cách định nghĩa hành vi của tác nhân bằng cách sử dụng một lời nhắc hệ thống duy nhất, nghiêm ngặt trong agent.py.
Trong Deep Agents, lời nhắc hệ thống đóng vai trò là lớp điều khiển cho quy trình làm việc, kết hợp lập kế hoạch và phân công để phân chia các nhiệm vụ phức tạp thành các bước có thứ tự.
Công MAIN_SYSTEM_PROMPTcụ điều phối và các tác nhân phụ bằng cách thực thi một trình tự 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 diễn ra thông qua các công cụ.
Trạng thái giao diện người dùng được cập nhật một cách có kiểm soát.
quá trình thực thi kết thúc một cách xác định vớifinalize()
MAIN_SYSTEM_PROMPT = “””
Bạn là một tác nhân sử dụng công cụ.
Các quy tắc bắt buộc:
– 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ác công việc thông 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 web tuyển dụng hoặc trang liệt kê/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/v.v.).
Lược đồ (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 bài đăng tuyển dụng riêng lẻ 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 bạn không thể 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_PROMPTXác định hành vi của một tác nhân phụ chuyên biệt. Trách nhiệm của nó chỉ giới hạn ở việc tìm kiếm các công việc phù hợp và trả về kết quả có cấu trúc theo định dạng được kiểm soát.
JOB_SEARCH_PROMPT = (
“Tìm kiếm và chọn 5 bài đăng thực tế 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 phần bao bọc):\n”
“<CÁC CÔNG VIỆC>\n”
‘[{“công ty”:”…”,”chức danh”:”…”,”địa điểm”:”…”,”liên kết”:”//…”,”Phù hợp tốt”:”một câu”},’ ‘
{“công ty”:”…”,”chức danh”:”…”,”địa điểm”:”…”,”liên kết”:”//…”,”Phù hợp tốt”:”một câu”},’ ‘
{“công ty”:”…”,”chức danh”:”…”,”địa điểm”:”…”,”liên kết”:”//…”,”Phù hợp tốt”:”một câu”},’ ‘
{“công ty”:”…”,”chức danh”:”…”,”địa điểm”:”…”,”liên kết”:”//…”,”Phù hợp tốt”:”một câu”},’
‘ {“company”:”…”,”title”:”…”,”location”:”…”,”link”:”//…”,”Good Match”:”one sentence”}]’
“\n</JOBS>”
“Mỗi công việc PHẢI:”
“- Là một vị trí tuyển dụng duy nhất (không phải bảng việc làm, trang lọc hoặc chỉ mục việc làm của công ty)”
“- Thuộc về một công ty cụ thể với trang mô tả công việc riêng”
“Bạn phải:”
“- Sử dụng internet_search để tìm các công việc phù hợp.”
“- KHÔNG xuất danh sách việc làm, JSON hoặc URL trong tin nhắn.”
“- Chỉ trả về mọi thứ bằng cách gọi công cụ cha `update_jobs_list` với một chuỗi JSON.”
)
Bước 2: Thêm các tiện ích phân tích sơ yếu lý lịch và trích xuất kỹ năng
Chúng tôi trích xuất văn bản thô từ các tệp PDF được tải lên bằng cách sử dụng hàm này pypdf. Hàm này được điểm cuối tải lên FastAPI sử dụng để chuyển đổi sơ yếu lý lịch 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 tôi trích xuất các tín hiệu cấu trúc nhẹ (ngôn ngữ, khung công tác, công cụ) từ sơ yếu lý lịch. Đ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 kết quả 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)
Bước 3: Xác định các công cụ để tìm kiếm, cập nhật giao diện người dùng và chấm dứt.
Các công cụ là bề mặt tích hợp giữa tác nhân và thế giới bên ngoài/giao diện người dùng.
Công cụ này internet_searchcó nhiệm vụ tìm kiếm các tin tuyển dụng thực sự. Nó cố ý lấy thêm kết quả tìm kiếm, lọc bỏ bất kỳ URL nào chứa chuỗi con “không phù hợp” (các trang web tuyển dụng/trang tìm kiếm) BAD_URL_SUBSTRINGSvà chỉ trả về các kết quả phù hợp đầu tiên max_results.
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 thiết lập” )
client = TavilyClient(api_key=tavily_key)
res = client.search(
query=query,
max_results=max_results * 3 , # lấy thêm, 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] Returning { len (trimmed)} filtered results” )
print (trimmed)
return trimmed
Công cụ này update_jobs_listlà cách duy nhất để dữ liệu công việc có cấu trúc được đưa đến giao diện người dùng, giúp việc cập nhật giao diện người dùng được rõ ràng và loại bỏ JSON khỏi tin nhắn trò chuyện.
@tool
def update_jobs_list(jobs_json: str) -> Dict[str, Any]:
“” “Gửi danh sách việc làm đến trạng thái giao diện người dùng.” “”
jobs = json.loads(jobs_json)
print (f “[TOOL] update_jobs_list: {len(jobs)} jobs” )
return { “jobs_list” : jobs }
finalizeCông cụ này báo hiệu rằng tác nhân đã hoàn thành quy trình làm việc của mình.
@tool
def finalize () -> dict:
“” “Báo hiệu hoàn thành.” “”
print( “[TOOL] finalize: Tìm kiếm công việc hoàn tất” )
return { “status” : “done” }
Bước 4: Lắp ráp đồ thị Deep Agents với các tác nhân con
Giờ chúng ta kết nối mọi thứ và xây dựng đồ thị Deep Agents bằng cách sử dụng build_agent().
def build_agent ():
“””Xây dựng đồ thị 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 các công việc phù hợp 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] Deep Agents graph created” )
print (agent_graph)
return agent_graph
Bước 5: Thiết lập FastAPI
Bước cuối cùng là khởi tạo phần backend và hiển thị nó như một ứng dụng FastAPI. Nó cũng xử lý việc tải lên sơ yếu lý lịch và phân tích cú pháp PDF, chuyển đổi các tệp thô thành văn bản và kỹ năng đã được xử lý trước khi gửi đến người đại diện.
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= “Find personalized job openings based on skills and preferences” ,
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= “Job finder” ,
graph=agent_graph,
),
path= “/” ,
)
print ( “[MAIN] Agent registered” )
except Exception as e:
print (” f”[LỖI] Không thể xây dựng tác nhân: { str (e)} ” )
raise
@app.get( “/healthz” )
async def health_check ():
“””Kiểm tra sức khỏe”””
return {
“status” : “khỏe mạnh” ,
“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 sơ yếu lý lịch (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 được cung cấp” )
try :
với 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 :
# for other formats, just read as text
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] Resume upload failed: { str (e)} ” )
raise HTTPException(status_code= 500 , detail= str (e))
def main ():
“””Run server”””
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()
5. 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 nó cục bộ. Vui lòng đảm bảo bạn đã thêm thông tin đăng nhập vào tệp agent/.env.
Từ thư mục gốc của dự án, điều hướng đến agentthư mục đó và khởi động máy chủ FastAPI:
cd agent
uv run python main.py
Phần phụ trợ sẽ bắt đầu vào//localhost:8123.
Trong một cửa sổ terminal mới, hãy khởi động máy chủ phát triển giao diện người dùng bằng lệnh sau:
npm run dev
Sau khi cả hai máy chủ đã hoạt động, hãy mở giao diện người dùng trong trình duyệt của bạn tại //localhost:3000/ để xem cục bộ.

Sau đó, bạn tải lên sơ yếu lý lịch của mình và tìm kiếm các công việc phù hợp.
Tùy thuộc vào truy vấn công việc, nó có thể trả về số lượng kết quả khác nhau. Đây là một kết quả khác!
CopilotKit cũng cung cấp Agent Inspector , một giao diện người dùng AG-UI hoạt động trực tiếp cho phép bạn kiểm tra quá trình chạy tác vụ, ảnh chụp trạng thái, thông báo và các lệnh gọi công cụ khi chúng được truyền từ máy chủ. Bạn có thể truy cập nó thông qua một nút CopilotKit được phủ lên ứng dụng của mình (rất hữu ích cho việc gỡ lỗi).
6. Luồng dữ liệu
Giờ đây, sau khi đã xây dựng cả giao diện người dùng (frontend) và dịch vụ agent, đây là cách dữ liệu thực sự luân chuyển giữa chúng. Điều này sẽ khá dễ hiểu nếu bạn đã theo dõi quá trình xây dựng từ đầu.
Giao diện người dùng Next.js (ResumeUpload + CopilotChat)
↓
useCopilotReadable đồng bộ sơ yếu lý lịch + tùy chọn
↓
POST /api/copilotkit (giao thức AG-UI)
↓
FastAPI + Deep Agents (điểm cuối /copilotkit)
↓
Ngữ cảnh sơ yếu lý lịch + kỹ năng được đưa vào tác nhân
↓
Điều phối Deep Agents
├─ internet_search (Tavily)
├─ lọc & chuẩn hóa việc làm
└─ update_jobs_list (gọi công cụ)
↓
Truyền phát AG-UI (SSE)
↓
Thời gian chạy CopilotKit nhận kết quả từ công cụ
↓
Giao diện người dùng thu thập đầu ra của công cụ
↓
Việc làm được hiển thị trong bảng + khung chat vẫn gọn gàng
Vậy là xong!
Giờ đây bạn đã có một trợ lý ứng tuyển việc làm được hỗ trợ bởi Deep Agents với CopilotKit làm lớp giao diện người dùng.

Bài viết liên quan: