SurfSense 多代理编排核心解析:task 工具协议、专家子代理路由与 Receipt 验证机制
2026/9/15 1:29:46 网站建设 项目流程

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_automationupdate_memory两个直接工具,连接器集成、MCP 调用、内容交付(报告、播客、视频演示等)全部通过task委派给子代理完成。

这正是 task 工具说明文档 的第一条定义:

  • task用于调用一个专家子代理(specialist subagent)
  • 专家子代理拥有工作区知识库(knowledge-base)操作以及已连接的第三方服务(Slack、Notion、Jira、Gmail 等)的专属工具;
  • 每个子代理在隔离环境中运行,拥有独立的工具栈与上下文,并返回单个综合后的结果

从 系统提示词组装器 的注释可以看到,最终系统提示词按固定顺序包含<routing><specialists>(动态名册)、<tools>(垂直切片)等区块;而 工具指令块构建器 保证task工具始终被注入——因为deliverablesknowledge_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_CONCURRENCY3批量子任务并行度(asyncio.gather+Semaphore);设为1可等效串行
SURFSENSE_TASK_BATCH_MAX_SIZE8单次批量task的最大子任务数,作为防注入/失控循环的硬上限
SURFSENSE_SUBAGENT_INVOKE_TIMEOUT_SECONDS300单个task调用的墙钟预算,超时抛出SubagentInvokeTimeoutError0关闭
SURFSENSE_SUBAGENT_BILLABLE_THRESHOLD15单轮累计调用软阈值,超过后运行时注入一次性警示 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>):动态生成的实时阵容

tasksubagent_type必须匹配<specialists>名册。名册是动态生成的:从 子代理注册表 中的SUBAGENT_BUILDERS_BY_NAME(L91-L109)读取每个专家的description.md摘要,并经 specialists 区块构建器 渲染为<specialists>块。

当前名册共 17 个专家,可分为四类:

类别专家说明
内置内容交付deliverables报告、播客、视频演示、简历、图片生成(Celery 后端)
知识库knowledge_base工作区 KB 的读写检索与文档操作(主代理的唯一文件通道)
开放网络检索web_crawlergoogle_searchgoogle_mapsyoutuberedditinstagramtiktokamazonwalmartindeed对应各数据源的结构化抓取与检索
连接的应用mcp_discovery托管 MCP 路由:Slack、Jira、Linear、ClickUp、Airtable、Notion、Confluence、Gmail、Calendar 等
文件连接器google_drivedropboxonedrive云盘文件操作,丰富知识库
记忆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):旧的按连接器命名的子代理(gmailslackjira等)在 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发出该回执的子代理deliverablesknowledge_basemcp_discovery
type路由内的产物类型report/podcast/video_presentation/resume/imagepagemessage
operation操作动词generatecreate/update/deletesend/postwrite_file/edit_file/rm
statussuccess/pending/failed验证教学的关键字段
external_id后端标识符报告行 id、Notionpage_id、Slackts、Gmailmessage_id、KBvirtualPath
verifiable_url可供外部验证的 URLSlack permalink、Jira issue URL、Linear identifier URL;Gmail/KB 无公开 URL 时为None
preview产物摘要(约 200 字符)报告 markdown 开头、播客转写开头、图片缩略图 URL
errorfailed时填充后端返回的纯文本错误原因

信号二: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_timeoutsubagent.ainvoke施加SURFSENSE_SUBAGENT_INVOKE_TIMEOUT_SECONDS(默认 300 秒)的预算;超时抛出SubagentInvokeTimeoutError,并被合成成一个带status=error语义的 ToolMessage,提示"以更窄的范围或不同专家重新路由"——这与deliverables视频渲染"等待渲染完成属有意为之而非卡死"的区分相互印证;
  • HITL 桥接:批量子任务不支持人类介入中断,单任务则通过GraphInterrupt重新抛出待处理的中断给父代理,由共享的 权限/ask 中间件 决策链路承接;
  • KB 写路径的唯一例外:KB 文件工具调用会先发出status="pending"的临时 Receipt,真正的 DB 写入发生在回合结束时的KnowledgeBasePersistenceMiddleware,随后把状态翻转为successfailed(见 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询