Function calling là gì? Cho model AI gọi hàm Python chạy trên CPU (2026)

AI

Trong bài này
  1. Cần chuẩn bị
  2. Bước 1: Hiểu function calling là gì
  3. Bước 2: Nắm quy trình 4 bước (model không tự chạy hàm)
  4. Bước 3: Chọn model nhỏ chuyên gọi hàm: BotPsy-0.8B
  5. Bước 4: Cài môi trường Python
  6. Bước 5: Viết hàm Python làm "công cụ"
  7. Bước 6: Xem prompt mà model thật sự nhìn thấy
  8. Bước 7: Chạy model, đọc lời gọi hàm và gửi kết quả lại
  9. Bước 8: Thử câu hỏi có nhiều tham số và câu hỏi không cần công cụ
  10. Bước 9: Vòng lặp agent: gọi nhiều hàm rồi mới trả lời
  11. Lỗi thường gặp
  12. Hỏi bằng tiếng Việt, model truyền tham số sai
  13. Model thường không gọi hàm, chỉ xin lỗi
  14. Cảnh báo "falling back to its reference PyTorch implementation"
  15. Câu hỏi thường gặp
  16. Function calling khác MCP thế nào?
  17. Model có tự chạy code của mình không?
  18. Dùng ChatGPT, Gemini, Claude API thì có khác không?
  19. Có dùng BotPsy-0.8B cho đồ án được không?
  20. Máy không có GPU chạy có nổi không?
  21. Nguồn tham khảo

Khi các bạn thấy ChatGPT tra thời tiết, Claude Code sửa file hay Gemini đặt lịch họp, phía sau đều là một cơ chế gọi là function calling (còn gọi là tool calling, tool use). Trong bài này mình giải thích function calling là gì, rồi cùng các bạn tự làm một "trợ lý sinh viên" nhỏ: model AI đọc câu hỏi, tự quyết định gọi hàm Python nào, chương trình chạy hàm, rồi model trả lời dựa trên kết quả.

Toàn bộ chạy bằng CPU, không cần API key, không cần GPU, dùng model BotPsy-0.8B vừa được đăng lên Hugging Face ngày 10/11/2026. Làm theo mất khoảng 20–30 phút (phần lớn là thời gian tải 1,7 GB model). Bài áp dụng cho transformers 5.23 và PyTorch 2.15, thử nghiệm ngày 14/11/2026.

Video demo trên kênh Huy Nguyen - Android A - Z.

Cần chuẩn bị

  • Máy có Python 3.10 trở lên (mình dùng 3.12), RAM trống khoảng 4 GB và 5 GB ổ cứng.
  • Biết viết hàm Python cơ bản và biết JSON là gì.
  • Nên đọc trước bài Token là gì trong AI và Hugging Face là gì nếu các bạn chưa từng chạy model bằng transformers.

Bước 1: Hiểu function calling là gì

Model ngôn ngữ chỉ sinh ra chữ, nó không tự đọc được lịch học hay database của các bạn. Function calling là cách mình mô tả cho model biết có những hàm nào (tên, công dụng, tham số). Khi cần dữ liệu, model sinh ra một "lời gọi hàm" có cấu trúc gồm tên hàm và giá trị tham số, thay vì bịa câu trả lời. Tài liệu của Claude cũng định nghĩa như vậy.

Trang tài liệu Tool use with Claude, đoạn định nghĩa tool use còn gọi là function calling được khoanh đỏ
Bước 1: định nghĩa function calling trong tài liệu chính thức của Claude.

Bước 2: Nắm quy trình 4 bước (model không tự chạy hàm)

Tài liệu Gemini API mô tả quy trình gồm 4 bước: khai báo hàm, gửi câu hỏi kèm khai báo cho model, chương trình của mình chạy hàm, rồi gửi kết quả lại để model viết câu trả lời. Điểm quan trọng nhất mà nhiều bạn hiểu nhầm: model không tự chạy code. Nó chỉ nói "hãy gọi hàm X với tham số Y", còn việc chạy là trách nhiệm của code các bạn viết.

Mục How function calling works trong tài liệu Gemini API, bước Execute Function Code được khoanh đỏ
Bước 2: bước 3 trong quy trình là việc của chương trình, không phải của model.

Bước 3: Chọn model nhỏ chuyên gọi hàm: BotPsy-0.8B

Các model lớn như GPT, Gemini, Claude đều gọi hàm rất tốt nhưng cần tài khoản và API key. Để học cơ chế, mình dùng qvac/BotPsy-0.8B của Tether AI Research: model 0,8 tỷ tham số, tinh chỉnh từ Qwen3.5-0.8B riêng cho function calling, giấy phép Apache 2.0, không cần đăng nhập để tải. Theo model card, điểm BFCL v4 (một bộ đo khả năng gọi hàm) là 44,0 so với 23,4 của model gốc. Đây là số do nhóm phát triển tự công bố, nên các bạn coi là tham khảo.

