# Agent AI thật sự hoạt động thế nào: tự build một agent bằng Python thuần

[Stephen Gruppetta](https://homus.dev/speakers/stephen-gruppetta) · [Real Python](https://homus.dev/orgs/real-python)

> Tự viết harness bằng Python và OpenAI API: vòng lặp while, list messages lớn dần, tool list_files/read_file, rồi đổi if thành while để có agent loop.

Topics: Agents, Context Engineering, Dev Tools

Canonical: https://homus.dev/talks/how-ai-agents-really-work-build-one-in-plain-python

## 1. Chatbot và agent: cùng một model, khác phần code

Stephen Gruppetta, giảng viên của Real Python, mở đầu bằng một phép so sánh: cùng một prompt, gõ hai lần. Câu hỏi là "What's in this folder?" (trong thư mục này có gì?). Chatbot bên trái trả lời rằng nó không truy cập được file của bạn. Agent bên phải thì tìm ra các file, đọc chúng rồi trả lời. Cùng một model, khác phần code (demo trong video đăng ngày 23/10/2026).

![Stephen Gruppetta nói trước camera, cạnh dòng chữ An agent ≠ an LLM](https://homus.dev/photos/oBbwZjcunDE-0023.jpg)

Dòng chữ trên màn hình tóm ý chính của phần mở đầu: "An agent ≠ an LLM", một agent không phải là một LLM (video đăng ngày 23/10/2026).

Anh nói người xem có lẽ đã biết LLM hoạt động thế nào, và có thể đã dùng agent hoặc đang định dùng. Nhưng hai thứ đó không giống nhau: một agent không phải là một LLM, và một LLM cũng không phải là một agent. Trong buổi này, anh muốn lấp khoảng cách giữa hai thứ đó (video đăng ngày 23/10/2026). Cách làm là hiểu xem từ một LLM ta đi tới một agent như thế nào, bằng việc tự viết agent của riêng mình. Không phải trong Claude Code hay trong Codex, mà tự tay viết.

Thực ra thứ mình viết là harness. Theo định nghĩa Stephen đưa ra (video đăng ngày 23/10/2026), harness là phần hạ tầng nằm quanh một LLM, và nếu làm đúng thì chính nó biến LLM thành một agent.

![Sơ đồ: You nối với ô while True the agent loop, nối với ô Model the LLM, tất cả nằm trong khung HARNESS the code around the model](https://homus.dev/photos/oBbwZjcunDE-0043.jpg)

Bạn (You) nói chuyện với vòng lặp `while True:` ("the agent loop"), vòng lặp nói chuyện với Model ("the LLM"), và khung HARNESS ghi "the code around the model", phần code bao quanh model (video đăng ngày 23/10/2026).

## 2. Nói chuyện trực tiếp với LLM qua OpenAI API

Bước đầu tiên là nói chuyện trực tiếp với một LLM. Stephen dùng OpenAI. Anh tạo một `OpenAI()` client, đây là cách code sẽ nói chuyện với LLM ở OpenAI. Sau đó anh gọi tới chat completions và tạo một chat completion. Anh gọi đó là "công thức thần kỳ" để nói chuyện với LLM (video đăng ngày 23/10/2026).

Trong lời gọi đó, việc đầu tiên là nói mình muốn dùng model nào. Stephen chọn `gpt-6-luna`, và theo anh đây là một model tốt mà không quá tốn tiền (nhận xét của Stephen, video đăng ngày 23/10/2026). Tiếp theo là truyền message, hay đúng hơn là messages ở số nhiều, vì thường sẽ có nhiều hơn một. Mỗi message phải nói vai trò (role) của nó: ở đây là user, "tôi là người dùng".

Stephen có sẵn một thư mục tên là `habits`. Anh không cho người xem thấy bên trong có gì, vì hai lý do. Thứ nhất, điều đó không quan trọng. Thứ hai, mình sẽ hỏi chính agent để nó tự tìm ra. Vì vậy message anh gửi là: "What's in the habits folder?"

Trước khi đi tiếp, anh nhắc rằng vì dùng OpenAI nên bạn cần một OpenAI API key, có thể đặt trong file `.env` (theo video đăng ngày 23/10/2026). Kết quả trả về từ lời gọi được gán vào biến `response`. Trong `response` có `choices`, ở số nhiều vì có thể có nhiều hơn một câu trả lời, nhưng thường chỉ có một, nên anh lấy phần tử đầu tiên (cách OpenAI API trả kết quả theo video đăng ngày 23/10/2026). Đoạn code đầy đủ trên màn hình:

```
from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
model="gpt-6-luna",
messages=[
{
"role": "user",
"content": "What's in the habits folder?",
},
],
)

print(response.choices[0].message.content)
```

Anh chạy file `ai_agent.py`. Câu trả lời nhận về: model không truy cập được file system từ cuộc chat này. Nói cách khác, nó không giúp được vì không thể đọc bên trong thư mục đó (demo trong video đăng ngày 23/10/2026).

![Editor ai_agent.py với lời gọi chat.completions và terminal in câu trả lời I can't access your filesystem from this chat](https://homus.dev/photos/oBbwZjcunDE-0223.jpg)

"Step 1: talk to the model directly". Sau lệnh `uv run ai_agent.py`, terminal in câu trả lời của model: "I can't access your filesystem from this chat. To see what's in `habits`, run `ls -la habits` and share the output if you'd like me to help." (demo trong video đăng ngày 23/10/2026).

## 3. Bọc model trong một vòng lặp while True: đó là harness

Từ đây, Stephen bắt đầu "gọt dần" để mở rộng những gì đoạn code này làm được. Việc đầu tiên là đưa tên model ra một hằng số `MODEL`, để không phải hard code ở chỗ nào khác, và muốn đổi model thì đổi một chỗ là xong. Sau đó anh tạo một vòng lặp `while True`.

Anh nhấn mạnh: thứ đang được build chính là harness, phần hạ tầng đặt quanh LLM. Thay vì hard code câu hỏi (video đăng ngày 23/10/2026), input của người dùng giờ lấy từ `input()`. Anh lưu ý rằng ở bước này mình chưa gửi gì cho model cả. Harness sẽ là phần đứng giữa: giữa anh (người dùng) với model, và với một vài thứ khác nữa. Anh đang nói chuyện với vòng lặp `while` này, và vòng lặp đó chính là harness (theo Stephen, video đăng ngày 23/10/2026).

Anh tạo `messages` (anh vừa gõ vừa nói mong mình đánh vần cho đúng), và `content` giờ không còn là chuỗi cố định mà là `user_input`.

Tiếp theo anh viết một hàm trợ giúp nhỏ tên `trace`. Anh nói rõ hàm này chỉ để mình nhìn thấy chuyện gì đang diễn ra, harness tự nó không cần đến. Hàm có một người gửi (sender) và một người nhận (receiver), vì thông tin sẽ được gửi đi và nhận về khắp hệ thống: sender gửi thông tin tới receiver. Nhờ vậy, anh có thể in ra trace lúc harness gửi thông tin tới model. Kết quả trả về, trước đó gọi là response, giờ anh đặt tên là `reply`, và tới lượt model gửi thông tin về cho harness. Rồi harness chuyển message đó lại cho anh (video đăng ngày 23/10/2026).

```
from openai import OpenAI

client = OpenAI()
MODEL = "gpt-6-luna"

def trace(sender, receiver, text):
print(f"  [{sender} -> {receiver}] {text}")

while True:
user_input = input("you -> harness: ")
messages = [{"role": "user", "content": user_input}]

trace("harness", "model", f"sending a message")
response = client.chat.completions.create(
model=MODEL,
messages=messages,
)
reply = response.choices[0].message
trace("model", "harness", f"text reply ({len(reply.content)}) characters")
print(f"harness -> you: {reply.content}")
```

Anh nhắc lại: lúc này model đang gửi thông tin vào vòng lặp `while`, và vòng lặp `while` là harness (theo Stephen, video đăng ngày 23/10/2026).

## 4. Chạy thử: model không nhớ câu vừa nói

Stephen chạy lại code. Dấu nhắc giờ là `you -> harness:`, và anh gõ "What's in the habits folder?". Trace cho thấy harness gửi message tới model ("sending a message"), model gửi một message trả về cho harness, anh đọc là dài 128 ký tự, rồi harness đưa lại câu trả lời cho anh (demo trong video đăng ngày 23/10/2026).

Model gợi ý có thể xem nội dung thư mục bằng `ls -la habits`, nên anh hỏi tiếp: "can you run that for me?" (bạn chạy giúp tôi được không?). Model trả lời: "What would you like me to run? Paste the command or code here, and I can help you with it." Tức là nó hỏi lại muốn chạy gì, hãy dán lệnh hoặc code vào (demo trong video đăng ngày 23/10/2026).

![Terminal: harness trả lời không truy cập được file system, người dùng hỏi can you run that for me, model hỏi lại What would you like me to run](https://homus.dev/photos/oBbwZjcunDE-0504.jpg)

Dòng 11 được tô vàng: `messages` bị tạo lại từ đầu ở mỗi vòng lặp. Trong terminal, sau câu "can you run that for me?", trace ghi "text reply (90) characters" và model hỏi lại "What would you like me to run?" (demo trong video đăng ngày 23/10/2026).

Phản ứng tự nhiên, theo Stephen, là "ý bạn là sao? Bạn vừa bảo tôi có thể liệt kê nội dung bằng `ls -la habits` mà, chuyện đó phải rõ chứ". Nó chưa phải là agent. Vì sao nó không nhớ hai bên vừa làm gì? Anh để người xem thử tìm ra lý do (demo trong video đăng ngày 23/10/2026).

Câu trả lời: ở bước này code chỉ gửi đúng một message, là `user_input` vừa gõ. Chỉ một message đi tới model. Như vậy là không ổn, và đó là việc phải sửa tiếp theo (theo Stephen, video đăng ngày 23/10/2026).

## 5. Gửi cả cuộc hội thoại: một list messages lớn dần

Stephen tạo một list tên `messages`, lúc đầu để trống, đặt bên ngoài vòng lặp. Thay vì ghi đè ở mỗi vòng, anh `append` vào list. Lý do: `messages` giờ là một list sẽ lớn dần (video đăng ngày 23/10/2026).

Anh nói có thể bạn đã biết điều này về LLM: câu trả lời mà LLM đưa ra cũng là một phần của cuộc hội thoại. Nên lần sau, trước khi gửi bất cứ gì cho LLM, mình muốn `append` cả câu trả lời đó vào. Theo Stephen (video đăng ngày 23/10/2026), đây cũng là lý do khi cuộc hội thoại dài ra thì nó đắt hơn: ngay cả khi bạn chỉ gõ "thank you" ở cuối, thực ra bạn đang gửi toàn bộ cuộc hội thoại, tất cả message bạn đã gửi và tất cả câu trả lời LLM đã đưa ra.

Dòng trace "sending a message" giờ không còn đúng về mặt kỹ thuật, nên anh đổi nó thành số message đang gửi: `sending {len(messages)} message(s)`. Chạy lại, trace ghi đang gửi 1 message. Ở câu hỏi tiếp theo anh gõ nhầm một chữ ("cam you run that for me?"), và anh nói LLM khá giỏi đoán những lỗi như vậy (nhận xét của Stephen, video đăng ngày 23/10/2026).

Lần này trace ghi "sending 3 message(s)". Vì sao là ba? Message đầu tiên của anh, câu trả lời của LLM, và message hỏi tiếp của anh. Model lần này vẫn chưa làm việc đó giúp anh, đó là bước sau. Nhưng ít nhất nó đã nhớ hai bên nói gì trước đó. Stephen nói rõ: tới đây vẫn chưa có agent, mới chỉ là một cuộc chat với LLM (demo trong video đăng ngày 23/10/2026).

Ba phần tử trong `messages` ở lần chạy thứ hai, đúng như terminal ghi: message đầu của Stephen, câu trả lời 120 ký tự của model, và câu hỏi tiếp (có lỗi gõ "cam"); cả ba được gửi cùng lúc, trace ghi "sending 3 message(s)" (demo trong video đăng ngày 23/10/2026).

## 6. Cho harness một tool: hàm list_files

Bước tiếp theo là cho hệ thống, cho harness, khả năng chạy một tool. Tool ở đây, theo Stephen, nghĩa là một hàm (function). Anh hỏi: giá mà model biết cách liệt kê file thì sao, như thế sẽ rất có ích. Anh import `Path` từ module `pathlib`: nếu bạn quen `pathlib` thì có thể lấy đường dẫn tới thư mục, duyệt qua từng mục bên trong, rồi ghép tên chúng lại thành một chuỗi, mỗi tên một dòng (video đăng ngày 23/10/2026).

```
def list_files(folder):
return "\n".join(path.name for path in Path(folder).iterdir())
```

Trước khi dùng được tool này, cần làm vài việc. Harness sẽ là bên dùng tool, nhưng LLM cần biết rằng tool này tồn tại. Stephen dán sẵn phần mô tả vì có khá nhiều boilerplate code: đây là hình dạng mà OpenAI yêu cầu cho các tool (theo video đăng ngày 23/10/2026). Đó là một list, và đây là tool đầu tiên. Kiểu của nó là function, và anh nói tool chính là function: chúng làm một việc gì đó.

Tool cần một cái tên: hàm anh vừa viết tên là `list_files` (anh tự hỏi lại "mình đặt tên là gì nhỉ?" rồi kiểm tra). Tool cũng cần một description. Vì sao? Vì theo Stephen, đó là cách LLM biết tool này làm gì (video đăng ngày 23/10/2026). Description ở đây là "List the files in a folder." Tiếp theo là các thuộc tính (properties) tool cần: nó cần `folder`, vì đó là đối số phải truyền vào hàm, kiểu string. Có gì bắt buộc không? Có, `folder` là bắt buộc. Đây là cách mình nói cho LLM biết mình có những tool nào.

![Code định nghĩa TOOLS với một tool kiểu function tên list_files, description List the files in a folder, tham số folder kiểu string, bắt buộc](https://homus.dev/photos/oBbwZjcunDE-0743.jpg)

Danh sách `TOOLS` với tool đầu tiên: `"type": "function"`, tên `list_files`, description "List the files in a folder.", tham số `folder` kiểu string và nằm trong `required`; nhãn Context phía trên giờ đã sáng, kèm chú thích "short-term memory" (video đăng ngày 23/10/2026).

```
TOOLS = [
{
"type": "function",
"function": {
"name": "list_files",
"description": "List the files in a folder.",
"parameters": {
"type": "object",
"properties": {"folder": {"type": "string"}},
"required": ["folder"],
},
},
},
]
```

## 7. Model xin một tool call

Stephen truyền danh sách tool vào lời gọi API (`tools=TOOLS`), để nói với model rằng các tool nằm ở đó. Việc duy nhất anh làm thêm ở bước này là in ra xem `reply` có tool call nào không (`print(reply.tool_calls)`). Anh nói khi dùng tool, OpenAI yêu cầu nói thêm reasoning effort là bao nhiêu, và anh đặt `reasoning_effort="none"`, theo anh vậy là đủ (theo Stephen, video đăng ngày 23/10/2026). Anh cũng tạm tắt dòng trace của câu trả lời bằng chữ.

Chạy lại với câu hỏi cũ. Trace ghi đang gửi 1 message. Lần này, ở dòng harness nói với anh, harness không có gì để nói: nội dung là `None`, vì thứ model trả về là một tool call. Dòng in bên dưới cho thấy tool call đó cùng "tất cả những thứ thú vị đi kèm". LLM đã tự điền tên thư mục từ câu chữ anh đưa ra (`{"folder":"habits"}`) và muốn gọi hàm `list_files` (demo trong video đăng ngày 23/10/2026).

![Terminal: harness to you None, rồi ChatCompletionMessageFunctionToolCall với arguments folder habits và name list_files](https://homus.dev/photos/oBbwZjcunDE-0843.jpg)

Lời gọi API giờ có `tools=TOOLS` và `reasoning_effort="none"`. Terminal in "harness -> you: None", rồi một `ChatCompletionMessageFunctionToolCall` với `arguments='{"folder":"habits"}'` và `name='list_files'` (demo trong video đăng ngày 23/10/2026).

"Tuyệt", Stephen nói. LLM đã nhận ra tool và báo cho harness: tôi cần một tool call. Vậy làm gì tiếp? (demo trong video đăng ngày 23/10/2026)

## 8. Harness chạy tool: ask_model và run_tool

Anh refactor một chút, vì ở đây có hai chuyện có thể xảy ra. Giữa harness và model, mình hoặc đang hỏi model một điều gì đó, hoặc đang chạy tool. Harness giờ nói chuyện được với model, và chạy được tool nếu model hỏi "bạn chạy tool này giúp tôi được không?" (theo Stephen, video đăng ngày 23/10/2026).

Anh tạo hàm `ask_model`, nhận vào các message, và cắt phần gọi API cũ dán vào đó. Anh thêm một trace ở đây: lần này luồng đi từ model sang harness, vì chính model là bên xin tool. Stephen nhấn mạnh: model chỉ bao giờ nói chuyện với harness, không bao giờ nói thẳng với mình (theo Stephen, video đăng ngày 23/10/2026).

```
def ask_model(messages):
trace("harness", "model", f"sending {len(messages)} message(s)")
response = client.chat.completions.create(
model=MODEL,
tools=TOOLS,
messages=messages,
reasoning_effort="none",
)
reply = response.choices[0].message
messages.append(reply)

for call in reply.tool_calls or []:
trace(
"model",
"harness",
f"please run {call.function.name}{call.function.arguments}",
)
if reply.content:
trace("model", "harness", f"text reply ({len(reply.content)}) characters")

return reply
```

Nhưng mình cũng phải gọi được tool, nên anh thêm một hàm nữa, `run_tool`, vì đó đúng là việc mình muốn làm. Tạm thời anh hard code `list_files` bên trong cho dễ; trong thực tế, phải dùng tên hàm mà model gửi tới (theo Stephen, video đăng ngày 23/10/2026).

Trong vòng lặp chính: nếu `reply` có tool call, anh tạo một vòng lặp `for call in reply.tool_calls`. Kết quả là `run_tool(call)`. Rồi anh `append` vào `messages`, lần này role không còn là user mà là `tool`. Phần này phải là một dictionary (anh xin lỗi vì lúc đầu viết sai). Cần kèm `tool_call_id`, chính là `call.id`, và quan trọng nhất là kết quả: hàm của mình đã trả về gì (cấu trúc message theo OpenAI API, video đăng ngày 23/10/2026).

Anh tóm lại luồng: hỏi model là gửi các message cho model. Model có thể trả về chữ, đó là phần content (`reply.content`), hoặc xin một tool. Giao tiếp luôn là giữa anh với harness, và giữa harness với model. Model luôn đi qua harness. Nếu model nói "tôi cần một tool call", thì harness là bên gọi tool. Vòng lặp `while` của anh, về bản chất, chính là harness (theo Stephen, video đăng ngày 23/10/2026).

![Sơ đồ harness có thêm ô tools list_files, read_file nối với vòng lặp while True](https://homus.dev/photos/oBbwZjcunDE-1103.jpg)

Sơ đồ harness lúc này có thêm ô "tools" (`list_files`, `read_file`) nối với vòng lặp `while True:`; model không nối trực tiếp với tools hay với bạn, mọi thứ đi qua vòng lặp (video đăng ngày 23/10/2026).

## 9. Câu trả lời đầu tiên có dùng tool, và chỗ harness ra quyết định

Chạy thử. Model nói: harness ơi, làm ơn chạy `list_files` với folder là `habits`. Harness gọi tool `list_files` với folder `habits`. Tool trả về 31 ký tự. Harness gửi 3 message cho model, model trả về một câu trả lời bằng chữ, và harness nói với anh: thư mục `habits` chứa `habits.py`, `README.md` và `habits.json`. Stephen xác nhận đó đúng là ba file trong thư mục của anh (demo trong video đăng ngày 23/10/2026).

![Terminal với chuỗi trace: model xin list_files, harness gọi tool, tool trả 31 characters, harness gửi 3 messages, model trả text reply 73 characters](https://homus.dev/photos/oBbwZjcunDE-1153.jpg)

Chuỗi trace của lần chạy đầu tiên có tool: `please run list_files{"folder":"habits"}`, rồi harness gọi `list_files`, tool trả "31 characters", harness gửi 3 message, model trả "text reply (73) characters" và liệt kê ba file (demo trong video đăng ngày 23/10/2026).

Stephen diễn tả lại lượt đó bằng lời của LLM: "Tôi rất muốn gọi một tool ở đây. Đây có phải là một trong các tool có sẵn không? Đúng rồi, bạn có một tool cho phép tôi liệt kê file. Làm ơn, làm ơn, làm ơn, harness thân mến, gọi tool đó giúp tôi được không?" Và harness có thể đưa ra vài quyết định: ở đây có cần xin phép (permission) không? Có cần làm thêm việc gì khác không? (theo Stephen, video đăng ngày 23/10/2026)

Anh hỏi tiếp: "can you read the contents fo the .py file" (bạn đọc giúp nội dung file .py được không?). Việc đầu tiên model làm là xin chạy `list_files`. Bạn có thể nói nó đã có thông tin này rồi, nhưng theo Stephen, model như đang nói: với file .py, để tôi xem thật sự tất cả các file .py là những file nào; nên nó xin liệt kê file một lần nữa. Lượt này trace ghi đang gửi 5 rồi 7 message. Rồi model trả lời: nó thấy `habits.py` có tồn tại, nhưng nó không có tool nào để đọc nội dung file (demo trong video đăng ngày 23/10/2026).

## 10. Thêm read_file và bảng ánh xạ tên sang hàm

Vậy thì cho nó thêm một tool. Để làm nhanh, Stephen quay lại danh sách tool và thêm một tool nữa "thật nhanh": `read_file`, với description "Read a text file and return its contents." (đọc một file text và trả về nội dung). Tool nhận một đối số `path`, bắt buộc. Hàm Python tương ứng cũng tên `read_file`, nhận `path` và chỉ đơn giản gọi `Path(path).read_text()`, vẫn dùng module `pathlib` (video đăng ngày 23/10/2026).

```
def read_file(path):
return Path(path).read_text()
```

```
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read a text file and return its contents.",
"parameters": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
},
},
```

Còn một vấn đề nữa: trong `run_tool`, tên hàm đang bị hard code, điều anh không muốn. Anh tạo một bảng ánh xạ (mapping) giữa tên tool và hàm, `TOOL_FUNCTIONS`. Anh nói có những cách khác để làm việc này, nhưng cách này đủ tốt. Thay vì gọi cứng `list_files`, `run_tool` giờ lấy hàm từ bảng ánh xạ theo tên mà model gửi tới, nên code chung chung hơn (theo Stephen, video đăng ngày 23/10/2026).

```
def run_tool(call):
args = json.loads(call.function.arguments)
trace("harness", "tool", f"{call.function.name}({args})")
result = TOOL_FUNCTIONS[call.function.name](**args)
trace("tool", "harness", f"{len(result)} characters")
return result
```

Anh hỏi: "Read the habits.py file and tell me what's in it". Lần này model xin chạy `read_file` với `habits/habits.py`, vì yêu cầu giờ đã rõ hơn. Harness gọi `read_file`. Tool, tức hàm, đưa thông tin về cho harness (664 ký tự), và harness gửi nó cho model. Model nói: tôi có câu trả lời bằng chữ cho bạn. `habits.py` là một habit tracker nhỏ chạy trên command line, lưu các thói quen và ngày check-in của chúng (demo trong video đăng ngày 23/10/2026).

![Terminal: model xin read_file habits/habits.py, tool trả 664 characters, model mô tả habits.py là một command-line habit tracker với load, save, check_in, streak](https://homus.dev/photos/oBbwZjcunDE-1403.jpg)

Kết quả của `read_file`: tool trả "664 characters", harness gửi 7 message, và model mô tả `habits.py` là "a small command-line habit tracker" với các hàm `load()`, `save(data)`, `check_in(habit)`, `streak(habit)` (demo trong video đăng ngày 23/10/2026).

Model không đưa ra toàn bộ nội dung file, chỉ nói rằng có `load`, `save`, `check_in` và những thứ tương tự. Stephen mở `habits.py` ra kiểm tra: đúng là có load, save, check-in, streak (demo trong video đăng ngày 23/10/2026).

## 11. Từ if sang while: agent loop

Còn một việc cuối cùng. Stephen thử lại với yêu cầu: "summarize the content of all the Python files in the habits folder" (tóm tắt nội dung của tất cả file Python trong thư mục habits). Model xin chạy `list_files`, tốt. Rồi model quay lại xin chạy `read_file`. Nhưng mọi thứ dừng ở đó: harness không đưa thông tin gì cho anh cả, dòng trả lời chỉ là `None` (demo trong video đăng ngày 23/10/2026). Đây là chỗ cần bước cuối cùng của agent loop.

Anh nhắc lại: mình đã nói là một message có thể kéo theo nhiều tool call. Nhưng nếu câu trả lời cho một message lại là một tool call nữa thì sao? Tất cả những gì cần làm là đổi `if` thành `while`. Trước đó là `if`, giờ mình muốn nói `while`. Vì sao? Vì "cứ tiếp tục đi". Đầu tiên có một tool call, chạy `list_files`. Khi có kết quả, tức danh sách file, model nói: được rồi, giờ tôi cần một tool call khác, `read_file`. Trước đó là `if`. Với `while`, nó sẽ chạy thêm lần nữa. "Famous last words", anh nói, rồi chạy thử (theo Stephen, video đăng ngày 23/10/2026).

```
while True:
user_input = input("you -> harness: ")
messages.append({"role": "user", "content": user_input})
reply = ask_model(messages)

while reply.tool_calls:
for call in reply.tool_calls:
result = run_tool(call)
messages.append(
{
"role": "tool",
"tool_call_id": call.id,
"content": result,
},
)
reply = ask_model(messages)

print(f"harness -> you: {reply.content}")
```

Cùng câu hỏi đó, và lần này đã đi vào một agent loop. Model xin chạy `list_files`. Harness gọi tool. Tool trả lời. Harness gửi kết quả về cho model. Model nói: được, giờ tôi biết có những file nào, vậy tôi cần đọc file .py. Tool đưa nội dung file đó. Harness đọc lại toàn bộ cuộc hội thoại, kể cả kết quả vừa có, và model nói: tốt, đây rồi, tôi có câu trả lời bằng chữ cho bạn (demo trong video đăng ngày 23/10/2026).

![Code while reply.tool_calls và terminal: model trả text reply 411 characters tóm tắt habits.py](https://homus.dev/photos/oBbwZjcunDE-1555.jpg)

Dòng 83 giờ là `while reply.tool_calls:`, nhãn Loop phía trên đã sáng. Terminal: harness gửi 5 message, model trả "text reply (411) characters", tóm tắt rằng `habits.py` đọc dữ liệu từ `habits.json`, ghi ngày hôm nay khi chạy `done <habit>`, và dù có nhãn "streak" thì nó đếm mọi lần check-in chứ không kiểm tra các ngày có liên tiếp hay không (demo trong video đăng ngày 23/10/2026).

"Đây chính là một agent", Stephen nói (video đăng ngày 23/10/2026).

## 12. Agent thật còn thêm những gì

Dĩ nhiên còn nhiều thứ hơn thế. Stephen kể ra: permission (quyền), và có thể bạn muốn giới hạn số lượt (turn) được chạy, rất nhiều tool hơn nữa, rồi memory. Ở video này, anh mới nói về memory nằm trong context, nhưng còn có thể có các file memory bên ngoài và những cách để mở rộng chúng (theo Stephen, video đăng ngày 23/10/2026).

![Hộp chữ Real agents also add: Permissions, Turn limits, Lots more tools, Long-term memory, đặt cạnh code while reply.tool_calls](https://homus.dev/photos/oBbwZjcunDE-1603.jpg)

Hộp chữ "Real agents also add…" trên màn hình liệt kê: "Permissions: ask before running a tool", "Turn limits: stop runaway loops", "Lots more tools", "Long-term memory: files, not just context" (video đăng ngày 23/10/2026).

Vậy là còn rất nhiều thứ nữa. Điều quan trọng, theo anh, là mình đã tự build từ đầu cái gọi là một agent (video đăng ngày 23/10/2026).

## 13. Tóm tắt: một agent hoạt động thế nào

Stephen tóm lại toàn bộ (theo cách anh mô tả trong video đăng ngày 23/10/2026). Harness cho phép bạn nói chuyện với harness. Harness nói chuyện với model. Model có thể gửi thông tin về cho người dùng, hoặc hỏi harness: "bạn chạy giúp tôi một tool được không, vì tôi đã thấy bạn có những tool nào".

Harness có thể quyết định có chạy tool đó hay không. Nó chạy tool, lấy thông tin từ tool, gửi lại cho model. Model có thể xin thêm tool, qua lại nhiều lần, cho tới khi model nói "tôi có câu trả lời rồi". Khi đó câu trả lời được gửi về cho người dùng. Đó là AI agent được build từ đầu (theo Stephen, video đăng ngày 23/10/2026).

![Sơ đồ harness đầy đủ: messages = [...] context short-term memory ở trên, while True ở giữa, tools ở dưới, Model bên phải, You bên trái](https://homus.dev/photos/oBbwZjcunDE-1623.jpg)

Trong khung HARNESS, vòng lặp `while True:` nối với ô `messages = [...]` ("context: short-term memory") ở trên và ô tools (`list_files`, `read_file`) ở dưới, một bên là bạn, một bên là Model (video đăng ngày 23/10/2026).

Stephen mong người xem thích hành trình tự tạo agent của riêng mình. Giờ khi đi dùng agent, bạn phần nào biết chuyện gì đang diễn ra phía sau. Anh hy vọng nó không còn là một điều bí ẩn nữa (video đăng ngày 23/10/2026).

## Nguồn và liên kết

- Tên gốc: How AI Agents Really Work: Build One in Plain Python
- Sự kiện: [YouTube](https://homus.dev/events/youtube)
- Video gốc: [https://www.youtube.com/watch?v=oBbwZjcunDE](https://www.youtube.com/watch?v=oBbwZjcunDE)

- Kênh YouTube Real Python (Stephen Gruppetta): [youtube.com/channel/UCI0vQvr9aFn27yR6Ej6n5UA](https://www.youtube.com/channel/UCI0vQvr9aFn27yR6Ej6n5UA)

- OpenAI Python library, repo ghi là "The official Python library for the OpenAI API" (đọc ngày 28/10/2026): [github.com/openai/openai-python](https://github.com/openai/openai-python)

- OpenAI, hướng dẫn function calling: trang mô tả tool calling là một cuộc hội thoại nhiều bước giữa ứng dụng và model, gồm năm bước chính, từ gửi request kèm tool tới nhận câu trả lời cuối (đọc ngày 28/10/2026): [platform.openai.com/docs/guides/function-calling](https://platform.openai.com/docs/guides/function-calling)

Nguồn: https://homus.dev/talks/how-ai-agents-really-work-build-one-in-plain-python · cập nhật 2026-10-28
