novel_keywords
高频关键词统计。对某部作品或单个章节做纯字面计数,产出四类条目:中文相邻二字组、中文相邻三字组、疑似人名/专名、英文词。输出不含单字——单字频次不参与统计,只保留重复出现的二字组、三字组与英文词,以及出现两次以上的疑似人名。全程零依赖、无分词模型、不调用在线大模型,结果同时落盘到书库数据目录留档。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
book | string | 是 | 书名,即 novels/ 下的子目录名。经 sanitizeSegment 过滤。目录不存在或目录下无可识别的章节文件时直接报错,不返回空结果。 |
chapter | string | 否 | 只统计该章节;省略则统计全书。取值可以是章号(1 / 01 / 第一章)、文件名或标题子串,解析规则见「约束与边界」。 |
top | integer | 否 | 返回的关键词数量,须为 1–100 的整数,默认 20。越界即报错,不截断到边界。 |
root | string | 否 | 章节库根目录。缺省回退顺序:args.root → config.root → 会话工作目录。MCP 形态下必须落在启动参数 --root 之内。 |
参数对象声明 additionalProperties: false,多传的键会被宿主拒绝。
输出结构
契约字段(output.schema.required):book / scope / totalChars / keywords 四项必返。
| 字段 | 类型 | 内容 |
|---|---|---|
book | string | 必返。清洗后的书名 |
scope | string | 必返。统计范围:单章模式为章节文件名;全书模式为 全书 N 章 |
totalChars | integer | 必返。各章原文长度之和(chapterText.length 累加,含 Markdown 标题行与空行) |
keywords | array | 必返。按 count 降序排列的条目数组,最多 top 条,可能为空数组 |
keywords[].word | string | 必返。词面 |
keywords[].count | integer | 必返。出现次数 |
keywords[].kind | string | 必返。枚举 cjk-bigram / cjk-trigram / name-candidate / word,分别对应二字组、三字组、疑似人名、英文词 |
reportFile | string | 报告落盘的绝对路径。落盘失败时不返回该字段(主流程不受影响) |
计算原理
全部统计为单遍字面扫描,没有词表以外的语言模型,也不做词性标注与切词。四种条目各自独立计数,最后合并、排序、截断。
1 · 语料拼接与字数口径
章节之间强制插入空行是 v4.0.0 的修正:旧版首尾相接会把上一章末尾与下一章开头当成相邻二字/三字组,产出正文里根本不存在的「词」。因此全书统计的结果不等于各章分别统计后求和。另需注意 totalChars 是原文长度累加,包含 Markdown 标题行与空行,与 novel_style_report 先剥离标题行再统计的口径不同,两个工具的同一字段不可互相校验。
2 · 中文相邻二字组与三字组
停用规则是「组内至少一个字是有信息量的实字」,用来压掉「的了」「是在」这类纯虚词碎片。取舍代价是重叠计数:同一段文字会同时产出二字与三字条目(例如「琉璃说道」会产出「琉璃」「璃说」「琉璃说」等),cjk-bigram 与 cjk-trigram 的计数不可相加,且三字组常为跨词边界的碎片,不应直接当作词汇表使用。
3 · 英文词
单个字母不参与统计(下限 2 个字母)。英文词在源头按出现计数,最终入选阈值是 count > 1,因此只出现一次的英文词不会出现在结果中。
4 · 疑似人名 / 专名
模式 A 的量词取懒惰是为修正贪婪误吞:贪婪会把「琉璃说道」匹配成「琉璃说」加剩余部分,使人名候选被动词尾巴污染。代词起头过滤针对「他说」「她又」「我问道」这类伪人名。该通道本质是启发式,故输出的 kind 标为 name-candidate(疑似)而非确定人名。
5 · 入选阈值、排序与截断
出现一次的词组在合并阶段就被丢弃,因此「篇幅可观但用词分散」的文本可能返回空数组——渲染层此时输出「(未提取到重复出现的关键词)」。空数组是正常结果,不等于统计失败。
6 · 报告落盘与保留份数
指纹含内容哈希与 scope:改稿或切换统计范围都会生成新文件,同长度改稿不再互相覆盖(v4.0.0 修正,旧版只哈希 text.length)。落盘失败被 try/catch 吞掉,仅表现为返回体缺少 reportFile。该文件只写不读:本工具不会因缓存存在而跳过计算,每次调用都重新统计;需要复用历史结果时由调用方自行读取该路径。
约束与边界
- 四类条目共用同一个
top预算。排序对合并后的全部条目全局进行,不按kind配额分配:top较小时结果可能被某一类(通常是高频二字组)占满,其它类别被整体挤出。需要分类观察某一类时应提高top(上限 100)后按kind过滤,或改用单章缩小语料。 - 字面统计,无分词。没有词性标注与词典切词:三字组常为跨词碎片,二字组也可能跨越词边界(「琉璃说」)。
cjk-bigram/cjk-trigram是「字面重复片段」而不是词汇表,不应直接当名词表使用。 - 输出不含单字。单字频次不参与统计;若确需单字分布,应改用句式分析或风格报告工具,而不是通过降低
top或放宽参数来绕过。 - 阈值不可配置。入选阈值(
>1/≥2)与停用字表是代码内常量,没有对应的工具参数;不同插件版本的停用表若发生变化,同一本书的关键词列表不可跨版本直接比较。 - 与风格工具的计数口径不同。
totalChars为原文长度累加(含 Markdown 标题行),风格报告侧先剥离^#{1,6}标题行再统计,且两者统计对象(字面片段 vs 六维密度)不同。同名数字字段之间不存在可比关系。 - 章节解析走
findChapter:v4.3.1 起先按精确文件名匹配(含省略扩展名,如第01章.md或第01章),再按章号数值匹配(1/01/第一章同义),最后按章节标题或文件名的子串匹配。同章号存在多个文件时按章号定位会报错并列出候选,需改用文件名。纯数字参数不做子串兜底——传入"1"不会命中「第11章」,只会在数值相等时命中。找不到章节时报错,不降级为全书统计。 - 报错而非空结果的情形:书名目录不存在(提示先用
novel_books核对作品名)、目录下没有.md/.markdown/.txt章节文件、chapter无法匹配、top不在 1–100 的整数区间。 - 只读顶层文件。章节扫描不递归,子目录中的稿件不会进入统计,也不会计入
totalChars。 - 报告目录的清理边界。保留 20 份的清理只匹配
<book>-keywords-*.json,同目录下的<book>-full.json、<book>-chapters-metrics.json等分析缓存不受影响。 - root 与工具开关:MCP 形态下
root越界会被静默回退书库根;execute首行执行assertToolEnabled,UI 中关闭该工具后调用直接报错。
相关工具
- novel_style_report —— 六维风格画像,其中的词汇维度与本工具同属字面统计但口径不同。
- novel_sentence_analysis —— 高频句式模板与题材/关键词共现,用于看「句式怎么排」而非「哪些字在重复」。
- novel_read —— 对可疑的三字组碎片回到正文核对上下文。
源码位置
- lib/index.js —— 工具注册与
execute:参数 schema、输出契约、语料拼接、报告落盘与保留清理(registerNovelKeywords,约 2782 行) - lib/core.js ——
extractKeywords四类条目统计与排序截断(约 397 行)、tallyCjkRun汉字串滑动窗口(约 446 行)、CJK_STOP_CHARS单字停用表与EN_STOP_WORDS英文停用词(约 187 行) - lib/core.js ——
scanChapters/findChapter章节定位、sanitizeSegment、atomicWriteJson原子写、novelDataDir数据目录、formatKeywords渲染 - lib/index.js ——
pruneAnalysisReports同书保留最近 20 份报告的清理(约 155 行)
lib/analysis.js:该文件服务于句式分类、情感量化与风格指纹,与本工具没有调用关系。关键词的计数、阈值与排序全部位于 lib/core.js 的 extractKeywords / tallyCjkRun,工具壳层位于 lib/index.js 的 registerNovelKeywords。