〖共创稿事节〗HarmonyOS 7 新特性实战(64):四万三千字压到三万三——异兽详情分层去重的硬约束、口吻门禁与钉点维护
# 〖共创稿事节〗HarmonyOS 7 新特性实战(64):四万三千字压到三万三——异兽详情分层去重的硬约束、口吻门禁与钉点维护
上一篇写地图浮雕的边界怎么从格子楼梯走到多边形真轮廓,这一篇回到内容层,处理一个更隐蔽的膨胀:异兽详情页。为了把每只异兽做出层次,文案分成了四层——详情正文、阅读注、学习卡(知识卡与故事卡)、馆长导读。层与层各有职责,但写着写着,同一件事被四层反复讲:36 只异兽的访客文案总量膨胀到 43,473 字,与原文的倍率最高 18.1 倍——读者点开一只三百字的异兽,要滑过三千多字才能看完。
结论先说:这轮治理把总量压到 33,256 字(−23%),倍率降到 15.4× / 12.4×,「馆藏简介 ↔ 白话解读」的 bigram Jaccard 重合度中位数从 17% 降到 11%,而 1,800 字的阅读厚度下限一只都没破。支撑它的是一套「硬约束先行」的改写流程,和一轮今天(2026-11-09)复跑才现出原形的钉点维护——钉点也会过期,这是本篇最想讲的一课。

