Skip to content

Plugin 系统

src/lab/plugin/ + src/lab/plugins/

这是 XnneHangLab 的插件框架层:负责发现插件、读取 plugin.toml、按类型加载,并把结果交给 ToolManager、SystemPromptBuilder 和 HookManager。


设计动机

插件系统的目标不是把所有能力都塞进一个"大注册表",而是把不同扩展点拆开:

  • tool:给 Agent 增加可调用工具
  • skill:给 system prompt 增加操作指引
  • hook:在对话生命周期里插入额外逻辑

这样做的好处是,能力边界更清楚,Profile 也能按场景只启用需要的插件。


目录结构

text
src/lab/
├── plugin/                     # 框架层:共享基类、加载器、公共工具
│   ├── __init__.py
│   ├── loader.py               # PluginLoader:读取 plugin.toml 并加载插件
│   ├── hook.py                 # HookPlugin 抽象基类
│   ├── http.py                 # 共享 HTTP 工具
│   └── search_types.py         # 共享搜索类型
├── agent/
│   └── hook_manager.py         # HookManager:管理 hook 生命周期
└── plugins/                    # 具体插件目录
    ├── web_fetch/
    ├── web_search_ddg/
    ├── web_search_searxng/
    ├── screen_shot/
    ├── diary/
    └── memory/

plugin/ 是框架,plugins/ 才是插件本体。这层区分很重要,因为插件之间不应该互相 import。


plugin.toml 格式

每个插件目录下都必须有一个 plugin.toml

toml
[plugin]
id = "web_fetch"
name = "Web Fetch"
description = "抓取网页内容并提取正文"
type = "tool"

[config]
timeout_s = 10.0
max_chars_default = 8000
respect_robots = false

[type_config]
entry = "WebFetchPlugin"
字段说明
[plugin].id插件唯一标识,Profile 里靠它启用
[plugin].typetool / skill / hook
[plugin].description插件的简短说明
[config].*默认配置,可被 Profile 覆盖
[type_config].entrytool / hook 插件的入口类名
[type_config].filesskill 插件要读取的内容文件
[type_config].priorityskill 注入顺序
[type_config].inlineskill 是否直接内联进 system prompt
[type_config].requiresskill 依赖的工具名
[type_config].requires_packagehook 对应的 [package] 开关

插件类型

tool 插件

tool 插件实现 ToolPlugin,对外返回一个或多个 BuiltinTool

python
from lab.tools.plugin import ToolPlugin
from lab.tools.base import BuiltinTool

class WebFetchPlugin(ToolPlugin):
    name = "web_fetch"
    description = "抓取网页正文"

    def __init__(self, *, timeout_s: float = 10.0) -> None:
        self.timeout_s = timeout_s
        self._tool = _WebFetchTool(self)

    def get_tools(self) -> list[BuiltinTool]:
        return [self._tool]

skill 插件

skill 插件不会实例化 Python 类,而是由 PluginLoader 读出 SkillDescriptor

python
@dataclass
class SkillDescriptor:
    id: str
    name: str
    description: str
    files: list[str]
    priority: int
    inline: bool
    requires: list[str]
    plugin_dir: Path

SystemPromptBuilder 会根据 inline 决定是直接展开内容,还是只给出技能说明和文件路径。


hook 插件

HookPlugin 用来插入生命周期逻辑,目前提供两个钩子:

python
class HookPlugin(ABC):
    @abstractmethod
    async def on_before_turn(self, user_text: str, ctx: AgentContext) -> str | None:
        """轮次开始前调用。返回字符串时注入 memory_context,返回 None 则跳过。"""
        ...

    async def on_after_turn(self, user_text: str, assistant_text: str, ctx: AgentContext) -> None:
        """轮次结束后调用。默认空实现,子类按需覆盖。"""
        return

on_before_turn 负责——在每轮对话前拉取相关记忆注入上下文。on_after_turn 负责——在 complete_response 完整收齐后触发,用于持久化本轮对话内容。两个钩子失败时都静默处理,不影响主流程。


继续看什么


PluginLoader

PluginLoader 负责把磁盘上的插件描述转换成运行时对象:

python
from lab.plugin.loader import PluginLoader

loader = PluginLoader()

tool_plugins, skill_descriptors, hook_plugins = await loader.load_many(
    ["web_fetch", "web_search_ddg", "diary", "memory"],
    profile_overrides={
        "web_fetch": {"timeout_s": 15.0},
        "memory": {"search_limit": 10},
    },
)

加载流程是:

  1. 找到 src/lab/plugins/<id>/plugin.toml
  2. 读取 [plugin].type
  3. 合并 [config] 默认值和 Profile 覆盖值
  4. 按类型实例化 ToolPlugin / HookPlugin,或生成 SkillDescriptor
  5. 把结果交给上层模块继续注册

这里最实用的一点是:Profile 覆盖值是按插件 id 分发的,不需要插件自己再手动解析 TOML。


Profile 驱动

插件启用和配置来自 profiles/*.toml

toml
[plugins]
enabled = ["web_search_ddg", "web_fetch", "diary", "memory"]

[plugins.web_fetch]
timeout_s = 15.0

[plugins.memory]
user_id = "xnne"
agent_id = "elaina"   # 决定读写哪个 agent 的记忆,每个 profile 各自配
search_limit = 10

enabled 决定加载哪些插件,[plugins.<id>] 则覆盖对应插件 plugin.toml 里的 [config] 默认值。


隔离规则

插件之间不能互相 import,这是硬性规则。

如果两个插件都需要同一段逻辑,答案不是"让其中一个 import 另一个",而是把共享逻辑提上来放进框架层:

python
# ❌ 这个方向不对——web_fetch 不是给别人提供工具函数的
from lab.plugins.web_fetch import clamp_int

# ✅ 共享工具应该放在 lab.plugin.*,这才是它的家
from lab.plugin.http import clamp_int

这么设计是因为插件应该是"可替换单元"——你删掉一个插件,其他插件不应该跟着崩。一旦插件互相 import,这个保证就没了。


与其他模块的关系

  • Profile 系统 决定启用哪些插件以及覆盖哪些配置
  • 工具系统 负责承接 tool 插件返回的 BuiltinTool
  • Skill 系统 负责承接 skill 插件注入到 system prompt 的内容
  • HookManager 负责管理 hook 插件,在 AgentCore.run_turn() 前调用 before_turn(),结束后调用 after_turn()
  • AgentFactory 负责把 PluginLoader 的结果真正注册到运行时

魔女の实验室