Trang model qvac/BotPsy-0.8B trên Hugging Face với thẻ function-calling và liên kết model gốc Qwen3.5-0.8B được khoanh đỏ
Bước 3: trang model BotPsy-0.8B, có thẻ function-calling và dựa trên Qwen3.5-0.8B.

Ở tab Files and versions, file trọng số model.safetensors nặng 1,71 GB. Lần chạy đầu transformers sẽ tự tải về thư mục cache.

Danh sách file của BotPsy-0.8B trên Hugging Face, dòng model.safetensors 1.71 GB được khoanh đỏ
Bước 3: file model.safetensors 1,71 GB, được đăng 4 ngày trước ngày chụp.

Bước 4: Cài môi trường Python

Mình cài bản PyTorch cho CPU (nhẹ hơn bản có CUDA rất nhiều) rồi cài transformers. Model dựa trên Qwen3.5 nên cần transformers từ bản 5.19 trở lên, theo model card.

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install transformers accelerate jinja2
Terminal cài môi trường, kết quả torch 2.15.0+cpu và transformers 5.23.0, Python 3.12.16
Bước 4: cài xong torch 2.15.0+cpu và transformers 5.23.0.

Bước 5: Viết hàm Python làm "công cụ"

Mình viết hai hàm: get_schedule trả về lịch học theo thứ (dữ liệu mẫu) và calculate_gpa tính điểm trung bình theo tín chỉ. Phần quan trọng là type hint và docstring: hàm get_json_schema của transformers đọc chúng để sinh ra JSON schema, chính là "bản mô tả" mà model sẽ đọc.

LICH_HOC = {
    "monday": [{"subject": "Android Basics", "time": "07:30", "room": "P301"}],
    "tuesday": [{"subject": "Database (SQL Server)", "time": "13:00", "room": "Lab 2"}],
    "wednesday": [],
}

def get_schedule(day: str) -> list:
    """
    Get the class schedule of the student for one day of the week.

    Args:
        day: Day of the week in English, lowercase, e.g. "monday".
    """
    return LICH_HOC.get(day.lower(), [])

def calculate_gpa(scores: list[float], credits: list[int]) -> float:
    """
    Calculate the weighted average score (GPA on a 10-point scale).

    Args:
        scores: Score of each subject, from 0 to 10.
        credits: Number of credits of each subject, same order as scores.
    """
    return round(sum(s * c for s, c in zip(scores, credits)) / sum(credits), 2)
Hàm get_schedule và JSON schema do get_json_schema sinh ra, có name, description, parameters và required
Bước 5: từ docstring, transformers sinh JSON schema gồm tên, mô tả và tham số bắt buộc.

Docstring viết bằng tiếng Anh vì model card khuyến nghị dùng tiếng Anh.

Bước 6: Xem prompt mà model thật sự nhìn thấy

Không có phép màu nào cả: chat template của model chèn danh sách hàm vào system prompt dưới dạng JSON, kèm hướng dẫn định dạng <tool_call>. Các bạn in ra được bằng apply_chat_template(..., tools=tools, tokenize=False).

from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("qvac/BotPsy-0.8B")
messages = [{"role": "user", "content": "What classes do I have on Tuesday?"}]
print(tok.apply_chat_template(messages, tools=[get_schedule, calculate_gpa],
                              tokenize=False, add_generation_prompt=True))
Prompt đầy đủ gửi vào model, gồm khối tools chứa JSON của hai hàm và mẫu định dạng tool_call
Bước 6: hai hàm được chèn vào system prompt, kèm mẫu định dạng <tool_call>.

Bước 7: Chạy model, đọc lời gọi hàm và gửi kết quả lại

Đoạn code chính gồm 3 việc: sinh câu trả lời lần 1, tách khối <tool_call> ra thành tên hàm và tham số, chạy hàm thật, thêm kết quả vào hội thoại với vai trò tool, rồi sinh câu trả lời lần 2.

import json, re, torch
from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained("qvac/BotPsy-0.8B", dtype=torch.bfloat16)
tools = [get_schedule, calculate_gpa]
TOOLS = {"get_schedule": get_schedule, "calculate_gpa": calculate_gpa}

def generate(msgs):
    ids = tok.apply_chat_template(msgs, tools=tools, add_generation_prompt=True,
                                  return_tensors="pt", return_dict=True)
    out = model.generate(**ids, max_new_tokens=400, do_sample=False)
    return tok.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True)

