docs/en/s01-the-agent-loop.mdXem bản gốc Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.
s01: Vòng lặp agent
s00 > [ s01 ] > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > s13 > s14 > s15 > s16 > s17 > s18 > s19 > s20 > s21 > s22 > s23 > s24 > s25 > s26 > s27
Không có vòng lặp thì không có agent.
Chương này hướng dẫn bạn cách xây dựng vòng lặp agent tối thiểu và giải thích lý do tại sao agent Hermes chọn phương pháp "nội dung đồng bộ + cầu nối không đồng bộ".
Chương này giải quyết vấn đề gì
Bản thân mô hình ngôn ngữ chỉ "tạo ra phần nội dung tiếp theo". Nó không thể tự mình thực thi các lệnh, quan sát kết quả và suy luận sâu hơn dựa trên các kết quả đó.
Nếu không có lớp mã thực hiện việc này nhiều lần, mô hình chỉ là "một chương trình có thể nói chuyện" -- chưa phải là "một agent có thể hành động".
Nhưng vòng lặp của agent Hermes không chỉ là một while True. Ngay từ đầu, nó phải đối mặt với các vấn đề trong thế giới thực mà các agent hướng dẫn đơn giản không gặp phải:
- Vòng lặp giống nhau phải phục vụ hai điểm vào -- CLI là người dùng gọi trực tiếp từ terminal; Cổng là các tin nhắn đến đồng thời từ nhiều nền tảng. Do đó, bản thân vòng lặp không thể phụ thuộc vào I/O đầu cuối.
- Gateway tạo một phiên bản agent mới cho mỗi tin nhắn -- không có agent toàn cầu nào còn tồn tại. Mỗi tin nhắn đến sẽ sinh ra một phiên bản, cung cấp lịch sử cho nó, chạy đến khi hoàn thành và kết thúc.
- Một số công cụ không đồng bộ -- hầu hết các công cụ (tệp I/O, lệnh đầu cuối) đều đồng bộ, nhưng các yêu cầu mạng và hoạt động của trình duyệt không đồng bộ. Bản thân vòng lặp vẫn đồng bộ và kết nối với các công cụ không đồng bộ.
Chương này chỉ đề cập đến phiên bản tối thiểu. Sự phức tạp ở trên sẽ được mở rộng trong các chương tiếp theo.
Điều khoản chính
run_conversation
Modal nhập cho vòng lặp agent Hermes.
Nó nhận một tin nhắn của người dùng và một danh sách các tin nhắn lịch sử tùy chọn, chạy vòng lặp cho đến khi mô hình ngừng gọi các công cụ và trả về câu trả lời cuối cùng cùng với lịch sử tin nhắn hoàn chỉnh.
Điểm mấu chốt: đó là một đồng bằng def, không phải một async def. Đây là một sự lựa chọn thiết kế có chủ ý.
ngân sách lặp lại và lặp lại
Một lệnh gọi API được tính là một lần lặp. agent Hermes mặc định có tối đa 90 lần lặp.
ngân sách có thể được coi là "khoản phụ cấp" hoặc "hạn mức tín dụng" -- tổng số lượng lệnh gọi API mà cuộc trò chuyện này được phép thực hiện là trần. Tại sao lại là "ngân sách" thay vì "max_iterations"? Bởi vì nó là tài nguyên có thể được sử dụng và chia sẻ, không chỉ là một bộ đếm tĩnh.
Ví dụ đơn giản:
budget = 90
User: "Refactor this file for me"
iter 1: Model says "Let me read the file first" -> calls read_file (remaining: 89)
iter 2: Model says "Let me check the tests too" -> calls read_file (remaining: 88)
iter 3: Model says "I'll make the changes" -> calls edit_file (remaining: 87)
iter 4: Model says "Done" -> stop (remaining: 86)
"Ngân sách chia sẻ" nghĩa là gì trong kịch bản Cổng:
Parent agent budget = 90
iter 1-10: Parent agent works on its own (remaining: 80)
iter 11: Parent agent delegates to a sub-agent for search
+-- Sub-agent uses 15 iterations (remaining: 65)
iter 12+: Parent agent continues, starting from 65
agent phụ không nhận được "90 khác." Nó rút ra từ ví của agent mẹ. Đó là lý do tại sao nó được gọi là ngân sách.

