DeepSeek Harness 事件系统五种分发模式让插件各司其职从“打电话”到“喊广播”一文看懂 Agent 框架的通信机制一、为什么需要事件DeepSeek Harness 的核心理念是“一切皆插件”。模型适配器、工具注册表、会话日志、Agent 主循环……全部是可替换的插件。问题来了插件之间怎么通信如果插件 A 要调用插件 B就得直接引用它——这就产生了耦合。更麻烦的是如果 A 调 B、B 又依赖 A就会形成循环依赖。Harness 的答案是事件。简单打个比方服务Service 打电话。你知道找谁拨号等对方接拿到答案再干活。事件Event 在办公室里喊一嗓子。你不知道谁在听甚至可能没人听喊完你就继续干自己的事。事件把“谁关心这件事”从“说话的人”那里剥离出去。想加一个新的监听方写个插件在配置里让它监听那个事件就行了——被监听的包一个字都不用改。二、五种分发模式速览Harness 的事件系统提供了五种分发模式对应不同的交互契约模式是否 await是否有返回值能否中断典型场景emit❌❌❌广播事实日志、遥测parallel✅❌❌必须等所有人都完成刷盘serial✅✅✅问一圈谁先答用谁bail❌✅✅同步版的 serialwaterfall✅✅✅请求改写/否决中间件下面逐个拆解。1. emit喊一嗓子就走这是最简单的模式。监听器同步执行返回值被忽略异常会阻断后续监听器。// 监听器记日志ctx.on(tool/executed,(name:string,duration:number){console.log([LOG] 工具 ${name} 执行耗时${duration}ms)})// 监听器上报遥测ctx.on(tool/executed,(name:string,duration:number){console.log([METRIC] tool.duration:${duration}ms)})// 发出事件不等任何人ctx.emit(tool/executed,code-search,123)关键点适合“通知类”场景——日志、监控、审计。喊完就走谁听谁不听发话的人不关心。2. parallel一起跑一起等所有监听器并行执行必须全部完成才算成功。有一个失败就抛出AggregateError。// 监听器1落盘ctx.on(session/flush,async(sessionId:string){awaitwriteToDisk(sessionId)})// 监听器2上报遥测ctx.on(session/flush,async(sessionId:string){awaitsendTelemetry(sessionId)})// 必须等两个都完成awaitctx.parallel(session/flush,session-abc-123)Harness 中session/flush是极少数使用 parallel 的事件。刷盘这种事必须确认所有数据都落稳了才能继续。3. serial排队问谁先答用谁监听器按注册顺序执行第一个返回有效值的监听器终止链条。什么是“有效值”null、false、undefined算“放行”继续往下问。其他任何值包括空字符串、数字0、空对象{}都算“有效答案”会立即返回并终止。ctx.on(agent/should-stop,async({cost}){if(cost10)returnCost exceeded// 有效值终止returnnull// 放行下一位})ctx.on(agent/should-stop,async({turn}){if(turn5)returnToo many turnsreturnundefined// 放行})// 问一圈谁先给出停止理由就用谁的constreasonawaitctx.serial(agent/should-stop,{turn:3,cost:12})在 Harness 中agent/turn-stopping就是用 serial 模式的——各插件依次判断是否应该停止当前轮次谁先给出“该停了”的理由谁就说了算。4. bailserial 的同步版和 serial 逻辑完全一样区别在于不 await——所有监听器必须是同步函数。ctx.on(slash/input-begin-command,(input:string){if(input.startsWith(/help)){return{command:help,args:input.slice(5)}}returnnull})// 同步调用不等constresultctx.bail(slash/input-begin-command,/help 如何安装)适合那些不能把调用链染成async的场景。5. waterfall洋葱模型最难也最强大这是最复杂、最核心的模式。监听器形成一个洋葱链每个监听器都可以调用next()把控制权交给下游不调用next()直接返回 →短路下游全部跳过修改传给下游的参数包装下游返回的结果// 监听器1缓存检查最外层ctx.on(agent/request,async(payload,next){if(cache.has(payload.prompt)){returncache.get(payload.prompt)// 不调 next() → 短路}returnnext()// 继续往里走})// 监听器2请求日志中间层ctx.on(agent/request,async(payload,next){console.log([请求] prompt长度${payload.prompt.length})constresultawaitnext()console.log([响应] 耗时${Date.now()-start}ms)returnresult})// 监听器3参数改写最内层ctx.on(agent/request,async(payload,next){constmodified{...payload,temperature:Math.min(payload.temperature,1.0)}returnnext(modified)// 修改后继续往下传})// 调用方提供“默认实现”最内层constresultawaitctx.waterfall(agent/request,{prompt:介绍一下AI,temperature:1.5},async(payload){// 真正调用 LLM APIreturnawaitcallLLM(payload)})关键纪律在 waterfall 中如果你只是“路过看一眼”比如记个日志必须调用await next()并把结果返回。忘了调next()链条就从这里静默断开所有下游都不会执行——不会报错只会发现“工具不跑了”。Harness 中agent/pre-step、agent/request、tools/execute、tools/pre-execute等核心扩展点都采用 waterfall 模式。三、一句话收尾事件是 Harness 让插件“各司其职、互不打扰”的秘诀。五种分发模式覆盖了从“广播通知”到“洋葱中间件”的全部通信需求——选对模式插件就像拼乐高一样插上去就能用拔下来系统不受影响。