尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

DeepSeek Harness 可塑运行时实战:用 Cordis 与 Session log 搭建可插拔 Agent 内核

发布时间:2026/9/28 19:03:09

资讯中心
01
ARTICLE

DeepSeek Harness 可塑运行时实战:用 Cordis 与 Session log 搭建可插拔 Agent 内核

DeepSeek Harness 可塑运行时实战:用 Cordis 与 Session log 搭建可插拔 Agent 内核
1. 为什么固定内核的 Agent 越写越难改如果你写过一段时间的 Agent 框架大概率经历过这个阶段一开始 loop 里就三件事——拼 prompt、调模型、执行工具代码清爽得像教科书。等到业务方开始提需求事情就变味了。今天要换一个模型供应商明天要给某个租户单独配一套会话存储后天又要求工具调用前插入审批流。每加一个需求你就在 loop 里塞一个 if塞到最后没人敢动那个文件。问题的根子不在代码写得烂而在于扩展点放错了层。普通 coding agent 的扩展方式是在固定 loop 外面挂工具、挂 Skill、挂交互面板这解决的是增加可执行动作。可一旦你要改的是工具调度顺序、上下文投影方式、终止条件、存储语义、会话隔离策略扩展点就不得不钻进 harness 内部。这时候固定内核就成了枷锁。DeepSeek Harness下称 DSH想回答的就是这个问题把 Agent harness 从固定内核推向可塑运行时。它的做法是引入两个支点——用 Cordis 管插件依赖和生命周期用 Session log 保存运行事实。前者回答系统现在装成什么样后者回答系统刚才做过什么。这篇就带你从零在本地跑通一个可替换内核组件的 Agent 运行时把模型调用统一走 TaoToken 的 Key/API 通道避免在多个供应商之间来回改配置。适合谁看已经写过基础 Agent loop、想进一步做模块化拆分的后端或全栈工程师正在被换模型就要改一堆代码折磨的团队以及想理解可塑运行时到底解决什么工程问题的技术负责人。下面所有步骤都可以直接复制执行我会给出 config.toml 骨架、Cordis 插件注册片段和 Session log 回放验证的完整过程。2. 前置准备TaoToken 统一 Key 与 API 通道在动手拆内核之前先把模型调用这条链路固定下来。可塑运行时最怕的就是换个内核组件结果模型接入方式也跟着变所以我们要让模型调用成为一个稳定的 Service而不是散落在各个插件里的裸 HTTP 请求。TaoToken 在这里扮演的角色是统一入口你只需要维护一个 Key、一个 base_url就能在同一个运行时里切换不同模型而不用为每个 provider 写一套适配代码。这对可塑运行时特别重要——因为你的 LLM seam 应该只依赖一个稳定的接口而不是依赖某家供应商的 SDK。先拿到 Key。访问控制台创建 API Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite创建完成后把 Key 存到环境变量里不要硬编码进 config.tomlexport TAOTOKEN_API_KEYsk-你的keyAPI 的基础地址是https://taotoken.net/api这个地址在后面的 config.toml 里会用到。注意它不带任何查询参数是干净的 base_url。如果你还没决定用哪个模型可以先在模型对话页面试几条 prompt确认返回格式和 tool call 行为符合预期https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite接入文档在这里遇到参数问题优先查它https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite提示把 Key 放进环境变量不只是安全习惯。可塑运行时会在插件热替换时反复重建 LLM Service如果 Key 写在配置文件里每次替换都可能触发一次配置重读反而增加不确定性。环境变量读一次就够了。3. 可复制配置config.toml 骨架与 Cordis 插件注册现在进入正题。我们要搭的运行时结构是这样的一个 Cordis 容器负责加载插件图LLM Service 作为最底层的 ProviderTool Service 和 Session Service 依赖它Agent Loop 再依赖前两者。任何一层被替换Cordis 会先停掉受影响的 Consumer再在新依赖下重新激活。3.1 config.toml 骨架先建目录结构mkdir -p dsh-runtime/plugins dsh-runtime/sessions cd dsh-runtime然后写 config.toml。这份配置把模型通道、插件加载路径、Session log 落盘位置都声明清楚[runtime] name dsh-local log_level info [llm] provider taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY model deepseek-chat timeout_ms 60000 [session] log_dir ./sessions append_only true projection surface [plugins] dir ./plugins enabled [ llm-service, tool-service, session-service, agent-loop ]这里有几个点值得说明。api_key_env指向环境变量名而不是 Key 本身这样配置可以进版本库。append_only true是 Session log 的核心约束——历史只追加不改写压缩时追加 replacement 节点而不是覆盖原事件。projection surface表示模型可见历史是从事件日志投影出来的不是直接读聊天记录。3.2 Cordis 插件注册片段Cordis 的核心思想是插件即服务。每个插件通过 context 注册服务通过 inject 声明依赖等依赖就绪后才激活。下面是一个 LLM Service 插件的注册片段// plugins/llm-service.js import { Service } from cordis export const name llm-service export const inject [config] export function apply(ctx, config) { class LLMService extends Service { constructor(ctx) { super(ctx, llm) this.baseUrl config.llm.base_url this.apiKey process.env[config.llm.api_key_env] this.model config.llm.model } async chat(messages, tools []) { const res await fetch(${this.baseUrl}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${this.apiKey} }, body: JSON.stringify({ model: this.model, messages, tools, stream: false }) }) if (!res.ok) { throw new Error(LLM request failed: ${res.status}) } return res.json() } } ctx.plugin(LLMService) // 注册可逆副作用卸载时清理 ctx.on(dispose, () { ctx.logger.info(llm-service disposed) }) }关键在最后那段ctx.on(dispose, ...)。Cordis 要求所有注册动作都同步登记 disposer卸载时逆序清理。工具、监听器、后台任务都该这么处理。这样当 LLM Service 被替换时旧的连接、定时器、缓存不会残留。Tool Service 的注册方式类似但它 inject 的是llm// plugins/tool-service.js export const name tool-service export const inject [llm] export function apply(ctx) { const tools new Map() ctx.provide(tools, { register(name, schema, handler) { tools.set(name, { schema, handler }) ctx.on(dispose, () tools.delete(name)) }, list() { return [...tools.values()].map(t t.schema) }, async invoke(name, args) { const tool tools.get(name) if (!tool) throw new Error(unknown tool: ${name}) return tool.handler(args) } }) }注意inject [llm]这行。它声明了 Tool Service 依赖 LLM Service。当 LLM Service 被替换时Cordis 会先停用 Tool Service等新 LLM Service 就绪后再重新激活它。这就是依赖驱动的加载/卸载。3.3 Session log 写入Session Service 负责把运行事实追加到日志。事件类型包括 turn、step、user message、assistant chunk、tool call、tool result以及压缩时的 replacement 节点// plugins/session-service.js import { appendFileSync, mkdirSync } from fs import { join } from path export const name session-service export const inject [config] export function apply(ctx, config) { const dir config.session.log_dir mkdirSync(dir, { recursive: true }) ctx.provide(session, { append(sessionId, event) { const record { ts: Date.now(), sessionId, ...event } appendFileSync( join(dir, ${sessionId}.jsonl), JSON.stringify(record) \n ) return record }, read(sessionId) { const path join(dir, ${sessionId}.jsonl) return require(fs) .readFileSync(path, utf8) .trim() .split(\n) .map(line JSON.parse(line)) } }) }append-only 的好处在这里体现得很直接恢复会话时不需要信任任何当前状态快照直接从事件日志重放即可。压缩不改写过去而是追加一个 replacement 事件改变当前 surface原始事件永远保留。4. 验证请求跑通一次带工具调用的会话配置和插件都就位后写一个入口脚本把运行时拉起来发一次真实请求验证链路。// index.js import { Context } from cordis import { readFileSync } from fs import { parse } from toml const config parse(readFileSync(./config.toml, utf8)) const ctx new Context() ctx.provide(config, config) await ctx.plugin(import(./plugins/llm-service.js), config) await ctx.plugin(import(./plugins/tool-service.js)) await ctx.plugin(import(./plugins/session-service.js), config) // 注册一个示例工具 ctx.tools.register( get_time, { type: function, function: { name: get_time, description: 返回当前时间, parameters: { type: object, properties: {} } } }, () ({ now: new Date().toISOString() }) ) const sessionId test-001 ctx.session.append(sessionId, { type: user_message, content: 现在几点了 }) const messages [ { role: system, content: 你是一个会调用工具的助手。 }, { role: user, content: 现在几点了 } ] const result await ctx.llm.chat(messages, ctx.tools.list()) console.log(LLM 返回:, JSON.stringify(result, null, 2)) ctx.session.append(sessionId, { type: assistant_message, content: result.choices?.[0]?.message })运行node index.js成功的话你会看到类似这样的输出{ id: chatcmpl-xxx, choices: [ { index: 0, message: { role: assistant, content: null, tool_calls: [ { id: call_abc, type: function, function: { name: get_time, arguments: {} } } ] }, finish_reason: tool_calls } ] }这说明 LLM Service 通过 TaoToken 通道拿到了模型响应并且模型正确识别了可用工具。接下来把 tool call 结果回填、再发一次请求就能看到完整的工具调用闭环。4.1 Session log 回放验证跑完之后检查 sessions 目录cat sessions/test-001.jsonl你应该看到两条事件按时间顺序追加{ts:1730000000000,sessionId:test-001,type:user_message,content:现在几点了} {ts:1730000000123,sessionId:test-001,type:assistant_message,content:{...}}回放验证的意义在于任何时候你都能从这条事实链重建模型当时看到了什么。写一个简单的投影函数function projectSurface(events) { const surface [] for (const e of events) { if (e.type user_message) { surface.push({ role: user, content: e.content }) } else if (e.type assistant_message) { surface.push({ role: assistant, content: e.content }) } else if (e.type replacement) { // 压缩替换指定区间但不删除原始事件 surface.splice(e.from, e.to - e.from 1, ...e.replacement) } } return surface }这个投影出来的 surface 就是下一次请求要发给模型的 messages。压缩发生时原始事件还在日志里只是 surface 变了。这就是可重建和可解释的基础。4.2 热替换内核组件现在验证可塑运行时的核心能力替换 LLM Service 而不重启进程。假设你要把模型从 deepseek-chat 换成另一个只需要卸载旧插件、加载新插件// 卸载旧 LLM Service await ctx.dispose(llm-service) // 用新配置重新加载 config.llm.model deepseek-reasoner await ctx.plugin(import(./plugins/llm-service.js), config)Cordis 会先停掉依赖llm的 Tool Service等新 LLM Service 就绪后重新激活它。整个过程里 Session log 不受影响因为它是独立的事实面。这就是事实面和拓扑面分离的价值——换内核组件不会丢历史。5. 本篇常见错排查报错一LLM request failed: 401Key 没读到。检查TAOTOKEN_API_KEY是否在当前 shell 会话里 export 过。如果你在子进程里跑环境变量不会自动继承需要在启动命令前显式带上TAOTOKEN_API_KEYsk-xxx node index.js报错二unknown tool: get_time工具注册发生在插件加载之后或者 Tool Service 被卸载重建时工具没重新注册。检查ctx.tools.register是否在await ctx.plugin(...)之后调用。更稳妥的做法是把工具注册也放进一个插件里让它跟着 Tool Service 的生命周期走。报错三Cannot find module cordis依赖没装。Cordis 是独立包npm init -y npm install cordis toml注意 Node 版本要支持顶层 await建议 18 以上。报错四Session log 里出现重复事件多半是插件被重复加载。Cordis 的ctx.plugin对同一个插件名默认只加载一次但如果你手动 dispose 后又 plugin可能产生两份监听器。检查 disposer 是否完整登记尤其是ctx.on注册的监听器有没有在 dispose 时移除。报错五热替换后旧工具还在这是典型的副作用没清理。Cordis 能管理的是纳入 context 生命周期的副作用如果你在插件里直接setInterval或者往全局 Map 里塞东西dispose 时不会自动清理。所有注册动作都要同步登记 disposer这是硬约束。注意Cordis 不提供操作系统级沙箱也不保证外部副作用可撤销。数据库写入、远程调用、已发送消息这类越界动作仍然需要插件自己做幂等或补偿。别指望框架帮你回滚一个已经发出去的 HTTP 请求。6. 把模型通道固定下来再谈内核可塑走到这里你已经有了一个能跑的可塑运行时雏形Cordis 管插件图Session log 管事实链LLM 调用统一走 TaoToken 的 Key/API 通道。这个结构最大的好处是当你需要换模型、换存储、换 Loop 时改动被限制在单个插件里不会波及整个 harness。如果你接下来要长期做编码类 Agent或者要跑多轮 Agent 任务建议把 Key 和通道配置固化下来用 Coding Plan 管理额度避免每次调试都重新配一遍https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite需要新建或轮换 Key 的时候走 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接入细节和参数说明以文档为准https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite最后留一个实操建议先别急着把整套 DSH 搬进生产。从能力 seam 生命周期清理 事件事实链这三件事开始借鉴通常比直接迁移整套框架更稳。等你真的遇到多 Provider、多会话隔离、多 runtime 组合的组合压力时再上完整的可塑运行时也不迟。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。