尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

[DeepSeek Harness插件内核-12]这算Cordis一个大BUG吗?[续]

[DeepSeek Harness插件内核-12]这算Cordis一个大BUG吗?[续] 在前文中我们介绍了通过调用isolate方法创建的子Context因与父Context共享同一个Fiber导致隔离能力丧失的问题。在文中为了演示这个问题我刻意调用inject方法而不是plugin方法来为子Context注册插件这是因为后者注册的插件根本无法执行。1. 调用plugin方法注册的插件无法执行以如下的演示程序为例我们创建了一个作为根的Context并采用指定的名称foobar注册了一个基于函数的服务函数输出字符串foo 然后我们调用Context的isolate方法针对服务名称foobar创建了一个与根Context隔离的子Context并在子Context上面使用相同的名称注册了另一个函数后者输出bar。我们在子Context注册了一个插件该插件会从当前Context中提取并执行注册的foobar服务函数。为了确认插件对应Fiber的状态我们分别在得到该Fiber的时候以及一秒后两次输出它的状态。import{Context}fromdeepseek-ai/cordisdeclaremoduledeepseek-ai/cordis{interfaceContext{foobar:()void;}}constcontextnewContext();context.provide(foobar,()console.log(foo));constsubContextcontext.isolate(foobar);subContext.provide(foobar,()console.log(bar));constFiberStateNames[PENDING,LOADING,ACTIVE,FAILED,DISPOSED,UNLOADING];constfibersubContext.plugin(ctxctx.foobar());console.log(FiberStateNames[fiber.state]);setTimeout(()console.log(FiberStateNames[fiber.state]),1000);输出LOADING FAILED从输出结果可以看出Cordis试图加载注册的插件但是失败了。2. 捕捉插件执行异常为了捕捉插件在执行过程中究竟发生了怎样的异常我们将插件函数针对注册服务的调用封装在如下所示的try/catch块中。import{Context}fromdeepseek-ai/cordisdeclaremoduledeepseek-ai/cordis{interfaceContext{foobar:()void;}}constcontextnewContext();context.provide(foobar,()console.log(foo));constsubContextcontext.isolate(foobar);subContext.provide(foobar,()console.log(bar));constFiberStateNames[PENDING,LOADING,ACTIVE,FAILED,DISPOSED,UNLOADING];constfibersubContext.plugin(ctx{try{ctx.foobar();}catch(e){console.error(e);throwe;}});console.log(FiberStateNames[fiber.state]);setTimeout(()console.log(FiberStateNames[fiber.state]),1000);process.stdin.resume();输出LOADING Error: cannot get propertyfoobarwithout inject at Object.callback(file:///E:/projects/typescript/ts-learning/App2/app.js:9:13)at Fiber.execute(file:///E:/projects/typescript/ts-learning/node_modules/deepseek-ai/cordis/lib/index.js:1070:28)at file:///E:/projects/typescript/ts-learning/node_modules/deepseek-ai/cordis/lib/index.js:1141:34 at composeError(file:///E:/projects/typescript/ts-learning/node_modules/deepseek-ai/cordis/lib/index.js:199:18)at Fiber._execute(file:///E:/projects/typescript/ts-learning/node_modules/deepseek-ai/cordis/lib/index.js:1136:10)at Fiber._reload(file:///E:/projects/typescript/ts-learning/node_modules/deepseek-ai/cordis/lib/index.js:1355:16)at process.processTicksAndRejections(node:internal/process/task_queues:104:5)FAILED虽然没有从输出中得到抛出异常的确切未知但是得到一个错误的消息cannot get property foobar without inject。可以确定错误发生在从Context中提取foobar属性的时候原因是不能提取未显式声明注入的服务其实这是合理的。3. 先声明再消费在 Cordis 框架中插件只能消费显式声明注入的服务是其依赖注入的核心安全机制。简单来说如果一个插件想要使用某个服务它必须在注册时通过inject属性显式声明除非这个服务注册在当前Fiber绑定的Context上。否则即使该服务在全局真实存在该插件在运行时也无法访问它就会出现上述的错误。从抛出的这个异常也可以进一步验证前文的论断由于作为根的context和通过调用isolate方法创建的subContext它们共享同一个Fiber所以它最终只会保留第二次存储的服务实例。由于插件具有自己专属的Fiber我们必须按照如下的方式调用inject方法以提供服务注入的方式来注册插件。import{Context}fromdeepseek-ai/cordisdeclaremoduledeepseek-ai/cordis{interfaceContext{foobar:()void;}}constcontextnewContext();context.provide(foobar,()console.log(foo));constsubContextcontext.isolate(foobar);subContext.provide(foobar,()console.log(bar));constFiberStateNames[PENDING,LOADING,ACTIVE,FAILED,DISPOSED,UNLOADING];constfibersubContext.inject([foobar],ctxctx.foobar());console.log(FiberStateNames[fiber.state]);setTimeout(()console.log(FiberStateNames[fiber.state]),1000);输出LOADING bar ACTIVE这种先声明后消费的模式是为了解决传统 Agent 框架中插件顺序依赖、组件硬编码以及热插拔时程序容易崩溃的痛点可以实现如下的特性和解决如下的问题3.1 实现真正的“热插拔”与故障隔离在 DeepSeek Harness 中所有的基础能力如工具中心、模型服务、文件系统、会话管理等全部都是动态加载、甚至随时可能被卸载的插件。如果不声明你的插件直接通过 import 或硬编码去调用另一个服务一旦那个服务因为沙箱断开、底层实现卸载或配置更新而突然消失你的插件就会因为找不到服务而直接引发未捕获的运行时报错Crash。通过 inject 声明后Cordis 能够精确追踪谁依赖了谁。底层实现被卸载依赖它的工具也会一起停下。 新的实现准备好后工具再重新加载。” 调用方不会拿着一个已经失效的过时句柄去盲目执行从而保证了系统整体的稳定性。3.2 靠依赖关系决定启动顺序而非书写顺序在传统的单体架构中开发者必须小心翼翼地安排代码的加载顺序例如必须先初始化大模型服务再初始化工具最后启动 Agent 循环。而在 Cordis 中“加载顺序通过服务依赖表达而非手动编排启动序列。” 当你声明了某个依赖插件声明所需的服务后会等待这些服务就绪才启动。这让 Cordis 微内核可以在启动时自动在内存中拓扑排序并安全地组装整套插件组合。3.3 解耦具体实现面向接口编程在 DeepSeek Harness 内部其他插件通过key 查找服务而非导入具体实现。 当你通过inject声明依赖一个如ctx.llm或ctx.tools的服务Key时你不需要关心这个 LLM 到底是 DeepSeek-V3 还是 GPT-4也不用管它是本地沙箱还是远程沙箱。通过先声明系统可以在运行时动态替换底层供给者而调用方的代码完全不需要修改。4. 自己提供的服务可以不同声明如果不提供服务注入声明唯有一途那就是按照如下的方式将服务注册在当前Context上import{Context}fromdeepseek-ai/cordisdeclaremoduledeepseek-ai/cordis{interfaceContext{foobar:()void;}}constcontextnewContext();context.provide(foobar,()console.log(foo));constsubContextcontext.isolate(foobar);constFiberStateNames[PENDING,LOADING,ACTIVE,FAILED,DISPOSED,UNLOADING];constfibersubContext.plugin(ctx{constdisposablectx.provide(foobar,()console.log(bar));ctx.foobar();returndisposable;});console.log(FiberStateNames[fiber.state]);setTimeout(()console.log(FiberStateNames[fiber.state]),1000);输出LOADING bar ACTIVE5. 异常具体在哪里抛出来的由于插件仅仅是调用ctx.foobar()方法提取并执行服务函数而且我们知道这个插件的ctx只是一个代理而已针对foobar方法的读取会被代理处理器拦截既然是在读取ctx的foobar属性时抛出了异常我们就通过代理处理器的定义来看看它究竟注入了怎样的拦截操作。通过DeepSeek Harness插件内核-07:Context全面解析的介绍我们知道这个代理处理器定义在ReflectService的静态属性handler上。exportclassReflectService{statichandler:ProxyHandlerContext{get:(target,prop,ctx:Context){...consterrornewError(cannot get property ${prop} without inject)try{...returnctx.events.waterfall(internal/get,ctx,prop,error,(){constkeytarget[symbols.isolate][prop]letfiber(ctx[symbols.shadow]asContext??ctx).fiberwhile(true){constimplfiber.store?.[prop]if(impl)returngetTraceable(ctx,impl.value)if(propinfiber.inject){error.messagecannot get required service ${prop} in inactive contextthrowerror}if(!fiber.runtime)throwerrorif(fiber.parent[symbols.isolate][prop]!key)throwerror fiberfiber.parent.fiber}})}catch(e:any){throweerror?enhanceError(e):e}},}}很幸运我们一眼就发现了抛出错误的那行代码我们来做一下简单的分析当我们利用subContext代理去读取foobar属性时它会根据提供的服务名称foobar从target(就是subContext对象)的symbols.isolate字典中提取作为服务实例唯一标识的Symbol此时得到的这个Symbol是指向我们希望的那个服务第二次注册的输出字符bar的那个函数由于当前Fiber的store并不能提供目标服务所以第一轮while循环会走完当进入第二轮循环后程序会走到倒数第二行if (fiber.parent[symbols.isolate][prop] ! key) throw error此时它将这个Symbol与父Context的symbols.isolate字典存储的对应Symbol进行比较对应第一次注册的服务。两个Symbol自然不相等于是异常被抛出。
返回列表