← 工程跃迁 / 第 01 章
架构定位

基础认知

建立模型输入、上下文和工程基础,让后续系统拥有清楚、可验证的起点。

模型接收什么信息,开发者如何建立可靠输入与工程基础?

主要层

01 · 基础认知

依赖能力

不要求先完成其他模块

为后续提供

受这些层约束

概念校准

验证与评测

它是什么
用测试、Schema、引用、状态和验收规则检查单次结果,并用数据集衡量系统表现。
它不是什么
不等于让模型对自己的输出再给一次意见;验证必须落到外部可检查证据。

入学诊断与证据地图

从三个真实场景出发,定义 Forge 的用户、边界、失败输入和第一组验收证据。

本章只做一件事写出 Forge 产品说明并让第一组场景测试通过。
开始前,Forge 已经具备这是 Forge 的起始状态。
完成后,Forge 将能够创建真实 Forge 产品说明、场景数据和第一组验收测试。
卡住时的最小恢复点先完成软件交付场景的一条成功输入和一条拒绝输入。
下载本章实验仓库Python 3.12 · pytest · Pydantic · SQLite · deterministic Mock
阶段能力链
01用不同用户、输入和成果定义软件、办公与音乐三个可验证场景02把泛化受众改写为可测试的用户困境、期望成果和拒绝边界03为场景定义必需输入、可选输入、输出产物和可机器检查的完成条件04把缺信息、越权和不支持输入分类为不同状态,而不是统一抛出模糊错误05把成功声明绑定到可定位、可复核且带哈希的证据记录06用已有证据而非自信评分计算能力差距,并优先安排阻塞后续工作的能力07从能力依赖生成可执行路线,并在依赖循环时明确失败
每一步只增加一种可验证能力;后一步建立在前一步已经通过的代码和测试上。

概念校准\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

STAGE 01

用不同用户、输入和成果定义软件、办公与音乐三个可验证场景

先看一个具体问题

为什么需要三个不同场景?

Forge 是一个多场景智能体平台,但当前代码库中只有 forge/init.py,没有任何场景定义。测试 test_catalog_covers_three_distinct_scenarios 要求 scenario_catalog() 返回恰好包含 software、office、music 三个键的字典,且每个场景的用户互不相同,请求和成果都非空。

如果只定义两个场景,测试会失败,因为断言 set(catalog) == {“software”, “office”, “music”} 不成立。你需要创建 forge/scenarios.py,定义一个 Scenario 数据类和一个 scenario_catalog() 函数,返回三个不同的场景实例。

先做判断

在开始实现之前,请预测:如果 scenario_catalog() 只返回 software 和 office 两个场景,测试 test_catalog_covers_three_distinct_scenarios 会怎样?

  • 测试通过,因为两个场景已经足够。
  • 测试失败,因为断言要求恰好三个键。
  • 测试失败,因为缺少 music 场景导致用户数不足。
  • 测试通过,因为用户不同即可。

判断依据: 正确答案是第二个选项。测试明确断言 set(catalog) == {“software”, “office”, “music”},缺少 music 会导致集合不匹配,测试失败。

它是什么

场景目录是什么

场景目录是一个从场景 ID 到 Scenario 对象的映射,每个 Scenario 包含 id、user、request 和 outcome 四个字段。它用于展示 Forge 在不同领域(软件、办公、音乐)中的适用性。

场景目录是静态的、确定性的:每次调用 scenario_catalog() 都返回相同的三个场景,不依赖外部状态或随机性。

它不是什么

场景目录不是什么

场景目录不是完整的实现规范,它只描述用户、请求和可观察成果,不包含具体的执行步骤或技术细节。

场景目录不是动态生成的,也不是从数据库或配置文件中读取的;它是硬编码在 forge/scenarios.py 中的常量数据。

它与相邻概念的关系

与其他组件的关系

场景目录是 Forge 产品说明的一部分,为后续的验收测试提供输入。测试 test_catalog_covers_three_distinct_scenarios 和 test_scenarios_describe_observable_outcomes 直接验证场景目录的结构和内容。

Scenario 数据类使用 dataclass(frozen=True) 定义,确保场景对象不可变,避免意外修改。

本阶段的边界决定: 场景目录必须包含三个场景,每个场景的用户、请求和成果都不同,但不需要包含实现细节。

STAGE 02

把泛化受众改写为可测试的用户困境、期望成果和拒绝边界