一、膨胀先量化:倍率与重合度
治理之前先回答两个问题:膨胀到什么程度?膨胀在哪?
第一个问题用倍率回答。口径固定为「访客可读正文 ÷ 原文正文」:原文取自《山海经》维基文库整理稿的经文段落(去空白后的汉字数),文案取四层正文之和。以第一批 18 只(v4)为例:
| 口径 | 去重前 | 去重后 |
|---|---|---|
| 36 只文案总量 | 43,473 字 | 33,256 字(−23%) |
| 第一批 18 只倍率 | 18.1× | 15.4× |
| 第二批 18 只倍率 | 13.7× | 12.4× |
第二个问题用重合度回答。抽「馆藏简介」与「白话解读」两层,各取汉字 bigram 集合算 Jaccard:
// 每只异兽算一遍:两层文案的 bigram 重合度。
// Jaccard 越高,说明两段话在讲同一件事——分层名存实亡。
const bigrams = (s) => {
const t = String(s ?? '').replace(/\s/gu, '');
const out = new Set();
for (let i = 0; i < t.length - 1; i += 1) out.add(t.slice(i, i + 2));
return out;
};
const jaccard = (a, b) => {
const A = bigrams(a); const B = bigrams(b);
let inter = 0;
A.forEach((g) => { if (B.has(g)) inter += 1; });
return inter / (A.size + B.size - inter);
};
去重前 18 只的中位数 17%——两层文案有六分之一的二字组合是共享的。字数膨胀和重合度偏高指向同一个病:层级之间没有分工合同,每层都在「把这只异兽完整介绍一遍」,只是措辞不同。治理的目标因此不是「删字」,是把每层压回它自己的职责:详情正文讲完整档案,阅读注只讲怎么读原文,学习卡讲知识与故事,馆长导读只讲观看动线。
二、先立硬约束,再动笔
改写最容易翻车的地方是「越删越薄」——把分层压成了空洞。所以动笔前先立一组硬约束,全部可机检:
| 约束 | 内容 | 防什么 |
|---|---|---|
| 阅读厚度下限 | 每只 visitorContentLength ≥ 1800 | 防删成空洞 |
| 必需事实钉点 | 每只 4~5 条事实关键词必须保留 | 防丢事实(如鹿蜀的「其文如虎」) |
| 篇幅硬顶 | 馆长导读每条 220–350 汉字 | 防导读层二次膨胀 |
| 名词表规模 | glossary 5–8 条、术语去重 | 防词条凑数或重复 |
| 口吻规则 | 七类 AI 腔禁用语(详见下) | 防改写带进机器腔 |
其中「阅读厚度」的口径本身就是一个约束:不能把重复模块算成补写成果。统计函数先做集合去重,再拼长度——同一句话在详情和导读里出现两次,只算一次:
// 只计不重复的访客正文:原文引用、标签、标题与完全重复的模块体都排除。
export function visitorContentLength(item) {
const prose = [item.summary, item.detail, item.readingNote,
...[item.knowledge, item.story].flatMap((card) => [card.summary, card.content, card.readingTip]),
...item.glossary.map((entry) => entry.note), ...item.curatorTopics.map((topic) => topic.content)];
return [...new Set(prose.map((value) => value.replace(/\s/gu, '')))].join('').length;
}
必需事实钉点是这一轮的主角。每只异兽在门禁里钉 4~5 条「必须保留的事实关键词」——鹿蜀必须留住「其文如虎」,帝江必须留住「识歌舞」。改写时逐条对照,改完由门禁逐条断言:
const REQUIRED_FACTS = {
beast_lushu: ['杻阳之山', '白首', '其文如虎', '赤尾'], // 虎文一词已随分层改写移入 glossary,正文钉点改钉「其文如虎」
beast_zhulong: ['钟山', '烛阴', '视为昼', '瞑为夜'],
beast_dijiang: ['浑敦', '识歌舞'],
// ... 36 只逐一钉
};
口吻规则防的是另一种翻车:改写交给语言模型时,「删减」很容易带进 AI 腔。规则收成七类正则,正文、词条、导读逐字段断言:
export const VISITOR_COPY_TONE_RULES = [
// ① 面向访客的叙述不许出现校勘动作(校勘、底本、异体字……)
/校勘|底本|异体|原注|注疏/,
// ② 指着界面元素说话(这个页面、本节内容……)
/这个页面|本节内容|此条目|当前卡片/,
// ③ 反转式 AI 句型(不是……而是……)
/不是[^。]{0,24}而是/,
// ④ 机械排比引导(首先……其次……)
/首先[^。]{0,60}其次/,
// ⑤ 阿拉伯数字计数叙述(4 个要点、3 种说法)
/\d+\s*(?:个|种|条|项)(?:要点|说法|特征|层次)/,
// ⑥ 空洞评价(丰富多彩、博大精深……)
/丰富多彩|博大精深|源远流长/,
// ⑦ 生成自述(为您提供、希望这份……)
/为您提供|希望这份|以上就是/
];
七类规则的来源不是拍脑袋,每一类都对应一次真实翻车:⑤「3 种说法」这种计数腔,是第一批文案最扎眼的机器味;②指着界面说话,在横竖屏切换时尤其荒谬——卡片内容没换,界面语境变了。
三、改写流水线:dump → rewrites → scan → apply
四步流水线把「改 36 只异兽」变成可审计的批处理:
1. dump:从规范源导出每只异兽的四层正文工作单,供改写者逐只处理;2. rewrites:改写产物按 beastId 落成结构化文件,字段与规范源一一对应;3. scan:扫描器对每份改写跑硬约束——篇幅、钉点、口吻、分层重合度,任何一条不过就不进入下一步;4. apply:改写器把通过的改写回写进规范源 .mjs 文件。
apply 是最容易写坏的一步:它直接编辑源码文件,必须按字段定位、按偏移安全替换。第一版在这里翻过车——knowledge 和 story 两个卡片字段共用同一个搜索起点,第二处 span 定到了错误位置,替换后整个文件语法报废。修法有两个关键点:每个字段从自己的键位置重新定位;所有编辑按起点倒序应用,先改文件后面的部分不影响前面字段的偏移:
// 每个卡片字段各自定位(from = 本只异兽块起点),span 互不串扰
for (const field of ['knowledge', 'story']) {
const span = keySpan(source, field, from, limit);
if (span === undefined) throw new Error(`${beastId}: no ${field} key`);
edits.push([span, `${field}: ${card(rewrite[field], target.indent)}`]);
}
// 编辑按 keyStart 倒序应用:先改后面,前面的偏移不受影响
edits.sort((a, b) => b[0].keyStart - a[0].keyStart);
for (const [span, text] of edits) {
source = source.slice(0, span.keyStart) + text + source.slice(span.valueEnd);
}
scan 通过后 apply 回写,回写完立刻重跑一遍全部门禁——改写器自己也要被门禁盯,因为它是一次大规模自动化编辑,任何定位错误都会静默弄坏一批数据。
四、钉点也会过期:今天复跑抓到的两处
前面都是 2026-10-13 当天的故事。真正有意思的是今天:为写这篇复跑门禁,两处红了。
第一处:鹿蜀的「虎文」钉点失效。 verify-rich-beast-depth 报「beast_lushu must retain '虎文'」。查了正文才发现:分层改写把「虎文」这个词从详情正文挪进了名词表(glossary 里仍有一张「虎文」词条),而钉点校验的 fullText 不包含名词表——事实还在,钉点看不到。这不是内容回归,是钉点的字段范围过期。处置按既有先例:改钉到承担同一职责的正文句——正文现在写「其文如虎」,钉点随之改写(见第二节代码里的注释)。
第二处:相柳的边界句真的被削掉了。 verify-batch2-pilot-content 报「beast_xiangliu must retain '古代'」。这一次不是钉点过期——查 git 历史确认,改写删掉了故事卡里承担审读边界的一句话:「这是古代神话以异物讲述地貌的方式」。分层去重要压掉的是重复,不是立场;这句边界是当年审读纪律的锚点,丢了就是真回归。处置:按口吻规则核对后把边界句补回故事卡,再重跑同步生成器,把规范源的修正同步进应用侧 .ets 数据文件:
node backend/scripts/generate-batch2-pilot-arkts.mjs
# PASS 2026.09.14-pilot-v15-visitor-copy: generated ShanhaiBatch2PilotData.ets
两处红、两种病、两种处置——这正是钉点体系的价值:它校验的是「读者必须能读到什么」,不是「文件里有没有这个字符串」。事实还在但钉点看不见,是校验口径要跟上数据结构;事实真的没了,就是内容回归,要修复而不是放宽校验。区分这两种情况靠的是查证据(正文现状 + git 历史),而不是调阈值。

