元框架系列教程第三篇——注册你的槽位
2026/8/25 14:55:21 网站建设 项目流程

第 3 篇:槽位注册

如何让 Norp 认识你定义的“新插口”

3.1 从问题出发

上一篇你学会了把组件放进注册表:

reg.register_tool("weather", WeatherTool()) reg.register_model("my_model", MyModel())

然后呢?

你要用它们,得通过槽位:

npa(tools=["weather"], model="my_model")

问题来了:

modeltoolssessionsandbox……这些槽位名字是哪里来的?为什么npa()model但不认my_custom_slot

答案是:这些名字来自一张槽位表(SLOT_SPECS)npa()启动时,先把你的参数名跟这张表做匹配:

  • 参数名在表里 → 当作槽位值处理
  • 参数名不在表里 → 当作任务参数透传(比如max_stepstask_timeout

默认情况下,槽位表里有 18 个内置名字。你想让npa(my_custom_slot=...)被当作槽位识别,就得往这张表里加一条记录。

这就是“槽位注册”——给 Norp 定义一个新的可填参数。

3.2 槽位的本质是什么?

一个槽位 = 三个东西的组合:

要素含义举例
名字npa()的关键字参数名"model""tools""vector_store"
语义字符串值怎么解释是模块地址?还是注册表名?还是字面值?
动作值非空时做什么注册到注册表?订阅钩子?写入extras

内置的 18 个槽位已经在框架启动时注册好了。你现在要做的是:把自己定义的新插口也加进去。

3.3 注册一个槽位(最小写法)

注册槽位用register_slot

from norpagent.arch import SlotSpec, register_slot register_slot(SlotSpec( name="audit_tag", string_semantics="literal", applier=_apply_audit_tag, ))

然后就可以用了:

npa(audit_tag="release-1")

这四个字段是最小集合:

  • name:槽位名,也就是npa()的参数名
  • string_semantics:字符串值的解释方式
  • applier:值非空时执行什么动作

注册后,npa(audit_tag="release-1")的行为是:

  1. 参数audit_tag被识别为槽位,不走任务参数透传
  2. 装配器把"release-1"(literal 语义,原样保留)传给applier
  3. applier拿到值,执行你定义的逻辑

3.4 string_semantics:字符串的四种解释方式

这是槽位注册里最需要想清楚的一点。

string_semantics决定了一个字符串进入这个槽位后怎么解释。有四种选择:

语义含义举例
address字符串 = 模块地址"myapp.models:create"→ 加载模块并实例化
name字符串 = 注册表组件名"openai_compat"→ 去注册表查这个名字
name_or_address先按注册表名查,查不到再按地址加载npa(model="something")
literal字符串 = 字面值,但“形如地址”的按地址加载"high"是级别,"myapp.sec:build"是地址

选择哪种语义取决于你的槽位要什么类型的值:

  • 你的槽位要传一个模块地址 → 用address
  • 你的槽位要传一个已注册组件的名字 → 用name
  • 你的槽位既要支持已注册名又要支持外部地址 → 用name_or_address
  • 你的槽位主要传字面值(级别、路径、目录),偶尔传地址 → 用literal

3.5 applier:值非空时做什么

applier是槽位的“执行逻辑”。它的签名是:

def applier(reg, layer, value, params, ctx): # reg: 注册表实例 # layer: 架构层实例 # value: 解析后的槽位值(字符串已按 string_semantics 解释过) # params: 运行时参数 dict # ctx: 四个可变容器 pass

ctx里有四个你可以操作的东西:

容器用途
ctx["components"]声明预设组件{kind: name},AgentRuntime 会据此构建组件
ctx["extras"]往引擎上挂额外对象,通过engine.extras[槽位名]取用
ctx["overrides"]改写预设的字段(比如覆盖preset.model
ctx["meta"]记录需要清理的对象,热重载时用于退订

applier 的核心职责:把“槽位值”翻译成“系统动作”。光有值没用,你得告诉框架怎么用这个值。

3.6 完整示例:开发一个“向量检索”槽位

目标:新增一个vector_store槽位。用户传值后,自动注册为通用组件,工具通过ctx.component("vector_store")取用。

from norpagent.arch import SlotSpec, register_slot def apply_vector_store(reg, layer, value, params, ctx): # 1. value 可能是实例、工厂、或已解析的模块 factory = value if callable(value) else (lambda v=value: v) # 2. 注册为通用组件(_arch_vector 是内部名字,不冲突) reg.register_component("vector_store", "_arch_vector", factory) # 3. 写入预设组件声明 → AgentRuntime 构建 ctx.components ctx["components"]["vector_store"] = "_arch_vector" # 4. 同时挂到 extras,引擎侧可直接取用 ctx["extras"]["vector_store"] = value register_slot(SlotSpec( name="vector_store", description="向量检索组件", string_semantics="literal", applier=apply_vector_store, remount_rebuild_agent=True, # 热替换后重建 AgentRuntime ))

使用:

npa(vector_store=MyVectorStore()) # 或 npa(vector_store="myapp.vector:create") # 或 npa(vector_store={"backend": "pg", "index": "./idx"})

工具侧取用:

store = ctx.component("vector_store")

3.7 remount_rebuild_agent:热替换后要不要重建?

自定义槽位分为两类:

  • 组件型:只往注册表/注册表里挂了点东西,下一次run()就能生效。这种不需要重建。
  • 装配型:改写了预设的componentsoverrides,或者替换了引擎核心组件。这种必须重建才能生效。

remount_rebuild_agent告诉框架:热替换这个槽位后,要不要触发AgentRuntime热重建。

  • False(默认):只更新extras/注册表,不重建引擎
  • True:热替换后立即重建AgentRuntime,新组件立即生效

vector_store例子改写了components,所以设True

3.8 重入安全:热替换时别叠罗汉

一个关键问题:

applier会在多个时机被重复调用:

  • 启动时装配
  • 每次npa.remount(vector_store=...)
  • 每次npa.remount()触发全量重新装配

如果applier每次都在reg.bus上挂一个新订阅,那订阅会越叠越多,同一个事件触发十几次。这是 bug。

解法:用ctx["meta"]记录旧对象,重挂前先清理。

内置的hooks槽位、security槽位、plugins槽位都遵循这个模式。框架热重载时会先调用卸载逻辑,再重新装配,保证:

  • 事件订阅不叠加
  • 组件注册不堆积
  • 沙箱/会话等资源被正确释放
def apply_my_slot(reg, layer, value, params, ctx): # 如果有旧订阅,先退订 meta = ctx.get("meta", {}) old = meta.get("my_slot_subscription") if old: reg.bus.unsubscribe(old) meta.pop("my_slot_subscription", None) # 挂新订阅 def handler(event): print(event) reg.bus.subscribe(handler, "on_content") meta["my_slot_subscription"] = handler

这样每次重挂都先清理再挂新,干净利落。

3.9 检查槽位注册是否成功

注册自定义槽位后,用layer.describe()看装配清单:

eng = npa.current() print(eng.layer.describe())

输出里能看到:

vector_store <- 地址 'myapp.vector:create' => VectorStore 实例 audit_tag <- 直接值 'release-1' => str

如果看不到,说明槽位没注册上,或者注册时机晚于npa()启动。

3.10 内置槽位受保护

18 个内置槽位名是受保护的:

async_loop, agent_runtime, model, tools, session, sandbox, scheduler, context_store, project_manager, hooks, security, plugins, frontend, ui, preset, logger, storage, error_handler

不能覆盖它们的规格,不能注销它们。它们的随时可以npa.remount热替换,但“插口本身”是框架结构的一部分,不可删除。

3.11 你现在能做的事情

读完这篇,你已经能:

  • 理解槽位 = 名字 + 语义 + 动作
  • register_slot注册自定义槽位
  • 选择合适的string_semantics
  • applier把槽位值翻译成系统动作
  • 理解remount_rebuild_agent什么时候用 True
  • ctx["meta"]保证热重载不叠加订阅
  • layer.describe()检查槽位是否生效

下一篇,你会学到地址解析——字符串"myapp.models:create"是如何变成一个可调用对象的。

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

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

立即咨询