docs/en/s10-subagent-delegation.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.
s10: Ủy quyền agent phụ
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
Một nhiệm vụ lớn không cần phải nhồi nhét vào một bối cảnh duy nhất. Giao nhiệm vụ phụ cho các bối cảnh riêng biệt và chỉ mang lại kết quả khi hoàn thành.
Chương này giải quyết vấn đề gì
Bởi s09, agent đã có sẵn công cụ, tính kiên trì, bộ nhớ, kỹ năng và quyền. Nó thực sự có thể hoạt động độc lập.
Nhưng khi các nhiệm vụ trở nên phức tạp hơn, một vấn đề bắt đầu xuất hiện:
Người dùng có thể chỉ nói một điều:
"Nghiên cứu các tính năng mới trong Python 3.12, sau đó cập nhật README."
Để hoàn thành việc này, agent có thể cần phải:
- Tìm kiếm qua 10 bài viết
- Đọc 5 tập tin
- Ghi file 3 lần
- Chạy thử nghiệm hai lần
Tất cả các bước trung gian này chồng lên nhau trong cùng một messages list, khiến bối cảnh tăng lên nhanh chóng. Nếu sau đó người dùng hỏi một câu hỏi hoàn toàn không liên quan, mô hình sẽ phải sàng lọc hàng núi tiếng ồn "kết quả tìm kiếm" và "nội dung tệp" để tìm thông tin hữu ích.
Quan trọng hơn: nếu hai nhiệm vụ phụ không liên quan đến nhau (ví dụ: "nghiên cứu các tính năng mới" và "kiểm tra phạm vi kiểm tra"), thì các bước trung gian của chúng sẽ gây nhiễu lẫn nhau, thực sự làm giảm chất lượng suy luận của mô hình.
Đây là vấn đề mà các agent phụ giải quyết:
Chạy các tác vụ đã bản địa hóa trong các bối cảnh riêng biệt và chỉ mang lại những kết quả cần thiết. Giữ bối cảnh của agent gốc rõ ràng.

