生产工程
用评测、可观测性、部署与证据证明系统在真实约束下可靠。
如何证明系统可靠,并完成部署、作品集和面试表达?概念校准
验证与评测
- 它是什么
- 用测试、Schema、引用、状态和验收规则检查单次结果,并用数据集衡量系统表现。
- 它不是什么
- 不等于让模型对自己的输出再给一次意见;验证必须落到外部可检查证据。
可观测性
- 它是什么
- 通过结构化日志、Trace、指标和版本信息解释系统在一次运行中发生了什么。
- 它不是什么
- 不只是保存终端输出;没有关联 ID、状态转换和敏感信息边界的日志难以诊断。
贯穿式作品集交付
集成三个场景,补齐发布包、演示、架构文档和证据索引。
Python 3.12 · pytest · Pydantic · SQLite · deterministic Mock概念校准\n\n在开始操作代码前,先把本章涉及的概念放回正确的工程边界。\n\n\n\n### 验证与评测 evaluation\n\n- 它是什么: Verification 用测试、Schema、引用、状态或验收规则检查一次结果;Evaluation 用数据集和指标衡量多次运行表现。\n- 它不是什么: 它不是让模型对自己的回答再给一次意见;通过与否必须落到外部可检查证据。\n- 与相邻概念的关系: Agent Loop 依赖 Verification 决定停止,RAG 需要分层评测,Planning 需要节点验收,Observability 提供运行记录。\n- 在 HeatStack Forge 中的位置: Forge 为单次运行保存合同测试与 evidence,再用固定场景集比较质量、延迟、成本、权限和恢复能力。\n- 典型误用与修正: 误用是把一个综合分数当成全部结论。修正方法是定义分层指标、失败样本和版本基线,并结合产品风险评审。\n\n### 可观测性 observability\n\n- 它是什么: 通过结构化日志、Trace、指标、状态转换和版本信息解释一次运行发生了什么,并支持定位失败与比较版本。\n- 它不是什么: 它不只是终端输出,也不是无限记录所有输入;缺少关联 ID、状态转换或敏感信息边界的日志既难排查又可能泄露数据。\n- 与相邻概念的关系: Harness 产生运行事件,Agent Loop 与工具调用形成 Trace,Evaluation 聚合指标,Memory 与恢复依赖版本记录。\n- 在 HeatStack Forge 中的位置: Forge 为每次任务生成 operation_id,记录阶段、工具、耗时、成本、错误类别、状态变化和证据路径,同时过滤敏感信息。\n- 典型误用与修正: 误用是等失败后再临时加日志。修正方法是在合同设计时定义事件与关联字段,并用故障注入确认能重建时间线。\n\n
为软件、办公研究和音乐场景定义可比较的输入、输出与验收合同
先看一个具体问题
为什么非空检查挡不住路径逃逸?
在 forge/portfolio/scenarios.py 中,ScenarioContract 目前只检查场景名称是否允许以及输入、输出、验收元组是否非空。
当输入路径为 ../private.csv 时,validate() 不会报错,因为该路径非空且场景 office 合法。
这意味着合同可以引用包外文件,破坏交付包的封闭性,并可能泄露工作区中的敏感数据。
本阶段需要补全路径限制逻辑,使任何绝对路径或包含 .. 的路径都被拒绝。
先做判断
在补全路径检查之前,请预测:如果只检查输入路径非空,ScenarioContract("office", ("../private.csv",), ("brief.md",), ("citations present",)).validate() 会发生什么?
- 抛出 ValueError,因为路径包含
.. - 正常返回,因为路径非空且场景合法
- 抛出 ValueError,因为场景
office不允许 - 抛出 TypeError,因为路径不是字符串
判断依据: 当前代码只检查非空,没有检查路径内容,所以 ../private.csv 会被接受,validate() 正常返回。这正是测试 test_scenario_path_escape_is_rejected 失败的原因。
它是什么
合同验证是完整性与路径限制的组合
ScenarioContract.validate() 必须同时检查三件事:场景名称在允许集合中、所有元组非空、输入路径不逃逸包目录。
路径限制通过拒绝绝对路径(以 / 开头)和包含 .. 的路径来实现,确保输入只能来自包内。
它不是什么
合同验证不是只检查非空
仅检查非空无法阻止 ../private.csv 这样的路径,因为该路径非空且场景合法。
合同验证也不是对文件内容或输出正确性的检查,它只负责合同本身的完整性和路径安全。
它与相邻概念的关系
路径检查与场景、完整性的关系
场景检查确保合同属于已知场景,完整性检查确保必要字段存在,路径检查确保输入来源受控。
三者缺一不可:如果省略路径检查,合同可能引用包外文件;如果省略完整性检查,合同可能缺少必要字段。
本阶段的边界决定: 路径检查必须同时拒绝绝对路径和包含 .. 的路径,因为绝对路径直接指向文件系统任意位置,而 .. 可以跳出当前目录。只拒绝其中一种仍可能被绕过。
把每次场景运行的输出、测试和追踪绑定为一个可验证结果
先看一个具体问题
交付结果缺少必需输出却被接受
在 Stage 01 中,我们定义了 ScenarioContract,它规定了每个场景必须交付哪些输出文件和通过哪些验收检查。现在需要实现 verify_delivery 函数,确保交付结果完整满足合同要求。
当前起始代码中 forge/portfolio/integration.py 文件尚未创建,因此测试在导入时就会失败。学习者需要创建该文件并实现 DeliveryResult 数据类和 verify_delivery 函数。
一个常见的错误是只检查结果中是否有任意输出,而忽略了合同要求的完整输出集合。例如,软件场景合同要求输出 patch.diff 和 test.txt,但结果只包含 patch.diff,如果只检查非空就会错误地接受这个不完整的交付。
本阶段的目标是让学习者理解验证必须基于合同要求进行集合减法,而不是仅检查非空。通过实现正确的验证逻辑,确保所有必需输出和验收检查都存在,并且 trace_id 非空。
先做判断
如果 verify_delivery 只检查 result.outputs 是否非空,会发生什么?
- 它会正确拒绝缺少 test.txt 的交付结果。
- 它会错误地接受缺少 test.txt 的交付结果。
- 它会因为类型错误而崩溃。
- 它会忽略验收检查。
判断依据: 正确答案是第二个选项。只检查非空无法发现缺失的必需输出,因为结果中至少有一个输出文件就会通过检查。正确的做法是从合同要求的输出集合中减去实际输出集合,如果差集非空则说明有缺失。
它是什么
合同覆盖验证
合同覆盖验证是一种基于集合运算的验证方法:将合同要求的输出集合和验收检查集合分别与实际结果中的输出集合和通过检查集合进行差集运算。
如果差集为空,说明所有要求都已满足;如果差集非空,则说明存在缺失项,交付不完整。
这种验证方式明确、可检查,不依赖于主观判断,能够精确指出缺失的具体项目。
它不是什么
不是非空检查
非空检查只验证结果中是否有任意输出,无法保证所有必需输出都存在。
它不能发现缺失的特定文件或检查,因此会错误地接受不完整的交付。
合同覆盖验证要求对每个必需项进行逐一核对,而不是笼统地判断是否存在输出。
它与相邻概念的关系
与 ScenarioContract 的关系
ScenarioContract 定义了场景的 expected_outputs 和 acceptance 集合,verify_delivery 使用这些集合作为验证的基准。
DeliveryResult 保存实际交付的输出和通过的检查,verify_delivery 将两者进行对比。
验证通过的条件是:场景名称匹配、所有 expected_outputs 都在 result.outputs 中、所有 acceptance 都在 result.passed_checks 中,并且 trace_id 非空。
本阶段的边界决定: 验证必须基于合同要求进行集合减法,而不是仅检查非空。如果只检查非空,就会漏掉缺失的必需输出或验收检查,导致不完整的交付被错误接受。
对发布文件排序并计算内容哈希,使相同输入产生相同清单摘要
先看一个具体问题
为什么相同输入却得到不同清单?
你刚完成 Stage 02 的交付验证,现在需要把发布文件打包成清单。
测试 test_same_inputs_produce_same_manifest 要求两次调用 build_manifest("1.0", {"a.txt": b"a"}) 返回完全相同的字典。
但当前 forge/portfolio/packaging.py 文件尚未创建,测试收集阶段直接报错 ModuleNotFoundError。
你需要实现一个函数,让相同输入产生相同输出,这是可复现发布的基础。
先做判断
在实现 build_manifest 时,以下哪个做法最可能破坏可复现性?
- 对文件路径排序后再生成清单
- 在清单中加入当前时间戳
- 使用 SHA-256 计算文件内容哈希
- 用 JSON 序列化时固定键顺序
判断依据: 加入时间戳会让每次构建的清单不同,即使输入文件完全相同,因此无法通过 test_same_inputs_produce_same_manifest。排序和固定序列化顺序是保证确定性的关键。
它是什么
可复现清单是什么
可复现清单是一个确定性函数:给定相同的版本号和文件内容,总是生成完全相同的清单字典。
它只依赖版本号和排序后的文件条目,不包含任何随时间变化的元数据。
清单摘要 manifestSha256 是对规范化 JSON 字符串的哈希,因此相同输入必然得到相同摘要。
它不是什么
可复现清单不是什么
它不是包含构建时间、构建序号或随机数的日志,那些非确定性信息会让相同输入产生不同摘要。
它也不是简单地把文件字典原样输出,因为字典顺序可能不稳定,必须显式排序。
它不负责验证文件内容是否正确,只负责记录文件路径、哈希和大小。
它与相邻概念的关系
与其他概念的关系
可复现清单是 Stage 02 交付验证的延伸:验证通过后,用清单固化发布内容。
它依赖哈希函数(SHA-256)来唯一标识文件内容,任何内容变化都会改变哈希。
排序是保证确定性的关键,因为 JSON 序列化时键顺序可能影响最终字符串。
本阶段的边界决定: 如果清单中包含时间戳,相同输入会产生不同摘要,因此必须排除所有非确定性元数据,只保留版本和排序后的文件条目。
用可检查的演示步骤连接用户问题、系统动作和证据文件
先看一个具体问题
演示脚本缺少证据路径导致不可验证
你正在为 HeatStack Forge 准备一个演示脚本,脚本包含三个步骤:展示请求、执行命令、展示测试结果。
当前代码中 validate_demo 只检查了叙述字段,没有检查命令和证据路径,导致一个缺少证据路径的步骤也能通过验证。
测试 test_demo_without_evidence_path_is_rejected 期望当证据路径为空时抛出包含 ‘incomplete’ 的 ValueError,但当前代码会接受这个不完整的步骤。
你需要修改 forge/portfolio/demo.py 中的 validate_demo 函数,确保每个步骤的叙述、命令和证据路径都非空。
先做判断
如果 validate_demo 只检查叙述字段,会发生什么?
- 缺少证据路径的步骤会被接受,演示无法验证
- 缺少命令的步骤会被拒绝,但缺少证据路径的步骤会被接受
- 所有步骤都会被拒绝,因为叙述字段可能为空
- 演示脚本无法运行,因为缺少必要的字段
判断依据: 正确答案是第一个选项。只检查叙述字段会导致缺少证据路径的步骤被接受,因为验证逻辑没有覆盖命令和证据路径。
它是什么
演示验证是完整性检查
演示验证通过检查每个步骤的叙述、命令和证据路径是否非空,确保演示脚本的每个步骤都有可验证的证据。
它使用 all() 函数同时检查三个字段,任何一个为空都会抛出 ValueError。
它不是什么
演示验证不是内容质量评估
演示验证不检查叙述是否优美、命令是否最优或证据文件是否真实存在,它只检查字段是否非空。
它也不检查步骤之间的逻辑顺序或步骤 ID 是否唯一,这些是其他验证逻辑的职责。
它与相邻概念的关系
演示验证与测试的关系
测试 test_complete_demo_passes 验证完整演示通过,而 test_demo_without_evidence_path_is_rejected 验证缺失证据路径被拒绝。
validate_demo 函数是这两个测试的核心,它必须同时满足通过和拒绝的条件。
本阶段的边界决定: 演示验证必须要求每个步骤都有叙述、命令和证据路径,缺少任何一项都不可验证。
记录上下文、选择、备选与后果,拒绝只有结论没有取舍依据的架构说明
先看一个具体问题
为什么只有决策 ID 和选择的记录无法通过验证?
在上一阶段,我们通过 DemoStep 和 validate_demo 验证了演示脚本的步骤完整性,但架构决策记录(ADR)还没有被纳入验证范围。
当前测试 test_decision_without_alternatives_is_rejected 期望:当 alternatives 为空元组时,DecisionRecord.validate() 必须抛出包含 'tradeoff' 的 ValueError。
然而,如果 validate() 只检查 decision_id 和 choice 是否非空,那么一个没有备选方案、后果和证据的记录也会被接受,导致测试失败。
你需要实现 DecisionRecord.validate(),确保它拒绝缺少取舍依据的架构决策记录。
先做判断
在实现 DecisionRecord.validate() 之前,请预测:如果只检查 decision_id 和 choice 非空,会发生什么?
- 所有测试都会通过,因为决策 ID 和选择已经足够。
test_decision_without_alternatives_is_rejected会失败,因为空alternatives不会被拒绝。test_complete_decision_record_passes会失败,因为完整记录也会被拒绝。- 两个测试都会失败,因为
validate()没有实现。
判断依据: 正确答案是第二个选项。只检查 decision_id 和 choice 会让空 alternatives 的记录通过验证,从而触发测试中的 AssertionError。
它是什么
架构决策记录(ADR)的验证是什么?
ADR 验证是一种外部可检查的规则,确保每个决策记录不仅包含决策 ID 和选择,还包含上下文、备选方案、后果和证据。
DecisionRecord.validate() 通过检查这些字段是否非空,以及选择是否不在备选方案中,来保证记录包含足够的取舍依据。
它不是什么
ADR 验证不是什么?
它不是让模型对自己的决策再给一次意见,也不是仅仅检查决策 ID 是否存在。
它不关心决策内容是否正确,只关心记录是否完整地呈现了决策的上下文、备选方案、后果和证据。
它与相邻概念的关系
ADR 验证与其他概念的关系
ADR 验证是 Verification 概念的具体应用:通过测试和规则检查一次结果,而不是通过数据集衡量多次运行表现。
它与 Observability 相关,因为完整的 ADR 提供了决策的上下文和证据,有助于解释系统为什么这样设计。
它与 Agent Loop 的停止条件相关:如果决策记录不完整,验证失败,循环应该继续补充信息。
本阶段的边界决定: 边界决策:验证必须基于外部可检查的证据(字段非空、选择不在备选方案中),而不是模型的主观判断。
按场景、能力和证据类型建立索引,并防止同一路径被不同哈希覆盖
先看一个具体问题
证据索引必须防止同一路径出现冲突哈希
在作品集交付中,我们需要把不同场景产生的证据(如文件、报告、测试结果)组织成可搜索的索引,以便后续按能力或场景快速查找。
当前 forge/portfolio/evidence_index.py 文件尚未创建,测试 test_evidence_is_grouped_by_capability 和 test_conflicting_hash_for_same_path_is_rejected 会因导入错误而失败。
如果只是简单地把每个证据条目追加到列表里,而不检查同一路径是否已经记录了不同的内容哈希,索引就会变得自相矛盾:同一个文件路径对应两个不同的内容指纹,下游消费者无法判断哪个是真实的。
因此,本阶段的核心决策是:在构建索引时维护一个路径到哈希的映射,一旦发现同一路径出现不同哈希,立即抛出包含 conflicting 的 ValueError,而不是静默覆盖或并存。
先做判断
在 build_index 中,如果同一个 path 先后出现两个不同的 sha256,以下哪种处理方式最符合证据索引的不可变身份要求?
- 直接追加第二个条目,让列表包含两个哈希,由使用者自行判断。
- 用后出现的哈希覆盖先前的哈希,保持路径唯一。
- 检测到冲突时抛出
ValueError,并让错误信息包含conflicting。 - 忽略第二个条目,只保留第一个哈希。
判断依据: 证据索引必须保证路径与内容哈希的一一对应,否则同一路径指向两个不同内容会让下游无法确定真实证据。因此,检测到冲突时应当拒绝并抛出异常,而不是覆盖或并存。
它是什么
证据索引是路径到哈希的不可变映射
证据索引把每个证据条目按能力(capability)分组,并在组内记录路径、场景、类型和内容哈希。
它同时维护一个全局的路径到哈希映射,用于在插入新条目时检查该路径是否已经出现过且哈希一致。
如果同一路径的哈希一致,可以安全地重复引用;如果哈希不同,说明内容已经改变或存在冲突,必须拒绝。
它不是什么
证据索引不是简单的追加列表
它不能允许同一路径携带多个不同的哈希,因为那样会让索引失去可信度。
它也不是一个自动去重或覆盖的存储结构;冲突必须显式抛出异常,让调用方决定如何处理。
它不负责验证哈希本身的正确性,只负责维护路径与哈希之间的一致性约束。
它与相邻概念的关系
索引与证据项、能力分组的关系
EvidenceItem 是不可变数据类,包含 path、sha256、scenario、capability 和 kind 五个字段。
build_index 接收一个 EvidenceItem 元组,返回一个字典:键是能力名称,值是该能力下的证据条目列表。
在构建过程中,seen 字典记录每个路径首次出现的哈希,后续条目如果路径相同但哈希不同,就会触发冲突异常。
本阶段的边界决定: 当且仅当同一路径已经存在且哈希不同时,必须抛出包含 conflicting 的 ValueError;如果哈希相同,则允许重复引用。
只有三场景、架构说明、演示脚本和证据索引齐全时发布 Forge 1
先看一个具体问题
发布门禁必须检查所有必需场景和部分
当前 forge/portfolio/release.py 文件尚未创建,测试 test_complete_portfolio_publishes 和 test_missing_music_scenario_blocks_release 在导入模块时就会失败。
你需要实现 PortfolioRelease 数据类和 publish 函数,使完整作品集能够发布,而缺少音乐场景的作品集被阻止并抛出包含 ‘music’ 的 ValueError。
发布门禁不能只检查场景集合非空,必须对照必需的场景集合和部分集合进行验证。
先做判断
如果 publish 函数只检查 release.scenarios 是否非空,当传入只包含 software 和 office 两个场景的作品集时,会发生什么?
- 发布成功,因为场景集合非空
- 抛出 ValueError,因为缺少 music 场景
- 返回空字符串
- 抛出 TypeError
判断依据: 正确答案是“发布成功,因为场景集合非空”。只检查非空无法发现缺失的必需场景,这正是故障夹具中注入的缺陷。
它是什么
发布门禁是对照必需集合的完整性检查
发布门禁将作品集中包含的场景集合与 REQUIRED_SCENARIOS 常量定义的必需场景集合进行比较,任何缺失的场景都会导致发布失败。
同样,它也会检查必需的部分集合 REQUIRED_SECTIONS,确保架构说明、演示脚本和证据索引等关键部分都存在。
只有当两个集合都没有缺失时,publish 才会返回发布标识符。
它不是什么
发布门禁不是简单的非空检查
发布门禁不会因为场景集合中至少有一个元素就认为作品集完整,它必须包含所有三个场景:software、office 和 music。
它也不是对作品集质量的评分,而是对发布契约的硬性验证,任何缺失都会阻止发布。
发布门禁不负责生成缺失的内容,它只负责检测并报告缺失项。
它与相邻概念的关系
发布门禁与作品集组件的关系
PortfolioRelease 数据类封装了版本号、场景集合、部分集合和工件哈希,publish 函数使用这些字段进行验证。
REQUIRED_SCENARIOS 和 REQUIRED_SECTIONS 常量定义了发布契约,测试用例根据这些契约构造输入并断言行为。
故障夹具中的缺陷是只检查场景非空,导致缺少 music 场景的作品集被错误发布。
本阶段的边界决定: 发布门禁必须对照必需集合检查场景和部分,任何缺失都会阻止发布;不能只检查非空或存在任意场景。
完成本章
发布 HeatStack Forge 1.0 和三套可复现案例。
本地实验自检
- 未开始
- 2阅读中
- 3实验已下载
- 4测试结果已读取
- 5本地自检通过
verification.json 只在当前浏览器中解析,不会上传。这里验证的是实验合同,不是服务器认证或第三方背书。
概念校准与一周复习
三道题检查你是否掌握了本章边界、交付证据和恢复方法。答案只保存在当前浏览器。