手搓Claude Code-第六章 subagent

手搓Claude Code-第六章 subagent

写在前面

  第六章开始事情变得有趣了起来,我们正式进入多agent的主题,前面五章全是一个agent在单打独斗,而真实的复杂的环境中往往需要多个agent协作来解决一个问题。就比如上一章节最后的时候,我们给agent单独跑了一个任务,但是发现整个执行非常慢。如果我们引入了多个agent,并发的处理多个不同的小任务,从而完成整个目标,是不是就会快了很多。当然,本章不涉及并发处理,仅仅介绍如何引入subagent去帮助我们的主agent处理任务的过程。完整代码见:

https://github.com/shareAI-lab/learn-claude-code/blob/main/s06_subagent/code.py

  我们的任务是:

  1,加入subagent,帮助处理多任务操作,体会整个构建。

  2,跑几个任务,观察多个agent之间协作的偏好。


一、subagent

  在开始之前,我想强调一点是,千万不要被这个sub迷惑了,其实多个agent的能力是一样的,因为都是调的同一个api嘛,当然也可调不同的api不同能力的模型。换句话说,同一个api下,agent能力的差别取决于system prompt的构造。

  了解这些后,我们开始模仿刚开始写一个agent的所有逻辑,去写subagent。

  先写system prompt,再修改下主agent的system prompt

python 复制代码
SUB_SYSTEM = (
    f"You are a coding agent at {WORKDIR}. "
    "Complete the task you were given, then return a concise summary. "
    "Do not delegate further."
)

SYSTEM = (
    f"You are a coding agent at {WORKDIR}. "
    "For complex sub-problems, use the task tool to spawn a subagent."
)

  再定义下subagent的工具和分发表。

