Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

FND-006:实践中的零妥协:代码健康度、错误处理规范与生产就绪标准

从 v0.7.0 开始 · 类型:质量 · 修订版 1

规范参考 · 经团队批准 · 修订版 1 原始 RFC 讨论:\#5653


在阅读此内容之前,给团队的一条提示。

这是 ZeroClaw 成熟度框架中的第六篇文档。在它之前的五篇分别探讨了架构、文档、治理、工程基础设施和协作——也就是围绕工作的结构性和人文性支撑体系。每一篇都回答了一个关于我们如何共同构建这个项目的不同问题。如果你已经全部读过,可能会注意到有一个问题它们都没有回答:是的,可我们究竟该如何把它写好?架构 RFC 告诉你要构建成什么形态。文档 RFC 告诉你如何记录它。治理 RFC 告诉你如何协调。CI/CD RFC 告诉你如何把关。文化 RFC 告诉你如何与身边的人协作。但它们都没有告诉你,在句子层面、在一个函数内部、在你做出抉择的那一刻,质量究竟是什么样子。

这就是本文档的目的。

这里的具体主题——错误处理、API 文档、测试设计、技术债务——表面上是 Rust 的话题。但它们所培养的能力却不止于此。技术在不断变化。每一次迭代的变化速度都比上一次更快。你今天所使用的工具——这门语言、这个框架、这个 AI 助手——终将被取代。其中一些甚至会在这个项目的生命周期内就被淘汰。而这份文档试图帮你培养的判断力却不会被取代。它将在你做出的每一个决策背后悄然累积复利,贯穿于你将来编写的每一种语言、构建的每一个系统,乃至于可能与软件毫不相干的工作之中。这就是我们对你的投资。不是投资于你编写 Rust 的能力,而是投资于你对质量、失败与匠艺的思考能力,以及将这份思考带入你今后拿起的每一件工具的能力——包括你今天正在使用的 AI 工具,以及那些尚未问世的工具。

慢慢来。


成熟度框架套件

本 RFC 是构成 ZeroClaw 成熟度框架的一组文档中的第六篇。这些文档设计为整体阅读,但每篇也可独立阅读。

RFC范围问题
刻意架构:微内核转型我们正在构建的内容及其结构#5574
文档标准与知识架构我们如何记录我们所构建的内容#5576
团队组织与项目治理我们如何协调和做出决策#5577
工程基础设施:CI/CD 流水线我们如何可靠地构建、测试和发布#5579
贡献文化:人际协作、AI 伙伴关系与团队成长我们如何协作与成长#5615
实践中的零妥协:代码健康度、错误处理规范与生产就绪标准如何编写持久化的代码此 RFC

前五个 RFC 回答的是结构性和人为层面的问题。而这一个回答的是隐藏在所有问题之中的核心问题:在既定的结构、既定的团队、既定的工具之下,把代码写好究竟意味着什么?


目录

  1. 一种开发哲学:对判断力的投资
  2. 诚实评估:代码库在告诉我们什么
    • 2.1 证据
    • 2.2 数字未显示的内容
    • 2.3 已经做得好的部分
  3. 门控与标准:核心区别
  4. 七大纪律
    • 4.1 错误处理作为设计考量
    • 4.2 公共 API 表面作为 Promise
    • 4.3 测试作为设计反馈
    • 4.4 技术债务分类
    • 4.5 应用层安全
    • 4.6 可观测性即可调试性
    • 4.7 在地板上方工作
  5. 这对 AI 辅助开发意味着什么
  6. 工艺的便携性
  7. 这对贡献者意味着什么

修订历史

修订日期摘要
12026年4月12日初始草稿

1. 开发理念:对判断力的投资

架构 RFC 引入了一个决策层级结构,用于描述该项目中的每一项决策应如何流转:

Vision
  └── Architecture
        └── Design
              └── Implementation
                    └── Testing
                          └── Documentation
                                └── Release

该层级结构回答了在每个层级上构建什么的问题。本 RFC 位于实现和测试层级内,并提出一个不同的问题:构建得有多好?

“质量如何”这个问题的答案不是一张清单。清单可以被满足,却未必被理解;而在软件领域,理解才是产生持久成果的关键。一个只记住规则的贡献者,会一直遵循这些规则,直到情况稍有不同。而一个内化了规则背后判断力的贡献者,则能在规则未曾预料到的情境中正确地加以运用,包括那些最重要的情境——也就是那些没人事先规划过的情境。

这种区别在本项目的语境中尤为重要。ZeroClaw 运行在一个由强大工具构成的环境中:AI 代码生成、能捕获各类常见错误的 CI 关卡、IDE 代码检查器、自动化安全扫描器。这些工具确实很有价值。它们设定了一个底线,一个代码不应在其之下被合并的最低标准。但它们做不到的是思考。它们无法判断一个错误是运行时错误还是程序员错误。它们无法评估一个测试是否在断言正确的行为。它们无法判断一个公共 API 的文档是否足够清晰,以便未来的贡献者能够据此正确实现。它们只能检查它们被编程去检查的内容。