kết thúc_lý do
Câu trả lời của model cho "tại sao tôi lại dừng lại?"
stop: nói xongtool_calls: muốn gọi công cụlength: đầu ra đã bị cắt bớt
Vòng lặp sử dụng điều này để quyết định phải làm gì tiếp theo.
tin nhắn và api_messages
agent Hermes duy trì hai bản sao của tin nhắn trong nội bộ:
messages: "sổ cái đầy đủ" nội bộ của bạn chứa mọi thứ, kể cả trạng thái nội bộ (chẳng hạn nhưreasoningtrường)api_messages: một bản sao được làm sạch có nguồn gốc từmessagesngay trước mỗi lệnh gọi API, chỉ chứa các trường mà mô hình có thể hiểu được
Ví dụ đơn giản:
# messages (full internal version)
messages = [
{"role": "user", "content": "What's the weather today?"},
{
"role": "assistant",
"content": "Let me check",
"reasoning": "User asked about weather, I should call a tool", # <- internal field
"_internal_token_count": 42, # <- internal field
},
]
# Clean up before calling the API ->
api_messages = [
{"role": "system", "content": "You are Hermes..."}, # <- prepended
{"role": "user", "content": "What's the weather today?"},
{
"role": "assistant",
"content": "Let me check",
# reasoning and _internal_token_count have been stripped
},
]
client.chat.completions.create(messages=api_messages, ...)
Tại sao duy trì hai bản sao?
messagesphải được duy trì và sẵn sàng để gỡ lỗi -- càng nhiều thông tin càng tốtapi_messagesphải được gửi tới API tương thích với OpenAI -- các trường bổ sung gây ra lỗi hoặc lãng phí token
Trong một câu: tin nhắn là bản nháp; api_messages là bức thư bạn thực sự gửi đi.

Bạn có thể bỏ qua sự khác biệt này trong phiên bản giảng dạy đầu tiên, nhưng bạn nên biết nó tồn tại.

