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:
- Text trả về cho user (“Tôi đã tạo file thành công”)
- 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:
- 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. - Khi nào fail:
max_iterationslà safety net. Không có giới hạn này, agent có thể loop mãi nếu prompt sai. - 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?
- 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 anthropic và ANTHROPIC_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=10cho agent đơn giản,=20-30cho 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ần | Trách nhiệm | Pitfall thường gặp |
|---|---|---|
| LLM | Quyết định step tiếp theo | Không nhớ, phải gửi lại history mỗi lần |
| Tools | Tác động lên thế giới | Schema không rõ ràng, LLM gọi sai arg |
| Memory | Lưu context giữa các step | Không truncate, vượt context window |
| Control loop | Quyết định lặp / dừng | max_iterations quá lớn, đốt token |
| Khái niệm | Khác | Vì sao agent thắng |
|---|---|---|
| Chatbot | Không có loop chủ động | Agent tự đi tìm thông tin |
| RPA | Workflow hardcode | Agent thích nghi với input lạ |
| Workflow LLM tuyến tính | Đường đi cố định | Agent quyết định runtime |
| Agent | Có 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.