Lần đầu tôi nghe từ “AI agent” trong một cuộc họp product, người trình bày bảo “thay vì chatbot, mình làm agent”. Cả phòng gật đầu. Tôi không hiểu khác nhau ở chỗ nào.

Sau đó tôi xem code mẫu của họ. Cũng là gọi LLM trong vòng for loop, thêm vài function được decorate, thêm một biến state. Khác chatbot ở đâu? Vài tháng sau, đến lượt tôi build một agent thật. Sau khi nó loop vô hạn lần đầu, đốt $40 trong 20 phút, gọi hàm delete_user với arg sai vì hiểu nhầm prompt, tôi bắt đầu thấy khác biệt ở đâu.

Bài này định nghĩa agent từ góc nhìn dev: agent là gì, vì sao nó khác chatbot, và 4 thành phần cốt lõi cần hiểu trước khi viết dòng code đầu tiên.

Phần 1: Định nghĩa agent

Định nghĩa ngắn nhất mà tôi thấy đúng:

Agent là một hệ thống dùng LLM để quyết định bước tiếp theo trong một vòng lặp, được trang bị tools để tác động lên thế giới.

Bốn từ khóa: LLM, quyết định, vòng lặp, tools. Thiếu một trong bốn là không còn agent nữa.

Phân biệt với các thứ hay bị nhầm:

  • Chatbot: LLM trong một vòng request-response. Không có loop chủ động. Người dùng gửi câu hỏi, bot trả lời. Bot không tự đi tìm gì.
  • RPA (Robotic Process Automation): Có loop, có actions trên thế giới, nhưng workflow được hardcode. Không có LLM quyết định.
  • Workflow LLM tuyến tính: Một chuỗi prompt → LLM → parse → prompt → LLM. Có nhiều bước, không có loop. Đường đi cố định.
  • Agent: LLM được hỏi sau mỗi bước “bước tiếp theo là gì”. Đường đi không cố định trước. Có tools để chạm vào file, DB, API, browser.

Sự khác biệt then chốt là ai quyết định bước tiếp theo. Workflow: developer quyết định lúc code. Agent: LLM quyết định lúc runtime.

Phần 2: Bốn thành phần cốt lõi

Một agent tối giản cần đúng bốn thứ. Thiếu một là gãy, thừa thì chưa cần lo lúc bắt đầu.

LLM

Bộ não. Nhận input (system prompt cộng history cộng tool results), output ra hai loại response:

  1. Text trả về cho user (“Tôi đã tạo file thành công”)
  2. Tool call ({"tool": "create_file", "args": {...}})

Model nào cũng được, miễn hỗ trợ tool use. Claude Sonnet 4.6 hoặc GPT-4o là default tốt. Open-source: Llama 3.3 70B, Qwen 2.5 có function calling. Series này dùng Claude Sonnet 4.6 làm reference.

Quan trọng: LLM không có state. Mỗi lần gọi, bạn phải gửi lại toàn bộ context (history, tool results). Đây là khác biệt lớn nhất với traditional code, nơi function có closure và biến.

Đọc thêm về cách LLM hoạt động: LLM hoạt động thế nào: mental model cho dev.

Tools

Cánh tay. Mỗi tool là một function được expose cho LLM kèm schema JSON:

tools = [
    {
        "name": "read_file",
        "description": "Read content of a file by path",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Absolute path"}
            },
            "required": ["path"]
        }
    },
    {
        "name": "list_dir",
        "description": "List entries in a directory",
        "input_schema": {
            "type": "object",
            "properties": {"path": {"type": "string"}},
            "required": ["path"]
        }
    }
]

LLM thấy schema, quyết định gọi tool nào với args nào. Code Python (hoặc bất kỳ runtime nào) thực thi tool, gửi kết quả lại cho LLM.

Tools là điểm agent chạm vào thế giới. File, DB, API, shell, browser, mọi thứ đều thành tool. Thiết kế tool sai là một trong những lý do agent fail nhiều nhất, sẽ đi sâu ở bài 11.

Memory

Sổ ghi chép. Trong bài này, memory tối giản nhất là conversation history: list các message trao đổi giữa user, LLM, và tool results.

messages = [
    {"role": "user", "content": "Đếm số file Python trong /repo"},
    {"role": "assistant", "content": [
        {"type": "tool_use", "id": "t1", "name": "list_dir", "input": {"path": "/repo"}}
    ]},
    {"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": "t1", "content": "main.py\nutils.py\nREADME.md"}
    ]},
    {"role": "assistant", "content": "Có 2 file Python: main.py và utils.py"}
]

Mỗi vòng lặp, history dài thêm. Sớm muộn sẽ chạm context window limit (200K tokens với Claude Sonnet 4.6, lớn nhưng không vô hạn).

Memory thực tế trong production phức tạp hơn: short-term (history), long-term (vector DB), scratchpad (notes LLM tự viết), episodic (replay). Đi sâu ở bài 4.

Control loop

Linh hồn. Vòng lặp quyết định khi nào lặp tiếp, khi nào dừng. Pseudo-code đơn giản nhất:

def agent_loop(user_input, max_iterations=10):
    messages = [{"role": "user", "content": user_input}]
    for i in range(max_iterations):
        response = llm.call(messages, tools=tools)
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            return response.text
        if response.stop_reason == "tool_use":
            tool_results = execute_tools(response.tool_calls)
            messages.append({"role": "user", "content": tool_results})
            continue

    raise RuntimeError("Max iterations exceeded")

