---
title: "Function calling là gì? Phân biệt với Structured Outputs và MCP"
description: "Function calling nối model với dữ liệu thật qua host app: vòng tool call, các bước app phải kiểm tra, và điểm khác với Structured Outputs cùng MCP."
canonical: "https://200lab.io/blog/function-calling-la-gi"
published: "2026-11-08T15:16:39Z"
updated: "2026-11-08T15:16:39Z"
authors: ["Việt Trần"]
tags: ["AI", "Agent", "System Design"]
reading_minutes: 15
access: "public"
---

Giả sử bạn xây một trợ lý chăm sóc khách hàng trong app mua sắm online. Người dùng đã đăng nhập với mã `user_sample_A` và hỏi đơn hàng `ORD-10042` của mình tới đâu rồi. Nếu model chỉ dựa vào dữ liệu đã train để trả lời, nó rất dễ bịa ra trạng thái giao hàng, tên đơn vị vận chuyển hoặc mã vận đơn. Để trả lời đúng, hệ thống bắt buộc phải tra cứu đơn hàng thật trong database chứ không để model tự đoán. Bài viết này giải thích function calling là gì, vòng xử lý từ câu hỏi tới câu trả lời đi qua đâu, những bước app phải kiểm tra trước khi chạy hàm, cũng như điểm khác nhau giữa cơ chế này với Structured Outputs và MCP.

## Function calling là gì?

**Function calling là cơ chế để language model trả về một yêu cầu gọi hàm có cấu trúc gồm tên hàm và bộ tham số, còn host app mới là bên kiểm tra rồi thực thi code thật.** Model không tự chạy hàm nghiệp vụ hay chạm trực tiếp vào database của bạn.

Hình dung chuyện này giống quầy lễ tân ở một tòa nhà. Khách hỏi hàng của mình đã tới chưa, lễ tân ghi đúng mã đơn vào phiếu yêu cầu rồi chuyển cho bộ phận kho kiểm tra, chứ lễ tân không tự mở cửa kho đi lục hàng.

