Files
deepseek-harness/docs/defensive-patterns.zh.md
T

3.4 KiB

防御性模式

English | 中文

来之不易的缺陷类别规则:下面每条模式都是本项目实际发布或差点发布的一类缺陷,以防止其复发的规则形式陈述。在编写生命周期、并发、子进程或清理代码之前请先阅读本文。测试层面的对应规则(真实入口路径、world 验证、资源归属)见 testing.md

正交结果独立上报

一个结果可以同时具有多重性质:进程可能既超时又以 exit 0 退出,因为它捕获了信号。每个独立事实(timedOutsignalexitCode)都应独立暴露;切勿将某个 flag 的上报嵌套在另一个 flag 的分支内,否则调用方会把一次被截断的运行误读为正常成功。

跨 seam 契约两侧都要遵守

当一个实现边界接收到同一结果的多种表示时,应在跨越公共 seam 前将其规范化。LlmAdapter.stream() 的实现可以抛出异常或发出 finish {kind:'error'|'aborted'},但 LlmService.stream() 只会通过终止 finish chunk 暴露模型请求失败;middleware 与消费方缺陷仍会抛出。这使消费方不必猜测捕获的异常究竟来自提供方、包装层、chunk 日志记录还是自身组装逻辑。请在类型定义处记录规范化契约;通过真实消费方覆盖每种来源形式。

异步状态不是同步状态

agent.followup() 没有逐消息的完成状态或结果;后台任务的完成与轮次边界存在竞争;reader.close() 在 EOF 和 dispose(资源释放)两种情况下都会触发。切勿把 agent/statuswhenIdle() 当作某次 followup() 的结果:多条已排队的后续消息、steering(中途引导)和注入工作可能共用同一个 running 区间,而取消或资源释放可能丢弃尚未启动的项。真正拥有一次运行的自动化调用方必须显式定义其区间——例如从消息的持久 inbox 回执到整个 agent 下一次进入 idle——并将选取的任何输出描述为整个区间的输出,而不是把因果关系归于该消息。这条守则是双向的:如果等待的转换永远不会发生,等待就会挂起,因此应显式处理「无需等待」的分支。

Dispose 必须达到完全停稳,而不仅仅是请求停止

一个清理流程如果发出 kill/abort 后就返回、而不等待工作实际停止,就会留下孤儿进程。请让清理逻辑异步化并 await 子进程退出(kill → await done),并在 kill 之前关闭监听器/通知注册表,使迟到的完成事件保持静默。测试应证明 dispose 确实等待了(await fiber.dispose() 之后 pid 已不存在),而不仅仅是进程最终会死。

在边界处包容回调异常

用户提供的监听器如果抛出异常,不得导致它所在的 promise 被 reject,也不得饿死排在它后面的监听器。请用 try/catch 包裹分发循环并记录日志;一个行为不当的订阅者绝不能破坏核心生命周期。

绝不将环境变量或可预测路径暴露给不可信输出

spawn 的命令应获得一份经过清洗的 env(去除 *KEY*/*SECRET*/*TOKEN*/*PASSWORD*),使 harness 凭证无法泄漏到输出、env 或溢出文件中。临时/溢出文件应使用私有(0700)目录、随机文件名和排他的仅所有者可访问打开方式('wx'0o600)——可预测的全局可读路径会招致符号链接竞争和信息泄露。