AI 7 — RAG & Xây dựng ứng dụng LLM (Agents)

13 thg 7, 2026 11 lượt xem
#ai
#agent
#mcp
#rag
#tool-calling

Mở đầu: từ "model biết tuốt" đến "ứng dụng biết việc"

Một mô hình ngôn ngữ lớn (LLM) như Claude rất giỏi suy luận và viết, nhưng nó có ba giới hạn cố hữu mà bạn không thể prompt cho qua được:

  1. Nó không biết dữ liệu riêng của bạn. Hợp đồng nội bộ, bảng giá sản phẩm, lịch sử giao dịch của khách hàng, tài liệu kỹ thuật của công ty — tất cả những thứ này không nằm trong dữ liệu huấn luyện.
  2. Nó không biết dữ liệu mới. Mọi model có một thời điểm cắt dữ liệu (knowledge cutoff). Sự kiện sau mốc đó, model hoàn toàn không hay biết.
  3. Nó có thể "bịa" (hallucination). Khi không biết, model vẫn trả lời trôi chảy và tự tin — đôi khi sai hoàn toàn. Đây là rủi ro lớn nhất khi đưa LLM vào sản phẩm thật.

Bài này trình bày các kỹ thuật để vượt qua ba giới hạn đó: RAG (đưa kiến thức vào ngữ cảnh), tool calling (cho model gọi hàm bên ngoài), agent (model tự lập kế hoạch và hành động nhiều bước), và MCP (chuẩn kết nối công cụ/dữ liệu). Mọi ví dụ code dùng họ model Claude của Anthropic qua SDK chính thức anthropic.

Các model dùng trong bài: Opus 4.8 (claude-opus-4-8), Sonnet 4.6 (claude-sonnet-4-6), Haiku 4.5 (claude-haiku-4-5), Fable 5 (claude-fable-5).


Vì sao cần RAG

RAG = Retrieval-Augmented Generation — "sinh văn bản có tăng cường bằng truy hồi". Ý tưởng cốt lõi rất đơn giản: thay vì hỏi model "trả lời theo những gì anh nhớ", ta tìm trước các đoạn tài liệu liên quan đến câu hỏi, chèn chúng vào prompt, rồi yêu cầu model trả lời dựa trên các đoạn đó.

Lợi ích:

  • Biết dữ liệu riêng/mới mà không cần huấn luyện lại model (fine-tune đắt và chậm). Chỉ cần cập nhật kho tài liệu.
  • Giảm hallucination: khi câu trả lời phải bám vào ngữ cảnh được cung cấp, model bịa ít hơn hẳn. Bạn còn có thể yêu cầu model dẫn nguồn (trích đoạn nào, tài liệu nào).
  • Kiểm soát và minh bạch: bạn biết chính xác model đã "đọc" gì để trả lời, dễ kiểm tra và gỡ lỗi.

RAG không thay thế kiến thức nền của model; nó bổ sung ngữ cảnh tươi mới cho từng câu hỏi cụ thể.


Kiến trúc RAG

Quy trình RAG chia làm hai pha. Pha lập chỉ mục (indexing) chạy offline, một lần (hoặc mỗi khi tài liệu thay đổi). Pha truy vấn (query) chạy mỗi khi người dùng hỏi.

Đi qua từng bước:

1. Tài liệu → Chunking

Tài liệu gốc (PDF, trang web, ghi chú, bảng dữ liệu) thường quá dài để nhét nguyên vào prompt. Ta cắt nhỏ thành các chunk (đoạn). Mỗi chunk là một đơn vị có thể được truy hồi độc lập.

2. Chunk → Embedding

Mỗi chunk được biến thành một embedding — một vector số (ví dụ 768 hay 1536 chiều) biểu diễn ý nghĩa của đoạn văn. Hai đoạn có nội dung gần nhau về nghĩa sẽ cho hai vector gần nhau trong không gian vector. Đây là nền tảng cho semantic search (tìm theo ngữ nghĩa, không phải khớp từ khóa).

3. Embedding → Vector Database

Các vector được lưu vào một vector database — cơ sở dữ liệu chuyên đánh chỉ mục và tìm các vector gần nhau nhanh (xem phần riêng bên dưới).