Trong [hướng dẫn function calling của OpenAI](https://developers.openai.com/api/docs/guides/function-calling), cơ chế này còn được gọi là tool calling: app khai báo danh sách tool kèm schema, model trả về một tool call chứa tên hàm cùng `arguments` khi cần dữ liệu ngoài, và code của bạn chạy xong sẽ gửi kết quả về lại. Phía Anthropic gọi cơ chế tương tự là tool use trong [tài liệu tổng quan tool use](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview), đồng thời tách rõ client tools do app của bạn tự chạy với server tools chạy phía nhà cung cấp.

Google cũng dùng tên function calling và khẳng định thẳng trong [tài liệu function calling của Gemini API](https://ai.google.dev/gemini-api/docs/function-calling) rằng model không tự thực thi hàm. Dù ba hãng đặt tên hơi khác nhau, bản chất kiến trúc vẫn là một: model đề xuất ý định gọi hàm, còn app giữ quyền thực thi.

## Vòng lặp function calling từ câu hỏi tới câu trả lời

Quay lại tình huống giả định ở đầu bài: khách `user_sample_A` hỏi trạng thái đơn `ORD-10042` gồm món `Sample Ceramic Mug`. Để lấy được thông tin thật mà không lộ dữ liệu nội bộ thừa, luồng xử lý đi qua bảy bước rõ ràng:

1. **Người dùng gửi câu hỏi:** Khách đang đăng nhập hỏi đơn `ORD-10042` tới đâu rồi.
2. **Host app gọi model kèm khai báo tool:** App gửi câu hỏi và schema mô tả hàm `get_order_status` cho model biết mình có tool tra cứu đơn.
3. **Model trả về tool call:** Thay vì viết câu trả lời ngay, model trả dữ liệu có cấu trúc gồm tên hàm `get_order_status` và `arguments` là `{"order_id": "ORD-10042"}`.
4. **App kiểm tra quyền và dữ liệu đầu vào:** Host app đối chiếu `arguments` với schema, kiểm tra trạng thái đăng nhập và xác nhận đơn hàng thuộc về `user_sample_A`.
5. **App chạy hàm hoặc gọi API nội bộ:** Code phía app truy vấn kho dữ liệu, lấy bản ghi có trạng thái `shipped`, đơn vị vận chuyển `Sample Carrier` và mã `TRACK-SAMPLE-7788`.
6. **App trả tool result cho model:** App đóng gói kết quả tra cứu gắn với `call_id` rồi gửi lại model, chủ động lược bỏ trường `owner_user_id`.
7. **Model viết câu trả lời cuối:** Model thường dựa vào tool result vừa nhận để diễn đạt câu trả lời tự nhiên cho người dùng, dù bản thân cơ chế này không bảo đảm model sẽ không tự suy diễn thêm chi tiết ngoài dữ liệu host app đã gửi.

```mermaid
flowchart TD
  A["Người dùng hỏi trạng thái đơn"] --> B["Host app gửi câu hỏi kèm schema tool"]
  B --> C["Model trả về tool call"]
  C --> D["App kiểm tra auth, schema và nghiệp vụ"]
  D --> E["Hàm hoặc API tra cứu đơn"]
  E --> F["App gửi tool result cho model"]
  F --> G["Model viết câu trả lời cho người dùng"]
```

Ở một lượt tra cứu đơn giản, vòng này chạy một lần là xong. Khi bài toán phức tạp hơn, model có thể nhận tool result rồi tiếp tục đề xuất thêm các tool call kế tiếp theo vòng lặp nhiều bước; lúc đó kiến trúc chuyển sang dạng agent như bài [AI Agent là gì](/blog/ai-agent-la-gi) đã viết. Ngoài ra, mặc định model vẫn có quyền không gọi tool, hỏi lại khi thiếu mã đơn hoặc từ chối yêu cầu ngoài phạm vi.

## Host app kiểm tra schema và quyền trước khi chạy hàm

Một hiểu lầm phổ biến của anh em mới làm tính năng AI là tin rằng khai báo JSON Schema hay bật tùy chọn `strict` đã đủ làm lớp bảo vệ. Thực tế, `strict` chỉ giúp model sinh `arguments` bám đúng hình dạng schema hơn, hoàn toàn không đảm bảo người dùng hiện tại có quyền chạm vào dữ liệu đó.

Host app của bạn phải tự làm guardrail ở ranh giới thực thi. App cần kiểm tra lại `arguments` xem có thiếu trường hay sai định dạng không, đối chiếu tài nguyên với người dùng đang đăng nhập, và áp dụng nguyên tắc least privilege, tức chỉ khai báo đúng những hàm thật sự cần thiết như cách tiếp cận ở bài [phân quyền cho AI agent](/blog/cho-bot-quyen-gi-phan-quyen-ai-agent).

Bạn cũng cần tách bạch giữa tool chỉ đọc như xem trạng thái đơn và tool có ghi dữ liệu như hủy đơn hay hoàn tiền. Với các thao tác ghi, app phải yêu cầu người dùng xác nhận rõ trước khi chạy; và nếu hệ thống có cơ chế tự gọi lại khi gặp lỗi timeout ở các hàm gây thay đổi dữ liệu, bạn bắt buộc phải thiết kế idempotency theo hướng dẫn tại bài [Idempotency key là gì](/blog/idempotency-key-la-gi) để tránh trừ tiền hay hủy nhầm hai lần.

Để thấy rõ vai trò chặn lọc của host app, mình viết một host stub mô phỏng và tự soạn sẵn năm mẫu tool call để chạy thử mà không gọi vào model thật. Chúng ta thử xem đoạn code dưới đây nha:

```python
# host_dispatch.py
# Simulated host dispatch: validate a tool call, then look up a sample order.
# This script does not call a live language model.

import json
import re

SAMPLE_ORDERS = {
    "ORD-10042": {
        "order_id": "ORD-10042",
        "status": "shipped",
        "carrier": "Sample Carrier",
        "tracking": "TRACK-SAMPLE-7788",
        "item": "Sample Ceramic Mug",
        "owner_user_id": "user_sample_A",
    },
    "ORD-10099": {
        "order_id": "ORD-10099",
        "status": "processing",
        "carrier": None,
        "tracking": None,
        "item": "Sample Notebook Set",
        "owner_user_id": "user_sample_B",
    },
}

ALLOWED_TOOLS = {"get_order_status"}
ORDER_ID_RE = re.compile(r"^ORD-\d+$")

def validate_schema(arguments):
    if not isinstance(arguments, dict):
        return "arguments must be an object"
    extra = set(arguments) - {"order_id"}
    if extra:
        return "unexpected argument"
    if "order_id" not in arguments:
        return "missing required argument: order_id"
    order_id = arguments["order_id"]
    if not isinstance(order_id, str) or not ORDER_ID_RE.match(order_id):
        return "order_id must match ORD- followed by digits"
    return None

def handle_tool_call(session_user_id, tool_call):
    name = tool_call.get("name")
    if name not in ALLOWED_TOOLS:
        return {"ok": False, "error": "unknown or disallowed function"}

    err = validate_schema(tool_call.get("arguments"))
    if err:
        return {"ok": False, "error": f"schema validation failed: {err}"}

    order_id = tool_call["arguments"]["order_id"]
    order = SAMPLE_ORDERS.get(order_id)
    if order is None:
        return {"ok": False, "error": "order not found"}
    if order["owner_user_id"] != session_user_id:
        return {"ok": False, "error": "permission denied"}

    return {
        "ok": True,
        "order_id": order["order_id"],
        "status": order["status"],
        "carrier": order["carrier"],
        "tracking": order["tracking"],
        "item": order["item"],
    }

def show(title, session_user_id, tool_call):
    print(f"=== {title} ===")
    print("session_user_id:", session_user_id)
    print("tool_call:", json.dumps(tool_call, ensure_ascii=True))
    result = handle_tool_call(session_user_id, tool_call)
    print("tool_result:", json.dumps(result, ensure_ascii=True))
    print()

if __name__ == "__main__":
    show(
        "valid lookup",
        "user_sample_A",
        {"name": "get_order_status", "arguments": {"order_id": "ORD-10042"}},
    )
    show(
        "invalid schema",
        "user_sample_A",
        {
            "name": "get_order_status",
            "arguments": {"order_id": "please look up my latest order"},
        },
    )
    show(
        "other customer's order",
        "user_sample_A",
        {"name": "get_order_status", "arguments": {"order_id": "ORD-10099"}},
    )
    show(
        "unknown function",
        "user_sample_A",
        {"name": "cancel_order", "arguments": {"order_id": "ORD-10042"}},
    )
    show(
        "order not found",
        "user_sample_A",
        {"name": "get_order_status", "arguments": {"order_id": "ORD-99999"}},
    )
```

Khi chạy bộ mẫu qua hàm điều phối này, kết quả thu được như sau:

```text
=== valid lookup ===
session_user_id: user_sample_A
tool_call: {"name": "get_order_status", "arguments": {"order_id": "ORD-10042"}}
tool_result: {"ok": true, "order_id": "ORD-10042", "status": "shipped", "carrier": "Sample Carrier", "tracking": "TRACK-SAMPLE-7788", "item": "Sample Ceramic Mug"}

=== invalid schema ===
session_user_id: user_sample_A
tool_call: {"name": "get_order_status", "arguments": {"order_id": "please look up my latest order"}}
tool_result: {"ok": false, "error": "schema validation failed: order_id must match ORD- followed by digits"}

=== other customer's order ===
session_user_id: user_sample_A
tool_call: {"name": "get_order_status", "arguments": {"order_id": "ORD-10099"}}
tool_result: {"ok": false, "error": "permission denied"}

=== unknown function ===
session_user_id: user_sample_A
tool_call: {"name": "cancel_order", "arguments": {"order_id": "ORD-10042"}}
tool_result: {"ok": false, "error": "unknown or disallowed function"}

=== order not found ===
session_user_id: user_sample_A
tool_call: {"name": "get_order_status", "arguments": {"order_id": "ORD-99999"}}
tool_result: {"ok": false, "error": "order not found"}
```

_Output của chương trình trên_

Trong ví dụ trên, hàm `handle_tool_call` nhận `session_user_id` từ ngữ cảnh đăng nhập của app chứ không nhận từ tham số do model truyền vào, rồi kiểm tra lần lượt năm tình huống:

- **Tra cứu hợp lệ `ORD-10042`:** Tên hàm nằm trong `ALLOWED_TOOLS`, mã đơn khớp định dạng và `owner_user_id` trùng với `user_sample_A`, nên hàm trả về `ok: true` kèm thông tin món `Sample Ceramic Mug` nhưng đã lọc bỏ `owner_user_id`.
- **Sai định dạng schema:** Khi `order_id` mang chuỗi `"please look up my latest order"`, bước kiểm tra chặn lại ngay vì giá trị không khớp mẫu `ORD-` đi kèm chữ số.
- **Đúng schema nhưng sai chủ sở hữu:** Mã `ORD-10099` vượt qua bước kiểm tra hình thức, nhưng thuộc về `user_sample_B` nên host app trả lỗi `permission denied`.
- **Gọi hàm ngoài danh sách cho phép:** Mẫu gọi `cancel_order` có `arguments` hợp lệ nhưng vẫn bị từ chối bằng lỗi `unknown or disallowed function` do app cố tình không mở quyền hủy đơn.
- **Đơn hàng không tồn tại:** Mã `ORD-99999` đúng chuẩn định dạng nhưng không có trong dữ liệu mẫu, hàm trả về `order not found` trong tool result để model có căn cứ phản hồi lại cho khách.

> 💡 Schema chỉ giúp model điền đúng khuôn dữ liệu, còn kiểm tra quyền sở hữu và giới hạn danh sách hàm luôn là trách nhiệm của host app.

Hai nhánh dưới đây lấy đúng dữ liệu mẫu của host stub: đơn `ORD-10042` của `user_sample_A` được tra cứu, còn `ORD-10099` của `user_sample_B` bị app chặn trước khi lộ dữ liệu. Đây là mô phỏng phía host, không phải output bảo đảm của một model thật.

## Model không bắt buộc phải gọi tool

Khai báo danh sách tool cho model không có nghĩa là lượt chat nào model cũng phải phát sinh tool call. Ở cấu hình mặc định `tool_choice` dạng `auto`, model tự đánh giá context cuộc trò chuyện và mô tả của từng hàm để quyết định hướng phản hồi phù hợp.

Theo tài liệu của các hãng, phản hồi của model thường rẽ theo ba nhánh ngoài trường hợp gọi hàm bình thường. Mình lấy vài tình huống minh họa giả định xoay quanh bài toán shop online để bạn dễ hình dung:

- **Hỏi lại khi thiếu tham số bắt buộc:** Nếu khách chỉ nhắn "kiểm tra giúp đơn hàng của mình tới đâu rồi" mà không đưa mã `ORD-...`, model có thể hỏi lại mã đơn thay vì tự gọi hàm; dù vậy tài liệu hãng cũng lưu ý model đôi khi tự đoán giá trị chưa được cung cấp nên bước validate ở host app vẫn không thể bỏ.
- **Trả lời trực tiếp không qua tool:** Khi khách hỏi chính sách đổi trả chung đã nằm sẵn trong phần hướng dẫn context hoặc hỏi một câu chào hỏi thông thường, model trả lời thẳng để tiết kiệm một vòng gọi hàm.
- **Từ chối yêu cầu ngoài phạm vi:** Nếu người dùng đòi xóa toàn bộ dữ liệu trái với chỉ dẫn hệ thống, model thường sẽ từ chối hoặc hỏi lại; riêng việc đòi xem đơn của tài khoản khác thì model vẫn có thể đề xuất tool call nên host app mới là bên phải kiểm tra quyền để chặn.

Nếu bài toán nghiệp vụ cần kiểm soát chặt hơn hành vi mặc định, API của các hãng vẫn cho phép bạn đổi cấu hình để ép model bắt buộc phải gọi ít nhất một tool, chỉ định đích danh một hàm cụ thể, hoặc tắt hẳn việc gọi tool trong lượt đó.

## Function calling khác Structured Outputs chỗ nào?

Khi làm việc với API của các hãng, bạn sẽ gặp một khái niệm rất dễ nhầm với function calling là Structured Outputs. Cả hai cơ chế đều dùng JSON Schema để ràng buộc đầu ra của model không bị thiếu trường bắt buộc hay sai kiểu dữ liệu, điểm khác biệt nằm ở đích đến của dữ liệu đó.

Theo [tài liệu Structured Outputs của OpenAI](https://developers.openai.com/api/docs/guides/structured-outputs) cũng như cách phân chia tương tự bên phía Gemini, bạn dùng function calling khi cần nối model vào hàm, tool hoặc nguồn dữ liệu bên trong hệ thống làm bước trung gian. Ngược lại, bạn dùng Structured Outputs ở phần định dạng phản hồi như `text.format` phía OpenAI khi muốn bản thân câu trả lời cuối cùng gửi về cho người dùng hoặc UI app phải bám chặt một schema cố định.

Gom lại thành bảng cho dễ nhìn:

| Tiêu chí | Function calling | Structured Outputs cho câu trả lời |
| --- | --- | --- |
| **Mục đích chính** | Kết nối model với hàm, API và dữ liệu của hệ thống | Ép câu trả lời cuối của model tuân theo cấu trúc cố định |
| **Schema đặt ở đâu** | Khai báo ở phần `parameters` của từng tool | Khai báo ở cấu hình định dạng đầu ra như `text.format` |
| **Ai xử lý tiếp** | Host app chạy hàm rồi gửi `tool_result` quay lại model | App nhận thẳng JSON cuối để hiển thị hoặc vẽ UI |
| **Ví dụ điển hình** | Trợ lý tra cứu trạng thái đơn hàng từ database | App dạy toán nhận từng bước lời giải tách sẵn để vẽ lên UI |

Hai cơ chế này không loại trừ nhau. Trong cùng một lượt xử lý, model có thể dùng function calling để lấy dữ liệu đơn hàng thật về trước, sau đó xuất câu trả lời cuối theo chuẩn Structured Outputs để UI mobile hiển thị thẻ trạng thái đơn hàng.

## Function calling khác MCP chỗ nào?

Một câu hỏi nữa cũng hay xuất hiện là khi đã có Model Context Protocol, viết tắt là MCP, thì hệ thống có còn dùng function calling nữa không. Theo [đặc tả Model Context Protocol](https://modelcontextprotocol.io/specification/2026-07-28), MCP là giao thức mở dùng JSON-RPC 2.0 để chuẩn hóa cách một host app thông qua client kết nối tới các server cung cấp Tools, Resources và Prompts. Nếu muốn xem kỹ kiến trúc thành phần và luồng kết nối thực tế, bạn đọc thêm bài [Model Context Protocol (MCP) là gì](/blog/model-context-protocol-mcp-la-gi).

**Function calling là cơ chế gọi hàm native ở tầng model, còn MCP là giao thức chuẩn hóa kết nối giữa app với các tool và nguồn dữ liệu bên ngoài.** MCP không thay thế function calling của bản thân model, và ngược lại function calling cũng không phải nền tảng giao thức hay thành phần phụ thuộc bắt buộc của MCP; hai bên giải quyết hai tầng hoàn toàn độc lập.

Trong thực tế, bạn hoàn toàn có thể ghép hai tầng này lại với nhau. Model vẫn giao tiếp với host app bằng cơ chế function calling quen thuộc, rồi phía sau hậu trường, host app đóng vai trò bộ chuyển đổi để dịch yêu cầu đó thành lệnh gọi sang MCP server; hoặc bản thân nền tảng cung cấp sẵn đường nối như built-in tool làm việc với MCP server của OpenAI hay MCP connector bên phía Anthropic.

Dù nối trực tiếp bằng hàm nội bộ hay đi vòng qua MCP, bài toán an toàn vẫn quay về tay người làm app. Đặc tả MCP yêu cầu rõ phải có sự đồng ý của người dùng trước khi truy cập dữ liệu hay kích hoạt tool vì tool là đường chạy code tùy ý, nhưng bản thân giao thức MCP không thể tự cưỡng chế các nguyên tắc bảo mật này ở tầng protocol.

> Model chỉ đề xuất tên hàm và bộ tham số dựa trên context chữ nghĩa, còn host app của bạn mới là nơi giữ quyền kiểm tra dữ liệu, xác thực người dùng và quyết định có chạy dòng code đó hay không.

Chúc các bạn nối tool mạch lạc, tra đơn trúng phóc và không bao giờ để model tự sáng tác mã vận đơn cho khách 🙂

## Đọc thêm

- [Multi-agent là gì? Khi nào một agent không đủ và 3 cách chia việc](https://200lab.io/blog/multi-agent-la-gi.md): Multi-agent là gì: mạnh ở bài toán rộng chạy được song song, nhưng chỉ nên tách khi phần việc làm độc lập được và bù nổi chi phí token.
- [Cách nhờ Grok Bot lọc khách quen và soạn nháp tin hỏi thăm](https://200lab.io/blog/grok-bot-repeat-customer-check-in.md): Soạn tin hỏi thăm khách quen bằng Grok Bot: bot lọc khách theo thời gian cháy, ghi rõ cách tính và chỉ viết nháp để bạn đọc lại rồi tự gửi.
- [Cách nhờ Grok Bot viết checklist đóng gói đơn hàng cho người mới](https://200lab.io/blog/grok-bot-packing-checklist.md): Soạn checklist đóng gói bằng Grok Bot từ lời kể lộn xộn của chủ shop: bot xếp rõ từng việc cho người thời vụ, hỏi lại chỗ thiếu và chỉ bám sát ý bạn.
- [Hướng dẫn nhờ Grok Bot và Jev phân loại email Gmail mỗi sáng cho shop nhỏ](https://200lab.io/blog/grok-bot-gmail-inbox-triage.md): Phân loại email bằng Grok Bot và Jev: Jev gắn nhãn kèm confidence, Grok Bot tóm tắt và soạn nháp, ca ngoài quy định chờ chủ shop, bot không tự gửi thư.
- [Embedding là gì? Chạy thử và chọn embedding model cho RAG tiếng Việt](https://200lab.io/blog/embedding-la-gi.md): Embedding là gì và vì sao quyết định RAG tìm đúng hay sai? Tạo vector embedding bằng Python, đo cosine similarity, so sánh và chọn embedding model tiếng Việt.
- [Mọi bài viết chủ đề AI](https://200lab.io/blog/tags/ai.md)

Toàn bộ nội dung: https://200lab.io/llms.txt
