资讯详情

0基础深入理解DeepSeek Harness 架构【9】作用域——一个进程里跑很多个互不干扰的 Agent

📅 2026/10/5 10:25:32 | 华诺云谱 👁 阅读
0基础深入理解DeepSeek Harness 架构【9】作用域——一个进程里跑很多个互不干扰的 Agent
本章摘要本章解决「同一进程里多个性格不同的 Agent 如何共存」的问题。核心思路是让注册本身带上作用域——同一个注册表、同一个上下文对象但「谁在注册」和「谁在读」决定了看到的东西不一样。全文依次讲清四件事① 三个核心原语ScopeKey/ScopedT/Scope与两个拆卸接口② 带作用域的注册表层ScopedLayers的惰性创建与回收规则③ 工具层面的两种差异化手段——作用域注册遮蔽与ToolRestriction过滤④ 更大粒度的 agent preset一整棵配置树级别的差异化与isolate隔离域。最后引入initiator发起方机制用于同进程内的因果归因并强调一条关键边界归因不等于授权。读完你会带走三句话作用域 可见性 所有权限制只作用于继承来的工具currentInitiator()是用来打日志的不是用来放行的。到这一章为止我们一直在讲「一个 agent 的世界」。但真实系统里往往同时跑着很多个主 agent、几个子 agent、一个总结用的辅助 agent。它们需要各自的工具集合、各自的提示词、各自的数据。这一章讲这套机制。9.1 问题的提出假设你要实现「同一个进程里两个 agentA 能删文件B 不能」。最直觉的做法是给服务加一个参数ctx.tools.get(name, agentId)。但这样会有一个连锁问题所有服务都要加这个参数而且每个调用点都必须传对。DeepSeek Harness 选择了另一条路**让注册本身带上作用域。**也就是说同一个注册表同一个上下文对象但「谁在注册」和「谁在读」决定了看到的东西不一样。9.2 核心三个词官方把这个包独立出来叫scope。它的定位很特殊scope/是这里唯一的非服务包一个零依赖库在模块图中位于session/与system-prompt/之下正是为了让它们消费它而不形成环。它的三个核心类型是类型是什么ScopeKey一个不透明的对象身份标识。已交付的 agent loop 用活跃的 Agent 对象本身当 key但这个原语从不检视那个对象——它只做身份比较ScopedT一个编译期的品牌标记标注在路由接收器上。它只记录「主体类型是什么」用于分派检查不暴露主体的属性。真正的事件主体仍然作为显式参数传入Scope一个「被铸造出来的注册上下文」配上两个拆卸接口关于Scope的两个拆卸接口官方设计得很细接口用途rawDispose保留 Cordis disposer 的确切身份。当你需要把这个作用域嵌进一个有序复合 effect 时用它——因为顺序在这里是有意义的dispose()面向直接调用方和竞态调用方的公共完全停稳边界。并发的多次调用会 await 同一个完成为什么要两个因为「释放」这件事有两种语境一种是「我是别人的一部分要和别人一起按顺序释放」另一种是「我就想现在把它关掉」。前一种需要精确的身份控制rawDispose后一种需要一个可以随便调、调多少次都行的接口dispose()。把它们分开比用一个接口满足两种需求更清晰。9.3 带作用域的注册表层实现「同一个注册表在不同作用域看到不同东西」的机制叫ScopedLayers。官方的说明里有三个很值得学的点规则为什么这么做全局 layer 立即创建作用域 layer 惰性创建绝大多数进程里只有少量作用域为每个潜在作用域都预建一层是浪费读取不会创建 layerpeek(undefined)表示「不存在作用域覆盖层」。如果读取也能创建 layer那么查询会导致内存增长——这是一个经典的隐性泄漏只在作用域 layer 的完整ScopeLayer为空时才回收它注意「完整」这个词layer 可能聚合了多个具名与匿名 table。只有全部都空了才能回收否则会丢掉兄弟 table 里的状态注册使用同一个上下文表示可见性与 effect 所有权这是关键一次注册既表达了「谁能看见它」也表达了「谁负责在卸载时撤销它」。它们本来就是同一件事另外还有两个容器原语它们的分工很明确容器特点NamedEntriesV按插入顺序查找支持动态迭代。重复项的错误由调用方处理——框架不替你决定「同名算不算冲突」AnonymousEntriesV每次 append 分配唯一标识因此「值相等」的条目仍然彼此独立两者都返回幂等重复执行多次效果和执行一次完全一样。所以可以安全地重试、且精确对应相应条目的撤销函数。还有一个迭代器的语义细节官方写得很精确**在同一轮非空 table 生命周期内迭代器可以观察后续变化table 被清空后现有迭代器不会再观察后续插入。**这个「清空是一道分界线」的规则避免了迭代器在一个已经被回收的表上继续工作。9.4 Agent 自己的作用域agent.ctx回到实际使用。每个 Agent 句柄上都挂着两样东西成员说明agent.ctxAgent 作用域的上下文。它的贡献是 agent 局部的、在销毁时撤销并且销毁后拒绝继续注册agent.session这个 agent 驱动的活跃会话它的日志是持久的真相来源于是「把注册限定到单个 agent」的做法就非常直接**用该 agent 的agent.ctx注册。**官方在「新行为的归属位置」表里就是这么写的。9.5 工具的作用域与限制工具这一侧有一对机制作用域注册和限制过滤器。它们解决的是两个不同的问题。机制一作用域注册遮蔽官方说明作用域内的工具会「遮蔽」shadow全局工具。同一个名字某个 agent 作用域里注册的那个会盖住全局的那个。而且注册表有一个统一的可见性解析器同时供展示、查找和执行三方使用。最后这一句很重要它保证了你看到的卡片、实际查到的定义、真正执行的代码是同一个。很多系统的 bug 就出在这里三者用了不同的解析逻辑。机制二ToolRestriction过滤当你想「隐藏」一批工具而不是替换它们时用过滤器interfaceToolRestriction{/** 保持可见的全局工具名其余全部移除 */readonlyallow?:readonlystring[]/** 从可见性中移除的全局工具名 */readonlydeny?:readonlystring[]}它的语义有几个精确的规则都很有用规则说明作用于继承来的工具也就是「部署全局层 这条链上的每个祖先作用域」。不作用于该作用域自己注册的工具多个限制取交集两个过滤器都限制的东西才会被限制掉——这样限制只能越加越严不能被后来的放宽自身注册不受约束官方给的理由很实际「因此被委派的子 agent 会保留其回报所依赖的工具」。如果连自己注册的工具都被自己的过滤器挡掉子 agent 就没法向父级汇报了仅 deny 的过滤器允许后续未列出的继承工具通过allow 列表则排除它们这是一个很容易搞反的地方只写 deny 是「黑名单」语义写了 allow 就变成「白名单」语义保留的run_code名字不能被限制它是 PTC 的传输名属于框架保留字限制只作用于「继承来的」工具自己注册的不受影响。这条规则是为了不切断子 agent 的汇报通道。9.6 agent preset一整棵配置树级别的差异化上面的机制解决的是「工具层面」的差异。如果差异更大呢比如某个会话需要一整套不同的配置——不同的模型、不同的工具集、不同的提示词、甚至不同的服务实现。这时用 agent preset。官方的定义是立即挂载 YAML 声明的 preset 版本把 Agent 和冷读取不启动 agent直接读已经落在磁盘上的数据。特点是快、便宜、不占资源绑定到作用域内的贡献并保留已替换的版本直到最后一个使用者释放它。几个关键事实preset 用YAML 声明因此不用写代码就能定义一套 agent 组合。它有版本的概念替换之后旧版本不会立刻被销毁而是保留到最后一个使用者释放它——这就是热重载能安全工作的前提。它提供select在会话开始第一个轮次之前选择、recompose重新绑定一个空白的 agent、composeFrom子 agent 加入父级保留的那个确切版本等操作。它还有一个查询方法serviceFor(agent, name)读取某个 agent 在其隔离 preset 组里提供的服务。关于「让某个会话拥有不同的能力集合」官方在归属位置表里的说法是组装一个 agent preset其中的服务行需要isolaterealm。这里的isolaterealm隔离域是一个重要的机制它让一组插件在同一个隔离区域里运行这样即使它们和外部的插件同名也不会互相干扰。官方在讲 Loader 内置插件时也提到group 行能把「一个提供方与它的消费方放进同一个isolaterealm」。关键作用域解决的是「可见性问题」preset 解决的是「组合问题」。前者管「你能看到哪些工具」后者管「你生活在哪一套配置里」。这两层是叠加的不是二选一。9.7 发起方initiator谁在发起这件事多 agent 场景里有一个很实际的需求某个异步操作执行到一半需要知道「这件事最开始是谁发起的」——为了打日志、做追踪、或者做归因。官方为此提供了一组方法并且对它们的语义做了非常克制的界定。三句话概括方法做什么currentInitiator()读取发起这个异步驱动链的 Agent。如果没有返回undefined。适用于「也支持无 agent 调用」的场景requireInitiator()读取发起方没有就抛异常。适用于「契约上禁止无 agent 调用」的私有助手或部署方拥有的外发请求withInitiator(agent, op)用一个确切的 Agent 作为进程本地的发起方来运行一个操作withoutInitiator(op)在一个「隐藏任何继承来的发起方」的边界里运行操作官方对这套机制的定位有一句非常重要的限定值得原样引用发起方方法只提供同进程内的因果归因。环境中的存在既不能证明存活也不代表授权主体和所有者始终是显式的在 worker、进程、持久化和线路边界上的身份同样是显式的。反直觉「有发起方」不等于「有权限」。这一点很容易被误用成一种隐式的授权机制看到currentInitiator()有值就认为「这是被授权的调用」。官方明确否定了这种用法——归因和授权是两件事。授权必须走审批 seam。另外还有一条很细腻的边界withoutInitiator的用途是——当你创建「惰性的共享定时器、队列泵、池维护、watcher 或 exporter」时让它们不要继承「恰好第一个初始化它们的那个 Agent」。否则那个 agent 的归因会莫名其妙地粘到整个进程的基础设施上。9.8 这一章要带走的三句话这一章要带走的三句话**作用域 同一次注册同时表达可见性和所有权。**它们本来就是一件事。**限制只作用于继承来的工具自己注册的不受影响。**这是为了让子 agent 还能汇报。归因不等于授权。currentInitiator()是用来打日志的不是用来放行的。这一章之后上一章讲的是能力 seam换掉一项底层能力时改动该落在哪里这一章讲的是同一套能力怎么按层级分发给不同的 Agent——前者改的是零件本身后者改的是零件能被谁看见。到这里全书的结构部分就全部讲完了。剩下的只有一件事把它用起来也就是下一章的内容。内容整理自 DeepSeek Harness 官方仓库docs/architecture.zh.md及其引用的子系统文档。官方项目处于开发者预览阶段具体字段与命令请以你手上的代码为准。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