如果不仅工具可以替换,模型接口、会话存储甚至 Agent 循环都能替换,系统应该怎样组织?DeepSeek Harness 选择把这些能力做成插件,由 Cordis 管理它们的依赖和生命周期。
本文分析官方仓库 deepseek-ai/deepseek-harness 的提交 c291e7961a515f6d7af9304e7fd1d257929aef26,根包版本为 0.1.5-rc.2。开发者预览阶段变化较快,以下路径与行为绑定此提交;没有安装、启动或执行模型。
先理解 Cordis 管什么
Context通过服务解析把 ctx.tools 一类访问连接到当前作用域提供的服务;插件注册、事件和资源清理围绕这个上下文组织。它不直接定义订单报告怎么生成。
插件声明依赖,例如 inject = ['tools']。服务尚未就绪时,插件处于等待状态;服务可用后才加载。配置文件中把一行放在另一行前面,不等于可靠的加载顺序,真正依据是服务依赖。
一个插件实例由 Fiber 表示,经历 PENDING、LOADING、ACTIVE、UNLOADING、DISPOSED 等状态,加载失败可进入 FAILED。它让“配置写了但工具没有出现”有可检查的原因:可能依赖未满足,也可能初始化失败,而不是先怀疑模型。
这些机制可以对照生命周期说明与服务解析阅读。作用域隔离描述服务解析范围,不是操作系统的文件或进程隔离。
注册一个工具时发生了什么
官方工具教学经过 defineTool → ctx.tools.register → ctx.tools.execute。定义包含参数、执行体和输出描述,注册把它加入当前服务,执行通过统一入口处理。工具描述进入模型可见信息与实际函数执行是两个阶段。
配套order-tool.ts返回 A1042 的虚构状态。代码中 parameters 描述输入,output.schema 描述规范结果,output.render 决定如何向会话或界面呈现结果。把规范值和呈现文本分开,可以减少某种 UI 需求污染业务返回结构。
这个工具只接受固定订单,没有连接真实身份系统。模型参数里的订单号仍不能证明访问权,业务部署需要在执行体或受控业务服务核对当前主体。
工具执行不是直接调用一个函数
tools/index.ts的 execute 进入准备和调度流程,再根据准备结果决定分派、结果后处理或直接终止。实际工具体由 dispatchToolBody 调用,结果再经过规范化和呈现。
例如取消发生在执行体开始前,可以标记为未分派;如果取消发生在工具体已经启动后,结果意义不同。源码把调用者取消信号与包装层信号组合,并等待已经开始的 Promise 收敛,再生成取消结果。这能保留生命周期关系,但不代表能强制杀掉一个忽略信号、永不返回的任意外部程序。
工具结果事件适合观测。tool-observer.ts只记录工具名称与关联 ID,不输出完整订单正文。tools/result 是结果观察位置,不能拿它替代动作开始前的业务授权。
从一个 await 看执行完成的含义
下面是固定提交 dispatchToolBody 中的关键语句,保留原变量名,增加中文注释;其他准备与清理逻辑省略。
// 先记录执行体已经开始,取消时才能区别“没执行”和“开始后中止”。
state.bodyInvoked = true;
// await 等待插件执行体收敛;调用一个忽略取消信号的函数不会自动获得强制终止能力。
const returned = await tool.execute(exec.arguments, exec);
// 返回值还要规范化,不能把任意插件对象直接当作最终工具结果。
const result = this.createSuccessResult(exec, tool, returned);
报告上传超时的例子中,是否已经进入执行体影响后续核对方式;但真正有没有上传成功,还要查询业务系统。运行框架可以更准确地描述本地阶段,却不能替代远端事实。
插件卸载为什么需要资源所有权
如果插件注册了工具和监听器,卸载后它们应移除;如果还启动了计时器或连接,也需要关闭。通过 Cordis 注册 API 建立的效果会关联到插件生命周期,自行管理的资源则应放进 ctx.effect 并返回清理函数。
例如插件 A 创建连接,插件 B 依赖 A 提供的服务。配置热更新使 A 卸载,B 不能继续假定原服务仍然存在。卸载顺序、进行中的任务和共享资源需要一起管理。
源码中的异步清理可以并发开始。若“先停止接收,再等待任务结束,再关连接”有严格顺序,应在同一个清理函数内显式等待,而不是注册三个互相独立的异步回调就假设依次执行。
卸载插件也不会自动回滚已经上传的文件。资源生命周期与外部业务补偿仍然是不同问题。
一条消息怎样经过循环与会话
agent-loop/index.ts创建具体 ReactLoopAgent,并管理活跃实例和关闭过程。agent.ts处理收件队列、turn 与 step,准备模型请求,收集响应并进入工具处理。
本提交可以看到 turn/start、step/start、assistant/message 等会话事件,以及请求头和投影逻辑。会话日志保存发生过什么,请求表面组织下一轮实际提交什么,UI 还可以有自己的显示方式。三者不必是同一份完整文本。
例如模型请求失败,系统可能保存一次 assistant attempt,而没有形成可作为最终回答的完整消息。重建会话时需要区分尝试和已完成消息,不能把所有中间流式片段都当成成功响应。
日志可以重建执行经过,却不能代替外部系统事实。一次上传调用缺少结果时,仍需查询上传服务,而不是仅凭会话事件推测成功或失败。
Code Mode 怎样改变模型与工具的往返
普通工具模式中,模型提出读取订单,收到结果后再提出读取政策,随后组织报告。代码模式允许模型写一段程序,在一次程序执行内组合多个工具请求,选择返回给模型的结果。
ptc.ts定义 run_code 的呈现与分派机制:嵌套调用仍进入工具注册表和调度体系,子调用日志保留用于重建,而模型历史主要接收外层整理后的结果。因此它不是绕过工具系统直接获得所有宿主函数。
这个提交已经区分 TypeScript 与 Python 的 SDK 呈现,依据所安装 CodeRuntime 的语言生成说明。不能只因早期示例使用 TypeScript,就写成代码模式永远只支持一种语言。具体部署提供哪种运行时,要继续看插件组合。
假设同时读取五份独立资料,代码可以先并发请求,再提取需要的字段,减少逐轮模型往返。若下一次读取依赖前一次推理判断,或者需要保留大量原文,节省就不一定明显。模型写错程序、结果裁剪过度和代码运行预算也会引入新问题,不能把模式切换写成必然性能提升。
模式由组合定义,不是额外训练一个模型
可以从base 配置观察公共服务,再从preset观察某类 Agent 的工具与呈现方式。官方介绍中的 Standard、Minimal、Code 和 Creator 表达不同使用方式,落地仍要追实际配置。
配置覆盖还存在具体语义:本提交 base 文件注明针对行的 config 替换,不是递归合并所有字段。升级插件或覆盖配置时,漏掉旧字段可能改变行为。阅读配置应该像阅读代码一样追踪最后生效值。
这个架构适合学习什么
最值得学习的是能力接口、服务作用域、生命周期和观测如何分开。它适合需要替换多种环境能力的系统,但插件多也意味着配置与依赖更复杂,必须有有效版本记录和诊断入口。
本系列不把 Cordis 的插件隔离称作沙箱,也不把工具 Schema 称作业务授权。理解了这两点,读者才能把架构的灵活性与实际运行边界同时看清。最后一篇回到AgentScope Java,观察另一种面向应用集成的组织方式。