第 3 篇:槽位注册
如何让 Norp 认识你定义的“新插口”
3.1 从问题出发
上一篇你学会了把组件放进注册表:
reg.register_tool("weather", WeatherTool()) reg.register_model("my_model", MyModel())然后呢?
你要用它们,得通过槽位:
npa(tools=["weather"], model="my_model")问题来了:
model、tools、session、sandbox……这些槽位名字是哪里来的?为什么npa()认model但不认my_custom_slot?
答案是:这些名字来自一张槽位表(SLOT_SPECS)。npa()启动时,先把你的参数名跟这张表做匹配:
- 参数名在表里 → 当作槽位值处理
- 参数名不在表里 → 当作任务参数透传(比如
max_steps、task_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")的行为是:
- 参数
audit_tag被识别为槽位,不走任务参数透传 - 装配器把
"release-1"(literal 语义,原样保留)传给applier 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: 四个可变容器 passctx里有四个你可以操作的东西:
| 容器 | 用途 |
|---|---|
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()就能生效。这种不需要重建。 - 装配型:改写了预设的
components或overrides,或者替换了引擎核心组件。这种必须重建才能生效。
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"是如何变成一个可调用对象的。