ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Claude Code改造为任务调度器:Agent、Supervisor与worktree实战

Claude Code改造为任务调度器:Agent、Supervisor与worktree实战 1. 从写代码到管流程这次改造到底改了什么第一次看到Claude Code 把自己改成了任务调度器这个说法我盯着屏幕愣了几秒。因为在我的认知里Claude Code 一直是个你问它答、你让它改它就改的交互式编码助手本质上还是个被动响应的工具。但任务调度器这个词一出来性质就变了——它意味着这个东西开始有了自己决定下一步做什么的能力。我花了大概两周时间把市面上能找到的相关资料、社区讨论、以及几个开源实现翻了个遍又自己动手搭了一套最小可用的原型。结论是这次改造真正值得看的不是它多了几个功能按钮而是它把Agent、Supervisor、worktree、MCP这几个原本各自为战的概念用一种非常克制的方式串成了一条完整的执行链路。先说清楚这套东西是什么。简单讲它让 Claude Code 从一个单次对话的执行者变成了一个可以接收任务、拆解任务、分派任务、回收结果、再决定下一步的调度中枢。你给它一个相对模糊的目标比如把这个项目的测试覆盖率提到 80%它不会直接开始写代码而是先规划需要先跑一遍覆盖率报告找出没覆盖的模块判断哪些是容易补的、哪些需要重构然后逐个击破。这解决的是什么问题是长链路任务的上下文丢失和状态管理问题。以前你用 Claude Code 做复杂任务得自己记住上一步做了什么、下一步该做什么一旦对话轮次多了模型自己也会忘。现在它把这些状态外化成了可管理的任务队列和 worktree 隔离环境。适合谁来参考我觉得三类人最该看一是已经在用 Claude Code 做日常开发、但总觉得不够自动化的工程师二是正在研究 Agent 框架和编排的开发者三是想理解 MCP 协议在实际项目里怎么落地的人。哪怕你只是想搞清楚agent 开发到底和普通脚本调用有什么区别这套设计也能给你一个很具体的参照。2. 整体设计思路拆解为什么是调度器而不是更聪明的助手2.1 核心矛盾单 Agent 的能力天花板在哪里在动手拆解之前得先想明白一个问题为什么不能让一个足够聪明的 Agent 干完所有事我拿自己踩过的坑举例。之前我试过让 Claude Code 一次性完成重构某个模块 补测试 更新文档三件事。结果它在重构到一半的时候因为上下文里塞了太多文件内容开始出现幻觉式修改——把一个根本没问题的函数改坏了还振振有词地说这是为了统一风格。这就是典型的单 Agent 上下文过载。单 Agent 架构有三个绕不过去的坎上下文窗口是硬约束再大的窗口塞进十几个文件的完整内容后模型对早期信息的注意力就会衰减。这不是模型不行是注意力机制的物理限制。状态无法持久化对话一结束中间过程全丢。下次接着做得重新把背景讲一遍。错误会累积一步错步步错而且模型自己很难发现我上一步其实做错了。所以调度器这个思路的本质是把一个大而复杂的任务拆成多个小而独立的子任务每个子任务交给一个干净的 Agent 实例去执行。调度器自己不干活它只负责决定谁在什么时候干什么。2.2 Supervisor 模式谁来做决策谁来做执行这套设计里最核心的角色叫Supervisor翻译过来就是监督者或者调度者。它和普通 Agent 的区别在于普通 Agent 是执行者拿到任务就开干Supervisor 是决策者它拿到的是任务列表和当前状态输出的是下一步该派谁去做什么。我画不出图也不打算用那种花里胡哨的流程图但你可以这样理解这个结构用户 → Supervisor决策层→ 多个 Worker Agent执行层→ 各自独立的 worktree隔离环境→ 结果回传给 Supervisor → Supervisor 决定下一步这里有个关键设计Supervisor 不直接读代码它只读任务状态和执行结果摘要。这样做的好处是Supervisor 的上下文永远保持精简不会被具体代码细节污染。它就像一个项目经理不需要自己会写每一行代码但要知道每个模块的进度和风险。我实测下来这种分层最大的收益是错误隔离。某个 Worker 把代码改崩了影响范围仅限于它自己的 worktreeSupervisor 收到执行失败的信号后可以决定重试、换方案、或者标记为阻塞。整个流程不会因为一个子任务的失败而全盘崩溃。2.3 worktree 隔离为什么不用分支而用工作树说到隔离很多人第一反应是用 git 分支不就行了。我一开始也这么想直到我同时跑了三个 Worker它们互相把对方的未提交改动覆盖了。git worktree和分支的本质区别在于分支共享同一个工作目录而 worktree 给每个分支分配一个独立的物理目录。这意味着Worker A 在/project-wt-a里改代码Worker B 在/project-wt-b里改代码互不干扰。每个 worktree 有自己的暂存区、自己的未提交状态。可以同时在不同 worktree 里跑测试不会因为文件锁冲突而失败。我用的命令大概是这样# 为任务 A 创建一个独立工作树 git worktree add ../project-wt-task-a -b task-a-branch # 为任务 B 创建另一个 git worktree add ../project-wt-task-b -b task-b-branch这样每个 Agent 实例都被关在自己的目录里它爱怎么折腾怎么折腾。等它干完活Supervisor 再决定是合并、丢弃还是保留这个 worktree。注意worktree 用多了会占磁盘而且git worktree list会越来越长。我的经验是任务完成后一定要及时git worktree remove否则一周下来能攒出几十个目录清理起来很烦。2.4 MCP 的角色让调度器能伸手够到外部工具MCPModel Context Protocol在这套设计里扮演的是能力扩展接口的角色。Supervisor 和 Worker 本身只会读写文件和执行命令但真实任务往往需要调用外部工具——比如查数据库、调 API、操作浏览器、甚至控制 Blender 这种专业软件。MCP 的价值在于它把这些外部能力标准化了。不管你是要接 Playwright 做浏览器自动化还是接 Burp Suite 做安全测试还是接某个内部系统只要对方实现了 MCP ServerAgent 就能用统一的方式调用。我自己的项目里接了一个自定义的 MCP Server用来查询内部的任务管理系统。配置大概长这样{ mcpServers: { task-system: { command: node, args: [./mcp-servers/task-system/index.js], env: { API_TOKEN: your-token-here } } } }配好之后Supervisor 就能在决策时调用query_task_status这类工具把外部系统的状态纳入自己的判断依据。这一步是让调度器真正有用的关键——否则它只能在自己的一亩三分地里打转。3. 核心细节解析与实操要点把设计落地时要盯紧的几个地方3.1 任务拆解的粒度太粗会失控太细会爆炸这是我在实操中踩得最狠的一个坑。第一次跑的时候我让 Supervisor 把一个优化首页加载速度的任务拆解结果它拆出了 47 个子任务其中有一半是检查某个具体文件的某一行。任务队列瞬间爆炸调度开销比实际干活还大。后来我总结出一个经验值单个子任务的预期执行时间控制在 2 到 10 分钟之间。低于 2 分钟的任务合并到相邻任务里高于 10 分钟的任务考虑再拆一层。怎么判断我一般看这个任务需不需要读超过 3 个文件或者改超过 50 行代码。如果需要就说明粒度还是太粗。另外任务描述里一定要包含明确的完成标准。比如不要写优化这个函数而要写把这个函数的时间复杂度从 O(n²) 降到 O(n log n)并保证现有测试全部通过。没有完成标准的任务Worker 会陷入我觉得差不多了的模糊状态。3.2 Worker 的上下文注入给多少信息才算刚好Worker 是实际干活的 Agent它需要知道我要做什么和我有什么约束。但给多了会污染上下文给少了它又做不对。我的做法是给每个 Worker 注入三类信息任务描述要做什么完成标准是什么。相关文件清单只给这个任务真正需要的文件路径不给整个项目结构。约束条件比如不要修改 public API、必须保持现有代码风格、测试必须用现有的测试框架。这里有个细节很关键不要让 Worker 自己去探索项目结构。我试过让 Worker 自己ls和grep去找相关文件结果它花了大量时间在无关目录里乱翻还经常找错文件。正确的做法是 Supervisor 在派发任务时就把文件清单准备好。# 派发任务时的数据结构示例 task { id: task-007, description: 将 utils/parser.py 中的 parse_config 函数改为支持 YAML 格式, files: [utils/parser.py, tests/test_parser.py], constraints: [ 保持函数签名不变, 新增依赖需写入 requirements.txt, 现有 JSON 解析测试必须继续通过 ], worktree: ../project-wt-task-007 }3.3 结果回收与状态更新别让 Supervisor 变成信息垃圾桶Worker 干完活要把结果回传给 Supervisor。这里最容易犯的错是把 Worker 的完整输出原封不动塞给 Supervisor。我一开始就是这么干的结果 Supervisor 的上下文很快就被各种日志、diff、测试输出塞满了决策质量直线下降。正确的做法是结构化摘要。Worker 回传的应该是一个精简的结果对象字段说明示例status执行状态success / failed / blockedsummary一句话总结已支持 YAML 解析新增 3 个测试用例files_changed改动的文件列表[utils/parser.py, requirements.txt]tests_passed测试是否通过trueblockers阻塞原因如有nullnext_suggestionWorker 建议的下一步建议补充 YAML 边界情况测试Supervisor 只看这个摘要需要细节时再按需拉取。这样它的上下文永远保持在可控范围内。3.4 失败重试策略不是所有失败都值得重试Worker 失败是常态关键是怎么处理。我见过有人设置失败就重试 3 次结果一个因为依赖没装而失败的任务重试 3 次还是失败白白浪费了十几分钟。我的策略是按失败类型分流环境类失败依赖缺失、权限不足不重试直接标记为阻塞通知人工处理。逻辑类失败测试没过、代码报错重试 1 次但重试时要换一种思路把上次的失败原因作为新约束注入。超时类失败重试 1 次但要把任务再拆细。冲突类失败worktree 合并冲突不自动重试交给 Supervisor 决策是手动合并还是放弃。实操心得重试时一定要把上次为什么失败告诉 Worker。我试过不带失败信息重试结果 Worker 用一模一样的方式又失败了一次纯属浪费。4. 实操过程与核心环节实现从零搭一个最小可用调度器4.1 环境准备与基础配置先把基础环境搭起来。我用的是 Ubuntu 环境Windows 下用 WSL 也可以但要注意路径映射的问题。# 确认 git 版本支持 worktree2.5 即可 git --version # 创建工作目录结构 mkdir -p ~/agent-scheduler/{tasks,worktrees,logs} cd ~/agent-scheduler # 初始化主项目仓库 git init main-project cd main-project echo # Main Project README.md git add . git commit -m initClaude Code 的安装这里不展开社区里教程很多。重点是要确认它能以非交互模式运行也就是可以通过命令行参数直接传入任务并拿到输出而不是必须开一个对话窗口。# 非交互模式的基本调用形式示意 claude-code --task your task description --output-format json4.2 Supervisor 的核心决策循环Supervisor 的本质是一个循环读状态 → 做决策 → 派任务 → 收结果 → 更新状态 → 再决策。我用 Python 写了一个简化版import json import subprocess from pathlib import Path class Supervisor: def __init__(self, project_root, task_queue_file): self.project_root Path(project_root) self.task_queue json.loads(Path(task_queue_file).read_text()) self.completed [] self.blocked [] def decide_next(self): 决定下一步返回下一个可执行任务或 None 表示完成 for task in self.task_queue: if task[status] pending: # 检查依赖是否满足 if all(dep in [t[id] for t in self.completed] for dep in task.get(depends_on, [])): return task return None def dispatch(self, task): 派发任务给 Worker worktree_path self.project_root.parent / fwt-{task[id]} subprocess.run([ git, worktree, add, str(worktree_path), -b, fbranch-{task[id]} ], cwdself.project_root, checkTrue) # 调用 Worker这里用 Claude Code 的非交互模式 result subprocess.run([ claude-code, --task, task[description], --workdir, str(worktree_path), --output-format, json ], capture_outputTrue, textTrue) return json.loads(result.stdout) def run(self): while True: task self.decide_next() if task is None: break task[status] running result self.dispatch(task) if result[status] success: task[status] done self.completed.append(task) else: task[status] failed self.handle_failure(task, result)这个循环看起来简单但里面有几个关键点依赖检查确保任务按正确顺序执行worktree 创建保证隔离结果解析决定任务状态。4.3 一个完整任务的执行记录我拿一个真实的小任务跑了一遍记录如下初始任务给 main-project 添加一个 CLI 入口支持--version和--help参数Supervisor 拆解结果task-001调研现有项目结构确定 CLI 入口文件位置依赖无task-002实现 CLI 参数解析逻辑依赖task-001task-003编写 CLI 的单元测试依赖task-002task-004更新 README 添加使用说明依赖task-002执行过程task-001 在 worktree 里跑了 3 分钟返回入口文件应放在src/cli.py项目使用 argparse 风格。task-002 拿到这个信息后在独立 worktree 里实现了参数解析改了 2 个文件测试通过。task-003 和 task-004 并行执行各自在自己的 worktree 里干活。全部完成后Supervisor 汇总四个 worktree 的改动生成合并建议。关键观察task-003 和 task-004 并行执行时因为 worktree 隔离两者完全没有互相干扰。如果用的是同一个工作目录task-004 改 README 的时候很可能和 task-003 的测试文件产生冲突。4.4 合并与清理收尾工作不能省所有任务完成后Supervisor 需要把各个 worktree 的改动合并回主分支。这一步我建议不要全自动而是生成一个合并计划让人确认。# 查看所有 worktree 的状态 git worktree list # 查看某个 worktree 的改动 cd ../wt-task-002 git diff main # 确认无误后合并 cd ../main-project git merge branch-task-002合并完成后清理 worktreegit worktree remove ../wt-task-002 git branch -d branch-task-002注意如果某个 worktree 里有未提交的改动git worktree remove会失败。这时候要么先提交要么用--force强制删除但改动会丢。我的习惯是合并前先让 Worker 自己提交一次这样即使强制删除也能从分支里找回。5. 常见问题与排查技巧实录5.1 任务卡住不动先查依赖再查 worktree最常见的问题是任务队列不动了。我遇到过好几次排查下来无非两个原因原因一依赖成环。task-A 依赖 task-Btask-B 又依赖 task-A两个都永远等不到对方完成。排查方法是把任务依赖画成一个有向图看有没有环。我现在的做法是在拆解阶段就做一次拓扑排序检查有环直接报错。原因二worktree 创建失败。比如磁盘满了、路径权限不对、或者分支名冲突。这时候 Supervisor 会一直卡在dispatch那一步。排查方法是手动跑一遍git worktree add看报什么错。现象可能原因排查命令任务队列长时间无变化依赖成环检查 depends_on 字段dispatch 报错worktree 创建失败git worktree add手动测试Worker 无输出非交互模式参数错误单独跑一次 claude-code 命令结果解析失败输出格式不是 JSON检查 --output-format 参数5.2 Worker 改错文件文件清单要精确到路径有一次 Worker 把测试文件改坏了排查发现是 Supervisor 给的文件清单里写的是tests 目录下的相关文件Worker 自己理解成了所有测试文件然后挨个改了一遍。教训是文件清单必须是精确的路径列表不能有模糊描述。宁可多列几个文件也不要让 Worker 自己去猜。5.3 上下文污染Supervisor 的输入要瘦身前面提过Supervisor 的上下文一旦被细节污染决策质量就会下降。我实测下来Supervisor 的输入里如果包含超过 2000 行的代码内容它开始出现抓不住重点的情况。解决办法是分层摘要。Worker 回传的结果里代码 diff 只保留改了哪些函数、改了多少行具体内容存到文件里Supervisor 需要时再按需读取。5.4 MCP 连接失败先确认协议版本接 MCP Server 的时候最常见的错误是连接超时或协议不匹配。我踩过的坑是本地 MCP Server 用的是旧版协议而 Claude Code 期望的是新版两边握手失败。排查步骤确认 MCP Server 能独立启动不报错。用curl或类似工具直接测试 Server 的端点是否响应。检查配置文件里的command和args路径是否正确相对路径经常出问题建议用绝对路径。看日志里有没有protocol version mismatch之类的提示。实操心得MCP Server 的启动日志一定要打开。我一开始没看日志以为是网络问题折腾了半天才发现是 Server 启动时缺了一个环境变量。5.5 并行任务互相干扰worktree 不是万能的worktree 能隔离文件系统但隔离不了外部资源。比如两个 Worker 同时往同一个数据库写数据或者同时调用同一个有速率限制的 API照样会出问题。我的处理方式是给任务加资源锁标记。Supervisor 在派发任务时检查如果两个任务标记了同一个资源锁就不并行执行改成串行。# 任务定义里加资源锁 task { id: task-010, description: ..., resource_locks: [database-write, external-api] }6. 这套设计真正值得借鉴的地方6.1 把智能和执行分开这套设计最聪明的地方是它没有试图让一个 Agent 变得无所不能而是把决策和执行拆成了两个角色。Supervisor 不需要会写代码它只需要会判断下一步该做什么Worker 不需要理解全局它只需要把眼前这一件事做好。这个思路其实可以迁移到很多场景。比如你做数据处理可以有一个调度层负责决定处理顺序多个执行层负责实际计算你做内容生产可以有一个策划层负责选题和排期多个创作层负责具体写作。6.2 隔离是可靠性的前提worktree 这个选择看起来是个小细节但它体现的是一个重要原则任何可能产生副作用的操作都应该在隔离环境里进行。这跟数据库事务、容器化部署是同一个思路。我现在的习惯是只要一个任务会修改文件就先给它开一个 worktree。哪怕任务很简单这个习惯也能在出问题时帮你省下大量排查时间。6.3 状态外化让流程可观测把任务状态、执行结果、依赖关系都外化成数据结构而不是藏在对话历史里这是让整个流程可观测的关键。你可以随时查看现在有哪些任务在跑、哪些完成了、哪些卡住了而不是靠翻聊天记录去回忆。这一点对于长链路任务尤其重要。我做过一个跨天的大任务中间隔了一晚上第二天接着跑的时候因为状态都在文件里完全不需要重新建立上下文。6.4 给后续扩展留的口子这套设计还有一个隐性优点它的每个环节都是可替换的。Supervisor 的决策逻辑可以换成更复杂的策略Worker 可以换成不同的模型worktree 可以换成容器MCP 可以接更多外部工具。我自己就在尝试把 Worker 换成不同能力的模型组合——简单的格式化任务用小模型复杂的重构任务用大模型。这样能在保证质量的前提下控制成本。最后分享一个我在实操中总结的小技巧每次跑完一轮调度把任务队列和执行日志存档。我攒了大概二十几轮之后回头一看发现很多任务的拆解模式是重复的。于是我把这些模式提取出来做成了一个任务模板库下次遇到类似任务Supervisor 可以直接套模板拆解质量和速度都提升了不少。这个思路你可以在自己的项目里试试尤其是那些周期性重复的开发任务效果很明显。
返回列表