先看一个具体问题

用户故事无限膨胀:缺少范围边界

在软件交付场景中,前端维护者经常抱怨验收标准不清晰,导致补丁无法评审。如果用户成果只描述想要的结果而不明确排除范围,团队就会不断添加新需求,故事永远无法完成。

当前 forge/users.py 尚未创建,测试 test_user_outcome_requires_a_scope_boundary 和 test_complete_user_outcome_is_accepted 因导入失败而无法运行。我们需要实现 UserOutcome 数据类和 validate_user_outcome 验证函数,强制每个用户成果至少包含一个 out_of_scope 项。

先做判断

如果 validate_user_outcome 只检查 role 和 desired_result 非空,而允许 out_of_scope 为空元组,会发生什么?

  • 测试 test_user_outcome_requires_a_scope_boundary 会通过,因为空元组也是合法输入。
  • 测试 test_user_outcome_requires_a_scope_boundary 会失败,因为缺少范围边界时应该抛出 ValueError。
  • 测试 test_complete_user_outcome_is_accepted 会失败,因为空元组会导致验证错误。
  • 两个测试都会通过,因为验证函数不需要检查 out_of_scope。

判断依据: 正确答案是第二个选项。测试 test_user_outcome_requires_a_scope_boundary 使用 pytest.raises 期望抛出 ValueError,如果验证函数不检查 out_of_scope,就不会抛出异常,测试失败。这提醒我们范围边界是用户成果的必要组成部分。

它是什么

用户成果是带边界的可测试契约

UserOutcome 是一个冻结数据类,包含 scenario_id、role、pain、desired_result 和 out_of_scope 五个字段。它描述特定场景下某个角色的痛点、期望结果以及明确排除的范围。

validate_user_outcome 函数检查 role、pain、desired_result 非空,并且 out_of_scope 至少包含一个元素。只有满足这些条件的用户成果才能通过验证,成为后续工作的可靠输入。

它不是什么

用户成果不是无限愿望清单

用户成果不是只列出想要的结果而不设限制。如果允许 out_of_scope 为空,用户故事就会无限膨胀,团队无法确定何时完成。

用户成果也不是对模型输出的自我评价。验证必须依赖外部可检查的字段和规则,而不是主观意见。

它与相邻概念的关系

验证与用户成果的关系

验证函数 validate_user_outcome 是用户成果的守门人。它确保每个用户成果都包含明确的排除边界,从而防止范围蔓延。

测试 test_user_outcome_requires_a_scope_boundary 和 test_complete_user_outcome_is_accepted 分别验证缺失边界时抛出异常和完整成果被接受,形成对验证函数的双重保障。

本阶段的边界决定: 当 out_of_scope 为空元组时,validate_user_outcome 必须抛出 ValueError,因为缺少范围边界的用户成果无法防止无限膨胀。

STAGE 03

为场景定义必需输入、可选输入、输出产物和可机器检查的完成条件

先看一个具体问题

验证器把可选字段当成必需字段,导致合法请求被拒绝

你正在为 Forge 场景定义输入输出合同。合同需要明确哪些输入是必需的,哪些是可选的,以及必须产生哪些输出产物和验收检查。

当前代码中,IOContract.validate() 方法错误地从 optional_inputs 计算缺失字段,而不是从 required_inputs 计算。这导致一个只包含必需字段的合法请求被报告缺少可选字段 deadline,而真正缺少必需字段 workspace 的请求却被忽略。

测试 test_only_required_inputs_are_reported_missing 期望:当提供 request 和 workspace 时返回空列表;当只提供 request 时返回 [‘workspace’]。但当前实现返回 [‘deadline’] 和 [],完全相反。

先做判断

在 IOContract.validate() 中,缺失字段列表应该从哪个输入集合计算?

  • required_inputs
  • optional_inputs
  • required_inputs 和 optional_inputs 的并集
  • required_inputs 和 optional_inputs 的交集

判断依据: 正确答案是 required_inputs。验证的目的是确保所有必需输入都存在,可选输入缺失不应报错。如果从 optional_inputs 计算,就会把可选字段当成必需,导致合法请求被拒绝。

它是什么

输入输出合同是场景的机器可检查边界

IOContract 是一个不可变数据类,包含四个元组:required_inputs(必需输入)、optional_inputs(可选输入)、output_artifacts(输出产物)、acceptance_checks(验收检查)。

