云计算百科
云计算领域专业知识百科平台

看完DeepSeek Harness源码:终于见到把Agent主循环做成插件的框架

文章目录

  • 1. 为啥这套代码值得你熬夜啃
    • 1.1 先吐槽下市面上的Agent框架
    • 1.2 人家反过来玩:主循环自己也是插件
  • 2. 一句话讲透全局架构
    • 2.1 三个关键词,搞定全局认知
    • 2.2 启动和运行是两回事
  • 3. 底层框架Cordis:五个概念就够了
    • 3.1 五个核心概念,记不住算我输
    • 3.2 插件的两种写法,对应两种依赖强度
    • 3.3 四种事件模式,最核心的是瀑布
  • 4. 启动期:怎么拼出一棵完整的插件树
    • 4.1 Bundle和Profile是什么关系
    • 4.2 Patch机制:最后写入的赢
  • 5. 最精髓的设计:能力接缝三角色
    • 5.1 什么是能力接缝
    • 5.2 举个最直观的例子
    • 5.3 框架里到处都是这种接缝
  • 6. 整个系统的地基:事件溯源日志
    • 6.1 唯一真相源,是只能追加的日志
    • 6.2 一条铁律:模型可见即已记录
  • 7. Agent主循环:turn/step状态机拆解
    • 7.1 先把名词掰扯清楚
    • 7.2 主循环自己也是个普通插件
    • 7.3 循环到底是怎么跑的
    • 7.4 输入的四种投递方式
    • 7.5 工具调度:兼顾并发和确定性
  • 8. 工具注册表与守护执行管线
    • 8.1 工具注册:作用域化,还可逆
    • 8.2 一次工具调用,要过整条管线
    • 8.3 内置工具有多全
  • 9. 最后聊聊这套架构的可取之处

在这里插入图片描述
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,
传送门https://blog.csdn.net/qq_34419312

1. 为啥这套代码值得你熬夜啃

1.1 先吐槽下市面上的Agent框架

不知道你们有没有改过那种老Agent框架的代码?

想加个新工具,得先翻主循环的源码,改prompt拼接逻辑,调工具分发规则,改完一跑,哦豁,原来的功能崩了三个。

就像你家装修想加个插座,得先拆承重墙,拆完发现水管也漏了,电线也断了,最后等于重装了半套房子。

现在绝大多数Agent框架,本质都是「一个写死的主循环 + 一堆内置能力」。核心逻辑是有特权的,你想扩展就得给核心打补丁,改一个多依赖的功能,牵一发而动全身,治理成本高到离谱。

1.2 人家反过来玩:主循环自己也是插件

DeepSeek Harness(后面简称dsh)最狠的一点,就是把「一切皆插件」刻进了骨子里。

模型适配器是插件,工具注册表是插件,会话日志是插件,连Agent的主循环本身,都是个普通插件。

没有什么「不能动的核心代码」,也没有什么「必须改源码才能加的功能」。你想扩展能力,永远是在旁边加个新插件,不用碰已有的东西。

就像从老板一言堂的小公司,变成了平台化的产业园。你想做什么业务,自己租个铺子注册就能干,不用改整个园区的规划,谁也别当特权阶层。

这套能力搭在一个叫Cordis的插件框架上,整个仓库是pnpm monorepo,光packages下面就有两百多个包,全是独立插件。

2. 一句话讲透全局架构

2.1 三个关键词,搞定全局认知

不用记复杂的架构图,记住三个词就行,这是理解整个框架的三把钥匙。

第一个是Cordis插件树。所有能力都挂在一个共享的上下文ctx上,每个服务占一个固定的key,大家靠key互相发现,不用直接import具体实现。

第二个是能力接缝。每个可替换的能力都拆成了「定义、实现、消费」三个角色,换一个实现,就能整块换掉产品的行为边界。

第三个是事件溯源会话。Agent交互的唯一真相是一条只能追加、不能修改的事件日志,模型看到的对话历史,都是从日志里当场推导出来的,不是单独存的。

2.2 启动和运行是两回事

简单说就是启动期组装插件,运行期跑状态机。

启动的时候,通过Profile和Bundle一层层叠加配置,把需要的插件拼成一棵树;运行的时候,所有插件共享上下文,由Agent Loop驱动一个个turn和step,所有状态都写进事件日志。

3. 底层框架Cordis:五个概念就够了

3.1 五个核心概念,记不住算我输

