Dùng AI

Tự xây một agent nhỏ bằng Python và API

Một AI agent tối giản chỉ cần ba phần: danh sách công cụ mô tả bằng JSON Schema, vòng lặp gửi câu hỏi và chạy công cụ mô hình yêu cầu, và điều kiện dừng. Bài này viết một agent tra điểm học sinh bằng Python với SDK chính thức của Anthropic, khoảng 100 dòng, kèm cách kiểm mà không tốn phí API.

Ý chính
  • Công cụ được khai báo bằng tên, mô tả và JSON Schema. Khi mô hình muốn dùng, phản hồi có stop_reason là tool_use; chương trình chạy công cụ rồi gửi lại tool_result mang đúng tool_use_id.
  • Vòng lặp chạy tới khi mô hình trả lời xong. Luôn có giới hạn số bước, trả lỗi công cụ về cho mô hình thay vì để chương trình sập, và xử lý trường hợp mô hình từ chối.
  • Khóa API để trong biến môi trường. Kiểm vòng lặp bằng một client giả trả phản hồi viết sẵn, không cần mạng và không tốn tiền.

Bài này dựa trên tài liệu tool use của Claude và bảng giá API của Anthropic, đọc ngày 11/11/2026, cùng SDK Python anthropic phiên bản 2.8.0. Đoạn code đã được kiểm bằng client giả, và đã kiểm với SDK thật rằng yêu cầu được dựng hợp lệ (dừng ở bước kết nối, không gọi API). Nếu chưa gọi API lần nào, đọc trước gọi API của mô hình AI bằng Python.

Agent khác một lần gọi API ở đâu?

Một lần gọi API: gửi câu hỏi, nhận câu trả lời, xong. Một agent thì mô hình có công cụ trong tay và tự quyết định dùng công cụ nào, bao nhiêu lần, cho tới khi trả lời được. Bài AI agent là gì giải thích khái niệm; bài này đi thẳng vào code.

Tài liệu tool use của Claude mô tả vòng trao đổi với một công cụ do bạn định nghĩa (gọi là công cụ phía client): bạn gửi kèm danh sách công cụ; khi cần, mô hình trả về stop_reason: "tool_use" cùng một hoặc nhiều khối tool_use nêu tên công cụ và tham số; code của bạn chạy công cụ rồi gửi kết quả trở lại trong khối tool_result; mô hình dùng kết quả để trả lời. Agent chỉ là vòng trao đổi đó lặp lại nhiều lần.

Ví dụ trong bài: một trợ lý tra điểm. Bảng điểm giả có ba học sinh. Câu hỏi "Điểm trung bình các bài của An và của Bình, gộp chung lại, là bao nhiêu?" đòi mô hình tra điểm hai bạn rồi tính. Đây là việc nhỏ nhưng có đủ các phần của một agent thật: nhiều bước, nhiều công cụ, và có lúc gọi hai công cụ cùng lúc.

Agent nhỏ bằng Python: vòng lặp công cụ Ví dụ: tra điểm An và Bình rồi tính trung bình Gửi câu hỏimessageskèm tools Mô hìnhtool_usetra_diem An, Bình Chương trìnhtool_result[8, 9, 7], [6, 7, 8] Trả lờiend_turn7,5 Còn tool_use thì gửi kết quả và hỏi tiếp Giới hạn số bước, lỗi công cụ trả về cho mô hình
Agent nhỏ bằng Python: vòng lặp công cụ Gửi câu hỏimessageskèm tools Mô hìnhtool_usetra_diem An, Bình Chương trìnhtool_result[8, 9, 7], [6, 7, 8] Trả lờiend_turn7,5 Còn tool_use thì gửi kết quả và hỏi tiếp Giới hạn số bước, lỗi công cụ trả về cho mô hình
Hình 1. Vòng lặp của agent nhỏ: gửi câu hỏi kèm danh sách công cụ, mô hình trả tool_use (tra điểm An và Bình), chương trình chạy công cụ và gửi tool_result ([8, 9, 7] và [6, 7, 8]), lặp lại cho tới khi mô hình trả lời (end_turn, 7,5).

