DeepSeek Harness 扩展点全解:tools/systemPrompt/agent/llm 背后的事件总线机制
CFzjyAI Engineer▍深耕领域AI Agent 架构设计Harness 插件开发Context Engineering多智能体协同 RAG▍技术底座PythonTypeScript·Cordis 插件MCP Skills·FastAPIDocker▍工程能力专注 Agent 运行时与 Harness 插件开发、长上下文与流式交互优化、LLM 全链路工程化落地「 让 AI 交互更智能让技术落地更高效 」欢迎技术探讨 / 项目合作关注解锁大模型与智能交互的无限可能DeepSeek Harness 扩展点全解tools / systemPrompt / agent / llm 背后的事件总线机制《DeepSeek Harness 插件架构》连载专栏 · 第 04 篇第 03 篇讲了插件间的直接协作服务调用这一篇讲另一种协作方式——事件。官方教程第四章的开篇把两者的分工一句话讲清“服务支持直接调用事件让插件无需知道有哪些插件正在监听就能发出通知。harness 使用事件处理工具结果、模型请求和审批决定等交互。”翻译一下服务是点名找人事件是对空广播——发通知的人不知道谁在听听通知的人不依赖谁在发。正是这层互不知晓让扩展点成为可能宿主发出一个事件任何插件都能挂上自己的逻辑宿主一行代码不用改。而第 01 篇埋下的伏笔——“宿主把自己也做成了插件”——在这一篇完全展开DSH 的四大扩展点tools 工具流水线、systemPrompt 提示词组装、llm 模型流式、agent 智能体协调全部是事件。理解了事件系统你就理解了 DSH 扩展性的全部来源。痛点一扩展 改宿主代码。想给工具调用加审计、给提示词注入规范、给模型输出脱敏——没有事件系统每件事都要 fork 框架。痛点二协作双方互相知道彼此存在。import 式协作让加一个监听者变成改发出方生态长不起来。痛点三拦截形同虚设。没有 waterfall 的短路语义审批类需求只能靠插件自觉——而自觉不可靠。本文事实全部来自官方 Cordis 教程《事件》一章与 Cordis 入门的分发模式表。读完你会拿到事件的声明/发出/监听完整三件套、五种分发模式的官方 API 与语义、waterfall 拦截的官方实战示例、和一段可运行的审批拦截模拟器。想追完整系列下一篇bundle 与 patch 底层实现可以先关注我的 CSDN 主页。文章目录DeepSeek Harness 扩展点全解tools / systemPrompt / agent / llm 背后的事件总线机制一、为什么需要事件原理拆解篇1.1 从一个直觉误区说起协作不一定认识彼此1.2 事件与服务的分工再明确二、基础篇事件的声明、发出与监听官方示例全拆2.1 完整三件套StatsService 官方示例2.2 事件与扩展点四大扩展点的真身三、进阶篇五种分发模式的官方 API3.1 官方语义表3.2 模式选择的两个工程维度四、高阶waterfall 拦截实战与 harness 真实事件4.1 官方 waterfall-demo转换与短路4.2 一段可运行的审批拦截模拟器4.3 harness 的真实 waterfall 事件五、实战避坑指南高频问题与一键解决速查表结语一、为什么需要事件原理拆解篇1.1 从一个直觉误区说起协作不一定认识彼此上一篇文章讲服务时说过消费方不导入提供方。这一篇更进一步有些协作连对方是谁都不该知道。想象 DSH 的宿主想支持工具调用前审批这个能力——宿主最好的设计是什么答案是宿主只负责在工具调用前发出一个事件“有人要调工具了参数是这些”至于谁来审批、怎么审批、批不批得下来宿主一概不管。任何团队都能写一个审批插件监听这个事件按自己的策略拦截没有任何审批插件时事件无人监听、调用直接放行——宿主的行为自然兼容两种世界。这就是事件的本质价值把能力钩子从具体实现中剥离。官方教程点名的三个 harness 事件就是绝佳注脚“工具结果、模型请求和审批决定”——工具的结果需要事件谁想记录、谁想改写宿主不知道模型的请求需要事件谁想换配置、谁想加参数宿主不知道审批的决定需要事件谁能拍板宿主不知道。宿主不知道四个字就是扩展性的全部秘密。1.2 事件与服务的分工再明确上一篇文章的官方实践规则值得在这里重读一遍“拦截和策略优先使用事件直接能力调用优先使用服务方法。”配上本篇的事件定义分工的判断标准就更清晰了你要做的事用服务方法用事件直接调用别人的能力注册工具、发起会话✅ 点名调用❌观察别人的行为记录日志、统计指标❌✅ emit 监听改写别人的行为脱敏、注入规范、换模型配置❌✅ waterfall 包装裁决别人的行为审批、权限❌✅ waterfall 短路一句话口诀我找你办事用服务你在办事我想掺一脚用事件。判断错了方向插件就会写得拧巴——比如用服务方法去做审计意味着每个被审计的插件都要 import 审计服务并主动上报漏一个就盲一个而用事件做审计监听器挂上就全覆盖谁调工具都逃不掉。二、基础篇事件的声明、发出与监听官方示例全拆2.1 完整三件套StatsService 官方示例官方教程第四章给了事件系统的完整最小示例一个负责计数并在变化时发通知的服务import{Service,typeContext}fromdeepseek-ai/cordisdeclaremoduledeepseek-ai/cordis{interfaceContext{stats:StatsService}interfaceEvents{stats/report(name:string,count:number):void}}exportclassStatsServiceextendsService{privatecountsnewMapstring,number()constructor(ctx:Context){super(ctx,stats)}bump(name:string){constnext(this.counts.get(name)??0)1this.counts.set(name,next)this.ctx.emit(stats/report,name,next)}}exportconstnamestatsexportfunctionapply(ctx:Context){ctx.plugin(StatsService)}逐块拆解三件套声明interface Events和上一篇的interface Context合并互为镜像——上一篇给服务名加类型这一篇给事件名加类型。事件签名stats/report(name: string, count: number): void声明了名字、参数、返回从此ctx.emit和ctx.on对这个事件都有完整类型。官方还给了命名约定“namespace/action命名约定让扁平的事件命名空间保持易读”——事件名是全局扁平的用斜杠分区stats/report、tool/call避免撞名。发出ctx.emitthis.ctx.emit(stats/report, name, next)——服务在状态变化时广播。注意发出方完全不关心有没有人听零监听者时 emit 静默通过这是互不知晓在发出侧的表现。监听ctx.on消费方长这样官方 reporter.tsimporttype{Context}fromdeepseek-ai/cordisimporttype{}from./stats.tsexportconstnamereporterexportconstinject[stats]exportfunctionapply(ctx:Context){ctx.on(stats/report,(name,count){console.log([stats]${name}-${count})})ctx.stats.bump(tool_call)ctx.stats.bump(tool_call)ctx.stats.bump(prompt)}两个细节值得注意import type {} from ./stats.ts不会在运行时导入任何内容——它的唯一作用是让 TypeScript 看到 stats.ts 里的声明合并拿到事件类型。官方原话“这行不会在运行时导入任何内容它的作用是让 TypeScript 看到声明合并。”又一个类型协作可选的例证。以及官方补的一句生命周期保证“因为ctx.on()属于 effect监听器会随插件一同消失绝不需要手动维护removeListener。”——第 02 篇的幽灵回调事故在事件系统里被默认消灭。运行输出官方原文[stats] tool_call - 1 [stats] tool_call - 2 [stats] prompt - 12.2 事件与扩展点四大扩展点的真身学完三件套回头看第 01 篇悬置的问题tools/systemPrompt/agent/llm 四大扩展点到底是什么答案现在很自然了——它们是 harness 自己声明、发出、并开放监听的事件组。官方文档明确“每个 harness 事件都会在其所属子系统页面自动生成的参考文档中记录其模式。”四大扩展点的本质扩展点背后的事件形态你能做的事tools 工具流水线工具调用前/后的事件组监听做审计、waterfall 拦截/改写参数与结果systemPrompt 提示词组装提示词组装相关事件贡献片段、按会话改写、拦截注入llm 模型流式模型请求/流式输出事件换适配器、改写输出、计量agent 智能体协调agent 决策相关事件如 agent/request替换模型调用配置、注入策略扩展点不是框架留给你的口子而是框架自己也在用的事件——DSH 内置的提示词组装逻辑和你的第三方提示词插件监听同一个事件官方能力与社区扩展在架构上完全平等。这就是一切皆插件的终极含义也是这个系列最想传递的一句话。三、进阶篇五种分发模式的官方 API3.1 官方语义表第 01 篇给过五种模式的教学版表格这一篇给官方教程第四章的调用级版本——每种模式对应一个 ctx 方法调用形式本身就在表达语义模式调用语义emitctx.emit(name, ...args)同步广播不会等待或收集返回的 promise 与值parallelawait ctx.parallel(name, ...args)所有监听器并发运行并一同等待serialawait ctx.serial(name, ...args)按序运行并等待第一个非 null/false/undefined 返回值胜出停止后续bailctx.bail(name, ...args)serial 的同步版本waterfallctx.waterfall(name, ...args, next)环绕中间件下节细讲注意官方对 serial 的措辞“第一个非 null/false/undefined 返回值胜出并停止后续监听器”——serial 的胜出语义让它更像竞标而非流水线谁先给出实质答案谁就终结这个事件。这和第 01 篇模拟器里我写的每步加工喂给下一步略有出入以官方为准serial 是第一个实质返回值胜出纯加工的串行链式需求应该用 waterfall 或自己组织。写插件时对着这张官方表查不要凭直觉。3.2 模式选择的两个工程维度选模式看两件事语义要不要返回值、要不要短路、要不要等和性能await 会阻塞分发方。上表里 await 的两兄弟parallel/serial会阻塞调用点不适合高频热路径不 await 的三兄弟emit/bail/waterfall同步返回监听器里的异步工作要自己管生命周期。一个经验法则热路径事件每次工具调用、每段流式输出都会经过优先考虑不 await 的模式把重活异步化冷路径事件启动、关闭、配置变更用 await 模式换取更强的保证。四、高阶waterfall 拦截实战与 harness 真实事件4.1 官方 waterfall-demo转换与短路先看洋葱结构图再读代码调用 next()放行:调用 next()结果包装后返回拦截:不调 next 直接 returnctx.waterfall 发出监听器1(外层)包装结果:转大写监听器2(内层)决策者:blocked 短路最内层默认逻辑(框架/宿主的行为)最终结果默认逻辑从未执行官方教程第四章给了 waterfall 的完整实战示例两个监听器演示了它的两种用法importtype{Context}fromdeepseek-ai/cordisdeclaremoduledeepseek-ai/cordis{interfaceEvents{demo/transform(input:string,next:()Promisestring):Promisestring}}exportconstnamewaterfall-demoexportfunctionapply(ctx:Context){// Listener 1: wrap the downstream result.ctx.on(demo/transform,async(input,next){constdownstreamawaitnext()returndownstream.toUpperCase()})// Listener 2: short-circuit when it owns the decision.ctx.on(demo/transform,async(input,next){if(input.includes(blocked))return** blocked **returnnext()})void(async(){console.log(awaitctx.waterfall(demo/transform,hello,async()hello))console.log(awaitctx.waterfall(demo/transform,blocked words,async()blocked words))})()}运行输出官方原文HELLO ** BLOCKED **官方原文带我们逐行走了一遍第二行的产生过程值得照抄进笔记“监听器 1 先运行并调用next()从而调用监听器 2监听器 2 看到blocked后直接返回而不调用next()因此最内层默认逻辑传给ctx.waterfall的函数从未运行返回途中监听器 1 再把替换消息转换为大写。”注意最后那句——短路发生在内层但外层的包装依然生效** blocked **被转成大写。waterfall 的执行结构是洋葱外层进入、内层短路、外层收尾三层逻辑各司其职。官方随后给出的纪律是本章最重要的一句话“只负责观察或标注的 waterfall 监听器必须调用next()不调用就直接返回代表有意短路。如果日志监听器忘记调用next()会悄无声息地吞掉所有下游的默认行为。”换句话说next 的调不调是观察者和决策者的法律分界线——观察者挡路是 bug决策者挡路是设计。4.2 一段可运行的审批拦截模拟器把 waterfall 的拦截语义翻译成 Python 教学版真实实现见 deepseek-ai/cordis模拟 DSH 的审批场景waterfall 审批拦截模拟器:洋葱式监听 决策者短路(等价 Cordis waterfall 语义)。from__future__importannotationsfromdataclassesimportdataclass,fieldfromtypingimportCallabledataclassclassApprovalBus:waterfall 事件:监听器收 (request, next),可包装/短路。listeners:list[Callable]field(default_factorylist)defon(self,fn:Callable)-None:注册监听器:先注册在外层(等价 ctx.on)。self.listeners.append(fn)defwaterfall(self,request:dict,default:Callable):洋葱分发:外层→内层→默认逻辑→逐层返回。defdispatch(i:int):ifilen(self.listeners):returndefault()# 最内层:默认执行returnself.listeners[i](request,lambda:dispatch(i1))returndispatch(0)if__name____main__:busApprovalBus()# 外层监听器1:审计日志(观察者,必须调用 next)bus.on(lambdareq,next:(print(f [audit] 请求:{req[tool]}),next())[1])# 内层监听器2:危险命令拦截(决策者,有权短路)defguard(req,next):ifreq[tool]rm -rf:print( [guard] 拦截危险命令,短路!)return{approved:False,reason:blocked by guard}returnnext()bus.on(guard)print(场景1:安全命令 ls — 走完整链路)r1bus.waterfall({tool:ls},lambda:{approved:True})print(f 结果:{r1})print(场景2:危险命令 rm -rf — guard 短路,默认执行从未发生)r2bus.waterfall({tool:rm -rf},lambda:(print( [默认执行] 命令已执行),{approved:True})[1])print(f 结果:{r2})跑一遍你会看到场景 1 里审计日志先打、默认执行发生、批准通过场景 2 里 guard 直接短路——默认执行函数根本没被调用这正是审批拦截的安全性来源被拦截的命令连执行函数都到不了。而审计监听器作为观察者每次都老老实实调用 next不挡路。4.3 harness 的真实 waterfall 事件官方教程点了两个真实的 harness 事件作为 waterfall 的工业级用例“agent/request允许插件替换模型调用配置approval/request允许策略代替用户作答。”这两个事件把本章的全部知识接入了真实业务agent/request是模型请求的决策点——任何插件都可以在模型调用前改写配置换模型、调参数、加防护approval/request是审批请求的决策点——策略插件可以代替用户作答自动批准/拒绝。写业务插件时先去子系统页面查事件的模式官方保证每个事件都有 mode 标注和生成文档看到 waterfall 的事件就知道这里可以做拦截看到 emit 的就知道这里只能做观察。模式标注就是事件的能力说明书。五、实战避坑指南高频问题与一键解决速查表序号遇到的报错或现象背后的真正原因一句话解决办法1事件发出后监听器没反应事件名拼错且没做声明合并拿不到类型检查用 declare module 声明事件名让编译器把关2waterfall 链条被吞掉默认逻辑不执行某个观察型监听器忘了调用 next()观察者必须 return next()只有决策者才能短路3监听器收到的事件参数是旧版结构消费方没 import type 提供方的声明文件import type {} from ‘./provider’ 引入类型合并4热重载后监听器重复触发手动 addEventListener 绕过了 ctx.on监听一律走 ctx.on属于 effect卸载自动移除5serial 事件的行为和预期不一致把 serial 当流水线加工用实际是首个实质值胜出查官方语义表串行加工需求用 waterfall 自组织6高频事件上挂重监听器导致卡顿用了 parallel/serialawait模式热路径事件用不 await 模式重活异步化7拦截插件形同虚设拦截逻辑写进了 emit 监听器广播模式无法拦截拦截必须走 waterfall短路是它独有的语义这张表的底层规律和第 02/03 篇一致Cordis 的一切能力都靠声明 语义起作用坑全在凭直觉用里。事件系统的直觉修正只有一条先查事件声明里的模式再写监听器。结语本文把扩展点拆回了它真正的形态——事件。事件的三件套声明合并、emit 发出、on 监听让协作双方互不知晓五种分发模式emit/parallel/serial/bail/waterfall覆盖了观察、并发、竞标、表决、拦截五种协作拓扑waterfall 的不调 next 即短路把拦截从插件自觉升级为框架语义。而 DSH 的四大扩展点——tools 工具流水线、systemPrompt 提示词组装、llm 模型流式、agent 智能体协调——全部是这套事件系统的实例化。理解了事件你就理解了一切皆插件的最后一块拼图宿主不是开放了几个口子而是把自己的全部行为都做成了事件。一张图总结全文知识要点文字回顾① 服务是点名找人、事件是对空广播——互不知晓是扩展性的秘密② 三件套Events 声明合并给类型、emit 发出不管听众、on 监听属于 effect③ 五种模式各有 ctx 方法调用形式即语义serial 是首个实质值胜出不是流水线④ waterfall 洋葱结构外层包装、内层短路、观察者必须 next⑤ 四大扩展点 harness 自己的事件组官方能力和社区插件架构平等⑥ 写监听器前先查事件的 mode 标注。下一篇连载预告《DeepSeek Harness 的 bundle/profile/patch 到底是什么?cordis.patch.yml 底层实现拆解》——配置系统与打包机制的完整原理关注 CSDN 主页 不错过更新。你写过观察者忘了 next的 bug 吗waterfall 拦截在你的项目里用来做什么?评论区聊聊,我挨个回。本文事实基于 DeepSeek Harness 官方 Cordis 教程《事件》一章(2026-07 版本),有出入评论区提醒我。参考资料正文数据来源DeepSeek Harness 官方 Cordis 教程《事件》、官方文档《Cordis 入门》分发模式与 Waterfall 语义两节、子系统事件参考页。