python 复制代码
SUB_TOOLS = [
    {"name": "bash", "description": "Run a shell command.",
     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
    {"name": "read_file", "description": "Read file contents.",
     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
    {"name": "write_file", "description": "Write content to a file.",
     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
    {"name": "edit_file", "description": "Replace exact text in a file once.",
     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
    {"name": "glob", "description": "Find files matching a glob pattern.",
     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
]
# NO "task" tool --- prevent recursive spawning

SUB_HANDLERS = {
    "bash": run_bash, "read_file": run_read, "write_file": run_write,
    "edit_file": run_edit, "glob": run_glob,
}

  想象一下,主agent怎么向subagent分发消息呢?那依然是通过block,就像用户和agent交互一样。不同的是用户直接输入的是自然语言,所以我们应该写一个函数用来将主block的内容抽取出来,返还给subagent。

  如果传入的是字符串或者单个对象,直接转化为字符串并返回;如果传入的列表,那么返回content中所有type=text中的元素。其实本质就是把含有信息的字符串返还回去,也就是主agent要给subagent传递的信息,不要传些乱七八糟的东西。

python 复制代码
def extract_text(content) -> str:
    """Extract text from message content blocks."""
    if not isinstance(content, list):
        return str(content)
    return "\n".join(getattr(b, "text", "") for b in content if getattr(b, "type", None) == "text")

  接着,正式编写subagent的主体。这里shareai也把它写进了一个函数,因为整个subagent其实是作为主agent的一个工具函数来实现的,而subagent也有自己的工具函数,这两者的工具函数,可以一样,也可以不同。直接贴出代码,我们一步步看。

python 复制代码
def spawn_subagent(description: str) -> str:
    """设计一个带有单独上下文的子智能体,然后只返回最终结果的摘要信息"""
    print(f"\n\033[35m[Subagent spawned]\033[0m")
    # 直接将这个description作为用户发起的对话内容,传进去
    messages = [{"role": "user", "content": description}]  # fresh context

    for _ in range(30):  # 最多对话30轮,一种安全限制
    		  # 和主agent几乎没差别,只是少了todolist
        response = client.messages.create(
            model=MODEL, system=SUB_SYSTEM,
            messages=messages, tools=SUB_TOOLS, max_tokens=8000,
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            break
        results = []
        for block in response.content:
            if block.type == "tool_use":
                # 子智能体也可以挂钩子
                blocked = trigger_hooks("PreToolUse", block)
                if blocked:
                    results.append({"type": "tool_result", "tool_use_id": block.id,
                                    "content": str(blocked)})
                    continue
                handler = SUB_HANDLERS.get(block.name)
                output = handler(**block.input) if handler else f"Unknown: {block.name}"
                trigger_hooks("PostToolUse", block, output)
                print(f"  \033[90m[sub] {block.name}: {str(output)[:100]}\033[0m")
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": output})
        messages.append({"role": "user", "content": results})

    """
	到这里,上面的循环就跑完了。现在的messages中有几种情况:
	1,没跑到三十轮,因为非工具调用退出了,那我们下面去拿调用后的结果(最理想的情况);
	2,跑到了三十轮,全是工具调用,交给subagent的任务还没有结束,那我们下面去拿最后一次工具调用的结果;
	3,第三十轮刚好是最后一次工具调用,因为有一轮的时间差,所以取最后一次工具调用的结果即可;
	4,模型抽风了,没给出正确的格式,导致extract_text找不到text中的内容,这是可能的。
	"""
    result = extract_text(messages[-1]["content"])
    # 如何4这种情况出现了,那就进入循环当中去。依然去取最后一次模型的回复结果。
    if not result:
        # last message is tool_result, look backwards for assistant text
        for msg in reversed(messages):
            if msg["role"] == "assistant":
                result = extract_text(msg["content"])
                if result:
                    break
        if not result:
            result = "Subagent stopped after 30 turns without final answer."
    print(f"\033[35m[Subagent done]\033[0m")
    return result  # only summary, entire message history discarded

  可以看到,其实最后抽取摘要还是比较复杂的,读者可以尝试修改,试试更简单高效的摘要办法,让子智能体无论有没有跑完,都返还给主agent有效的信息。比如我自己的改进做法,让子智能体自己去总结摘要,代码贴在最后了

  最后把subagent作为工具注册进去。

python 复制代码
# Add task tool to parent's tools
TOOLS.append({
    "name": "task",
    "description": "Launch a subagent to handle a complex subtask. Returns only the final conclusion.",
    "input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]},
})
TOOL_HANDLERS["task"] = spawn_subagent

  主agent的loop不需要改动,我们来给一些任务去试试。


二、给些任务

  主要观察Subagent spawned,可以看到子智能的探索过程:多次使用glob去搜索相关的配置文件,结果都是(no matches)说明找不到;然后ls查看目录结构,最后找到一个像配置文件的hello.py,读它以后发现就是简单的python脚本,最后得出的结论是,没有使用任何正式的测试框架。结果与预期是完全一致的!

python 复制代码
s05 >> Use a subtask to find what testing framework this project uses
[HOOK] UserPromptSubmit: working in /Users/bx/Documents/coding/learn_cladudecode
[HOOK] task

[Subagent spawned]
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] glob
  [sub] glob: (no matches)
[HOOK] bash
  [sub] bash: total 16
drwxrwxrwx@ 10 bx  staff   320  6月 25 15:06 .
drwxrwxrwx@  9 bx  staff   288  6月 25 15:05 .
[HOOK] bash
  [sub] bash: /Users/bx/Documents/coding/learn_cladudecode/s05_todo_write/hello.py
/Users/bx/Documents/coding/lear
[HOOK] read_file
  [sub] read_file: import subprocess
import os

TOOLS = [{
    "name":"bash",
    "description": "run a shell command."
[HOOK] read_file
  [sub] read_file: from pathlib import Path # 

safe_path = Path.cwd() / "s01_agentLoop\.env"
print(safe_path)

text = 
[HOOK] read_file
  [sub] read_file: double = lambda x : x**2

print(double(3))

get_path = lambda args: args.get("path", "")


cbeck = l
[HOOK] read_file
  [sub] read_file: import json, ast

todos = "({'content':'test'},)"

# 会报错
# print(json.loads(todos))


print(ast.lite
[HOOK] read_file
  [sub] read_file: class Block:
    def __init__(self, type_, text):
        self.type = type_
        self.text = text
[HOOK] read_file
  [sub] read_file: import os
import subprocess
from anthropic import Anthropic
from dotenv import load_dotenv

load_dot
[HOOK] read_file
  [sub] read_file: 我是小明,来自三年二班。
[HOOK] read_file
  [sub] read_file: import os, ast, json
import subprocess
from pathlib import Path 
from anthropic import Anthropic
fro
[HOOK] read_file
  [sub] read_file: #!/usr/bin/env python3
"""
s05: TodoWrite --- add a planning tool on top of s04 hooks.

  +---------+ 
[HOOK] read_file
  [sub] read_file: {
    "python-envs.defaultEnvManager": "ms-python.python:conda",
    "python-envs.defaultPackageMana
[HOOK] read_file
  [sub] read_file: {
    "recommendations": [
        "doubao.doubao-app-share-vscode-plugin"
    ]
}
[Subagent done]
[HOOK] Stop: session used 1 tool calls
## Finding: No Formal Testing Framework

This project **does not use any formal testing framework**. Here's the summary:

- **Language**: Python (not Node.js --- no `package.json`)
- **Test files**: Plain `test.py` scripts found in directories like `s01_agentLoop/`, `s02_tooluse/`, `s03_permission/`, and `s05_todo_write/`
- **No testing libraries**: No `pytest`, `unittest`, `nose`, or any other framework is imported or configured
- **No config files**: No `pytest.ini`, `conftest.py`, `pyproject.toml`, or `setup.cfg` with test settings
- **How tests work**: They are simple Python scripts that print output and are run manually with `python test.py` --- no assertions, no test discovery, no test runner

  再试试创造一个文件,创造一个slugify函数用于将文本转化为URL友好的slug格式。

  这个过程完美展示了协作过程,依旧观察子智能体的执行部分。子智能体首先创建文件,然后简单地测试函数运用bash验证,最后返回成功结果给主智能体;主智能体收到后先读文件检查文件内容是否正确,然后运用测试案例去验证功能,最后更新进度追踪;这里主智能体还使用了todolist去完美展现了整个任务的协作流程。

  最后那个❌并非报错,而是主agent提醒我们:shell 转义问题------$$$ 被解释为 shell 变量。这不是 slugify() 函数本身的 bug。

python 复制代码
s05 >> Use a task to create s06_subagent/string_tools.py with a slugify(text: str) function, then verify it from the parent agent
[HOOK] UserPromptSubmit: working in /Users/bx/Documents/coding/learn_cladudecode
[HOOK] task

[Subagent spawned]
[HOOK] bash
  [sub] bash: (no output)
[HOOK] write_file
  [sub] write_file: Wrote 1400 bytes to /Users/bx/Documents/coding/learn_cladudecode/s06_subagent/string_tools.py
[HOOK] bash
  [sub] bash: 'Hello World!'                                -> 'hello-world'
'  Python   &    RegEx  '            
[Subagent done]
[HOOK] read_file
[HOOK] bash
[HOOK] todo_write

## Current Tasks
  [✓] Create s06_subagent/string_tools.py with slugify() via subagent
  [▸] Verify slugify() from parent agent
  [ ] Fix test verification (shell escaping issue with $$$)
[HOOK] bash
[HOOK] todo_write

## Current Tasks
  [✓] Create s06_subagent/string_tools.py with slugify() via subagent
  [✓] Verify slugify() from parent agent
  [✓] Fix test verification (shell escaping issue with $$$)
[HOOK] Stop: session used 7 tool calls
All ✅! Here's a summary of what happened:

1. **Subagent** created `s06_subagent/string_tools.py` with a `slugify(text: str) -> str` function that:
   - Lowercases input
   - Replaces spaces/special chars with hyphens
   - Removes non-alphanumeric characters (except hyphens)
   - Collapses multiple hyphens and strips edges

2. **Parent agent verification** --- I read the file and ran all 7 test cases. All passed:
   - `'Hello World!'` → `'hello-world'`
   - `'  Python   &    RegEx  '` → `'python-regex'`
   - `"It's a beautiful day!!!"` → `'it-s-a-beautiful-day'`
   - `'---leading and trailing---'` → `'leading-and-trailing'`
   - `'Multiple   spaces   and $$$ symbols'` → `'multiple-spaces-and-symbols'`
   - `'Hello---World'` → `'hello-world'`
   - `''` → `''`

> **Note**: The initial run had one ❌ due to a shell escaping issue where `$$$` was interpreted as a shell variable --- not a bug in the `slugify()` function itself. Using a heredoc (`<< 'PYEOF'`) fixed it.

总结

  建议大家自己动手执行一遍,你会发现任务执行的速度快了许多。其实我们目前只使用到了一个子智能体去完成子任务,还没有出现多个子智能体完成不同的子任务的情况,到时就需要考虑任务的委派,工程进度的有向无环图,读写有可能交叉并行,事情就有意思了起来。

  这样做的缺点也是有的,后台token的消耗很快,而且如果整个控制层面也显得十分麻烦,不过现在agent infra才刚刚开始,再过段时间,也许都已经集成到了服务当中了

:改进spawn_subagent函数,让子智能体自己去总结摘要。

python 复制代码
```python
def spawn_subagent(description: str) -> str:
    print(f"\n\033[35m[Subagent spawned]\033[0m")
    messages = [{"role": "user", "content": description}]

    for _ in range(30):
        response = client.messages.create(
            model=MODEL, system=SUB_SYSTEM,
            messages=messages, tools=SUB_TOOLS, max_tokens=8000,
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            break
        results = []
        for block in response.content:
            if block.type == "tool_use":
                blocked = trigger_hooks("PreToolUse", block)
                if blocked:
                    results.append({"type": "tool_result", "tool_use_id": block.id,
                                    "content": str(blocked)})
                    continue
                handler = SUB_HANDLERS.get(block.name)
                output = handler(**block.input) if handler else f"Unknown: {block.name}"
                trigger_hooks("PostToolUse", block, output)
                print(f"  \033[90m[sub] {block.name}: {str(output)[:100]}\033[0m")
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": output})
        messages.append({"role": "user", "content": results})
    
    # 让子智能体自己总结
    messages.append({
        "role": "user", 
        "content": "请用一段话总结你刚才的工作成果,只返回最终结果,不要包含工具调用细节。"
    })
    
    summary_response = client.messages.create(
        model=MODEL, system=SUB_SYSTEM,
        messages=messages, tools=[],  # 不提供工具,强制纯文本回复
        max_tokens=2000,
    )
    
    result = extract_text(summary_response.content)
    print(f"\033[35m[Subagent done]\033[0m")
    return result or "Subagent completed but no summary generated."
相关推荐
shushangyun_2 小时前
2026智能采购商城系统选型指南:如何引领企业数字化采购升级
java·大数据·数据库·人工智能·机器学习
元Y亨H2 小时前
Python - FastAPI 全方位介绍
python·fastapi
沐籽李2 小时前
Roche 团队如何设计酸性 pH 激活的 anti-CD3 抗体
人工智能·aidd·抗体设计·ph敏感抗体
aaaa954726652 小时前
2026最新5款AI编程工具平替实测合集|Claude Code低成本迭代深度对比
人工智能·microsoft
Tairitsu_H2 小时前
[LC优选算法#17] 链表 | 合并 K 个升序链表 | K个⼀组翻转链表
数据结构·算法·链表
Larcher3 小时前
从零实现 RAG 语义搜索——让 AI 听懂你的"弦外之音"
人工智能
Larcher3 小时前
从零搭建文件读取 MCP 服务——让 AI 拥有读文件的能力
人工智能
西瓜橙3 小时前
别再让 AI 一把梭:我用 Loop Engineering 把 AI编程 拽进可验证闭环
人工智能
艾莉丝努力练剑3 小时前
OpenCode AI 编程:Ubuntu 24.04 环境安装与使用指南
linux·服务器·网络·人工智能·tcp/ip·ubuntu