资讯详情

深度拆解DeepSeek Agent框架:插件化架构的骨架级设计

📅 2026/10/20 11:31:23 | 华诺云谱 👁 阅读
深度拆解DeepSeek Agent框架:插件化架构的骨架级设计
1. 先说结论它把“框架”做成了可以按需拼装的底座拆这套 DeepSeek 开源的 Agent 框架之前我先给自己设了一个预期无非就是注册工具、写个call_llm、然后跑循环。市面上大多数 Agent 框架的“插件化”就是这个水平——你注册了多少个工具它就给你多少把锤子但房子还是那栋房子。真正拆开之后我发现自己之前的判断错得离谱。这套框架的插件化不是“把工具变成插件”而是“把整个 Agent 系统的骨架拆成了可以替换的插件”。模型可以换、工具可以换、记忆可以换、执行流程的中间环节也可以换。你拿到的不是一个已经盖好的房子而是一套标准尺寸的砖和一份结构图——你可以随时换上自己的砖甚至可以把某面墙拆掉重砌。为了让你能直观理解“这种程度”到底是什么意思我先把插件化的层次列成一个标尺插件化层级能换什么代表做法这套框架是否做到工具级工具函数、API 调用注册函数列表是能力级模型接入、记忆存储、知识库Provider 抽象是骨架级工作流编排、循环策略可替换的 Runner / 策略是协议级消息协议、上下文传递方式自定义协议层部分开放核心用协议定义大多数框架能做到第一层就不错了做到第二层的已经算良心产品。这套框架做到了第三层而且是“主动设计”出来的那种第三层不是堆代码堆出来的偶然结果。我这次拆解用的方法很简单粗暴先把源码包下载下来按目录结构看一遍然后从主入口文件往回追找出所有“接口”和“实现”的关系最后跑一个最小 Demo再往里挂自己的插件逐个验证它的扩展点是不是真的像文档说的那样能无缝接入。整个过程大概花了一个周末下面这些内容就是这次拆解的主干记录。这篇拆解适合两类人看一是想自己设计 Agent 框架或给现有系统做插件化的架构师二是正在用这类框架、想搞清楚“我到底能改到什么程度”的业务开发者。第一类人能在这里找到比较完整的扩展点设计思路第二类人能直接抄走我验证过的插件写法。2. 插件边界定义框架先决定“谁可以不被替换”2.1 从“整套运行时”到“组件包”先说一个我拆完以后最大的感受这套框架的代码组织方式从一开始就奔着“权限分明”去的。一般的开源框架主目录下面通常是一堆core、llm、tools、memory之类的业务目录每个目录里的类互相 import边界模糊得很。这套框架的主目录结构不太一样代码量最大、层级最根部的部分是一个纯粹的“协议目录”里面几乎没有业务逻辑只定义了一堆抽象接口和数据类。业务实现全部放在独立的目录里而且必须依赖那层协议协议层绝不反向依赖任何业务实现。这个设计的价值我是在尝试换模型的时候才真正体会到的。我之前折腾过很多框架换模型时最痛苦的不是改 API 地址而是要顺着调用链把框架内部所有硬编码的模型判断逻辑全部翻出来改一遍。这套框架不会出现这种情况因为协议层定义了“一个模型插件必须长什么样”框架核心只和这个定义打交道不和某个具体模型的三方 SDK 直接耦合。2.2 约定优于配置插件目录和 manifest除了协议层这套框架还有一个很“工业化”的设计——插件不靠代码里逐个 import 来加载而是靠目录结构和描述文件来发现。每个插件包是一个独立的目录目录里必须有一个描述文件类似plugin.yaml或manifest.json里面声明这个插件的名称、入口模块、依赖关系、需要注册的组件类型。框架启动时扫描插件目录根据描述文件加载对应模块。也就是说你新增一个插件不需要去改框架的启动代码只要把目录放进去在描述文件里声明好框架就能在运行时找到它。这种做法的好处在团队协作时特别明显。一个项目里如果有人负责模型接入、有人负责工具开发、有人负责记忆策略各有各的插件目录互不干扰。合并代码时不会有那场经典的“同一个core/config.py被五个人改了十个版本”的冲突。2.3 依赖方向的单向性为什么这能让插件随便换我读源码时特意画过一张依赖关系脑图协议层在最底层框架核心层调度器、上下文管理在中间具体插件在最顶层。整张图里只存在“上层依赖下层”的箭头没有任何一条从上往下、从下往上逆行的依赖。这个单向依赖的约束就是“插件可以随便换”的底层保证。因为协议层是稳定的核心层面对的永远是协议层定义好的接口而不是某个插件的具体实现。插件 A 和插件 B 只要都实现了同一个 Provider 接口核心层就不关心你换的是 A 还是 B。很多人在自己项目里做插件化失败问题就出在依赖方向失控核心代码直接 import 了某个插件的具体类表面上做了接口抽象实际上 pdb 一调试全是硬引用换一个插件就要改核心代码。这套框架在这一点上卡得很死协议层之外的代码几乎不允许被核心层直接引用这是它插件化能真正落地的地基。3. 模型接入层把“换个模型”从改代码变成改配置3.1 ModelProvider 的设计模型不是内置功能是插件我在拆解时重点盯的第一个扩展点是模型接入层。它这里的模型接入不叫“模型管理模块”而是叫 ModelProvider直译过来就是“模型提供方”。名字的细微差别其实就是设计理念的差别框架不认为自己和某个模型绑定了框架只认“谁能给我提供模型的调用能力”谁有这个能力谁就进来。ModelProvider 这个协议接口在源码里大致长这样的形态# 协议层定义的模型提供方接口示意 class ModelProvider(Protocol): def complete(self, messages: list[dict], **kwargs) - StreamingResponse | Response: ...注意complete的返回值有两个形态普通响应和流式响应。这个设计几乎没给自己留退路——任何一个模型插件如果要接入这套框架就必须兼容这两种返回方式。反过来说你在上面写业务代码时永远不需要关心“这次是 DeepSeek 还是别的模型”因为框架已经把模型调用封装成了统一的接口。3.2 为什么一个“OpenAI 兼容协议”就够了模型插件内部具体怎么实现框架基本不设限。但源码和文档里反复强调一种推荐做法尽量把自己的模型接口转换成 OpenAI 兼容的格式。这说的不是直接调用某个官方 SDK而是指插件的输入输出数据结构尽量套用 OpenAI 定义的 messages 结构。很多模型厂商的 API 格式各不相同有的用user/assistant直接传有的要把多轮对话拆成 list有的要用特殊字段声明推理强度有的对temperature的取值范围都定义得不一样。如果每个模型插件都按自己的格式传参框架核心就得为每一种格式写一套适配逻辑插件化的意义就废了。而 OpenAI 兼容格式本质上已经成了行业默认的“对话协议语言”。框架里收到的所有消息、上下文、工具调用结果都以这套格式在流动。模型插件要做的就是把它自家模型的参数翻译成这套语言或者把自家的响应翻译回这套语言。翻译的过程永远是“插件内的事”核心层完全不关心。我后来带着这个思路去写自己的模型插件整个过程清爽得不行不需要碰框架任何核心代码只需要老老实实做好翻译层。3.3 参数归一化与错误重试被大多数人忽略的难点真正拆到模型插件的具体实现时我发现最考功夫的是三个细节参数换算、流式处理、错误重试。这三个细节做得不好的模型插件接进来以后看起来能用但实际运行一久全是暗坑。先说参数换算。不同模型的超参命名和语义差异很大有的叫temperature有的叫top_p取值范围完全不同还有上下文长度有的模型是 32K有的是 128K如果不做归一化换算同一个 Agent 任务在不同模型上跑出来的结果可能完全不同。框架的做法是在插件内部维护一个统一的参数字典插件负责把框架侧传进来的标准参数映射成待接入模型自己的参数。这个映射逻辑是插件的一部分框架不认识也没必要认识。然后是流式返回。有些 Agent 任务的交互感靠的就是逐字输出所以模型插件的complete一定要同时支持普通返回和流式返回两种模式。框架的协议层定义了一套流式包装逻辑插件只需要按协议要求吐chunk框架负责把它们组装成完整响应。我自己写测试时才发现很多模型 SDK 的流式和普通返回用的是两套代码路径一旦没做区分长任务跑起来就是卡死、断流、首字延迟高这一类毛病。最后是错误重试。模型接口在线时稳定但真跑到生产环境限流、超时、连接重置这类问题一个接一个。框架的协议层对错误重试没有写死策略但模型插件的实现里普遍会带入“重试消耗预算”的概念连续失败达到上限就不无限重试而是把错误转换成结构化信息交还给 Agent 去做后续判断。这个思路比“单纯重试三次”高级因为 Agent 能根据错误信息自己决定是换一种问法还是结束这个子任务。4. 工具层与记忆层运行时动态发现插件4.1 JSON Schema callable工具描述本身就是一种插件工具插件的长相比我预想的要现代。它不要求你实现一堆回调它只要求你提供两个东西一份 JSON Schema 格式的参数描述和一个可调用对象。# 工具插件结构示意 def read_table(path: str, sheet: int 0) - list[dict]: 读取表格数据返回行列表 ... tool_meta { name: read_table, description: 读取本地 Excel/CSV 表格数据支持指定工作表, parameters_schema: { type: object, properties: { path: {type: string}, sheet: {type: integer} }, required: [path] }, entrypoint: read_table }工具描述和实现分离带来的直接好处是大模型看到的“工具能力说明”是经过设计的而不是从函数签名里瞎猜的。比如你的函数参数叫p没人知道那是指“路径”但如果 JSON Schema 里写着description: 传入待读取文件的路径模型调用你工具的成功率会明显高一个档次。框架在加载工具插件时会读取tool_meta把name/description/parameters_schema统一塞进模型上下文的 tools 列表而entrypoint则保留在本地等模型“决定”调用时再真正执行。这个流程其实和 LangChain 之类工具的注册逻辑相似差别在细节做得深它把工具的“发现、描述、加载、执行”拆成了四个不同阶段每个阶段都有插件可以介入的缝隙。4.2 自动注册与懒加载新工具不需要重启框架这套框架在工具加载机制上比我预期要“聪明”一点。它扫描插件目录时只读取描述文件建立索引并不实际 import 工具模块。真正执行工具调用时才把模块加载进来。这个懒加载设计一开始我没当回事直到我自己测试了一个“冷门工具”一个只是偶尔用一次的函数从索引建立到真正加载框架能跳过所有不必要的初始化开销。对于一个工具数量很多、每个工具都要连数据库或者加载模型权重的重场景这个设计的性能价值会被放大得很明显。如果你的项目里要挂几十个工具尽量让每个工具模块保持轻量别在模块顶层做重初始化——框架的懒加载能帮你省掉不少损耗但也没法帮你躲过你在 import 时就开始读大文件这种硬伤。自动注册还带来了一个体验上的质变真实业务中频繁新增工具、修改工具描述不用每次改完就重启框架、再重新跑一遍初始化。前提是你不要用重初始化风格的插件模块。这个原则对插件开发者来说应该当成默认规范。4.3 记忆不是胶水是观察点MemoryProvider 的钩子位很多 Agent 框架把“记忆”做成了夹在上下文里的字符串拼接工具恨不得全文塞进模型窗口里拉倒。这套框架把记忆做成了另一个 Provider——MemoryProvider而且它给记忆预设了几个明确的观察点。协议层里记忆相关的钩子位大概包括某一步执行完成后、某个插件注册事件发生后、Agent 准备提交最终结果之前。每个钩子都向记忆插件开放“写入/读取/压缩/归档”的权限但框架不会主动调用这些方法——它只负责在正确的时机把控制权让渡给记忆插件。这几行设计的直接作用是记忆策略变成了一种可插拔能力。你想用“最近 N 条消息覆盖”的便宜策略可以你想做“向量化 语义检索”的高级策略也可以甚至你想让 Agent 完全失忆不挂记忆插件就行。这套框架不会因为某个策略写得稀疏就拖累核心流程反而给上层业务保留了充足的探索空间。我实际验证下来发现记忆插件最容易踩的坑是“写入时机不对”。有些策略需要全量快照有些需要增量日志如果框架没有一个明确的观察点机制这些策略只能自行找时机要么写重复了要么写漏了。这套框架的观察点设计等于把这个问题从插件开发者手里接管了开发者只需要决定“在这个时机做什么”不需要关心“怎么触发这个时机”。5. 动作循环的钩子系统拦截所有中间态5.1 Agent 流程里的六个可拦截节点工具层和模型层只是插件化的第一层真正让我“服”的是它对动作循环本身的处理。Agent 的完整执行流程本质上是一个循环“接收任务 - 决定下一步 - 调用工具或模型 - 观察结果 - 再决定下一步 - 输出最终答案”。这套框架没有把这段流程封装成一个黑盒而是在每个关键节点都开了口子。我数了一下它至少暴露了六个可拦截节点节点位置发生时机适合挂什么逻辑任务开始前Agent 收到任务但还没做任何处理初始化上下文、加载用户偏好模型调用前决定下一步动作前提前注入提示词、调整上下文模型返回后模型给出了下一步行动建议校验输出格式、记录原始响应工具调用前即将执行某个工具权限校验、参数审查、成本拦截工具返回后工具执行结果回传中结果清洗、确认重试策略最终结果输出前Agent 准备产出最终答案最终校验、格式化、日志归档每一个节点的钩子都接收同一个上下文对象修改后再返回形成一个“可拦截、可修改、可放行”的通道。你可以在任何节点插进去写逻辑框架不会在意你是谁——只要你遵守“传入上下文、传出上下文”这个约定。5.2 写一个“观察型钩子”的实操示例我当时为了验证钩子的实用性写了一个非常简单的“观察型钩子”——它不修改任何行为只是把每一步模型请求和工具调用的原始记录打印到日志里。# 钩子插件示意记录每一步的请求与响应 def before_model_call(ctx): logger.info(【模型请求】%s, ctx.current_user_message) return ctx def after_model_call(ctx): logger.info(【模型响应】%s, ctx.last_model_output) return ctx def before_tool_call(ctx): logger.info(【工具调用】%s参数%s, ctx.next_tool_name, ctx.next_tool_args) return ctx def after_tool_call(ctx): logger.info(【工具结果】%s, ctx.last_tool_result) return ctx挂上这个钩子以后整个 Agent 的每一步决策过程都在日志里看得到。这对排查问题特别有用——以前排查 Agent 的“诡异输出”你得靠猜现在可以直接从日志里回溯是哪一步给了错误信息、是模型误判还是工具返回出错。我建议所有刚开始用这套框架的人都先写一个同样的观察型钩子。它不改变任何行为但能让你短时间内建立起对框架运行时状态的直觉。等你把日志里那十几步看得明明白白再去写复杂的拦截逻辑思路会顺很多。5.3 钩子之间怎么通信上下文传递与状态共享钩子写多了以后自然会遇到一个问题多个钩子之间怎么共享状态假设 A 钩子在某个节点做了权限校验B 钩子在另一个节点想读取校验结果怎么办框架的答案是把上下文对象当作承载状态的信使。钩子可以在上下文里写入自定义字段后面任何节点的任何钩子都能读到前提是你要显式地在上下文里声明好命名空间。def permission_hook(ctx): ctx.extra_state[allowed] True return ctx def audit_hook(ctx): if ctx.extra_state.get(allowed): ... return ctx这个设计像极了 Web 框架里的 request 对象——大家共用同一个请求上下文但各自往里面塞自己的业务标记。好处是钩子之间解耦坏处是如果没人管extra_state会变成垃圾堆。所以我自己的习惯是每批钩子维护一个固定的命名空间用完的字段及时清理且不跨命名空间读取别人字段避免写出一堆互相依赖的魔法值。6. 一次真实改造给报表监控需求挂上插件6.1 业务场景与改造目标理论讲再多不如实际操作一遍。我拿一个真实的业务小需求来完整走一遍这套流程。需求背景很简单团队每天要人工检查一份销售数据表看有没有异常比如某一天某项指标跌破阈值。人工检查又慢又容易漏。我准备用这套 Agent 框架做一个自动巡检员每到一个时间点让 Agent 读表格、与预设阈值比对、发现问题就发一条消息到内部通讯群。如果用传统方式做我得在整个系统里新写一个“读表格并判断”的模块再把它接到某个定时器上。而在这套框架里我不需要动任何核心代码只需要写两个插件、声明一个编排配置。6.2 第一步写一个“读表检查”工具插件第一个插件是工具类插件负责读取表格、根据阈值判断异常。我实现了一个相对通用的函数把表格数据拉出来后封装成与阈值匹配的逻辑。# 工具插件检查销售表格指标 def check_sales_table(path: str, thresholds: dict) - dict: table load_table(path) abnormal_items [] for row in table: for key, limit in thresholds.items(): if key in row and row[key] limit: abnormal_items.append({ date: row[date], metric: key, current: row[key], threshold: limit }) return {pass: not abnormal_items, abnormalities: abnormal_items} tool_meta { name: check_sales_table, description: 读取销售表格依据阈值字典检查是否存在指标异常, parameters_schema: {...}, entrypoint: check_sales_table }这个函数本身没有魔法就是普通的表格处理和阈值比对。但挂上框架的机制以后“模型决定要不要调用它”就变成了 Agent 的判断能力。模型看到需求描述是“检查今天的报表有没有问题”它会自动决定去读取哪些路径、以什么顺序调用哪些工具不需要我写死调用流程。6.3 第二步写一个“通知上报”钩子插件第二个插件是钩子类插件作用在于执行结果返回后的通知上报。当check_sales_table返回异常列表时钩子会检查上下文里的工具结果发现pass为 False 就发通知。def after_tool_call(ctx): if ctx.last_tool_name check_sales_table: result ctx.last_tool_result if not result[pass]: send_notification(result[abnormalities]) return ctx这里妙就妙在“检查行文逻辑”被搬到了钩子里。工具插件只管查询和判断通知发送是另一个插件的职责两者之间通过上下文传递数据互不依赖。后续如果不想发消息了或者想改成一键导出报告只需要换个钩子插件工具本身完全不用动。6.4 第三步配置编排与运行验证写完两个插件后我用“配置编排”的方式把它们组织起来启用哪些插件、各自的加载顺序、每个插件需要的参数表格路径、阈值字典、通知目标。这些全部放在配置文件里框架启动时读取并加载插件。实测效果比预想的好Agent 收到“检查今天报表”的任务后自己完成了“识别需要读取的文件、按阈值判断、发现三条异常记录、发出通知”的完整流程。整个过程我没有写一行 Agent 逻辑判断代码只写了工具、钩子和配置。这个改造最有价值的感受是需求变更时几乎零成本。隔了一周团队说“我想把阈值改成动态从另一张表读取”我只需要改工具插件的内部逻辑或配置里的阈值来源团队说“告警消息想再加一个抄送人”我只改钩子。两个插件的改动互不影响整个系统的可维护性上了不止一个台阶。6.5 为什么这套设计能跑通拆下来最大的体会是这套设计能跑通不是因为具体哪一行代码写得好而是因为框架在抽象层次上留对了缝隙。工具只负责“做”钩子只负责“观察和干预”模型只负责“决策”框架只负责“编排”。每一层都只依赖协议不知道其他层的具体实现。所以业务逻辑可以像积木一样拆开、替换、单独测试。这种设计见得多了以后我自己做项目的架构时也开始刻意给自己留“缝隙”——先把稳定的协议定义清楚再把可变的部分做成插件而不是把所有东西揉成一团。7. 拆完框架之后我踩过的几个坑7.1 插件加载顺序与依赖关系文档没说透的隐藏规则我没有被框架设计惊艳到失去理智实际用下来还是踩了几个不轻不重的坑。第一个坑出在插件加载顺序上。框架虽然支持声明插件依赖关系但它是按“描述文件里的声明顺序”进行加载的而不是自动拓扑排序。如果你的钩子插件要在工具插件加载完之后才能注册成功就必须在描述文件里显式声明依赖。我开始没注意结果钩子总是报“找不到工具元数据”。后来在描述文件里把依赖字段补齐才解决。这个坑虽然一踩一个准但也让我理解了框架设计者的用意思图显式声明依赖比自动推理依赖更可控。因为自动拓扑排序听着省事但一旦插件数量多了隐式依赖会变成隐形炸弹查起来比显式声明慢十倍。所以我的建议是不管框架是否支持自动排序一律在描述文件里把依赖写清楚。7.2 热重载不是万能的改 manifest 还是要重启第二个坑是热重载。框架支持运行中替换工具函数的具体实现开发期体验很好但如果你改了描述文件里的字段——比如改了工具名、参数描述、依赖列表——热重载基本失效必须重启框架才能生效。其实想想也合理描述文件是框架建立插件索引的依据索引都变了不改重启等于让框架拿着旧地图找新路。这个限制对生产环境部署不算大问题但对开发调试来说是个常犯的错你改了tool_meta以为刷新页面就能测新描述结果模型永远看不到更新白白怀疑了半天人生。我的教训是改工具逻辑用热重载改工具描述先重启。7.3 记忆持久化框架给了钩子但没给存储第三个坑是关于记忆的。框架把记忆抽象成 Provider提供了读取和写入的观察点但没有原生实现很多团队期望的“重启不丢记忆”能力。默认情况下记忆是存在内存里的Agent 进程一重启所有历史就清空了。这不算框架的缺陷因为记忆持久化本来就应该由业务方根据实际环境去选型——文件存储、数据库、向量库都可以。但它确实是个容易在项目中期才发现的坑你辛辛苦苦调好了 Agent 的多轮对话记忆策略一重启全没了然后才想起要补一个 storage adapter。我自己的做法是写了一个轻量级的持久化插件把记忆内容序列化后落入本地 SQLite重启后自动加载回来。代码量不大但确实属于“框架不管、业务必需”的那一类。如果你准备拿这套框架做长期运行的服务记忆持久化这项工作是绕不开的。7.4 多个钩子重复触发的隐患最后一个坑是我自己写钩子时搞出来的。我在工具调用前和模型调用后分别挂了统计逻辑结果统计出来的数字翻倍了。调试了很久才发现某个场景下同一个钩子被注册了两遍——第一次加载插件时自动注册我在业务代码里又手动注册了一遍。这个坑提醒我一个重要原则钩子注册应该遵循单一事实源。要么全部靠插件目录自动扫描注册要么全部靠业务代码手动注册千万别混着来否则就是在给自己埋雷。最后再分享一点个人体会整个拆解和改造做完之后我最深的感受反而不是“这个框架真牛”而是“插件化”这三个字在很多项目里被严重低估了。大部分框架的插件化只是给使用者一扇门而这套框架的插件化是把整面墙都做成了可拆的模块每个接缝处还预留了标准的连接件。对我个人来说这次拆解最大的收获还不是学会怎么挂插件而是建立了一套判断框架可扩展性的直觉拿到一个新框架先看它的接口层定义了什么、核心层没引用什么再看看它敢不敢把核心流程的中间环节全部开放出来。能做好这两点的框架扩展性是装不出来的。如果你也想拿这套框架做点东西我的建议是从一个最小需求开始先写一个观察型钩子跑通全流程再逐步加工具、加记忆、加自己的编排逻辑。动手之前最好把协议层几个核心接口通读一遍花半小时能给你省掉后面好几个小时的排查时间。插件化不是“加个配置就能无限扩展”的银弹但当你理解了它的边界在哪里你会发现它能替你扛掉的复杂度远超你的预期。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