深度拆解:一个只负责分活的多智能体研究助理
主代理派工,三个子代理以文件目录作黑板交换产物,钩子记录工具调用生成审计与战报。
示例版本说明:本文对照 CodeBuddy 官方示例仓库 提交
25953e3核对。下文代码为结构摘录,省略了部分导入、类型和实现;博客仓库不包含这些示例目录。unstable_v2_*属于不稳定接口,运行时请锁定依赖并对照当前文档。
深度拆解:一个「只负责分活」的多智能体研究助理
关键词:Multi-Agent · AgentDefinition · Hooks · 文件系统黑板 · Task 工具 配套代码:CodeBuddy 官方示例仓库 的
research-assistant/(Python,codebuddy-agent-sdk) 日期:2026-09-04
先看要点
research-assistant是一个多智能体研究系统:你抛出一个研究问题,它最终还你一份带图表、带引用的 PDF 研究报告。- 架构上只存在一个「前台角色」——lead agent。它的系统提示词写得很直白:你只负责规划与派活,绝不亲自干活,示例通过
allowed_tools=["Task"]配合提示词,让它以派活为主;该权限配置本身不能证明其他工具已被硬性禁用。 - 三个「执行者」用
AgentDefinition注册:researcher(搜集)→ data-analyst(分析出图)→ report-writer(整合成 PDF),是一套标准的流水线式编排。 - 子智能体之间不直接对话,而是通过约定的
files/目录交换产物——这是本示例最值得抄的设计:用文件系统当多智能体的「共享黑板」。 - 全程由 Hooks 拦截所有工具调用做追踪,落成 JSONL 审计日志与一份「会话记录」,让看不见的多智能体协作变成看得见的流水线台账。
一、先看整体:一次研究是怎么「跑」起来的
把代码里的真实调用链画出来,长这样:
你:「量子计算的最新进展有哪些?」 │ ▼┌─────────────────────────────────────────────┐│ LEAD AGENT(研究主管) ││ system_prompt:你只负责分解任务并派活 ││ allowed_tools = ["Task"] ← 派活配置 ││ 绝不直接搜索、绝不直接写文件 │└─────────────────────────────────────────────┘ │ Task(researcher, description=…) ▼┌─────────────────────────────────────────────┐│ 1. researcher(研究员) ││ tools: WebSearch, Write ││ 产出 → files/research_notes/*.md │└─────────────────────────────────────────────┘ │ 拿研究笔记后,Task(data-analyst) ▼┌─────────────────────────────────────────────┐│ 2. data-analyst(数据分析师) ││ tools: Glob, Read, Bash, Write ││ 读笔记 → 抽数据 → matplotlib 画图 ││ 产出 → files/charts/*.png ││ files/data/*.md(数据摘要) │└─────────────────────────────────────────────┘ │ Task(report-writer) ▼┌─────────────────────────────────────────────┐│ 3. report-writer(报告撰写员) ││ tools: Skill, Write, Glob, Read, Bash ││ 读全部中间产物 → reportlab 合成 ││ 产出 → files/reports/<问题>.pdf │└─────────────────────────────────────────────┘ │ ▼ 你收到一份带图表与引用的 PDF 研究报告 ✅三个关键事实,从代码里可以直接验证:
- lead 以调度为主:它的
allowed_tools=["Task"],而角色分工写在system_prompt=lead_agent_prompt里; - 执行者各配专属工具箱:researcher 连写图表的 Bash 都没有,report-writer 干脆不给 WebSearch——工具配置表达分工,但不能代替文件、进程和网络层的隔离;
- 协作不靠嘴、靠文件:
files/research_notes/、files/charts/、files/data/、files/reports/四个目录,就是子智能体之间的「交接货架」。
二、角色表:三个执行者,各管一段
| 角色 | 名称(key) | 工具箱 | 在流水线中的职责 | 产物 |
|---|---|---|---|---|
| 研究员 | researcher | WebSearch, Write | 全网搜集信息、交叉引用,写研究笔记 | files/research_notes/ |
| 数据分析师 | data-analyst | Glob, Read, Bash, Write | 读笔记、抽数值、用 matplotlib 可视化 | files/charts/、files/data/ |
| 报告撰写员 | report-writer | Skill, Write, Glob, Read, Bash | 汇总全部材料,reportlab 出 PDF(提示词要求不额外搜索) | files/reports/ |
注意 data-analyst 的 tools 里没有 WebSearch、report-writer 也没有——它们的 description 也反复强调“不进行网络搜索 - 只读取现有研究笔记并创建 PDF 报告”。这些配置用于收窄职责,但 Bash 本身仍可执行网络请求,不能把「未提供 WebSearch」等同于「无法联网」。
三、代码精读:三行配置,拼出一个「团队」
3.1 定义子智能体:一个 agents 字典
# research-assistant/research_agent/agent.py(摘录,已精简)from codebuddy_agent_sdk import CodeBuddySDKClient, CodeBuddyAgentOptions, AgentDefinition, HookMatcher
agents = { "researcher": AgentDefinition( description=( "当需要收集任何主题的研究信息时使用此智能体。" "研究员使用网络搜索从互联网上查找相关信息、文章和来源。" "将研究发现写入 files/research_notes/ 供报告撰写员后续使用。" ), tools=["WebSearch", "Write"], prompt=researcher_prompt, # model="claude-opus-4.5" # 不指定模型,使用默认配置 ), "data-analyst": AgentDefinition( description=( "在研究员完成工作后使用此智能体生成定量分析和可视化图表。" "数据分析师从 files/research_notes/ 读取研究笔记," "提取数值数据(百分比、排名、趋势、对比)," "并通过 Bash 使用 Python/matplotlib 生成图表。" "将图表保存到 files/charts/,数据摘要写入 files/data/。" ), tools=["Glob", "Read", "Bash", "Write"], prompt=data_analyst_prompt, ), "report-writer": AgentDefinition( description=( "当需要创建正式研究报告文档时使用此智能体。" "报告撰写员从 files/research_notes/ 读取研究发现," "从 files/data/ 读取数据分析,从 files/charts/ 读取图表," "然后使用 reportlab 将它们整合成…PDF 报告,保存在 files/reports/。" ), tools=["Skill", "Write", "Glob", "Read", "Bash"], prompt=report_writer_prompt, ),}AgentDefinition 的三个字段含义:
description—— 写给人看、更写给 lead 看。lead 决定「这活该派给谁」时,靠的就是这段语义描述(模型会把任务与 description 做匹配)。tools—— 该子智能体启动后可见的工具白名单,决定它“手脚”的范围。prompt—— 子智能体自己的系统提示词(从prompts/目录加载),决定它的“行为准则”。
3.2 组装主智能体:lead 的「紧身衣」
options = CodeBuddyAgentOptions( permission_mode="default", # 本文改为默认权限模式;上游示例使用 bypassPermissions setting_sources=["project"], # 从项目 .codebuddy 目录加载技能 system_prompt=lead_agent_prompt, # 研究主管人设 allowed_tools=["Task"], # 配合提示词将职责聚焦于派活,不是系统沙箱 agents=agents, # ← 把团队注册进去 hooks=hooks, # ← 全程埋点)这一小段配置就是整个编排系统的全部“胶水”。SDK 的运行时会保证:lead 每次调用 Task,都会按名字从 agents 里取出对应 AgentDefinition,以它的 prompt/tools 起一个子智能体运行时,并把子智能体的最终回复回传给 lead,让主管能「验收后再决定下一步派谁」。
3.3 挂上追踪器:Hook 匹配所有工具
tracker = SubagentTracker(transcript_writer=transcript, session_dir=session_dir)
hooks = { 'PreToolUse': [ HookMatcher(matcher=None, hooks=[tracker.pre_tool_use_hook]) # 匹配所有工具 ], 'PostToolUse': [ HookMatcher(matcher=None, hooks=[tracker.post_tool_use_hook]) ],}matcher=None 表示不挑工具,所有工具调用都过一遍钩子——因为要观测的不只是 Task,还包括子智能体内部的 WebSearch、Write、Bash,这样才能拼出完整流水线台账。
四、黑盒里的哨兵:SubagentTracker 怎么「看见」协作
多智能体最大的工程痛点是不可观测:几个分身各干各的,谁先谁后、谁写了什么,lead 心里清楚,但你(开发者/用户)一无所知。
SubagentTracker 的解法是双通道记录:
- Hook 通道(结构):
PreToolUse钩子捕获tool_name/tool_input,PostToolUse钩子捕获tool_response。每次Task调用会被登记为一个SubagentSession(以parent_tool_use_id为键),子智能体内部每一次工具调用则归入tool_calls.jsonl的结构化日志——父子关系、调用时序全被记录下来。 - 消息流通道(内容):配合消息流解析维护“当前由哪个子智能体在跑”的执行上下文,把模型的思考与输出对应到正确的会话上。
它的核心数据模型如下(结构示意,省略了构造函数,不能作为完整 Python 类直接运行):
class ToolCallRecord: # 一次工具调用 timestamp: str tool_name: str
class SubagentSession: # 一个子智能体的执行会话 subagent_type: str parent_tool_use_id: str # ← 用它关联 lead 的 Task 调用
class SubagentTracker: # Map: parent_tool_use_id -> SubagentSession self.sessions: Dict[str, SubagentSession] = {}而 _log_tool_use() 会在控制台实时打印(print(message, flush=True)),同时写入人类可读的 transcript 与机器可读的 tool_calls.jsonl。于是运行时你会看到类似这样的「实时战报」:
你:量子计算的最新进展有哪些?
[Task] 研究员研究员(Researcher) 开始任务… → WebSearch: 量子纠错 2026 进展 → Write: files/research_notes/quantum-error-correction.md[Task] 数据分析师(Data-Analyst) 开始任务… → Bash: python draw_charts.py → Write: files/charts/roadmap.png[Task] 报告撰写员(Report-Writer) 开始任务… → Write: files/reports/量子计算研究报告.pdf一个「看不见的多智能体会议」,被还原成一条可追溯、可回放、可审计的生产线台账。
五、为什么用「文件系统当黑板」是个好设计
多智能体间的协作协议,业界主流有几种:共享内存消息总线、数据库、API 调用……而本示例选的是最朴素的一种——文件目录约定。它的收益:
| 维度 | 文件系统黑板带来的好处 |
|---|---|
| 简单 | 子智能体无需感知彼此存在,只要读写约定目录,天然解耦 |
| 稳健 | 已完整写入的文件可供后续复用;自动续跑仍需任务状态、完整性校验与重入逻辑 |
| 可审 | 每一步产物都是实体文件,人类可直接打开检查、比对、修正 |
| 规模 | 谁产出、谁消费由目录结构表达,新加角色只需新加目录与约定 |
| 成本 | 不必先部署消息中间件,但需可读写的共享目录,并管理权限、并发和任务隔离 |
在真实代码里,这条「交接协议」不是写在架构文档里,而是直接写进每个子智能体的 description:
- researcher 的 description 明确「将研究发现写入
files/research_notes/供报告撰写员后续使用」; - data-analyst 明确「从
files/research_notes/读取…将图表保存到files/charts/,数据摘要写入files/data/」; - report-writer 明确「从 notes/data/charts 三处读取 → 合成 PDF 到
files/reports/」。
把「交接协议」写进模型可见的 description,而不是埋在代码里——这是比任何类型系统都更直接的“接口文档”。
六、运行起来的样子
# 在 research-assistant/ 下uv run python research_agent/agent.py # 交互式 REPL,多轮追问# 启动后在 REPL 中输入研究问题;上游入口未解析命令行问题参数交互模式底层是一个常驻的 CodeBuddySDKClient:同一客户端 while True 循环里反复 query(),每轮沿用同一 options(也就共享同一套智能体团队与钩子)。上游入口在进入 REPL 前调用一次 setup_session(),因此一次程序运行内的多轮研究共用日志目录,并非每个问题自动新建目录。files/ 产物目录也需要另外按任务隔离,避免旧材料混入新报告。
最终交付物:
research-assistant/├── research_agent/│ ├── agent.py # 入口:chat() 组装 agents/options/hooks,含 REPL 输入│ ├── prompts/ # lead/researcher/data-analyst/report-writer 提示词│ └── utils/│ ├── subagent_tracker.py # 追踪:SubagentSession / JSONL / 控制台战报│ ├── transcript.py # 会话记录(setup_session / TranscriptWriter)│ └── message_handler.py # assistant 消息解析(区分文本与 tool_use)└── files/ # 产出物(gitignore) ├── research_notes/ # 中间笔记(researcher 产出) ├── charts/ # 图表(data-analyst 产出) ├── data/ # 数据摘要(data-analyst 产出) └── reports/ # PDF 报告(report-writer 产出)七、这套实现中可复用的做法
把研究助理「去业务化」之后,留下的是一套通用的多智能体工程范式:
- 一个调度者,一串执行者 —— 调度者以
Task派活,执行者各有工具配置;职责通过工具配置和提示词表达;强边界还需拒绝规则、权限回调及系统隔离,不能称为「物理锁死」。 - 流水线而非星形总线 —— 角色间按“产物依赖”排成链:
研究 → 分析 → 成稿;没有谁需要“知道所有人”。 - 产物即接口 —— 每个角色的输出落成约定目录中的实体文件,description 里写清“我把什么放哪、我从哪拿什么”。
- 一切皆可观测 —— Pre/PostToolUse 钩子把每个工具调用、每次子智能体生成变成日志与 JSONL;没有日志的多智能体系统,出了错只能靠猜。
这套范式尤其适合研究报告、代码审查、数据分析、长文档生成这类「多阶段、多专业、产物可分割」的任务。反过来,如果你的任务是一个连续对话的问答(不需要分工),就不要硬套多智能体——直接走会话 API 更划算。
本系列其余文章:
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
评论区
正在加载 GitHub 评论…