DeepSeek Agent框架插件化机制拆解:从工具注册到多轮循环的工程实践
1. 从能跑就行到想拆开看看一个 Agent 框架引发的折腾第一次把 DeepSeek 开源的 Agent 框架拉下来跑通 demo 的时候我的反应其实挺平淡的——无非就是模型 工具调用 循环这套东西市面上能见到的实现没有一百也有八十。真正让我坐不住的是第二天我想给它加一个自定义工具本以为要翻半天源码、改核心逻辑结果只写了一个类、注册了一下整个流程就通了连主循环都没碰。这个体验上的反差就是我决定把它拆一遍的直接原因。插件化这三个字大家都挂在嘴边但真正做到加功能不用动核心的框架其实不多大部分所谓的插件机制本质上是预留了几个 if-else 分支让你填。而这个框架给我的感觉是它把什么是不变的、什么是可变的这条线划得比较清楚扩展点开在了对的位置上。这篇内容适合两类人看一类是正在选型 Agent 框架、想知道它的扩展机制到底靠不靠谱的开发者另一类是已经用过、但只停留在调 API 层面想搞清楚为什么它加工具这么顺的人。我会从整体架构讲到插件注册的具体机制再到工具描述怎么影响模型决策、多轮循环怎么收敛、状态怎么管最后聊聊我实测中踩到的几个坑。全程不吹不黑拆到哪说到哪。需要先说明一点下面涉及的具体类名、方法名我会尽量贴近它的实际设计但不同版本可能有出入你以自己拉到的代码为准。我更想讲清楚的是设计思路因为思路是跨版本稳定的API 是会变的。2. 拆之前先想清楚Agent 框架到底在解决什么问题2.1 剥掉包装Agent 的核心就三件事很多人一上来就看目录结构我觉得顺序反了。应该先问一个 Agent 框架抛开所有花哨的概念它到底在替你干什么我的答案是三件事。第一件是把模型的输出变成可执行的动作。模型吐出来的是一段文本里面可能藏着我要调用某个工具、参数是这些的意图框架要负责把这段文本解析成结构化的调用请求。第二件是把动作的执行结果喂回给模型让它基于新信息决定下一步。第三件是控制这个循环什么时候停不能无限转下去也不能转一轮就草草收场。这三件事听起来简单但每一件都有大量细节。比如第一件模型输出格式不稳定怎么办第二件工具执行报错了是直接抛给模型还是框架自己重试第三件怎么判断模型是真的完成了还是在敷衍一个框架的成熟度基本就体现在它对这三件事的细节处理上。2.2 为什么插件化是这类框架的命门理解了上面三件事就能明白为什么插件化这么关键。因为在这三件事里唯一真正多变的就是有哪些工具可用。主循环的逻辑是相对固定的模型调用是相对固定的但工具集是千人千面的——做数据分析的要 SQL 工具做运维的要命令行工具做客服的要查订单的工具。如果框架把工具集写死在核心代码里那每加一个工具就要改核心改核心就有回归风险用的人越多、改的人越多核心就越不敢动。反过来如果框架把工具定义成一个标准接口核心只认接口不认具体实现那扩展就变成了实现接口 注册两件事核心代码一行不用动。这就是插件化的价值它把变化隔离在核心之外。我拆这个框架最大的收获就是看清楚了它是怎么做到这一点的以及它在哪些地方为了这个目标做了取舍。2.3 一个容易被忽略的前提工具描述本身就是 Prompt在往下讲之前有个认知必须先建立起来否则后面很多设计你会觉得多此一举。那就是在 Agent 框架里工具的定义不只是给代码看的更是给模型看的。你写的工具名、工具描述、参数说明最终都会被拼进发给模型的 prompt 里。模型是根据这些文字来决定要不要调这个工具、怎么填参数的。所以工具描述写得好不好直接决定了 Agent 的智商。我见过太多人工具逻辑写得漂漂亮亮描述就一句查询数据结果模型根本不知道该什么时候用它。这个前提会贯穿后面所有章节。你会在工具注册、参数校验、错误处理这些地方反复看到它的影子——框架的很多设计本质上都是在帮模型更好地理解工具。3. 整体骨架一次请求在框架里到底走了哪几步3.1 主循环的四个阶段我把一次完整的 Agent 请求拆成了四个阶段这样讲起来清楚。接收阶段用户输入进来框架把它和系统提示、历史消息、工具列表拼成一个完整的请求体。推理阶段请求发给模型拿到模型的回复。分发阶段判断回复里有没有工具调用意图有的话解析出来、执行、把结果塞回消息历史。收敛阶段判断是否继续循环还是把最终答案返回给用户。这四个阶段里接收和推理是相对固定的分发和收敛是插件化发挥主要作用的地方。分发阶段要处理有哪些工具、怎么调收敛阶段要处理什么时候停。我拆代码的时候重点就放在这两个阶段。值得一提的是这个框架把消息历史当成了一个贯穿始终的核心数据结构。每一轮的工具调用和结果都是以消息的形式追加进去的而不是单独维护一个工具调用记录。这个设计选择很关键因为它意味着模型在下一轮能看到完整的上下文包括它自己之前调了什么、拿到了什么结果。这是多轮 Agent 能记住自己在干嘛的基础。3.2 消息结构一切状态的载体消息结构大概是这样的每条消息有角色系统、用户、助手、工具有内容助手消息里可能还挂着工具调用列表工具消息里则带着对应的调用 ID。这个 ID 是干嘛的是用来把工具返回的结果和模型发起的调用对应起来的。为什么要这么设计因为模型可能在一轮里同时发起多个工具调用返回的时候如果不带 ID框架就不知道哪个结果对应哪个调用。这个细节看起来小但如果你自己实现过 Agent就会知道这是最容易出错的地方之一——尤其是并发执行多个工具的时候。我实测下来这个框架在消息结构上是比较克制的没有搞一堆自定义字段基本贴着通用的对话消息格式来。好处是兼容性好你换个模型供应商消息结构不用大改坏处是一些框架特有的元信息没地方放只能塞进内容里或者另开字段。这是个取舍没有绝对的对错。3.3 为什么主循环要设计成可中断的还有一个设计我一开始没太在意后来才觉得妙主循环是可中断的。也就是说框架在每一轮循环之间都留了钩子允许外部插入逻辑比如检查是否超时、是否达到最大轮数、是否被用户手动取消。这个设计的意义在于Agent 的执行时间是不可控的。模型可能一轮就答完也可能转十几轮。如果没有中断机制一个跑飞的任务会一直占着资源。可中断意味着框架把循环控制权部分交还给了调用方这在生产环境里非常重要。我个人的经验是任何要上生产的 Agent都必须有最大轮数限制和超时控制。这个框架把这两件事做成了可配置项而不是写死这点是加分的。4. 插件化机制拆解加一个工具为什么不用动核心4.1 工具的标准接口长什么样这是整个框架最核心的部分我拆得最细。一个工具本质上要实现一个标准接口这个接口大概包含这么几个要素工具名、工具描述、参数定义、执行方法。工具名是模型用来引用它的标识必须唯一而且最好用英文、下划线分隔因为模型对这类命名的识别更稳。工具描述是一段自然语言告诉模型这个工具是干嘛的、什么时候该用。参数定义通常用 JSON Schema 来描述包括每个参数的类型、是否必填、含义。执行方法就是真正的业务逻辑接收解析好的参数返回结果。关键在于核心代码只依赖这个接口不依赖任何具体工具。主循环在分发阶段拿到的是一个工具列表它遍历这个列表找到匹配的工具调用它的执行方法。至于这个工具是查数据库还是发邮件核心完全不关心。这就是插件化的第一层面向接口编程。听起来是老生常谈但真正在 Agent 场景里做对不容易因为工具的执行结果格式五花八门有的返回字符串有的返回对象有的直接抛异常。框架必须定义清楚执行方法应该返回什么否则核心处理不了。4.2 注册机制工具是怎么挂上去的有了标准接口接下来就是注册。这个框架的注册机制我印象比较深它支持几种方式。一种是装饰器注册你在函数上打个标记框架启动时自动扫描收集。一种是显式注册你手动把工具实例加到一个注册表里。还有一种是配置式注册通过配置文件声明要加载哪些工具。为什么要支持多种因为使用场景不同。装饰器适合快速开发写起来最省事显式注册适合需要动态控制加载哪些工具的场景比如根据用户权限决定给哪些工具配置式适合把工具集和代码解耦运维层面就能调整。我实测下来最常用的是装饰器但生产环境我更倾向显式注册因为可控。装饰器的问题是隐式你看到一段代码不一定知道它注册了什么工具排查问题的时候要多跳几层。显式注册虽然啰嗦但一眼就能看清这个 Agent 到底有哪些能力。注册表本身通常是个字典结构键是工具名值是工具实例。查找的时候 O(1)很快。这里有个细节如果两个工具重名怎么办好的框架会报错或者警告差的框架会静默覆盖。我建议你在注册的时候加一层校验重名直接抛异常别等到线上才发现工具被覆盖了。4.3 参数校验别让模型自由发挥工具注册进来之后模型会填参数。但模型填的参数不一定合法——可能类型不对可能漏了必填项可能编了一个不存在的枚举值。这时候框架要做的就是在调用执行方法之前先按 JSON Schema 校验一遍。这个校验环节极其重要但很多人会忽略。我见过有人的工具直接拿模型给的参数去拼 SQL结果模型填了个奇怪的值直接报错甚至出安全问题。有了校验非法参数在进入业务逻辑之前就被拦下来了框架可以把校验错误作为工具结果返回给模型让模型重新填。这个把错误返回给模型让它重试的机制是 Agent 框架区别于普通函数调用的关键。普通函数调用参数错了就抛异常Agent 里参数错了应该告诉模型你哪里错了重新来因为模型有能力自我修正。我实测中模型第一次填错参数、第二次填对的概率相当高前提是错误信息要清晰。所以校验报错的时候别只返回一句参数错误要具体到参数 xxx 应该是整数你给的是字符串。信息越具体模型修正得越快。4.4 执行隔离一个工具崩了不能拖垮全局最后一个机制是执行隔离。工具的执行方法里可能有各种意外——网络超时、文件不存在、第三方接口挂了。如果这些异常直接往上抛整个 Agent 循环就断了。这个框架的处理方式是捕获工具执行中的异常把它转成一条工具结果消息返回给模型。模型看到这个工具执行失败了原因是 xxx可以选择换个工具、换个参数或者直接告诉用户失败了。这样单个工具的故障不会导致整个任务崩溃。我踩过的一个坑就在这里。早期我自己写的工具异常处理很随意有的抛有的吞结果模型收到的错误信息格式不统一有时候能理解有时候理解不了。后来我统一了所有工具执行失败都返回一个结构化的错误对象包含错误类型和错误描述。模型对这个格式的识别明显更稳定。提示工具执行失败时返回给模型的错误信息本质上也是一段 prompt。写得清楚模型就能自救写得含糊模型就只能瞎猜或者放弃。5. 工具描述怎么写模型才愿意用、用得对5.1 描述是给模型看的使用说明书前面反复强调过工具描述会进 prompt。这一节就具体讲讲怎么写。我的经验是把工具描述当成给一个聪明但完全不了解你系统的同事写使用说明——他能力很强但不知道你的业务背景你得把什么时候用、怎么用、有什么坑讲清楚。一个合格的描述通常包含三部分这个工具做什么、什么时候该用它、参数怎么填。第一部分一句话说清功能第二部分说清触发场景第三部分如果有容易混淆的参数要额外说明。举个例子一个查询订单的工具描述不该只写查询订单而应该写根据订单号或用户 ID 查询订单详情当用户询问订单状态、物流信息时使用。订单号和用户 ID 至少提供一个。这样模型就知道什么时候调、需要什么。5.2 工具名和描述的常见反模式我总结了几种常见的写法问题你看看自己有没有中招。第一种是名字太泛比如叫process、handle、do。模型看到这种名字完全不知道是干嘛的只能靠描述猜命中率低。好的名字应该自解释比如query_order_status、send_email。第二种是描述太短一句话带过。模型缺少判断依据要么不用要么乱用。我建议描述至少两句话把场景说清楚。第三种是多个工具功能重叠描述又没区分开。模型面对两个都能查信息的工具会随机选结果时好时坏。解决办法是在描述里明确边界比如查实时数据用 A查历史归档用 B。第四种是参数说明缺失。模型不知道某个参数该填什么格式就会瞎填。尤其是日期、枚举这类有格式要求的参数一定要在描述里给例子。5.3 实测描述改一版命中率能差多少我做过一个不太严谨但很有说服力的对比。同一个工具第一版描述就一句话第二版把使用场景和参数示例都补上其他条件不变跑同一批测试用例。结果是第二版的工具调用准确率明显更高尤其是那些该不该调这个工具的边界 case改善最明显。这个实验让我彻底改变了对工具描述的态度。以前我觉得描述就是文档写给自己看的现在我把它当成 prompt 工程的一部分是要反复打磨的。你花在描述上的时间会直接转化成 Agent 的可靠性。还有个技巧如果你发现模型总是漏调某个工具可以在描述里加一句当用户提到 xxx 时务必使用本工具。这种强调句对模型有实际影响。反过来如果某个工具总是被误调就在描述里加限制条件比如仅在用户明确要求 xxx 时使用。6. 多轮循环与状态管理Agent 怎么记住自己在干嘛6.1 循环终止的三个条件Agent 循环不能无限转必须有终止条件。这个框架里我看到了三种终止方式值得说一下。第一种是模型主动结束。模型这一轮没有发起任何工具调用只返回了文本框架就认为它给出了最终答案循环结束。这是最常见的终止方式。第二种是达到最大轮数。框架有个配置项限制最多循环多少轮超了就强制结束。这是兜底防止模型陷入死循环。第三种是外部中断。前面提到的可中断机制调用方可以随时喊停。这三种要配合使用。只有第一种遇到模型抽风会死循环只有第二种正常任务可能被误杀。我的建议是最大轮数设得稍微宽松一点比如 10 到 15 轮同时配合超时控制双保险。6.2 消息历史膨胀怎么办多轮循环带来一个现实问题消息历史越来越长。每一轮的工具调用和结果都追加进去几轮下来 prompt 就很大了既费 token 又可能超出模型上下文限制。这个框架的处理比较朴素就是把所有消息都留着。这在轮数少的时候没问题轮数多了就要自己想办法。我实测中用过几种策略一种是截断只保留最近 N 轮一种是摘要把早期的工具结果压缩成一句话还有一种是按需保留只保留和当前任务相关的消息。截断最简单但可能丢信息摘要需要额外调一次模型按需保留最理想但实现复杂。我的建议是先用截断设一个合理的窗口观察效果。如果发现模型经常忘记早期信息再考虑摘要。这里有个坑截断的时候不能把工具调用和对应的工具结果拆开。如果保留了助手的工具调用消息却把对应的工具结果消息截掉了模型会看到一个发起了调用但没有结果的悬空状态行为会很奇怪。截断要以完整的调用-结果对为单位。6.3 状态该放框架里还是放工具里这是个设计问题我纠结过一阵。有些状态是框架级的比如当前轮数、消息历史有些状态是工具级的比如某个工具内部的缓存、连接池。我的结论是框架只管和对话流程相关的状态业务状态交给工具自己管。框架不应该知道你的工具内部有什么状态它只负责把调用和结果串起来。工具如果需要跨调用保持状态自己维护但要注意线程安全和生命周期。这个边界划清楚很重要否则框架会越来越重最后变成一个什么都管的大泥球。我见过一些框架为了方便把各种业务状态都塞进核心结果核心越来越难维护插件化也就名存实亡了。7. 我实测中踩到的几个坑7.1 工具返回结果太长把上下文撑爆第一个坑很典型。我写了个工具返回一大段 JSON结果几轮下来上下文就满了模型开始报错或者行为异常。后来我改成返回精简后的关键字段问题就解决了。教训是工具返回给模型的结果不是越全越好而是越相关越好。模型不需要知道所有细节它只需要知道这一步的结果是什么、下一步该干嘛。把无关字段砍掉既省 token 又减少干扰。如果确实需要返回大量数据可以考虑返回一个摘要加一个引用 ID模型需要细节的时候再调另一个工具去取。这种分页思路在处理大数据时很有用。7.2 并发工具调用时的顺序问题第二个坑是并发。模型有时候会在一轮里同时发起多个工具调用框架如果并发执行返回顺序可能和调用顺序不一致。虽然每个结果都带 ID理论上能对应上但如果你的工具之间有依赖关系并发就会出问题。我的处理是默认串行执行除非明确知道工具之间无依赖。串行慢一点但结果可预测。如果确实要并发确保工具是纯函数式的不依赖执行顺序。这个坑的隐蔽之处在于它不一定每次都出问题可能十次里错一次排查起来很痛苦。所以宁可保守一点。7.3 模型假装调用了工具第三个坑最坑。有时候模型会在文本里写我将调用 xxx 工具但实际上并没有真的发起结构化调用只是嘴上说说。框架如果只解析结构化调用就会漏掉这种情况导致循环提前结束任务没完成。解决办法是在系统提示里明确要求模型必须通过结构化方式调用工具不要在文本里描述调用意图。同时框架层面可以加一层检测如果模型文本里出现了工具名但没有结构化调用给个警告或者强制再走一轮。我实测下来加了系统提示之后这种情况少了很多但没完全消失。所以关键任务上还是要有人工兜底或者结果校验。7.4 错误信息格式不统一导致模型看不懂第四个坑前面提过这里再展开说。工具执行失败时如果错误信息格式五花八门模型的理解成本就高。我统一成{error: true, type: xxx, message: yyy}这种结构后模型对错误的处理明显更稳。还有个细节错误信息里不要暴露敏感的内部细节比如堆栈、文件路径。这些对模型没用还可能泄露信息。给模型看的错误信息应该是人话版的说清楚哪里错了、可能的原因是什么就够了。8. 拆完之后我对插件化的新理解拆完这个框架我最大的感受是插件化不是一个功能而是一种纪律。它要求框架作者在每一个设计决策上都问自己这个逻辑是核心的还是可变的。核心的留下可变的开出去。说起来简单做起来要在无数细节上克制。这个框架让我印象最深的一点是它对扩展点的克制。它没有搞一大堆钩子、事件、中间件就是老老实实把工具接口定义清楚把注册机制做顺把循环控制留出余地。少即是多扩展点太多反而让人不知道从哪下手。如果你也在做类似的东西我的建议是先把工具这一个扩展点做到极致别急着加别的。工具是 Agent 最核心的扩展维度把这个做顺了框架的可用性就上来了。至于记忆、规划、多 Agent 协作这些等工具机制稳定了再说。最后分享一个我自己的习惯每次拆完一个框架我都会试着用它实现一个自己的小需求而不是只看代码。因为看代码只能看到它设计成什么样动手用才能发现它实际好不好用。这次我给自己加了个查天气的工具从写接口到跑通大概二十分钟全程没碰核心代码。这个体验本身就是对它插件化设计最好的验证。