Chuẩn bị

Cài SDK chính thức và đặt khóa API trong biến môi trường:

pip install anthropic
export ANTHROPIC_API_KEY="khóa của bạn"

Trên Windows PowerShell, dòng thứ hai là $env:ANTHROPIC_API_KEY="khóa của bạn". Client anthropic.Anthropic() tự đọc biến này, nên không bao giờ viết khóa vào code. Nếu dự án có tệp .env, nhớ để nó trong .gitignore; xem giữ khóa API an toàn.

Toàn bộ agent

"""Một agent nhỏ viết bằng Python và API của Claude: mô hình tự gọi công cụ, chương trình chạy công cụ, lặp tới khi xong.

Cài: pip install anthropic
Khóa API đặt trong biến môi trường ANTHROPIC_API_KEY (không viết khóa vào code).
Chạy: python agent_nho.py
"""
import ast
import json
import operator

MO_HINH = "claude-opus-5-5"
HE_THONG = "Bạn là trợ lý tra cứu điểm. Luôn dùng công cụ tra_diem để lấy điểm và công cụ tinh để tính, không tự tính nhẩm."

# Dữ liệu giả để chạy thử. Trong ứng dụng thật, đây là cơ sở dữ liệu hay API của bạn.
BANG_DIEM = {"An": [8, 9, 7], "Bình": [6, 7, 8], "Chi": [9, 9, 10]}

CONG_CU = [
    {
        "name": "tra_diem",
        "description": "Trả danh sách điểm các bài kiểm tra của một học sinh theo tên. Dùng khi cần điểm của học sinh.",
        "input_schema": {
            "type": "object",
            "properties": {"ten": {"type": "string", "description": "Tên học sinh, ví dụ: An"}},
            "required": ["ten"],
            "additionalProperties": False,
        },
        "strict": True,
    },
    {
        "name": "tinh",
        "description": "Tính một biểu thức số học chỉ gồm số, + - * / và dấu ngoặc. Dùng thay cho tính nhẩm.",
        "input_schema": {
            "type": "object",
            "properties": {"bieu_thuc": {"type": "string", "description": "Ví dụ: (8 + 9 + 7) / 3"}},
            "required": ["bieu_thuc"],
            "additionalProperties": False,
        },
        "strict": True,
    },
]

PHEP = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv}


def tinh(bieu_thuc):
    """Tính biểu thức bằng cây cú pháp, không dùng eval, nên không chạy được mã lạ."""
    def gia_tri(nut):
        if isinstance(nut, ast.Constant) and isinstance(nut.value, (int, float)):
            return nut.value
        if isinstance(nut, ast.BinOp) and type(nut.op) in PHEP:
            return PHEP[type(nut.op)](gia_tri(nut.left), gia_tri(nut.right))
        if isinstance(nut, ast.UnaryOp) and isinstance(nut.op, ast.USub):
            return -gia_tri(nut.operand)
        raise ValueError("chỉ được dùng số, + - * / và dấu ngoặc")
    return round(gia_tri(ast.parse(bieu_thuc, mode="eval").body), 4)


def tra_diem(ten):
    if ten not in BANG_DIEM:
        raise ValueError(f"không có học sinh {ten}; các tên có: {', '.join(BANG_DIEM)}")
    return BANG_DIEM[ten]


THUC_THI = {"tra_diem": tra_diem, "tinh": tinh}


def chay_cong_cu(ten, dau_vao):
    """Chạy một công cụ; lỗi được trả về cho mô hình đọc (is_error) thay vì làm sập chương trình."""
    try:
        return json.dumps(THUC_THI[ten](**dau_vao), ensure_ascii=False), False
    except Exception as loi:  # noqa: BLE001
        return f"Lỗi: {loi}", True


