Harness Academy
Tài liệu gốc 25 13 phútTiếng Việt + English
docs/en/s22-hook-system.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

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.

Hệ thống móc vòng đời

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

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ổngMóc plugin
Phạm viChỉ chế độ cổngCó sẵn trong cả CLI và Gateway
Đăng kýCác tập tin trong ~/.hermes/hooks/ thư mụcregister_hook() trong mã
Lời mờiemit(event_type, context)invoke_hook(hook_name, **kwargs)
Mô hình thực hiệnKhông đồng bộ (hỗ trợ không đồng bộ)Đồng bộ
Trường hợp sử dụngSự 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ệnKhi Nó Bắnbối cảnh Chứa
gateway:startupQuá trình cổng bắt đầuplatforms (danh sách các nền tảng được kết nối)
session:startĐã tạo phiên mớiplatform, user_id, session_key
session:endPhiên kết thúcplatform, user_id, session_key
session:resetNgười dùng thực thi /newplatform, user_id, session_key
agent:startagent bắt đầu xử lý tin nhắnplatform, user_id, message
agent:endagent hoàn tất xử lýplatform, user_id, message, response
command:*Lệnh gạch chéo nàoplatform, 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ócKhi Nó BắnNó có thể làm gì
pre_tool_callTrướ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_callSau thực thi công cụGhi lại kết quả, theo dõi thời gian đã trôi qua
pre_llm_callTrước khi bắt đầu lượt trò chuyệnĐưa ngữ cảnh bổ sung
post_llm_callSau khi lượt trò chuyện kết thúcGhi lại kết quả
on_session_startLượt đầu tiên của phiên mớiĐang khởi tạo tài nguyên
on_session_endSau mỗi lượt trò chuyện kết thúcDọ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ướcMóc cổngMóc plugin
Độ chi tiếtThô (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 khaiThư 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ụngHoạt động: giám sát, cảnh báo, kiểm toánPhát triển: gỡ lỗi, tùy chỉnh, mở rộng
Tải lại nóngQué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ầns21s22
Phương pháp mở rộngChỉ thay đổi mãThư mục hook + đăng ký mã
Hành vi khởi nghiệpĐã sửaCó 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ênKhông có mócon_session_start/end
Cách ly lỗiKhô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:

  1. Gateway hooks -- Các loại sự kiện, HOOK.yaml + handler.py, HookRegistry
  2. BOOT.md -- Móc cổng tích hợp, tự kiểm tra khởi động
  3. Móc plugin -- pre/post_tool_call, on_session_start/end
  4. 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_call chữ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_end cá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_call hook 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.

Mục lục nguồn