简介Jiayan甲言是一款面向古代汉语文言文/古文的NLP工具包旨在弥补通用自然语言处理工具集中于现代汉语、对古汉语支持不足的短板为古汉语学者、语言爱好者及相关研究者提供分词、词性标注、断句与标点、文言词库构建等能力。压缩包共28个文件以20个Python脚本为核心另含文本词典、Markdown说明、许可证与依赖清单等整体约217KB结构清晰便于离线阅读和本地部署。已有3318人学习/下载属于小而实用的古汉语信息处理类资源。值得关注的是包内实现了无监督词库合成、基于有向无环词图与最大概率路径的动态规划分词、序列标注以及断句标点等功能读者可获得完整可运行的工具链同时附带的示例脚本和说明文档能帮助快速了解模块调用方式并可结合自身语料扩展文言词典用于古籍整理、文学研究或NLP教学实验中。1. 甲言不是又一个分词器而是给文言文单独开的“一条道”一个做古籍数字化的开发者跟我吐槽他拿现代中文的分词工具切《资治通鉴》切出来全是“单字成词”因为古文里“之”“乎”“者”“也”这些虚词单独成义现代分词器压根不认更要命的是原文没有标点分词之前得先断句而断句又依赖分词结果鸡生蛋蛋生鸡。这正是我当初遇到 Jiayan甲言时的场景一个专门面向古代汉语的 NLP 工具包把词库合成、分词、词性标注、断句、标点五件事串成一条完整流水线。它不是把现代汉语工具拿来做“迁移学习”而是从词表构建这一步就换了一套设计逻辑。这篇笔记我按自己做古籍语料处理的顺序展开先讲清楚为什么古汉语处理必须自成一派再给出一套能在本地复现的最小流程然后逐个拆分词、词性标注、断句和标点的参数与坑。你手里如果有一批未断句的文言文或者想给某一部古籍做全文标注这文能让你少走至少两天弯路。2. 古汉语 NLP 的底层逻辑为什么通用工具一上来就翻车2.1 没有标点的文本让分词和断句互相“绑架”现代中文 NLP 的默认输入是“有标点、词边界模糊、句子边界清晰”的文本。分词器只要把字串切成词句子已经由句号问号分好了。古汉语语料完全反过来原文多半没有标点句子边界本身就缺失。于是问题变成了为了分词得先知道哪些字组成一句话为了断句得先知道哪些字能组成词——这是一个典型的相互依赖问题。Jiayan 的做法是把这个链条拆开分两步走先基于词库做一个“宽松的候选切分”把可能的词边界全找出来然后用统计模型在这些边界上做打分选出概率最高的一组切分同时结合断句模型输出标点位置。实操层面我习惯把断句和分词当成一个联合任务来看而不是先用某种规则把句子切好再丢给分词器。原因很简单古文里大量单字词尤其是虚词作为断句线索出现你要是先切句很多跨句的固定搭配会被拦腰截断。这也是为什么我建议你拿到一款古汉语工具先看它是不是“分词和断句联合建模”而不是把两者做成两个完全独立的模块。古汉语处理的另一个底层差异在于词表形态。现代汉语词汇以双字词为主古文则以单字词和双字词混合为常态。一篇《史记》选段里“之”可以当结构助词、动词、代词三种身份单靠词典匹配根本分不清必须依赖上下文特征。Jiayan 这类工具的思路是把“词典匹配”只作为特征之一真正的切分和标注决策交给序列标注模型。这意味着你给工具喂什么样的词库直接决定模型能观察到哪些候选词。我在自己的项目里踩过的典型坑是拿一个现代汉语通用词典当种子词库去跑古文结果“因为”“虽然”这类现代双字词被大量切出来反而把“因”“为”两个独立词的上下文特征全污染了。后来我换成只含古文常用词和专名的小词库效果反而明显提升。这件事说明一个原则古汉语 NLP 的词库要“少而准”不要“大而全”。2.2 词库合成领域词表是怎么“长”出来的Jiayan 支持词库合成简单说就是允许你把自己的领域词汇表、专名表、已有词典按权重合并成一份新词库供分词和词性标注使用。这个设计对古籍处理至关重要通用词典覆盖不到人名、地名、官制名、书名号里的篇目名而这些恰是古文里信息密度最高的部分。我一般这样组织词库合成流程先准备一个基础古文常用词表覆盖虚词、实词、常见双字词再把自己项目里的专名表比如某部史书里的人名地名索引做成一个高优先级词表最后把两者按“专名 基础词”的优先级合并。合并时要注意一个关键参数是否允许长词嵌套短词。例如“司马”和“司马相如”同时存在工具默认会倾向于切出更长的专名但如果你把“司马”当成官职名独立成词两者会打架。我踩过这个坑解决办法是给专名表加一个“最长匹配优先”标记而不是靠词频压。合并策略里还有一个容易忽略的点词频的初始化。直接拿两个词表拼接会导致某些通用词在语料中出现频率被高估因为在模型眼里所有词都来自同一个分布。常见做法是给每一条词带上初始权重比如专名词条权重设为 3.0基础虚词设为 1.0让模型在解码时把这个权重作为先验分数。我自己跑的数据里把专名权重从默认 1.0 调到 2.5 之后人名识别的 F1 值肉眼可见地涨了一截但如果调到 5.0 以上模型会开始把含有同一个字的普通词组也错切成专名典型例子是把“司马门”切成“司马门”。所以权重调节要小步试探一次 0.5 地加。词库合成完成之后输出一份纯文本词表文件每行一个词可附带词性和权重喂给分词器前记得检查编码格式统一为 UTF-8。2.3 语料形态预处理步骤的顺序也能决定成败古汉语语料进模型之前预处理顺序比现代汉语更敏感。我的处理顺序固定为原文 OCR 文本 → 统一为简体或保留繁体按项目需求→ 去掉无关的现代标点/章节标记 → 按自然段切分 → 交给工具做断句与分词。这里有个特别值得注意的决策点要不要做繁简转换。如果训诂材料里有大量异体字和通假字我建议保留繁体原貌因为转换成简体之后“裡/里”“後/后”“乾/干”等字会被合并词性标注时会丢失关键区分信息。反过来如果你的下游任务不需要字形信息简体可以让词表更紧凑。Jiayan 这类工具本身一般不做繁简归一化这个决策得你在预处理阶段定好而且定了就别在中途改否则同一篇文本前一半用繁体词表后一半用简体词表词性标注结果会像“阴阳脸”。我自己的血泪经验是预处理脚本里写了一行“统一转简体”跑完《春秋左传》相关的专名表全废了因为人名里的“裡”被转成“里”和地名“里”撞车。后来所有涉及历史专名的语料一律保留繁体。文本清洗阶段还有一个反直觉的点不要把现代标点全删干净。常见的做法是保留顿号、分号和引号作为断句模型的弱提示因为这些符号在古籍整理本里通常是整理者加上的句读痕迹能帮断句模型减少候选位置。如果全删成“光秃秃”的白文模型要在每个字后都做一次二分类误判率会上升。我一般保留逗号、句号、问号和引号但会把它们标记为“弱特征”告诉模型这些标点仅供参考、权重不要给太高。这个思路听起来有点“玄学”实际操作里却非常管用我用同一批《明史》选段测试保留标点提示后断句准确率提升了好几个百分点。原因也好理解古籍整理本的标点是前人句读的结果本身就携带了大量语言学先验扔掉它等于主动丢信息。3. 在本地跑通 Jiayan 的最小流程环境、词表与第一段标注结果3.1 安装与依赖先确认 Python 版本和模型文件路径把 Jiayan 这类古汉语工具包在本地跑起来最常见的问题不是代码而是环境。我建议在干净的 Python 3.8 到 3.10 虚拟环境里装避开 3.11 之后某些科学计算扩展包对旧模型的兼容问题。安装时注意看安装日志里的依赖项列表如果其中有 CRF 相关的扩展大概率是用于序列标注的这类扩展在 Windows 下偶发编译失败Linux 和 macOS 下基本顺利。装好之后先别急着跑找到工具包自带的模型文件目录确认里面至少包含一个词表文件和一个标注模型文件。模型文件缺失或者路径配置不对启动时会报“模型文件不存在”这是多数“装上跑不起来”案例的根源跟工具本身没关系。一个常见做法是先把工具自带的基础词库 dump 出来看看格式。词表文件一般是纯文本每行一个词条可能包含词性标记和频次信息。这个动作很有必要因为后面你自己做词库合成时要严格对齐它的字段分隔符。我遇到过某版本用 Tab 分隔、某版本用逗号分隔的情况直接把自己的词表替换进去后解析器报错。最初级但最稳妥的验证方式拿一段不超过五十字的白话文或简单文言段跑一次分词和断句先把流程走通再去碰长文本。别一上来就跑整部《资治通鉴》一旦输出乱套你分不清是模型问题还是你自己的语料问题。3.2 最小复现命令一段《论语》选段走通全流程下面给出一段我用过的、能完整覆盖分词、词性标注、断句标点的最小流程。注意接口名与参数各版本和不同工具包有差异实际以安装包注释为准但组织的调用逻辑是通用的。示例假设我们已经把语料存成纯文本文件input.txt内容为某古籍选段的白文。import jiayan # 1. 加载基础词库同时加载一个自定义的高优先生名表 base_lex jiayan.load_lexicon(lexicon_base.txt) proper_lex jiayan.load_lexicon(proper_names.txt, priority2.5) # 2. 合成词库合并两份词表写入新文件 merged jiayan.merge_lexicons([base_lex, proper_lex], outputlexicon_merged.txt) # 3. 初始化分析器传入合成词库和模型路径 analyzer jiayan.create_analyzer( lexiconlexicon_merged.txt, modelmodel_annotator.bin, use_posTrue, enable_sentence_splitTrue, ) # 4. 读取语料并按自然段处理 with open(input.txt, encodingutf-8) as f: text f.read().strip() # 5. 执行断句 分词 词性标注 result analyzer.analyze(text, split_sentenceTrue, with_posTrue) for sent in result: print(sent) for token, pos in sent.tokens: print(f{token}\t{pos})这段代码的逻辑并不复杂前两步把基础词库和专名词库合成新词表第三步创建分析器时指定合成词表、模型文件路径并开启词性标注和断句开关第四步读取文本第五步一次性输出带词性和断句结果。需要注意的点有三个。其一priority2.5是专名词条的权重后面模型打分时会把这个词的先验概率乘以该权重权重越大越容易被切出来。其二enable_sentence_split必须和split_sentenceTrue配合使用只开一个的话要么断句模型不生效要么输出结构对不上。其三输出的sent.tokens里每个 token 有词性和在句中的位置信息标点符号作为独立的 token 输出方便你后续直接渲染成带标点的文本。参数层面use_posTrue会显著增加计算量如果你只需要断句和分词建议关掉词性标注速度能提一倍。split_sentenceTrue开启后模型会在每个候选位置输出一个 0 到 1 的分数分数超过阈值的位置就插入标点。这个阈值通常可以用set_threshold一类的方法调整默认值对一般古籍整理够用但如果你的语料是特别长的骈文句子平均长度超过 25 字可以适当调高阈值减少误断。我自己的习惯是先保留默认跑一遍看断句结果里哪些位置明显不对再反向调整阈值——先观察再调参而不是上来就拧。3.3 输入输出格式词表字段顺序和输出对象结构输入格式决定你能不能直接把现有数据灌进去。词表文件每一行建议按“词面、词性、权重”排缺少权重字段时默认取 1.0。输出对象的组织方式各版本差异较大有的把分词结果放在.tokens有的放在.words有的把句子边界放在.sentences。我见过最别扭的一个版本输出的句子列表里每个句子内部不再区分 token而是直接给一个带空格分隔的字符串词性信息要另开一个列表对齐对不上就整个错位。所以我在这里的建议是拿到输出对象后先打印result.__dict__把字段名看清再写下游代码。这一步能帮你省掉大量“输出了但我解析不了”的时间。编码方面输入文本和词表文件保证都是 UTF-8 无 BOMWindows 下尤其容易在文件首部混入 BOM 导致第一条词读出来是乱码。如果你在 Windows 下跑建议读文件时用encodingutf-8-sig兜底。输出标点符号的处理我单独说一句。模型输出的标点往往只有“”“。”“”三类没有现代文本里的逗号、顿号、分号之分。古籍整理场景下这种粗粒度输出反而合适句号表示句子边界逗号表示句中停顿问号表示语气。如果你下游需要更细的标点比如区分直接引语里的冒号引号得自己在后处理阶段根据引号标记补充别指望模型直接吐给你。这一点我在第四章节还会展开写。我实际处理明朝奏疏文本时发现模型常在“臣闻”之后不输出逗号因为这个词组在语料里频繁出现、已经被当成一个固定搭配。这时候在词库里把“臣闻”单独成词反而会削弱断句质量——决策要看你最终要的是词级别信息还是句子级别信息。4. 四大核心功能的参数、输出与效果调优4.1 分词权重、未登录词与最小词长分词在古汉语里最典型的“翻车现场”是长专名和单字虚词打架。模型对候选词打分时除了词库权重还会计算上下文特征比如“之”字出现在两个名词中间时模型倾向于把它切成独立的助词而不是拼进前一个词。调整分词表现的核心参数有三个。第一个是未登录词惩罚系数调高它模型更倾向于用词库里的词去覆盖文本调低则更容易把不认识的字串原样切出来。我自己的测试面对包含大量生僻地名的方志类语料把未登录词惩罚从默认值调低 20%新地名被完整切出的概率明显提升但调到过低模型会把普通双字词也拆成单字。第二个是最小词长限制默认一般是 1也就是允许单字成词。古汉语必须允许单字成词这一点和第 2.1 节讲的现代工具不适用是同一原因但你可以限制一些特殊情况比如关闭纯单字虚词单独成词时可以让“之乎者也”变成整体语气词块这在处理某些语录体文本时效果更好。第三个是词频罚时系数它控制词频对切分的影响力高词频词更容易被模型选中。一个值得单独测试的场景人名和地名的交替出现。比如“项羽”和“项伯”这种共享首字的专名模型默认会根据后文决定切到哪。如果你词库里“项羽”和“项伯”权重一样模型就要靠上下文猜容易把“项伯”切成“项伯”。我常用的手法是给专名表注入额外的一列“上下文锚定词”例如“项氏”“项王”为辅让模型在“项”后如果出现“氏”或“王”就倾向于把它当成专名来解。这种方法本质上是用词库合成做了一次“半监督”标注。分词结果的验证也简单我自己会另写一个脚本把分词结果按词性统计出高频 Top 50 打印出来人工扫一眼。如果排名靠前的是“之、其、於、為”这类单字虚词说明分词偏碎如果排名靠前全是四字成语式的大词说明长词权重给过了句子被“过度合并”。4.2 词性标注词表标记风格与上下文的平衡词性标注在古文里最难的不是“名动形”这些实词而是兼类词。“之”在“之子于归”里是代词在“吾欲之南海”里是动词在“大道之行也”里是助词。模型判断依据主要在前后两个词的搭配模式上所以词性标注效果好不好你的词表里有没有给高频兼类词分别列出独立词条关系很大。实操时我把“之”“其”“以”“於”“而”这些高频虚词在词库里拆成多个带词性的词条例如“之_D”代表代词、“之_P”代表助词、“之_V”代表动词。这样做的原理是模型在解码时看到同一个字形会在候选词列表里找到多个条目去和上下文匹配打分比单一条目由模型硬猜靠谱得多。代价是词库体积膨胀但值得。另外要注意词性标注的标签体系。有的工具包用简单标签n/v/a有的用扩展标签体系包括语气词、发语词、兼词等。古汉语里“也”“矣”“焉”都是句末语气词但功能细节不同扩展标签对这些有区分简单标签全归为一类。我建议如果下游做句法分析或翻译辅助直接用扩展标签体系因为语气词和助词是古文句法的重要线索。如果你只是做信息抽取比如抽人名地名简单标签足够还不容易因为在扩展标签之间混淆而掉精度。词性标注还有一个隐藏参数上下文窗口大小。默认一般看前后各一个字我遇到长定语修饰结构“左右或欲引相去者”这种时会把窗口调整为前后各两个字让模型能在两个词之外看到更多修饰关系。窗口加大的代价是计算时间翻倍对长文本来说感受明显建议只在短句多、修饰复杂的文体上启用。4.3 断句与标点模型输出后后处理必须补齐的标点体系断句与标点输出是 Jiayan 这类古汉语工具包最有价值的部分因为它是后续一切分析的前置。模型输出标点的位置通常以“在某个字后插入标点”的方式给出但你要明白它本质上是在每个候选位置做一个二分类是否断句再对确定为断句的位置做一个多分类断成句号还是逗号还是问号。这意味着它的错误模式也有两种该断的地方没断不该断的地方断了。前者常见于长句子中间的分句边界后者常见于“主谓之间”的短停顿被误判成句子边界。参数层面的应对调高断句阈值可以减少“过度断句”调低能减少“漏断”两者此消彼长。实际使用中要看你下游任务的容错方向——做全文标点补全可以偏多一点断句再做人工校对做自动摘要则更需要句子边界准确宁漏勿错。标点后处理方面我强烈建议你做一步“强制规则覆盖”把可以枚举的固定句式标点规则写在后处理里。例如“者……也”判断句“者”后可加逗号“也”后可加句号“乎”字句疑问语气后可加问号。这类规则覆盖不了所有情况但能纠正模型一部分系统性错误而且实现成本极低一行正则替换的事。模型输出的标点往往只有逗号句号问号三类你要恢复出引号、书名号或专名线得依赖词性标注里的“人名/地名/书名”标签做映射。我自己会写一个极简渲染函数遇到人名标签的词前后加专名线遇到书名标签加书名号遇到语气词“也”“矣”后面本身是模型已加的句号就不动如果后面没有标点就强制补一个句号。这一步说白了是把模型不可控的输出拉回到古籍整理的排版规范里。4.4 参数速查什么时候动什么参数不同任务的参数调整优先级完全不同。我整理了一张自用的速查表按场景列了最值得先动手的参数场景优先调整参数调整方向副作用专名密集史书专名权重、未登录词惩罚专名权重调高、未登录词惩罚调低普通双字词可能被切碎长句多奏疏/论说断句阈值、上下文窗口阈值调高、窗口调大短停顿可能漏断虚词敏感语录/对话兼类词拆分、词频罚时高频虚词拆多条、罚时调高词表膨胀、速度下降标点只求粗粒度标点类别数限制为逗号/句号/问号三类后处理工作加重这张表的使用方式是先定位你的语料最突出的痛点只调一个参数观察再调下一个。我见过不少人一口气把五个参数全改了结果输出变了但根本定位不到是哪一步起的作用——排障的思路是先隔离变量。5. 古汉语 NLP 落地避坑记录四条高频踩坑经验5.1 繁简混排导致同一句话里同一个字被标成两个词性现象一段语料里有“後”和“后”混用分词后“後”标成名词“后”皇后也标成名词但“皇后”这个复合词没有被合成因为字形一个是“皇后”一个是“皇後”词库匹配不上。原因预处理阶段没有统一字形。来源可能是 OCR 识别混合了底本里的异体字或词表本身自带简繁两套写法。解决预处理时做一次字形归一二选一并且词表同步归一。我后来写了一个映射表把工具词库里所有含“後/后”“裡/里”“乾/干”的条目逐一核对确认含义后再统一方向。这个动作没法自动化只能半自动加人工抽查但一次性的投入能避免后续所有错标。5.2 模型把中间停顿错判成句子边界一句话被切碎成三段现象一段 30 字的论说句模型在“臣闻”后和“窃以为”前都断了句输出三个完整句号。原因断句阈值设得太低模型把句中所有语气停顿都当成了句子边界加上语料里这类奏疏开头的惯用句式训练样本充足模型学到的“断句倾向”过高。解决把断句阈值往上调直到“臣闻”“窃以为”这类领起词后面不再被断成句号为止。同时可以在后处理加规则如果“臣闻”后紧跟的下一句不超过 12 字强制合并回前一句。血的教训是调阈值不能靠拍脑袋我在三篇不同文体上试同一个阈值史记和奏疏的最优值能差出 20%——所以每次切换语料类型先拿 200 字样本跑一遍再定参数。5.3 自建词库全是大词模型开始过度合并原本身为独立的单字词消失现象自建词库里加入了《汉书》的“汉王”“汉军”“汉室”跑完后文本里所有单独出现的“汉”字都被合并进这三类大词人名“汉孺”反而没被切出来。原因大词权重过高且没有限制最大词长。词库合成时我把专名权重调到了 4.0直接压过了上下文特征。解决把专名权重降到 2.0 附近并给词表增加一条限制超过 4 个字的候选词需要额外满足“语料中出现次数大于 5”才允许作为整体切出。这个办法是某次调参会议上一位做古籍整理的开发者教的从那以后我所有自建词库都默认开最长词限制副作用是少数五个字的长官名会被切碎但绝大多数场景利大于弊。5.4 断句模型在直接引语上反复翻车引号里的问句被标成句号现象文本里的直接引语“王曰‘子将何之’”模型在“何之”后输出句号而不是问号。原因训练语料里直接引语的问句样本占比太少模型倾向把所有句末都输出句号。解决后处理时检测引号内句尾如果句尾词是“乎”“邪”“耶”“欤”强制把句号改为问号。这个规则朴素但有效因为古文疑问句的句尾语气词高度固定。这个坑的本质是模型对低频模式的学习能力弱于高频模式你不能指望它面面俱到但可以用规则补足系统性遗漏。我后来把这类规则做成一张表跑不同文本时按文体启用不同的规则集奏疏类启用“奏对规则集”史书类启用“对话规则集”效果比分头调模型靠谱得多。6. 进阶用法把词库合成当成迭代杠杆用两轮标注完成领域适配工具包用熟之后真正决定效果上限的已经不是参数而是词库和语料的迭代循环。我目前实践下来最有效的一套流程是“两轮标注法”特别适合你手头有一批标注成本极高的古籍语料时使用。第一轮用小规模种子语料两千字左右配合默认词库跑一遍人工校对分词和词性结果把错切、漏切的地方记下来将纠错结果合并回词库生成 v2 词库。第二轮用 v2 词库跑完整语料观察新出现的未登录词和标点误判再针对性补充专名表或调整断句阈值。这个循环每次两小时左右一般跑三轮之后一部中等篇幅的古籍就能达到可用的标注质量。进阶技巧里我最想强调的是不要只把词库当“输入”要把词库当成模型的“记忆外挂”。每次跑完一批语料就把标注结果里高频且置信度高的新词追加进词库相当于让模型在下一轮时“记得”这批文本的特色。我处理某部明代笔记时第一轮跑出不少“锦衣卫”“东厂”相关的专名把这些人名和机构名追加进词库后第二轮再从新文本里抽人名时错误率掉了一多半。反过来词库里永远要留一条“脏词隔离区”把明显是 OCR 错字或生造词的条目单独放一个文件绝不并进主词库否则错误会通过词库“传染”给下一轮标注。验证模型效果时我习惯用一组不变的“基准句”来回归测试。选二十句左右覆盖典型句式和词性分布的文言句子每次调参数或改词库后都跑一遍对比输出差异。这个习惯帮我抓出过不少“改好一个地方、弄坏两个地方”的隐蔽回归。说明一下这些基准句的选取不要只挑容易的一定要包含判断句、被动句、疑问句、长定语、人名地名交替出现这五类至少各两句才能覆盖主要的变化维度。这个习惯的由来是因为我有一次调整专名权重后全文 F1 看着涨了但一查基准句发现三个人名全被切错——要不是有基准句在这个 bug 可能就带着发布了。希望这篇笔记能帮你把 Jiayan 真正用起来让你的语料在处理之前先“心里有底”少走那些我走过的冤枉路。本文还有配套的精品资源点击获取