轻量Agent框架设计复盘:插件系统从v1到v3的架构演化与设计决策

发布时间:2026/7/22 1:01:58
轻量Agent框架设计复盘:插件系统从v1到v3的架构演化与设计决策 轻量Agent框架设计复盘插件系统从v1到v3的架构演化与设计决策一、v1的起点一个简单的工具注册表2025年Q1开始构建一个轻量Agent框架。v1的设计很简单——一个全局的工具注册表var toolRegistry map[string]ToolFunc{} func RegisterTool(name string, fn ToolFunc) { ... } func CallTool(name string, args map[string]any) (any, error) { ... }Agent调用工具时LLM生成工具名和参数框架从注册表查找并调用。这个设计支撑了前3个月的所有功能。直到需要支持工具需要初始化配置如API Key、工具调用需要限流、工具间需要依赖关系时发现全局注册表模式已经到上限了。v1的核心问题工具是全局单例无法隔离配置和状态工具注册是静态的运行时无法添加或更新。二、v2到v3的两次关键重构v2重构目标从函数到接口v2定义了Plugin接口将工具从函数提升为带生命周期的对象type Plugin interface { Name() string Description() string Schema() jsonschema.Schema // 工具的输入参数定义 Init(config PluginConfig) error Execute(ctx context.Context, input map[string]any) (any, error) Stop() error } type PluginConfig struct { Settings map[string]any RateLimit *RateLimitConfig } // v2的工具注册——工厂模式 type PluginFactory func(config PluginConfig) (Plugin, error) type PluginManager struct { mu sync.RWMutex plugins map[string]Plugin factories map[string]PluginFactory } func (pm *PluginManager) Create(name string, config PluginConfig) error { pm.mu.Lock() defer pm.mu.Unlock() factory, ok : pm.factories[name] if !ok { return fmt.Errorf(plugin %s not registered, name) } plugin, err : factory(config) if err ! nil { return fmt.Errorf(create plugin %s: %w, name, err) } if err : plugin.Init(config); err ! nil { return fmt.Errorf(init plugin %s: %w, name, err) } pm.plugins[name] plugin return nil }v2解决了配置隔离和生命周期管理问题。但新的痛点是多个Plugin共享同一个HTTP Client或数据库连接时重复创建资源插件执行需要统一的限流和重试但与业务逻辑混在一起。v3重构目标依赖注入 中间件管道v3引入了一个轻量DI容器和中间件管道// v3DI容器管理共享资源 type Container struct { services map[string]any } func (c *Container) Provide(name string, service any) { c.services[name] service } // 中间件管道 type Middleware func(next PluginExecutor) PluginExecutor type PluginExecutor func(ctx context.Context, plugin Plugin, input map[string]any) (any, error) // 内置中间件 func RateLimitMiddleware(limiter *rate.Limiter) Middleware { return func(next PluginExecutor) PluginExecutor { return func(ctx context.Context, plugin Plugin, input map[string]any) (any, error) { if !limiter.Allow() { return nil, ErrRateLimited } return next(ctx, plugin, input) } } } func RetryMiddleware(maxRetries int, backoff time.Duration) Middleware { return func(next PluginExecutor) PluginExecutor { return func(ctx context.Context, plugin Plugin, input map[string]any) (any, error) { var lastErr error for i : 0; i maxRetries; i { result, err : next(ctx, plugin, input) if err nil { return result, nil } lastErr err time.Sleep(backoff * time.Duration(1i)) // 指数退避 } return nil, lastErr } } } // 管道组装 func (pm *PluginManager) Execute(ctx context.Context, name string, input map[string]any) (any, error) { pm.mu.RLock() plugin : pm.plugins[name] pm.mu.RUnlock() if plugin nil { return nil, ErrPluginNotFound } // 组装中间件管道 executor : pm.baseExecutor // 核心执行逻辑 for _, mw : range pm.middlewares { executor mw(executor) // 层层包裹 } return executor(ctx, plugin, input) }v3的设计让限流和重试从业务代码中完全移除成为可插拔的中间件。更重要的是通过DI容器Plugin从容器获取共享资源而非自己创建// v3的Plugin——通过DI容器获取依赖 type WebSearchPlugin struct { httpClient *http.Client // 从容器注入 cache Cache // 从容器注入 } func (p *WebSearchPlugin) Init(config PluginConfig) error { // 从DI容器获取共享资源而非自己创建 p.httpClient config.Container.Get(httpClient).(*http.Client) p.cache config.Container.Get(cache).(Cache) return nil }三、WASM沙箱v3的隔离边界v3最激进的改动是引入了WASM插件支持。动机用户提交的自定义插件代码需要沙箱隔离执行不能直接在服务进程内运行。type WASMPluginRuntime struct { engine *wasmtime.Engine pool *SandboxPool // 预创建沙箱池 } type SandboxPool struct { available chan *wasmtime.Store factory func() (*wasmtime.Store, error) maxSize int } func (p *SandboxPool) Acquire(ctx context.Context) (*wasmtime.Store, error) { select { case store : -p.available: return store, nil case -ctx.Done(): return nil, ctx.Err() default: // 池已空创建新的不超过上限 return p.factory() } } func (p *SandboxPool) Release(store *wasmtime.Store) { select { case p.available - store: default: // 池已满丢弃 } }WASM方案的抉择用WASM还是用Docker容器做隔离WASM的启动时间约1msDocker容器约500ms。对于每个工具调用可能触发多次插件执行的场景WASM的冷启动优势是决定性的。但代价是WASM生态不如Docker成熟——某些需要系统调用的插件无法在WASM中运行。四、设计的取舍与反思v1→v2的正确之处从函数到接口的抽象是必要的。没有这个抽象后续所有功能配置管理、生命周期、热更新都无法实现。v2→v3的争议之处DI容器的引入增加了框架的复杂度。部分用户反馈我只想写个搜索插件为什么要理解DI容器——这是过度抽象的信号。后来的改进是DI容器对简单插件的使用者完全透明只有高级用户才需要感知。WASM的决定从性能和隔离性角度是正确的。但维护成本高——WASM编译目标需要额外的工具链构建时间增加。如果用户不需要自定义插件执行WASM是多余的负担。五、总结插件系统从v1到v3的核心经验v1的全局注册表够用3个月不要过早抽象v2的接口抽象解决的是配置隔离和生命周期问题值得投入v3的中间件管道让横切关注点从业务代码中分离可维护性大幅提升DI容器对复杂场景有价值但对简单插件是负担——分层暴露渐进可用WASM沙箱启动快但生态有限适用于计算密集型操作不适合需要系统调用的场景最大教训每次架构升级后都要给用户留一个简单路径。框架不能强迫所有使用者承受最复杂场景的设计代价。好的框架应该让简单的事保持简单复杂的事才需要复杂。