“工具能够验证的内容”与“能够长期服务于用户、贡献者及项目的质量”之间的差距,需要由判断力来弥补。本文档旨在帮助你培养这种判断力——并非取代工具,而是引导你如何运用它们。


2. 诚实评估:代码库在告诉我们什么

本节并非批评,而是诊断。此处适用的框架与架构 RFC 中所述相同:无法命名就无法改进,而具体细节之所以有价值,正是因为它们具体。

2.1 证据

来自 RFC §5574 的工作区分解工作取得了成功。这些 crate 已经存在,trait 边界也是真实有效的,编译器还能强制约束依赖方向。这确实是出色的工作。然而,在这些新的 crate 内部,曾经那个单体架构所具有的种种模式被原封不动地延续了下来,因为代码库在团队对“实现层面的质量”形成统一认知之前就已经迁移了。

这些是实测数据,而非估算值:

指标它表示
zeroclaw-config/src/schema.rs16,800 行现在是代码库中最大的文件;架构 RFC 中曾指出原始的 loop_.rs 有 9,500 行;本文件已超过它
zeroclaw-channels/src/orchestrator/mod.rs11,813 行第二大文件;一个承担集中责任的单一模块
zeroclaw-runtime/src/onboard/wizard.rs7,988 行单个文件中的单个工作流
zeroclaw-runtime/src/agent/loop_.rs6,101 行从单体应用中的约 9,500 个减少而来:真实、可衡量的进展;但仍然庞大
zeroclaw-channels/src/orchestrator/telegram.rs5,122 行一个通道实现;一个文件
crates 中的 .unwrap() / .expect() 调用5,630每一个都是关于错误处理的延迟判断,参见 §4.1
遗留 src/ 目录中的 .unwrap() / .expect() 调用240该迁移将模式大规模地延续下去。
zeroclaw-api 中的公共函数371整个基础 API 层;其他所有 crate 都依赖于它
zeroclaw-api 中的文档注释行~27公共 API 中未编写文档的比例约为 14:1,参见 §4.2
#[allow(unused_imports)] / #[allow(dead_code)] 在遗留 src/ 模块中约 30 多个实例编译器已识别出不再使用的代码;已要求它不要提示
整个代码库中的 TODO / FIXME / todo!() / unimplemented!()20明显偏低,表明大多数技术债务是隐性的,而非已标记的

最后一行值得单独说明。在一个如此规模的代码库中,存在二十个明确标记的不完整工作项,这并不意味着工作即将完成。它表明大多数未完成的工作并未被正确标记。未标记的技术债务更难发现、更难优先级排序、更难分配。沉默并不等同于完成。

2.2 数字未显示的内容

这些数字衡量的是可量化的内容。而更具影响力的质量问题是无法计数的:

  • 这 5,630 个 .unwrap() 调用是位于关键路径中,还是位于测试工具中
  • 现有的测试是在测试行为还是测试实现细节
  • 仅通过签名和类型,能否正确实现 zeroclaw-api 中的公共函数
  • 在生产故障期间发出的日志消息是否包含足够的上下文以诊断该故障
  • 安全模块中的贡献者是否了解哪些数据跨越了信任边界,哪些没有。

这些是判断性问题。它们没有 CI 门禁。它们具有本文件提议命名的标准,以及我们共同构建的审查和导师文化。

2.3 已经做得好的部分

诊断不应掩盖真正构建良好的部分。

zeroclaw-api 中的 trait 层架构设计是正确的。ProviderChannelToolMemoryObserverRuntimeAdapterPeripheral 都是清晰、合理的抽象。它们是恰当的接缝。问题不在于设计本身,而在于该设计尚未在文档、测试覆盖率和错误处理规范中得到充分体现。本 RFC 旨在弥合这一差距。

这套安全模型经过深思熟虑。配对码、自治级别、沙箱层级以及策略强制执行都体现出真正的设计意图。每一位在信任边界附近编写代码的贡献者都需要理解这一意图,而本 RFC 的部分目的,正是为贡献者提供识别这些边界所在位置的术语体系。

可观测性基础设施已经成熟。OpenTelemetry、Prometheus 和 DORA 指标均已基于一个清晰的 Observer trait 实现。基础设施已就绪,当前的教学差距在于贡献者如何使用它,以便在出现问题时真正发挥作用。

测试套件并非不存在。现有的测试投入是真实的。本 RFC 所描述的工作关乎这些投入的质量与分布:哪些内容被测试、如何测试,以及这些测试是否真正证明了它们看似证明的东西。

ADR-004 是一份优秀的架构记录。它证明了当预期明确时,团队能够产出高质量的设计文档。本 RFC 正在为代码本身提出同等的预期。


3. 门控与标准:核心区别

这是整个文档的核心思想。清晰地理解它比掌握第 4 节中的任何具体技术都更为重要。