4. Truy hồi (semantic search, top-k)

Khi người dùng hỏi, ta cũng embedding câu hỏi thành một vector, rồi yêu cầu vector DB trả về top-k chunk có vector gần nhất (k thường là 3–10). Đây là các đoạn tài liệu có khả năng chứa câu trả lời.

5. Ghép ngữ cảnh vào prompt

Ta lắp các chunk top-k vào prompt cùng câu hỏi, kèm chỉ dẫn rõ ràng cho model: "chỉ trả lời dựa trên ngữ cảnh dưới đây, và dẫn nguồn".

6. LLM sinh câu trả lời có dẫn nguồn

Model đọc ngữ cảnh và sinh câu trả lời. Vì câu trả lời được "neo" vào tài liệu thật, độ chính xác cao hơn và ta có thể yêu cầu trích dẫn.

Ví dụ pha truy vấn với Claude

Dưới đây là phần "ghép ngữ cảnh + gọi model" — phần lõi của RAG (giả sử bạn đã có hàm search_chunks truy hồi top-k từ vector DB):

import anthropic

client = anthropic.Anthropic()

def answer_with_rag(question: str) -> str:
    # 1. Truy hồi top-k chunk liên quan (từ vector DB của bạn)
    chunks = search_chunks(question, k=5)  # trả về [{"text": ..., "source": ...}, ...]

    # 2. Ghép ngữ cảnh, đánh số nguồn để model dẫn lại
    context = "\n\n".join(
        f"[Nguồn {i+1}: {c['source']}]\n{c['text']}"
        for i, c in enumerate(chunks)
    )

    # 3. Gọi Claude với chỉ dẫn bám ngữ cảnh + dẫn nguồn
    resp = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=1024,
        system=(
            "Bạn là trợ lý trả lời CHỈ dựa trên ngữ cảnh được cung cấp. "
            "Luôn dẫn nguồn theo dạng [Nguồn N]. "
            "Nếu ngữ cảnh không đủ thông tin, hãy nói rõ là không tìm thấy."
        ),
        messages=[{
            "role": "user",
            "content": f"Ngữ cảnh:\n{context}\n\nCâu hỏi: {question}",
        }],
    )
    return next(b.text for b in resp.content if b.type == "text")

Lưu ý hai chi tiết quan trọng:

  • System prompt ép model bám ngữ cảnh và thú nhận khi không biết — đây là cơ chế giảm hallucination chính.
  • Đánh số nguồn giúp model dẫn lại chính xác, người dùng kiểm tra được.

Chunking & embedding: những lưu ý thực hành

Chất lượng RAG phụ thuộc rất nhiều vào hai bước này. Nếu chunk dở thì dù model giỏi đến đâu, nó cũng nhận được ngữ cảnh sai.

Về chunking:

  • Kích thước chunk: quá nhỏ (vài câu) thì mất ngữ cảnh xung quanh; quá to (vài trang) thì pha loãng ý nghĩa, vector trở nên "trung bình" và khó khớp. Khởi điểm thường gặp: 300–800 token mỗi chunk.
  • Overlap (chồng lấn): cho các chunk liền kề chồng nhau một chút (ví dụ 10–20%) để tránh cắt đứt một ý quan trọng ngay ranh giới.
  • Cắt theo cấu trúc tự nhiên: ưu tiên cắt theo đoạn văn, mục, tiêu đề — đừng cắt giữa câu. Một bảng dữ liệu nên giữ nguyên một khối.
  • Giữ metadata: lưu kèm nguồn, tiêu đề mục, ngày tháng cho mỗi chunk để vừa dẫn nguồn vừa lọc được.

Về embedding:

  • Dùng cùng một model embedding cho cả lúc lập chỉ mục và lúc truy vấn câu hỏi — nếu khác model, các vector không cùng "không gian" và việc so khớp vô nghĩa.
  • Chuẩn hóa văn bản trước khi embedding (bỏ ký tự rác, chuẩn hóa khoảng trắng) để vector ổn định hơn.
  • Embedding và sinh văn bản là hai việc khác nhau: bạn dùng một model embedding để biến văn bản thành vector, và dùng Claude để sinh câu trả lời cuối. Đừng nhầm hai vai trò.

