Cập nhật 2026 — Bài viết này dành cho kỹ sư backend/AI đã có kinh nghiệm với Python, LLM và orchestration. Chúng ta sẽ xây dựng một Agent có khả năng scrape job LinkedIn theo từ khóa, vector hóa CV rồi chấm điểm tương đồng, đồng thời tối ưu chi phí inference xuống dưới $1 cho mỗi 10.000 lượt match.

1. Tại sao tôi viết bài này — kinh nghiệm thực chiến

Tháng 11 năm ngoái tôi phụ trách onboarding cho một nhóm 12 bạn fresher vừa ra trường tại TP.HCM. Họ ngồi cả ngày copy-paste tin tuyển dụng từ LinkedIn sang Google Sheet rồi dán CV vào Notion. Tôi dành ba ngày cuối tuần build một pipeline Python scrape 1.500 jobs/giờ, vector hóa CV và gửi email matching trong vòng 24 giờ. Kết quả: nhóm giảm 87% thời gian tìm việc, tỉ lệ phỏng vấn tăng từ 9% lên 31%. Chính vì thế tôi quyết định open-source lại pipeline này, nhưng nâng cấp lên chuẩn production với LangChain + MCP, đồng thời tinh chỉnh để chạy ổn định ở quy mô 10.000 jobs/ngày.

2. Kiến trúc tổng quan

Hệ thống gồm bốn lớp:

MCP được Anthropic giới thiệu cuối 2024 và nhanh chóng trở thành chuẩn kết nối tool cho agent. Lý do tôi chọn MCP thay vì viết tool thuần LangChain: MCP cho phép tái sử dụng tool ở nhiều framework (LangChain, LlamaIndex, raw Claude SDK) mà không phải viết lại adapter, đồng thời tool có thể chạy ở process riêng nên không crash host khi scrape lỗi.

3. Thiết lập môi trường

# requirements.txt
langchain==0.3.7
langchain-openai==0.2.5
langchain-mcp==0.1.0
mcp==1.0.0
httpx==0.27.2
beautifulsoup4==4.12.3
faiss-cpu==1.9.0.post1
sentence-transformers==3.2.1
python-dotenv==1.0.1

Cài đặt

python -m venv .venv source .venv/bin/activate pip install -r requirements.txt

.env

HOLYSHEEP_API_KEY=YOUR_HOLYSHEEP_API_KEY LINKEDIN_PROXY_POOL=resi1.provider.net:8000,resi2.provider.net:8000

4. MCP Server cho LinkedIn Jobs

import asyncio
import httpx
from bs4 import BeautifulSoup
from mcp.server import Server
from mcp.types import Tool, TextContent

app = Server("linkedin-jobs-mcp")

USER_AGENT = (
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/130.0 Safari/537.36"
)

async def scrape_linkedin(keyword: str, location: str, limit: int) -> list[dict]:
    url = (
        f"https://www.linkedin.com/jobs/search?"
        f"keywords={keyword}&location={location}&f_TPR=r86400"
    )
    async with httpx.AsyncClient(
        timeout=15.0,
        headers={"User-Agent": USER_AGENT},
        follow_redirects=True,
    ) as client:
        resp = await client.get(url)
        resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    cards = soup.select("div.base-card")[:limit]
    jobs = []
    for c in cards:
        title_el = c.select_one("h3.base-search-card__title")
        company_el = c.select_one("h4.base-search-card__subtitle")
        link_el = c.select_one("a.base-card