docs/en/s22-hook-system.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.
s22: Hệ thống hook & BOOT.md
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
Đến s21, các khả năng cốt lõi và cơ chế tự tiến hóa của agent đều đã sẵn sàng. Nhưng nếu bạn muốn agent "chạy tự kiểm tra mỗi khi nó khởi động" hoặc "thêm lớp kiểm tra trước mỗi lệnh gọi công cụ" -- bạn không cần sửa đổi mã lõi. Bạn chỉ cần gắn một cái móc.

Chương này giải quyết vấn đề gì
Ba kịch bản.
Kịch bản 1: Tự kiểm tra khi khởi động. Bạn muốn agent tự động kiểm tra xem có bất kỳ công việc định kỳ nào bị lỗi qua đêm hay không và liệu dung lượng ổ đĩa có đủ mỗi lần nó khởi động hay không. Không cần thay đổi mã -- chỉ cần viết một BOOT.md:
# Startup Checklist
1. Check if any cron jobs failed overnight
2. Check disk usage, alert if any partition > 80%
3. If there are errors in deploy.log, summarize them
agent tự động thực hiện danh sách kiểm tra này khi khởi động, giống như một kỹ sư trực đang thực hiện kiểm tra tuần tra.
Kịch bản 2: Kiểm tra công cụ. Bạn muốn ghi lại một mục mỗi lần terminal công cụ được gọi -- ai đã thực thi lệnh gì và khi nào. Không có thay đổi nào đối với mã của công cụ đầu cuối -- chỉ cần đính kèm một pre_tool_call móc:
def audit_tool(tool_name, args, **kw):
if tool_name == "terminal":
log(f"[audit] {datetime.now()} terminal: {args.get('command')}")
Kịch bản 3: Tóm tắt phiên. Sau khi mỗi cuộc trò chuyện kết thúc, hãy tự động tạo một bản tóm tắt và lưu vào tệp nhật ký. Không có thay đổi nào đối với vòng lặp lõi -- chỉ cần đính kèm một on_session_end cái móc.
Điểm chung của cả ba trường hợp này là: chèn logic tùy chỉnh vào các điểm cụ thể trong vòng lặp lõi mà không sửa đổi mã lõi.
Đề xuất đọc
s02-tool-system.md--pre_tool_call/post_tool_callhooks bắn trước và sau khi gọi công cụs12-gateway-architecture.md-- Cổng nối kích hoạt tại các điểm chính trong định tuyến tin nhắns20-background-review.md-- Đánh giá cơ bản có thể được coi là một hook "hậu phiên" tích hợp sẵn
Các khái niệm chính
Móc vòng đời là gì
agent có một số thời điểm quan trọng từ khi khởi động đến khi tắt:
gateway:startup -> session:start -> agent:start
|
(tool call loop)
pre_tool_call -> execute -> post_tool_call
|
agent:end -> session:end
Hook là một hàm gọi lại mà bạn gắn vào một trong những khoảnh khắc này. Khi thời điểm đó đến, hệ thống sẽ tự động gọi hàm của bạn.
BOOT.md là gì
Một tệp Markdown được đặt tại ~/.hermes/BOOT.md. Khi Cổng khởi động, nó sẽ tự động gửi nội dung của nó dưới dạng lời nhắc tới agent một lần để thực thi.
Đây không phải là tập lệnh shell -- đó là một tập hợp hướng dẫn dành cho agent. Bạn có thể viết những thứ như "kiểm tra nhật ký" hoặc "gửi tin nhắn tới Discord" và nhân viên sẽ sử dụng các công cụ riêng của mình để thực hiện chúng.
Hệ thống hai móc
Hermes Agent có hai hệ thống móc độc lập, được thiết kế cho các tình huống khác nhau:
| Móc cổng | Móc plugin | |
|---|---|---|
| Phạm vi | Chỉ chế độ cổng | Có sẵn trong cả CLI và Gateway |
| Đăng ký | Các tập tin trong ~/.hermes/hooks/ thư mục | register_hook() trong mã |
| Lời mời | emit(event_type, context) | invoke_hook(hook_name, **kwargs) |
| Mô hình thực hiện | Không đồng bộ (hỗ trợ không đồng bộ) | Đồng bộ |
| Trường hợp sử dụng | Sự kiện cấp nền tảng (kết nối, định tuyến tin nhắn) | Sự kiện cấp agent (gọi công cụ, yêu cầu API) |
Vì mục đích giảng dạy, trước tiên chúng ta sẽ giải thích từng cái riêng biệt, sau đó thảo luận tại sao lại có hai cái.
Mô hình tinh thần tối thiểu
~/.hermes/BOOT.md
| "Check cron and disk on startup"
|
v
Gateway starts -> emit("gateway:startup")
|
| boot-md hook: read BOOT.md -> launch one-shot agent -> execute instructions
|
v
User sends message -> emit("session:start") / emit("agent:start")
|
| -> plugin hooks: invoke_hook("pre_llm_call") -> inject context
| -> model call
| -> plugin hooks: invoke_hook("pre_tool_call") -> audit log
| -> tool execution
| -> plugin hooks: invoke_hook("post_tool_call")
|
v
Agent finishes -> emit("agent:end")
|
| -> plugin hooks: invoke_hook("on_session_end") -> session summary
|
v
Session ends -> emit("session:end")
Phần 1: Móc cổng
Các loại sự kiện
| Sự kiện | Khi Nó Bắn | bối cảnh Chứa |
|---|---|---|
gateway:startup | Quá trình cổng bắt đầu | platforms (danh sách các nền tảng được kết nối) |
session:start | Đã tạo phiên mới | platform, user_id, session_key |
session:end | Phiên kết thúc | platform, user_id, session_key |
session:reset | Người dùng thực thi /new | platform, user_id, session_key |
agent:start | agent bắt đầu xử lý tin nhắn | platform, user_id, message |
agent:end | agent hoàn tất xử lý | platform, user_id, message, response |
command:* | Lệnh gạch chéo nào | platform, user_id, command, args |
command:* là ký tự đại diện -- đăng ký một lần để nghe tất cả các lệnh.
Cấu trúc tệp móc
~/.hermes/hooks/
└── my-audit-hook/
├── HOOK.yaml <- Declares name, description, which events to listen for
└── handler.py <- The actual code that runs
HOOK.yaml:
name: my-audit-hook
description: Log all agent activity
events:
- agent:start
- agent:end
handler.py:
async def handle(event_type: str, context: dict):
if event_type == "agent:start":
print(f"[audit] agent started for {context.get('user_id')}")
elif event_type == "agent:end":
print(f"[audit] agent done, response: {context.get('response', '')[:50]}")
Người xử lý có thể là def hoặc async def -- hệ thống tự động phát hiện bằng cách sử dụng asyncio.iscoroutine().
Đăng ký móc
class HookRegistry:
def __init__(self):
self._handlers: dict[str, list[Callable]] = {}
def register(self, event_type: str, handler: Callable):
self._handlers.setdefault(event_type, []).append(handler)
async def emit(self, event_type: str, context: dict | None = None):
handlers = list(self._handlers.get(event_type, []))
# Wildcard matching: command:* matches all command:xxx
if ":" in event_type:
base = event_type.split(":")[0]
handlers.extend(self._handlers.get(f"{base}:*", []))
for fn in handlers:
try:
result = fn(event_type, context or {})
if asyncio.iscoroutine(result):
await result
except Exception as e:
print(f"[hooks] Error in handler for '{event_type}': {e}")
Thiết kế chính: các ngoại lệ móc không bao giờ lan truyền. try/except đảm bảo một móc bị lỗi không thể làm hỏng toàn bộ Cổng.
Tự động phát hiện
def discover_and_load(self, hooks_dir: Path):
"""Scan hooks directory, load HOOK.yaml + handler.py."""
if not hooks_dir.exists():
return
for hook_dir in sorted(hooks_dir.iterdir()):
manifest = hook_dir / "HOOK.yaml"
handler_file = hook_dir / "handler.py"
if not manifest.exists() or not handler_file.exists():
continue
meta = yaml.safe_load(manifest.read_text())
# Dynamically import handler.py
spec = importlib.util.spec_from_file_location(meta["name"], handler_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
handle_fn = getattr(module, "handle")
for event in meta.get("events", []):
self.register(event, handle_fn)
Phần 2: BOOT.md
BOOT.md là một hook Gateway tích hợp sẵn, được liên kết với gateway:startup sự kiện.
Nó hoạt động như thế nào
async def handle_boot_md(event_type: str, context: dict):
boot_path = HERMES_HOME / "BOOT.md"
if not boot_path.exists():
return
content = boot_path.read_text()
def _run():
# Create a one-shot agent to execute BOOT.md instructions
conn = init_db(DB_PATH)
session_id = create_session(conn)
prompt = build_system_prompt(os.getcwd())
result = run_conversation(
user_message=content,
conn=conn,
session_id=session_id,
cached_prompt=prompt,
max_iterations_override=20,
)
conn.close()
return result
# Run in a background thread so it doesn't block Gateway startup
thread = threading.Thread(target=_run, daemon=True, name="boot-md")
thread.start()
Kịch bản: Luồng tự kiểm tra khi khởi động
1. Gateway starts
2. HookRegistry discovers the built-in boot-md hook
3. emit("gateway:startup") -> handle_boot_md is called
4. Reads ~/.hermes/BOOT.md:
"1. Check if any cron jobs failed
2. Check disk usage"
5. Background thread launches a one-shot agent
6. Agent calls terminal tool to run df -h, finds /data at 85%
7. Agent calls terminal tool to check cron logs, finds last night's backup failed
8. Agent sends results to user via Gateway callback (if a home channel is configured)
9. Gateway continues running normally, unaffected by the boot process
Tại sao không sử dụng tập lệnh shell? Bởi vì các lệnh BOOT.md được thực thi bởi agent, agent này có quyền truy cập vào tất cả các công cụ của nó -- terminal, trình duyệt, công cụ MCP. df -h chỉ là ví dụ đơn giản nhất; bạn cũng có thể viết "đăng nhập vào bảng điều khiển giám sát và kiểm tra cảnh báo."
Phần 3: Móc plugin
Các loại móc
| Móc | Khi Nó Bắn | Nó có thể làm gì |
|---|---|---|
pre_tool_call | Trước thực thi công cụ | Kiểm tra ghi nhật ký, chặn các hoạt động nguy hiểm |
post_tool_call | Sau thực thi công cụ | Ghi lại kết quả, theo dõi thời gian đã trôi qua |
pre_llm_call | Trước khi bắt đầu lượt trò chuyện | Đưa ngữ cảnh bổ sung |
post_llm_call | Sau khi lượt trò chuyện kết thúc | Ghi lại kết quả |
on_session_start | Lượt đầu tiên của phiên mới | Đang khởi tạo tài nguyên |
on_session_end | Sau mỗi lượt trò chuyện kết thúc | Dọn dẹp, tóm tắt |
Đăng ký
class PluginHookRegistry:
def __init__(self):
self._hooks: dict[str, list[Callable]] = {}
def register_hook(self, hook_name: str, callback: Callable):
self._hooks.setdefault(hook_name, []).append(callback)
def invoke_hook(self, hook_name: str, **kwargs) -> list:
results = []
for cb in self._hooks.get(hook_name, []):
try:
ret = cb(**kwargs)
if ret is not None:
results.append(ret)
except Exception as e:
print(f" [hook] {hook_name} error: {e}")
return results
Kịch bản: Móc kiểm tra công cụ
# Registration
hooks = PluginHookRegistry()
audit_log = []
def audit_tool_call(tool_name, args, **kw):
audit_log.append({
"time": datetime.now().isoformat(),
"tool": tool_name,
"args": args,
})
hooks.register_hook("pre_tool_call", audit_tool_call)
# Triggered during tool dispatch
hooks.invoke_hook("pre_tool_call", tool_name="terminal",
args={"command": "rm -rf /tmp/build"})
# audit_log now has one entry
Tích hợp với run_conversation
# Before tool call
hooks.invoke_hook("pre_tool_call", tool_name=tool_name, args=tool_args)
output = registry.dispatch(tool_name, tool_args)
# After tool call
hooks.invoke_hook("post_tool_call", tool_name=tool_name,
args=tool_args, result=output)
Tại sao có hai hệ thống móc
| Kích thước | Móc cổng | Móc plugin |
|---|---|---|
| Độ chi tiết | Thô (cấp phiên, cấp nền tảng) | Tốt (cấp lệnh gọi công cụ, cấp độ lệnh gọi API) |
| Triển khai | Thư mục tệp, không cần mã hóa | Đăng ký mã, yêu cầu Python |
| Trường hợp sử dụng | Hoạt động: giám sát, cảnh báo, kiểm toán | Phát triển: gỡ lỗi, tùy chỉnh, mở rộng |
| Tải lại nóng | Quét lúc khởi động, không có bản cập nhật nóng | Đăng ký mã, thực thi khi khởi động |
Liệu cả hai có thể hợp nhất thành một? Về mặt kỹ thuật thì có, nhưng nó sẽ khiến các tình huống đơn giản trở nên phức tạp hơn. Nhân viên vận hành chỉ cần viết HOOK.yaml và một vài dòng Python -- họ không cần hiểu chuỗi đăng ký plugin.
Điều gì đã thay đổi (s21 -> s22)
| Thành phần | s21 | s22 |
|---|---|---|
| Phương pháp mở rộng | Chỉ thay đổi mã | Thư mục hook + đăng ký mã |
| Hành vi khởi nghiệp | Đã sửa | Có thể tùy chỉnh qua BOOT.md |
| Kiểm toán công cụ | Không có | trước/post_tool_call |
| Vòng đời phiên | Không có móc | on_session_start/end |
| Cách ly lỗi | Không có | Tất cả các ngoại lệ hook đều không lan truyền |
Những lỗi thường gặp của người mới bắt đầu
1. Chặn vòng lặp chính bằng thao tác móc chậm
# Wrong: making an external API call in pre_tool_call
def slow_audit(tool_name, args, **kw):
requests.post("https://audit.example.com", json={...}) # 2 seconds
Nhân viên đợi thêm 2 giây cho mỗi cuộc gọi công cụ.
Khắc phục: Đưa các thao tác chậm vào hàng đợi hoặc chuỗi. Bản thân hook chỉ nên thực hiện các thao tác nhanh (ghi vào tệp cục bộ, xếp hàng).
2. handler.py Thiếu hàm xử lý
# Wrong: handler.py
def on_event(event_type, context): # Wrong function name
...
Hệ thống sử dụng getattr(module, "handle") để tìm chức năng nhập. Nếu tên không handle, nó sẽ không được tìm thấy.
Khắc phục: Phải đặt tên cho hàm handle.
3. Giả sử Hook có thể sửa đổi đối số của công cụ
# Wrong: trying to modify args in pre_tool_call
def modify_args(tool_name, args, **kw):
args["command"] = "echo safe" # Won't take effect
các args hook nhận được được truyền theo giá trị, không phải theo tham chiếu. Việc sửa đổi chúng không ảnh hưởng đến việc thực hiện thực tế.
Khắc phục: Để chặn thực thi, hãy quay lại {"action": "block"} nên hệ thống sẽ chặn cuộc gọi.
4. Viết các thao tác tương tác trong BOOT.md
# Wrong: requires user input
1. Ask the user which project to check
BOOT.md chạy khi khởi động Gateway khi không có người dùng nào ở đó. Các thao tác tương tác sẽ bị treo.
Khắc phục: BOOT.md chỉ nên chứa các hướng dẫn tự động không yêu cầu sự tham gia của người dùng.
Ranh giới giảng dạy
Chương này bao gồm bốn điều:
- Gateway hooks -- Các loại sự kiện, HOOK.yaml + handler.py, HookRegistry
- BOOT.md -- Móc cổng tích hợp, tự kiểm tra khởi động
- Móc plugin -- pre/post_tool_call, on_session_start/end
- Tại sao lại có hai hệ thống -- Mức độ chi tiết khác nhau, phương pháp triển khai khác nhau, người dùng mục tiêu khác nhau
Không được bảo hiểm:
pre_api_request/post_api_request-> tương tự như pre_tool_call, cùng mẫu- Cơ chế tải và phát hiện plugin (điểm vào pip) -> chi tiết quản lý gói
- Móc tải nóng -> tối ưu hóa sản xuất
- Chèn bối cảnh thông qua
pre_llm_call-> cách sử dụng nâng cao
Chương này liên quan đến chương khác như thế nào
- Công cụ gửi của s02 ->
pre_tool_call/post_tool_callchữa cháy trước và sau khi gửi đi - s12's Gateway -> Cổng hook kích hoạt tại các điểm chính trong GatewayRunner
- Nhiệm vụ theo lịch trình của s15 -> BOOT.md có thể kiểm tra trạng thái công việc định kỳ
- Đánh giá lý lịch của s20 -> đánh giá về cơ bản là một phần được tích hợp sẵn
on_session_endcái móc - Kiến trúc plugin của s24 -> plugin hook là nền tảng của hệ sinh thái plugin
Sau chương này, bạn sẽ có thể trả lời
- Để viết một hook ghi lại mọi lần thực thi công cụ terminal, bạn cần những file gì?
- Tại sao BOOT.md được thực thi bởi agent thay vì bằng shell?
- Tại sao các hook Gateway và hook Plugin không được gộp vào một hệ thống duy nhất?
- Điều gì xảy ra nếu một hook ném ra một ngoại lệ? agent có gặp sự cố không?
- Có thể
pre_tool_callhook ngăn chặn việc thực thi công cụ? Làm sao?
Một lớp: Móc cho phép bạn chèn logic tùy chỉnh vào các thời điểm quan trọng trong vòng đời của agent -- mà không thay đổi mã lõi, không ảnh hưởng đến các tính năng khác và với các ngoại lệ không lan truyền. BOOT.md là hook đơn giản nhất: một tệp Markdown mà agent tự động thực thi khi khởi động.