Vector database

Một vector database là cơ sở dữ liệu được tối ưu cho phép toán "tìm các vector gần nhất với vector truy vấn" (nearest-neighbor search) trên hàng triệu/tỷ vector, nhanh ở mức mili-giây. Nó dùng các chỉ mục xấp xỉ (ANN — approximate nearest neighbor) như HNSW để không phải so sánh tuần tự từng vector.

Một vài lựa chọn phổ biến (chỉ nêu tên để bạn tham khảo):

  • pgvector: tiện ích mở rộng cho PostgreSQL. Tốt nếu bạn đã có Postgres và muốn để vector cạnh dữ liệu quan hệ.
  • FAISS: thư viện của Meta, chạy trong tiến trình (in-process), rất nhanh cho thử nghiệm và quy mô vừa; bạn tự quản lý lưu trữ.
  • Pinecone: dịch vụ vector DB được quản lý (managed), không cần tự vận hành hạ tầng.

Điểm chung: tất cả đều lưu (vector, metadata, id) và trả về top-k theo độ tương đồng (thường là cosine similarity hoặc khoảng cách Euclid). Lựa chọn phụ thuộc quy mô, hạ tầng sẵn có và ngân sách vận hành.


Tool calling (function calling)

RAG đưa kiến thức tĩnh vào ngữ cảnh. Nhưng nhiều câu hỏi cần hành động động: tra số dư tài khoản theo thời gian thực, gọi một API, chạy một phép tính, truy vấn database. Đây là việc của tool calling (còn gọi là function calling).

Cơ chế: bạn mô tả các công cụ (tên, mô tả, lược đồ tham số) cho model. Model không tự thực thi công cụ — nó chỉ quyết định khi nào cần gọi công cụ nào với tham số gì, rồi trả về một yêu cầu gọi công cụ. Code của bạn thực thi công cụ đó và gửi kết quả lại cho model. Đây là một vòng lặp (tool use loop).

Ví dụ tool calling với Claude

import anthropic
client = anthropic.Anthropic()
tools = [{
    "name": "get_balance",
    "description": "Lấy số dư tài khoản theo số tài khoản",
    "input_schema": {"type": "object",
        "properties": {"account_no": {"type": "string"}},
        "required": ["account_no"]},
}]
resp = client.messages.create(
    model="claude-opus-4-8", max_tokens=1024, tools=tools,
    messages=[{"role": "user", "content": "Số dư tài khoản 0001-A?"}],
)
# Nếu resp.stop_reason == "tool_use": thực thi tool rồi gửi tool_result lại cho model

Hoàn tất vòng lặp tool use

Đoạn trên mới là bước đầu. Khi resp.stop_reason == "tool_use", model đang "xin" bạn chạy công cụ. Bạn chạy, rồi gửi kết quả về dưới dạng khối tool_result, và model dùng kết quả đó để soạn câu trả lời cuối:

def get_balance(account_no: str) -> str:
    # Code thật của bạn: truy vấn core banking, database, v.v.
    return "12,500,000 VND"

if resp.stop_reason == "tool_use":
    tool_use = next(b for b in resp.content if b.type == "tool_use")
    result = get_balance(**tool_use.input)

    follow_up = client.messages.create(
        model="claude-opus-4-8", max_tokens=1024, tools=tools,
        messages=[
            {"role": "user", "content": "Số dư tài khoản 0001-A?"},
            {"role": "assistant", "content": resp.content},  # giữ nguyên khối tool_use
            {"role": "user", "content": [{
                "type": "tool_result",
                "tool_use_id": tool_use.id,   # phải khớp id của tool_use
                "content": result,
            }]},
        ],
    )
    print(next(b.text for b in follow_up.content if b.type == "text"))

Ba quy tắc bắt buộc:

  • Luôn gắn lại toàn bộ resp.content (chứa khối tool_use) vào lịch sử, không chỉ phần text.
  • tool_use_id trong tool_result phải khớp id của khối tool_use tương ứng.
  • Nếu model gọi nhiều công cụ một lúc, trả tất cả tool_result trong một tin nhắn user.

Agent: tự lập kế hoạch, hành động nhiều bước