先把底层逻辑说清楚,dsh所有玩法都建立在这上面。

Plugin(插件):就是一个实现了服务的对象,可以是带inject和apply的函数,也可以是Service的子类。说白了就是一个个功能模块。

Context(上下文):就是个服务大厅。每个服务占一个固定的窗口号,比如ctx.tools、ctx.llm,你要办事直接找窗口,不用管窗口里坐的是谁,只要接口对就行。

inject(依赖声明):插件提前说清楚自己要依赖谁,框架等依赖的服务都就绪了,才会激活这个插件。加载顺序全靠依赖关系表达,不用你手写一堆启动顺序的代码,改一次骂一次。

Typed Event(类型化事件):服务通过TypeScript的声明合并注册事件,支持四种分发模式,扩展全靠事件。

Effect(可逆副作用):所有的注册操作——不管是注册工具、注册prompt还是注册监听器,都通过effect完成。卸载的时候会按顺序自动回滚,干干净净,不会有残留。

说直白点,就是租房子退租的时候,你得把自己的东西全搬走,垃圾带走,房东收房的时候和你住进来的时候一模一样。热重载为什么能做到无泄漏?全靠这个机制。

3.2 插件的两种写法,对应两种依赖强度

dsh里插件有两种常见写法,对应不同的依赖硬度。

一种是函数式插件,导出name、inject和apply。里面还可以用ctx.inject写可选依赖——对方存在才激活这段逻辑,不存在就跳过,属于「有你更好,没你也能活」。

另一种是Service子类,用static inject声明硬依赖——依赖不存在,这个插件直接就不构造了,属于「没你我活不了」。

能力的可选性,全靠依赖声明就表达清楚了,不用写一堆if判断。

3.3 四种事件模式,最核心的是瀑布

四种事件分发模式各有用处,但最灵魂的是waterfall(瀑布)模式。

emit就是纯广播,发出去就不管了,大家各收各的,没有返回值,适合做日志、埋点这种观察者场景。

waterfall是环绕式中间件,每个监听器都能拿到参数,还能调用next传给下一个,不想传就直接短路。什么拦截请求、改写prompt、否决工具调用,全靠这个实现。就像过安检,每个人都能查你包,还能给你塞东西,不想让你过直接就拦下了。

parallel是并行执行,大家一起跑,等全部结束再返回,适合批量处理。

serial是串行执行,按注册顺序一个一个来,有返回值,适合做终止判断这种需要顺序结果的场景。

整个框架里的扩展点,基本全是瀑布模式,这是统一的拦截改写机制,不用每个地方各搞一套。

4. 启动期:怎么拼出一棵完整的插件树

4.1 Bundle和Profile是什么关系

既然一切皆插件,那一个能跑的程序,本质就是一堆插件拼出来的树。怎么拼?靠两级配置。

Bundle就是捆绑包,一个npm包,里面带一份cordis.patch.yml配置,相当于一批插件的打包分发。比如基础包dsh-base,里面包含了模型适配、工具、持久化这些核心能力。

Profile就是配置档,按顺序叠多个Bundle,再加上用户自己的自定义配置。比如web版就是基础包加web界面包,无头版就是基础包加命令行运行包。

打个比方,Bundle就是预制菜包,有基础款、素菜款、肉菜款;Profile就是你自己搭配套餐,基础包加素菜包,最后再自己加点辣椒油,层层叠加。

4.2 Patch机制:最后写入的赢

所有配置叠加,都靠patch文件。你可以替换某一行的配置,可以插入新插件,可以禁用某个已有插件。

规则很简单:最后写的算。用户配置覆盖Bundle配置,命令行参数覆盖用户配置。

想换个默认模型?不用改源码,写一行patch就覆盖了。想关掉热重载?加个disabled:true就行。想加个自定义插件?insert一段进去就完事。

就像游戏打MOD,你想改什么直接打补丁,不用动游戏本体。想看看实际启动的配置长啥样?一条命令就能dump出来,每一行都能改,没有黑箱。

更灵活的是,patch里还支持写JS表达式,依赖就绪后再求值。比如用环境变量指定模型,环境变了配置自动跟着变,不用写死。

5. 最精髓的设计:能力接缝三角色

5.1 什么是能力接缝

这是dsh最核心的可组合性机制,也是「一切皆插件」能落地的关键。

一个可替换的能力,拆成三个角色:服务定义、服务提供者、消费者。三个角色齐全了,才叫一个接缝。

