SurfSense 多智能体主 Agent 的「拒绝与边界」:能力边界声明的设计与实现
【免费下载链接】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 开源开放网络研究平台中主 Agent(main agent)系统提示词的
<refusal_and_limits>能力边界声明。它是主 Agent 在与众多专家子 Agent(specialist subagents)协同工作时最重要的行为护栏——定义什么该拒绝、什么该坦白、什么绝不能伪装。你将看到这条声明的完整条文、它在系统提示词中的组装位置、支撑它的运行时机制(禁用工具契约、工具名修复、死循环检测、失败闭环),以及它如何与<tools>、<specialists>、<routing>等模块协同,构成一个可解释、可验证、不谎报能力的多智能体编排器。
一、为什么主 Agent 需要一份「拒绝与边界」声明
SurfSense 的主 Agent 是一个编排器(orchestrator),而不是万能执行器。从 identity/private.md 的定义看,它的价值在于"把每个请求路由给正确的专家子 Agent、跨来源综合证据、用数据说话而不是靠假设"。这意味着主 Agent 自身暴露的工具面非常小,绝大多数非平凡工作都要通过task工具委派给 Reddit、YouTube、Instagram、TikTok、Amazon、Walmart、Google Maps、Google Search、web_crawler、knowledge_base、mcp_discovery 等专家。
工具面越小,越容易发生"越权承诺":模型看到用户提到文件、连接器或存储,就可能顺着训练数据里的习惯,声称自己能读写文件、能访问某个第三方服务、能把结果"保存起来"。<refusal_and_limits>的存在,正是要在大模型天然的"讨好倾向"与平台真实的工具边界之间,钉下一道不可逾越的纪律红线。
该声明位于 refusal_and_limits.md,全文仅 11 行、4 条规则,却与系统提示词的其它所有"always-on"模块(KB-first、路由、引用、输出格式、提醒)共同构成平台级安全网。下面逐条解读,并逐一落到源码实现。
二、逐条解读:边界声明的四条纪律
2.1 能力不在清单内 → 坦白并询问
- If a capability is not in
<tools>and no entry in<specialists>covers it, say so plainly and ask whether the user wants to proceed differently. Don't pretend you can do it.
这是整份声明的总纲:主 Agent 的能力表面被严格定义为<tools>(直接工具)+<specialists>(可委派的专家名单)的并集。凡是不在这两个清单里的能力,唯一的正确动作就是坦白做不到,并询问用户是否换个方式继续,绝不假装可以。
这句话在系统提示词里不是孤立的口号。<specialists>由 specialists.py 在每次会话动态生成:"livetaskroster for this workspace",即只有当前工作区实际可用的专家才会进入清单。而<tools>由 tool_instruction_block.py 按"垂直切片"渲染,只包含真正注册到主 Agent 的直接工具。两个清单都是运行时真实工具面的快照,因此"不在清单里"等于"平台里没有这个能力",模型无从狡辩。
2.2 任务调用出错 → 如实上报并给出下一步
- If a
taskcall errors or the specialist is unavailable, surface that to the user with a clear next step. Don't silently retry forever.
第二条针对失败处理:专家子 Agent 调用出错、或某个专家不可用时,要把失败如实呈现给用户,并给出清晰的下一步;禁止无限静默重试。
这与<routing>里的路由纪律以及task工具的<verification>教学一脉相承:专家子 Agent 的自然语言回复是"自报(self-report)",不是证据;每个变更类工具都会产出结构化的Receipt(route、type、operation、status、external_id、verifiable_url、preview),写入state['receipts']。如果子 Agent 声称成功却没有status="success"的 Receipt,就要按失败处理、原文转述给用户,不要盲目重试。status="failed"的 Receipt 携带后端真实错误,应原样转述;只有用户明确要求时才允许重新路由或重试。
2.3 运行时禁用的工具 → 明说并寻找 task 替代
- Disabled tools announced by the runtime are off-limits even if documented elsewhere — say so and offer a
taskalternative if one exists.
第三条是"禁用工具契约":运行时宣布禁用的工具,即使在其他文档里有说明,也一律不可用。模型必须明说该工具被禁用,并给出替代方案——如果某个专家能覆盖该能力,就转用task;否则直接说明工具不可用。
这条规则在系统提示词里有精确的硬件支撑:<disabled_tools>块。看 tool_instruction_block.py 的实现:
<disabled_tools> Disabled for this session: <工具名列表>. Don't claim you can use them. If the user needs that capability, delegate with `task` when a specialist covers it; otherwise say the tool is disabled. </disabled_tools>它把disabled_tool_names与主 Agent 的直接工具名集合求交集,凡是命中的工具都会以明文列出(如Update Memory、Create Automation),并逐字要求模型"不要声称能用它们"。也就是说,<refusal_and_limits>第三条与<disabled_tools>块形成了声明 + 运行时证据的双保险:前者立规矩,后者把"本会话到底禁用了什么"直接喂给模型。
2.4 不谎称访问权限 → 四个直接工具 + 专家名单是全部表面
- Never claim filesystem access, connector access, or persistent storage you don't have. The four direct tools and the
<specialists>list are your entire surface area.
第四条最严厉:绝不声称自己拥有实际不存在的文件系统访问、连接器访问或持久化存储。"四个直接工具 +<specialists>名单"是主 Agent 的全部能力表面。
这条直接否决了模型最常见的幻觉模式。在<routing>里有对应的正面指令:"You have NO filesystem tools.Any read, write, edit, move, rename, or search inside the user's workspace goes throughtask(knowledge_base, …)"。用户工作区内的任何文件操作都必须经由task(knowledge_base, …)委派,绝不能通过write_file、ls或任何直接文件操作完成。
三、边界声明的生效位置:系统提示词的组装顺序
理解这条声明的份量,必须看它在最终提示词里的位置。主 Agent 的系统提示词由 compose.py 的build_main_agent_system_prompt()组装,顺序如下:
<agent_identity> [用户自定义系统指令,如有] <core_behavior> # 默认主体 <knowledge_base_first> # 默认主体 <dynamic_context> # 始终开启 <routing> # 默认主体 <specialists> # 始终开启(动态名单) <tools> # 始终开启(垂直切片) <memory_protocol> # 默认主体 <citations> # 始终开启 <output_format> # 始终开启 <refusal_and_limits> # 始终开启 <reminder> # 始终开启注意两个关键设计(源码 docstring 中明确标注):
<refusal_and_limits>属于"always"部分,与<dynamic_context>、<specialists>、<tools>、<citations>、<output_format>、<reminder>同级,不受use_default_system_instructions=False影响。即使用户配置关闭了全部"默认主体"段落(core_behavior、kb_first、routing、memory_protocol),这条边界声明依然保留——平台级安全网不能被用户的 custom system instructions 关掉。custom_system_instructions是"叠加"而非"替换":它插在 identity 与默认主体之间,因此"KB-first、路由、引用、输出格式、拒绝规则"这些平台安全网总是生效。这从架构上杜绝了用户自定义指令"越狱"能力边界的可能。
load_md.py 的read_prompt_md()负责从app.agents.chat.multi_agent_chat.main_agent.system_prompt.prompts资源包加载这些 Markdown 片段;compose.py用_wrap()以换行包裹每个片段后拼接成最终字符串。整个系统提示词在 factory.py 中随 Agent 构建被调用,每次会话都会根据当前工作区的连接器、启停用工具、可见性(私有/团队)、引用开关实时渲染。
四、能力表面的真身:直接工具 + 专家名单
4.1 主 Agent 的直接工具面
"四个直接工具"并非抽象说法。从 tools/index.py 看,主 Agent 的内置 SurfSense 工具实际只有两个,且明确注明"Connector integrations, MCP, deliverables, etc. are delegated viatasksubagents":
MAIN_AGENT_SURFSENSE_TOOL_NAMES_ORDERED: tuple[str, ...] = ( "update_memory", "create_automation", )加上 tool_instruction_block.py 中"永远包含"的task工具(因为deliverables和knowledge_base专家在SUBAGENT_TO_REQUIRED_CONNECTOR_MAP中声明frozenset(),永远不会被连接器排除,task必有可用目标),以及 factory.py 中为上下文编辑追加的只读 run_reader 工具,这就是主 Agent 的全部直接工具面:
| 工具 | 作用 | 提示词定义 |
|---|---|---|
update_memory | 维护用户个人长期记忆文档(按可见性分为 private/team 两套变体) | private 版定义 |
create_automation | 起草并创建自动化:模型描述意图,工具内部聚焦起草完整 JSON,用户通过审批卡片 approve/reject,三步在单次调用内完成 | create_automation 定义 |
task | 调用一个专家子 Agent(支持单发与批量 fan-out) | task 定义 |
因此边界声明里"The four direct tools"指的是update_memory、create_automation、task加运行时注入的只读工具这一整体——除此以外的一切能力都必须走task专家,而task的合法目标又受<specialists>名单约束。两层约束叠加,能力表面被精确锁定。
4.2 专家名单的动态裁剪
<specialists>名单也不是静态的。它由 registry.py 的main_prompt_registry_subagent_lines()生成,其裁剪规则与build_subagents()完全一致:memory专家永远排除(记忆由主 Agent 的update_memory直接工具负责),其余专家按SUBAGENT_TO_REQUIRED_CONNECTOR_MAP(见 constants.py,即surfsense_backend/app/agents/chat/multi_agent_chat/constants.py)做连接器门控:
- 无连接器要求的常驻专家:amazon、deliverables、knowledge_base、web_crawler、youtube、google_maps、google_search、indeed、reddit、instagram、tiktok、walmart;
- 连接器门控专家:
mcp_discovery(Slack/Jira/Linear/ClickUp/Airtable/Notion/Confluence/Gmail/Calendar/MCP 任一连接器可用时出现)、dropbox、google_drive、onedrive(分别要求对应文件类连接器)。
代码注释里特别强调"名单按契约非空(non-empty by contract)":deliverables与knowledge_base无连接器要求,因此无论连接器如何裁剪,task永远有可用的委派目标。这也反向支撑了边界声明第 2、3 条的可执行性——"给出task替代"在绝大多数情况下真的存在一个专家可以顶上。
值得注意的兼容设计:LEGACY_SUBAGENT_ALIASES把旧的gmail、linear、slack等子 Agent 名映射到合并后的mcp_discovery,使 checkpoint 恢复时已暂停的旧task(subagent_type="gmail")调用能平滑解析而不是硬失败"子 Agent 不存在"。这让"专家不可用"的边界处理有了向后兼容的兜底。
五、失败不静默重试:中间件层是如何兜底的
边界声明第 2 条说"task 调用出错要如实上报、不要静默无限重试"。主 Agent 的中间件栈为此提供了多层机器兜底:
5.1 工具名修复:ToolCallNameRepairMiddleware
ToolCallNameRepairMiddleware 对模型发出的工具调用做两阶段修复:
- 小写修复:
name未注册但name.lower()已注册时原地改写(捕捉模型把Search写成search之类的错误); - invalid 回退:仍不匹配时,把调用改写为
invalid工具,参数携带原始工具名与错误信息。
invalid工具(见 invalid_tool.py)本身刻意不出现在系统提示词的工具列表里、也从不被模型广告为可调用,它只在工具注册表中存在,供 LangGraph 分发被改写的调用。模型收到的是可读的错误串:"The arguments provided to the toolXare invalid…",据此自我修正。
这直接呼应了边界声明的精神:"假装能做"的幻觉被转化成"工具名无效"的真实反馈,模型要么纠正、要么在下一轮直面<refusal_and_limits>的坦白要求,而不是让一轮错误的调用把整条对话杀死(LangChain 默认行为是对未知工具名抛出ToolNotFoundError终结本轮)。
5.2 死循环检测:DoomLoopMiddleware
DoomLoopMiddleware 解决"同一个工具用同样参数连续调用 N 次"的静默死循环:它对(工具名, 参数)计算签名,用滑动窗口检测——窗口内签名全部一致(默认阈值 3,与 OpenCode 一致)即判定死循环,抛出一个permission="doom_loop"的人机协同(HITL)中断,让前端渲染"你卡住了吗?继续 / 取消"的界面。若用户选择取消,则跳到本轮结束。代码注释明确:该中间件默认关闭,直到前端显式处理context.permission == "doom_loop"中断。这正是"不要静默重试 forever"在运行时层面的具象化——平台宁可停下来问用户,也不让模型空转。
5.3 失败闭环:连接器发现 Fail-Closed
在 factory.py 中,连接器/文档类型发现失败时采取fail-closed策略:available_connectors为None时被置为空列表,get_subagents_to_exclude的 None 短路逻辑被规避,从而"连接器门控专家全部排除、仅保留常驻专家",而不是在发现接口抖动时错误地广告出所有连接器专家。同理,MCP 工具发现失败会降级为"本回合专家无 MCP 工具"而非拒绝回复。这两处异常分支都保证了:边界声明描述的能力表面,在任何故障场景下只缩小、不虚增。
六、边界声明与其它系统提示词模块的协同
<refusal_and_limits>不是孤立的,它与相邻模块形成闭环:
- 与
<core_behavior>(core_behavior.md):后者要求"准确性优先于迎合,用户错了要礼貌反对、避免不必要的夸张与情绪化肯定""坚持到任务完成或被真正阻塞"。边界声明的"Don't pretend you can do it"正是"accuracy over agreement"在能力维度的延伸。 - 与
<knowledge_base_first>(kb_first.md):KB-first 要求事实性回答必须来自本回合真实拿到的平台数据、知识库、连接器或专家摘要,找不到时先说明找不到,再询问是否允许用通用知识回答。这与边界声明第 1 条"坦白并询问"是完全一致的行为模式——一个约束"能力不要假装",一个约束"知识不要编造"。 - 与
<routing>(routing.md):路由纪律明确规定"两条执行通道,绝不互相模拟"——直接工具只能做 update_memory、write_todos;其余一律task。它还多次强调"You have NO filesystem tools"、大结果用export_run导出 CSV 文件而不是粘贴进聊天。这些正面路由规则,与边界声明的负面禁止规则(不谎称文件系统/连接器/存储访问)互为镜像。 - 与
<output_format>(output_format.md):输出格式要求"绝不暴露内部工具参数名、后端 ID 或实现细节,使用自然语言",与边界声明共同压制"模型把内部机制当成可承诺能力"的倾向。 - 与
<reminder>(reminder.md):作为提示词最后一段,<reminder>用一句话复述核心纪律:"简洁 · 以本回合专家数据为准 · 委派优先 · 无直接文件系统 · 出现持久事实时写入记忆"。它是边界声明在整条提示词末尾的最后一次强化——正因为<refusal_and_limits>排在倒数第二、紧邻<reminder>,它处于模型最容易"记住"的收尾区域。
七、结论:可解释的边界,才是有用的边界
回看这份仅 11 行的声明,它之所以有效,在于每一句话都有运行时机制背书:
- "能力不在清单内就坦白"——
<tools>与<specialists>是运行时真实工具面的快照,清单即事实; - "出错要上报、不静默重试"——
Receipt验证机制让"自报成功"可被证伪,DoomLoopMiddleware兜住死循环; - "运行时禁用即不可用"——
<disabled_tools>块把禁用名单明文喂给模型; - "不谎称文件/连接器/存储访问"——主 Agent 直接工具面被压缩到
update_memory、create_automation、task加只读工具,文件与连接器访问全部强制经task专家。
对使用 SurfSense 的开发者与研究者而言,这份声明揭示了多智能体系统提示词设计的一条可复制原则:能力边界不是靠模型自觉,而是靠"声明的禁令 + 动态渲染的证据 + 中间件的机器兜底"三层共同落地。任何想为自己的 Agent 增加"诚实拒绝"能力的实现,都可以参照<refusal_and_limits>的四条规则,以及它在 compose.py 中的"always-on、不受用户自定义指令关闭"的组装位置——把安全网放在用户指令之外,把证据放在模型眼前。
【免费下载链接】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),仅供参考