validate(payload) 方法接收一个字典,返回缺失的必需输入列表。如果 output_artifacts 或 acceptance_checks 为空,则抛出 ValueError。

它不是什么

合同不是对所有字段的全面验证

合同不检查可选输入是否存在,也不检查输入值的类型或格式。它只报告缺失的必需输入。

合同不负责执行验收检查,只确保验收检查列表非空。

它与相邻概念的关系

合同与场景、用户的关系

场景(scenarios.py)和用户(users.py)是合同的使用者。合同定义了场景执行前必须满足的输入条件。

验收检查(acceptance_checks)是场景完成后必须通过的机器可检查条件,输出产物(output_artifacts)是场景必须生成的文件或数据。

本阶段的边界决定: 验证只报告缺失的必需输入,可选输入缺失不算错误。输出产物和验收检查必须非空,否则合同无效。

STAGE 04

把缺信息、越权和不支持输入分类为不同状态,而不是统一抛出模糊错误

先看一个具体问题

一个模糊错误无法区分三种失败

当前 forge/failures.py 文件尚不存在,测试文件 tests/test_stage.py 在导入 IntakeStatus 和 classify_intake 时直接报 ModuleNotFoundError,导致测试收集阶段就失败。

即使我们创建了文件并实现一个简单的分类函数,如果只返回一个笼统的错误状态,测试 test_destructive_request_requires_approval 会断言 destructive=True 的请求必须返回 NEEDS_APPROVAL,而 test_missing_and_unsupported_inputs_are_distinct 会断言缺少 workspace 时返回 NEEDS_CLARIFICATION、media_type 为 video 时返回 UNSUPPORTED。

因此,学习者必须实现一个能区分不同失败类别的分类器,而不是用一个模糊错误掩盖所有问题。

先做判断

在实现 classify_intake 之前,请预测:对于 payload={“request”:“clean repo”,“workspace”:“repo”,“destructive”:True},分类器应该返回什么状态?

  • READY,因为请求和 workspace 都存在
  • NEEDS_APPROVAL,因为 destructive 为 True
  • NEEDS_CLARIFICATION,因为缺少其他字段
  • UNSUPPORTED,因为 destructive 不是支持的媒体类型

判断依据: 正确答案是 NEEDS_APPROVAL。destructive 操作具有潜在破坏性,必须经过审批边界,不能因为基本字段齐全就直接标记为 READY。

它是什么

失败分类是一个决策树

classify_intake 按固定顺序检查输入:先检查必要字段是否缺失,再检查是否属于破坏性操作,最后检查媒体类型是否受支持。

每个检查对应一个明确的 IntakeStatus 枚举值,使得调用方能够根据状态采取不同后续动作。

它不是什么

它不是笼统的错误处理

失败分类不是返回一个通用的 error 字符串或抛出异常,而是用枚举值精确表达失败原因。

它也不负责执行审批或澄清流程,只负责标记状态,后续处理由其他组件完成。

它与相邻概念的关系

状态与检查顺序的关系

检查顺序决定了优先级:缺失字段检查在最前,因为缺少必要信息时无法进行后续判断;破坏性检查在媒体类型检查之前,因为破坏性操作即使媒体类型受支持也必须审批。

IntakeStatus 枚举定义了所有可能的状态,classify_intake 的返回值必须是其中之一。

本阶段的边界决定: 分类器只根据输入 payload 的静态字段做出判断,不访问外部系统,也不修改任何状态;它只负责返回一个明确的 IntakeStatus。

STAGE 05

把成功声明绑定到可定位、可复核且带哈希的证据记录

先看一个具体问题

缺少必需证据时,完成检查错误地报告成功

当前 forge/evidence.py 文件尚未创建,导致测试收集阶段直接报错 ModuleNotFoundError,因为测试文件尝试导入 all_checks_pass 和 record_evidence。

即使我们创建了文件并实现一个简单的 all_checks_pass,如果它只检查已存在记录的 passed 字段,就会忽略 required 集合中缺失的检查 ID。

在故障夹具中,all_checks_pass 对只有 schema 记录、缺少 failure-case 的输入返回 True,而测试 test_missing_required_evidence_prevents_success 期望返回 False。

这个缺陷的根源是完成判断只基于现有记录,没有对照声明的必需检查集合,导致部分证据被错误地视为完整。

先做判断

