基础认知
建立模型输入、上下文和工程基础,让后续系统拥有清楚、可验证的起点。
模型接收什么信息,开发者如何建立可靠输入与工程基础?概念校准
验证与评测
- 它是什么
- 用测试、Schema、引用、状态和验收规则检查单次结果,并用数据集衡量系统表现。
- 它不是什么
- 不等于让模型对自己的输出再给一次意见;验证必须落到外部可检查证据。
入学诊断与证据地图
从三个真实场景出发,定义 Forge 的用户、边界、失败输入和第一组验收证据。
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
用不同用户、输入和成果定义软件、办公与音乐三个可验证场景
先看一个具体问题
为什么需要三个不同场景?
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) 定义,确保场景对象不可变,避免意外修改。
本阶段的边界决定: 场景目录必须包含三个场景,每个场景的用户、请求和成果都不同,但不需要包含实现细节。
把泛化受众改写为可测试的用户困境、期望成果和拒绝边界
先看一个具体问题
用户故事无限膨胀:缺少范围边界
在软件交付场景中,前端维护者经常抱怨验收标准不清晰,导致补丁无法评审。如果用户成果只描述想要的结果而不明确排除范围,团队就会不断添加新需求,故事永远无法完成。
当前 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,因为缺少范围边界的用户成果无法防止无限膨胀。
为场景定义必需输入、可选输入、输出产物和可机器检查的完成条件
先看一个具体问题
验证器把可选字段当成必需字段,导致合法请求被拒绝
你正在为 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)是场景必须生成的文件或数据。
本阶段的边界决定: 验证只报告缺失的必需输入,可选输入缺失不算错误。输出产物和验收检查必须非空,否则合同无效。
把缺信息、越权和不支持输入分类为不同状态,而不是统一抛出模糊错误
先看一个具体问题
一个模糊错误无法区分三种失败
当前 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。
把成功声明绑定到可定位、可复核且带哈希的证据记录
先看一个具体问题
缺少必需证据时,完成检查错误地报告成功
当前 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。
用已有证据而非自信评分计算能力差距,并优先安排阻塞后续工作的能力
先看一个具体问题
从证据计算差距并按依赖权重排序
当前 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 降序排列。
从能力依赖生成可执行路线,并在依赖循环时明确失败
先看一个具体问题
为什么需要拓扑排序?
在诊断模块中,多个能力之间存在依赖关系,例如工程能力依赖诊断能力,而代理循环又依赖工程能力。
如果学习路线不尊重这些依赖,学习者可能先遇到代理循环的内容,却缺少工程和诊断的基础,导致无法理解。
因此我们需要一个函数 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 产品说明、场景数据和第一组验收测试。
本地实验自检
- 未开始
- 2阅读中
- 3实验已下载
- 4测试结果已读取
- 5本地自检通过
verification.json 只在当前浏览器中解析,不会上传。这里验证的是实验合同,不是服务器认证或第三方背书。
概念校准与一周复习
三道题检查你是否掌握了本章边界、交付证据和恢复方法。答案只保存在当前浏览器。