def chay_agent(cau_hoi, client=None, toi_da=8, in_buoc=print):
    if client is None:
        import anthropic
        client = anthropic.Anthropic()  # tự đọc ANTHROPIC_API_KEY từ biến môi trường
    messages = [{"role": "user", "content": cau_hoi}]
    for buoc in range(1, toi_da + 1):
        phan_hoi = client.beta.messages.create(
            model=MO_HINH,
            max_tokens=16000,
            system=HE_THONG,
            tools=CONG_CU,
            messages=messages,
            betas=["server-side-fallback-2026-07-01"],
            fallbacks="default",  # nếu mô hình từ chối vì bộ lọc an toàn, máy chủ thử lại bằng mô hình dự phòng
        )
        if phan_hoi.stop_reason == "refusal":
            return "Mô hình từ chối yêu cầu này."
        if phan_hoi.stop_reason != "tool_use":
            van_ban = "".join(b.text for b in phan_hoi.content if b.type == "text")
            return van_ban + (" [bị cắt vì chạm max_tokens]" if phan_hoi.stop_reason == "max_tokens" else "")
        messages.append({"role": "assistant", "content": phan_hoi.content})
        ket_qua = []
        for khoi in phan_hoi.content:
            if khoi.type == "tool_use":
                noi_dung, la_loi = chay_cong_cu(khoi.name, khoi.input)
                in_buoc(f"Bước {buoc}: {khoi.name}({json.dumps(khoi.input, ensure_ascii=False)}) -> {noi_dung}")
                ket_qua.append({"type": "tool_result", "tool_use_id": khoi.id, "content": noi_dung, "is_error": la_loi})
        messages.append({"role": "user", "content": ket_qua})  # mọi kết quả trong MỘT tin nhắn
    return f"Dừng: quá {toi_da} bước mà chưa xong."


if __name__ == "__main__":
    print(chay_agent("Điểm trung bình các bài của An và của Bình, gộp chung lại, là bao nhiêu?"))

Khi chạy với khóa thật, mỗi lần mô hình gọi công cụ, chương trình in một dòng. Với kịch bản điển hình (mô hình tra điểm hai bạn cùng lúc, rồi tính), các dòng in ra có dạng:

Bước 1: tra_diem({"ten": "An"}) -> [8, 9, 7]
Bước 1: tra_diem({"ten": "Bình"}) -> [6, 7, 8]
Bước 2: tinh({"bieu_thuc": "(8 + 9 + 7 + 6 + 7 + 8) / 6"}) -> 7.5

Mô hình có thể chọn thứ tự hay cách tính khác (ví dụ tính trung bình từng bạn trước), vì nó tự quyết định. Đây là điểm khác cơ bản giữa agent và một script: bạn không viết các bước, bạn viết công cụ và điều kiện.

Giải thích từng phần

1. Khai báo công cụ

Mỗi công cụ trong CONG_CU có ba phần: name, description và input_schema theo JSON Schema. Mô hình chỉ thấy ba phần này, không thấy code của hàm. Vì vậy mô tả phải nói rõ công cụ làm gì và khi nào dùng; tham số cũng có mô tả kèm ví dụ giá trị. Anthropic, trong bài "Building effective agents", khuyên viết mô tả công cụ như docstring cho một lập trình viên mới: tên tham số rõ, có ví dụ, thử nhiều đầu vào để xem mô hình dùng sai ở đâu.

Trường strict: true (cùng additionalProperties: false trong lược đồ) theo tài liệu bảo đảm lời gọi công cụ khớp đúng lược đồ. Không có nó, mô hình đôi khi thêm tham số thừa hay đặt sai kiểu.

2. Chỉ dẫn hệ thống hướng mô hình dùng công cụ

Mặc định, tài liệu cho biết mô hình tự quyết định ở mỗi lượt có gọi công cụ hay trả lời thẳng. Muốn nó dùng công cụ nhiều hơn, tài liệu gợi ý viết chỉ dẫn trong system prompt. Agent ví dụ ghi rõ: luôn dùng tra_diem để lấy điểm và tinh để tính, không tự tính nhẩm. Với bài toán có số liệu, đây là cách giảm lỗi tính sai.

3. Vòng lặp

