Skip to main content
这里只讲一条纪律,其余都是它的推论:
未声明即不可达。 不是「给了再拒绝」——定义里没写的东西, 根本不会出现在它的工具表和提示词里。这样模型看不见不存在的能力, 也就不会去试;真试了,拿到的也是一句明确的「你没有这个」。

一份完整的定义

一份这样的定义,让一个只会读文件跑命令的子代理,变成够得着业务知识库的售后专员。 下面逐个维度说清楚它到底拿到了什么。

维度一:工具

白名单扩容过一轮——以前只有 bash / read / write / edit / glob / grep 六个, 业务工具一个都够不到。现在可授予的是一张**「允许表的允许表」**: 刻意缺席的工具,缺席理由和「白名单扩容」同等重要: 声明大小写不敏感,落库一律是规范注册名。 写 webfetch 能解析成 WebFetch。 这条是被真实 bug 逼出来的:会话工具注册名大小写不统一(WebFetch 是驼峰, use_skill 是蛇形),而旧解析器对每个工具名无条件小写化, 于是驼峰注册名永远匹配不上——工具静默解析为空,且没有任何报错。 运行时还有第二个条件:声明的工具必须确实存在于当前会话的工具表。 两个条件都满足才授予,否则记一条诊断告诉你缺了什么,而不是悄悄不给。

维度二:技能

skills 是按名白名单,解析复用主代理同一套分层发现 (工作区 > 生态·工作区 > 系统 > 生态·用户 > 插件)。
  • 没声明 skills → 不注入任何技能目录,use_skill 也不可用;
  • 声明了但某个技能在当前作用域不存在 → 记诊断,那一条从目录里消失, 不静默:用户需要知道自己声明的东西没生效;
  • 目录里只有 name / description / location 三行,正文经 use_skill 按需加载。
技能上限 16 个——每个都会占系统提示词目录块的一行。

维度三:MCP 服务器

子代理拿到的不是共享网关,而是作用域化网关:
  • search / describe:工具索引按声明过滤,只列已声明服务器的工具;
  • call 一个未声明的服务器 → 立即拒绝,并告诉它「你被授权了这些」;
  • call 一个已声明的服务器 → 命中该服务器的审批白名单就直接执行; 没命中则把审批卡转发到父线程,你在正在看的那个会话里看到并裁决。
最后一条是本设计里唯一让子代理能阻塞在人的地方。选它而不是「一律拒绝」的理由: 审批白名单默认不配置,一律拒绝等于任何未预配置的 MCP 服务器对子代理都不可用, 垂直业务代理形同虚设。父线程审批卡复用现有机制,不需要新建审批系统—— 子代理挂在父卡片上,是它在无法自行询问用户时唯一诚实的选项。 不止 MCP:子代理的 write / edit / bash 走的是同一条父会话审批链。 这些内置工具当初是从父会话的工具表里按定义取的,却没有接审批钩子—— “把活交给带写入能力的子代理”于是等于把权限放大一档。现在判定收口在同一个函数里: 档位(变更前确认 / 工作区内自动 / 自动编辑 / 完全访问)、工作区边界与可写根清单、 配置类工具的确认、无人值守的即时裁决,对子代理与主代理一视同仁,卡显示在父会话。 Task 本身不作为审批项——它不写盘、无副作用。 代价已明确接受:一个等待裁决的后台子代理会挂起,TaskStop 可以中止它(中止时它 名下挂起的卡按拒绝结算,子代理不会卡在那次调用上)。已知残留:父会话没有活跃 请求时(主代理这一轮已跑完、子代理还在后台),直播通道送不出卡,卡片要等下一次 快照/挂载拉取才现身。
声明了但服务器当前没启用/没配置时,网关照样挂载,只记一条诊断。 因为「能力不存在」和「配置还没到位」的下一步动作完全不同—— 后者该去设置页配服务器,而不是让模型以为自己没这项本事。

维度四:知识源

一份知识源就是一份文档:名称 + 工作区相对 glob,正文留在磁盘。 永不预加载。 系统提示词只拿到每源一行的目录,检索走 kb_search: 纯内存逐行扫描打分,返回排好序的 path:line 命中,子代理再自己 read 打开感兴趣的文件。 命中是一行,不是答案——工具描述里就这么写着。

8 MiB

单次检索最多读取的字节数,覆盖常规产品手册规模。

50 条

单次返回命中上限,比 grep 的 200 更紧。

400 字符

