模拟专家工作流的甲骨文辅助释读系统原型设计
甲骨文释读并不是把一张拓片丢给模型、让它直接输出一个字那么简单。真正的释读过程是一套围绕字形、部件、辞例、历史演变和出土信息展开的多重证据考据流程。这也是为什么很多 OCR 或图像识别方案在普通文字资料上表现不错一旦遇到甲骨文就失效因为系统缺少对人类专家工作流的模拟。这篇文章要讨论的是如何把一个模仿人类专家工作流的辅助释读系统落地成一个可运行、可追溯、可扩展的原型而不是做一个“一键出字”的黑盒。所谓“模仿人类专家工作流”并不是让产品界面长得像某个考古工具而是要把专家释读甲骨时经历的观察、拆解、检索、假设、验证、定案这一套动作抽象成计算机可以执行的阶段和状态。系统只负责提供证据、排序候选、记录判断过程最终是否采信仍由专家决定。这样既保留考据的严谨性也让每一次释读结论都有过程留痕便于复查和修正。1. 为什么甲骨文释读需要一套可模仿专家工作流的系统1.1 甲骨文释读的难点不在单个字形而在证据链甲骨文距今约三千年很多字形在现代人看来非常陌生。单个字形可以拆成若干部件但部件与部件之间的组合方式并不固定同一个字在不同时期、不同贞人、不同版面上又存在异写。更麻烦的是很多字在史料中没有现成对照需要结合卜辞语境、同版对贞、后世金文、篆书演化等线索综合判断。这意味着释读一个甲骨字本质上是建立一条证据链字形结构支持什么解读辞例位置是否吻合后世字书有没有佐证同批甲骨中是否出现可类比写法。如果系统只输出一个候选结果不展示证据链专家很难判断这个结果是否可信也无法在论文或考释中引用系统依据。1.2 常见 AI 辅助方案的短板现在有不少项目会把甲骨文识别做成图像分类问题收集一批已标注字形图片训练卷积神经网络输入新字形后直接输出类别。这种方案在公开数据集上可以取得不错的分数但在真实释读场景中会暴露出三个明显问题。第一训练数据通常是已经整理好的单个字形图片而真实拓片上字形与刻痕、裂纹、拓印噪声混在一起系统缺乏从原图到单字的定位能力。第二模型输出的是一个固定字表里的类别遇到未登录字或异写字时无法给出“最接近的部件组合”或“可参考的考释意见”。第三释读过程不可解释专家不知道模型为什么给出这个结论也无法在模型结果基础上局部修正。所以只做端到端识别不足以支撑真正的辅助释读。更合理的方式是把专家工作流拆成若干可校验的步骤每个步骤的输出都作为下个步骤的输入同时把每一步的关键证据保存下来。1.3 把专家工作流建模成系统流程一位甲骨文专家拿到一份拓片或字形图片时通常不会立刻下结论。他首先会观察字形轮廓、刻痕方向、残断情况然后尝试拆出偏旁或部件与已知部件库对照再把候选字形放回卜辞原文看辞例通不顺最后结合历史考释文献和后世文字演化综合给出释读建议。这个过程可以映射为一条计算流水线观察对原始图像做增强、去噪和字形定位。拆解把字形切分为部件或者提取局部特征块。检索用部件和字形全局特征匹配已有字形库、部件库和语料库。假设根据匹配结果生成候选释读并附上证据文本。验证把候选字放回辞例计算上下文合理性。定案由专家确认或修正并将结果写回知识库。这套流程既能支撑专家复审也能让系统在数据积累后逐步提升。后续训练的模型可以替换流水线中的单个模块而不必推翻整体设计。2. 系统设计从专家考据过程到模块化流水线2.1 整体架构与数据流向辅助释读系统的架构可以分成四层数据层、算法层、工作流层和交互层。数据层存放原始拓片、切分后的字形图、部件标注、卜辞语料、专家释读记录。算法层负责图像预处理、特征提取、候选检索和置信度计算。工作流层维护每条释读任务当前处于哪个阶段以及阶段之间允许的转移关系。交互层面向专家提供观察、修改、批注和确认入口。数据流向是这样的原始图片经过预处理模块得到候选字形区域每个字形区域进入特征提取模块转成向量向量在数据库中检索相似字形和部件检索结果交给解释模块生成候选字表和证据文本工作流状态机记录这个过程中的每一步专家在交互层看到结果后可以同意、修改或补充证据最终写回标注库。这种分层的好处是模块之间只通过标准数据结构通信。例如预处理模块输出的是归一化坐标和裁剪图检索模块不关心坐标来自哪个预处理算法工作流模块只负责状态转移不关心特征向量具体如何计算。2.2 工作流状态机设计为了让系统真正“模仿专家工作流”状态机设计是关键。每个释读任务必须明确处于哪个阶段不能跳过必要步骤也不能在证据不足时直接进入最终结论。这里定义一套最小状态集状态含义输入输出observe观察字形与图像质量原始图片字形定位框、预处理图decompose拆解字形结构字形图部件列表、局部特征块retrieve检索相似字形与部件特征向量候选字形集、相似度分数hypothesize生成释读假设候选结果候选字表、证据说明verify验证辞例与文献候选字表上下文匹配度、文献线索dictate专家定案候选结果与证据最终释读、批注review复核并入库最终释读标注记录、反馈日志状态转移不是发散的而是有方向的。比如从 hypothesize 可以回到 retrieve说明候选集不足从 verify 可以回到 hypothesize说明辞例验证不通过但不能从 observe 直接跳到 dictate因为这样会绕过证据构建过程。状态机中每个节点都应当保留当前任务的操作人、时间、输入摘要和输出摘要。这样做一方面方便专家回看另一方面也为数据集迭代积累过程数据。2.3 模块边界与接口约定模块之间的接口越稳定系统越好维护。这里定义几个核心数据接口。字形区域用归一化坐标表示而不是直接保存绝对像素。因为同一张拓片可能被缩放、裁切或放到不同屏幕上展示归一化坐标可以避免坐标错位。部件的候选结构建议包含部件名、匹配分数、匹配区域坐标、来源字形编号。这样解释模块可以知道“这个部件来自哪个字形”而不是只看到一个孤零零的名字。候选释读结果建议包含字头、置信度、证据列表、支持材料编号。证据列表要尽量结构化比如“部件匹配得分 0.81”“辞例《合集》12345 中出现该字形”“同版有对贞字形”。结构化证据比一段自然语言更容易被后续程序二次处理。3. 环境准备与项目初始化3.1 技术栈选型原型系统可以使用 Python 3.9 以上版本配合 OpenCV、NumPy、scikit-learn、Flask 和 SQLite。这套组合在普通开发机上就能运行不依赖 GPU适合学习阶段先跑通工作流。生产环境如果要处理大批量拓片通常会引入更深的模型、向量数据库和对象存储但原型阶段越简单越好。选型时重点考虑三点图像处理库是否成熟、Web 框架是否容易写接口、数据库是否方便存储结构化证据。这里列出原型依赖的清单依赖用途版本建议Python开发语言3.9opencv-python图像预处理与特征提取4.xnumpy向量计算1.24scikit-learn相似度检索与指标计算1.3flask提供 Web API2.3sqlite3存储字形、任务与反馈Python 内置如果原始材料没有明确版本落地前要先确认当前环境中依赖版本彼此兼容尤其是 opencv-python 与 numpy 的版本组合。3.2 目录结构与配置文件一个清晰的项目目录能降低理解成本。推荐按职责划分目录而不是把代码全部堆在入口文件里。oracle_assistant/ ├── config.yaml ├── requirements.txt ├── app/ │ ├── __init__.py │ ├── main.py │ ├── workflow.py │ ├── preprocessing.py │ ├── feature.py │ ├── retrieval.py │ ├── explain.py │ └── store.py ├── data/ │ ├── raw/ │ ├── crops/ │ ├── components.json │ ├── corpus.json │ └── oracle.db └── frontend/ └── index.htmlraw目录放原始拓片图crops放切分出来的单字图components.json存放部件字典corpus.json存放卜辞语料。初始阶段都可以用少量示例数据验证流程。config.yaml用来集中管理参数避免把阈值写死在代码里。project: name: oracle_assistant data_dir: ./data db_path: ./data/oracle.db preprocess: min_contour_area: 100 target_size: 64 feature: method: hog hog_block_size: 16 hog_cell_size: 8 hog_bins: 9 retrieval: top_k: 10 similarity_threshold: 0.72 workflow: stages: - observe - decompose - retrieve - hypothesize - verify - dictate - review参数集中管理后调参不需要改代码也方便实验对比。3.3 数据库表结构与初始化数据库主要保存字形图片、释读任务、专家决策和反馈日志。表结构要尽量简单但需要支持状态流转和证据追溯。CREATE TABLE glyphs ( id INTEGER PRIMARY KEY, image_path TEXT NOT NULL, source_book TEXT, source_rubbing TEXT, label TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE decisions ( id INTEGER PRIMARY KEY, glyph_id INTEGER NOT NULL, stage TEXT NOT NULL, status TEXT NOT NULL, confidence REAL, evidence TEXT, expert_comment TEXT, created_by TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (glyph_id) REFERENCES glyphs(id) ); CREATE TABLE feedback_log ( id INTEGER PRIMARY KEY, decision_id INTEGER NOT NULL, action TEXT NOT NULL, detail TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (decision_id) REFERENCES decisions(id) );这里把每个阶段的决策都存成一条decisions记录而不是只存最终结果。这样专家可以在feedback_log里看到“什么人在什么时间改了什么候选字”这是构建可审计释读流程的基础。初始化数据库时可以先往glyphs表插入少量图片路径用于测试。代码中建议加一个init_db()函数在服务启动时自动建表。4. 核心实现按专家步骤拆解算法模块4.1 图像预处理与字形定位图像预处理的目的是把拓片上的刻痕从背景中分离出来。真实拓片往往有纸纹、裂纹和深浅不一的墨色直接二值化容易把噪声也当成字形。这里使用高斯模糊消除部分噪声再用 Otsu 自适应阈值做二值化最后用轮廓检测找出候选字形区域。min_contour_area用于过滤过小的噪声块避免把污渍当作字形。import cv2 import numpy as np def preprocess_and_locate(image_path, min_area100): img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(fCannot read image: {image_path}) blurred cv2.GaussianBlur(img, (3, 3), 0) _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 反转二值图确保字形区域为白色前景 binary_inv cv2.bitwise_not(binary) contours, _ cv2.findContours( binary_inv, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) boxes [] for contour in contours: area cv2.contourArea(contour) if area min_area: continue x, y, w, h cv2.boundingRect(contour) boxes.append({x: x, y: y, width: w, height: h, area: area}) return boxes这里要注意坐标值是相对原图的像素坐标。如果后续要展示到网页或保存到数据库建议额外保存图像宽高或者转换为归一化坐标避免前端拿到不同分辨率的图时对不齐。4.2 字形部件分解与特征提取在简单原型中部件分解可以先用连通域把字形拆成多个局部块。更复杂的方式是训练一个部件检测模型但学习阶段不需要一上来就依赖标注数据。如果components.json已经记录了字形中每个部件的外接框那么系统可以直接按框裁剪图片再用特征提取得到向量。{ components: [ { id: c001, name: 又, image: data/crops/c001.png, description: 表示手部的部件 }, { id: c002, name: 示, image: data/crops/c002.png, description: 表示神主或祭祀的部件 } ] }特征提取这里采用 HOG 特征。HOG 描述的是图像局部梯度方向分布对笔画结构比较敏感适合作为字形检索的基线方法。先把图像统一缩放到 64x64再计算 HOG 向量。import cv2 def extract_hog(image_path, target_size(64, 64)): img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(fCannot read image: {image_path}) img cv2.resize(img, target_size) hog cv2.HOGDescriptor( target_size, (16, 16), (8, 8), (8, 8), 9 ) return hog.compute(img).flatten()HOG 参数不是唯一选择。target_size太小会丢失笔画细节太大会让向量维度过高、计算变慢。在 64x64 下HOG 向量维度通常已经足够用于原型验证。4.3 候选检索与证据拼接候选检索阶段系统拿查询字形向量去字形库里检索相似字形。这里使用余弦相似度作为距离指标因为它对向量长度不敏感更适合比较图像描述子。import numpy as np from sklearn.metrics.pairwise import cosine_similarity def retrieve_candidates(query_vec, gallery_vecs, gallery_ids, top_k10): scores cosine_similarity([query_vec], gallery_vecs)[0] ranked np.argsort(scores)[::-1][:top_k] candidates [] for idx in ranked: candidates.append( { glyph_id: gallery_ids[idx], score: float(scores[idx]), } ) return candidates在真实项目中gallery_vecs可能来自数千张已标注字形图。计算所有相似度虽然可行但数据量增大后会变慢。生产环境一般会改用向量数据库或者先用部件检索缩小候选范围再对少量候选做精细匹配。检索结果还需要和部件拆解结果合并。假设查询字形被拆成了三个部件每个部件都能在部件库中匹配到多个候选那么最终证据就可以拼接成“‘又’部件匹配到字形 A相似度 0.84‘示’部件匹配到字形 B相似度 0.79”。这种结构化证据比单一整字匹配分数更容易让专家理解。4.4 释读假设生成与置信度计算释读假设生成模块负责把候选字形、部件匹配和辞例线索整理成可读的证据并计算一个综合置信度。原型阶段置信度不需要太复杂可以用加权平均。def build_candidate(glyph_id, feature_score, component_hits, corpus_hits): score 0.5 * feature_score if component_hits: score 0.3 * np.mean([h[score] for h in component_hits]) if corpus_hits: score 0.2 * min(1.0, len(corpus_hits) / 3) evidence [] evidence.append(f整字特征匹配得分{feature_score:.2f}) if component_hits: parts 、.join([h[name] for h in component_hits[:3]]) evidence.append(f可拆解部件{parts}) if corpus_hits: evidence.append(相关辞例 .join(corpus_hits[:2])) return { glyph_id: glyph_id, confidence: round(score, 2), evidence: evidence, }这里权重只是示例。实际项目中权重应当由专家根据任务类型调整如果字形清晰但辞例缺失特征权重可以调高如果字形模糊但辞例信息丰富辞例权重应更高。把权重放到config.yaml里比硬编码更容易实验。置信度不代表真实正确率只代表系统对证据一致性的判断。因此界面中不建议显示为“系统判断 90% 正确”而应显示为“候选有 0.82 的证据置信度共 3 条证据”。5. 专家交互与结果留痕5.1 工作流状态流转接口工作流状态机要限制非法跳转。例如专家还没完成部件拆解就不能直接进入检索阶段。这里用一个 Python 类维护状态。class WorkflowState: VALID_TRANSITIONS { observe: {decompose}, decompose: {retrieve}, retrieve: {hypothesize}, hypothesize: {verify, retrieve}, verify: {dictate, hypothesize}, dictate: {review}, review: {done}, } def __init__(self, glyph_id, stageobserve): self.glyph_id glyph_id self.stage stage self.payload {} def transition(self, next_stage): allowed self.VALID_TRANSITIONS.get(self.stage, set()) if next_stage not in allowed: raise ValueError( fIllegal transition: {self.stage} - {next_stage} ) self.stage next_stage接口层用 Flask 暴露一个状态流转接口。请求体里只需要传next_stage系统返回当前状态和提示信息。curl -X POST http://127.0.0.1:8000/api/glyph/1/stage \ -H Content-Type: application/json \ -d {next_stage: decompose}响应示例{ glyph_id: 1, stage: decompose, message: 当前任务可进入部件拆解阶段 }这种接口设计确保前端只能引导专家按步骤操作避免“一键跳到最后”的坏习惯。5.2 专家确认与修正流程专家在界面中看到候选释读结果后可以选择“采纳”“修改”或“补充证据”。采纳操作会把当前候选写入decisions表同时记录操作人。修改操作需要填写新的字头和理由系统将这些信息存入feedback_log。def confirm_decision(decision_id, result_label, expert_comment, created_by): # 检查当前决策是否存在 # 更新 decisions 表的 label、expert_comment、created_by # 在 feedback_log 中追加一条 actionconfirm 的记录 pass这里不建议直接删除候选结果。即使专家认为某个候选是错误的错误的候选及其证据仍然有研究价值应该被保留下来。系统后续做模型迭代时可以把这些被否定的负样本加入训练集。专家的修正意见会写回知识库供下一次检索使用。例如专家补充“这个部件更接近‘止’而不是‘又’”系统可以把这一条记录为部件匹配规则未来遇到类似字形时优先推荐接近“止”的解释。5.3 运行验证一个最小闭环示例为了让整个流程可验证可以准备一张只有单个字形的拓片图然后依次调用各模块最后确认一条释读记录。这个最小闭环不需要完整前端通过 Python 脚本即可跑通。python app/main.py --init-db python tools/run_pipeline.py --image data/raw/example.jpgrun_pipeline.py的逻辑是调用预处理模块定位字形对每个字形提取 HOG 特征检索字形库生成候选打印证据。输出大致如下字形 1: 定位框: x120, y80, width90, height110 候选1: 字形编号 101, 置信度 0.78 证据: 整字特征匹配得分0.76 可拆解部件又、示 相关辞例暂无 候选2: 字形编号 202, 置信度 0.65 证据: 整字特征匹配得分0.64 可拆解部件又、口如果系统能按照这个顺序输出结构化证据说明流水线已经跑通。接下来可以接入 Web 前端把同样的结果展示在浏览器里。6. 常见问题与排查路径6.1 图像切分与坐标错位现象预处理得到的字形框要么把整个拓片当成一个字形要么漏掉小字。原因min_contour_area设置不合适二值化后噪声太多字形粘连严重。检查方式把二值化结果和定位框画到图上人工观察是阈值问题还是轮廓合并问题。解决方案先在config.yaml中调低或调高min_contour_area如果噪声太多改用形态学开运算去掉小噪点如果字形粘连需要使用分水岭或基于标注框的切分方法。6.2 候选检索结果发散现象查询字形和返回的候选字形在视觉上差异很大相似度分数却接近。原因特征提取粒度太粗字形库太小不同刻痕风格导致特征偏移。检查方式打印每个候选的相似度分数并对候选图做缩略图对比。解决方案增加特征向量维度使用更稳定的局部特征扩大字形库样本量或者用部件级检索替换整字级检索先找部件再组合。6.3 状态机流程卡死或重复提交现象专家点击下一步后页面无变化接口返回 400或者同一个阶段被重复记录多次。原因前端重复发送请求状态转移没有做幂等状态机不允许回退。检查方式查看后端日志确认请求参数和当前状态检查数据库里decisions表是否出现重复记录。解决方案接口层增加状态校验当前阶段与请求目标阶段一致时直接返回成功但不重复写日志允许从verify回退到hypothesize避免专家发现问题后无法修改。下表汇总常见问题问题现象常见原因检查方式处理建议定位框包含整页面积阈值过大或轮廓合并绘制定位框到原图调低 min_contour_area分离连通域小字被漏掉面积阈值过小被过滤查看轮廓面积分布按边界框宽高比和面积双重过滤特征匹配得分虚高字形库太小或特征太简单打印相似度分数分布使用更精细特征增加负样本状态机重复提交前端无幂等控制查看 feedback_log添加请求唯一标识后端去重前端坐标对不上使用绝对像素坐标对比原图与缩略图保存归一化坐标前端还原7. 从原型到生产落地注意事项与最佳实践7.1 学习环境与生产环境的差异学习环境的目标是快速跑通工作流可以使用少量本地图片和 SQLite算法模块用 HOG 这类传统方法即可。生产环境则要额外考虑数据规模、权限、审计和模型更新。生产环境至少需要补齐以下能力配置外置化数据库连接、对象存储路径、模型路径不能写死在代码里。日志与监控记录每次释读操作监控接口耗时和检索失败率。权限与审计只有具备考古或文字学背景的专家才能定案所有修改操作要留痕。模型版本管理特征提取模型升级后要能区分新旧向量避免新旧特征混用。回滚方案当专家修正了错误释读后系统要支持把已经入库的结果修改回来并保留原始记录。如果原型阶段不提前考虑这些生产接入时往往会推倒重来。7.2 可复用的工程清单以下清单可以在开发前逐项检查避免遗漏关键节点。是否定义完整的释读状态集合是否限制状态转移方向每个阶段是否保存输入、输出和操作人信息图像定位框是否使用归一化坐标特征提取参数是否集中在配置文件中检索阈值和 Top-K 是否可以动态调整候选结果是否包含结构化证据而不是只有分数专家确认后是否写回知识库形成闭环反馈日志是否保留负样本供后续模型迭代是否区分原型演示数据和生产可信数据7.3 后续扩展方向原型系统跑通后可以从三个方向继续深入。第一把整字特征匹配替换为深度学习特征提取模型例如用孪生网络学习“字形是否相似”提高异写字和残断字形的召回能力。第二构建甲骨文知识图谱。把字形、部件、辞例、出处、历代考释文献连接成图检索阶段不只做图像匹配还可以沿着知识图谱扩展相关证据生成更完整的考释建议。第三引入可配置的专家规则。专家可以把个人经验写成规则例如“当出现某两个部件组合时优先考虑指向祭祀类动词的释读”。规则可以接入explain.py与图像特征共同决定候顺序。辅助释读系统的核心价值不在某个模型有多强而在于它能把专家的工作方法沉淀为系统流程让每一份释读结论都有据可查、有迹可循。对开发者的建议是不要一头扎进最深的模型里先搭好工作流骨架再逐步替换算法模块这样才能真正做出能被文字学研究者使用的工具。

相关新闻