如果 all_checks_pass 只检查传入记录列表中每个记录的 passed 字段,当 required 集合包含一个不在记录中的检查 ID 时,它会返回什么?

  • 返回 True,因为所有现有记录都通过了
  • 返回 False,因为缺少必需检查
  • 抛出 KeyError,因为找不到该检查 ID
  • 返回 None,因为无法判断

判断依据: 正确答案是返回 True。如果实现只遍历现有记录并检查 passed,它不会意识到 required 中有缺失的 ID,因此会错误地认为所有必需检查都已通过。这正是故障夹具中的缺陷。

它是什么

证据记录与完成检查的契约

EvidenceRecord 是一个不可变数据类,包含 check_id、artifact、digest 和 passed 四个字段,其中 digest 是对内容字节进行 SHA-256 哈希得到的十六进制字符串。

record_evidence 函数负责创建证据记录,它要求 check_id 和 artifact 非空,否则抛出 ValueError,然后计算内容哈希并返回记录。

all_checks_pass 函数接收记录列表和 required 集合,必须验证 required 中的每个检查 ID 都存在于记录中且对应记录的 passed 为 True。

这个契约确保成功声明必须由完整且通过的证据集支持,而不是仅由部分记录支持。

它不是什么

不是仅检查现有记录

all_checks_pass 不能只检查传入记录列表中每个记录的 passed 字段,因为这样会忽略 required 集合中缺失的检查 ID。

它也不是对证据内容进行语义验证,而是仅根据检查 ID 的存在和 passed 布尔值进行形式化判断。

证据摘要不是基于文件名或路径计算的,而是基于内容字节,因此相同内容在不同文件名下会产生相同的 digest。

它与相邻概念的关系

证据、测试与验收的关系

测试 test_missing_required_evidence_prevents_success 构造一个只有 schema 记录的列表,并传入 required 集合 {“schema”,“failure-case”},期望 all_checks_pass 返回 False。

测试 test_evidence_digest_is_content_addressed 验证相同内容在不同 artifact 名称下产生相同 digest,确保摘要由内容决定。

故障夹具中的缺陷实现只检查现有记录的 passed,导致测试失败,修复后必须同时检查 required 集合的包含关系和每个必需记录的 passed 状态。

本阶段的边界决定: all_checks_pass 必须要求 required 集合是记录中 check_id 集合的子集,并且每个必需记录的 passed 为 True;任何缺失或未通过的必需检查都会导致返回 False。

STAGE 06

用已有证据而非自信评分计算能力差距,并优先安排阻塞后续工作的能力

先看一个具体问题

从证据计算差距并按依赖权重排序

当前 forge 目录中已有 scenarios.py、users.py、io_contracts.py、failures.py 和 evidence.py,但缺少 gaps.py,导致测试收集阶段直接报错 ModuleNotFoundError。

你需要创建 forge/gaps.py,实现 Capability 数据类和 assess_gaps 函数,使测试 test_blocking_gaps_are_prioritized 和 test_demonstrated_capability_is_not_a_gap 通过。

测试要求 assess_gaps 返回缺失能力 ID 列表,并且必须按依赖权重降序排列,权重高的能力排在前面。

同时,如果某个能力的证据水平已经达到或超过要求水平,它就不应出现在结果中。

先做判断

在实现 assess_gaps 之前,请预测:如果直接按依赖权重升序排序缺失能力,测试 test_blocking_gaps_are_prioritized 会得到什么结果?

  • 返回 [‘python’, ‘slides’],测试通过
  • 返回 [‘slides’, ‘python’],测试失败
  • 返回 [‘http’, ‘python’, ‘slides’],测试失败
  • 返回 [‘python’, ‘slides’, ‘http’],测试失败

判断依据: 正确答案是返回 [‘slides’, ‘python’],测试失败。因为升序排序会把权重小的 slides 放在前面,而测试期望权重大的 python 在前。

它是什么

能力差距评估模型

assess_gaps 接收能力列表和证据水平字典,通过比较每个能力的 required_level 与证据水平,筛选出尚未达到要求的能力。

筛选出的缺失能力需要按照依赖权重降序排列,权重高的能力优先处理,因为它们可能阻塞后续模块的学习。

Capability 数据类包含 id、required_level 和 dependency_weight 三个字段,其中 dependency_weight 表示该能力对其他能力的依赖影响程度。

它不是什么

不是简单的列表过滤

这个模型不是仅仅返回所有能力 ID,而是只返回证据水平低于要求水平的能力。

