Codex vs DeepSeek Harness:两种Agent架构路线,谁才是未来?
文章目录
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
前言
最近深扒了DeepSeek Harness的架构,给我最大的感触就是:这哥们儿是真敢玩。
之前拆解Codex的时候,人家好歹还留了个特权内核当“定海神针”;到Harness这儿倒好,直接把“一切皆插件”卷到了天花板——连Agent主循环都能当插件随便换。
今天就给大家盘盘这套架构到底是怎么搭的,里面有哪些神设计值得直接抄作业,又有哪些坑得提前避开。
1. 先扒底座:Cordis的五个核心概念
先透个底,Harness的底座不是从零自研的,是基于Cordis插件框架做的二次开发。
所以想看懂它的骚操作,得先把Cordis的五个核心概念整明白,不然看代码就像看天书,每个字都认识,放一起不知道啥意思。
1.1 插件:本质就是实现Service的对象
第一个概念,插件本质上就是个实现Service的东西。
它可以是个带inject和apply的函数,也可以是个Service子类,由Cordis把生命周期挂载到上下文里。
这意味着什么?意味着Harness里从模型适配器、工具注册表,到会话日志,甚至Agent主循环,全都是一模一样的插件形态。
没有谁是“亲儿子”,没有谁有特权,想换哪个换哪个。
搁别的项目里,主循环那都是核心中的核心,改一行都怕牵一发而动全身;这儿倒好,直接跟其他插件平起平坐。
1.2 上下文:装Service的仓库
第二个概念,上下文就是Service的仓库。
每个Service在ctx上占一个固定的键,比如ctx.tools、ctx.llm。别的插件要用的时候,按键找就行,不用import具体的实现代码。
这才是解耦的精髓啊兄弟们。你换个LLM实现,调用方的代码一行都不用改,因为人家依赖的是ctx.llm这个键,不是某个具体的类。
搁很多项目里,改个底层实现,上游调用方得跟着改半天依赖,对比一下就知道差距了。
1.3 依赖靠inject声明,加载顺序自动推
第三个概念,依赖写在inject里,加载顺序自动算出来。
你插件需要什么服务,写进inject字段,Cordis就会等这些服务都就绪了再启动你的插件。不用你手写一大串启动顺序,还生怕排错了。
做过大型项目启动逻辑的都懂,启动顺序的隐式依赖就是个天坑。加个新功能,初始化顺序没排对,直接给你报个空指针,查都查半天。
1.4 通信靠类型化事件,分派模式写进契约
第四个概念,事件通信,而且分派模式是契约的一部分,不是口头约定。
总共四种分派方式,什么时候用、什么表现,明明白白:
| 分派模式 | 是否await | 分派顺序 | 有返回值 |
|---|---|---|---|
emit 散发 | 否 | 按注册顺序观察 | 无 |
waterfall 瀑布 | 否 | 按注册顺序观察 | 有 |
parallel 并行 | 是 | 所有监听器并行观察 | 无 |
serial 串行 | 是 | 按注册顺序观察 | 有 |
新增事件还得用@mode标签写进文档,生成工具会自动校验声明和实际对不对得上。
搁很多项目里,这玩意儿全靠老员工口口相传:“这个事件能不能改返回值啊?”“好像不行吧,上次我改就崩了。”传承全靠踩坑。
1.5 插件注册是可逆的
第五个,也是我觉得最该抄的设计:注册是可逆的。
所有的注册,不管是提示词片段、工具schema还是监听器,全都是通过ctx.effect()或者ctx.on()来装,卸载的时候能按原路撤销。注册方法必须返回一个disposer。
说真的,多数项目的插件机制就是个貔貅——只往里装,不往外吐。热重载?运行时关个功能?想都别想。
就这一条,看似简单,收益是整个扩展体系直接就能双向操作了。
2. 插件长啥样?契约就五个字段
讲完概念看实际代码。Harness的插件就是原生的Cordis插件,没搞自己的基类、装饰器那些花里胡哨的东西。
基础接口就五个可选字段,给你们看代码:
export interface<T = any> {
/** 用于 fiber 诊断与 logger 名的显示名 */
name?: string
/** 插件启动前用于校验 config 的 standard-schema 校验器 */
Config<any, T>
/** 插件所需的服务,仅当全部可用时才加载 */
inject?: Inject
/** 插件提供的服务名 */
provide?: string | string[]
/** 插件声明自己消费哪些服务的 intercept 配置 */
<boolean>
}
这里提个坑,Cordis 4里已经没有reusable字段了,看旧文档写插件很容易踩这雷。
2.1 条件注入:依赖写在哪一层是个学问
给大家看个很讲究的小插件,功能就是声明工具集的展示形态。代码很短,门道不少。
export const name = 'tool-presentation'
/** 所需服务。注意 codeRuntime 不在这里。 */
export const inject = ['tools']
export interface Config {
mode: ToolPresentationMode
}
export const<Config> = z.object({
mode: z.union(['native', 'ptc', 'both'] as const).required(),
})
export function apply(ctx: Context, config: Config): void {
if (config.mode === 'native') {
ctx.tools.presentAs('native')
return
}
// 这个等待本身就是那次响亮的失败
ctx.inject(['codeRuntime'], (runtimeCtx: Context) => {
runtimeCtx.tools.presentAs(config.mode)
})
}
看出门道没?codeRuntime没写在顶层的inject里,而是在apply内部按条件注入。
为啥?因为native模式根本不需要代码运行时啊。你要是写在顶层,那只用native的部署,就因为缺个运行时,这插件直接挂了。
这就好比你点个素面,店家非要给你加牛肉,说套餐里都有——你说你烦不烦?
当然,这么做也有前提,就是得有激活审计机制,让没满足的等待能被看见,不能悄无声息就没了。
2.2 配置也是契约的一部分
还有个纪律我特别认同:插件里不许有硬编码的可调参数。
凡是随部署变的选择,都必须是Config字段,能从cordis.yml里改;协议常量、安全参数这些就必须写成常量。
很多项目就分不清这个边界。部署该改的参数写死在代码里,安全该固定的反而做成可配置。哪天不小心把安全阈值改了,出问题都不知道为啥。
3. 能力接缝:全是插件为啥不散架
有人肯定要问了,全是插件,没有特权内核,那系统不会散成一盘沙吗?
人家用“接缝”这个概念给串起来了。一个正经的接缝必须包含三个角色:声明接口的Service Definition、实现接口的Service Provider、用接口的Consumer。
缺一个都不算真正的扩展点。
这就直接排除了那种假扩展点——就写个空接口,既没第二个实现,也没明确的调用方。看着像能扩展,实际上换个实现根本跑不起来。
3.1 几个典型的接缝例子
给大家举几个例子,直观感受下:
ctx.llm:接缝,实现有DeepSeek官方版、PI AI版、还有回放专用版ctx.fs:接缝,本地、沙箱、E2B远程三种实现随便切ctx.approval:接缝,没装审批模块的时候默认拒绝,天生安全
最绝的是ctx.agentLoop,它的角色是bundle,也就是个组合包。没有任何扩展依赖它本身,大家依赖的是dsh-agent的事件和服务。
这才是“连主循环都能换”的真正含义——不是它写得有多灵活,是没人把它当爹供着。
4. Agent循环:全是可拦截的扩展点
接下来讲Agent的运行流程。Codex是分线程、轮次、步三层;Harness就轮次和步两层,但中间插了一堆可拦截的扩展点。
4.1 先搞懂两个基础定义
先明确两个概念,不然后面说的都对不上号:
一个步骤 = 一次模型请求 + 它调用的所有工具。
一个轮次 = 零个或多个步骤。领输入的时候打开,没活干了就关闭。
注意“零个步骤”是合法的。也就是说,请求被拒绝了,也会生成一个空轮次记在日志里。
这点真的很细节。搁别的系统里,请求被拦了就拦了,啥痕迹没有;排查问题的时候,你都不知道是用户没发请求,还是系统直接给吞了。
4.2 三个事件域,别用错地方
Harness把事件分成了三个域,做扩展的时候第一件事就是选对域。
会话事件:会落日志,重启还在。比如轮次开始结束、消息、工具调用这些。
Agent事件:运行时的活跃状态,观察拦截用。比如 inbox、步骤状态这些。
能力事件:具体能力的事件,比如fs/、tools/。不用import循环就能加策略。
简单说:需要重启还能看到的,走会话事件;就运行时用一下的,走另外俩。
别等写完了才发现状态丢了,再补持久化,那成本就高了。
4.3 瀑布式事件:可以直接短路
有一批事件是瀑布式的,比如agent/pre-step、agent/request这些。监听器必须调用next()才能往下传,不调用直接返回,整条链就短路了。
这设计太实用了。想拦截个请求?直接不调用next()就完事了。想改点东西?改完再调用next()传下去。
当然也有坑,漏写next()的话,后面的逻辑全静默失效,连个报错都没有。所以人家直接写成硬性纪律了。
5. 工具执行流水线:十个环节,细节拉满
这部分是我觉得打磨得最好的,也是最值得直接抄走的。工具执行整条流水线分十个环节,每个环节能干啥、不能干啥,边界清清楚楚。
5.1 第1环:参数落盘了就不许改
第一步tool/call,先把调用参数写进日志,UI也同步渲染出待执行卡片。
从这之后,前置策略就不能改写参数了。
为啥?因为已经记录下来、展示给用户了。你偷偷改了,日志和界面记的是一套,实际执行的是另一套,这不就造假了吗?
想改参数?可以,在落盘之前改。生成调用的那一侧负责,策略层别碰。
5.2 第3环:单调性守卫,只能拒绝不能放行
这个设计我愿称之为神来之笔。守卫的返回类型是这样的:
type ToolGuard =<ToolExecution>) => string | undefined
返回字符串就是拒绝,字符串是拒绝理由;返回undefined就是不表态。
发现了吗?没有“放行”这个返回值。
这意味着什么?后注册的守卫,永远没法把前一个守卫的拒绝给改成放行。决策只能越来越严,注册顺序不影响最终结果。
搁常见的实现里,每个钩子返回个布尔值,那可就乱套了——谁最后加载谁说了算,权限全靠加载顺序,离谱。
5.3 第2环:没审批?默认拒绝
审批环节也是默认安全逻辑。没有任何审批应答方的时候,直接判定为不可用,也就是拒绝。
也就是说,你装个基础版,没装交互前端,所有需要审批的操作全给你拒了。
反过来想,要是默认放行,那才叫可怕。忘了装审批模块,啥危险操作都能直接跑。
5.4 中间三环分工明确
tools/execute环:包裹真正的执行逻辑,超时、重试、度量都在这。也是唯一能替换取消信号的地方。
tools/post-execute环:可以改结果内容或者结果值,也能直接判定失败。
finalizeContent:内容唯一收口,失败路径也走这。保证所有结果都经过同一段代码定稿。
最后到tools/result就只能看了,结果已经深度冻结,改不了了。
总结一下:改写在指定位置,观察在冻结之后,中间有个定稿点绕不过去。
6. 会话日志:模型可见即已记录
持久化这块,Harness走得更远。它直接把日志定为模型上下文的唯一来源,还用一条运行时不变量给钉死了:模型可见即已记录。
也就是说,但凡能送到模型眼前的东西,必须能从日志里重建出来。加个新的输入类型?先加个会话事件再说。
这直接把一类疑难杂症给消灭了。就是那种:回放会话的时候,跟当初运行的结果不一样,查半天发现是有个提示词没写进日志。
这种bug最恶心了,正常跑的时候啥事儿没有,一恢复、一分叉就出问题。
6.1 只有三种事件能进模型历史
日志里有五十多种事件,但能投影给模型看的只有三种:用户消息、助手消息、工具结果。
而且还有个“表面”机制,事件可以追加,也可以遮蔽——注意是遮蔽,不是删除。被替换的事件还在日志里,只是不投影给模型了。
每次遮蔽都得交代清楚:你遮的是谁、依据是什么。全都是可审计的。
比如Code Mode里,模型写的程序发起的子调用,就不会重新进模型上下文。让程序自己消化中间结果,只把最终结论交回来,不然上下文早就炸了。
6.2 落盘:一个会话一个文件
存储格式是JSONL加zstd压缩,一个会话一个文件。路径设计也很讲究,不用进程当前目录当默认值。
这点太重要了。Agent会随便执行bash命令啊,工作目录说变就变,你默认用cwd存会话文件,回头给你撒的到处都是,找都找不到。
还有崩溃修复机制。进程崩了最后一帧可能是坏的,人家有明确的修复流程:先截掉坏的尾巴,把能解的事件补回去,再追加结束事件,把没走完的轮次和工具都补齐。
工具结局还分两种:未开始、结局未知。有没有副作用,一目了然。
宁可留下明确标异常的历史,也不留看起来正常但残缺的历史。这点真的很专业。
7. 上下文压缩:细节控的操作
压缩这块,比常见的做法细太多了,有几个判断特别反常识。
7.1 两个触发时机
压缩就两个触发点:
压力触发:token数到窗口的80%了,提前准备压缩。
溢出触发:真的超了报错了,直接压缩然后重试。
而且配置校验很严,尾部保留比例不能大于阈值比例,不然加载期直接报错。
总比运行时莫名其妙压缩失败强吧。
7.2 压缩请求是主线请求的真前缀
这个设计我真的要吹爆。压缩调用的system和tools段,直接复用主线请求的,就末尾加一条压缩指令。
为啥这么干?因为这样和主线请求有公共前缀啊!服务端的KV缓存直接就能命中。
反常识吧?很多人以为给摘要单独写个精简的system提示词更省token,实际上因为和主线请求没公共前缀,缓存全失效,总开销反而更高。
把视角从“单次请求多大”换成“和上一次共享多少前缀”,结论直接反过来。
7.3 收敛门:摘要不能越缩越长
还有个收敛门,压缩后的摘要token数要是比原内容还多,直接报错。
别笑,真的可能。本来就没几句话,一摘要,啰啰嗦嗦更长了。要是没这道门,后续的压力判断直接陷入死循环。
8. Profile与组合包:分层装配的艺术
说了这么多可替换,那到底怎么替换?靠Profile和组合包。
运行中的Harness就是一棵插件树,启动的时候一层一层叠加上去的。
组合包就是Cordis配置+代码的分发包;Profile就是具体的组装方案,列了要叠哪些组合包,还有自己的patch。
叠加顺序是固定的:先组合包,再profile自己的patch,再全局的patch,最后命令行的overlay。
一条patch的粒度就是按id定位条目,替换整个config,或者插新条目。给你们看个例子:
- id: session-persistence-jsonl
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: !!js dshHomePath('sessions')
8.1 想知道最终生效的是什么?打出来
多层配置叠加最常见的问题就是:到底哪层生效了?
人家直接给了个命令,直接打印当前生效的全部配置:
dsh --profile web --dump-config
而且打出来的任何一条,你都能用自己的patch替换。
可替换性不是靠预留几个扩展点实现的,是靠所有东西都以可寻址的条目形式存在。
就这一条命令,成本极低,收益极高。多少项目踩过配置叠加的坑,到线上了都不知道实际跑的是什么配置。
9. Host与Client:被类型系统逼出来的边界
还有个很有意思的设计:Host和Client分成两个独立的类型检查单元。
为啥要分开?不是因为前后端分离的常规操作,是因为类型层面冲突。
两边都用声明合并往ctx上挂服务,还用同一批键名。放同一个program里,类型定义直接打架。
解决办法不是改键名,是干脆分成两个program,互相看不见。
你敢信?架构边界居然是被类型系统逼出来的。
9.1 边界不靠lint,靠两道门禁
人家也没写什么“client不许import host”的lint规则,靠两道机器检查保证隔离。
第一道是Project Reference面隔离,脚本遍历检查引用对不对。
第二道是客户端构建期纯度门,不在白名单的跨包运行时import直接报错。
为啥不用lint?因为lint查的是语法,这条边界要查的是“运行时会不会真的产生依赖”,这是打包器才知道的事儿。
10. 子Agent:同一个接口,后面可以是另一个产品
子Agent在Harness里就是个普通接缝,提供方有六个。
有同进程新建的,有从当前会话分叉的,还有更狠的:直接委派给Codex、Claude Code,甚至通过SDK委派给别的实现。
也就是说,Codex和Claude Code在这儿的地位,跟本地bash执行器是一样的,都是某个接缝下的一个实现。
接口抽象得足够干净,下面装的是自家函数还是别家产品,对调用方来说没区别。
11. 总结:最值得抄的七个设计
最后给大家提炼一下,最值得迁移到自研Agent里的七个设计,按优先级排:
-
注册即可逆副作用。统一原语,返回disposer,插件能装就能卸。
-
模型可见即已记录。日志是唯一来源,从根源杜绝上下文不一致。
-
能力接缝三角色齐备。接口、实现、消费方缺一不可,拒绝假扩展点。
-
把不可推翻编码进类型。守卫只能拒绝不能放行,顺序不影响结果。
-
结构性事实一律生成。目录、依赖图都从代码生成,人工只写判断。
-
装配做成可寻址条目,加dump命令。配置透明,避免叠加黑洞。
-
辅助调用做成主线真前缀。复用缓存,性能反而更好。
11.1 没有银弹:代价也得说
当然,没有免费的午餐。不设特权内核也有代价。
第一,概念负担重。上手得先学Cordis五个概念,不然门都入不了。
第二,边界会出现在意想不到的地方。比如Host和Client的分离,是类型系统逼的,不是业务需求。
第三,装配可见性依赖工具。没有dump命令,你根本不知道最终跑的是什么。
第四,兼容承诺难给。可替换的边界太多,每个都是兼容负担。
所以说,Codex和Harness不是优劣之分,是同一道题的两个解。
一个留着特权内核换演进速度,一个取消特权内核换彻底的可替换性。选哪个,取决于你打算把哪一部分长期钉死。
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了
更多推荐



所有评论(0)