Hàm chay_agent là trái tim của agent:

  1. Gửi toàn bộ messages cùng danh sách công cụ.
  2. Nếu stop_reason là refusal, dừng và báo mô hình từ chối.
  3. Nếu stop_reason không phải tool_use (thường là end_turn), lấy văn bản trả lời và kết thúc; nếu là max_tokens thì đánh dấu câu trả lời bị cắt.
  4. Nếu là tool_use: thêm nguyên phản hồi của mô hình vào messages, chạy từng công cụ, rồi thêm một tin nhắn vai user chứa mọi tool_result, mỗi kết quả mang đúng tool_use_id của lời gọi tương ứng.
  5. Lặp lại, tối đa toi_da lần.

Hai chi tiết dễ sai. Thứ nhất, phải gửi lại nguyên phan_hoi.content (gồm cả khối tool_use), không chỉ phần chữ. Thứ hai, khi mô hình gọi nhiều công cụ trong một lượt, gửi mọi kết quả trong cùng một tin nhắn; tách ra nhiều tin nhắn làm mô hình dần ít gọi song song.

4. Lỗi công cụ là thông tin, không phải sự cố

Hàm chay_cong_cu bắt mọi lỗi và trả về chuỗi "Lỗi: ..." kèm is_error: True. Ví dụ hỏi điểm của "Dũng", công cụ trả "không có học sinh Dũng; các tên có: An, Bình, Chi". Mô hình đọc được và trả lời đúng là không có bạn đó, hoặc hỏi lại tên. Nếu để lỗi làm sập chương trình, agent dừng giữa chừng mà không ai biết vì sao.

5. Công cụ tính toán an toàn

Công cụ tinh không dùng eval, vì eval chạy được mọi mã Python, kể cả lệnh xóa tệp, nếu mô hình bị lôi kéo đưa vào một "biểu thức" độc hại. Thay vào đó, nó phân tích biểu thức thành cây cú pháp bằng mô-đun ast và chỉ chấp nhận số cùng bốn phép tính. Đây là nguyên tắc chung: công cụ của agent chỉ nên làm đúng việc của nó, vì đầu vào đến từ mô hình chứ không từ bạn.

6. Giới hạn và dự phòng

toi_da chặn vòng lặp vô hạn khi mô hình cứ gọi công cụ mãi. Tham số fallbacks="default" (beta, đi cùng betas=["server-side-fallback-2026-07-01"]) yêu cầu máy chủ chạy lại yêu cầu bằng một mô hình dự phòng nếu mô hình chính từ chối vì bộ lọc an toàn; vì đây là tính năng beta nên lời gọi dùng client.beta.messages.create. Nếu chuỗi dự phòng vẫn từ chối, stop_reason là refusal và agent báo lại.

Kiểm agent mà không tốn phí

Gọi API thật để thử mỗi lần sửa code vừa tốn tiền vừa không ổn định, vì mô hình có thể chọn cách làm khác. Cách làm tốt hơn là truyền vào một client giả: một đối tượng có hàm beta.messages.create trả về các phản hồi viết sẵn theo đúng hình dạng của SDK (có stop_reason, có các khối text và tool_use). Hàm chay_agent nhận tham số client chính là để làm việc này.

Với client giả, bạn kiểm được những điều quan trọng một cách tất định:

  • vòng lặp gửi lại đúng lịch sử: câu hỏi, phản hồi có tool_use, rồi một tin nhắn chứa cả hai tool_result với đúng id;
  • lỗi công cụ được trả về với is_error;
  • từ chối, chạm max_tokens và chạm giới hạn số bước đều được xử lý;
  • công cụ tinh từ chối các chuỗi như __import__('os')... hay lũy thừa.

Sau khi phần logic đã chắc, chạy với API thật vài lần để xem mô hình dùng công cụ ra sao.

Chi phí của một agent

Theo tài liệu tool use, yêu cầu có công cụ tính tiền theo token như mọi yêu cầu khác, nhưng có thêm: tên, mô tả và lược đồ của công cụ trong tham số tools, các khối tool_use và tool_result, cộng một chỉ dẫn hệ thống đặc biệt mà API tự thêm khi có công cụ (với Claude Opus 5.5 là 286 token). Vì mỗi bước gửi lại cả lịch sử, chi phí tăng theo số bước; bài chi phí khi dùng agent có phép ước tính chi tiết.

