手搓Claude Code-第六章 subagent

发布时间:2026/7/6 13:44:42
手搓Claude Code-第六章 subagent 手搓Claude Code-第六章 subagent写在前面一、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 promptSUB_SYSTEM(fYou are a coding agent at{WORKDIR}. Complete the task you were given, then return a concise summary. Do not delegate further.)SYSTEM(fYou are a coding agent at{WORKDIR}. For complex sub-problems, use the task tool to spawn a subagent.)再定义下subagent的工具和分发表。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 spawningSUB_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中所有typetext中的元素。其实本质就是把含有信息的字符串返还回去也就是主agent要给subagent传递的信息不要传些乱七八糟的东西。defextract_text(content)-str:Extract text from message content blocks.ifnotisinstance(content,list):returnstr(content)return\n.join(getattr(b,text,)forbincontentifgetattr(b,type,None)text)接着正式编写subagent的主体。这里shareai也把它写进了一个函数因为整个subagent其实是作为主agent的一个工具函数来实现的而subagent也有自己的工具函数这两者的工具函数可以一样也可以不同。直接贴出代码我们一步步看。defspawn_subagent(description:str)-str:设计一个带有单独上下文的子智能体然后只返回最终结果的摘要信息print(f\n\033[35m[Subagent spawned]\033[0m)# 直接将这个description作为用户发起的对话内容传进去messages[{role:user,content:description}]# fresh contextfor_inrange(30):# 最多对话30轮一种安全限制# 和主agent几乎没差别只是少了todolistresponseclient.messages.create(modelMODEL,systemSUB_SYSTEM,messagesmessages,toolsSUB_TOOLS,max_tokens8000,)messages.append({role:assistant,content:response.content})ifresponse.stop_reason!tool_use:breakresults[]forblockinresponse.content:ifblock.typetool_use:# 子智能体也可以挂钩子blockedtrigger_hooks(PreToolUse,block)ifblocked:results.append({type:tool_result,tool_use_id:block.id,content:str(blocked)})continuehandlerSUB_HANDLERS.get(block.name)outputhandler(**block.input)ifhandlerelsefUnknown:{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中的内容这是可能的。 resultextract_text(messages[-1][content])# 如何4这种情况出现了那就进入循环当中去。依然去取最后一次模型的回复结果。ifnotresult:# last message is tool_result, look backwards for assistant textformsginreversed(messages):ifmsg[role]assistant:resultextract_text(msg[content])ifresult:breakifnotresult:resultSubagent stopped after 30 turns without final answer.print(f\033[35m[Subagent done]\033[0m)returnresult# only summary, entire message history discarded可以看到其实最后抽取摘要还是比较复杂的读者可以尝试修改试试更简单高效的摘要办法让子智能体无论有没有跑完都返还给主agent有效的信息。比如我自己的改进做法让子智能体自己去总结摘要代码贴在最后了。最后把subagent作为工具注册进去。# Add task tool to parents toolsTOOLS.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脚本最后得出的结论是没有使用任何正式的测试框架。结果与预期是完全一致的s05Use a subtask to find what testing framework this project uses[HOOK]UserPromptSubmit:workingin/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:total16drwxrwxrwx10bx staff3206月2515:06.drwxrwxrwx9bx staff2886月2515: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:importsubprocessimportos TOOLS[{name:bash,description:run a shell command.[HOOK]read_file[sub]read_file:frompathlibimportPath#safe_pathPath.cwd()/s01_agentLoop\.envprint(safe_path)text[HOOK]read_file[sub]read_file:doublelambdax:x**2print(double(3))get_pathlambdaargs:args.get(path,)cbeckl[HOOK]read_file[sub]read_file:importjson,ast todos({content:test},)# 会报错# print(json.loads(todos))print(ast.lite[HOOK]read_file[sub]read_file:classBlock:def__init__(self,type_,text):self.typetype_ self.texttext[HOOK]read_file[sub]read_file:importosimportsubprocessfromanthropicimportAnthropicfromdotenvimportload_dotenv load_dot[HOOK]read_file[sub]read_file:我是小明来自三年二班。[HOOK]read_file[sub]read_file:importos,ast,jsonimportsubprocessfrompathlibimportPathfromanthropicimportAnthropic 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 used1tool calls## Finding: No Formal Testing FrameworkThis project**doesnotuseanyformal testing framework**.Heres the summary:-**Language**:Python(notNode.js — no package.json)-**Test files**:Plain test.py scripts foundindirectories like s01_agentLoop/,s02_tooluse/,s03_permission/,ands05_todo_write/-**No testing libraries**:No pytest,unittest,nose,oranyother frameworkisimportedorconfigured-**No config files**:No pytest.ini,conftest.py,pyproject.toml,orsetup.cfgwithtest settings-**How tests work**:They are simple Python scripts thatprintoutputandare run manuallywithpython test.py — no assertions,no test discovery,no test runner再试试创造一个文件创造一个slugify函数用于将文本转化为URL友好的slug格式。这个过程完美展示了协作过程依旧观察子智能体的执行部分。子智能体首先创建文件然后简单地测试函数运用bash验证最后返回成功结果给主智能体主智能体收到后先读文件检查文件内容是否正确然后运用测试案例去验证功能最后更新进度追踪这里主智能体还使用了todolist去完美展现了整个任务的协作流程。最后那个❌并非报错而是主agent提醒我们shell 转义问题——$$$ 被解释为 shell 变量。这不是 slugify() 函数本身的 bug。s05Use a task to create s06_subagent/string_tools.pywitha slugify(text:str)function,then verify itfromthe parent agent[HOOK]UserPromptSubmit:workingin/Users/bx/Documents/coding/learn_cladudecode[HOOK]task[Subagent spawned][HOOK]bash[sub]bash:(no output)[HOOK]write_file[sub]write_file:Wrote1400bytesto/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.pywithslugify()via subagent[▸]Verify slugify()fromparent agent[]Fix test verification(shell escaping issuewith$$$)[HOOK]bash[HOOK]todo_write## Current Tasks[✓]Create s06_subagent/string_tools.pywithslugify()via subagent[✓]Verify slugify()fromparent agent[✓]Fix test verification(shell escaping issuewith$$$)[HOOK]Stop:session used7tool calls All ✅! Heres a summary of what happened:1.**Subagent**created s06_subagent/string_tools.pywitha slugify(text:str)-str function that:-Lowercasesinput-Replaces spaces/special charswithhyphens-Removes non-alphanumeric characters(excepthyphens)-Collapses multiple hyphensandstrips edges2.**Parent agent verification**— I read thefileandranall7test cases.All passed:-Hello World! → hello-world- Python RegEx → python-regex-Its 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 interpretedasa shell variable —nota buginthe slugify() function itself.Using a heredoc(PYEOF)fixed it.总结建议大家自己动手执行一遍你会发现任务执行的速度快了许多。其实我们目前只使用到了一个子智能体去完成子任务还没有出现多个子智能体完成不同的子任务的情况到时就需要考虑任务的委派工程进度的有向无环图读写有可能交叉并行事情就有意思了起来。这样做的缺点也是有的后台token的消耗很快而且如果整个控制层面也显得十分麻烦不过现在agent infra才刚刚开始再过段时间也许都已经集成到了服务当中了。附改进spawn_subagent函数让子智能体自己去总结摘要。pythondefspawn_subagent(description:str)-str:print(f\n\033[35m[Subagent spawned]\033[0m)messages[{role:user,content:description}]for_inrange(30):responseclient.messages.create(modelMODEL,systemSUB_SYSTEM,messagesmessages,toolsSUB_TOOLS,max_tokens8000,)messages.append({role:assistant,content:response.content})ifresponse.stop_reason!tool_use:breakresults[]forblockinresponse.content:ifblock.typetool_use:blockedtrigger_hooks(PreToolUse,block)ifblocked:results.append({type:tool_result,tool_use_id:block.id,content:str(blocked)})continuehandlerSUB_HANDLERS.get(block.name)outputhandler(**block.input)ifhandlerelsefUnknown:{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_responseclient.messages.create(modelMODEL,systemSUB_SYSTEM,messagesmessages,tools[],# 不提供工具强制纯文本回复max_tokens2000,)resultextract_text(summary_response.content)print(f\033[35m[Subagent done]\033[0m)returnresultorSubagent completed but no summary generated.