1. 缘起:一个古籍爱好者的技术冲动
1.1 为什么我会盯上“古籍”这个方向
先说清楚这个项目到底要干什么。古籍 skill 项目,核心目标是把中国历代典籍——经史子集、笔记方志、金石碑帖——整理成一套可被 AI 智能体直接调用的结构化技能包,让一个通用大模型在面对“帮我查一下《说文解字》里‘仁’字的训释”“《资治通鉴》里赤壁之战前后三个月发生了什么”“这句诗出自哪本集子、哪个版本”这类问题时,能给出有出处、有版本、有上下文的高质量回答,而不是一本正经地胡说八道。
我接触古籍这件事其实挺偶然。几年前帮一位做地方志研究的朋友整理一批影印本,他抱怨说现在用 AI 查古籍,十次有八次是编的,引文对不上原书,卷次张冠李戴,甚至连作者都搞错。我当时的第一反应是:这不就是典型的 RAG 场景吗?把古籍文本灌进知识库,检索出来再让模型组织答案,理论上就能解决。但真正动手之后才发现,古籍这个领域跟普通文档问答完全不是一回事——繁简转换、异体字、避讳字、版本差异、句读标点、注疏体例,每一个都是坑。这个项目就是在这种“不服气”的心态下起步的。
适合看这个系列的人,我大致分三类:一是对古籍数字化、古典文献处理有兴趣的技术人;二是想把 RAG、Agent Skill 这套东西落到具体垂直领域的产品或研究者;三是单纯好奇“AI 到底能不能读懂古书”的爱好者。不管你是哪一类,我都会把踩过的坑、做过的取舍、跑通的流程原原本本写出来,能抄的作业直接抄。
1.2 从“古籍”到“skill”:一次概念上的对齐
在动手之前,得先把几个词的含义对齐,不然很容易各说各话。
古籍在这里不是泛指“旧书”,而是特指有版本价值、有文献学意义的传世文本。它有几个绕不开的特征:竖排繁体、无标点或只有句读、大量异体字与通假字、注疏与正文混排、同一本书有多个版本且内容有出入。这些特征决定了它不能像处理现代 PDF 那样一把梭。
skill这个词最近在智能体圈子里很热,指的是把某一类任务的能力封装成一个可被 Agent 调用的模块——它可能是一段提示词、一组工具函数、一套检索策略,或者三者的组合。一个 skill 的本质是“让模型在特定场景下表现得更专业”。古籍 skill 就是专门服务古籍问答场景的能力包。
RAG(检索增强生成)是这个项目的技术底座。简单说就是:模型回答之前,先去知识库里把相关原文捞出来,再基于原文组织答案。古籍场景下 RAG 的价值尤其大,因为古籍问答对“出处准确性”的要求极高,模型自己记的东西不可信,必须靠检索兜底。
把这三个词串起来,项目的定位就清楚了:用 RAG 做检索底座,用 skill 做能力封装,最终交付一个能准确回答古籍问题的智能体模块。这个定位决定了后面所有的技术选型和工程取舍。
1.3 一个真实需求倒推出来的项目边界
我给自己定的第一个验收标准很朴素:拿二十个真实的古籍问题去测,包括字词训释、人物生平、事件始末、诗文出处、版本比对五类,要求答案必须给出原文引用和出处,且引用内容能在语料里逐字对上。这个标准听起来不高,但真做起来,光是“逐字对上”这一条就卡掉了市面上大部分方案。
为什么强调逐字对上?因为古籍领域有个特殊性:错误的引用比没有引用更糟糕。现代文档问答里,模型稍微改写一下原意,用户可能察觉不到;但古籍引文一旦错一个字,可能就是另一个版本、另一层意思,甚至直接改变学术判断。所以这个项目的边界从一开始就很明确——宁可回答得保守,也不能编造出处。
这个边界也直接影响了后面的架构选择:检索层必须能精确定位到“某书某卷某篇某段”,生成层必须被强约束在检索结果内作答,评测层必须能自动校验引文一致性。这三条要求,构成了整个项目的技术骨架。
2. 整体设计与思路拆解
2.1 为什么不做“大而全”,先做“小而准”
项目启动时,我面临一个经典的选择:是先把尽可能多的古籍灌进知识库,追求覆盖面,还是先选一小批文本,把准确率做到极致?
我选了后者,理由有三。第一,古籍的数字化质量参差不齐,很多公开语料是 OCR 直接出来的,错字漏字严重,灌得越多噪声越大。第二,RAG 的检索质量跟语料结构强相关,如果语料本身没有清晰的卷、篇、段层级,检索出来的片段就是一团糊,模型没法用。第三,也是最重要的一点——古籍问答的用户对准确率的容忍度极低,一个错误就足以让人对整个系统失去信任,所以早期必须用“准”来建立口碑,而不是用“全”来堆数量。
具体做法是:先选《论语》《道德经》《诗经》三部篇幅适中、版本相对统一、注疏资料丰富的经典作为种子语料,把整条链路跑通、跑准,再逐步扩展。这个策略后来被证明是对的,因为前三个月里我改动的 80% 都是链路问题,而不是语料问题。
2.2 技术选型的三个关键取舍
取舍一:向量检索还是关键词检索?
古籍问答里,很多问题是精确的字词查询,比如“‘克己复礼’出自哪里”。这种场景下,纯向量检索反而不如关键词检索准,因为向量模型对古汉语的语义表征能力有限,容易把意思相近但字面不同的段落排到前面。我的方案是混合检索:先用 BM25 类关键词检索召回候选,再用向量检索做语义重排,两者加权融合。实测下来,混合检索的 Top-5 命中率比单用向量高出将近 30 个百分点。
取舍二:整段检索还是分句检索?
古籍的段落往往很长,一段里可能包含多个主题。如果整段检索,召回的内容太杂,模型容易被无关信息干扰;如果分句检索,又容易丢失上下文,导致断章取义。我的做法是分层检索:先按“篇”级别粗筛,再在篇内按“句群”级别精排,最后把命中句群的前后各两句一起送给模型作为上下文。这样既保证了精度,又保住了语境。
取舍三:让模型自由生成还是强约束生成?
前面说过,古籍引文必须逐字准确。所以生成层我用了强约束:提示词里明确要求模型只能使用检索结果中的原文,不得改写、不得补充、不得推断;如果检索结果不足以回答,必须明确说“现有语料无法回答”。这个约束会牺牲一部分回答的流畅度,但换来的是可信度。我个人的判断是,在古籍这个场景下,可信度远比流畅度重要。
2.3 skill 封装:把能力变成可复用的模块
链路跑通之后,下一步是把它封装成 skill。为什么要封装?因为裸的 RAG 链路只能回答“查资料”这一类问题,而真实的古籍使用场景要复杂得多——用户可能想比对版本、想追溯某个词的用法演变、想按主题聚合相关段落。这些需求需要不同的检索策略和提示词模板,如果每次都从头拼,维护成本极高。
skill 封装的核心思路是把“意图识别 + 检索策略 + 生成模板”打包成一个可调用的单元。比如“字词训释 skill”负责处理单字单词的释义查询,“出处溯源 skill”负责处理引文出处查询,“事件脉络 skill”负责处理历史事件的来龙去脉。每个 skill 内部有自己的检索参数和提示词,对外只暴露一个统一接口。这样既保证了专业性,又方便扩展。
3. 核心细节解析与实操要点
3.1 语料清洗:古籍数字化的第一道坎
语料清洗是整个项目里最枯燥、但最不能省的一步。我拿到的原始语料大致有三种来源:公开的电子文本、影印本 OCR 结果、以及手工录入的片段。这三种来源的噪声类型完全不同,处理方式也不一样。
公开电子文本的主要问题是繁简混杂和异体字不规范。比如“説”和“说”、“為”和“为”在同一份文件里混用,如果不统一,检索时同一个字会分裂成多个 token,召回率直接腰斩。我的处理方式是先做繁简统一(统一转成繁体,因为古籍原貌是繁体),再做异体字归并(把“峯”“峰”这类异体归到正字),最后建立一张映射表,保留原始字形以便回溯。
OCR 结果的主要问题是形近字错误和版面串行。竖排古籍 OCR 特别容易把“曰”识别成“日”,把“己”“已”“巳”搞混,还会因为版框、鱼尾、批注的干扰把不同行的字串到一起。这类错误没法靠规则完全修掉,我的做法是先用规则过滤明显的串行(比如一行里出现两个不相关的篇名),再用一个小模型做形近字纠错,最后人工抽检。抽检比例我定在 5%,实测下来能覆盖大部分高频错误。
手工录入的片段问题最少,但格式最乱,需要统一成“书-卷-篇-段”的四级结构。这个结构是后面检索的基础,必须一开始就定死。
提示:语料清洗阶段一定要保留原始文本和清洗后文本的对照关系,后面排查检索问题时,经常需要回溯到原始文本确认到底是语料错了还是检索错了。
3.2 结构化切分:让每一段都有“身份证”
古籍的层级结构是“书→卷→篇→章→句”,但不同书的层级深度不一样,有的书没有“章”,有的书“篇”下面直接就是“段”。如果强行套一个固定层级,反而会破坏原书结构。我的方案是用统一的数据结构承载不统一的层级:每个文本片段都带一组元数据,包括书名、卷次、篇名、段落序号、字符起止位置,层级缺失的字段留空。这样检索时既能按层级过滤,又不会因为层级不齐而丢数据。
切分的粒度我试过三种:按自然段、按固定字数、按语义句群。最后选了按语义句群为主、固定字数为辅的混合策略。原因是古籍的自然段有时候特别长(比如《孟子》里有些段落上千字),直接切会超出模型上下文;而纯按字数切又容易把一句话拦腰截断。混合策略是先按句号、分号、问号这些强标点切成句群,如果某个句群超过 500 字,再按逗号二次切分。实测下来,这个粒度在检索精度和上下文完整性之间平衡得最好。
3.3 检索策略:混合检索的参数怎么调
混合检索的核心是两个参数:关键词检索的权重和向量检索的权重。这两个权重不是拍脑袋定的,我用了一个小规模的标注集来调——找了 100 个真实古籍问题,人工标注每个问题的正确答案所在段落,然后网格搜索权重组合,看哪个组合的 Top-5 命中率最高。
调参的结果有点反直觉:关键词检索的权重比向量检索高,大概 6:4 的样子。原因是古籍问题里精确查询占比很高,用户问“某句话出自哪里”的时候,关键词匹配的可靠性远高于语义匹配。但向量检索也不能去掉,因为有些问题是语义性的,比如“《庄子》里讲‘无用之用’的段落有哪些”,这种问题关键词匹配不到,必须靠向量。
另外还有一个细节:检索时要对书名、篇名做加权。如果用户问题里明确提到了书名,那么来自该书的片段应该获得额外加分。这个加权看起来简单,但对准确率的提升很明显,因为古籍里很多表述是跨书重复的,不加权的话很容易召回错误来源。
3.4 生成约束:怎么让模型“不敢乱说”
生成层的约束我用了三层。第一层是提示词约束,明确告诉模型“只能使用检索结果中的原文,不得改写、不得补充、不得推断”。第二层是引用格式约束,要求模型在每处引用后标注出处,格式统一为“《书名·篇名》”。第三层是后置校验,模型输出后,用程序自动检查每处引文是否能在检索结果里逐字找到,找不到的直接标记为“待人工复核”。
这三层里,第三层是最关键的。因为提示词约束再严,模型偶尔还是会“手滑”改写一两个字,后置校验能把这些漏网的抓出来。校验的实现不复杂,就是把模型输出的引文和检索结果做字符串匹配,允许一定的标点差异,但不允许任何实词差异。实测下来,加了后置校验之后,引文错误率从 8% 降到了 1% 以下。
注意:后置校验只能保证“引文在检索结果里存在”,不能保证“引文确实回答了问题”。后者需要人工抽检或者更复杂的评测,不能指望自动校验全包。
4. 实操过程与核心环节实现
4.1 环境搭建:从零到能跑通的最小闭环
环境这块我尽量选轻量的方案,避免一上来就搞重型基础设施。核心依赖就三样:一个向量数据库、一个嵌入模型、一个生成模型。向量数据库我用了本地可跑的轻量方案,嵌入模型选了支持中文的通用模型,生成模型先用本地部署的开源模型跑通流程,后面再考虑接更强的模型。
搭建顺序是这样的:先把语料清洗和切分的脚本跑通,产出一批结构化的 JSON 文件;再把这些 JSON 灌进向量库,同时建立关键词索引;然后写一个最简单的检索函数,输入问题、输出候选段落;最后接上生成模型,跑通“问题→检索→生成”的完整链路。这个最小闭环大概花了两天,但后面所有的优化都是在这个闭环上迭代。
这里有个经验:不要一开始就追求端到端自动化。我见过不少人一上来就搭复杂的 pipeline,结果某个环节出错,排查半天找不到问题在哪。先把每个环节单独跑通、单独验证,再串起来,效率反而高。
4.2 语料入库:结构化数据的组织方式
入库这一步的关键是元数据设计。我给每个文本片段设计了这样一组字段:
| 字段名 | 含义 | 示例 |
|---|---|---|
| book | 书名 | 论语 |
| volume | 卷次 | 卷一 |
| chapter | 篇名 | 学而 |
| section | 段序号 | 3 |
| text | 原文 | 子曰:巧言令色,鲜矣仁。 |
| char_start | 字符起始位置 | 1024 |
| char_end | 字符结束位置 | 1041 |
| source | 语料来源 | 公开电子文本A |
这组字段里,book、volume、chapter用于检索时的层级过滤,char_start、char_end用于回溯原文,source用于版本比对。看起来简单,但每一个都是后面会用到的。比如做版本比对的时候,同一段话在不同来源里的文本可能不同,靠source字段就能快速定位差异。
入库时还有一个细节:同一段文本要同时进关键词索引和向量索引。关键词索引用于精确匹配,向量索引用于语义匹配,两者共享同一份元数据。这样检索时可以先分别召回,再按元数据去重合并。
4.3 检索链路:一次完整查询的拆解
拿一个真实问题来拆解检索链路:“《论语》里‘巧言令色’这句话完整的是什么,出自哪一篇?”
第一步是意图识别。这个问题包含两个意图:查原文、查出处。意图识别可以用规则做,也可以用模型做。我早期用规则,后来发现规则覆盖不全,就换成了小模型分类,准确率能到 90% 以上。
第二步是查询改写。用户问题里的“巧言令色”是关键词,但直接拿这四个字去检索,可能召回多个包含这个词的段落。所以要把问题改写成更适合检索的形式,比如提取出核心词“巧言令色”,加上书名限定“论语”。
第三步是混合检索。关键词检索召回包含“巧言令色”的段落,向量检索召回语义相关的段落,两者按权重融合,取 Top-10。
第四步是重排。Top-10 里可能有噪声,用一个小的重排模型再排一次,取 Top-3 送给生成模型。
第五步是生成与校验。生成模型基于 Top-3 组织答案,后置校验检查引文一致性。
这条链路跑下来,一个问题的处理时间大概在 2-3 秒,其中检索占大头。如果追求速度,可以在检索层加缓存,把高频问题的检索结果缓存起来。
4.4 参数计算:切分粒度与检索窗口的确定
切分粒度和检索窗口这两个参数是相互关联的,需要一起定。我的计算逻辑是这样的:
假设生成模型的上下文窗口是 4096 token,那么送给模型的检索结果不能超过这个限制。如果每个片段平均 200 字(约 300 token),那么最多能送 10 个片段。但实际使用中,为了保证生成质量,我一般只送 3-5 个片段,留出空间给提示词和生成内容。
反过来推切分粒度:如果片段太短(比如 50 字),检索精度高但上下文不足;如果片段太长(比如 1000 字),上下文足但噪声大。折中下来,200-500 字是比较合适的区间。这个区间既能保证一个完整的语义单元,又不会超出上下文限制。
检索窗口我定的是“命中片段 + 前后各两句”,这样既能保证命中内容的完整性,又能提供必要的语境。实测下来,这个窗口大小在准确率和速度之间平衡得不错。
5. 常见问题与排查技巧实录
5.1 检索召回不准:从三个方向排查
检索召回不准是最常见的问题,排查时我一般按三个方向走。
方向一:语料问题。先确认目标段落是否真的在语料里,以及语料里的文本是否和预期一致。我遇到过好几次“检索不到”,最后发现是语料清洗时把某个字误改了,导致关键词匹配不上。排查方法很简单:直接用目标段落里的一个独特词去搜,看能不能搜到。
方向二:切分问题。如果目标段落被切成了多个片段,检索时可能只召回其中一部分,导致上下文不完整。排查方法是看目标段落的切分边界,确认关键信息是否被切到了不同片段里。
方向三:权重问题。如果关键词检索和向量检索的权重不合适,可能导致正确答案排在后面。排查方法是把两个检索的结果分别打出来看,确认正确答案在哪个检索里排名靠前,然后调整权重。
5.2 生成内容跑偏:约束失效的几种情况
生成内容跑偏通常有几种表现:改写了原文、补充了检索结果里没有的内容、引用了错误的出处。这几种情况的成因不同,处理方式也不同。
改写原文一般是因为提示词约束不够强,或者检索结果里的原文本身就有多个版本,模型不知道该用哪个。处理方式是加强提示词,明确要求“逐字引用”,同时在检索结果里标注版本来源。
补充内容是模型“自作聪明”的典型表现,尤其是在检索结果不足以回答问题时,模型倾向于用自己的知识补全。处理方式是明确告诉模型“如果检索结果不足,必须说无法回答”,并且在后置校验里检查是否有检索结果之外的实词出现。
引用错误出处一般是元数据没带对,或者模型把多个片段的出处搞混了。处理方式是要求模型在每个引用后立即标注出处,而不是在最后统一标注,这样能减少混淆。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 检索不到目标段落 | 语料缺失或清洗错误 | 用独特词直接搜 | 回溯原始语料,修正清洗规则 |
| 召回内容不完整 | 切分粒度过细 | 检查切分边界 | 调整切分粒度或窗口大小 |
| 正确答案排名靠后 | 检索权重不合适 | 分别看两个检索的排名 | 调整关键词与向量权重 |
| 生成内容改写原文 | 提示词约束不足 | 检查提示词 | 加强约束,加后置校验 |
| 引用出处错误 | 元数据缺失或混淆 | 检查片段元数据 | 要求即时标注出处 |
| 回答“无法回答”过多 | 检索召回不足 | 检查召回数量 | 放宽检索条件或增加召回数 |
5.4 几个踩过的坑和独家技巧
坑一:繁简转换的陷阱。我一开始用了一个通用的繁简转换工具,结果发现它把一些古籍里的专有名词也转了,比如“乾隆”被转成“干隆”。后来改成只对非专有名词做转换,专有名词保留原字形。
坑二:标点符号的干扰。古籍原文很多没有标点,我加标点时用了现代标点规则,结果发现有些地方加错了,反而影响了检索。后来改成只加句读级别的标点,不加逗号顿号,减少干扰。
技巧一:用“反向检索”验证语料质量。随机抽一批语料片段,拿片段里的句子去检索,看能不能检索回原片段。如果检索不回来,说明语料或索引有问题。这个方法能快速发现系统性问题。
技巧二:给高频问题建缓存。古籍问答里,高频问题占比很高,比如“《论语》有多少篇”“《道德经》第一章是什么”。这些问题可以直接缓存答案,不用每次都走完整链路,能大幅提升响应速度。
技巧三:用“出处一致性”做自动评测。拿一批已知出处的问题去测,看模型给出的出处和标准答案是否一致。这个评测不需要人工,可以自动化跑,适合做回归测试。
6. 后续扩展方向与个人体会
6.1 从单书到丛书:语料扩展的节奏
种子语料跑通之后,下一步是扩展。但扩展不是简单地往里灌数据,而是要有节奏。我的计划是分三批:第一批是儒家经典,第二批是道家和其他诸子,第三批是史书和集部。每批扩展之后都要重新跑评测,确认准确率没有明显下降,再进下一批。
扩展时最大的挑战是版本差异。同一本书的不同版本,文本可能有出入,如果都灌进去,检索时会出现多个版本混在一起的情况。我的处理方式是给每个版本打标签,检索时默认只用主版本,需要版本比对时再显式指定。
6.2 skill 生态:从单一 skill 到 skill 组合
单个 skill 只能解决单一类型的问题,真实使用中往往需要多个 skill 协作。比如用户问“《论语》里‘仁’字出现了多少次,分别在哪些篇”,这个问题需要“字词统计 skill”和“出处溯源 skill”配合。所以下一步是把 skill 做成可组合的,让一个主 skill 能调度多个子 skill。
组合的关键是意图路由。主 skill 先识别用户意图,再决定调用哪些子 skill,最后把结果合并。这个路由逻辑可以用规则做,也可以用模型做,我倾向于用模型做,因为古籍问题的意图组合太灵活,规则很难覆盖全。
6.3 我个人的几点体会
做这个项目最大的体会是:古籍这个领域,技术只是工具,文献学才是内核。我一开始想用纯技术手段解决所有问题,后来发现很多问题的根源在文献学层面——版本怎么选、异体字怎么归并、注疏怎么处理,这些都不是技术能拍板的,必须懂一点文献学的基本知识。
第二个体会是:准确率是古籍问答的生命线。我见过太多 AI 古籍项目,覆盖面很广,但引文错误率很高,用户用一次就不想用了。宁可回答得少一点、保守一点,也要保证每一条引文都能对上原书。
第三个体会是:评测比开发更重要。这个项目里我花在评测上的时间大概占了三成,但正是这些评测让我能快速发现回归问题,保证每次改动都不会让准确率下降。如果你也在做类似的项目,我强烈建议一开始就把评测框架搭起来,哪怕只是最简单的自动校验。
最后分享一个小技巧:古籍问答的提示词里,加一句“如果不确定,请说‘现有语料无法回答’”,能显著降低模型的幻觉率。这句话看起来简单,但效果比很多复杂的约束都管用。