它也不是按字母顺序或任意顺序排列,而是必须按照依赖权重降序排列,确保高优先级能力先被处理。

它不涉及修改证据水平或能力定义,只负责计算差距并排序。

它与相邻概念的关系

与其他模块的关系

assess_gaps 依赖 Capability 数据类来定义能力结构,并使用 evidence_levels 字典来获取当前证据水平。

排序结果直接影响后续学习路径的规划,权重高的能力应优先安排学习任务。

该模块与 evidence.py 配合,证据水平来自实际测试或评估结果,而不是主观评分。

本阶段的边界决定: 当证据水平等于或超过 required_level 时,该能力不算差距;排序时只考虑缺失能力,且必须按 dependency_weight 降序排列。

STAGE 07

从能力依赖生成可执行路线,并在依赖循环时明确失败

先看一个具体问题

为什么需要拓扑排序?

在诊断模块中,多个能力之间存在依赖关系,例如工程能力依赖诊断能力,而代理循环又依赖工程能力。

如果学习路线不尊重这些依赖,学习者可能先遇到代理循环的内容,却缺少工程和诊断的基础,导致无法理解。

因此我们需要一个函数 build_route(),它接收能力依赖图,返回一个满足所有依赖的拓扑顺序。

当依赖图中存在循环时,例如 A 依赖 B 且 B 依赖 A,任何线性顺序都无法同时满足两者,必须明确拒绝。

先做判断

给定依赖图 {“diagnostic”: set(), “engineering”: {“diagnostic”}, “agent-loop”: {“engineering”}},如果直接按字母顺序输出,会得到什么结果?

  • [‘agent-loop’, ‘diagnostic’, ‘engineering’]
  • [‘diagnostic’, ‘engineering’, ‘agent-loop’]
  • [‘engineering’, ‘diagnostic’, ‘agent-loop’]
  • [‘diagnostic’, ‘agent-loop’, ‘engineering’]

判断依据: 按字母顺序输出会得到 [‘agent-loop’, ‘diagnostic’, ‘engineering’],但 agent-loop 依赖 engineering,engineering 依赖 diagnostic,所以 agent-loop 出现在最前面违反了依赖关系。

它是什么

拓扑排序是依赖驱动的线性化

拓扑排序将依赖图中的节点排成线性序列,使得每条依赖边从先决条件指向后继节点。

在 build_route() 中,我们维护一个剩余节点集合,每次只输出那些依赖集合为空的节点,然后从其他节点的依赖集合中移除这些已输出节点。

这个过程重复直到所有节点输出,如果某次迭代没有就绪节点但仍有剩余节点,说明存在循环依赖。

它不是什么

拓扑排序不是任意顺序

拓扑排序不是按字母顺序、插入顺序或随机顺序输出节点,而是必须满足所有依赖约束。

它也不是深度优先搜索或广度优先搜索的直接结果,尽管这些算法可以辅助实现拓扑排序。

拓扑排序不保证唯一性:当多个节点同时就绪时,可以选择任意顺序,但必须保持依赖关系。

它与相邻概念的关系

依赖图与路线的关系

依赖图是输入,路线是输出;路线中的每个节点必须在其所有依赖节点之后出现。

如果依赖图中存在循环,则不存在有效的拓扑排序,因此 build_route() 必须抛出 ValueError。

在诊断模块中,能力依赖图由 forge/gaps.py 等模块提供,build_route() 生成的路线将指导后续学习阶段。

本阶段的边界决定: 当依赖图中存在循环时,必须抛出 ValueError 并包含 ‘cycle’ 字样,而不是返回部分路线或无限循环。

完成本章

创建真实 Forge 产品说明、场景数据和第一组验收测试。

FORGE / LOCAL CHECK

本地实验自检

  1. 未开始
  2. 2阅读中
  3. 3实验已下载
  4. 4测试结果已读取
  5. 5本地自检通过

verification.json 只在当前浏览器中解析,不会上传。这里验证的是实验合同,不是服务器认证或第三方背书。

概念校准与一周复习

三道题检查你是否掌握了本章边界、交付证据和恢复方法。答案只保存在当前浏览器。

  1. 1哪一项最能证明你真正完成了「入学诊断与证据地图」?
  2. 2关于「验证与评测」,哪一种理解最准确?
  3. 3实验卡住时,哪个动作是本章建议的最小恢复点?