Thuật ngữ chính
agent mẹ là gì
Phiên bản agent hiện đang trò chuyện với người dùng, giữ quyền chính messages.
agent phụ là gì
Một phiên bản agent được tạo tạm thời bởi agent mẹ với độc lập của chính nó messages. Nó trả về kết quả sau khi hoàn thành nhiệm vụ con, sau đó bị hủy.
subagent và agent mẹ chia sẻ cùng một đường dẫn mã (cả hai đều sử dụng AIAgent.run_conversation()). Sự khác biệt duy nhất là:
- agent phụ có cái riêng
messages(cách ly bối cảnh) - Bộ công cụ của subagent bị hạn chế (không được ủy quyền, không được sửa đổi bộ nhớ)
- subagent tiêu thụ từ quỹ lặp của agent mẹ
Chia sẻ ngân sách lặp lại là gì
subagent không nhận được "90 lệnh gọi API riêng biệt". Nó rút ra từ ngân sách của agent mẹ:
Parent agent budget = 90
iter 1-10: Parent agent does its own work (80 remaining)
iter 11: Parent agent dispatches subagent to search
+-- Subagent uses 15 iters (65 remaining)
iter 12+: Parent agent continues, starting from 65
Điều này ngăn chặn các agent phụ tiêu thụ tài nguyên ngoài tầm kiểm soát.
Giới hạn độ sâu ủy quyền là gì
Hermes Agent giới hạn độ sâu ủy quyền ở 2 cấp độ: cha mẹ -> con. Một subagent không thể sinh ra một subagent khác.
Tại sao? Bởi vì việc ủy quyền đệ quy dễ dàng vượt khỏi tầm kiểm soát. Một cấp độ ủy quyền đã bao gồm phần lớn các tình huống.
Mô hình tinh thần tối thiểu
Parent agent
|
| 1. Model calls the delegate_task tool
| passing goal + context + toolsets
v
Subagent (independent messages, restricted toolset)
|
| 2. Reads files / searches / executes commands in its own context
| All intermediate steps stay in the subagent's messages
v
Subagent returns final reply
|
| 3. Parent agent receives just a tool_result
| containing the subagent's final reply text
v
Parent agent continues (context still clean)
Chỉ có một điểm mấu chốt:
Không có bước trung gian nào của subagent được chuyển ngược lại vào tin nhắn của agent mẹ. agent gốc chỉ nhận được kết quả cuối cùng.
Cấu trúc dữ liệu chính
1. Lược đồ công cụ delegate_task
{
"name": "delegate_task",
"description": "Delegate a task to a subagent with isolated context",
"parameters": {
"goal": "str -- goal description for the subtask",
"context": "str -- context information for the subagent (optional)",
"toolsets": "list -- available toolsets for the subagent (optional)",
},
}
Mô hình bắt đầu ủy quyền bằng cách gọi công cụ này.
2. Công cụ bị chặn đối với agent phụ
DELEGATE_BLOCKED_TOOLS = [
"delegate_task", # No recursive delegation
"clarify", # Cannot ask user questions (subagent has no user interaction channel)
"memory", # Cannot modify memory (prevent subtask side effects)
"send_message", # Cannot send messages to user
"execute_code", # Security restriction
]
Tại sao lại hạn chế những điều này?
delegate_task: Ngăn chặn sự tái diễnclarify: agent phụ không có kênh tương tác với người dùng; nó không thể tạm dừng để hỏi người dùngmemory: Nhiệm vụ con chỉ thực hiện một lần; chúng không nên tạo ra tác dụng phụ dai dẳngsend_message: subagent không được bỏ qua agent gốc để nói chuyện trực tiếp với người dùng
3. Bộ công cụ mặc định cho subagent
DEFAULT_TOOLSETS = ["terminal", "file", "web"]
Chỉ có những khả năng cơ bản nhất: chạy lệnh, đọc/ghi tệp, tìm kiếm. Đủ, nhưng không vượt quá.
4. Đoàn đợt
agent Hermes hỗ trợ điều động song song tối đa 3 agent phụ:
# Single task
delegate_task(goal="Research Python 3.12 new features")
# Batch tasks (up to 3 in parallel)
delegate_task(tasks=[
{"goal": "Research Python 3.12 new features"},
{"goal": "Check current test coverage"},
{"goal": "Read the last 10 entries from CHANGELOG"},
])
Sử dụng chế độ hàng loạt ThreadPoolExecutor để thực hiện song song, trả về kết quả theo thứ tự đầu vào.
Thực hiện tối thiểu
Bước 1: Đăng ký công cụ delegate_task
from tools.registry import registry
def handle_delegate(args, **kwargs):
goal = args["goal"]
context = args.get("context", "")
toolsets = args.get("toolsets", ["terminal", "file", "web"])
# Build the subagent
child = build_child_agent(goal, context, toolsets, kwargs)
# Execute
result = child.run_conversation(goal)
return result["final_response"]
registry.register(
name="delegate_task",
toolset="agent",
schema={...},
handler=handle_delegate,
)
Bước 2: Xây dựng subagent
def build_child_agent(goal, context, toolsets, parent_kwargs):
# Subagent system prompt: only includes task goal and context
child_system_prompt = f"""You are an assistant focused on a single task.
Task goal: {goal}
Context: {context}
Return the result directly when done. Do not ask the user questions."""
# Reuse the parent agent's API configuration
child = AIAgent(
base_url=parent_kwargs["base_url"],
api_key=parent_kwargs["api_key"],
model=parent_kwargs.get("delegation_model", parent_kwargs["model"]),
system_prompt=child_system_prompt,
enabled_toolsets=toolsets,
disabled_tools=DELEGATE_BLOCKED_TOOLS,
max_iterations=30, # Subagent gets a smaller budget
iteration_budget=parent_kwargs["iteration_budget"], # Shares parent budget
)
return child
Bước 3: Xử lý việc khấu trừ ngân sách
# After the subagent finishes
result = child.run_conversation(goal)
# The iterations consumed by the subagent have already been deducted from the shared budget
# When the parent agent continues, the budget automatically reflects the deduction
Đây là phiên bản tối thiểu. agent phụ messages sẽ bị loại bỏ khi hàm trả về -- và đó chính xác là vấn đề.
Lựa chọn thiết kế độc đáo của agent Hermes
1. Mô hình ủy quyền có thể khác nhau
agent phụ có thể sử dụng mô hình rẻ hơn:
# config.yaml
delegation:
model: anthropic/claude-haiku
provider: openrouter
agent mẹ sử dụng Claude Sonnet làm lý do chính trong khi subagent sử dụng Haiku để tìm kiếm và đọc tệp. Điều này có thể làm giảm đáng kể chi phí.
2. Báo cáo tiến độ
Trong khi subagent thực thi, agent mẹ có thể xem nó đang làm gì trong thời gian thực:
+-- [1] web_search "Python 3.12 features"
+-- [1] read_file "requirements.txt"
+-- [1] web_search "Python 3.12 type hints"
Điều này được thực hiện thông qua tool_progress_callback: mỗi khi subagent thực thi một công cụ, nó sẽ gọi lại để cập nhật hiển thị tiến trình của agent mẹ.
Cơ chế cốt lõi: agent gốc không thăm dò tiến trình của subagent trong một vòng lặp. Thay vào đó, mỗi khi subagent thực thi một công cụ, nó đẩy tiến tới lớp hiển thị của agent mẹ thông qua chức năng gọi lại được đưa vào tại thời điểm tạo. Đây là mẫu gọi lại sự kiện/mẫu người quan sát cổ điển -- không cần thăm dò ý kiến.
Đây là một ví dụ tối thiểu minh họa toàn bộ quy trình:
# 1. Define "what to do when progress arrives"
def on_progress(event_type, tool_name, preview, args):
print(f"Subagent is using: {tool_name} -- {preview}")
# 2. Inject this function when creating the subagent
child = AIAgent(
tool_progress_callback=on_progress, # <-- inject callback
)
# 3. Internally, the subagent calls this callback before each tool execution
# (This code lives in AIAgent._handle_tool_calls -- you don't write it)
# Pseudocode:
for tool in tools_to_run:
self.tool_progress_callback("tool.started", tool.name, tool.preview, tool.args)
result = self.execute(tool) # Actually execute the tool
self.tool_progress_callback("tool.completed", tool.name, ...)
Toàn bộ quá trình gồm ba bước: xác định lệnh gọi lại -> đưa vào subagent -> subagent tự động kích hoạt nó trong khi thực thi. agent mẹ không cần thăm dò ý kiến và subagent không cần biết ai nhìn thấy tiến trình -- nó chỉ gọi lệnh gọi lại. Việc in ra terminal hay gửi tới Telegram đều được xác định bằng cách triển khai lệnh gọi lại.
Nhưng tại sao hàm gọi lại có thể hiển thị mọi thứ trên giao diện của agent gốc? Câu trả lời là các bao đóng. Hàm gọi lại chụp đối tượng hiển thị của agent gốc tại thời điểm tạo. Bất kể ai gọi nó sau, nó vẫn hoạt động trên cùng một đối tượng:
# Parent agent side: create the callback
def make_callback(parent_spinner):
"""spinner is captured by the closure -- the function remembers its creation environment"""
def callback(tool_name):
# Even though this function will be called inside the subagent,
# parent_spinner is the parent agent's object, captured at creation time
parent_spinner.print_above(f"+-- {tool_name}")
return callback
Dòng thời gian:
1. Parent agent creates spinner (displayed on parent's terminal)
spinner = KawaiiSpinner("Subagent working...")
2. Parent agent creates callback, wrapping its own spinner inside
cb = make_callback(spinner) # <-- spinner captured by closure
3. Parent agent injects the callback into the subagent
child = AIAgent(tool_progress_callback=cb)
4. Subagent calls cb("web_search") when executing a tool
-> cb internally runs spinner.print_above("+-- web_search")
-> spinner belongs to the parent agent! So it prints on the parent's terminal
subagent gọi một hàm, nhưng hàm đó hoạt động trên đối tượng của agent mẹ. Các agent cha và con chạy trong cùng một tiến trình, chia sẻ bộ nhớ, do đó spinner được nắm bắt bởi việc đóng cửa chính là công cụ quay vòng mà agent mẹ đang hiển thị. Không cần giao tiếp xuyên suốt quá trình.
3. Cơ chế nhịp tim
Cơ chế nhịp tim giải quyết một vấn đề rất cụ thể: Cổng có "thời gian chờ không hoạt động" (mặc định là 30 phút). Nếu agent gốc không hiển thị hoạt động nào trong thời gian quá dài, Gateway sẽ coi như agent đó bị kẹt và tắt agent đó.
Trong quá trình hoạt động bình thường, agent mẹ cập nhật dấu thời gian hoạt động của nó (_last_activity_ts) mỗi khi nó gọi API hoặc thực thi một công cụ. Nhưng sau khi nó gửi một subagent, agent gốc tự dừng lại và chờ kết quả -- nó không còn gọi bất kỳ công cụ nào nữa. Từ góc nhìn của Gateway, agent mẹ "không có hoạt động nào".
Vào thời điểm subagent kết thúc (có thể mất vài phút), Cổng có thể đã giết agent mẹ.
Giải pháp là một nhịp tim -- một chuỗi nền có nội dung "Tôi vẫn còn sống" thay mặt cho agent mẹ cứ sau 30 giây:
# Essentially a loop that updates the parent agent's activity timestamp every 30 seconds
def _heartbeat_loop():
while not stop_event.wait(30): # Wake up every 30 seconds
parent_agent._touch_activity("subagent still working")
# ^ This updates _last_activity_ts = time.time()
# Gateway sees the timestamp updating and won't kill the parent
# Start the background thread
heartbeat_thread = threading.Thread(target=_heartbeat_loop, daemon=True)
heartbeat_thread.start()
# Subagent starts working
result = child.run_conversation(goal)
# Subagent is done, stop the heartbeat
stop_event.set()
Đó là tất cả những gì cần làm. Nhịp tim = một chủ đề nền chạy _last_activity_ts = time.time() cứ sau 30 giây, được thiết kế để đánh lừa khả năng phát hiện không hoạt động của Gateway để nó biết "agent gốc không bị kẹt, nó chỉ đang đợi subagent."
4. Cách ly thông tin xác thực
subagent có thể sử dụng các thông tin xác thực API khác nhau:
_resolve_delegation_credentials() # Check delegation config
# If delegation config has its own provider/api_key, use those
# Otherwise fall back to the parent agent's credentials
Điều này rất hữu ích trong các tình huống doanh nghiệp: agent chính sử dụng khóa API cao cấp trong khi các subagent sử dụng khóa có giới hạn tỷ lệ.
Cách nó kết nối với vòng lặp chính
Logic ủy nhiệm không nằm trong vòng lặp lõi. Nó được đăng ký như một công cụ thông thường trong hệ thống công cụ.
Core loop
|
| Model decides to call delegate_task
v
dispatch("delegate_task", args)
|
v
delegate_task handler
|
| Creates child AIAgent
| Calls child.run_conversation()
| Subagent runs its own loop internally
| Returns the final result
v
tool_result written back to parent agent's messages
|
v
Core loop continues
Từ quan điểm của vòng lặp, delegate_task không khác gì web_search -- cả hai đều là lệnh gọi công cụ. Sự khác biệt duy nhất là nó mất nhiều thời gian hơn để thực thi và phức tạp hơn trong nội bộ.
Những sai lầm phổ biến nhất của người mới bắt đầu
1. Cho cả agent phụ
Ủy quyền đệ quy = mất kiểm soát. agent Hermes giới hạn độ sâu ở mức 2 (MAX_DEPTH = 2); các subagent không thể sinh ra các subagent khác.
2. Đưa toàn bộ thông điệp của subagent về cho agent mẹ
Các bước trung gian của subagent không được quay trở lại agent mẹ. Chỉ trả lại văn bản trả lời cuối cùng là đủ. Nếu không, bạn sẽ đánh bại mục đích cô lập bối cảnh.
3. Cung cấp ngân sách riêng cho agent phụ
agent phụ nên chia sẻ ngân sách của agent mẹ. Nếu mỗi agent phụ độc lập nhận được 90 cuộc gọi thì 3 cuộc gọi chạy song song có nghĩa là 270 cuộc gọi -- chi phí và thời gian sẽ vượt khỏi tầm kiểm soát.
4. Để subagent sửa đổi bộ nhớ
Các chất phụ là dùng một lần. Nếu một người có thể sửa đổi MEMORY.md, hai agent con song song có thể ghi đồng thời vào cùng một tệp - gây ra xung đột. Việc sửa đổi bộ nhớ chỉ nên được thực hiện bởi agent gốc.
5. Không báo cáo tiến độ
Người dùng đợi 30 giây mà không có phản hồi trên màn hình và cho rằng hệ thống bị hỏng. Ngay cả khi agent phụ bận, bạn vẫn cần cho người dùng biết điều gì đang xảy ra.
Phạm vi của chương này
Chương này trình bày kỹ lưỡng ba điều:
- Cách ly bối cảnh -- subagent có các thông báo độc lập; các bước trung gian không quay trở lại agent mẹ
- Hạn chế công cụ -- subagent không thể ủy quyền, sửa đổi bộ nhớ hoặc đặt câu hỏi cho người dùng
- Chia sẻ ngân sách -- agent phụ sử dụng từ ngân sách của agent chính, không phải ngân sách riêng
Các chủ đề bị trì hoãn:
- Chi tiết nhóm chủ đề để ủy quyền hàng loạt -> tối ưu hóa sản xuất
- Chi tiết cụ thể về việc thực hiện nhịp tim -> chương Cổng (
s12) - Hoàn thiện logic định tuyến thông tin xác thực -> Chương cấu hình (
s11)
Nếu người đọc có thể đạt được "agent mẹ gọi delegate_task, subagent sẽ hoàn thành nhiệm vụ trong một bối cảnh riêng biệt và chỉ văn bản kết quả mới quay trở lại agent mẹ", thì chương này đã phục vụ được mục đích của nó.
Sau chương này, bạn sẽ có thể trả lời
- Tại sao lại cần agent con thay vì để agent mẹ trực tiếp thực hiện mọi việc?
- Mối quan hệ giữa tin nhắn của agent con và tin nhắn của agent mẹ là gì?
- Tại sao subagent không sử dụng được công cụ delegate_task và bộ nhớ?
- Chia sẻ ngân sách lặp lại có nghĩa là gì?
- Cơ chế ủy quyền hoạt động ở tầng nào của kiến trúc hệ thống? Nó có sửa đổi vòng lặp lõi không?
Trong một câu: subagent của agent Hermes là một phiên bản AIAgent tạm thời với các thông điệp độc lập của riêng nó. Nó chia sẻ ngân sách lặp lại và cấu hình API của agent mẹ. Sau khi hoàn thành nhiệm vụ, nó chỉ trả về văn bản kết quả -- không có bước trung gian nào làm ô nhiễm bối cảnh của agent mẹ.