门禁是二元的:通过或不通过。它是自动化的,由工具强制执行,并定义了代码合并的最低标准。CI/CD RFC 构建了这些门禁。它们是真实且可工作的。

它检查什么
cargo fmt --check代码在工作区中保持一致的格式
cargo clippy --workspace --all-targets -D warnings无 Clippy 已知的反模式;工作区范围
cargo deny check无未确认的安全公告;许可证和源代码合规
cargo nextest run --workspace存在的测试通过

标准是理想化的。它描述了质量在最低要求之上的表现。它通过判断、同行评审以及团队共同养成的习惯来执行。

标准它描述了什么
错误处理规范故障被分类;操作错误会在正确的层级上以上下文形式呈现
API 文档每个公开项都有足够的文档,以便在不阅读实现的情况下正确使用
测试质量测试断言行为,而非实现;测试难度被视为设计反馈
债务分类未清偿债务会被标记、定位并进行风险加权;高风险债务有明确的所有者
安全态势信任边界在实现层面是明确的,而不仅仅是在策略层面。
可观测性规范日志消息回答诊断问题;跨度绑定有意义的单元工作

门禁(Gates)与标准并非对立关系,而是互补的层次。仅有门禁而无标准,会导致代码通过所有检查,但仍无法满足用户需求;仅有标准而无门禁,则标准无法强制执行。两者缺一不可。目前项目具备良好的门禁机制,但标准体系尚不完善。

一个代码库可能通过所有检查,但对后续贡献者来说仍然难以理解;在应该暴露错误时保持沉默;无法独立测试;并且在用户输入与业务逻辑交汇的边界处存在安全隐患。绿色对勾回答的问题是“这段代码是否通过了我们制定的规则?”但它并不能回答“这段代码是否优质?”这两个问题并不相同。

这并不是对这些关卡的批评。关卡的价值恰恰在于它们定义了一个共享的、可强制执行的基准,每位贡献者都在此基准之内工作。本文档的目标是建立共享的术语和判断力,从而定义在该基准之上什么才是好的标准,并清楚地解释为什么这种判断力无法委托给工具。


4. 七大纪律

4.1 错误处理作为设计考量

每一次 .unwrap() 调用都是一个决策。但代码库中 5,630 处调用里的大多数都不是经过深思熟虑做出的。它们是默认产生的,因为当你需要从 ResultOption 中取出一个值并想继续往下写时,.unwrap() 是阻力最小的路径。默认做出的决策的问题在于,它们根本不是决策,而是拖延。而它们所拖延的,是一个真正的问题:当这里失败时,应该发生什么?

答案取决于你正在处理哪种类型的故障。有三种类型的故障,每种都有三种不同的正确应对方法。

程序员错误是对那些在正确代码中本应不可能发生的不变量的违反。比如一个要求非空 Vec 的函数却被传入了空的 Vec,又或者一个枚举匹配进入了类型系统本应使其不可达的分支。这些代表的是 bug,而非运行时故障,而是错误的逻辑。panic! 是正确的响应方式,因为目标是在开发阶段就发现这些问题,而不是在运行时让用户面对它们。assert!debug_assert! 是合适的工具。带有说明为何此状态不可能发生的消息的 .expect() 在这里同样适用。它使推理过程变得明确且可搜索,这样下一个阅读代码的人就能理解为何这个 panic 是有意为之的。

操作错误是预期内的故障模式。网络超时。不存在的文件。已过期的 API 密钥。携带错误状态的提供方响应。提供了格式错误输入的用户。这些都不是 bug。它们是与外界交互的系统的正常运行状况。正确的应对方式是 Result<T, E>? 运算符会将故障传播给更有能力决定如何处理它的调用方。对操作错误使用 .unwrap() 相当于一个被推迟的 panic:它终将在真实条件下、在真实用户面前触发,既没有有用的上下文,也没有恢复的机会。

配置错误是指在启动时发现的格式错误或缺失的配置。正确的应对方式是快速失败,但要明确具体。不是带堆栈跟踪的 panic,也不是含糊的“配置无效“消息,而是一条能指向具体字段、说明预期内容并告知操作者应提供什么的消息。因配置错误而无法启动 ZeroClaw 的用户,应当在退出进程时清楚地知道究竟需要修复什么。

失败类型这是什么意思正确的响应
程序员错误违反了不变量;在正确的代码中不应出现此情况panic!assert!.expect("reason this is safe")
操作错误预期失败模式;世界不配合Result<T, E>?、带有上下文的结构化错误类型
配置错误无效或缺失的启动配置快速失败,并提供具体、可操作的提示

在每次使用 .unwrap().expect() 之前,先问问自己:这是哪种类型的失败?如果答案是“程序员错误:在正确的代码中不会出现此状态”,那么使用 .expect() 并附上解释原因的注释就是正确的选择,它能向每一位未来的读者传达你的推理过程。如果答案是其他任何情况,请使用 ? 或显式处理该失败。