五、终局门禁:36 条目全字段比对
分层去重的收口门禁是四道,今天全部复跑通过:
| 门禁 | 断言内容 | 今日结果 |
|---|---|---|
| verify-rich-beast-depth | 第一批 18 只:≥1800 字、来源钉点、口吻 | PASS |
| verify-batch2-pilot-content | 第二批 18 只:同口径 + 边界句钉点 | PASS |
| verify-visitor-copy-tone | 36 只全部访客字段逐条过七类口吻规则 | PASS |
| verify-strict-content-samples | 应用侧 .ets 与规范源全字段同步 | PASS |
最后一道最重:它不只比对目录文件,还把应用侧 36 条目、1,116 个现代字段、72 张学习卡、144 次馆长真实读取逐一 deepEqual 比对——馆长导读不是存好的文本,是运行时从话题数据现算的,门禁连「现算结果与规范源一致」也钉死了:
// 馆长导读按真实读取路径现算,防止「数据对了、组装错了」这类静默错位
const guide = createShanhaiCuratorGuide(topic.topic, beast, undefined, undefined);
assert.equal(guide.content, `${topic.title}。${topic.content}`, `${id}: actual ${topic.topic} guide`);
今日复测的数字(与 10-13 同口径):
| 指标 | 数值 |
|---|---|
| 36 只文案总量 | 33,111 字(第一幅 17,313 / 第二批 15,798) |
| 倍率 | 第一批 15.4× / 第二批 12.3× |
| 「馆藏简介↔白话解读」Jaccard | 中位 11% / 最大 21% |
| 篇幅最紧 | beast_zhuyan 2,076 字(下限 1,800 未破) |


收个尾
复盘这轮治理,能沉淀的顺序依然不能颠倒:
- 膨胀先量化再动刀。 倍率回答「膨胀到什么程度」,Jaccard 回答「膨胀在哪」——没有这两个数,「文案有点长」只会变成一轮没有终点的删减。
- 硬约束先行,改写在后。 1,800 下限、事实钉点、口吻规则全部可机检之后,改写才敢放开手——约束不是改写的补充,是它的前提。
- 工具也要被门禁盯。 apply 直接编辑源码文件,定位错误的代价是一批数据报废;倒序应用编辑和逐字段定位是它自己的安全带。
- 钉点是合同,合同要维护。 钉住「读者必须读到什么」的钉点,会随数据结构搬家而失效;复跑抓到的两处红,一处改钉、一处修复——区分它们靠证据,不靠调阈值。
四层文案的分工合同,最后落成一句话:详情正文讲档案,阅读注讲读法,学习卡讲知识与故事,馆长导读讲动线。每层守自己的职责,重合度自然降下来;反之,靠措辞变换压字数,重合度迟早涨回去。43,473 到 33,111 的差值里,删掉的不是信息,是四层都在重复的那份「完整介绍」。
附:本篇证据清单
- 门禁复跑(2026-11-09):
backend/scripts/verify-rich-beast-depth.mjsPASS(第一批 18 只 ≥1800、事实钉点、口吻)、backend/scripts/verify-batch2-pilot-content.mjsPASS(第二批同口径)、app-harmony/scripts/verify-visitor-copy-tone.mjsPASS、app-harmony/scripts/verify-strict-content-samples.mjsPASS(36 条目 / 1,116 现代字段 / 72 学习卡 / 144 次馆长真实读取)。 - 指标复测:36 只文案总量 33,111 字(同口径公式 sum/detail/note/K/S)、Jaccard 中位 11% 最大 21%、最紧 beast_zhuyan 2,076 字;验收当日数字 43,473 → 33,256(−23%)见验收记录。
- 验收记录:
acceptance/2026-10-13-detail-layer-dedup/(分层结构截图、阅读注独立层展示)。 - 口吻规则与硬约束源码:
backend/scripts/content-copy-quality.mjs(七类规则)、backend/scripts/strict-content-contract.mjs(glossary 规模、导读篇幅、话题顺序)、backend/scripts/visitor-content-length.mjs(去重口径)。 - 改写工具链:
.workbuddy-ai/tmp/apply-rewrite.mjs(按字段定位、倒序应用编辑)。 - 关联稿件:审读口径与内容治理的第一轮(审核说内容太少)见系列第 52 篇;全书对账管线见系列第 62 篇;地图浮雕边界四轮返工见系列第 63 篇。
- 已知欠账:第二批倍率 12.3× 与验收当日 12.4× 的 0.1 差异来自后续轮次的零星内容修订,同口径下无需处理;名词表术语的 bigram 重合未纳入 Jaccard 口径,如后续词条扩张再评估。
更多推荐
所有评论(0)