def parse_tool_calls(text):
    calls = []
    for block in re.findall(r"<tool_call>(.*?)</tool_call>", text, re.S):
        name = re.search(r"<function=([\w\-]+)>", block).group(1)
        args = {}
        for k, v in re.findall(r"<parameter=([\w\-]+)>\s*(.*?)\s*</parameter>", block, re.S):
            try:
                args[k] = json.loads(v)      # [8.5, 7, 9] -> list
            except json.JSONDecodeError:
                args[k] = v                  # tuesday -> chuỗi
        calls.append({"name": name, "arguments": args})
    return calls

reply = generate(messages)
calls = parse_tool_calls(reply)
messages.append({"role": "assistant", "content": "",
                 "tool_calls": [{"type": "function", "function": c} for c in calls]})
for c in calls:
    result = TOOLS[c["name"]](**c["arguments"])
    messages.append({"role": "tool", "name": c["name"], "content": json.dumps(result)})
print(generate(messages))
Kết quả chạy: model sinh tool_call get_schedule với day bằng tuesday, chương trình chạy hàm và model trả lời có môn Database lúc 13:00 ở Lab 2
Bước 7: model gọi get_schedule("tuesday"), rồi trả lời đúng theo dữ liệu hàm trả về.

Trên 4 nhân CPU, mỗi lượt sinh mất khoảng 5–6 giây. Lời gọi hàm chỉ có 27 token nên rất nhanh.

Bước 8: Thử câu hỏi có nhiều tham số và câu hỏi không cần công cụ

Với câu hỏi tính GPA, model tự tách điểm và tín chỉ thành hai danh sách đúng thứ tự, hàm Python tính ra 7,94 (các bạn tự kiểm tra: (8,5×3 + 7×4 + 9×2) / 9 = 7,94). Với câu hỏi "API là gì", model trả lời luôn mà không gọi hàm nào. Đó là hành vi đúng: chỉ gọi công cụ khi cần.

Model gọi calculate_gpa với scores 8.5, 7, 9 và credits 3, 4, 2, kết quả 7.94; câu hỏi về API được trả lời trực tiếp, TOOL CALLS rỗng
Bước 8: tính GPA bằng hàm thật, câu hỏi kiến thức chung thì không gọi hàm.

Bước 9: Vòng lặp agent: gọi nhiều hàm rồi mới trả lời

Một "AI agent" thực chất là đoạn code trên đặt trong vòng lặp: chừng nào model còn gọi hàm thì mình còn chạy hàm và gửi kết quả lại, đến khi model trả lời bằng chữ thì dừng. Mình giới hạn tối đa 3 lượt để tránh lặp vô hạn.

for turn in range(1, 4):
    reply = generate(messages)
    calls = parse_tool_calls(reply)
    if not calls:
        print("Trả lời:", reply)
        break
    messages.append({"role": "assistant", "content": "",
                     "tool_calls": [{"type": "function", "function": c} for c in calls]})
    for c in calls:
        fn = TOOLS.get(c["name"])
        result = fn(**c["arguments"]) if fn else {"error": f"unknown tool {c['name']}"}
        messages.append({"role": "tool", "name": c["name"],
                         "content": json.dumps(result, ensure_ascii=False)})
Vòng lặp agent: model gọi get_schedule cho monday và tuesday trong cùng một lượt rồi trả lời bằng tiếng Việt lịch học hai ngày
Bước 9: hỏi hai ngày, model gọi hai hàm trong cùng một lượt rồi tổng hợp câu trả lời.

Về cơ bản, các coding agent như Claude Code hay Copilot agent mode cũng chạy theo vòng lặp này, chỉ khác là công cụ của chúng là "đọc file", "chạy lệnh terminal", "tìm kiếm web". MCP (xem bài MCP server là gì) là chuẩn chung để chia sẻ các công cụ đó giữa nhiều ứng dụng.

Lỗi thường gặp

Hỏi bằng tiếng Việt, model truyền tham số sai

Với câu "Thứ Ba tuần này mình học môn gì, phòng nào?", BotPsy có gọi đúng hàm nhưng truyền day = "thứba". Hàm trả về danh sách rỗng, và ở lượt sau model lại đoán sang monday. Đây là lỗi thật mình gặp khi chạy, và nó cho thấy model nhỏ dễ bị lệch khi tham số là chữ tự do.

Model truyền day bằng thứba cho get_schedule, hàm trả về danh sách rỗng, lượt sau model lại gọi với monday
Lỗi 1: tham số "thứba" không có trong dữ liệu, model đoán tiếp sang monday.