? 运算符值得理解的地方在于它所_表达_的含义,而不仅仅是它所做的事情。它表达的是:我承认此操作可能会失败。我明确地将这个失败向上传播给我的调用者,因为调用者更适合决定如何处理它。这种承认在架构上是有意义的:它使错误处理契约在调用点可见,并将决策推向最具上下文信息的那一层。

目标不是消除所有的 .unwrap() 调用。有些 .unwrap() 是正确的。目标是让每一个 .unwrap() 都代表一个经过深思熟虑的决策,并且其推理过程对任何阅读代码的人都是可见的。.unwrap().expect("此向量由调用者保证非空——参见 SOP 引擎不变量中的 §4.2") 之间的区别不仅仅是风格问题。它是“延迟判断”与“文档化判断”之间的区别。

4.2 公共 API 表面作为 Promise

pub 是一个合约。

当你将函数、结构体、特征或模块标记为公开(public)时,你是在向每个调用者做出承诺。这包括下个月实现该接口且已不记得你最初意图的贡献者。这包括读取你的 crate 以生成实现的 AI 助手。这包括需要理解此代码原本意图的生产环境故障排查人员。这也包括你在两个月后处理其他任务时重新回到这段代码。

没有文档的公共项就如同一个没有条款的承诺。调用者无从得知你在编写它时做了哪些假设、它会在何种情况下返回哪些错误条件、它有哪些副作用、并发调用是否安全,或者两个名称相似的函数之间存在哪些细微差别。他们只能根据名称、类型签名和实现代码主体去推断,而这些本来你只需用三句话就能告诉他们的东西。

zeroclaw-api 的情况足够具体,值得直接点名。这是整个架构所依赖的唯一一个 crate。工作区中的每一个 provider、channel、tool、memory 后端、observer、运行时适配器以及外围实现,都是基于这些 trait 和类型构建的。这个基础层中未经文档化的接口,会将困惑传播到每一个实现它的 crate、每一个对其进行测试的用例,以及每一段与之协作的 AI 生成代码中。14:1 的未文档化公共 API 面积比例并非文档风格上的偏好,而是契约中的一处缺口——而架构 RFC 曾指出,这正是系统中最重要的一层。

这里的 AI 维度既实用又直接:当你要求 AI 助手实现某个 trait 或调用某个没有文档的函数时,AI 会根据名称和类型签名推断意图。有时这种推断是正确的。但更多时候,它生成的代码能够编译通过、能通过类型检查器,却在某些特定条件下表现错误——这些条件是 AI 无从预料的,因为根本没人把它们写下来。文档不仅仅是给人看的。它是你提供给每一个将与你的代码协作的工具、以及每一个将依赖你代码的人的规范说明。

至少,zeroclaw-api 中的每个公开项都应包含:

  • **一句话描述它的功能。**不是它是什么,而是它做什么。
  • 一个 # Errors 部分(如果返回 Result):在什么条件下会失败,调用者需要处理哪些错误变体?
  • # Panics 章节(如果可能触发 panic):在什么条件下,以及为什么?
  • 前置条件(如果有任何非显而易见的条件):在调用此函数之前,必须满足哪些条件?

对公共 trait 方法编写三句话的文档注释,对下一位实现者的价值要远大于一百行毫无说明的实现代码。实现代码告诉他们代码做了什么,而文档告诉他们代码本应做什么——当二者出现分歧时,后者才是真正重要的。

4.3 测试作为设计反馈

测试的目标不是产生一个绿色的对勾,而是创建一份精确、可执行的记录,说明某段代码_应当做什么_:一旦该行为发生改变,这份记录就会高调地报错。

这种区分很重要,因为存在两种根本不同的测试类型,而只有一种能够实现该目标。

直接读取结构体内部状态、直接设置值、调用方法并对返回值进行断言的测试,测试的是_实现_。如果实现发生变化,或者相同的行为通过不同的机制来实现,即使用户关心的任何东西都没有改变,测试也会失败。这会在重构时制造阻力,却没有带来安全性。这种测试还往往会在行为以测试未预料到的方式出错时仍然通过。

通过公开接口构造值、通过公开方法执行行为,并对可观察结果进行断言的测试,是在测试行为。如果实现方式发生变化但行为保持不变,测试就会通过。如果行为发生了对用户重要的变化,测试就会失败。这正是能够自信地进行重构的原因:测试检查的是你是否得到了正确的结果,而不是你是否以某种特定方式得到了结果。

更为重要的原则是诊断性原则:

一个难以编写的测试通常是在告诉你关于设计的一些问题。

如果为一个函数编写单元测试需要建立数据库连接、模拟六个依赖项、构建一个完整的配置对象,并显式启动异步运行时,那么这个函数很可能承担了太多职责、依赖了太多东西,或者位于架构中错误的层级。这种困难不是需要绕过的麻烦,而是一种反馈。测试正在诚实地揭示代码尚未诚实面对的问题。

