1. 从能跑就行到想拆开看看一个 Agent 框架的吸引力在哪第一次看到 DeepSeek 开源 Agent 框架的消息时我的反应和大多数人差不多——又一个 Agent 框架能有什么新东西市面上做 Agent 编排的轮子已经不少了从早期的任务链式调用到后来的多智能体协作概念翻来覆去就那么几套。但真正让我决定花时间把它拆一遍的原因是社区里反复出现的一个评价插件化做得非常干净。干净这个词在工程语境里其实很奢侈。它意味着接口边界清晰、依赖关系不纠缠、扩展点设计得恰到好处。大多数框架的插件系统要么是简单的注册回调要么是过度设计的抽象层用起来总有一种为了扩展而扩展的别扭感。而这个框架被反复提到插件化说明它在架构层面确实做了不一样的选择。这篇文章适合两类人看一类是正在选型 Agent 框架、想知道它值不值得投入时间的技术负责人另一类是自己动手写过 Agent 编排逻辑、想看看成熟项目怎么处理扩展性问题的开发者。我会从架构分层、插件注册机制、工具调用链路、上下文管理、多 Agent 协作几个维度把拆解过程中的发现和思考完整记录下来。不是官方文档的复述而是一个从业者拿着放大镜看代码时的真实感受。拆之前我先说结论这个框架的插件化不是支持插件这么简单它把插件当作一等公民来设计整个运行时都是围绕插件生命周期构建的。这个设计决策带来的连锁反应值得仔细聊。2. 架构分层为什么它没有走大一统路线2.1 核心层与扩展层的物理隔离很多 Agent 框架在架构上犯的一个通病是核心逻辑和扩展逻辑混在一起。你去看源码目录core/里面塞满了各种工具实现、模型适配器、记忆模块想换个向量数据库得改核心文件。这种设计在项目早期跑得快但一旦要支持多种后端代码就会变成一团乱麻。这个框架的做法很干脆核心层只定义协议和生命周期所有具体实现都在扩展层。我拆的时候特意看了它的模块依赖图核心包对外部依赖几乎为零连 HTTP 客户端都是抽象接口。这意味着你可以把核心层单独拿出来做单元测试不需要启动任何外部服务。具体来说核心层包含的东西非常克制Agent 抽象基类定义run、step、reset等生命周期方法但不关心具体怎么执行消息协议统一的消息格式包括角色、内容、工具调用请求和结果插件注册表管理插件的注册、发现和实例化执行引擎驱动 Agent 循环但具体每一步做什么由插件决定扩展层则包含所有可变的部分模型提供方适配、工具实现、记忆存储、输出解析器等等。这种分层的好处是核心层的稳定性极高扩展层的迭代不会影响核心逻辑。2.2 依赖注入的时机选择拆到依赖注入这块时我注意到一个细节框架没有在 Agent 初始化时就注入所有依赖而是延迟到插件实际被调用时才解析。这个选择乍看只是性能优化实际上影响很大。假设你的 Agent 配置了十个工具但某次对话只用了其中两个。如果初始化时就实例化所有工具那八个没用的工具白白消耗了资源。更关键的是有些工具可能依赖外部服务比如数据库连接、API 客户端提前初始化意味着提前建立连接在工具根本没被用到的情况下这些连接就是浪费。延迟解析的代价是每次调用都要查一次注册表但这个开销在 Agent 场景下完全可以忽略——毕竟一次模型推理的时间是毫秒到秒级查表是微秒级。框架在这里做了一个正确的取舍用可忽略的性能损失换取资源利用率的提升。注意延迟解析要求插件实现必须是幂等的或者框架要保证单例。我拆的时候确认了注册表内部用了缓存机制同一个插件多次请求返回同一实例不会重复创建。2.3 配置驱动的插件装配框架的插件装配完全由配置驱动这一点在拆解时让我觉得非常舒服。你不需要改任何代码来增减插件只需要调整配置文件。配置的结构大致是这样的agent: name: my_agent plugins: - type: model provider: openai_compatible config: base_url: http://localhost:8000/v1 model: my-model - type: tool name: calculator enabled: true - type: memory backend: in_memory config: max_turns: 20这种设计的好处是部署时不需要重新构建镜像。运维人员可以通过环境变量或配置中心动态调整 Agent 的能力组合。对于需要频繁调整工具集的场景比如客服机器人要根据业务线切换知识库这个特性非常实用。但配置驱动也有代价类型安全变弱了。配置文件写错了字段名可能要等到运行时才发现。框架的应对方式是提供了配置校验钩子插件可以在加载时校验自己的配置段不合法就抛出明确的错误信息。这个补偿机制做得比较到位。3. 插件注册机制从发现到实例化的完整链路3.1 注册表的三种发现模式拆到插件注册表时我发现它支持三种发现模式覆盖了不同的使用场景显式注册是最基础的方式开发者手动调用registry.register(plugin_class)把插件类注册进去。这种方式最直接适合插件数量少、关系明确的场景。缺点是新增插件要改代码不够灵活。入口点发现利用了 Python 的entry_points机制。插件包在setup.py或pyproject.toml里声明入口点框架启动时自动扫描已安装的包把符合条件的插件加载进来。这种方式适合插件以独立包形式分发的场景用户pip install之后就能用不需要改任何代码。目录扫描是第三种方式框架会扫描指定目录下的 Python 文件把符合插件协议的对象自动注册。这种方式适合快速原型开发把插件文件往目录里一扔就能生效。三种模式不是互斥的框架会按优先级依次尝试。我实测下来的感受是入口点发现是最优雅的但调试时不如显式注册直观。目录扫描最方便但生产环境要小心因为目录里任何符合协议的对象都会被加载可能引入意料之外的插件。3.2 插件协议的接口设计插件协议的设计是判断一个框架插件化水平的关键。我仔细看了它的接口定义核心方法只有三个class Plugin(Protocol): property def name(self) - str: ... def setup(self, context: PluginContext) - None: ... async def execute(self, input: PluginInput) - PluginOutput: ...name是唯一标识用于注册和查找。setup在插件被实例化后调用一次用于初始化资源、读取配置。execute是实际执行逻辑异步接口。这个接口设计的好处是极简。三个方法覆盖了插件的完整生命周期没有多余的钩子。对比一些框架动辄十几个生命周期方法的插件接口这个设计降低了实现负担。但极简也有代价某些高级场景比如插件间的依赖排序、热重载需要框架在外部提供支持而不是通过接口暴露。我注意到setup接收一个PluginContext对象里面包含了配置、日志器、其他插件的引用等。这个设计让插件可以访问框架提供的公共服务而不需要自己创建。比如日志器插件直接用context.logger就行格式和框架其他部分保持一致。3.3 插件间的通信与依赖插件之间怎么通信是插件化架构里最容易出问题的地方。如果插件直接互相引用就形成了硬依赖一个插件的改动可能影响另一个。这个框架的做法是通过上下文对象暴露服务插件按名称查找依赖。举个例子一个网页搜索插件可能需要HTTP 客户端插件来发请求。它不会直接import那个插件而是在setup时通过context.get_plugin(http_client)获取引用。如果那个插件不存在就抛出明确的错误。这种松耦合让插件可以独立替换——只要新插件实现了相同的接口调用方不需要改代码。但这里有个坑插件加载顺序会影响依赖解析。如果搜索插件在 HTTP 客户端插件之前加载get_plugin就会失败。框架的解决方案是支持声明依赖插件可以在元数据里声明dependencies: [http_client]注册表会做拓扑排序确保依赖先加载。我拆的时候验证了这个逻辑排序算法用的是标准的 Kahn 算法处理循环依赖时会报错。提示写插件时尽量声明依赖不要依赖加载顺序的巧合。我见过因为字典遍历顺序变化导致插件加载失败的案例排查起来很费时间。4. 工具调用链路一次请求到底经历了什么4.1 从用户输入到工具选择的完整流程理解工具调用链路最好的方式是跟着一次真实请求走一遍。假设用户问帮我算一下 1234 乘以 5678框架内部发生了什么第一步输入预处理。用户消息进入 Agent 后先经过输入解析插件。这个插件负责把原始文本转换成框架内部的消息格式可能还包括一些清洗和标准化。这一步是可选的如果没配置解析插件消息直接透传。第二步上下文组装。框架从记忆插件中取出历史对话和当前消息一起组装成模型需要的上下文。这里涉及一个关键决策历史对话保留多少轮。保留太多会超出模型上下文窗口保留太少会丢失信息。框架把这个决策交给记忆插件不同的记忆后端可以有不同的策略。第三步模型推理。组装好的上下文发给模型插件模型返回的结果可能是纯文本也可能是工具调用请求。框架不关心模型是什么只关心返回格式是否符合协议。第四步工具调用解析。如果模型返回了工具调用请求框架会解析出工具名称和参数然后从注册表中查找对应的工具插件。这里有个细节工具名称的匹配是大小写敏感的而且支持命名空间前缀比如math.calculator避免不同插件包之间的名称冲突。第五步工具执行。找到工具插件后框架调用它的execute方法传入参数。执行可能是异步的框架会等待结果。如果工具执行超时或抛异常框架会捕获并转换成模型能理解的错误消息。第六步结果回传模型。工具执行结果被包装成消息追加到上下文中再次发给模型。模型根据工具结果生成最终回复。这个循环可能重复多次直到模型不再请求工具调用。4.2 工具描述的自动生成与手动覆盖模型怎么知道有哪些工具可用靠的是工具描述。框架会从插件中提取工具的名称、描述、参数 schema组装成模型能理解的格式。提取过程支持自动和手动两种模式。自动模式从插件的类型注解和文档字符串生成 schema。比如一个函数的参数标注了int类型文档字符串写了参数说明框架就能生成对应的 JSON Schema。这种方式省事但生成的描述可能不够精确影响模型的工具选择准确率。手动模式允许插件显式提供 schema完全控制工具描述的内容。对于工具选择准确率要求高的场景手动模式更可靠。我实测发现给工具写一段好的描述比调模型参数更能提升工具调用准确率。描述里要说清楚工具能做什么、不能做什么、参数格式是什么最好给一两个例子。框架还支持工具分组把相关的工具放在同一个命名空间下。模型看到的是分组后的工具列表选择时先选组再选具体工具。这个设计在工具数量多的时候特别有用能减少模型的认知负担。4.3 工具执行的安全边界工具执行是 Agent 框架里风险最高的环节因为工具可能执行任意代码、访问外部资源。框架在这块做了几层防护超时控制是基础。每个工具调用都有超时限制默认值可以在配置里调整。超时后框架会取消执行如果是异步任务或标记为失败。我建议对可能耗时的工具单独设置较短的超时避免一个慢工具拖垮整个对话。参数校验在工具执行前进行。框架会根据 schema 校验传入的参数类型不对、必填项缺失都会在执行前拦截。这个校验不能完全依赖模型因为模型可能生成格式正确但语义错误的参数。沙箱隔离是可选的。框架支持把工具执行放在子进程中限制其文件系统和网络访问。这个特性对执行用户提供的代码特别重要。不过沙箱有性能开销不是所有场景都需要开启。注意沙箱隔离不是万能的。如果工具本身需要访问网络或文件系统沙箱配置不当会导致工具无法正常工作。我建议先在开发环境测试沙箱配置确认工具能正常运行后再上生产。5. 上下文管理记忆插件的设计哲学5.1 短期记忆与长期记忆的边界Agent 的上下文管理是个容易被低估的复杂问题。模型有上下文窗口限制但对话可能很长不可能把所有历史都塞进去。框架把这个问题拆成短期记忆和长期记忆两层。短期记忆是当前对话的历史通常保留最近 N 轮。N 的取值需要权衡太小会丢失上下文太大会挤占模型窗口。框架默认保留 10 轮但可以通过配置调整。我实测下来对于大多数问答场景10 轮足够覆盖用户的追问和澄清。长期记忆是跨对话的知识比如用户的偏好、历史事实。这部分通常存在向量数据库里需要时通过相似度检索召回。框架把长期记忆的读写抽象成插件接口具体用什么向量库由插件决定。两层记忆的配合方式是短期记忆始终加载长期记忆按需检索。每次请求时框架先加载短期记忆然后根据当前输入从长期记忆中检索相关内容合并后一起发给模型。这个合并策略会影响模型的表现框架允许插件自定义合并逻辑。5.2 上下文压缩的触发时机当短期记忆超出窗口限制时框架会触发上下文压缩。压缩策略有几种滑动窗口是最简单的直接丢弃最老的消息。这种方式实现简单但可能丢失重要信息。比如对话开头用户说的关键约束滑出窗口后模型就忘了。摘要压缩用模型把老消息总结成一段话保留关键信息。这种方式信息保留更好但需要额外的模型调用增加延迟和成本。关键信息提取从老消息中抽取实体、意图、约束等结构化信息以紧凑格式保留。这种方式平衡了信息保留和成本但实现复杂度高。框架默认用滑动窗口但允许插件替换压缩策略。我拆的时候发现压缩策略的接口设计得很灵活插件可以完全控制压缩的时机和方式。比如可以实现一个只在 token 数超过阈值时才压缩的策略避免频繁压缩。5.3 记忆插件的持久化选择记忆插件的持久化后端直接影响 Agent 的部署形态。框架内置了几种常见后端后端类型适用场景优点缺点内存开发测试、单次会话零依赖、速度快重启丢失、无法共享文件单机持久化简单、无需额外服务并发差、不适合多实例关系数据库结构化记忆、多实例成熟、支持复杂查询需要维护数据库向量数据库语义检索、长期记忆检索质量高需要额外服务、有成本选择哪种后端取决于具体场景。开发阶段用内存最方便生产环境如果要多实例部署关系数据库或向量数据库更合适。框架的插件化设计让切换后端只需要改配置不需要改代码。我踩过的一个坑是内存后端在多进程部署时会出问题。每个进程有独立的内存空间用户请求被负载均衡到不同进程时记忆不共享导致对话上下文断裂。如果要用内存后端必须确保同一用户的请求总是路由到同一进程会话粘性或者换用共享存储后端。6. 多 Agent 协作插件化带来的意外便利6.1 Agent 作为插件的组合方式拆到多 Agent 协作这块时我发现了一个有意思的设计Agent 本身也可以被当作插件。一个 Agent 插件对外暴露的接口和普通工具插件一样接收输入、返回输出。这意味着一个 Agent 可以调用另一个 Agent就像调用普通工具一样。这个设计带来的灵活性超出预期。你可以把复杂任务拆成多个专职 Agent每个 Agent 负责一个子领域然后通过一个协调 Agent 来调度。协调 Agent 不需要知道子 Agent 的内部实现只需要知道它们的输入输出接口。比如一个客服场景协调 Agent 负责理解用户意图然后把问题分发给订单查询 Agent、退换货 Agent、产品咨询 Agent。每个子 Agent 有自己的工具集和记忆互不干扰。协调 Agent 的代码非常简洁就是根据意图路由请求。6.2 协作模式的选择与权衡框架支持几种多 Agent 协作模式各有适用场景路由模式是最常见的一个协调者根据输入决定由哪个 Agent 处理。这种模式结构清晰适合任务边界明确的场景。缺点是协调者可能成为瓶颈而且协调者的判断错误会导致整个流程失败。流水线模式把任务拆成多个阶段每个阶段由一个 Agent 处理前一个的输出是后一个的输入。这种模式适合有明确步骤的任务比如提取信息→分析→生成报告。缺点是灵活性差中间某个阶段出错会影响后续所有阶段。辩论模式让多个 Agent 对同一问题给出方案然后通过某种机制投票、评审选出最佳方案。这种模式适合需要多角度思考的复杂问题但成本高因为要跑多次推理。框架没有强制某种模式而是提供了基础原语让开发者自己组合。这种提供积木而非成品的设计哲学和它的插件化思路一脉相承。6.3 Agent 间通信的消息格式多 Agent 协作时消息格式的统一很重要。框架定义了一套 Agent 间通信的消息协议包含发送者、接收者、消息类型、负载等字段。这套协议和模型交互的消息协议是分开的因为两者的语义不同。Agent 间消息支持几种类型请求期望回复、通知不期望回复、响应对请求的回复。这种类型区分让 Agent 可以决定是否需要等待回复避免不必要的阻塞。我注意到框架在消息路由上做了优化如果两个 Agent 在同一个进程内消息直接通过内存传递如果跨进程则通过消息队列。这个优化对开发者透明不需要改代码。但配置消息队列需要额外的工作我建议先用同进程模式跑通逻辑确认没问题后再考虑分布式部署。7. 拆完之后的一些真实感受拆这个框架花了我差不多一周的业余时间过程中有几次原来可以这样的感叹也有一些觉得可以更好的地方。最让我欣赏的是插件协议的克制。三个核心方法没有多余的钩子但通过上下文对象和依赖声明覆盖了绝大多数扩展场景。这种少即是多的设计需要很强的判断力因为每加一个接口方法都是在增加所有插件实现者的负担。另一个亮点是配置驱动的装配。把插件组合从代码里抽出来放到配置里看起来只是形式变化实际上改变了部署和运维的方式。运维人员可以独立调整 Agent 能力不需要开发介入。这个特性在需要快速响应业务变化的场景下价值很大。当然也有我觉得可以改进的地方。插件间的依赖管理还是偏弱虽然有拓扑排序但缺少版本约束。如果插件 A 依赖插件 B 的某个接口而 B 升级后接口变了A 可能在运行时才报错。更完善的做法是引入接口版本号加载时校验兼容性。调试体验也有提升空间。插件化架构的调用链路比较长出问题时定位到具体插件需要一些经验。框架提供了日志但日志的粒度控制不够细有时候需要开 debug 级别才能看到关键信息但 debug 日志又太多淹没重点。如果能按插件粒度控制日志级别会好用很多。最后说一个实操建议如果你打算基于这个框架做二次开发先从写一个最简单的插件开始。不要一上来就设计复杂的插件体系而是写一个能跑通的最小插件理解注册、加载、执行的完整链路然后再逐步扩展。我见过一些团队一上来就设计十几个插件结果接口没定好返工成本很高。插件化架构的收益来自长期迭代不是一次设计就能到位的。
阅读完成 · 觉得有帮助?