应用部分 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)均来自此模型,而非抽象场景。
此技术的工程名称是从可观察工件中恢复规范:日志、指标、聊天记录、事后分析和可验证的决策痕迹。如果遇到形象化的表述"Spec-necromancy",请仅将其视为此重构的简短标签,而非独立技术。
目标
SRE团队流失后,自动事件管理项目中留下了碎片:47页非结构化日志、若干Slack线程、仪表板截图和没有正式SDD的事后分析。本章的目标是展示如何根据这些痕迹恢复基于Qwen Code的三诊流水线的工程可用规范。替代方案——一组看似合理的猜测——不适合我们。
完成本节后,你将能够:
- 区分需求与
memory bank背景模型(完整定义见下方"关键思想"); - 将证据收集为统一的事件链;
- 提取隐式规则并将其转化为可验证的用户故事;
- 固定每个条目的来源,以便有争议的决策日后可被审计和重新论证(GitHub Spec Kit中SDD框架的"规范作为可执行工件")。
最小教学场景
教学案例
生产事件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 excludes P1 or only reduces confidence?"
如果你没有自己的日志,请使用此摘录。如果你有真实材料,请替换为自有内容,但保持相同的最小要求:两个来源、一个声明、一个开放问题。
步骤
- 将模板
genealogy.md复制到工作目录。预期结果:出现包含来源、状态、置信度和开放问题部分的文件。 - 记录一个候选声明:例如,
>=3 NodeNotReady in 10 minutes creates 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: "When memory_percent >= 90% for 10m for appointments-api, P1 is created."
status: needs_clarity
evidence_ref: ["grafana:HM-2026-05-17-01", "postmortem:api-memory-2026-05"]
uncertainty: medium
open_questions:
- "Is the prohibition on auto-resolve without two stable windows confirmed?"
可审查痕迹
在教学包中仅保留填写完成的genealogy.md或其片段。草稿日志摘录和临时表格如未成为可验证证据,则无需存入仓库。
关键思想
恢复规范的第一纪律——严格区分事实需求与memory bank背景模型。memory bank指的是单独的基础设施上下文层:所有有助于解释事实但本身不构成合同的内容。
如果这个术语看起来陌生,请通过第一卷的视角来理解。那里存在于tech-stack.md(我们用什么编写)和QWEN.md(代理的永久上下文)中的内容,在第二卷中统称为memory bank。这是同一个背景层,只是现在它被明确与需求分离,因为在生产场景中"合同 vs 上下文"的区别变得至关重要。
与memory bank不同,需求描述功能的行为。什么构成触发器。何时创建事件。应用什么SLA。谁接收升级。在什么条件下事件被关闭。
memory bank存储其他内容:集群拓扑、团队列表、历史约定、API限制、常用通信渠道和操作术语。为什么区分这很重要。如果混淆层次,SDD中很容易出现虚假规则,如"canary始终不可升级"。实际上这可能只是测试命名空间的上下文,而非产品的通用行为。
在工件清点阶段就引入区分。在SDD中归入可通过可观察场景验证的声明:>=3 NodeNotReady in 10 minutes creates P1、NOC在15分钟内收到通知、关闭需要2个连续的OK。
将以下有助于解释事实但本身不构成合同的内容发送到memory bank:
- 事件当晚谁值班;
- 为什么Slack中使用旧服务名称;
- 哪些团队有权访问Grafana。
这种过滤器降低了Qwen Code将基础设施背景误认为业务规则并开始基于偶然细节设计行为的风险。
第二个思想——将证据收集并归一化为统一的时间事件链。每个来源有其独特特征:
- 日志提供可观察状态和事件顺序;
- Slack显示操作员的意图和手动绕过;
- 事后分析固定原因和后果;
- 指标允许评估降级规模。
分析前将来源统一到共同时间(UTC)。去除重复,提取事件代码,并用统一的事件标识符、集群、节点或部署标识链接记录。没有这一步,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: 当node上频繁出现NodeNotReady时创建P1。
问题:没有阈值、没有窗口、没有证据标记。规则既无法验证也无法反驳。
好的示例:
> REQ-NR-01: 当单个node在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 "Read @evidence/matrix.csv. Find repeating rules
for incident node_not_ready. Return claims with evidence, counterexample,
missing_context and confidence. Do not assert facts without evidence." \
--approval-mode plan \
--output-format json \
> sdd/drafts/nr-claims.qwen.json
qwen -p "Read @sdd/drafts/nr-claims.qwen.json and cross-examine:
for each claim check source, counterexample and missing_context.
Mark claim as approved, needs_clarity or 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,且仅在2个连续OK持续10分钟后才允许关闭。
将金丝雀命名空间的例外作为单独条件处理,而非末尾注释。否则验证器无法区分标准路径和放宽阈值。这种格式将关于"快速响应"的讨论转化为具体数字、事件和状态。
同一合同的最小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: "When >=3 NodeNotReady in 10m for one node and 5xx growth, P1 is created."
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/。
read evidence/normalized_node_logs
sort events by ts
filter event_code == "NodeNotReady"
group by cluster,node in 10m windows
mark windows where count >= 3
link marked windows to alerts and Slack messages in [-15m,+5m]
[-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、evidence/matrix.csv(含日志、Slack、指标和事后分析链接)、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合同有何不同,为什么混淆它们很危险?- 何时不能将假设转化为approved需求?
- 你通过两份事后分析恢复了一条规则,但服务所有者半年前已离职。在将其加入
requirements.md之前,你会如何处理这条规则?