这一点与架构 RFC 所确立的 crate 结构直接相关。crate 拆分的目的之一就是创建能够独立测试的组件。zeroclaw-tool-call-parser 应当能够以一个 &str 输入进行测试,而无需运行时。zeroclaw-config 应当能够通过直接构造配置结构体来测试。zeroclaw-api 中的 trait 实现应当针对该 trait 的伪造(fake)实现来测试,而非针对完整的生产环境堆栈。当你发现自己无法在脱离整个环境的情况下测试某个组件时,应当反思是否有一项架构本不打算引入的依赖已悄然进入了实现。测试正在给你答案;问题在于你是否在倾听它。

一种在实践中逐步提升测试质量的方法:

  • 当你修复一个 bug 时,编写一个能够捕获该问题的测试。这一习惯若能持续践行,将使测试套件逐步覆盖那些真正重要的失败场景。
  • 当你添加行为时,编写一个测试来证明该行为存在并且可以独立验证。
  • 当测试难以编写时,在考虑使用 mock 之前,先花时间思考 为什么。这个问题的答案通常比你即将编写的测试更有价值。

4.4 技术债务分类

“债务”这个词很有用,因为它准确地传达了一层含义:它会产生利息。在代码库中流量较高的区域,未经审视的债务会不断累积:新代码会适应它的存在,新的假设建立在旧的假设之上,而每增加一层,解决它的成本就越高。

团队在技术债务方面最常见的错误是将其视为二元问题:要么一切都是债务,且无法采取任何措施;要么没有任何债务,也不应花费任何时间。这两种观点都是错误的。更有用的问题是:目前,哪些债务在哪些位置带来了最大的风险?

两个轴确定优先级。

靠近信任边界。 处理用户输入、执行安全策略、运行工具、管理身份验证或处理来自外部源的数据的代码,都位于靠近信任边界的位置。此处的故障可能被利用、静默地破坏状态,或导致具有安全后果的错误行为。靠近信任边界的债务相对于其规模而言,风险不成比例地高。

影响范围。 zeroclaw-api 是其他一切的基础依赖,其中的技术债务比单个通道实现中的债务影响范围更大。基础类型中的错误假设会传播到任何使用该类型的地方。叶子 crate 中的债务则只影响该 crate 的使用者。

高爆炸半径低爆炸半径
靠近信任边界当前周期中的地址在下一个计划周期中处理
远离信任边界在计划的重构中处理在相邻工作流经过时,适时处理

该框架意味着,在安全策略执行路径中的 .unwrap() 与在 CLI 显示格式化器中的 .unwrap() 并非相同的问题。两者都出现在 5,630 的计数中。该计数告诉我们范围,而分类则告诉我们优先级。

当你在某个文件中工作并发现了技术债务——一个代表未处理操作错误的 .unwrap()、一个膨胀到要处理四项独立职责的函数、一个 #[allow(dead_code)] 在默默压制着无人调用的代码——你不需要修复所有问题。你需要问自己:这是否处于高风险位置?如果是,就在本次 PR 中处理它,或者提交一个后续 issue,注明具体位置、风险以及建议的负责人。如果不是,你可以用一条 // TODO(debt): <description> 注释标记它,使其变得可见而不显得紧迫。你不应该做的是让它完全无标记地遗留下来,因为正是这种沉默使得 5,630 个被推迟的决策在无人察觉趋势的情况下不断累积。

绞杀者无花果(Strangler Fig)模式同样适用于这个层面。架构 RFC 在 crate 级别应用了它:围绕旧结构构建新结构,随时间逐步向内迁移。同样的模式也适用于大型文件内部。你不会在单个 PR 中重写 schema.rs,而是先识别出那些最接近信任边界、变更最频繁或最难测试的函数,优先将它们提取出来,逐步改进结构,让其余部分以团队能够持续承受的节奏跟进。

4.5 应用层安全

CI/CD RFC 为 供应链 的安全态势奠定了基础:cargo deny 能够发现依赖项中的已知漏洞、强制执行许可证合规性,并确保依赖项来自已批准的来源。这相当于项目的“免疫系统”,用于保护进入项目的内容。本节关注的是运行代码的安全态势。

cargo deny 无法发现你的应用程序逻辑所引入的漏洞。它无法判断用户输入在到达业务逻辑之前是否经过了验证。它无法判断工具执行是否遵守了其应强制执行的自主级别。它也无法判断错误路径是否静默地吞掉了安全检查的失败。这些都需要由理解信任边界以及两侧代码应如何正确编写的贡献者来完成。

在靠近信任边界编写的任何代码都应遵循以下三个原则:

信任边界是显式声明的,而非默认假定的。 信任边界是指数据从你直接控制范围之外到达的任何节点:来自任意渠道的用户输入、来自提供方的 API 响应、来自文件系统的文件内容、插件输出、工具结果、硬件读数。在每个信任边界处,都要先验证再处理。不要假定那些并非由你自己产生的数据的结构、大小、类型或内容。ZeroClaw 安全模型在策略层面定义了这些边界。实现层面也应当在代码层级体现这些边界——这并非因为策略会失效,而是因为纵深防御意味着系统的每一层都在各尽其责,而不是寄希望于其他每一层都已尽到了它们的职责。