Mô hình tinh thần tối thiểu
user message
|
v
Assemble system prompt (persona + memory + project config + tool definitions)
|
v
model API (OpenAI-compatible format)
|
+-- finish_reason: stop -----> return final reply
|
+-- finish_reason: tool_calls --> execute tools
|
v
tool result
|
v
write back to messages
|
v
next iteration
Điều thực sự quan trọng không phải là "có một vòng lặp".
Điều thực sự quan trọng là hai điều:
- Kết quả công cụ phải được ghi lại vào lịch sử tin nhắn -- nếu không mô hình không thể thấy kết quả thực hiện ở lượt tiếp theo.
- Lời nhắc hệ thống được tập hợp lại ở phía trước thông báo cho mỗi lệnh gọi API -- nó không nằm trong danh sách thông báo; nó được chuẩn bị riêng mỗi lần.
Cấu trúc dữ liệu chính
Tin nhắn
Một tin nhắn có định dạng OpenAI. Ba vai trò:
# User message
{"role": "user", "content": "Search for what's new in Python 3.12"}
# Assistant message (with tool calls)
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc",
"function": {
"name": "web_search",
"arguments": '{"query": "Python 3.12 new features"}',
},
}
],
}
# Tool result
{
"role": "tool",
"tool_call_id": "call_abc",
"content": "Python 3.12 introduces...",
}
Lưu ý tool_call_id -- nó khớp một kết quả với lệnh gọi tương ứng. Mô hình có thể gọi nhiều công cụ trong một lượt và mọi kết quả phải khớp với lệnh gọi phù hợp.
Lời nhắc hệ thống
Không được lưu trữ trong tin nhắn. Được chuẩn bị trước dưới dạng thông báo vai trò hệ thống đầu tiên trước mỗi lệnh gọi API:
api_messages = [{"role": "system", "content": system_prompt}] + messages
Lời nhắc của hệ thống agent Hermes được tập hợp từ nhiều nguồn: tệp cá nhân (SOUL.md), bộ nhớ (MEMORY.md), cấu hình dự án (HERMES.md), định nghĩa công cụ và chỉ mục kỹ năng. Những điều này được trình bày chi tiết trong s04.
Thực hiện tối thiểu
Bước 1: Tạo ứng dụng khách
from openai import OpenAI
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key="...")
Tất cả các nhà cung cấp mô hình đều được truy cập thông qua ứng dụng khách này. Chuyển đổi mô hình chỉ yêu cầu thay đổi base_url và api_key.
Bước 2: Xây dựng vòng lặp
def run_conversation(user_message, system_prompt, tools, max_iterations=90):
messages = [{"role": "user", "content": user_message}]
for i in range(max_iterations):
# Assemble API messages: system prompt + conversation history
api_messages = [{"role": "system", "content": system_prompt}] + messages
# Call the model
response = client.chat.completions.create(
model="anthropic/claude-sonnet-4",
messages=api_messages,
tools=tools,
)
assistant_msg = response.choices[0].message
# Write the assistant reply back into history (whether or not it has tool_calls)
messages.append({
"role": "assistant",
"content": assistant_msg.content,
"tool_calls": [
{"id": tc.id, "function": {"name": tc.function.name, "arguments": tc.function.arguments}}
for tc in (assistant_msg.tool_calls or [])
] or None,
})
# No tool_calls -> done
if not assistant_msg.tool_calls:
return {"final_response": assistant_msg.content, "messages": messages}
# Execute each tool and write results back
for tool_call in assistant_msg.tool_calls:
output = run_tool(tool_call.function.name, tool_call.function.arguments)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": output,
})
return {"final_response": "Reached maximum iteration count", "messages": messages}
Đây là vòng lặp agent Hermes tối thiểu.
Nó liên quan như thế nào đến mã thực
thực tế AIAgent.run_conversation() phức tạp hơn nhiều, nhưng bộ xương cốt lõi hoàn toàn giống nhau:
- Tập hợp tin nhắn
- Gọi API
- Soạn lại tin nhắn trợ lý
- Nếu có tool_calls, hãy thực hiện chúng và ghi lại kết quả
- Tiếp tục
Ngoài khung này, mã thực còn bổ sung: kiểm tra nén trước, móc plugin, chèn bộ nhớ, xử lý trường lý luận, hỗ trợ ngắt, đầu ra phát trực tuyến và thử lại lỗi. Tất cả những điều này sẽ được mở rộng trong các chương sau.
Ba thiết kế độc đáo trong Hermes Agent Loop
1. Quản lý phiên bản trong Gateway: Bộ nhớ đệm và tái sử dụng, không phải tạo lại
Trong chế độ CLI, một phiên bản AIAgent duy nhất chạy từ đầu đến cuối.
Giải thích giảng dạy đơn giản cho chế độ Cổng kết nối là "một phiên bản mới được tạo cho mỗi tin nhắn". Việc triển khai thực tế thông minh hơn -- Gateway duy trì bộ đệm phiên bản:
# gateway/run.py
self._agent_cache: Dict[str, tuple] = {} # session_key -> (AIAgent, config_signature)
Dòng chảy:
Message arrives -> compute config signature (model + api_key + provider + toolsets)
-> check cache
+-- hit -> reuse existing instance (system prompt and tool defs unchanged)
+-- miss -> create new instance, store in cache
-> update lightweight per-message fields (callbacks, reasoning_config)
-> call run_conversation()
Bộ đệm chỉ bị vô hiệu khi người dùng chạy /new (đặt lại phiên), /model (chuyển đổi mô hình) hoặc dự phòng được kích hoạt.
Lý do quan trọng nhất để sử dụng lại các phiên bản là bộ nhớ đệm nhắc nhở -- Anthropic API yêu cầu lời nhắc hệ thống giữ nguyên trong các lượt để nhận được lượt truy cập bộ nhớ đệm. Tái sử dụng phiên bản = tiết kiệm tiền và thời gian.
Nhưng nguyên tắc cốt lõi vẫn giữ nguyên: không lưu trữ trạng thái tin nhắn chéo trong các biến thực thể. Lịch sử hội thoại luôn được truyền vào từ SQLite; nó không được lưu trữ trong bộ nhớ cá thể. Các phiên bản agent có thể bị loại bỏ và xây dựng lại bất cứ lúc nào và mã không được dựa vào chúng để "sống sót".
2. Bộ nhớ đệm hệ thống
Trong cuộc gọi đầu tiên, lời nhắc hệ thống được tập hợp từ nhiều nguồn và được lưu vào bộ nhớ đệm. Các cuộc gọi tiếp theo sẽ sử dụng lại phiên bản đã lưu trong bộ nhớ đệm mà không cần tập hợp lại.
Đây không chỉ là một sự tối ưu hóa hiệu suất. Cơ chế lưu vào bộ nhớ đệm nhanh chóng của Anthropic yêu cầu dấu nhắc hệ thống không thay đổi qua các lượt. Nếu nó được tập hợp lại mỗi lần (ví dụ: do tệp bộ nhớ đã được sửa đổi), bộ nhớ đệm sẽ bị vô hiệu.
Vì vậy, khi agent Hermes tiếp tục một phiên, nó sẽ đọc lại lời nhắc hệ thống được lưu trữ trước đó từ SQLite thay vì tập hợp lại nó.
3. Vòng lặp đồng bộ + Cầu không đồng bộ
Bản thân vòng lặp là đồng bộ def. Nhưng một số công cụ (yêu cầu mạng, thao tác trình duyệt) yêu cầu không đồng bộ.
Vấn đề cốt lõi: Hầu hết các công cụ trong vòng lặp agent đều đồng bộ (đọc tệp, ghi tệp, chạy lệnh), nhưng một số công cụ không đồng bộ (yêu cầu HTTP, tự động hóa trình duyệt). Làm thế nào để bạn hỗ trợ cả hai trong một vòng lặp?
Hai cách tiếp cận:
Cách tiếp cận A: Tạo toàn bộ vòng lặp async def
async def run_conversation(...):
...
result = await run_tool(...) # await every tool
Trông giống nhau nhưng chi phí thì: tất cả các công cụ đồng bộ đều cần trình bao bọc không đồng bộ, dấu vết ngăn xếp lỗi trở nên phức tạp hơn, việc gỡ lỗi trở nên khó khăn hơn.
Cách tiếp cận B (lựa chọn của Hermes): Vòng lặp đồng bộ; các công cụ không đồng bộ được kết nối theo yêu cầu
def run_conversation(...): # plain def, not async
...
if tool_is_async:
result = event_loop.run(async_tool(...)) # bridge to the event loop
else:
result = sync_tool(...) # call directly
"Vòng lặp sự kiện liên tục" là gì?
asyncio.run() tạo một vòng lặp sự kiện mới trên mỗi cuộc gọi và hủy nó sau đó. agent Hermes không sử dụng mẫu này. Thay vào đó, nó tạo một vòng lặp sự kiện khi khởi động và duy trì vòng lặp đó để tái sử dụng:
# Created once at startup
loop = asyncio.new_event_loop()
# Reused every time an async tool needs to run
def bridge_async(coro):
return asyncio.run_coroutine_threadsafe(coro, loop).result()
Tại sao phải kiên trì? Bởi vì một số tài nguyên không đồng bộ (chẳng hạn như phiên trình duyệt và kết nối WebSocket) tồn tại trên nhiều lệnh gọi công cụ. Nếu một vòng lặp sự kiện mới được tạo mỗi lần, các kết nối đó sẽ bị đứt.
Tóm tắt một câu: Vòng lặp chính vẫn đồng bộ để đơn giản. Chỉ khi gặp một công cụ không đồng bộ, nó mới giao nhiệm vụ cho vòng lặp sự kiện thường trú để thực thi, sau đó lấy lại kết quả và tiếp tục quy trình đồng bộ.
Những sai lầm phổ biến nhất của người mới bắt đầu
1. Không viết lại tin nhắn trợ lý
Kết quả của công cụ được ghi lại nhưng thông báo trợ lý thì không. Trong lệnh gọi API tiếp theo, mô hình không thể thấy những gì nó đã nói ở lượt trước.
2. Không ràng buộc tool_call_id
Mô hình gọi hai công cụ trong một lượt, nhưng không có kết quả nào mang id. Mô hình không thể cho biết kết quả nào tương ứng với lệnh gọi nào.
3. Đưa lời nhắc hệ thống vào danh sách tin nhắn
Lời nhắc hệ thống phải được thêm vào trước mỗi lệnh gọi API, không được lưu trữ như một phần của danh sách tin nhắn. Nếu không, nó sẽ được lưu giữ, nén và sao chép.
4. Không đặt giới hạn lặp lại
Một vòng lặp không có max_iterations sẽ chạy mãi mãi nếu mô hình tiếp tục gọi tools. agent Hermes mặc định là 90.
5. Giả sử các phiên bản agent tồn tại lâu dài
Ở chế độ Cổng, mọi tin nhắn đều có một phiên bản mới. Không lưu trữ trạng thái thông báo chéo trong các biến thể hiện.
Ranh giới giảng dạy
Chương này chỉ cần nhấn mạnh một điều:
tin nhắn -> mô hình -> tool_calls -> tool_result -> lượt tiếp theo
Vòng lặp này là nền tảng cho mọi cơ chế tiếp theo.
Những gì được cố tình bỏ đi:
- Cách thức đăng ký và gửi đi các công cụ ->
s02 - Cách duy trì cuộc trò chuyện ->
s03 - Cách tập hợp lời nhắc hệ thống ->
s04 - Phải làm gì khi ngữ cảnh quá dài ->
s05 - Phải làm gì khi API bị lỗi ->
s06
Nếu bạn có thể viết vòng lặp tối thiểu ở trên từ bộ nhớ thì bạn đã hoàn thành chương này.
Một câu để nhớ
Vòng lặp Hermes Agent mang tính đồng bộ, gọi bất kỳ mô hình nào thông qua giao diện tương thích với OpenAI, được chia sẻ bởi Gateway và CLI và kết quả của công cụ phải được ghi lại vào thông báo để mô hình tiếp tục hoạt động.