组件自扩展

Agent 的能力不是写死的。遇到没有现成工具的任务,它可以现场写一个组件、落盘、3 秒后自己用上——这就是自扩展闭环。

闭环流程

一次自扩展通常走完这六步。任何一步失败都要停下来修,不要叠加着往下走。

  1. 任务进来,现有工具都不合适
  2. 用 component.dir() 确认真实写入路径(pip 安装与绿色版不同,不要猜)
  3. 取模板或直接 generate,把骨架写到 components/ 下
  4. 补全 _cmd_ 方法的实现与 docstring
  5. 等 3 秒,或手动 reload(),确认出现在工具列表里
  6. 立刻用真实输入调一次验证,通过才算完成

component 工具命令

内置组件 component 是自扩展的入口,11 个子命令覆盖生成到排障。

命令用途
template(style)取模板代码:minimal / full / example / background
generate(name, description, commands)生成单文件组件并写入 components/
generate_package(name, description, commands, modules)生成包组件(文件夹 + modules),适合复杂功能
list / info(name)列出已注册组件,可按名搜索、分页
reload()手动触发一次全量扫描重载
dir()返回组件的真实目录路径
sandbox_list / sandbox_add / sandbox_remove查看 / 加入 / 移出免沙箱白名单,改完立即生效
cleanup(dry_run, force)清理加载错误、失效白名单条目、残留沙箱进程
install(package, upgrade)从 PyPI 安装组件包(cellium-comp-*)

生成与生效

# 生成骨架
component.generate("hash_tool", "File hash calculator",
  commands='[{"name":"md5","desc":"Calculate MD5 hash"},{"name":"sha256","desc":"Calculate SHA256 hash"}]')

# 文件落盘 → 3 秒内被 watcher 捕获 → 自动加载 → 立刻可调用
hash_tool.md5(input_data="/etc/hostname")

让 LLM 自己查用法

组件工具的 description 会随命令表变化。拿不准某个命令的参数时,先调一次 help 或 info,比反复试错快。

热插拔监控

ComponentWatcher 每 3 秒拍一次快照(路径 → mtime),只处理顶层 .py 文件。

事件动作
新文件出现实例化、注册、包装成工具、写入配置
mtime 变化卸载旧实例后重新加载,on_load 重新执行
文件被删除从内存与工具注册表移除,配置同步清理

注意:包组件(文件夹)只按目录内所有 .py 的最大 mtime 判断,watcher 本身只扫顶层 .py。改包内部文件后需要 component.reload() 才会生效。

组件规范

照这个骨架写,参数 schema 和工具描述都能自动生成正确。

from typing import Any, Dict
from app.core.interface.base_cell import BaseCell


class HashTool(BaseCell):
    """文件哈希计算器。支持 md5 / sha256。"""

    @property
    def cell_name(self) -> str:
        return "hash_tool"

    def _cmd_md5(self, input_data: str = "", **kwargs) -> Dict[str, Any]:
        """计算文件 MD5

        Args:
            input_data: 文件路径
            **kwargs: session_id / platform_context 由框架注入

        Returns:
            {"success": bool, "md5": str}
        """
        import hashlib
        h = hashlib.md5()
        with open(input_data, "rb") as f:
            for chunk in iter(lambda: f.read(1 << 20), b""):
                h.update(chunk)
        return {"success": True, "md5": h.hexdigest()}
  • cell_name 用 @property 返回,全小写、全局唯一,它就是工具名
  • 命令方法必须 _cmd_ 前缀,docstring 第一行是描述,Args: 段是参数说明
  • 必须返回 Dict(不要返回裸字符串),错误也返回 error 字段而不是抛异常
  • 框架会注入 session_id / platform_context 到 kwargs,需要时直接取

保留工具名

  • shell — ShellTool
  • memory — MemoryTool
  • file — FileTool

沙箱陷阱

默认子进程执行,很多「本地能跑、组件里不行」都是这个原因。

  • 子进程拿不到主进程的内存状态,跨命令共享数据要落文件或进白名单
  • 进程空闲 300 秒被回收,模块级变量会丢;需要常驻状态就别依赖全局变量
  • 单次命令超过 120 秒直接判超时,长任务拆成多步或放后台

目录结构

components/
├── __init__.py
├── _example_component.py    # 官方模板(以 _ 开头,不参与加载)
├── component_builder.py     # component 工具本体
├── scheduler.py             # 定时任务
├── skill_manager.py         # Skill 索引
├── skill_installer.py       # Skill 安装
├── sub_agent.py             # 子 Agent 并行
├── web_fetch.py / web_search.py
├── qq_files.py / telegram_files.py / feishu_files.py / weixin_files.py
├── skills/                  # Skill 包目录
└── my_tool.py               # Agent 自己生成的组件

为什么能立刻用上

AgentLoop 不在启动时固化工具列表,而是每轮对话都向 ComponentToolRegistry 重新读取。所以组件加载完成的下一轮,LLM 的可用工具里就有它——不需要重启进程。

Key Point

自扩展的边界:能生成组件,但不要自动 pip 安装依赖、不要改内置组件文件、不要在没有验证的情况下宣称完成。生成 → 生效 → 真实调用验证,三步都过了才算成功。