欢迎光临
我们一直在努力

[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} from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis'{
interface Context {
foobar: ()=>void;
}
}

const context = new Context();
context.provide("foobar", ()=>console.log("foo"));
const subContext = context.isolate("foobar");
subContext.provide("foobar", ()=>console.log("bar"));

const FiberStateNames = ['PENDING', 'LOADING', 'ACTIVE', 'FAILED', 'DISPOSED', 'UNLOADING'];
const fiber = subContext.plugin(ctx=>ctx.foobar());
console.log(FiberStateNames[fiber.state]);
setTimeout(()=>console.log(FiberStateNames[fiber.state]),1000);

输出:

LOADING
FAILED

从输出结果可以看出Cordis试图加载注册的插件,但是失败了。

2. 捕捉插件执行异常

为了捕捉插件在执行过程中究竟发生了怎样的异常,我们将插件函数针对注册服务的调用封装在如下所示的try/catch块中。

import { Context} from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis'{
interface Context {
foobar: ()=>void;
}
}

const context = new Context();
context.provide("foobar", ()=>console.log("foo"));
const subContext = context.isolate("foobar");
subContext.provide("foobar", ()=>console.log("bar"));

const FiberStateNames = ['PENDING', 'LOADING', 'ACTIVE', 'FAILED', 'DISPOSED', 'UNLOADING'];
const fiber = subContext.plugin(ctx=>{
try{
ctx.foobar();
}catch(e){
console.error(e);
throw e;
}
});
console.log(FiberStateNames[fiber.state]);
setTimeout(()=>console.log(FiberStateNames[fiber.state]),1000);
process.stdin.resume();

输出:

LOADING
Error: cannot get property "foobar" without 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} from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis'{
interface Context {
foobar: ()=>void;
}
}

const context = new Context();
context.provide("foobar", ()=>console.log("foo"));
const subContext = context.isolate("foobar");
subContext.provide("foobar", ()=>console.log("bar"));

const FiberStateNames = ['PENDING', 'LOADING', 'ACTIVE', 'FAILED', 'DISPOSED', 'UNLOADING'];
const fiber = subContext.inject(["foobar"], ctx=>ctx.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} from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis'{
interface Context {
foobar: ()=>void;
}
}

const context = new Context();
context.provide("foobar", ()=>console.log("foo"));
const subContext = context.isolate("foobar");

const FiberStateNames = ['PENDING', 'LOADING', 'ACTIVE', 'FAILED', 'DISPOSED', 'UNLOADING'];
const fiber = subContext.plugin(ctx=>{
const disposable = ctx.provide("foobar", ()=>console.log("bar"));
ctx.foobar();
return disposable;
});
console.log(FiberStateNames[fiber.state]);
setTimeout(()=>console.log(FiberStateNames[fiber.state]),1000);

输出:

LOADING
bar
ACTIVE

5. 异常具体在哪里抛出来的?

由于插件仅仅是调用ctx.foobar()方法提取并执行服务函数,而且我们知道这个插件的ctx只是一个代理而已,针对foobar方法的读取会被代理处理器拦截,既然是在读取ctx的foobar属性时抛出了异常,我们就通过代理处理器的定义来看看它究竟注入了怎样的拦截操作。通过DeepSeek Harness插件内核-07:Context全面解析的介绍,我们知道这个代理处理器定义在ReflectService的静态属性handler上。

export class ReflectService {
static handler: ProxyHandler<Context> = {
get: (target, prop, ctx: Context) => {

const error = new Error(`cannot get property "${prop}" without inject`)
try {

return ctx.events.waterfall('internal/get', ctx, prop, error, () => {
const key = target[symbols.isolate][prop]
let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber
while (true) {
const impl = fiber.store?.[prop]
if (impl) return getTraceable(ctx, impl.value)
if (prop in fiber.inject) {
error.message = `cannot get required service "${prop}" in inactive context`
throw error
}
if (!fiber.runtime) throw error
if (fiber.parent[symbols.isolate][prop] !== key) throw error
fiber = fiber.parent.fiber
}
})
} catch (e: any) {
throw e === error ? 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自然不相等,于是异常被抛出。

赞(0)
未经允许不得转载:171主机测评 » [DeepSeek Harness插件内核-12]这算Cordis一个大BUG吗?[续]
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址