SurfSense 多代理编排核心解析:task 工具协议、专家子代理路由与 Receipt 验证机制
【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense
导读
本文以 SurfSense 主代理(main agent)系统提示词中的task工具协议文档为主体,深入剖析这个开源 NotebookLM 替代方案如何通过"专家子代理(specialist subagent)"完成知识库操作与第三方服务(Slack、Notion、Jira、Gmail 等)编排。读者将掌握task的单模式与批量模式调用规范、<routing>路由决策规则、以及基于Receipt的两路地面真值验证机制,并看到这些协议在 task 工具实现 与 Receipt 契约 中的源码级落地。
一、task工具在多代理架构中的定位
SurfSense 的主代理(supervisor)遵循"单一职责 + 委派"原则:主代理自身只暴露极小的工具面。从 主代理工具注册表 可以看到,_MAIN_AGENT_TOOL_FACTORIES中只有create_automation与update_memory两个直接工具,连接器集成、MCP 调用、内容交付(报告、播客、视频演示等)全部通过task委派给子代理完成。
这正是 task 工具说明文档 的第一条定义:
task用于调用一个专家子代理(specialist subagent);- 专家子代理拥有工作区知识库(knowledge-base)操作以及已连接的第三方服务(Slack、Notion、Jira、Gmail 等)的专属工具;
- 每个子代理在隔离环境中运行,拥有独立的工具栈与上下文,并返回单个综合后的结果。
从 系统提示词组装器 的注释可以看到,最终系统提示词按固定顺序包含<routing>、<specialists>(动态名册)、<tools>(垂直切片)等区块;而 工具指令块构建器 保证task工具始终被注入——因为deliverables与knowledge_base两个专家在连接器排除逻辑中永不缺席,<specialists>名册非空是硬性契约。
二、单模式(single mode)调用规范
task的单模式只接受两个参数,全部必须显式提供:
| 参数 | 含义 | 关键要求 |
|---|---|---|
subagent_type | 要调用的专家名称 | 必须匹配<specialists>名册中的条目(见下文名册) |
description | 完整任务提示词 | 专家看不到当前线程,必须把全部上下文、约束以及你期望拿回的内容写进去;专家会用自己的输出格式回复,不要替它规定格式 |
description是全量自包含的任务提示词,这是整个协议最重要的心智模型:子代理与父线程之间不存在共享上下文,任何依赖线程历史的信息如果不写进description,专家就无从得知。
单模式的标准用法示例(来自 example.md):
user: "Save these meeting notes to my KB: …" → task(subagent_type="knowledge_base", description="Save the notes below to a new document under /documents/notes/. Pick a sensible title and folder; tell me the path you used.\n\n<notes>…</notes>")user: "What did Maya say about the Q2 roadmap in Slack last week?" → task(subagent_type="mcp_discovery", description="In Slack, find messages from Maya about the Q2 roadmap from the past week. Return the most relevant quotes with channel and timestamp.")注意第二个示例:Slack 查询同样以纯文本description下发,并明确要求返回"带 channel 和时间戳的引用",这就是"专家用自己的格式回复"的体现。
三、批量模式(batch mode):多路并发扇出
当单个用户请求会展开为 3 个或更多相互独立的专家调用时(例如"根据这份列表创建五个 issue"),应使用批量形状:
task(tasks=[ {subagent_type: "mcp_discovery", description: "…child 1…"}, {subagent_type: "mcp_discovery", description: "…child 2…"}, … ])批量模式的协议约束(来自 task 工具说明文档 与 路由规则):
tasks是{description, subagent_type}对象的数组,与单模式参数互斥;- 运行时在小型并发上限(semaphore)内并发执行子任务;
- 每个子任务对应一个
ToolMessage块,并以[task <index>]前缀标识,父代理需逐个读取核对; - 批量子任务不支持人类介入(human-in-the-loop)中断——某个子任务需要审批时会冒出错,此时必须把该任务重新以非批量的
task(...)调用单独派发; - 1–2 个独立调用不需要批量形状,直接发出两条并行
task(...)调用即可。
从 checkpointed_subagent_middleware 常量 可以看到批量扇出的运行时上限由环境变量控制:
| 环境变量 | 默认值 | 含义 |
|---|---|---|
SURFSENSE_TASK_BATCH_CONCURRENCY | 3 | 批量子任务并行度(asyncio.gather+Semaphore);设为1可等效串行 |
SURFSENSE_TASK_BATCH_MAX_SIZE | 8 | 单次批量task的最大子任务数,作为防注入/失控循环的硬上限 |
SURFSENSE_SUBAGENT_INVOKE_TIMEOUT_SECONDS | 300 | 单个task调用的墙钟预算,超时抛出SubagentInvokeTimeoutError;0关闭 |
SURFSENSE_SUBAGENT_BILLABLE_THRESHOLD | 15 | 单轮累计调用软阈值,超过后运行时注入一次性警示 ToolMessage 让编排器收尾;0关闭 |
批量扇出的完整示例(来自 routing.md):
user: "Create issues in Linear for each of these five bugs: <list>" → task(tasks=[ {subagent_type: "mcp_discovery", description: "In Linear, create an issue titled '<bug 1>' … Return the issue URL."}, {subagent_type: "mcp_discovery", description: "In Linear, create an issue titled '<bug 2>' … Return the issue URL."}, … ]) Read back the [task 0]…[task 4] blocks in the combined ToolMessage and verify each via its Receipt's verifiable_url per the <verification> teaching before confirming to the user.四、路由规则:何时委派、如何并行、如何串行
task的具体调用时机、频率与作用域规则统一存放在<routing>区块(即 routing.md)。主代理有两条执行通道,必须选择真正拥有该工作的那条,绝不能用一条通道去模拟另一条。
4.1 直接工具(自己调用)
update_memory:维护持久记忆;write_todos:当一轮任务跨越多个专家或步骤时维护结构化计划,在每个task调用之前标记in_progress、返回后标记completed;单步请求跳过。
4.2task路由决策要点
- 一个专家一个
task调用:单个调用只针对一个专家,该专家只拥有自己领域的工具,超出领域的任务不会被执行; - 并行化独立工作:两条
task调用互不引用对方输出,且指向不同专家(或同一专家但范围不重叠,如读取两个不相关路径)时,应作为并行调用发出; - 依赖工作跨轮次串行:一个专家的输出必须作为另一个专家输入时(如"先在 KB 找到路线图,再邮件给 Maya"),在连续轮次中调用,先把第一个结果烘焙进第二个的提示词,并用
write_todos跨轮次维持计划; - 同一专家内捆绑步骤:读 + 写 + 总结应放进同一个任务提示词;
- 完整指令放任务提示词内,专家看不到线程;
- 不要假装知道专家数据源内容,调用专家并使用它返回的结果。
4.3 领域分工经验法则
<routing>还沉淀了若干领域分工规则:
- Search 负责发现,crawler 负责阅读:搜索结果只是指针而非来源;答案在页面本体时,少量已知 URL 用
task(web_crawler, …)(maxCrawlDepth=0)抓取,整站/大量页面则带更高深度抓取;公开事实能被工具检索就必须检索后回答并引用,而不是甩一个 URL; - 受众情绪去平台找:社区讨论找 Reddit、视频内容找 YouTube、短视频趋势找 TikTok、实体店评论找 Google Maps、零售产品评价找 Amazon/Walmart——平台专家返回的是结构化的"对话本身";
- 线下实体找 Maps,开放网络找 Search:无实体门店的实体(纯线上公司、软件厂商、出版物)用 Search;
- N 列表按"独立实体"计数:同一品牌/母组织的多个分支、门店、子页面只算一个实体,网站域名是所有权信号;不足 N 时诚实扩量,不够就交付更短列表并附一行说明——"诚实的 10 条胜过注水的 15 条";
- 完整数据集落成文件而非聊天:几百行数据用 web_crawler 的
export_runCSV 工具保存,回报工作区路径与行数; - 主代理没有文件系统工具:工作区内任何读写、编辑、移动、搜索都走
task(knowledge_base, …)。
五、专家名册(<specialists>):动态生成的实时阵容
task的subagent_type必须匹配<specialists>名册。名册是动态生成的:从 子代理注册表 中的SUBAGENT_BUILDERS_BY_NAME(L91-L109)读取每个专家的description.md摘要,并经 specialists 区块构建器 渲染为<specialists>块。
当前名册共 17 个专家,可分为四类:
| 类别 | 专家 | 说明 |
|---|---|---|
| 内置内容交付 | deliverables | 报告、播客、视频演示、简历、图片生成(Celery 后端) |
| 知识库 | knowledge_base | 工作区 KB 的读写检索与文档操作(主代理的唯一文件通道) |
| 开放网络检索 | web_crawler、google_search、google_maps、youtube、reddit、instagram、tiktok、amazon、walmart、indeed | 对应各数据源的结构化抓取与检索 |
| 连接的应用 | mcp_discovery | 托管 MCP 路由:Slack、Jira、Linear、ClickUp、Airtable、Notion、Confluence、Gmail、Calendar 等 |
| 文件连接器 | google_drive、dropbox、onedrive | 云盘文件操作,丰富知识库 |
| 记忆 | memory | 记忆维护(构建时默认排除在task名册之外) |
从 连接器映射常量 可看到两个关键机制:
SUBAGENT_TO_REQUIRED_CONNECTOR_MAP(L31-L61):每个专家声明其所需的连接器 token,amazon/deliverables/knowledge_base/web_crawler等声明frozenset()(永不因连接器缺失被排除),而mcp_discovery采用 any-of 门控——工作区至少连接一个受支持应用时才出现在名册中;LEGACY_SUBAGENT_ALIASES(L67-L80):旧的按连接器命名的子代理(gmail、slack、jira等)在 MCP 整合后保留为mcp_discovery的别名,使旧 checkpoint 恢复时不至于硬失败。
六、验证机制:把"自我报告"当作假设而非事实
这是task协议中最重要的安全设计。task 工具说明文档 的<verification>区块开宗明义:
子代理的自然语言回复是一种"自我报告"(self-report),不是证据。专家可能声称 Slack 消息已发布、Jira issue 已创建、报告已生成,即便底层工具调用静默失败或被限流。要把 "Done"、"Posted to #general"、"Created ENG-42" 这类成功措辞当作假设,而不是事实。
6.1 两路地面真值信号
信号一:state['receipts'](结构化回执)
每个变更类(mutating)工具都会向一个 append-only 列表发出结构化的Receipt。父代理(supervisor)不直接看到原始列表,但每个子代理的<output_contract>会把匹配的 Receipt 放在evidence.receipts下回传。若子代理报告成功,却没有一条status="success"的匹配 Receipt,则该操作没有发生——必须按失败处理并原样呈现给用户,不要盲目重试(异步交付物如播客/视频则为"pending")。
Receipt的字段契约定义在 receipt.py(L74-L120),make_receipt工厂(L123-L154)只保留非None字段:
| 字段 | 含义 | 示例 |
|---|---|---|
route | 发出该回执的子代理 | deliverables、knowledge_base、mcp_discovery等 |
type | 路由内的产物类型 | report/podcast/video_presentation/resume/image;page;message |
operation | 操作动词 | generate、create/update/delete、send/post、write_file/edit_file/rm等 |
status | success/pending/failed | 验证教学的关键字段 |
external_id | 后端标识符 | 报告行 id、Notionpage_id、Slackts、Gmailmessage_id、KBvirtualPath |
verifiable_url | 可供外部验证的 URL | Slack permalink、Jira issue URL、Linear identifier URL;Gmail/KB 无公开 URL 时为None |
preview | 产物摘要(约 200 字符) | 报告 markdown 开头、播客转写开头、图片缩略图 URL |
error | 仅failed时填充 | 后端返回的纯文本错误原因 |
信号二:task(web_crawler, …)(外部确认)
当 Receipt 携带verifiable_url时,主代理可以调用task(web_crawler, …)抓取该 URL,在系统外部确认操作真实发生。适用于两类场景:
- 用户明确点名的高价值变更(如"给整个团队发启动邮件");
- 子代理自我报告与用户预期相互矛盾时。
6.2 Receipt 状态语义(务必逐条细读)
status="success":变更已在后端提交。若存在verifiable_url且请求是高价值的,可经task(web_crawler, …)外部确认;否则信任 Receipt 并告知用户已完成。Celery 支撑的交付物(播客、视频演示)也落在这里——子代理已等待 worker 完成,success意味着产物确实已保存。status="failed":该 Receipt 的error字段携带后端错误,逐字呈现给用户;只有在用户明确要求时才重路由或重试。status="pending":目前罕见——当前变更类工具都会等待后端返回。若遇到,告知用户工作已启动(引用external_id/preview便于日后查找),不要抓取、也不要重新派发同一个task(...)调用指望"这次能完成"。
Receipt 状态是Literal["success", "pending", "failed"](receipt.py L71),验证教学依赖该字段做决策分支。
6.3 验证机制的一个完整实战走查
来自 routing.md 的"发布启动公告到 #general"示例:
This turn: task(mcp_discovery, "In Slack, post '<launch announcement text>' to #general. Return the message permalink.") Next turn (with the receipt's verifiable_url in hand): task(web_crawler, "Crawl <verifiable_url from the receipt> and confirm the post is live; return what you find.") → confirm the post is live, then tell the user it's up with the URL. If the reply has NO Receipt with status="success", treat it as a silent failure: surface the error verbatim, do not retry.同理,example.md 中批量创建五个 Linear issue 后,也要求"Read back the[task 0]…[task 4]blocks in the combined ToolMessage and verify each via its Receipt'sverifiable_url"——先验证再向用户确认。
七、底层实现:task 工具的运行时骨架
协议的运行时载体是 task_tool.py,它是父代理与子代理在运行时的唯一会面点:读取父代理暂存的 resume 值,决定下发全新状态还是定向Command(resume=...),并把子代理新的待处理中断重新抛回父代理。几个值得注意的实现事实:
- 墙钟超时预算:
_ainvoke_with_timeout对subagent.ainvoke施加SURFSENSE_SUBAGENT_INVOKE_TIMEOUT_SECONDS(默认 300 秒)的预算;超时抛出SubagentInvokeTimeoutError,并被合成成一个带status=error语义的 ToolMessage,提示"以更窄的范围或不同专家重新路由"——这与deliverables视频渲染"等待渲染完成属有意为之而非卡死"的区分相互印证; - HITL 桥接:批量子任务不支持人类介入中断,单任务则通过
GraphInterrupt重新抛出待处理的中断给父代理,由共享的 权限/ask 中间件 决策链路承接; - KB 写路径的唯一例外:KB 文件工具调用会先发出
status="pending"的临时 Receipt,真正的 DB 写入发生在回合结束时的KnowledgeBasePersistenceMiddleware,随后把状态翻转为success或failed(见 receipt.py 模块 docstring)——这正是验证机制能覆盖 KB 写操作的原因。
八、小结
task是 SurfSense 多代理编排的枢纽协议:通过"单专家单调用、独立工作并行、依赖工作跨轮串行、3 路以上独立调用走批量扇出"的路由纪律,把知识库与第三方服务的操作安全地委派给隔离运行的专家子代理;再通过Receipt双信号验证机制,把"子代理说了什么"与"系统实际发生了什么"严格分离,确保任何成功声明都有可审计的结构化证据支撑。理解这套协议,就等于理解了 SurfSense 主代理如何在保持自身工具面极小的同时,可靠地驾驭 17 个专家、数十个连接器与复杂的异步交付物。
进一步阅读建议:
- 协议全文:task 说明文档、task 示例、路由规则
- 契约与注册:Receipt 定义、子代理注册表、连接器映射常量
- 运行时实现:task 工具、并发/超时常量、系统提示词组装
【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考