主题
09|先跑通两个实验,再接真实世界
本章对应随书交付的 prototype/ 源码。它解决一个比“自动写一篇文章”更具体的问题:一个建议从哪里来,怎样获得执行资格,执行后如何留下证据,结果不合适时能否恢复原来的页面。
这次实际跑通的是完全离线、使用合成数据的本地预览闭环。第一轮保留一个标题变化,第二轮撤销另一个标题变化。这里的保留与撤销由预设规则和样例指标触发,不代表真实用户更喜欢某个标题,更不是搜索排名、广告收入或因果实验的证明。
9.1 需要什么,运行后会得到什么
准备 Python 3.12 或更新版本,以及一个可写的专用目录。程序只使用 Python 标准库;不必安装依赖,也不必准备 API key。实际验收环境为 Linux、Python 3.12。代码没有把开发机的绝对路径写进导入语句,因此可以把整个项目复制到其他目录再运行。
进入 prototype 文件夹,执行:
sh
python -m seo_loop demo --workdir ./demo-output终端输出 JSON 报告;工作目录新增数据库、静态站和两份报告。第一次运行应看到两个终态决策:cycle-1 为 keep,cycle-2 为 rollback。actions_used 应为 2,audit_valid 应为 true,live_seo_gain_measured 应为 false。
再执行同一条命令。程序会识别已有的实验结果,跳过已完成的动作,不增加动作计数。这一步验证的是幂等:重复接到同一个任务时,不把同一项修改执行两遍。
静态站需要通过本地服务器查看:
sh
python -m http.server 8000 --bind 127.0.0.1 --directory ./demo-output/site打开 http://127.0.0.1:8000/。绑定地址限定在本机;用完后按 Ctrl+C 退出。站点包括解释秒/毫秒的指南,以及在浏览器内运行的时间戳转换器。输入不会被发送给统计脚本或广告系统。默认页面禁止索引,示例域是 https://preview.example.invalid;它不是已发布的网站。
构建器用同级版本目录和符号链接切换站点,避免读者看到新旧文件混合的半成品。Linux 已做实际测试。Windows 下符号链接可能需要开发者模式或相应权限;这部分没有实机验收。缺少权限时使用 Linux/WSL,而不是随意删掉一致性保护。
9.2 一次 demo 到底做了哪些事
演练时钟固定在 2026 年的合成时间窗口内。日期固定的目的是让重跑结果一致,不是让过去的批准在真实系统里继续有效。真实接入必须使用可信的当前 UTC 时间,不能由调用者随意回拨时间来延长批准。
- 程序建立两个词种子和候选查询,保存用户任务、初始证据强度、竞争成本等合成特征。
- 用初始权重计算机会分数,建立
timestamps主题和两个页面计划。 - fixture AI 角色对机会、页面计划和反馈提供结构化分析。输出留在数据库供审查;它们是可重复的模拟响应,没有发生真实模型调用。
- 程序把预先写好的教学内容渲染成两个页面,保存内容版本哈希。
- 第一轮先保存修改前指标,再产生一个
update_title建议。通过规则检查后,生成仅限本地合成演练的批准记录。 - 执行器再次核对批准、页面版本、证据、预算和冷却时间,只改标题与更新时间,并保存旧版本。
- 程序读取第一轮修改后指标。CTR 相对变化超过样例门槛,任务成功率没有超过允许的下降幅度,于是保留。
- 第二轮重复同样流程。指标触发撤销规则,程序恢复第一轮保留后的页面对象,并重新构建静态站。
- 最后导出决策、机会反馈和审计快照。
这里的 AI 主导体现在多个分析节点共用结构化输入输出,而写入权限始终由确定性程序掌握。原型没有让模型从零搜索互联网、核验事实、写完整长文再自动生产发布。页面正文是教学脚手架;机会评分也是透明的固定公式。这样能独立测试决策和执行边界,不把“模型写出了看似合理的文字”混同于系统已经可靠。
初始机会分数为 0.5 × task_fit + 0.3 × evidence_strength + 0.2 × (1 − competition_cost)。这三个输入都是 0 到 1 的合成特征,没有查询真实搜索量。该公式仅用于演示排序如何进入后续流程;它不预测收入,也不是推荐给所有站点的通用评分器。
9.3 三条命令及其边界
| 命令 | 输入与动作 | 产物与验收 | 失败时怎么处理 |
|---|---|---|---|
demo --workdir DIR | 使用固定合成数据,建立状态并执行两轮本地实验 | 两个终态决策、静态站、report.json、audit.json | 保留整个目录,查看错误,不要反复创建新目录掩盖问题 |
status --workdir DIR | 读取状态并验证审计链 | JSON 当前状态;不会自动恢复待渲染文件 | 审计链失败时停止后续操作,保留数据库调查 |
recover --workdir DIR | 若 render_pending 为 true,从数据库当前页面重新渲染 | 待渲染标记清除,页面与数据库恢复一致 | 修复磁盘空间、写权限或站点目录问题,确认并移除 STOP 后重试 |
这三条命令都没有生产发布、提交索引、配置广告、读取真实账户或调用付费模型的步骤。CLI 也没有通用“执行任意模型动作”命令。可选真实适配器由 Python 代码单独调用,接入前仍要经过本书第十章的权限和数据验收。
工作目录应只放本次演练内容。site 是指向同级发布目录的链接,不能只复制这个链接就当作站点备份。要带走可直接阅读的静态文件,可以复制其指向的完整目录;要恢复整个实验,还要保留数据库与审计信息。
9.4 数据库如何把动作连成一条证据链
SQLite 是本地权威状态,JSON 报告只是某一刻的导出。数据库使用 WAL 日志和完整同步设置;引擎在工作目录持有操作系统文件锁,拒绝另一个引擎同时修改同一目录。进程异常退出后锁由操作系统释放,锁文件可以继续存在,不要仅凭“文件还在”判断任务仍在运行。
| 表 | 核心字段 | 用途 |
|---|---|---|
entities | kind, id, data, hash | 不可原地改写的关键词、证据、事实、页面计划、建议、决策等 JSON 对象 |
pages | id, version, data | 每个页面当前生效的对象 |
versions | hash, page_id, data | 页面历史对象,供核对与回滚 |
metrics | id, revision, page_id, data, hash | 同一观测窗口的不同数据修订版 |
approvals | id, proposal_hash, page_id, action, version, expires_at, scope, used_by | 批准绑定与单次消费记录 |
actions | id, proposal_hash, page_id, before_hash, after_hash, status, data | 执行过什么、改前改后是什么、目前结果如何 |
experiments | id, page_id, status, data | 观察中或已终结的实验 |
audit | seq, event, at, data, previous_hash, hash | 按顺序关联的事件哈希链 |
meta | key, value | 动作预算、冷却时间、待渲染标记等运行状态 |
entities 中同一种对象和同一个 ID 如果出现不同内容,程序会拒绝覆盖。指标则允许增加 revision 保存修订;相同 ID、相同修订号却改变数据会被拒绝。哈希基于稳定排序的 JSON 计算,便于重复核验。
哈希链能发现部分不一致或意外篡改,不能替代外部可信日志。拥有数据库写权限的人也可能重建整条链。生产环境仍需权限隔离、异地备份、保留策略和独立审计;原型没有把 SQLite 变成不可伪造的公证系统。
页面状态的流转很短:动作记录先是 applied,相应实验是 observing。数据够用后,二者进入 kept 或 rolled_back。如果数据缺失、过旧或不足,判定为 wait,实验仍留在 observing;程序不会用零补齐,再装作观察到了下降。
9.5 模型建议为什么不能直接变成写入
执行器只认识如下字段,不能增加路径、脚本、任意 URL 或部署参数:
json
{
"id": "cycle-1",
"page_id": "timestamp-tool",
"action": "update_title",
"expected_version": "由当前页面计算的64位SHA-256值",
"title": "Unix timestamp converter: seconds, milliseconds and UTC",
"evidence_ids": ["evidence-timestamp-tool"],
"hypothesis": "Explicit task and unit wording may help fixture users choose the relevant page."
}上面是字段说明,不是可直接执行的合法样例,因为 expected_version 必须是实际哈希。examples/proposal.json 是演练中导出的合法实例。
校验按几层进行:先确认严格 JSON 没有重复键、非有限数值或额外字段,再检查字段类型、ID 格式和动作白名单。标题必须是受限长度的纯文本;不能带标签、换行或空字符。随后读取数据库,确认页面存在、目标版本没有变化、证据属于同一页面且标明合成性质。
批准绑定的是整个建议内容的哈希,另保存目标、动作、版本、失效时间和范围。只要标题、假设或证据 ID 中任何一项改变,原批准都不能用于这份新建议。批准过期、已消费或指向别的页面时,同样停止。完全相同的动作 ID 重试返回已记录结果;相同 ID 配上不同内容则拒绝。
这些检查防止建议越权,但不能判断一个标题是否误导,或引用是否真的支持结论。模型分析里的“为什么”仍要接受编辑核验。真实发布前,应让具备权限的人看到目标、差异、证据和风险,并由可信服务签发批准;不要沿用 demo 自动生成本地批准的方法。
9.6 什么样的数据才允许作判断
核心指标对象包含 id、page_id、version_hash、observed_at、period_start、period_end、final、revision、impressions、clicks、task_successes、task_attempts、synthetic。时间必须带时区,次数必须是非负整数,点击不能超过展示,任务成功次数不能超过尝试次数。
原型把 final 当作上游已经完成的数据成熟度判断。在真实来源中,不同 API 对最终状态的支持不同;不能因为这个字段存在,就认为 GA4 天然提供同样的最终状态标志。真实适配层还要保留抽样、隐私阈值、分页截断、来源时区、查询过滤和数据修订信息。第七章说明了这些差异;核心演练没有把所有外部质量标志平铺进指标对象。
开始修改之前,程序要求修改前数据属于当前页面版本、标明最终状态、没有来自未来、窗口足够长且有足够样本。修改后评估还要确认两组窗口等长且不重叠、版本一致、修改后窗口不跨过修改时间。数据过旧、低样本、分母为零或仍在修订时,不自动保留或撤销。
这份初始策略使用:至少 100 次展示、20 次任务尝试、7 天窗口,数据窗口结束距判断时间不超过 14 天。同页动作间隔至少 7 天。保留要求 CTR 相对变化至少 10%,任务成功率下降不超过 2 个百分点。这些数字是让演练分支可测试的初始门槛,不是统计功效计算,也没有季节性校正或对照组。
因此,“达到门槛”只能说明这次合成输入经过了规则,不说明真实变化由标题造成。生产中的低流量站点可能需要更长观察窗口;快速修改也可能让归因更难。应预先选择主要目标、保护指标、最小可检测变化和停止规则,而不是见到数据后再挑有利门槛。
修改前指标若出现新修订,原实验暂停,要求重新建立基线并审查。已结束的实验不会被悄悄改写;如果后来的指标修订使当时依据变化,status 的 data_quality_alerts 会提示人工复核。这保护了历史决策的可解释性,也避免把“当时合理”混成“永远正确”。
9.7 故障演练:确认程序停在哪里
先运行全部自动测试:
sh
python -m unittest discover -s tests -v只运行核心测试时使用:
sh
python -m unittest discover -s tests -p test_core.py -v测试在临时目录中制造故障,不需要修改你的正式演练数据库。以下是核心用例和可观察的结果:
| 故障 | 用例 | 应看到什么 |
|---|---|---|
| 重复触发同一工作流 | test_double_run_idempotency | 两次报告一致,动作仍为 2 |
| 同一动作 ID 换了内容 | test_changed_id_is_rejected | 拒绝,不覆盖旧动作 |
| 批准后改标题 | test_approval_binds_content | 哈希不一致,拒绝写入 |
| 批准过期/页面已变 | test_expired_approval、test_stale_page_version | 拒绝陈旧批准或陈旧版本 |
| 缺少、暂定、过旧指标 | test_missing_metrics_wait、test_provisional_metrics_wait、test_stale_metrics_wait | 留在观察态,不伪造零值 |
| 样本过少/分母为零 | test_low_volume_wait、test_zero_denominators_wait | 暂停判断,不出现除零或假结论 |
| 修改前数据被修订 | test_revised_baseline_wait | 要求重新审查基线 |
| 完成后数据被修订 | test_terminal_revision_reported | 历史结论不改写,报告新增告警 |
| 目录穿越、脚本、额外动作 | test_unsafe_path、test_shell_action_and_extra_fields | 模式或白名单校验拒绝 |
| 证据不存在/格式错误 | test_missing_evidence、test_invalid_evidence_type_rejected | 无法得到执行资格 |
| 冷却期、预算、停止开关 | test_cooldown、test_budget、test_kill_switch | 禁止新的实验动作 |
| 修改入库后构建崩溃 | test_crash_recovery | 保留待渲染标记,恢复后不重复扣预算 |
| 需要回滚 | test_rollback_restores_old_artifacts | 页面哈希和全站文件字节恢复到实验前 |
| 同目录启动第二个引擎 | test_workdir_lock | 第二个引擎停止,避免状态互相覆盖 |
| 审计数据被改写 | test_audit_tamper_detected | 验证失败,报告不能继续冒充完整证据链 |
| 重试一直超时 | test_retry_bounded | 达到次数上限后返回错误 |
停止开关也可以手动演练。在 demo-output 目录创建一个空文件,名称必须是 STOP。再运行 demo,程序应明确报告停止。确认后删除这个文件,再运行即可恢复。它阻止实验操作和待渲染恢复,并不会卸载已启动的 HTTP 服务;如果你还开着服务器,需要另外按 Ctrl+C。
恢复演练的核心问题是:数据库提交和文件写入不是同一个事务。原型先把希望生效的页面写入数据库,同时标记 render_pending=true;构建器完成原子目录切换后,再清除标记。若进程在中间退出,下次恢复会以数据库为准重新渲染。它不能解决生产 CDN、远程发布和搜索引擎缓存的分布式一致性;那些需要独立的发布状态与回滚协议。
重试只用于有明确幂等性的读取,最多五次。不要把这个工具函数套在付费请求、发信或结果不确定的外部写入上。真实适配器还应区分“确定未执行”和“可能已经成功但响应丢失”,再决定是否重试。
9.8 预算到底限制了什么
核心默认最多四个本地标题动作;演练用了两个。计数与动作入库在同一 SQLite 事务里完成,重复动作不会重复扣减。输入策略中的无穷值、NaN、负数和非法类型会被拒绝,避免门槛被特殊数值绕过。
这个预算没有覆盖真实 token 成本、第三方数据费用、服务器账单或历史站点版本占用的磁盘空间。可选 provider 的调用限制与核心动作预算是不同层。生产系统应将费用、请求额度、任务时长、输出数量和磁盘保留策略分开控制,并把“还有钱”与“还有权限”分别判断。
examples/policy.json 用于说明默认参数;CLI 不自动读取它。需要做程序级演练时,可以读取 JSON 并传给 Engine(workdir, policy=...)。不要改低门槛只为让实验出现“成功”。
9.9 已实测部分与仍然缺少的部分
已实测的范围是:离线运行、SQLite 持久化、严格对象校验、两轮本地实验、重复执行、版本与批准绑定、缺失/修订/低样本处理、停止开关、预算、受限重试、构建恢复,以及按字节核对的回滚。源码旁的自动测试和 qa/core/ 保存了可复核的执行结果。
未实测真实账户登录、API 计费、真实数据导入、生产域名、DNS、CDN、索引提交、搜索收录、广告审批、收入或排名变化。真实适配器的 mock HTTP 测试证明的是接口处理分支,没有证明外部服务在你的账号上可用。没有真实事实语义核验、关键词市场需求验证、随机实验、统计显著性分析或全自动长文生产。
原型也不是多租户系统。虽然同目录锁能防止两个本地引擎同时写入,它没有队列、分布式锁、RBAC、审批签名、跨机器恢复、秘密管理或备份调度。不要把函数可导入理解成这些生产控制已经存在。
下一步应当从一份真实、只读、授权清楚的数据快照开始:保留来源与质量元数据,让规则给出“可判断”或“不能判断”,由人核对后再扩大范围。先验证这些连接是否可信,再考虑增加自动生成数量。