最小化占用范围。 一个需要读取文件的函数不应当能够写入文件。一个处理某通道消息的 trait 实现不应当能够访问另一通道的状态。一个在自治级别 1 上运行的工具不应当处于能够行使需要级别 3 才能行使的能力的位置。安全模型已经定义了这些约束。这种规范性在于:编写的实现所获取的能力不会超过当前任务所需,并能在实现试图触及其预期范围之外的内容时及时察觉。

在安全边界附近要大声报错。 安全检查中的错误、策略评估失败、签名验证失败、未授权的工具调用尝试、配对码不匹配,绝不应被悄无声息地吞掉。它应该被记录、传播并显式处理。显示辅助函数中的错误可以通过一条日志消息优雅地恢复,但授权路径中的错误则不行。要清楚你正在编写的是哪一类函数,并让这一判断来驱动你以多大力度暴露其中的失败。

这些并非高深的安全原则,而是适用于任何接触用户可影响内容的代码的基础卫生要求。该架构 RFC 将安全模型描述为“深思熟虑的”。本 RFC 所要求的工作,是让这种深思熟虑在实现层面变得清晰可辨:体现在校验输入的函数中,体现在处理策略失败的错误路径中,体现在系统被要求执行的操作与实际执行的操作之间的边界上。

4.6 可观测性即可调试性

可观测性基础设施已经相当成熟:OpenTelemetry 追踪、Prometheus 指标、DORA 跟踪以及简洁的 Observer trait 都已就位。这是生产级别的工作。教学上的差距在于:拥有这些基础设施,与以一种在出问题时(理想情况下是在你了解问题之前)真正能提供帮助的方式去使用它们,二者之间是有距离的。

考虑两条日志消息。它们都能编译通过,都能通过 CI 检查,且语法均正确。

#![allow(unused)]
fn main() {
error!(请求失败);
}
#![allow(unused)]
fn main() {
error!(
    provider = %provider_name,
    model    = %model_id,
    user     = %sender_id,
    tool     = %tool_name,
    attempt  = attempt,
    elapsed  = ?elapsed,
    err      = %e,
    “提供程序请求失败 — 重试次数已耗尽”
);
}

第一种是记录。它确认出了某些问题。第二种是_诊断_。它回答了真正重要的问题:我们当时想做什么、处于什么上下文、使用了什么参数,以及到底哪里出了错。两者的区别不在于技术上的复杂程度,而在于编写这条消息的人是否考虑到了将来某天需要阅读它的人。

在编写任何 warn 级别或更高级别的日志消息之前,需要问的问题是:

在最糟糕的时刻需要诊断此故障的人需要知道什么?

六个月后的你,可能已经忘记了这段代码的编写过程,而那个人或许就是你。也可能是从未见过这个模块的其他贡献者,或者是从终端复制了日志片段来提交 bug 报告的用户。请为他们而写。几乎总是重要的字段包括:我们当时试图做什么、当时的上下文范围是什么,以及具体出了什么问题。

追踪跨度(span)的设计遵循同样的原则。一个跨度应代表一个有意义的工作单元,携带理解该工作所需的上下文,并且其名称在火焰图或追踪查看器中阅读时具有意义。

#![allow(unused)]
fn main() {
// 一条记录
let _span = span!(Level::INFO, 进程);

// 诊断信息
let _span = span!(
    Level::INFO,
    "agent.tool_call",
    tool = %tool_name,
    turn = turn_number,
    sender = %sender_id,
);
}

结构化日志记录和有意义的 span 设计并非风格偏好。它们是让你现有的可观测性基础设施真正发挥作用的关键,不仅在开发期间如此,在用户手中同样如此——这些用户在你永远无法看到的硬件上、在你未曾预料的配置中运行 ZeroClaw,并遭遇你未曾计划应对的错误。基础设施提供了能力,而贡献者使用它的规范程度决定了这种能力能否转化为可诊断的系统。

4.7 在地板上方工作

前面六项准则各自针对一个特定领域。本节将它们综合成一幅完整的图景,展示“达标“在实践中究竟是什么样子:当审阅者、未来的贡献者或用户接触到符合本 RFC 所述标准的代码时,他们实际会有怎样的体验。