Cách sửa: giới hạn giá trị bằng enum. Trong docstring của transformers, các bạn ghi (choices: [...]) ở cuối mô tả tham số, schema sẽ có trường enum. Mình thêm cả dòng ánh xạ "Thu Ba = tuesday" vào mô tả hàm và cho hàm trả về lỗi rõ ràng khi giá trị không hợp lệ. Lần chạy tiếp theo model đã gọi đúng tuesday, nhưng câu trả lời cuối vẫn lẫn lộn: nó nói không trả lời được và nhắc tới "tuần thứ nhất", "tuần thứ Ba". Vì vậy mình thêm system prompt yêu cầu trả lời ngắn, bằng tiếng Việt, chỉ dựa trên dữ liệu công cụ trả về. Sau hai thay đổi này, kết quả đã đúng.

DAYS = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]

def get_schedule(day: str) -> list:
    """
    Get the class schedule of the student for one day of the week.
    Vietnamese: Thu Hai = monday, Thu Ba = tuesday, ... Chu Nhat = sunday.

    Args:
        day: Day of the week. (choices: ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"])
    """
    if day not in DAYS:
        return {"error": f"invalid day '{day}', use one of {DAYS}"}
    return LICH_HOC.get(day, [])
Schema có enum bảy ngày trong tuần, và với system prompt model gọi get_schedule tuesday rồi trả lời bằng tiếng Việt môn Database tại Lab 2
Sửa lỗi 1: enum cho tham số và system prompt rõ ràng.

Model thường không gọi hàm, chỉ xin lỗi

Cùng câu hỏi tiếng Việt đó, model gốc Qwen3.5-0.8B (chưa tinh chỉnh cho function calling) không gọi hàm nào mà trả lời "Tôi không thể cung cấp thông tin về lịch học cụ thể của bạn", dù hàm get_schedule có ngay trong prompt. Với câu hỏi tiếng Anh đơn giản thì model gốc vẫn gọi hàm đúng. Nếu cần gọi hàm ổn định, các bạn nên chọn model có thẻ function-calling / tool-calling trên Hugging Face và thử với chính câu hỏi của người dùng thật.

Model gốc Qwen3.5-0.8B trả lời không thể truy cập lịch học, TOOL CALLS rỗng
Lỗi 2: model gốc Qwen3.5-0.8B bỏ qua công cụ khi hỏi bằng tiếng Việt.

Cảnh báo "falling back to its reference PyTorch implementation"

Lần chạy đầu transformers in ra 4 cảnh báo vì thiếu causal_conv1d và flash-linear-attention. Chính cảnh báo cũng nói rõ: cách chạy này đúng nhưng chậm hơn ("correct but much slower"). Mình không cài hai gói này, và mọi kết quả trong bài đều chạy ở chế độ đó trên CPU, nên các bạn có thể bỏ qua cảnh báo khi học.

Bốn dòng cảnh báo transformers về causal_conv1d và flash-linear-attention đang dùng bản PyTorch tham chiếu
Lỗi 3: cảnh báo thiếu kernel tối ưu, chương trình vẫn chạy bình thường.

Câu hỏi thường gặp

Function calling khác MCP thế nào?

Function calling là khả năng của model: đọc mô tả công cụ và sinh ra lời gọi có cấu trúc. MCP là giao thức để một ứng dụng (Claude Desktop, VS Code…) lấy danh sách công cụ từ một server bên ngoài. Ứng dụng lấy công cụ qua MCP rồi vẫn đưa cho model bằng function calling.

Model có tự chạy code của mình không?

Không. Model chỉ sinh ra tên hàm và tham số, chạy hay không là do code của các bạn. Với hàm có tác dụng thật (xóa file, gửi email), nên kiểm tra tham số hoặc hỏi lại người dùng trước khi chạy.

Dùng ChatGPT, Gemini, Claude API thì có khác không?

Ý tưởng giống hệt. Khác ở chỗ API đã tách sẵn lời gọi hàm thành JSON (không cần tự viết parse_tool_calls), nhưng cần tài khoản và API key.

Có dùng BotPsy-0.8B cho đồ án được không?

Được, với các tác vụ đơn giản và chạy offline (giấy phép Apache 2.0). Model card ghi rõ model hợp với tác vụ độ khó thấp đến trung bình, chủ yếu được huấn luyện bằng tiếng Anh, và không nên dùng cho việc liên quan đến tiền, dữ liệu cá nhân hay sức khỏe. Muốn chạy nhẹ hơn, nhóm phát triển có bản GGUF cho llama.cpp.

Máy không có GPU chạy có nổi không?

Trong thử nghiệm của mình (4 nhân CPU), mỗi lượt sinh mất khoảng 5–8 giây, cả vòng gọi hàm và trả lời khoảng 11–13 giây. File model bản BF16 nặng 1,71 GB nên laptop RAM 8 GB nhiều khả năng đủ, nhưng mình chưa thử trực tiếp trên laptop.

Nguồn tham khảo

Ảnh chụp và thử nghiệm ngày 14/11/2026.