# 〖共创稿事节〗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-tone36 只全部访客字段逐条过七类口吻规则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 未破)

去重后的详情页分层结构:原文、阅读注、学习卡与馆长导读各司其职

白泽详情中段:馆长导读层压回 220–350 字的观看动线职责

收个尾

复盘这轮治理,能沉淀的顺序依然不能颠倒:

  • 膨胀先量化再动刀。 倍率回答「膨胀到什么程度」,Jaccard 回答「膨胀在哪」——没有这两个数,「文案有点长」只会变成一轮没有终点的删减。
  • 硬约束先行,改写在后。 1,800 下限、事实钉点、口吻规则全部可机检之后,改写才敢放开手——约束不是改写的补充,是它的前提。
  • 工具也要被门禁盯。 apply 直接编辑源码文件,定位错误的代价是一批数据报废;倒序应用编辑和逐字段定位是它自己的安全带。
  • 钉点是合同,合同要维护。 钉住「读者必须读到什么」的钉点,会随数据结构搬家而失效;复跑抓到的两处红,一处改钉、一处修复——区分它们靠证据,不靠调阈值。

四层文案的分工合同,最后落成一句话:详情正文讲档案,阅读注讲读法,学习卡讲知识与故事,馆长导读讲动线。每层守自己的职责,重合度自然降下来;反之,靠措辞变换压字数,重合度迟早涨回去。43,473 到 33,111 的差值里,删掉的不是信息,是四层都在重复的那份「完整介绍」。

附:本篇证据清单

  • 门禁复跑(2026-11-09):backend/scripts/verify-rich-beast-depth.mjs PASS(第一批 18 只 ≥1800、事实钉点、口吻)、backend/scripts/verify-batch2-pilot-content.mjs PASS(第二批同口径)、app-harmony/scripts/verify-visitor-copy-tone.mjs PASS、app-harmony/scripts/verify-strict-content-samples.mjs PASS(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 口径,如后续词条扩张再评估。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