Code dùng claude-opus-5-5. Theo bảng giá đọc ngày 11/11/2026, mô hình này giá 4 đô la cho một triệu token đầu vào và 20 đô la cho một triệu token đầu ra; claude-haiku-5-5 giá 0,10 và 0,50 đô la (cho lời nhắc tới 100.000 token). Với bài tập học như tra điểm, mô hình nhỏ thường đủ; đổi bằng cách sửa hằng MO_HINH, rồi thử lại xem chất lượng có giữ được không.

Đi xa hơn

Khi đã hiểu vòng lặp, có ba hướng mở rộng:

  • Dùng tool runner của SDK. Tài liệu cho biết SDK có tool runner tự chạy công cụ và gửi kết quả về, để bạn không phải tự viết vòng lặp. Viết tay một lần như bài này giúp bạn hiểu cái mà công cụ đó làm hộ.
  • Nối công cụ qua MCP thay vì định nghĩa trong code, để dùng lại các máy chủ có sẵn; xem kết nối AI qua MCP server.
  • Thêm công cụ có tác dụng ghi (gửi email, sửa bảng tính) kèm bước xin xác nhận của người dùng trước khi chạy. Xem tự động hóa việc văn phòng với agent.
Cẩn thận
  • Viết khóa API thẳng vào code rồi đưa lên GitHub.
  • Không giới hạn số bước: agent bị kẹt sẽ gọi API mãi và tốn tiền.
  • Chỉ gửi lại phần chữ của phản hồi, làm mất khối tool_use, khiến API báo lỗi vì tool_result không có lời gọi tương ứng.
  • Dùng eval hay os.system trong công cụ với đầu vào do mô hình đưa ra.
  • Để lỗi công cụ làm sập chương trình thay vì trả về cho mô hình.
  • Mô tả công cụ một dòng mơ hồ, khiến mô hình gọi sai công cụ hay truyền sai tham số.

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

Có cần thư viện như LangChain để làm agent không?

Không bắt buộc. Một agent đơn giản chỉ cần SDK của nhà cung cấp và vòng lặp khoảng vài chục dòng như bài này. Thư viện khung giúp khi hệ thống lớn dần, nhưng hiểu vòng lặp gốc giúp bạn gỡ lỗi khi dùng bất kỳ khung nào.

Mô hình có tự chạy code của công cụ không?

Không, với công cụ do bạn định nghĩa. Mô hình chỉ trả về tên công cụ và tham số; chương trình của bạn chạy hàm và gửi kết quả về. Công cụ phía máy chủ như tìm kiếm web thì do hãng chạy.

Vì sao mỗi lần chạy, agent làm các bước khác nhau?

Vì mô hình tự quyết định cách làm ở mỗi lượt. Kết quả cuối nên giống nhau, nhưng thứ tự gọi công cụ có thể khác. Đó là lý do kiểm logic của vòng lặp bằng client giả, còn kiểm chất lượng bằng nhiều lần chạy thật.

Làm sao để agent dùng công cụ thay vì tự đoán?

Viết rõ trong chỉ dẫn hệ thống, ví dụ "luôn dùng công cụ tinh để tính", và viết mô tả công cụ cụ thể về khi nào nên dùng.

Học tiếp

  • Muốn nắm lại khái niệm agent và vòng lặp, đọc AI agent là gì.
  • Muốn bắt đầu từ một lời gọi API đơn giản nhất, đọc gọi API của mô hình AI bằng Python.
  • Muốn giữ khóa API ngoài code và ngoài git, đọc giữ khóa API an toàn.

Nếu bạn muốn học Python và AI bài bản cùng giảng viên, tham khảo khóa học AI của Big-O Coding.

Muốn học bài bản cùng giảng viên?

Khóa AI – Introduction to Machine Learning: nền tảng Máy học từ thuật toán cơ bản đến Neural Networks và LLM, học bằng Python. Học thử buổi 1 miễn phí.

Xem Khóa AI

Nguồn tham khảo

Chia sẻ: Facebook LinkedIn
Nhắn Zalo tư vấnZalo Gọi 0937 401 483