单条命中行长度上限,与 grep 一致。
超预算会显式标注截断,不静默——静默截断会让模型误以为「库里只有这些内容」。 设置页用目录选择器:选完目录自动收敛成工作区相对 glob。 手打 ./docs/**/*.md 不该是「给 agent 一个知识库」的前置知识。 选到工作区之外的目录会被直接拒绝(检索侧按 path.join(cwd, rel) 解析, 绝对路径会拼成无意义的串,静默搜不到任何东西)。 声明知识源就必须同时授予 read,否则它拿到 path:line 却打不开文件。 解析层只警告(不赔掉整份定义),保存路径直接拒绝——填完才被拒太晚了, 所以编辑时就 inline 提示。
外部系统(飞书表格、Notion、数据库)不走知识源,由 mcp.servers 授予, agent 直接调那些服务器的工具。把 MCP 也做进知识源等于同一件事说两遍, 还多一套要维护的类型分支。

维度五:记忆

三档互斥,缺省即「无」: 私有是推荐档。 子代理写不进用户主记忆,结构上不可能污染; 它生成的内容可能是幻觉,本就不该进主代理的下一次提示词。 共享档保留是因为「业务 agent 与主代理共享一条经验」确有场景—— UI 上会附一行知情说明,不是阻止,是知情。 三件套工具 memory_write / memory_read / memory_search,仅在非「无」时挂载。 关键一条:scope 参数不进工具 schema,目录在闭包里钉死, 子代理在参数里伪造 scope: "global" 无门可过——隔离是结构性的,不是运行时校验参数值。 投递方式与主记忆同构的两层:根级 *.md 视为常驻记忆,经提示词段注入 (逐文件 4K、整段 12K 预算,超预算的文件整体略去并留一行说明); daily/*.md 只参与关键词检索。 并发上,同一子代理的多次委派写同一目录——按目录串行 append, 不用文件锁(sidecar 是单进程,进程内串行就够)。 主记忆的全局开关不控制子代理记忆:那是主代理提示词的缓存纪律, 用一个默认关闭的开关去否决你显式声明的 memory: private 是错的耦合。
knowledge 与 memory 的分工:knowledge 是外部权威资料(产品手册、政策库), 只读;memory 是agent 自己攒下的经验,可读可写。 一个是「世界告诉它的」,一个是「它自己记住的」。

提示词怎么拼

顺序固定:框架 → 能力目录 → 定义正文。 能力目录的段序也固定:技能 → 知识源 → MCP → 记忆。 定义正文放最后,让它对「怎么干活」有最后发言权。
不变量:每个维度未声明时,该段整体省略—— 所以既有定义的输出提示词字节级不变, provider 侧的 prompt cache 才命中。这条不变式进了测试。

设置页怎么改

编辑器是二级页面而不是弹窗。加了能力维度后,内容远超一个 sm:max-w-2xl 弹窗能从容承载的高度——表单被挤出视口,保存按钮要滚到底才够得着。 改成整页后:顶部返回、内容区独立滚动、操作条常驻底部,两列布局让高度回到一屏内。 表单分五组:基本信息 / 系统提示词 / 可用工具 / 能力授予 / 知识源。 几条实现上的取舍值得单独说:
  • 三个选择器都用真实候选,不给自由文本——工具取自 sidecar 的可授予清单, 技能取自技能清单,MCP 服务器取自 MCP 配置。 能力声明是引用既有实体,不是新定义;给自由文本框只会让人写出不存在的名字。 后端是唯一事实源,旧 sidecar 缺这个字段时回落旧 6 项,不白屏。
  • 引用了不存在的名字,以警示色 chip 显式出现,hover 说明、点击可移除, 不静默丢弃。
  • YAML 原文页签走同一套解析校验,手写的四个能力键原样生效, 表单页签的内容不会覆盖这里的编辑。
  • 内置定义的只读弹窗也列出它的能力(技能 / MCP / 知识源 / 记忆档位)—— 否则「为什么这个 agent 够不到我的 Notion」无从排查。 「复制为系统级」完整携带四个维度,不做静默丢字段。

本期明确不做

这些是刻意留在边界外的,不是遗漏:
  • 向量 RAG / embedding / 切分:仓库目前无此基础设施。 真需要时它是 kb_search 的实现替换,不影响 schema;
  • 业务 API(HTTP)工具:WebFetch / WebSearch 无鉴权、无路径约束, 不新建受约束的 api_call;业务 API 走 MCP 通道;
  • 子代理嵌套委派:维持现状,delegate 不能继续 Task;
  • 把私有记忆导入主记忆:需要时人工复制文件即可, 加了反而引入不可逆的合并语义;
  • 跨工作区共享记忆:私有记忆绑在 <工作区> 下, 同一定义在两个工作区各有一份。

回到

四个调度工具、四层发现与活动回放。