DEMO · 五步走完闭环
不用接上游,也不用模型密钥,就能看它转一圈。
此前要理解这套机制,得同时具备上游接入、模型密钥、公网可达三个条件。 现在启动服务、打开一个页面、点五个按钮就够了。 本页的所有数字与产物取自一次真实运行,不是示意。
怎么跑
两条命令,一个页面。
# 设一个口令,本机演示用什么都行,正式部署务必用强随机值
export BIANMU_API_TOKEN=<你设的口令>
make serve
浏览器打开 http://127.0.0.1:8040/demo,用同一个口令登录, 依次点五个按钮即可。每点一步,页面显示该步的真实产物。
演示数据写在独立目录里,绝不碰真实埋点数据——后者是不可追补的。 页面顶部固定显示当前演示目录,「重置演示」只清这个目录。 有一条测试专门盯着这件事:演示跑完,真实目录不能多出任何日志文件。
五步分别在干什么
1 · 灌入数据
一个模拟上游的脚本假装成编目系统:产出 14 条内容的字段值(其中刻意混入错误), 模拟编目员校对,然后通过真实的 HTTP 接口把两条事件推过来。
走接口而不是直接写文件是刻意的——演示跑通就等于证明了上游接入这条路是通的, 这个脚本本身就是一份可运行的接入示例。
真实运行结果
一条修正事件长这样:
《山河故人令》 foreign_title
「Shan He Gu Ren Ling」 → 「Legend of the Riverlands」 原因:来源错误
两条事件缺一不可。只有修正事件,你能算出「这个月改了 23 次」, 却算不出「修正率降了没有」——吞吐量一变就不可比。完成事件记录每条内容 各字段有没有值、有没有被改动,它才是分母。
2 · 差异分析
按字段聚合,看哪些字段错得最多、主要原因与来源是什么。全程不用模型。
真实运行结果 —— 三种注入的错误模式被如实排了出来
| 字段 | 修正数 | 主因 | 主要来源 |
|---|---|---|---|
| foreign_title | 10 | 来源错误 | douban_snapshot |
| year | 9 | 格式不符 | baike_snapshot |
| overseas_platform | 4 | 漏抓补全 | douban_snapshot |
三类数量刻意不同(10 / 9 / 4),排行才有真实区分度—— 如果样本设计成每类一样多,这一步就看不出它在工作。
3 · 提取规则
两步走:先由主要修正原因确定性地选定规则类型(这一步不用模型), 再让模型只填该类型 schema 内的几个参数。
把模型的自由度从「写一段话」收窄到「填几个值」,幻觉空间随之收窄, 而且结构化参数天然可校验——编出一个不存在的数据源名,入库时当场被拒。
真实运行结果 —— 三种错误模式各自提出对应类型的规则
| 类型 | 字段 | 回流层 | 参数 |
|---|---|---|---|
| source_priority | foreign_title | L1 | order: [douban_api, baike_snapshot] |
| normalize | year | L3 | mapping: {"2020年": "2020", …} |
| fallback_source | overseas_platform | L4 | when: empty, sources: [douban_api] |
默认不调模型(dry-run),所以演示不依赖任何密钥。 配了密钥可以勾选真实模型,顺便看语义自测如何拦下不成立的规则。
4 · 审核放行
这一步刻意不能自动跳过。演示时最容易图省事把它自动化, 但那会传达完全错误的信息——错误规则一旦生效会被批量放大, 且因为它是「系统行为」而非个例错误,事后复核反而更难发现。
所以必须有人填写审核人姓名并点击。未署名时流转直接被拒。
放行后规则从「草案」走到「灰度 10%」——先放一成流量,验证有效再谈全量。
5 · 看效果
把生效规则渲染成上游可加载的规则包。这是整个系统对外的最终产物。
真实运行产出的规则包(节选)
{
"schema": "bianmu-ruleset/1",
"version": 1,
"rules": [
{ "type": "source_priority", "field": "foreign_title", "layer": "L1",
"canary_pct": 10,
"params": { "order": ["douban_api", "baike_snapshot"] } },
{ "type": "fallback_source", "field": "overseas_platform", "layer": "L4",
"canary_pct": 10,
"params": { "when": "empty", "sources": ["douban_api"] } }
]
}
上游加载它,按类型执行、按灰度比例抽样。参考实现只依赖标准库,可直接抄。
演示之外的三页
五步走完之后,工作台另外三页承接的是日常使用。它们各自在开头说明「这一页回答什么问题」, 不需要先看文档:
| 页面 | 回答什么 |
|---|---|
| 看板 | 人工到底还要改多少、最费人力的是哪个字段、规则生效后有没有变好。 结论先行,再给明细与按占比的条形图。 |
| 修正录入 | 手工补录一条修正。先选剧目,再对着 AI 的产出值逐字段改, 可疑项直接标出原因——而不是把一整套字段摊开让人从头填。 |
| 运行状态 | 东西在不在跑、数据在哪有多少、配置是否齐全。只读。 |
运行状态页刻意不做启停按钮。工作台就是被控制的那个服务本身—— 「停止」点下去页面自己就没了,「启动」在服务没跑时根本打不开。 更要紧的是,让网页能杀进程等于给这个服务加一条可执行系统操作的攻击面, 换来的却只是省下切一次终端。启停命令以可复制文本给出,由人在终端执行。
这一页也只显示密钥「已配置 / 未配置」,绝不显示内容本身。
演示数据里埋了什么
14 条剧集,注入三种真实场景里会出现的错误,各自对应一类规则:
| 字段 | 错误形态 | 修正原因 | 应提出的规则类型 |
|---|---|---|---|
| foreign_title | 拼音转写而非官方英文名 | 来源错误 | source_priority |
| year | 带「年」「月」等杂质 | 格式不符 | normalize |
| overseas_platform | 留空 | 漏抓补全 | fallback_source |
样本是确定性的——同一条内容每次劣化结果一样, 所以每次演示看到的排行和规则都相同,不会「上次看到的和这次对不上」。
为什么不实时抓取
考虑过让演示实时从公开站点抓真实数据,但那是增加依赖而不是减少: 反爬、限流、页面结构变更三重不确定,演示时网络一抖就演不成。
离线样本零依赖、可复现,而且这条链路本来就在更早的版本里 「摒弃爬虫改走快照解析」——演示没有理由把已经绕开的问题重新引进来。
实时抓取留作后续的可选开关,不是默认路径。
也可以在命令行跑
模拟上游脚本可以单独运行,直接对着任意一个部署好的服务推数据:
python3 scripts/demo_upstream.py \
--base-url http://127.0.0.1:8040 --token $BIANMU_API_TOKEN
它走的是真实的埋点接口,可当作接入参考实现。 注意这会写入真实数据目录,仅在确实想灌入数据时使用。
验证规则包真能被消费:
python3 examples/upstream_loader.py <演示目录>/rules/ruleset-latest.json
打印上游按类型分派后的配置——这就是编目链路实际会用到的东西。