服务定义:声明接口的抽象类,占住ctx上的某个key,定好规范。就像国标插座的标准,规定了孔位、电压,大家都按这个来。

服务提供者:具体的实现类,可以有很多个。就像墙插、插排、充电宝,都是国标插座的实现。

消费者:注入并使用这个服务的一方,只依赖抽象的key,不管背后是什么实现。就像你的手机充电器,只要是国标五孔就能插,不用管插的是墙插还是充电宝。

5.2 举个最直观的例子

就拿Shell能力来说。

定义部分只规定了shell能做什么:解析请求、执行命令、启动后台进程,占住ctx.shell这个位置。

实现可以有很多种:本地bash、沙箱bash、本地powershell,只要符合接口规范就行。

消费者就是面向模型的bash工具,它只调用ctx.shell的方法,永远不知道背后跑的是本地还是沙箱。

这意味着什么?你想把整个执行环境从本地迁到远程沙箱,不用改bash工具,不用改文件工具,不用改终端工具,甚至不用改LSP工具。只需要换一个Shell的实现,所有依赖它的工具,全家桶一起跟着迁走。

换一个provider,就改变整个产品的能力边界,这才是真正的解耦。

5.3 框架里到处都是这种接缝

除了shell,文件系统、网页搜索、语言服务、沙箱、上下文压缩、子Agent……全都是这种接缝设计。

有的接缝只允许同时有一个实现,比如shell;有的允许多个实现按名字共存,比如模型适配器、子Agent。

说白了,就是把所有能变的地方,都做成了可插拔的接口。以后想加新的实现,不用动任何现有代码,加个插件就行。

6. 整个系统的地基:事件溯源日志

6.1 唯一真相源,是只能追加的日志

在讲主循环之前,必须先讲会话日志,因为循环本身几乎不存状态,一切都从日志里推导。

一个会话,就是一条只能追加、不能修改的事件日志。用户发消息、模型返回分片、工具调用、工具结果……所有事情都以事件的形式追加进去。

这就是唯一的真相。模型看到的对话历史,不是单独存的数组,是当场从日志里派生出来的。

就像会计做账,只能新增凭证,不能涂改旧账,账本就是唯一的真相。你想查账、想回放、想分叉,全靠这本账。

不像有些框架,会话状态存在七八个地方,改漏一个就出bug,查问题的时候,你都不知道哪个数据是对的。

6.2 一条铁律:模型可见即已记录

整个框架有一条铁律:任何能被模型看到的输入,都必须能从日志里重建出来。

你想加个新的模型可见输入?那就必须对应加一个新的会话事件。

这条规则保证了,只要日志在,就能100%复现当时的场景。回放、调试、审计、甩锅,全都有据可查。

7. Agent主循环:turn/step状态机拆解

7.1 先把名词掰扯清楚

先统一一下术语,别被各种名词绕晕。

step(步):一次模型请求,加上它触发的工具执行,就是一步。

turn(轮):一次把收到的输入全部处理完的过程,里面可以包含0个或者多个step。简单说,用户发一句话,Agent把该干的活都干完,就是一轮。

round(回合):更外层的策略迭代,比如目标循环、多Agent协作循环,比turn更大。

7.2 主循环自己也是个普通插件

别觉得主循环有多特殊,在dsh里,它就是个声明了5个依赖的普通插件。

它把自己注册成Agent工厂,会话创建的时候就用这个工厂生成驱动器。你觉得默认的循环不好用?完全可以自己实现Agent接口,把工厂换成你的,整个循环逻辑就全换了。

连主循环都能随便换,这才是「一切皆插件」的字面意思。

7.3 循环到底是怎么跑的

整个循环的逻辑很清晰,就是一直驱动turn,直到没有待处理的输入为止。

每一轮turn,先追加一个turn/start事件,然后进入循环:

第一步是preStep,认领输入、组装系统提示词,然后走agent/pre-step瀑布。这里是扩展点,插件可以改写消息,也可以直接否决,上下文压缩就是挂在这里的。

然后追加step/start事件,认领的用户消息也全部落盘。

接下来是step核心:从日志派生出对话历史,构建请求,调用大模型,流式分片全部落盘,组装成完整消息再落盘。如果有工具调用,就分发执行,结果也落盘。

最后追加step/end事件,如果模型自然停止,而且没有新的待处理输入,就结束这一轮,追加turn/end事件。