Bốn quyết định trong loop này, mỗi cái đều có thể sai theo cách riêng:

  1. Khi nào dừng: stop_reason == "end_turn" là tín hiệu LLM tự bảo xong. Có lúc LLM nói xong nhưng task thật chưa hoàn thành.
  2. Khi nào fail: max_iterations là safety net. Không có giới hạn này, agent có thể loop mãi nếu prompt sai.
  3. Tool execution có lỗi: Tool throw exception thì làm gì? Truyền error trở lại cho LLM (để nó tự retry) hay raise lên trên?
  4. Token budget: History dài lên mỗi vòng. Phải có cơ chế truncate hoặc summarize.

Loop nhìn đơn giản nhưng là phần dễ sai nhất. Đi sâu ở bài 3.

Phần 3: Vì sao 4 thành phần này lại đủ

Câu hỏi tự nhiên: tại sao không cần planner riêng, scheduler riêng, state machine riêng?

Câu trả lời: ở mức tối giản, LLM tự làm planner và scheduler thông qua tool selection. Mỗi lần được gọi, LLM nhìn history rồi quyết định “step tiếp theo gọi tool gì”. Đó là planning. Nó cũng quyết định “có gọi tool nào nữa không”, đó là scheduling.

Khi agent phức tạp lên, có thể tách planner ra riêng (Plan-and-Execute pattern, bài 7). Nhưng đó là tối ưu, không phải bắt buộc. Agent từ zero không cần.

So sánh với một analogy quen thuộc: agent giống như một dev junior đang debug. Dev nhìn code, nghĩ một bước, chạy lệnh, đọc output, nghĩ tiếp, chạy lệnh tiếp. Đến khi nào fix xong hoặc bí, mới dừng. LLM thay vai dev. Tools thay vai shell. Memory thay vai notebook. Loop thay vai sự kiên nhẫn.

Phần 4: Một agent tối giản, 30 dòng

Code dưới đây chạy được với Anthropic SDK. Cần pip install anthropicANTHROPIC_API_KEY trong env.

import os
from anthropic import Anthropic

client = Anthropic()
TOOLS = [{
    "name": "get_time",
    "description": "Get current time in ISO format",
    "input_schema": {"type": "object", "properties": {}}
}]

def execute_tool(name, args):
    if name == "get_time":
        from datetime import datetime
        return datetime.now().isoformat()
    return f"Unknown tool: {name}"

def agent(user_input, max_iter=5):
    messages = [{"role": "user", "content": user_input}]
    for _ in range(max_iter):
        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason == "end_turn":
            return resp.content[0].text
        if resp.stop_reason == "tool_use":
            tool_results = []
            for block in resp.content:
                if block.type == "tool_use":
                    result = execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result),
                    })
            messages.append({"role": "user", "content": tool_results})
    return "Max iterations exceeded"

print(agent("Bây giờ là mấy giờ?"))

Chạy ra: Bây giờ là 14:32 ngày 18/05/2026 (giờ Việt Nam). (hoặc tương đương). Agent đã gọi get_time, nhận kết quả, format câu trả lời, trả về.

Bài 5 sẽ mở rộng agent này lên file system, browser, và tool nhiều hơn.

Pitfall đầu tiên thường gặp: max iterations sai

Lần đầu tôi build agent, tôi để max_iterations=100. Nghĩ là an toàn. Sai.

Agent gặp một edge case (file nó cố tạo bị permission denied), LLM cố retry, mỗi lần retry tốn ~3000 token, lặp đúng 100 lần trước khi tôi nhận ra. Mất $12 trong 8 phút. Trên Claude Opus 4.7 thì còn đau hơn nhiều.

Bài học: max_iterations không phải safety net. Đó là budget. Mỗi iteration tốn token. 100 iteration × 3000 token × $3/MTok input + $15/MTok output ≈ $5 cho một agent đơn lẻ trong worst case Sonnet. Trên Opus, nhân 5 lần.

Cách đúng:

  • Mặc định max_iterations=10 cho agent đơn giản, =20-30 cho task phức tạp
  • Thêm token budget tracking: cộng dồn token usage mỗi vòng, abort khi vượt ngưỡng
  • Thêm early termination heuristic: nếu LLM gọi cùng tool với cùng args 3 lần liên tiếp, có khả năng loop, abort

Sẽ đi sâu phần này ở bài 22.

Cheatsheet

Thành phầnTrách nhiệmPitfall thường gặp
LLMQuyết định step tiếp theoKhông nhớ, phải gửi lại history mỗi lần
ToolsTác động lên thế giớiSchema không rõ ràng, LLM gọi sai arg
MemoryLưu context giữa các stepKhông truncate, vượt context window
Control loopQuyết định lặp / dừngmax_iterations quá lớn, đốt token
Khái niệmKhácVì sao agent thắng
ChatbotKhông có loop chủ độngAgent tự đi tìm thông tin
RPAWorkflow hardcodeAgent thích nghi với input lạ
Workflow LLM tuyến tínhĐường đi cố địnhAgent quyết định runtime
AgentCó cả 4 thành phần(chính là nó)

Lời kết

Agent không phải magic. Đó là LLM cộng tools cộng memory cộng loop, được ghép lại theo một pattern cụ thể. Khi bạn hiểu 4 thành phần này, framework như LangGraph, CrewAI, hay AutoGen sẽ chỉ là cách tổ chức lại các thành phần đó, không phải concept mới.

Bài tiếp theo, Tool use cơ bản: function calling, JSON schema, error handling, sẽ đi sâu vào phần “tools”: viết schema sao cho LLM gọi đúng, handle error đúng cách, và pattern idempotency để retry không gây hại.

Trước khi đọc tiếp, gợi ý: chạy thử agent 30 dòng ở trên. Cảm giác lần đầu thấy LLM tự quyết định gọi tool nào là khoảnh khắc concept trở thành intuition. Khó truyền tải bằng bài viết.