从 Team Protocol 到 MCP:Claude Code Harness 后半程架构演进分析
从 Team Protocol 到 MCP:Claude Code Harness 后半程架构演进分析
前面十几个章节里,我们已经完成了一个 Agent 的基本能力:
- Tool Calling
- Hook
- Todo
- Memory
- Task System
- Background Task
- Scheduler
- Team
看到这里会发现一个变化。
最开始的 Agent 其实只有一个循环:
1 | while True: |
但到了 Team 阶段之后,系统已经不再是一个 Agent。
而变成了:
1 | Lead |
这时候很多以前不存在的问题开始出现:
- Agent 之间如何协作?
- Agent 如何自己找任务?
- Agent 之间如何避免修改同一个文件?
- 如何接入外部系统?
s16~s19 实际上都在解决这些问题。
一、s16:为什么 Team 需要协议
s15 里面已经有 Team 了。
Lead 可以给 Alice 发消息:
1 | BUS.send( |
Alice 收到消息后执行。
看起来没问题。
但如果 Alice 正在执行:
1 | write_file(...) |
或者:
1 | bash("npm install") |
这时候直接退出线程会怎样?
可能出现:
1 | 文件写了一半 |
整个系统会进入不可预期状态。
所以问题不是:
1 | 能不能发消息 |
而是:
1 | 消息如何确认 |
request_id 的作用
s16 最核心的设计其实不是 Inbox。
而是:
1 | request_id |
例如:
1 | { |
Alice 收到后:
1 | { |
Lead 再根据:
1 | pending_requests |
找到对应请求。
这里本质上是在做:
1 | Request |
模式。
其实和 HTTP 非常像:
1 | Client |
只是这里变成了:
1 | Lead |
为什么不能只发消息
很多初学者会觉得:
1 | send_message() |
不就够了吗?
其实不够。
因为:
1 | 发送成功 |
例如:
1 | BUS.send(...) |
返回成功。
只能证明:
1 | 消息进入邮箱 |
不能证明:
1 | Alice 真执行了 |
所以才需要:
1 | match_response() |
以及:
1 | ProtocolState |
来维护状态。
二、s17:Lead 不应该负责分配所有任务
有了协议之后。
系统运行起来了。
但新的问题出现了。
看 Task Board:
1 | create_task(...) |
Lead 创建完任务后还要:
1 | Alice做这个 |
如果有:
1 | 100个任务 |
怎么办?
Lead 会变成瓶颈。
从派单到抢单
所以 s17 最重要的函数其实是:
1 | scan_unclaimed_tasks() |
以及:
1 | claim_task() |
Agent 空闲时:
1 | available = scan_unclaimed_tasks() |
发现任务:
1 | claim_task(task.id) |
直接认领。
这里意味着系统从:
1 | Lead 派任务 |
变成:
1 | Agent 自己找任务 |
为什么要增加 IDLE 状态
观察 Teammate 的循环。
前面章节里:
1 | run() |
基本就是:
1 | 收到任务 |
但 s17 之后变成:
1 | WORK |
循环。
对应代码实际上就是:
1 | while not shutdown: |
这样 Agent 才能持续工作。
否则:
1 | 任务完成 |
下一次还得重新创建 Agent。
为什么说 s17 才是真正的 Agent Team
因为从这一章开始:
Lead 不再负责具体调度。
Lead 只负责:
1 | 创建任务 |
Agent 自己负责:
1 | 发现任务 |
这时候 Team 才真正具备自治能力。
三、s18:Task 和 Workspace 必须绑定
到了 s17。
Agent 已经会自己找活干了。
但很快又出现新的问题。
例如:
1 | Task A: |
Alice 和 Bob 同时工作。
结果:
1 | write_file("config.py") |
两个人都改了。
最后谁覆盖谁?
不知道。
create_worktree 干了什么
核心代码:
1 | def create_worktree(name, task_id=""): |
里面真正关键的是:
1 | run_git([ |
这一句。
它会创建:
1 | .worktrees/ |
两个独立目录。
为什么还要 bind_task_to_worktree
创建目录之后:
1 | bind_task_to_worktree( |
很多人第一次看这里会误解。
以为:
1 | 绑定成功 |
实际上不是。
看代码:
1 | task.worktree = worktree_name |
这里只修改:
1 | task.worktree |
没有修改:
1 | task.status |
因此:
1 | pending |
仍然保持不变。
后面:
1 | claim_task() |
时才进入:
1 | in_progress |
为什么这么设计
因为:
1 | Task System |
和:
1 | Worktree System |
其实是两个系统。
Task 负责:
1 | 谁干活 |
Worktree 负责:
1 | 在哪干活 |
s18 只是通过:
1 | task.worktree |
把它们关联起来。
这样设计耦合度更低。
自动切换目录
认领任务之后:
1 | _run_claim_task() |
里面有:
1 | wt_ctx["path"] = ... |
后面:
1 | run_bash( |
于是:
1 | Alice |
所有工具:
1 | bash |
都运行在:
1 | auth-refactor |
目录。
Bob 同理。
这就是隔离的实现方式。
四、s19:为什么需要 MCP
到这里已经有很多工具:
1 | bash |
但问题来了。
如果公司有:
1 | Jira |
怎么办?
难道每个都写:
1 | query_issue() |
吗?
显然不现实。
MCPClient 干了什么
s19 引入:
1 | class MCPClient |
里面最重要两个接口:
1 | register() |
和:
1 | call_tool() |
对应 MCP 协议:
1 | tools/list |
本质上:
1 | 发现工具 |
整个 s19 最核心的函数
我认为是:
1 | assemble_tool_pool() |
因为 Agent 根本不知道什么叫 MCP。
Agent 只认识:
1 | TOOLS |
所以:
1 | MCP Tool |
必须转换成:
1 | 普通 Tool |
看代码:
1 | prefixed = ( |
然后:
1 | tools.append(...) |
加入工具池。
再:
1 | handlers[prefixed] = ... |
注册调用逻辑。
于是:
1 | jira.create_ticket |
变成:
1 | mcp__jira__create_ticket |
为什么要重建 Tool Pool
前面章节一直有:
1 | prompt cache |
但 s19 删掉了。
原因就在这里。
例如:
1 | connect_mcp("docs") |
执行前:
1 | TOOLS |
执行后:
1 | TOOLS |
工具池已经变了。
缓存自然失效。
所以:
1 | assemble_tool_pool() |
必须重新执行。
normalize_mcp_name 为什么存在
看起来只是:
1 | re.sub(...) |
实际上很重要。
否则:
1 | docs-server |
和:
1 | docs/server |
可能生成非法 Tool 名称。
统一转换后:
1 | mcp__docs_server__search |
整个 Tool Namespace 才能保持稳定。
总结
s16 到 s19 其实对应 Agent Team 发展过程中的四个阶段。
第一步:
1 | 如何协作 |
于是有了:
1 | Protocol |
第二步:
1 | 如何自动工作 |
于是有了:
1 | Autonomous Agent |
第三步:
1 | 如何避免互相影响 |
于是有了:
1 | Worktree Isolation |
第四步:
1 | 如何扩展能力 |
于是有了:
1 | MCP Plugin |
如果说前面的章节是在解决:
1 | Agent 如何完成任务 |
那么 s16~s19 解决的其实是:
1 | 多个 Agent 如何长期协作完成任务 |
到了这里,Claude Code Harness 已经不再是一个简单的 ReAct Agent,而逐渐演化成了一个具备协议、自治、隔离和插件能力的工程系统。
