ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于RAG与Agent Skill的古籍智能问答系统实战:从语料清洗到混合检索

基于RAG与Agent Skill的古籍智能问答系统实战:从语料清洗到混合检索 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段序号3text原文子曰巧言令色鲜矣仁。char_start字符起始位置1024char_end字符结束位置1041source语料来源公开电子文本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 古籍项目覆盖面很广但引文错误率很高用户用一次就不想用了。宁可回答得少一点、保守一点也要保证每一条引文都能对上原书。第三个体会是评测比开发更重要。这个项目里我花在评测上的时间大概占了三成但正是这些评测让我能快速发现回归问题保证每次改动都不会让准确率下降。如果你也在做类似的项目我强烈建议一开始就把评测框架搭起来哪怕只是最简单的自动校验。最后分享一个小技巧古籍问答的提示词里加一句“如果不确定请说‘现有语料无法回答’”能显著降低模型的幻觉率。这句话看起来简单但效果比很多复杂的约束都管用。
返回列表