7.4 输入的四种投递方式

驱动器有个收件箱,对外提供四种投递消息的方式,语义都不一样。

send和followup:排队等下一轮处理,正常消息都走这个。

steer:插队,直接进当前正在跑的步,适合紧急干预。

inject:悄悄注入上下文,不会唤醒驱动器,等下次有消息的时候一起被带走,适合偷偷塞参考资料。

就像给员工派活,有正常排期的,有紧急插队的,还有默默放他桌上让他自己看的,设计得很细。

7.5 工具调度:兼顾并发和确定性

工具调用的调度也很有意思。

它严格按照模型给出的顺序提交结果,但执行的时候,能并行的就放进并发池里跑,遇到独占的就形成屏障。

这样既利用了并发提升速度,又严格保证了结果顺序,回放的时候结果是确定的,不会因为并发时序不一样跑出不同结果。

中止的时候,还会给没跑的调用补一个合成的错误结果,保证日志回放依然合法,不会断档。

8. 工具注册表与守护执行管线

8.1 工具注册:作用域化,还可逆

工具不是全局一刀切的,是作用域化的。

有全局都能用的工具,也有只挂在某个Agent上的工具。还可以对某个Agent屏蔽特定的全局工具,屏蔽之后,不管是prompt里还是执行时,都感知不到,和真的不存在没区别。

这就是不同Agent有不同人格、不同能力的底层实现。每个Agent看到的工具集都可以不一样,不用改工具本身的代码。

而且所有注册都是可逆的,卸载插件的时候,注册的工具会自动被清理掉,干干净净。

8.2 一次工具调用,要过整条管线

你以为工具调用就是直接跑execute函数?太天真了。一次调用要穿过一整条守护管线。

首先是tools/pre-execute瀑布,权限校验、沙箱策略、审批逻辑,全在这一层。之后还有单调守卫,只能拒绝不能放行,守住安全底线。

然后是tools/execute环绕瀑布,超时控制、重试逻辑、性能指标,这些通用的环绕逻辑都包在这里,里面才是工具真正的execute代码。

执行完还有tools/post-execute瀑布,可以接受、阻断、替换结果,也可以追加上下文。最后还有内容归一化,把结果处理成统一格式,再冻结成权威结果,写进日志。

好处是什么?你想加个安全审计、加个调用限流、加个日志埋点,不用改每一个工具的代码,直接挂在管线上就行。所有工具自动生效,完全解耦。

比如文件系统的读写检查策略,就是个独立插件,挂在文件相关的事件上,连工具的代码都不用碰。

8.3 内置工具有多全

内置的工具覆盖面相当广,基本常用的能力都有了。

Shell类有bash、powershell,支持一次性执行和持久终端。

文件类有读写编辑,还有图片读取、字符串替换编辑器。

检索类有文件遍历、内容搜索,打包了ripgrep。

网页类有搜索、页面抓取。

还有代码智能的LSP工具、子Agent委派工具、工作流编排、后台任务管理、会话状态管理、用户交互工具……基本能想到的都有了。

9. 最后聊聊这套架构的可取之处

总结下来,这套架构最亮眼的地方,其实就是把软件工程的老道理,扎扎实实落到了Agent框架里。

第一,没有特权核心。连主循环都是插件,扩展永远是加东西,不是改东西。热重载、插件化,都是自然带来的结果,不是硬加的功能。

第二,能力接缝解耦做得彻底。消费者只依赖抽象,不依赖具体实现。换一个provider,一整条能力链跟着换,改动成本降到了最低。

第三,事件溯源做真相源。状态全从日志派生,回放、分叉、审计、调试,全都统一在一条流上。「模型可见即已记录」的铁律,让整个系统天然可审计。

第四,统一的瀑布扩展点。拦截、改写、策略,全用同一种模式。权限、审批、沙箱、压缩、超时、重试,都是独立插件,互不耦合。

说白了,好的架构从来不是让你一开始写出完美的代码,而是让你后面改需求、加功能的时候,不用骂娘。

很多框架喊了很多年「开闭原则」,喊了很多年「解耦」,真落到代码里,还是核心逻辑一坨,扩展全靠打补丁。这套框架算是把「对扩展开放,对修改关闭」真的玩明白了。

P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/qq_34419312

赞(0)
未经允许不得转载:网硕互联帮助中心 » 看完DeepSeek Harness源码:终于见到把Agent主循环做成插件的框架
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!