Một agent nâng vòng lặp tool use lên mức tự chủ hơn. Thay vì một lượt gọi công cụ, agent lặp liên tục theo chu trình quan sát → suy nghĩ → hành động cho đến khi hoàn thành mục tiêu. Model tự quyết định: cần dùng công cụ nào, theo thứ tự nào, khi nào đủ thông tin để dừng.

Ví dụ: "Tổng hợp chi tiêu tháng này của khách hàng X và cảnh báo nếu vượt hạn mức". Một agent có thể: gọi công cụ tra giao dịch → cộng dồn → gọi công cụ tra hạn mức → so sánh → soạn cảnh báo. Mỗi bước, model nhìn kết quả của bước trước rồi quyết định bước tiếp.

Triển khai agent về bản chất là lặp lại vòng lặp tool use: chừng nào stop_reason còn là tool_use thì còn chạy công cụ và gửi kết quả lại; khi stop_reason == "end_turn" thì model đã có câu trả lời cuối và ta dừng. Với Claude, bạn có thể bật adaptive thinking (thinking={"type": "adaptive"}) để model tự cân nhắc mức độ suy luận giữa các bước — rất hữu ích cho agent nhiều bước.

Một lưu ý quan trọng: agent mạnh nhưng cũng tốn kém (nhiều lượt gọi, độ trễ cao) và khó kiểm soát hơn. Chỉ nên dùng agent khi bài toán thực sự cần tự chủ nhiều bước — phần "khi nào dùng gì" bên dưới sẽ làm rõ.


MCP — Model Context Protocol

Khi số lượng công cụ và nguồn dữ liệu tăng lên, việc tự viết code tích hợp cho từng cái trở nên rối rắm. MCP (Model Context Protocol) là một chuẩn mở để kết nối LLM với công cụ và nguồn dữ liệu bên ngoài theo một cách thống nhất.

Hãy hình dung MCP như "cổng USB-C cho AI": một server MCP công bố các công cụ/tài nguyên theo chuẩn, và bất kỳ ứng dụng LLM nào nói được MCP đều cắm vào dùng được — không cần viết lại tích hợp riêng cho mỗi model hay mỗi công cụ. Ví dụ: một MCP server cho GitHub, một cho hệ thống file, một cho database nội bộ; agent của bạn kết nối tất cả qua cùng một giao thức.

Lợi ích chính của MCP:

  • Tái sử dụng: viết một MCP server cho hệ thống của bạn một lần, dùng được với nhiều ứng dụng/model.
  • Tách bạch: phần kết nối công cụ tách khỏi phần logic của model, dễ bảo trì.
  • Hệ sinh thái: nhiều công cụ phổ biến đã có sẵn MCP server.

Trong thực tế, bạn có thể dùng MCP để cấp cho agent quyền truy cập một loạt công cụ chuẩn hóa, thay vì khai báo thủ công từng tool như ví dụ trên.


RAG gặp SQL Builder của trang web (ý tưởng)

Trang web này có công cụ SQL Builder giúp dựng và chạy truy vấn lên dữ liệu của bạn. Ta có thể liên kết RAG với SQL Builder ở mức ý tưởng để trả lời câu hỏi cần dữ liệu có cấu trúc tươi mới:

Khác biệt so với RAG cổ điển: thay vì truy hồi văn bản từ vector DB, ta truy hồi dữ liệu có cấu trúc bằng một truy vấn SQL do công cụ dựng, rồi đưa bảng kết quả vào ngữ cảnh cho Claude tóm tắt/giải thích. Đây có thể coi là một biến thể của RAG: nguồn tri thức là database thay vì kho tài liệu. Bạn còn có thể đặt SQL Builder làm một công cụ (tool) để agent tự gọi khi cần số liệu.


Khi nào dùng single call, workflow, hay agent

Không phải lúc nào cũng cần agent. Nguyên tắc vàng: bắt đầu từ mức đơn giản nhất đáp ứng được nhu cầu. Phần lớn bài toán chỉ cần một lượt gọi hoặc một workflow do bạn điều phối.

