Xây dựng một trợ lý phỏng vấn AI cục bộ

Nội dung

    Một trợ lý phỏng vấn hoạt động mà không cần gọi điện toán đám mây. Không dùng API bên ngoài. Không cần tải lên sơ yếu lý lịch. Không rò rỉ âm thanh. Chỉ là máy tính xách tay của tôi thôi. Vì vậy, tôi đã xây dựng một trợ lý AI cục bộ có chức năng: – Ghi âm từ trình duyệt – Chuyển đổi giọng nói thành văn bản – Đọc mô tả công việc – Lưu trữ bộ nhớ hội thoại bằng cách sử dụng các embedding. – Sàng lọc ứng viên dựa trên câu trả lời thực tế – Giải thích lý do tại sao ứng viên phù hợp hoặc không phù hợp. Xây dựng một trợ lý phỏng vấn AI cục bộ

    Nếu bạn muốn 100% local – không cloud – không API ngoài – không rò rỉ dữ liệu, thì bạn phải thiết kế workflow theo tư duy:

    Browser → Local service → n8n → Local LLM → Local Vector DB → trả kết quả

    Không có một byte nào ra internet.

    Nếu dùng React, .NET và trí tuệ nhân tạo cục bộ thì hệ thống sàng lọc phỏng vấn địa phương gồm ba phần:

    • Ứng dụng React dành cho cuộc trò chuyện âm thanh
    • API .NET dành cho điều phối
    • Ngăn xếp AI cục bộ cho khả năng suy luận và ghi nhớ

    Trợ lý đặt câu hỏi, lắng nghe, ghi nhớ các câu trả lời trước đó và so sánh chúng với bản mô tả công việc.

    Ngăn xếp công nghệ
    Phản ứng (Vite)
    API Web .NET 8
    Ollama (thạc sĩ luật địa phương)
    Hạt nhân ngữ nghĩa
    bộ nhớ vectơ cục bộ
    API âm thanh trình duyệt
    Mọi thứ đều vận hành trên máy của bạn.

    Trong bài này sẽ trình bày workflow n8n theo từng lớp rõ ràng để bạn có thể build ngay.

    KIẾN TRÚC TỔNG THỂ (LOCAL ONLY)

    Thành phần đề xuất:

    • n8n (self-host, local)
    • Ollama (chạy LLM local)
    • whisper.cpp hoặc faster-whisper (STT local)
    • Qdrant / Chroma (vector DB local)
    • Browser MediaRecorder API (ghi âm local)
    • SQLite / Postgres local (lưu metadata nếu cần)

    PHẦN 1 — GHI ÂM TỪ TRÌNH DUYỆT

    Không dùng cloud STT.

    Cách làm

    Trình duyệt dùng:

    • MediaRecorder API
    • Xuất file .webm hoặc .wav
    • POST file đó về n8n qua Webhook node

    n8n Workflow 1 — Nhận Audio

    Webhook Node

    • Method: POST
    • Binary data: ON
    • Expect field: audio

    Write Binary File Node

    • Lưu file vào:
    /data/interview/audio/{{ $now }}.wav
    

    PHẦN 2 — SPEECH TO TEXT (LOCAL)

    Dùng whisper.cpp hoặc faster-whisper

    Ví dụ command:

    ./main -m models/ggml-large-v3.bin -f audio.wav -otxt
    

    n8n Node

    Execute Command Node

    Command:

    whisper audio.wav --model large-v3 --output_dir /data/interview/transcript
    

    Output: file .txt

    Read Binary File Node

    • Đọc transcript.txt

    PHẦN 3 — ĐỌC JOB DESCRIPTION

    JD có thể:

    • Lưu sẵn trong DB
    • Hoặc upload một lần rồi embedding

    Workflow: Chuẩn hóa JD

    Set Node

    Tạo field:

    job_description
    

    Split In Batches Node

    Chunk 500–800 tokens

    Embedding Node (local Ollama)

    Model embedding local ví dụ:

    nomic-embed-text
    

    HTTP Request Node:

    POST:

    //localhost:11434/api/embeddings
    

    Body:

    {
      "model": "nomic-embed-text",
      "prompt": "{{ $json.chunk }}"
    }
    

    Insert vào Qdrant (local)

    HTTP:

    //localhost:6333/collections/jd/points
    

    PHẦN 4 — LƯU BỘ NHỚ HỘI THOẠI

    Mỗi câu trả lời ứng viên:

    1. Lưu raw text
    2. Tạo embedding
    3. Insert vào collection conversation

    Workflow:

    Transcript → Embedding → Qdrant → Done

    PHẦN 5 — SÀNG LỌC ỨNG VIÊN

    Bây giờ là phần quan trọng nhất.

    Logic

    1. Lấy transcript
    2. Tìm các đoạn JD liên quan (semantic search)
    3. So sánh câu trả lời với yêu cầu
    4. Tính điểm
    5. Giải thích lý do

    Workflow 2 — Phân tích ứng viên

    Webhook Trigger (sau mỗi câu hoặc cuối buổi)
    Embed transcript

    POST Ollama embedding

    Search Qdrant

    Search top 5 đoạn JD liên quan:

    POST /collections/jd/points/search
    
    Merge Node

    Gộp:

    • Transcript
    • Relevant JD chunks
    LLM Node (Ollama local)

    POST:

    //localhost:11434/api/generate
    

    Body:

    {
      "model": "mistral",
      "prompt": "
    Bạn là chuyên gia tuyển dụng.
    So sánh câu trả lời ứng viên với yêu cầu công việc.
    
    JD:
    {{ $json.jd }}
    
    Câu trả lời:
    {{ $json.answer }}
    
    Trả về:
    - Điểm (0–10)
    - Lý do phù hợp
    - Lý do không phù hợp
    - Kỹ năng thiếu
    "
    }
    

    PHẦN 6 — LƯU KẾT QUẢ

    Bạn có thể:

    • Lưu JSON file local
    • Hoặc SQLite

    Node:

    Write Binary File hoặc Execute Query

    SƠ ĐỒ LUỒNG HOẠT ĐỘNG

    Browser
    → Webhook
    → Save audio
    → Whisper
    → Transcript
    → Embedding
    → Qdrant
    → Search JD
    → LLM Evaluate
    → JSON Report

    TỐI ƯU CHUYÊN SÂU

    Nếu bạn muốn hệ thống chuyên nghiệp hơn:

    ✔ Thêm scoring rubric cố định

    Không để LLM tự do chấm.

    Ví dụ:

    • Technical skill (40%)
    • Experience relevance (30%)
    • Communication clarity (15%)
    • Problem solving (15%)
    Thêm structured output

    Yêu cầu LLM trả về:

    {
      "score": 7.5,
      "technical": 8,
      "experience": 6,
      "communication": 7,
      "missing": ["Docker", "CI/CD"]
    }
    
    Tách workflow thành 3 pipeline
    1. Audio pipeline
    2. Memory pipeline
    3. Evaluation pipeline

    Đừng gộp tất cả vào một workflow — sẽ khó debug.

    CẢNH BÁO THỰC TẾ

    Bạn đang xây một hệ thống chấm điểm con người.

    Nếu prompt không chuẩn:
    → LLM sẽ thiên vị
    → Hoặc đánh giá mơ hồ

    Bạn phải:

    • Viết rubric rõ
    • Chuẩn hóa JD
    • Chuẩn hóa câu hỏi phỏng vấn

    Nếu không, hệ thống sẽ chỉ “nghe có vẻ thông minh” chứ không chính xác.

    Thiết kế prompt đánh giá chuẩn HR

    Nếu bạn muốn hệ thống chấm ứng viên nghiêm túc như HR thật, thì prompt không được chung chung kiểu “hãy đánh giá mức độ phù hợp”.

    Bạn cần:

    1. rubric cố định
    2. trọng số rõ ràng
    3. format JSON bắt buộc
    4. nguyên tắc chống thiên vị
    5. logic trừ điểm

    Dưới đây là prompt chuẩn để dùng trực tiếp trong Ollama local.

    PROMPT ĐÁNH GIÁ ỨNG VIÊN (CHUẨN HR – STRUCTURED)

    Bạn đưa vào LLM 4 phần:

    • Job Description
    • Rubric
    • Transcript
    • Hướng dẫn output

    SYSTEM PROMPT

    Bạn là chuyên gia tuyển dụng 15 năm kinh nghiệm.
    Bạn đánh giá dựa trên bằng chứng cụ thể trong câu trả lời.
    Không suy đoán.
    Không đánh giá dựa trên giới tính, tuổi, vùng miền, giọng nói.
    Chỉ đánh giá dựa trên nội dung thực tế.
    Nếu thông tin không đủ, ghi rõ "không đủ dữ liệu".
    Phải trả về JSON hợp lệ.
    Không giải thích ngoài JSON.
    

    RUBRIC CHUẨN (Có thể tái sử dụng cho mọi JD)

    Thang điểm 0–10 cho mỗi tiêu chí.
    
    1. Technical Competency (40%)
    - 0–3: Không có kỹ năng liên quan
    - 4–6: Có hiểu biết cơ bản
    - 7–8: Thành thạo thực tế
    - 9–10: Chuyên gia, có dẫn chứng cụ thể
    
    2. Relevant Experience (25%)
    - Đã làm việc tương tự?
    - Có kết quả đo lường?
    - Có vai trò chính hay phụ?
    
    3. Problem Solving & Thinking (15%)
    - Có tư duy cấu trúc?
    - Có ví dụ thực tế?
    - Có phân tích nguyên nhân–kết quả?
    
    4. Communication Clarity (10%)
    - Trả lời rõ ràng?
    - Có cấu trúc?
    - Có lan man?
    
    5. Cultural / Role Fit (10%)
    - Hiểu đúng vai trò?
    - Phù hợp môi trường làm việc?
    

    USER PROMPT TEMPLATE (DÙNG TRONG N8N)

    JOB DESCRIPTION:
    {{job_description}}
    
    INTERVIEW QUESTION:
    {{question}}
    
    CANDIDATE ANSWER:
    {{transcript}}
    
    YÊU CẦU:
    
    1. Trích dẫn bằng chứng cụ thể từ câu trả lời.
    2. Không suy đoán những gì ứng viên không nói.
    3. Nếu thiếu thông tin cho một tiêu chí, ghi rõ.
    
    Trả về JSON đúng format:
    
    {
      "overall_score": number,
      "technical_score": number,
      "experience_score": number,
      "problem_solving_score": number,
      "communication_score": number,
      "culture_fit_score": number,
      "strengths": [],
      "weaknesses": [],
      "missing_skills": [],
      "evidence": {
          "technical": "",
          "experience": "",
          "problem_solving": "",
          "communication": "",
          "culture_fit": ""
      },
      "recommendation": "strong_yes | yes | borderline | no",
      "confidence_level": "high | medium | low"
    }
    

    CÔNG THỨC TÍNH ĐIỂM

    Trong prompt bạn thêm:

    overall_score =
    (technical_score * 0.4) +
    (experience_score * 0.25) +
    (problem_solving_score * 0.15) +
    (communication_score * 0.1) +
    (culture_fit_score * 0.1)
    
    Làm tròn 1 chữ số thập phân.
    

    Đừng để LLM tự tính lung tung.

    PHIÊN BẢN CHẤM NHIỀU CÂU HỎI

    Nếu bạn đánh giá cả buổi phỏng vấn:

    Thêm phần:

    Dựa trên toàn bộ câu trả lời, đánh giá xu hướng tổng thể:
    - Có nhất quán không?
    - Có phóng đại?
    - Có mâu thuẫn giữa các câu?
    

    Và thêm field:

    "consistency_check": "",
    "risk_flags": []
    

    PHIÊN BẢN NÂNG CAO (ANTI-BULLSHIT MODE)

    Thêm rule:

    Nếu ứng viên nói chung chung (ví dụ: "em có kinh nghiệm nhiều năm"),
    mà không có số liệu hoặc ví dụ cụ thể,
    giảm 1–2 điểm Technical hoặc Experience.
    

    PROMPT RÚT GỌN (CHO REALTIME)

    Nếu bạn muốn phản hồi ngay sau mỗi câu trả lời:

    Chấm riêng câu trả lời này cho tiêu chí:
    - Technical
    - Problem solving
    - Communication
    
    Không tính overall.
    Trả JSON.
    

    LỜI KHUYÊN THẲNG THẮN

    Nếu bạn không ép LLM trả JSON + rubric rõ ràng:

    → Nó sẽ trả lời kiểu “Ứng viên khá tốt”
    → Không dùng được trong hệ thống tự động

    Và nếu bạn không chuẩn hóa câu hỏi phỏng vấn:

    → Điểm sẽ dao động vô nghĩa

    Gửi ý promte có thể AI gợi ý thêm:

    • Prompt chuyên cho vị trí kỹ thuật (Developer)
    • Prompt chuyên cho Sales
    • Prompt chuyên cho vị trí quản lý
    • Hoặc framework đánh giá theo competency model quốc tế
    • Thiết kế chiến lược ranking nhiều ứng viên
    • Hoặc tối ưu schema cho >1 triệu vector

    Thiết kế schema Qdrant

    Nếu bạn muốn hệ thống phỏng vấn local hoạt động ổn định, nhanh và mở rộng được, thì schema Qdrant phải thiết kế theo tư duy:

    Tách collection theo mục đích
    Tối ưu payload để filter nhanh
    Chuẩn hóa vector dimension
    Cho phép scoring + audit + multi-interview

    Tôi sẽ thiết kế schema chuẩn production cho bạn.

    TỔNG THỂ KIẾN TRÚC COLLECTION

    Bạn nên có 3 collections riêng biệt:

    1. job_descriptions
    2. interview_memory
    3. candidate_profiles

    Đừng gộp chung — sẽ khó filter và chậm.

    COLLECTION: job_descriptions

    Dùng để semantic search yêu cầu công việc.

    Vector Config

    • Size: 768 hoặc 1024 (tuỳ model embedding)
    • Distance: Cosine

    Ví dụ nếu dùng nomic-embed-text → 768

    Payload Schema

    {
      "job_id": "string",
      "job_title": "string",
      "department": "string",
      "chunk_index": "integer",
      "content": "string",
      "required_skills": ["string"],
      "min_year_experience": "integer",
      "level": "junior | mid | senior | lead",
      "created_at": "timestamp"
    }
    

    Index tối ưu

    Bạn phải tạo payload index cho:

    • job_id (keyword)
    • department (keyword)
    • level (keyword)

    Điều này cho phép filter trước khi search vector.

    COLLECTION: interview_memory

    Lưu từng câu trả lời ứng viên.

    Vector Config

    Same dimension như JD embedding.

    Payload Schema

    {
      "candidate_id": "string",
      "interview_id": "string",
      "question_id": "string",
      "question_text": "string",
      "answer_text": "string",
      "timestamp": "timestamp",
      "technical_score": "float",
      "confidence_level": "string",
      "risk_flags": ["string"]
    }
    

    Tại sao phải lưu score vào payload?

    Vì bạn có thể:

    • Query những câu < 5 điểm
    • Tìm pattern yếu
    • Audit lại decision

    Đừng chỉ lưu transcript.

    COLLECTION: candidate_profiles

    Lưu embedding tổng hợp toàn bộ buổi phỏng vấn.

    Dùng để:

    • So sánh nhiều ứng viên
    • Ranking
    • Talent pool search

    Payload Schema

    {
      "candidate_id": "string",
      "job_id": "string",
      "overall_score": "float",
      "technical_avg": "float",
      "experience_avg": "float",
      "problem_solving_avg": "float",
      "communication_avg": "float",
      "culture_fit_avg": "float",
      "recommendation": "string",
      "missing_skills": ["string"],
      "strengths": ["string"],
      "weaknesses": ["string"],
      "interview_date": "timestamp"
    }
    

    VECTOR PARAMETERS TỐI ƯU

    Khi tạo collection:

    {
      "vectors": {
        "size": 768,
        "distance": "Cosine"
      },
      "optimizers_config": {
        "default_segment_number": 2
      },
      "hnsw_config": {
        "m": 16,
        "ef_construct": 100
      }
    }
    

    Nếu bạn có < 100k vectors → config này đủ nhanh.

    CHIẾN LƯỢC SEARCH TỐI ƯU

    Khi đánh giá câu trả lời

    1. Embed answer
    2. Search trong job_descriptions
      • Filter job_id
      • Limit 5
    3. Pass 5 chunks vào LLM

    Khi so sánh nhiều ứng viên

    Embed “ideal candidate profile”
    Search trong candidate_profiles

    CHIẾN LƯỢC ID

    Đừng dùng random UUID khó đọc.

    Dùng format:

    job_202602_dev_backend
    cand_20260224_001
    interview_20260224_001
    

    Dễ audit.

    MEMORY TỐI ƯU HÓA

    Đừng để collection phình to vô nghĩa.

    Sau mỗi buổi phỏng vấn:

    • Tổng hợp lại thành 1 profile vector
    • Có thể xóa chi tiết từng câu nếu không cần audit sâu

    BẢO MẬT (VÌ BẠN MUỐN 100% LOCAL)

    • Chạy Qdrant trong Docker network nội bộ
    • Không expose port ra internet
    • Không bật telemetry
    • Không log transcript vào console

    SAI LẦM PHỔ BIẾN

    1. Không tách collection
    2. Không index payload
    3. Không lưu metadata score
    4. Không filter trước khi vector search
    5. Dùng dimension sai với model embedding

    TỐI ƯU HIỆU NĂNG

    Nếu máy bạn RAM 24GB:

    • Batch insert 100 vectors/lần
    • Set ef_search = 64
    • Không cần quantization

    Nếu sau này > 500k vectors:
    → bật scalar quantization

    Collection: competency_framework

    Lưu chuẩn năng lực nội bộ công ty.

    Khi đánh giá:

    • Search JD
    • Search competency framework
    • Merge → LLM đánh giá sâu hơn

    Hệ thống sẽ giống ATS thực thụ.

    Xây dựng trình mô phỏng phỏng vấn AI bằng LLaMA

    prompt =
    Bạn là một người phỏng vấn giàu kinh nghiệm đang tuyển dụng cho vị trí {role} .

    Hãy tạo chính xác 5 câu hỏi phỏng vấn.

    Bối cảnh:
    – Trình độ kinh nghiệm: {experience}
    – Hình thức phỏng vấn: {mode}
    – Độ tự tin của ứng viên: {confidence}
    – Kỹ năng cốt lõi: {skills}

    Quy tắc:
    – Câu hỏi phải hoàn toàn phù hợp với vai trò
    – Kết hợp câu hỏi lý thuyết, thực hành và tình huống
    – Sau mỗi câu hỏi, hãy thêm:
    “Tại sao câu hỏi này được đặt ra: <một dòng ngắn>”
    – Ngắn gọn
    – Không có bình luận thêm

    answer_prompt = f”””
    Bạn là một người phỏng vấn chuyên nghiệp.
    Đối với mỗi câu hỏi phỏng vấn dưới đây:
    – Lặp lại câu hỏi
    – Sau đó cung cấp một câu trả lời lý tưởng ngắn gọn (2-4 dòng)
    Định dạng nghiêm ngặt:
    Q: <câu hỏi>
    A: <câu trả lời>
    Không có bình luận bổ sung.

    Để lại một bình luận

    Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

    Chat with us
    Hello! How can I help you today?