应用部分 1. 从遗留系统中恢复规范
状态:建议。 收集证据、标准化时间轴以及区分需求与 memory bank —— 这些都是成熟的工程实践。本章末尾的三方文件仲裁属于前沿探索。
对于学习性实践,只需收集一个 genealogy.md 并将已确认的需求与假设分开。文件仲裁、标准化器和历史数据回放仅在完整生产路径中才需要。
本章延续第一卷第13部分:在那里我们恢复现有项目的宪法,在这里我们从事故痕迹中恢复一个生产需求。保持聚焦范围狭窄:一个声明、两个来源、一个开放问题。任何需要标准化器、历史回放或文件仲裁的内容都属于完整路径。
阅读前
- 第一卷的支撑:第13部分教授如何恢复现有项目的宪法;在这里你恢复一个生产需求。
- 本地学习案例:
node_not_ready,因为它易于展示来源和不确定性。
capstone/的线索:为主要high_memory_usage编写一条genealogy.md记录,包含两个evidence_ref和一个开放问题。- 第一遍的主要术语:
evidence_ref和memory bank(需求与背景上下文之间的边界)。本章的其他术语——Verifier/Implementor/Safety、协调员-记录员、标准化器、文件仲裁——为参考性内容,在第8部分详细讲解。 - 暂时搁置的内容:日志标准化器、历史回放和文件仲裁。
在第一卷中,AgentClinic 是一个基于 TypeScript、Hono、服务端 JSX、SQLite 和 Vitest 的学习项目。在第二卷中,我们使用 AgentClinic-production 学习模型。同一个项目在概念上部署于 Kubernetes。Grafana 和 PagerDuty 向其三诊回路发送 webhook,而长期运行的副本积累了操作历史。第二卷中的 Python 仅用于 examples/ 中的小型可运行脚本,而非作为主力应用的技术栈。
无需搭建真实集群。第1-11章处理的遗留痕迹是生产场景的学习性事后分析、仪表板和日志。后续的具体事件(node_not_ready、appointment_latency / appointment_latency_spike、autoscale_200pct、cdn_error_budget_burn、high_memory_usage)均来自该模型,而非抽象场景。
这种工程实践的名称是:从可观察的工件——日志、指标、聊天记录、事后分析和可验证的决策痕迹——中恢复规范。如果你遇到形象化的表述「规范死灵术」,请将其仅视为这种重构的简短标签,而非独立的技术。
目标
在 SRE 团队流失后,自动事件管理项目中留下了碎片:47页非结构化日志、若干 Slack 线程、仪表板截图和没有正式 SDD 的事后分析。本章的目标是展示如何根据这些痕迹恢复适用于基于 Qwen Code 的三诊管道的工程可用规范。替代方案——一组看似合理的猜测——不适合我们。
完成本部分后,你将能够:
- 区分需求与
memory bank背景模型(完整定义见下方「关键思想」); - 将证据收集为统一的事件链;
- 提取隐式规则并将其转化为可验证的用户故事;
- 固定每个条目的来源,以便有争议的决策日后可被审计和重新论证(SDD 框架「规范作为可执行工件」来自 GitHub Spec Kit)。
最小学习场景
学习案例
生产事件 node_not_ready:根据指标日志、PagerDuty 升级和一份事后分析,需要恢复一个需求——NodeNotReady 事件何时成为 P1,以及何时不能自动关闭。
准备
book2/examples/templates/genealogy.md—— 来源模板。- 下方学习摘录——日志、事后分析和 Slack 线程的最小替代物。
- 一个有争议的事实:计划部署窗口、金丝雀命名空间或手动取消升级。
一个合乎逻辑的问题:genealogy.md 与 git log 或 git blame 有何不同。简而言之:区别在于 git 中没有这里承载意义的字段。git log 显示哪个文件被更改以及谁更改了它。genealogy.md 显示需求本身从何而来、我们对其有多大把握(uncertainty)、哪些来源证实它(evidence_ref)以及哪些开放问题仍未得到回答。git 历史中的提交「added requirement」无法区分「我们从事后分析中确定知道这一点」与「我们在聊天中猜测的这一点」。在 genealogy.md 中,这种区别是强制性的。
最小学习摘录:
grafana:NR-2026-05-17-01 cluster=prod-k8s node=worker-07 event=NodeNotReady count=3 window=10m
pagerduty:NR-2026-05-17-01 escalation=created owner=platform_oncall severity=P1
postmortem:node-not-ready-2026-05 note="auto-resolve was rejected until two stable OK windows"
open_questions:
- "canary namespace 排除 P1 还是仅降低置信度?"
如果你没有自己的日志,请使用此摘录。如果你有真实材料,请用你自己的内容替换这些行,但保持相同的最小要求:两个来源、一个声明、一个开放问题。
步骤
- 将模板
genealogy.md复制到工作目录。预期:出现一个包含来源、状态、置信度和开放问题部分的文件。 - 记录一个候选声明:例如,
>=3 NodeNotReady 在 10 分钟内产生 P1。 - 添加至少两个证据标记(
evidence_ref)和一个缺失上下文。预期:该声明不能被理解为「作者的个人意见」。 - 将需求与
memory bank分开:集群拓扑和值班人员姓名不应成为合同。 - 将声明重写为 Given/When/Then 格式,并指明未来 JSON Schema 的哪个字段将验证阈值、严重级别和关闭条件。
- 设置状态为
approved、needs_clarity或rejected。预期:有争议的事实不会被掩盖为已确认的需求。
验证事实
在 genealogy.md 中有一条记录,同时显示声明、来源、置信度级别、缺失上下文和与可验证行为的关联。如果阈值或 SLA 无法用来源引用辩护,则该需求保持为假设。
如何进入 capstone/
仅将一条受保护的记录转移到 capstone/genealogy.md:声明、两个 evidence_ref、置信度级别和开放问题。不要将整个时间轴、日志摘录和 Slack 引用转移到仓库中,除非它们成为特定需求的可验证证据。
high_memory_usage 的最小片段:
- claim: "当 appointments-api 的 memory_percent >= 90% 持续 10m 时,产生 P1。"
status: needs_clarity
evidence_ref: ["grafana:HM-2026-05-17-01", "postmortem:api-memory-2026-05"]
uncertainty: medium
open_questions:
- "未经两个稳定窗口确认自动解决是否被禁止?"
可审查的痕迹
在学习包中仅保存填写好的 genealogy.md 或其片段。草稿日志摘录和临时表格如果未成为可验证证据,则无需放入仓库。
关键思想
恢复规范的第一纪律——严格区分事实性需求和 memory bank 背景模型。memory bank 指的是一个独立的基础设施上下文层:所有有助于解释事实但本身不构成合同的内容。
如果这个术语看起来陌生,请通过第一卷的视角来理解。那里存在于 tech-stack.md(我们用什么编写)和 QWEN.md(代理的永久上下文)中的内容,在第二卷中统称为 memory bank。这是同一个背景层,只是现在它被明确地与需求分开,因为在生产场景中「合同 vs 上下文」的区别变得至关重要。
与 memory bank 不同,需求描述功能的行为。什么被视为触发器。何时创建事件。应用什么 SLA。谁获得升级。在什么条件下事件关闭。
memory bank 存储其他内容:集群拓扑、团队列表、历史约定、API 限制、常用通信渠道和操作术语。为什么这种区分很重要。如果混淆层次,SDD 中很容易出现虚假规则,如「金丝雀始终不可升级」。实际上这可能只是测试命名空间的上下文,而非产品的通用行为。
在工件清点阶段就引入这种区分。在 SDD 中纳入可通过可观察场景验证的声明:>=3 NodeNotReady 在 10 分钟内产生 P1、NOC 在 15 分钟内收到通知、关闭需要 2 个连续的 OK。
将以下内容发送到 memory bank:有助于解释事实但本身不构成合同的内容:
- 事件当晚谁值班;
- 为什么在 Slack 中使用旧服务名称;
- 哪些团队有权访问 Grafana。
这种过滤器降低了 Qwen Code 将基础设施背景误认为业务规则并基于偶然细节开始设计行为的风险。
第二个思想——将证据收集并标准化为统一的时间事件链。每个来源都有其自身特征:
- 日志提供可观察状态和事件发生的顺序;
- Slack 显示操作员的意图和手动绕过;
- 事后分析固定原因和后果;
- 指标允许评估降级规模。
分析前将来源统一到通用时间(UTC)。去除重复,提取事件代码,并用统一的事件标识符、集群、节点(node)或部署(deployment)关联记录。没有这一步,SDD 恢复将变成关于记忆的争论,而非系统行为的重建。
标准化链构建为 ts → source → event_code → actor → affected_scope → evidence_ref 的序列,其中最后一个字段是证据标记(evidence_ref),指向原始工件中的具体位置。在 node_not_ready 案例中,框架可能显示 10 分钟内 3 次 NodeNotReady 事件几乎总是先于 P1 创建。然后 15 分钟后发生 NOC 升级。关闭仅在两个稳定的 OK 之后发生。
单独记录例外:计划部署窗口、金丝雀命名空间、临时指标丢失或手动取消升级。不要将这些例外作为噪声删除——它们往往指向未来规范的隐藏条件。
> [概念接口] —— 这些命令显示本地标准化器的预期接口。教材仓库中没有现成的 timeline_builder.py 和 evidence_matrix.py;如果你从学习最小值转向完整路径,请在自己的项目中实现它们。
rg -n "NotReady|NodeNotReady|ALERT|deploy" evidence/raw/* > evidence/index.txt
python3 tools/timeline_builder.py --input evidence/raw --out evidence/timeline.ndjson
python3 tools/evidence_matrix.py \
--timeline evidence/timeline.ndjson \
--slack evidence/slack_export.json \
--metrics evidence/metrics.csv \
--out evidence/matrix.csv
验证:evidence/timeline.ndjson 中的每一行包含 ts、source、event_code、cluster、namespace、actor 和 evidence_ref;空字段阻止向需求推导的过渡。
接下来,图表展示如何从遗留系统获得恢复的 SDD。右侧出现「仲裁」块,包含三个角色和协调员:这是完整路径,在第8部分详细讲解。第一遍将「仲裁」块视为一个步骤「独立角色验证有争议的需求」——此处无需阅读详细角色组成。
flowchart TD
subgraph 输入["输入:遗留系统"]
L[日志、事后分析、Slack、指标]
end
subgraph 处理["处理"]
P[解析和时间链]
R[需求假设和用户故事]
end
subgraph 仲裁["仲裁(完整路径,第8部分)"]
TBR[独立角色验证有争议的需求]
end
subgraph 结果["结果"]
S[恢复的 SDD 和 genealogy.md]
end
L --> P --> R --> TBR --> S第三个思想——通过 Qwen Code 提取隐式需求,但按来源和上下文评估每个声明。Qwen Code 在这里不是作为业务逻辑的作者,而是作为提取的中介。向它传递事实、环境约束和严格的回答格式,其中禁止没有证据引用的声明。
好的请求不是要求「设计 SDD」,而是做其他事情:
- 在时间链中找到重复规则;
- 指明证实来源;
- 命名反例;
- 分配置信度级别。
这样模型增强分析,但没有权利将猜测转化为需求。期望从 Qwen Code 获得候选声明(claims)列表,而非最终规范。
不好的示例:
> REQ-NR-01:当节点上频繁出现 NodeNotReady 时产生 P1。
问题:没有阈值、没有窗口、没有证据标记。该规则既无法验证也无法质疑。
好的示例:
> REQ-NR-01:当单个节点在 10 分钟内出现 >=3 NodeNotReady 且 5xx 相关增长时产生 P1。证据:logs/node-2026-05-12.parquet#row_4123、slack/thread_11#msg_7、grafana/node_5xx#segment_11:00。置信度:中等。缺失上下文:计划部署窗口。
这在实践中的意义。这种记录比流畅的用户故事文本更有用:它立即显示需求在何处稳定、在何处需要服务所有者验证。如果规则仅由一份事后分析证实且与指标不符,即使听起来令人信服,它仍保持为假设。
> [项目脚本] —— qwen -p 本身是可运行的,但输入 @evidence/matrix.csv 需要先在您的项目中收集。用单独的标准化解析器稳定最终 JSON 格式。
qwen -p "读取 @evidence/matrix.csv。找到 node_not_ready 事件的重复规则。
返回包含证据、反例、缺失上下文和置信度的 claims。
没有证据不要断言事实。" \
--approval-mode plan \
--output-format json \
> sdd/drafts/nr-claims.qwen.json
qwen -p "读取 @sdd/drafts/nr-claims.qwen.json 并进行交叉审查:
对每个 claim 检查来源、反例和缺失上下文。
将 claim 标记为 approved、needs_clarity 或 rejected。" \
--approval-mode plan \
--output-format json \
> sdd/drafts/nr-claims-cross.qwen.json
验证:Qwen 此处以无头计划模式运行。Qwen Code 的最终 JSON 是带会话消息的报告;如果项目需要严格的 claims.json,请添加单独的标准化解析器并用测试验证它。
第四个思想——将需求同时编码为 Given/When/Then 和机器可读合同,如 JSON Schema。Given/When/Then 将需求保持在行为语言中:初始状态、事件、预期结果。
JSON Schema 固定必填字段、允许值、数值边界和数据结构。合同可在 CI 或本地验证管道中验证。双重记录消除「人类可理解」与「机器可验证」之间的鸿沟。
对于 node_not_ready,行为故事如下:
- Given 集群
prod-k8s处于活跃班次,且在 10 分钟内为单个节点记录>=3 NodeNotReady; - When 事件与部署或相关指标中 5xx 增长相关联;
- Then 创建
severity=P1的事件,预期 8 分钟内初步响应,15 分钟后自动升级到NOC,且仅在 10 分钟内 2 个连续OK后才允许关闭。
将金丝雀命名空间的例外作为单独条件,而非末尾注释。否则验证器无法区分标准路径和放宽阈值。这种格式将关于「快速响应」的讨论转化为具体数字、事件和状态。
同一合同的最小 JSON Schema(含 triggers 和 auto_resolve_window 正则表达式的完整形式在完整路径中):
{
"$id": "urn:spec:node-not-ready:v1",
"type": "object",
"required": ["rule_id", "severity", "sla_minutes", "conditions"],
"properties": {
"rule_id": {"type": "string"},
"severity": {"type": "string", "enum": ["P0", "P1", "P2", "P3"]},
"sla_minutes": {"type": "integer", "minimum": 1, "maximum": 120},
"conditions": {
"type": "object",
"required": ["event_code", "count", "window_minutes", "namespace_rule"],
"properties": {
"count": {"type": "integer", "minimum": 3},
"window_minutes": {"type": "integer", "minimum": 1},
"namespace_rule": {"type": "string", "enum": ["standard", "canary"]}
}
}
}
}
第五个思想仅适用于完整路径:有争议的恢复需求可提交文件仲裁。三个角色投票——验证者、实现者、安全;协调员记录日志,不投票。验证者检查数字和状态的一致性,实现者检查在当前三诊管道中的可实现性,安全者检查安全行动边界和在 critical_risk 时的否决权。角色、裁决和先例在第8部分详细讲解;可运行的学习类比在 [examples/tribunal/](examples/tribunal/)。对于学习最小值,此步骤不需要:只需带有来源、置信度级别和开放问题的 genealogy.md 就足够了。
第六个思想——维护 genealogy.md,每个需求来源的独立注册表。为什么需要它。如果一个月后无法解释以下内容,恢复的 SDD 将迅速失去价值:
- 为什么选择 3 个事件/10 分钟的阈值;
- 谁确认了 8 分钟 SLA;
- 为什么金丝雀获得单独模式。
genealogy.md 将声明与日志、Slack、指标、事后分析、文件仲裁决策和当前不确定性级别关联。这样规范成为证据链,而非集体记忆的文本快照。
- req_id: NR-01
statement: "当单个节点在 10m 内出现 >=3 NodeNotReady 且 5xx 增长时产生 P1。"
source:
- logs: evidence/normalized_node_logs.parquet#row_4123
- slack: export/slack_thread_11.json#msg_7
- metrics: grafana/node_5xx_timeseries.csv#segment_2026-05-12T11:00
status: approved
adjudicated_by: [Verifier, Implementor, Safety]
uncertainty: low
open_questions: []
如果条目保持争议,不要将其掩盖为已确认合同。设置 uncertainty: medium 或 uncertainty: high,指明怀疑原因并添加验证计划:
- 请求服务所有者;
- 在历史数据上运行回放;
- 与相邻集群比较;
- 收集缺失指标。
这种来源注册表对未来项目宪法尤为重要。只有来源清晰、作用域明确且有修订机制的规则才应进入宪法。
示例与应用
「最小学习场景」中的 4 行学习摘录已经是过滤后的标准化结果。原始集合包含:
- 9 小时观察;
- 11 条相关 Slack 消息;
- 47 页未清理日志;
- 1,248 个
NodeNotReady事件; - 63 个告警;
- 8 个先前关闭的事件。
标准化后可见,NodeNotReady 的急剧增长与部署重合,部分事件进入具有不同自动升级逻辑的金丝雀段,出现两个行为分支:标准 P1 和具有放宽阈值的金丝雀路径。
> [概念接口] —— 标准化器的伪代码。第二卷的可运行示例保持使用 Python 标准库,位于 book2/examples/。
读取 evidence/normalized_node_logs
按 ts 排序事件
过滤 event_code == "NodeNotReady"
按集群、节点在 10m 窗口内分组
标记 count >= 3 的窗口
将标记窗口与 [-15m,+5m] 内的告警和 Slack 消息关联
[-15m,+5m] 窗口是必要的,因为操作员可能在正式记录事件之前讨论问题,或在自动告警之后。如果事件属于没有 SLO 降级的金丝雀命名空间——设置单独标记,而非作为噪声删除。如果计划部署窗口解释了部分 NodeNotReady,直接在需求中指明这是阻止 P1 创建还是仅降低置信度。
恢复的 SDD 仅在回放后才成为工作工件:通过新的 JSON 合同运行历史事件,检查产生的严重级别、SLA 和升级是否与确认的结果匹配。不匹配并不总是意味着合同错误——有时它们表明旧实践是矛盾的或取决于特定值班人员。在这种情况下更改什么——规范、memory bank 或 genealogy.md 中的假设状态——由第8部分的文件仲裁决定。
总结
从遗留系统恢复规范不是从直觉而是从可验证的证据链恢复 SDD。路径如下:
- 遗留工件标准化为时间轴;
- Qwen Code 提取带置信度级别的候选声明;
- 需求与
memory bank分离; - 然后编码为 Given/When/Then 和 JSON Schema;
- 对于完整路径,通过协调员/实现者/验证者的文件仲裁;
- 在
genealogy.md中获得来源。
此过程将日志、聊天和事后分析的混乱转化为合同。合同可被验证、质疑、在历史数据上重放并迁移到更严格的规则系统。下一章我们将故意用矛盾毒害规范,研究 Qwen Code 在何处开始陷入困境。
工件与就绪标准
| 工件 | 就绪条件 |
|---|---|
含一个需求或假设的 genealogy.md | 需求与 memory bank 分离,有争议事实标记为假设 |
至少两个 evidence_ref 和一个缺失上下文 | 声明不能被理解为「作者意见」,阈值/SLA 由来源引用辩护或明确标记为暂不可确认 |
| Given/When/Then 表述 | 可验证字段与 JSON Schema 覆盖内容关联 |
完整路径添加 evidence/timeline.ndjson、带日志/Slack/指标/事后分析引用的 evidence/matrix.csv、含候选声明的 sdd/drafts/nr-claims.qwen.json、contracts/node_not_ready.schema.json 以及无法手动确认需求的文件仲裁记录。当 Given/When/Then 和 JSON Schema 描述同一合同、标准化器产生可复现时间链、且验证器或文件仲裁产生可验证 verdict 时,认为完整路径就绪。
实践
- 将 [
examples/templates/genealogy.md](examples/templates/genealogy.md) 复制到capstone/genealogy.md,为主要案例high_memory_usage填写一条记录:声明、至少两个evidence_ref、置信度级别和一个开放问题。「最小学习场景」中的学习摘录可作为真实日志的替代物。 - 将声明重写为 Given/When/Then,并指明 JSON Schema 的哪三个字段验证阈值、严重级别和关闭条件。无法用来源引用辩护的字段保留为
uncertainty: medium,而非已确认合同。
- 打开 [
appendix-a-bridges-to-book.md](appendix-a-bridges-to-book.md),标记第一卷的哪一章是你genealogy.md的支撑。如果没有支撑——这是需求尚未绑定到学习模型的信号。
检查问题
- 为什么证据比自信的需求表述更重要?
memory bank与 SDD 合同有何不同,为什么混淆它们很危险?- 何时不能将假设转化为已确认需求?
- 你通过两份事后分析恢复了一条规则,但服务所有者半年前已离职。在将其加入
requirements.md之前,你会如何处理这条规则?