维度在底部,门会传递楼层以上,符合标准
错误处理代码编译成功;无 Clippy 警告错误被分类;操作错误会附带上下文信息;恐慌(panic)是有意为之且已记录在案
文档如果存在文档测试,则通过每个公开项都可以在不阅读实现的情况下被正确理解和正确使用
测试存在的测试通过测试断言行为;测试难度被视为设计反馈;关键故障模式已覆盖
债务没有编译器错误或警告(其余的通过 #[allow] 抑制)债务被标记、定位并进行风险加权;高风险债务具有所有者和时间线。
安全cargo deny 检查通过信任边界是明确的;安全故障会明显暴露;实现尊重其预期范围
可观测性代码运行并输出了某些内容日志消息用于回答诊断问题;跨度将具有有用上下文的工作单元绑定在一起。
代码组织文件编译成功;模块结构存在函数只做一件事;文件用于组织相关关注点;大文件是提取的候选对象,而非常态

这些目标都无法完全通过自动化实现。它们都需要由理解其重要性并具备一致应用判断力的贡献者来完成。这正是本文档所努力的方向。


5. 这对 AI 辅助开发意味着什么

文化 RFC 讨论了如何将 AI 工具作为协作团队的一部分来使用。本节探讨更具体的内容:当 AI 生成的代码遇到上述标准时会发生什么,以及当它未能达到标准时,如何识别并弥补差距。

AI 工具在通过检查方面确实表现出色。它们能够生成可编译的代码,满足类型检查器的要求,通过 Clippy 检查,并且通常还会在实现的同时生成测试。这确实具有实际价值,而本节的目的并非贬低这一点。问题不在于 AI 工具不可靠,而在于它们在错误的地方表现得过于可靠:它们擅长生成能够通过检查的代码,而不是符合标准的代码。

原因是结构性的。AI 根据它能推断出的内容来生成代码。如果一个函数没有文档,AI 就会从名称和签名来推断意图,有时这种推断是正确的,有时则会产生微妙的错误行为,而这些行为只在无人测试过的条件下才会显现。如果一个错误类型没有文档说明它在何时被返回,AI 就会根据变体的名称来处理它。如果一个测试套件测试的是实现而非行为,AI 就会生成与这些测试相匹配的实现,而这些实现可能符合也可能不符合测试本应捕获的预期行为。AI 输出的质量上限取决于你所提供的上下文的质量。更好的上下文、更清晰的文档、更具体的错误类型、以行为为中心的测试,会产生更好的输出。不完善的上下文产生的输出虽然能通过各种关卡,却把判断推给了下一个审查它的人。

这为使用 AI 工具的贡献者带来了一项具体且不可推卸的责任。

审查不会因为代码是 AI 写的就成为可选项。 文化 RFC 已经明确指出了这一点,这里有必要结合具体细节再强调一遍:在审查 AI 生成的代码时,能否编译、测试是否通过这类把关问题只是审查的起点,而非终点。标准的问题应当是:这段代码是否正确处理了运行时错误,还是直接 .unwrap() 了它们?新增的公共 API 是否有文档?测试断言的是行为还是实现?这段代码是否靠近信任边界,如果是,它是否对输入做了校验?无论代码出自谁手、用了什么工具来生成,这些问题都是你的责任。

AI 放大的是你的判断力,而非你判断力的缺失。 一个尚未在脑中建立起良好错误处理认知模型的贡献者,会原封不动地接受 AI 生成的错误处理代码:连同 .unwrap() 一并照单全收。而一个已经将 §4.1 内化于心的贡献者,在看到同样的输出时能够引导工具:“这是一条运行时错误路径;请使用 ?,并带上上下文将失败传播给调用方。”工具便会生成一个修正后的版本。同样的模式适用于 §4 中的每一项准则。在懂得该提出何种要求的人手中,这个工具威力强大。缺少这样的引导,它产出的代码只能勉强通过编译器,却把真正的决策推给了链条上的下一个人。

这种关系在两个方向上相互叠加。 一个理解标准的团队会随着工具的改进,从 AI 工具中获得越来越多的价值,因为他们能够更精确地驾驭功能更强大的工具。“工具生成了什么”与“标准要求什么”之间的差距,将变成他们可以通过引导而非手动重写来弥合的东西。一个没有培养出这种判断力的团队,则只能更快地达到同样的质量底线,却无法突破它。本文档自始至终所描述的投入,也直接是对团队未来将使用的每一款 AI 工具长期效能的投入,因为这些工具的价值会随着引导它们的判断力的清晰程度而成比例提升。


6. 工艺的便携性

技术日新月异。它的变化速度一次比一次快,而且这种变化速率还在不断加速。本文档中提到的具体工具:Rust、cargoclippy、OpenTelemetry SDK、团队当前使用的 AI 助手,都将被取代。其中一些甚至会在本项目的生命周期内被淘汰。平台将会变化,语言将会演进。五年后的工具生态将与今天大不相同,十年后又会再次截然不同。

本文档中的心智模型不会改变。

“当这里失败时应该发生什么,以及谁需要知道?”这个问题不会因为语言的改变而失效。在你学习下一门语言时,你会提出它。在你设计一个以“语言”为线路协议的分布式系统时,你会提出它。在你构建任何被他人依赖、却无法亲自监管的东西时,你都会提出它。Rust 为回答这个问题所提供的具体机制——Result<T, E>? 运算符、带上下文的结构化错误类型——只是对一个无处不在的问题的一种解答。

“我所承诺的公共接口是什么,我的文档是否如实反映了这一承诺?”这个问题,你会在设计 API 时提出,在编写技术规范时提出,在界定团队职责范围时提出,在向另一个团队、向 AI 工具、向客户、向承包商传达需求时提出。这种关于公共接口的“承诺与条款”模型,其适用范围远不止于 Rust,也远不止于软件领域。

“我的测试究竟证明了什么?”这个问题不仅限于软件领域,而是适用于任何需要验证系统是否按预期运行的领域。提出这个问题的本能——区分“你的实现存在”的证据和“正确的事情确实发生了”的证据——才是真正的技能所在。而用 Rust 表达这一点的语法只是次要的细节。

“需要诊断此故障的人需要了解什么?”这个问题是一个工程问题,适用于你所构建的、被他人依赖的任何东西。从更深层次来看,它也是一个关于同理心的问题——它提醒你,在你的工作另一端的那个人是一个真实的人,面对着一个真实的问题,在某个你无法预料的时刻,处于某种你无法亲临现场为其提供说明的情境中。

你学习的不只是 Rust。你是在借助 Rust 这一载体,学习构建值得信赖的事物。这种能力是可迁移的。只要你持续实践,它就会不断积累,并在你今后涉足的每一门语言、每一个系统、每一个团队、每一个领域中发挥作用。

这是项目对你的投资。不是针对你特定的技术技能,而是针对你未来构建任何事物时所展现的判断力、工艺和用心程度。同时,这也是你对未来将依赖你所构建之物的每个人的投资。


7. 这对贡献者意味着什么

如果您是 Rust 新手或软件开发新手:

§4 中的七项准则并不是你必须先掌握才能参与贡献的前提条件。它们是这片领域的地图:是你在工作过程中会遇到的事物,并被清晰地命名,让你在见到它们时能够明白自己所面对的是什么。

从第 4.1 节开始。错误处理的思维模型是你早期可以内化的最具杠杆效应的概念,它并不局限于 Rust。当你阅读现有代码并遇到 .unwrap() 时,问自己它属于哪一类。当你编写新代码时,对你的选择也问同样的问题。这个习惯若能持续实践,将提升它所触及的每一行代码,并培养一种将伴随你整个职业生涯的判断力。

不要等到觉得自己准备好了才去应用这些标准。不完美地应用它们,在不确定某项内容属于哪个类别时提出问题,并将审查中收到的反馈视为其本意所是的教学。没有人一开始就知道这些知识。它们是通过你正在做的这类工作,慢慢学到的。

如果您正在使用 AI 工具来帮助您贡献:

本文档中的标准就是细致审查在评估 AI 生成代码时所依据的准则。从实践角度看,它们也是让 AI 输出在进入审查之前变得更加正确的上下文。在要求 AI 实现某个功能之前,先检查它将要实现的接口是否已有文档。如果没有,请先编写文档,或将文档作为你要求 AI 产出内容的一部分。这样输出会更加正确,你也填补了基础中的一处实际缺口,而后续接手的贡献者也将从中受益。

当你收到对 AI 生成代码的审查反馈时,请将其视为对代码本身的反馈,而不是对你使用 AI 这一选择的反馈。无论代码由谁编写,适用的标准都是一致的。关键问题始终是:这段代码是否符合标准?如果不符合,需要做出哪些更改,以及原因是什么?

如果您正在审查拉取请求:

这些把关问题——能否编译、测试是否通过、Clippy 是否接受——是底线,而非上限。仅回答这些问题的评审是不完整的评审。请使用 §3 中的框架和 §4 中的准则来组织你的观察。明确指出你所采用的标准,解释它为何重要,并清晰地区分阻塞性问题与非阻塞性建议。

审查的目的不是挑错,而是传递理解。每一条包含解释的具体反馈——“这是一条运行时错误路径;这就是 .unwrap() 在此处会带来生产环境风险的原因,以及应改用什么方案”——都是对你所审查的贡献者的一笔投资。这笔投资会产生复利。理解了原则的贡献者,会在接下来十次需要应用它的场景中正确运用,而无需再被反复提醒。

如果您是维护者或更资深的贡献者:

你最有能力让这些标准成为现实,不是通过自上而下地强制执行,而是通过在自己的代码中以身作则,并在代码评审中点名引用这些标准。开源项目中最有效的教学发生在 PR 讨论和代码注释中,而不是文档里。本文档提供了相应的术语词汇。在日常评审中持之以恒地使用它们,才能将其从纸面上的文字转变为共同遵循的实践。

当你在一个运行时错误路径上看到 .unwrap() 时,请明确指出这一点。当你看到一个没有文档的公开函数时,请提出这样的问题:未来的实现者在这里需要了解什么?当你看到一个会在合理重构中失效的测试时,请解释为什么这很重要。这些并不是纠错:它们是文化 RFC 所指出的持续指导,即更有经验的贡献者能够提供的最重要的东西之一。