Tiêu chíSingle callWorkflowAgent
Bản chấtMột yêu cầu, một phản hồiNhiều bước, logic do code điều khiểnNhiều bước, model tự quyết trình tự
Ai điều khiển vòng lặpKhông có vòng lặpCode của bạnModel
Ví dụPhân loại, tóm tắt, trích xuất, hỏi-đáp một lượt; RAG đơn giảnPipeline cố định: trích xuất → kiểm tra → định dạng; RAG nhiều bước có công cụTác vụ mở, khó đặc tả trước: "biến tài liệu này thành báo cáo hoàn chỉnh"
Chi phí & độ trễThấp nhấtTrung bìnhCao nhất
Khả năng kiểm soátCaoCao (bạn viết logic)Thấp hơn (model tự đi)
Khi nào chọnViệc rõ ràng, một bướcQuy trình biết trước nhưng nhiều bướcCần tự chủ, không đoán trước được các bước

Bốn câu hỏi sàng lọc trước khi quyết dùng agent: bài toán có phức tạp/khó đặc tả trước không? Kết quả có đáng với chi phí và độ trễ cao hơn không? Model có đủ năng lực cho dạng việc này không? Lỗi có bắt và sửa được không (có test, review, rollback)? Nếu có câu trả lời "không", hãy lùi về workflow hoặc single call.


Tóm tắt

  • RAG giải quyết ba giới hạn của LLM (không biết dữ liệu riêng/mới, hay bịa) bằng cách truy hồi tài liệu liên quan và đưa vào ngữ cảnh, thường kèm dẫn nguồn.
  • Kiến trúc RAG: tài liệu → chunking → embedding → vector DB → truy hồi top-k (semantic search) → ghép ngữ cảnh → LLM sinh câu trả lời. Chất lượng chunking và việc dùng cùng một model embedding cho cả hai pha là quyết định.
  • Vector database (pgvector, FAISS, Pinecone…) lưu vector và tìm các vector gần nhất nhanh.
  • Tool calling cho model gọi hàm/công cụ bên ngoài qua một vòng lặp: model xin gọi → code thực thi → gửi tool_result lại → model trả lời.
  • Agent lặp chu trình quan sát–suy nghĩ–hành động, tự lập kế hoạch và dùng nhiều công cụ; mạnh nhưng tốn kém.
  • MCP là chuẩn mở để kết nối công cụ/nguồn dữ liệu cho LLM, giúp tái sử dụng và tách bạch tích hợp.
  • Chọn single call → workflow → agent theo mức độ phức tạp; ưu tiên đơn giản nhất.
  • Mọi code dùng họ Claude qua SDK anthropic (Opus 4.8, Sonnet 4.6, Haiku 4.5, Fable 5).

Tự kiểm tra

  1. Nêu ba giới hạn của LLM mà RAG giúp khắc phục, và giải thích RAG giảm hallucination bằng cơ chế nào.
  2. Liệt kê đúng thứ tự các bước trong kiến trúc RAG, từ tài liệu thô đến câu trả lời có dẫn nguồn.
  3. Vì sao phải dùng cùng một model embedding cho cả lúc lập chỉ mục lẫn lúc truy vấn câu hỏi?
  4. Trong tool calling với Claude, khi stop_reason == "tool_use" thì bên nào thực thi công cụ, và phải gửi gì lại cho model để hoàn tất vòng lặp?
  5. Phân biệt workflow và agent: ai điều khiển vòng lặp trong mỗi trường hợp, và khi nào nên chọn agent thay vì workflow?
  6. MCP giải quyết vấn đề gì khi số lượng công cụ và nguồn dữ liệu tăng lên?

Đọc tiếp

AI 8 — MLOps & Đạo đức AI

Bài viết liên quan

Phân biệt AI/ML/DL, các kiểu học máy, quy trình ML end-to-end và thuật ngữ nền tảng.

13 thg 7, 2026 14

Chất lượng dữ liệu, xử lý thiếu/ngoại lai, mã hoá, chuẩn hoá, tạo đặc trưng và tránh data leakage.

13 thg 7, 2026 12

Mô hình ngôn ngữ lớn hoạt động ra sao, token, context window, tham số sinh, và kỹ thuật viết prompt.

13 thg 7, 2026 11

Đưa mô hình ra production: phục vụ, giám sát, model/data drift, versioning, và AI có trách nhiệm.

13 thg 7, 2026 10