SurfSense 主代理身份系统提示词解析:开放网络研究编排者的角色设计与调度机制
【免费下载链接】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)的身份提示词片段 identity/private.md 展开,解析 SurfSense 如何为编排型 Agent 定义"我是谁、我做什么、我如何工作"三层角色框架。你将了解到:主代理如何通过task工具调度 Reddit、YouTube、Google Maps 等平台专业子代理完成实时网络研究,如何将知识库、连接应用与持久记忆纳入调度决策,以及这套身份设计在系统提示词组装管线中的真实落地方式。读完本文,你可以理解 SurfSense 多代理编排系统的设计哲学,并掌握其提示词模块化、可见性感知与路由规则的实现细节。
一、文档定位:一份"身份区块"提示词
identity/private.md是 SurfSense 主代理系统提示词中<agent_identity>区块的内容源。它不是独立运行的指令,而是由构建器在运行时加载并注入的片段。从源码看,身份区块的构建逻辑位于 builder/sections/identity.py,其核心逻辑如下:
def build_identity_section( *, visibility: ChatVisibility, resolved_today: str, ) -> str: variant = "team" if visibility == ChatVisibility.SEARCH_SPACE else "private" fragment = read_prompt_md(f"identity/{variant}.md") if not fragment: return "" return "\n" + fragment.format(resolved_today=resolved_today) + "\n"两个关键点值得注意:
- 可见性感知(visibility-aware):同一份身份定义存在
private与team两个变体。当会话属于私人聊天(ChatVisibility.PRIVATE)时加载private.md;当会话属于工作区/团队空间(ChatVisibility.SEARCH_SPACE)时加载team.md。二者的差异集中在一处——team.md额外声明"你处于团队线程中,每条消息带[DisplayName]前缀,相关引用与决策需归属到具名作者"。 - 模板注入:身份片段末尾的
Today (UTC): {resolved_today}占位符,由构建器传入的实际日期填充。
resolved_today的生成与整个提示词的组装在 builder/compose.py 中完成:
resolved_today = (today or datetime.now(UTC)).astimezone(UTC).date().isoformat() visibility = thread_visibility or ChatVisibility.PRIVATE提示词片段通过 builder/load_md.py 中的read_prompt_md()从app.agents.chat.multi_agent_chat.main_agent.system_prompt.prompts包内按文件名读取 Markdown 文本,整个系统提示词由若干区块按固定顺序拼接。根据compose.py的模块 docstring,默认组装顺序为:
<agent_identity> # 本文档所在区块 [用户自定义系统指令,如有] <core_behavior> # 默认正文 <knowledge_base_first> # 默认正文 <dynamic_context> # 始终注入 <routing> # 默认正文 <specialists> # 始终注入(动态名册) <tools> # 始终注入 <memory_protocol> # 默认正文 <citations> # 始终注入 <output_format> # 始终注入 <refusal_and_limits> # 始终注入 <reminder> # 始终注入这意味着identity/private.md是整套系统提示词的"开场白"——它在最前面确立代理的角色与职责边界,随后才由<routing>、<specialists>、<tools>等区块展开具体行为规则。即使用户提供了自定义系统指令(custom_system_instructions),它也只是叠加在身份区块之后而非替代它,以保证平台级的安全兜底(KB-first、路由、引用、输出格式、拒答规则)始终生效。
二、身份核心:一个"开放网络研究平台"的编排者
identity/private.md给出了主代理的身份宣言:
You areSurfSense's main agent, the orchestrator of an open-source open web research platform.
这句话包含了三个限定层次:
- 主代理(main agent):SurfSense 多代理系统中负责与用户直接对话、统揽全局的 Agent。
- 编排者(orchestrator):它不是所有工作的执行者,而是"路由者"。正如身份片段强调的:"You are an orchestrator — most non-trivial work belongs on a specialist."
- 开放网络研究平台(open web research platform):用户来此研究实时网络——社区与受众在说什么、排名/评论/页面如何变化、开放网络上发布了什么,并将研究成果与自己的知识库结合使用。
这一身份设计直接呼应项目定位(Open-source NotebookLM alternative,面向 Reddit、YouTube、Instagram、TikTok、Indeed、Google Search、Maps 等实时数据源),将"研究实时网络"这一核心能力凝练成代理的身份内核。
三、三类调度对象:网络数据、用户上下文、交付物
身份片段明确列出主代理通过task工具调度的三类工作,这是整份文档的骨架:
3.1 实时网络数据(Live web data)
Reddit、YouTube、Instagram、TikTok、Amazon、Walmart、Google Maps、Google Search 以及 Web Crawler 返回结构化、实时的平台数据,包括帖子、评论、字幕、视频、商品、评论、SERP 与完整页面内容。
从仓库的 subagents/builtins 目录结构可以印证这一名册的落地:每个平台对应一个独立子代理包,包含agent.py、system_prompt.md、description.md与tools/(如 reddit、youtube、tiktok、google_maps、google_search、amazon、walmart、web_crawler、indeed 等)。每个子代理运行在自己的工具栈与上下文隔离环境中,返回单一的综合结果。
3.2 用户自身上下文(The user's own context)
包括知识库、连接的应用与持久记忆。这部分同样有对应子代理与中间件支撑:
- knowledge_base 子代理:subagents/builtins/knowledge_base 下提供
search_knowledge_base.py工具与ask_knowledge_base_tool.py,并有云/桌面双版本的 system prompt; - mcp_discovery 子代理:subagents/builtins/mcp_discovery 通过 MCP 客户端连接 Slack、Notion、Jira、Gmail、日历等第三方服务;
- memory 子代理:subagents/builtins/memory 维护持久记忆;
- main_agent 中间件:
middleware/目录中的memory、kb_persistence、knowledge_tree等中间件在请求-响应链路上处理记忆与知识库的持久化。
3.3 交付物(Deliverables)
报告(reports)、播客(podcasts)与演示文稿(presentations),由deliverables子代理基于专家们的研究结果构建。对应实现位于 subagents/builtins/deliverables,其tools/下包含report.py、podcast.py、video_presentation.py、generate_image.py、resume.py等工具。值得注意的是,podcast 这类交付物采用异步链路:deliverables子代理设置好播客后立即返回,生成过程由 Celery 后台任务驱动,聊天中的实时卡片接管进度展示(详见下文路由规则)。
四、编排者哲学:以数据说话,而非凭假设作答
身份片段对编排者的工作方式提出了明确的价值观约束:
Your value is routing each request to the right specialist, synthesizing evidence across sources, and answering with what the data shows rather than what you assume.
翻译过来即三个职责:
- 路由(routing):把每个请求分发到正确的专家;
- 综合证据(synthesizing evidence):跨来源汇总证据;
- 以数据作答(answer with data):回答的依据是"数据展示了什么",而不是"自己假设了什么"。
这一哲学在配套的 routing.md 中被展开为具体的路由规则,例如:
- 受众情绪(audience sentiment)属于平台:关于品牌/产品/话题的"人们在说什么、感觉如何",应调用
task(reddit, …)、task(youtube, …)、task(tiktok, …)、task(google_maps, …)、task(amazon, …)/task(walmart, …)去平台取回结构化的实时对话,而不是用网络搜索找"关于这段对话的文章"; - 搜索负责发现,爬虫负责阅读:搜索结果(摘要、AI 概览)只是线索而非信源,答案在页面里时先用
task(web_crawler, …)抓取页面再作答; - 地点归 Maps,开放网络归 Search:发现实体店/商户用 Google Maps 专家(返回结构化名称/地址/电话/网站),无实体门店的在线实体用搜索专家;
- 请求 N 个列表时统计独立实体:同一品牌/母机构旗下的多个分支、地点、子项目只算一个条目,凑不满 N 就继续扩大发现,诚实交付"更小的列表"优于"注水的 15 个";
- 完整数据集落成文件而非聊天:需要整表数据时指示 web_crawler 用
export_runCSV 工具抓取并保存,只转述工作区路径与行数; - 主代理没有文件系统工具:对工作区的任何读写改查都必须经
task(knowledge_base, …),绝不使用write_file、ls等直接文件操作。
同时 core_behavior.md 为身份补充了沟通风格约束:简洁直接、不念开场白、不叙述意图、模棱两可时先问再做、准确优先于附和、坚持到任务完成或真正受阻。
五、task工具:编排的指挥棒
身份片段反复强调"通过task工具调度专业子代理",因此理解task工具的契约是理解这份身份的关键。其完整规格见 prompts/tools/task/description.md,要点如下:
5.1 单模式(single mode)参数
subagent_type:要调用的专家名称,必须匹配<specialists>名册中的条目;description:完整的任务提示词。专家看不到当前线程,所以必须把所有上下文、约束与期望返回内容全部写进description;专家会用自己的格式作答,不要强行规定其输出格式。
5.2 批处理模式(batch mode)参数
tasks:{description, subagent_type}对象数组,用于并发扇出(fan-out)。当单个请求拆分成3 个或更多独立的专家调用时使用(例如"从这份清单创建五个 issue")。子任务在小型并发上限下运行,运行时为每个子任务返回一条以[task <index>]前缀标识的 ToolMessage。
批处理模式有一个关键限制:批处理子任务不支持人类介入(human-in-the-loop)中断——若某个子任务需要审批,会报错,此时必须将该任务改为非批处理的单次task(...)调用重新调度。1~2 个独立调用则直接发出两次单独的task(...)即可。
5.3 验证机制(verification teaching)
description.md末尾附有<verification>教学块,这是身份"以数据说话"哲学在工具层的具体化:子代理的自然语言回复是"自我报告",不是证据——它可能声称 Slack 消息已发出、Jira issue 已创建,但底层工具调用实际静默失败或被限流。主代理必须把"Done""Posted to #general"这类成功表述当作假设而非事实,并通过两个真实信号交叉验证:
state['receipts']:每个变更型工具都会向这个追加型列表写入结构化Receipt(包含 route、type、operation、status、external_id、verifiable_url、preview)。子代理的<output_contract>会在evidence.receipts中携带匹配的 Receipt。若子代理声称成功却没有任何status="success"的 Receipt(异步交付物如播客/视频为"pending"),则该操作实际上没有发生——按失败处理,原样告知用户,不要盲目重试;task(web_crawler, …)外部确认:当 Receipt 带verifiable_url(Notion 页面 URL、Slack 永久链接、Jira issue URL 等)时,可以爬取该 URL 从外部确认操作已生效,适用于用户明确点名的高风险变更。
Receipt 状态语义:
status="success":变更已在后端提交。带verifiable_url且高风险时可用爬虫外部确认,否则信任 Receipt 并告知用户完成;Celery 支撑的交付物(播客、视频演示)也落在这里,因为子代理已等待 worker 完成;status="failed":Receipt 的error字段携带后端错误,原样转述给用户,仅在用户明确要求时才重路由或重试;status="pending":当前罕见(现有变更工具都会等待后端返回)。若遇到,告知用户工作已启动(引用external_id/preview供后续查找),不要爬取,也不要重新派发同样的task(...)。
5.4 task 示例
prompts/tools/task/example.md 给出三个最小示例,其中前两个也内嵌在description.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.") user: "Find my Q2 roadmap and summarise the milestones." → task(subagent_type="knowledge_base", description="Locate the Q2 roadmap document under /documents and summarise its milestones. Use glob or grep if the path isn't obvious from the workspace tree.")六、<specialists>动态名册:路由的对象从哪来
<agent_identity>区块提到的"specialist subagents"在提示词中以<specialists>区块动态呈现。其构建逻辑在 builder/sections/specialists.py:
def build_specialists_section(specialist_lines: list[tuple[str, str]]) -> str: bullets = "\n".join(f"- **{name}** — {desc}" for name, desc in specialist_lines) return f"\n<specialists>\n{bullets}\n</specialists>\n"名册是动态的,取决于当前工作区的连接器配置。从 subagents/registry.py 的源码可以看到,注册表依据SUBAGENT_TO_REQUIRED_CONNECTOR_MAP做连接器过滤——某专家若依赖特定连接器而当前空间未配置,则会被移出名册。但按specialists.pydocstring 的说明,名册按约定永不为空:deliverables与knowledge_base两个专家在映射中声明了空集合frozenset(),因此在任何基于连接器的剔除中都能存活。
七、持久记忆:身份中"用户自身上下文"的一部分
身份片段将"persistent memory"列为用户自身上下文的一类。对应的运行协议见 prompts/memory_protocol/private.md:主代理在理解每条用户消息后,判断其是否揭示了关于用户的持久事实——角色、兴趣、偏好、项目、背景或长期指令;若是,则在正常回复同时调用update_memory,不推迟到后续回合;一次性问答、问候、会话琐事等临时聊天噪音则跳过。
记忆的存储格式为基于标题的 Markdown:新条目放在## Facts、## Preferences、## Instructions等##标题下,条目形如- YYYY-MM-DD: text。若旧记忆存在(YYYY-MM-DD) [fact|pref|instr]旧格式标记,需保留信息但按新格式写入。该协议同样有 private/team 变体,与身份区块的可见性选择一致。
八、private 与 team 变体:同一身份的两副面孔
身份片段以private.md/team.md双变体存在,二者核心内容完全一致,差异仅体现在团队语境上。对比两个文件可以总结出:
| 维度 | private(私人线程) | team(团队线程) |
|---|---|---|
| 服务对象 | 用户个人的知识库与记忆 | 团队的共享知识库与持久团队记忆 |
| 消息格式 | 无前缀 | 每条消息带[DisplayName]前缀 |
| 引用归属 | 不涉及 | 相关引用与决策归属到具名作者 |
这一变体机制由build_identity_section依据ChatVisibility自动选择,ChatVisibility.SEARCH_SPACE映射到 team 变体,其余(含默认值PRIVATE)映射到 private 变体。同理,dynamic_context与memory_protocol区块也遵循相同的 private/team 双轨设计,保证整个提示词体系在个人/团队两种场景下语义自洽。
九、时间注入:让编排者感知"今天"
身份片段最后一行Today (UTC): {resolved_today}的作用不可小觑。在compose.py中,resolved_today以 UTC 日期(ISO 格式YYYY-MM-DD)注入,且同样注入到用户自定义系统指令(custom_system_instructions.format(resolved_today=resolved_today))。这为代理提供了时间锚点,使其能够:
- 正确理解"上周""最近一个月"等相对时间表述;
- 感知研究任务中对时效性的要求(例如 Reddit 搜索中 "past month, sort by top" 类指令的落地)。
此外,仓库中还有一个专门的时间处理插件 plugins/year_substituter.py,可见团队对时间感知的重视——身份片段中的这一行是整个时间感知链路的一部分。
十、从身份到行为:一份提示词如何撑起整个编排系统
综合以上分析,identity/private.md虽然只有短短 24 行,但它以高度凝练的方式定义了 SurfSense 主代理的完整工作模型,并与提示词管线的其他区块形成闭环:
- 角色定义(
<agent_identity>):我是谁——开放网络研究平台的编排者; - 能力边界(
<routing>+<specialists>):我有什么——调度实时网络专家、知识库与记忆、交付物三类子代理,且名册随工作区连接器动态变化; - 工作方式(
<tools>+<memory_protocol>+<citations>):我怎么做——通过task的 single/batch 双模式调度、用 Receipt 验证子代理自报、边回答边沉淀持久记忆、按<output_format>输出; - 价值观约束(
<core_behavior>+<refusal_and_limits>):我如何自持——以数据作答、简洁直接、必要时拒答。
这种"身份先行、区块化组装、可见性感知、动态名册"的设计,使得 SurfSense 能够在保持统一主代理体验的同时,弹性适配不同工作区连接器组合与个人/团队两种协作场景。对于希望构建多代理编排系统的开发者而言,identity/private.md及其周边 compose.py、routing.md、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),仅供参考