在图书馆书目系统和学术发现系统的实际项目中我们经常遇到一类非常棘手的问题同一部作品的不同版本、不同语种译本、不同载体形态在数据库里被拆成了成千上万条独立的书目记录而用户检索时却希望一次看到“这部作品”整体而不是反复翻页筛选。为了做到这一点编目领域提出了“超级作品”Superwork的概念但真正落地时聚类效果往往依赖人工维护或规则匹配扩展性很难保证。近年来随着本地大语言模型Local LLM能力的提升我们可以尝试用 LLM 来完成书目特征抽取、实体归一化、作品聚类辅助等任务把过去需要大量编目经验的流程转化为半自动化的聚类管线。本文将围绕这个思路从核心概念、环境准备、技术拆解到实战流程分享一套可以动手验证的方案。1. 超级作品聚类与本地 LLM它们为什么能组合1.1 从书目记录到“超级作品”在继续往下之前我们需要先统一几个术语。你如果接触过 FRBRFunctional Requirements for Bibliographic Records书目记录的功能需求会知道 FRBR 把作品分成几个层次作品Work、内容表达Expression、载体表现Manifestation和单件Item。一部小说比如《百年孤独》最初是西班牙语原稿这是“作品”后来被翻译成中文、英文、日文形成不同文字的“内容表达”每个语种又可能有精装本、平装本、电子书、有声书这些是“载体表现”而图书馆里每一本实际收藏的书则是“单件”。“超级作品”Superwork可以理解为 FRBR Work 之上再聚合一层把一部作品的所有相关表达、版本、改编、续写、甚至相关评论资源尽可能聚集到同一个逻辑簇下。这种聚类的价值非常直接用户在发现系统中搜《百年孤独》结果页可以首先展示一个作品簇再按版本/语种折叠。编目员可以减少重复书目记录的维护成本。馆藏分析、学术评价、文献计量分析时可以按作品簇统计而不是按分散的记录统计。但问题是MARC 数据、DC 元数据、出版社数据源里的题名、作者、ISBN、出版日期等信息往往并不规整。同一部作品可能以“百年孤独”、“One Hundred Years of Solitude”、“Cien años de soledad”等多个名称出现作者署名也可能有多种形式。单纯靠字符串匹配或 ISBN 匹配很难高质量聚类。1.2 为什么选择本地 LLM 而不是云端 API传统聚类方法通常依赖规则归一化作者名、清理题名大小写、按题名作者计算相似度等。规则方法的好处是稳定、可解释坏处是处理语义层面的变体非常吃力。云端大模型 API 效果虽好但在图书馆这类场景中会碰到现实问题书目数据可能包含尚未公开的馆藏细节、读者隐私或版权受限内容不适合直接发送给第三方 API。大批量书目记录的 embedding 抽取和实体归一化费用不低。学术机构、图书馆系统通常要求本地化部署数据不出内网。本地 LLM 的价值就在这里它既能提供接近云端模型的文本理解能力尤其是命名实体识别、文本分类、结构化信息抽取又能把数据留在本地。配合开源模型、向量数据库和规则引擎可以构建一条可审计、可离线运行的聚类管线。1.3 一条理想的工作流应该长什么样在动手写代码前先把目标流程画出来这里不用复杂的架构图用文字描述即可从图书馆集成系统LMS、仓储系统或 Excel/CSV 导出书目元数据。清洗数据去重、补齐作者与题名字段、识别语种与载体类型。对每一条书目记录抽取“作品指纹特征”例如规范化题名、规范化作者、语种、出版形式等。将特征交给本地 LLM执行两类任务一是从 MARC/DC 长文本中提取结构化字段二是对模糊记录对进行语义判断例如判断两组作者题名是否指向同一部“超级作品”。把 LLM 输出与规则聚类的结果融合生成作品簇 ID。将聚类结果导回发现系统索引或输出为 CSV/MARC 用于人工复核。本地 LLM 在整个流程中的定位并不是完全替代编目员也不是替代规则的精确匹配而是处理“规则判断不了”的语义匹配部分。2. 环境准备与版本说明由于手头的真实项目环境往往各不相同本文给出的版本信息只作为参考思路。实际运行时应根据你的操作系统、显卡显存和模型生态选择对应的安装命令。2.1 硬件与运行环境本地 LLM 的推理需要一定的硬件资源。如果只是处理几千条测试书目使用 CPU 运行量化后的小模型也可行但速度较慢如果要处理数万条甚至更多记录建议准备一张至少 16GB 显存的 NVIDIA GPU。一个比较常见且务实的环境组合是操作系统Ubuntu 22.04 LTSWindows 也可以但部分框架的 CUDA 环境配置会多一些步骤Python 版本3.10 或 3.11GPUNVIDIA RTX 4090 / A6000 / 3090 等 16GB 以上显存显存不够时可以把模型量化到 4bit/8bit 运行2.2 本地 LLM 推理框架选型当前开源社区常见的本地推理思路包括工具/框架特点适用场景Ollama安装简单命令友好内置模型管理个人验证、快速原型llama.cpp 系列纯 CPU/GPU 混合推理支持 GGUF 量化资源受限环境vLLM高吞吐服务化推理支持 OpenAI 兼容接口批量数据处理TransformersHugging Face 生态灵活可微调需要自定义模型与解码逻辑如果是个人电脑上做测试我建议先用 Ollama 跑一个 Qwen3、Llama 3.1 或同类中英文能力都比较好、且支持工具调用的模型。这样可以把精力聚焦在聚类管线的设计上而不是花大量时间调试推理代码。需要注意的是本地模型的能力与参数量、量化精度直接相关。不要指望一个 3B/8B 的量化模型能做到大型云端模型同样水平的复杂推理但做字段抽取、二分类判断和归一化文本效果已经足够。2.3 Python 库与数据工具本文的示例会用到以下 Python 库pandas表格数据处理requests调用 Ollama HTTP API 或 vLLM OpenAI 兼容接口rapidfuzz计算字符串相似度作为规则聚类的补充langid / pycld3识别语种pyyaml保存配置版本不必写死安装最新稳定版即可。完整的依赖可以放入 requirements.txt# 文件路径requirements.txt pandas2.0.0 requests2.31.0 rapidfuzz3.0.0 pyyaml6.0 langid1.1.63. 核心原理书目特征抽取、归一化与聚类策略3.1 先解决“字段抽取”问题很多书目数据并非干净的表格。以 MARC 21 为例一条记录包含若干字段比如 100 字段是个人作者主款目245 字段是题名说明260/264 字段是出版信息。但当我们从不同系统导出数据时得到的可能是 JSON、XML 或纯文本。这时候本地 LLM 可以完成一个关键步骤从非结构化文本中抽取结构化字段。例如有一段备注性质的文本Title: One Hundred Years of Solitude Statement of responsibility: by Gabriel Garcia Marquez Published: Buenos Aires : Editorial Sudamericana, 1967我们希望抽取为 JSON{ title: One Hundred Years of Solitude, author: Gabriel Garcia Marquez, publication_place: Buenos Aires, publisher: Editorial Sudamericana, publication_year: 1967 }这类任务对 LLM 来说相对简单真正的难点在于让输出格式恒定方便程序后续解析。常见的做法是在 prompt 里给出明确的格式要求并使用低温度采样比如 temperature 调成 0.1然后再加一层 JSON 解析兜底。3.2 归一化是聚类的基石规则与 LLM 结合之前必须做一次扎实的归一化。即使 LLM 语义理解能力很强如果数据里存在大量不必要的噪音聚类质量也会下降。归一化通常包括作者名归一化处理姓在前名在后、姓名缩写、拉丁化形式差异。例如“Garcia Marquez, Gabriel”和“Gabriel García Márquez”应映射为同一实体。题名归一化去掉首冠词、统一大小写、转 Unicode 为标准形式、去掉多余标点。出版日期归一化只提取年份处理“[1967]”、版权年等格式。语种归一化识别并映射为 ISO 639-1 代码。作者名的归一化尤其容易踩坑因为不同语言的名字习惯差异很大。中文作者相对简单欧美作者需要处理 Last Name, First Name 格式西语/葡语作者还有双姓的情况。没有一个通用规则能处理所有场景这也是为什么后续需要 LLM 做语义判断。3.3 规则聚类与 LLM 语义判断的配合聚类的第一步通常是“粗聚类”。我们先用确定性规则把明显相同的记录放到一起。例如规范化题名完全相同 作者规范化完全相同 语种相同则先归为一个候选簇。这种做法的好处是精确率高召回率不一定够。因为“One Hundred Years of Solitude”和“Cien años de soledad”虽然属于同一个超级作品但它们规范化后并不完全一致。这时候就需要 LLM 对跨语言、跨署名的候选簇做二次判断。例如系统可以把两条记录的信息拼成一个 prompt询问 LLM以下两条书目记录是否指向同一个“作品”Work注意忽略语种、出版社、版本差异但要区分同名但不同作者的作品。 记录A题名百年孤独作者加西亚·马尔克斯语种中文出版年2011 记录B题名One Hundred Years of Solitude作者Gabriel Garcia Marquez语种英语出版年1970 请回答 JSON{same_work: true/false, confidence: high/medium/low, reason: 一句话解释}这种“规则聚类 LLM 筛选”混合策略比单纯让 LLM 聚类所有记录要稳定得多也能节省大量推理时间。3.4 为什么需要“人工复核闭环”完全自动化的作品聚类在很长一段时间内都会存在边界案例。例如同名同作者但属于不同作品比如作者写了系列丛书每本书都是独立作品。同一作品被改编为电影、剧集、漫画是否需要归入同一个超级作品这会因库而异。翻译版本的副标题不同可能让 LLM 认为这是不同作品。更稳妥的工程方案是把 LLM 聚类结果输出为一个包含“聚类原因”的候选队列由编目员抽样复核再把复核结果作为 few-shot 示例纳入后续 prompt 中。这个闭环既保证了质量也能持续优化提示词。4. 完整实战用 Ollama Python 构建书目超级作品聚类原型下面我们来做一个可以实际运行的最小原型。这个原型的数据是模拟的但它包含了完整的思路读取书目 CSV、清洗数据、调用本地 LLM 抽取字段、规则粗聚类、LLM 语义判断、输出结果。4.1 创建项目结构建议先建一个干净的目录bibliographic-superwork/ ├── data/ │ ├── raw_records.csv │ └── sample_pairs.json ├── scripts/ │ ├── clean.py │ ├── extract_with_llm.py │ ├── cluster_rules.py │ ├── llm_judge.py │ └── merge_output.py ├── output/ │ └── results.csv └── requirements.txt4.2 准备模拟数据为了演示我们生成一份模拟的原始书目数据。注意这里特意放了同一个作品的不同语种版本和错拼变体# 文件路径scripts/generate_sample.py import pandas as pd rows [ { raw_text: 百年孤独 / 加西亚·马尔克斯著 ; 范晔译. 海口: 南海出版公司, 2011., source_id: rec001, }, { raw_text: One Hundred Years of Solitude / by Gabriel Garcia Marquez. New York: Harper Row, 1970., source_id: rec002, }, { raw_text: Cien años de soledad / Gabriel García Márquez. Buenos Aires: Sudamericana, 1967., source_id: rec003, }, { raw_text: One Hundred Years of Solitude (Penguin Modern Classics) / Gabriel Garcia Marquez. London: Penguin, 2000., source_id: rec004, }, { raw_text: Love in the Time of Cholera / Gabriel Garcia Marquez. New York: Knopf, 1988., source_id: rec005, }, ] df pd.DataFrame(rows) df.to_csv(data/raw_records.csv, indexFalse) print(Saved to data/raw_records.csv)这些记录里rec001 到 rec004 其实都指向同一个“超级作品”《百年孤独》只是语种、出版社、年代不同rec005 是作者的另外一部小说不应该被错误归并。4.3 用本地 LLM 抽取结构化字段在调用 Ollama 之前先确认你已经在本地安装 Ollama并下载了模型。以 qwen2.5:7b 为例ollama pull qwen2.5:7b然后我们写一个抽取模块。核心逻辑是把 raw_text 发送给模型让模型返回 JSON。# 文件路径scripts/extract_with_llm.py import json import requests import pandas as pd OLLAMA_URL http://localhost:11434/api/generate MODEL_NAME qwen2.5:7b def extract_fields(raw_text: str) - dict: prompt f 你是一名编目助手。请从下面的原始书目描述中提取结构化字段。 只输出 JSON不要有多余文字。 原始描述 {raw_text} 要求输出字段 - title: 作品原题名去掉副标题 - author: 主要作者 - language: ISO 639-1 语种代码 - publisher: 出版社 - publication_year: 出版年份 - medium: 载体类型book, ebook, audio 等 示例输出 {{title: 百年孤独, author: 加西亚·马尔克斯, language: zh, publisher: 南海出版公司, publication_year: 2011, medium: book}} resp requests.post( OLLAMA_URL, json{ model: MODEL_NAME, prompt: prompt, stream: False, temperature: 0.1, }, timeout120, ) resp.raise_for_status() text resp.json()[response].strip() # 防模型输出多余内容简单提取 JSON 子串 try: start text.find({) end text.rfind(}) 1 return json.loads(text[start:end]) except json.JSONDecodeError: return {title: , author: , language: , publisher: , publication_year: , medium: } def main(): df pd.read_csv(data/raw_records.csv) extracted [] for _, row in df.iterrows(): result extract_fields(row[raw_text]) result[source_id] row[source_id] extracted.append(result) print(row[source_id], result) out_df pd.DataFrame(extracted) out_df.to_csv(output/extracted_fields.csv, indexFalse) if __name__ __main__: main()这里最关键的地方是 prompt 中明确要求“只输出 JSON”并且在后面用 find 和 rfind 截取 JSON 子串。这样即使模型输出中夹杂了少量说明文字程序也能兜底解析。运行后你会在 output/extracted_fields.csv 中看到每条记录的抽取结果。可能有个别字段抽取不准比如语言代码返回了全称或出版年份带了括号。这没关系下一步清洗会处理这些噪音。4.4 编写归一化与清洗逻辑清洗这一步原本可以交给 LLM但对于可枚举的规则还是用代码更可靠。我们这里实现三个清洗函数# 文件路径scripts/clean.py import re import unicodedata def normalize_title(title: str) - str: 题名归一化去标点、统一空格、小写。 if not isinstance(title, str): return # 去掉常见首冠词这里示范英文/西语 title re.sub(r^(the|a|an|el|la|los|las|les|die|der|das)\s, , title.strip().lower()) # 转成 Unicode NFKC title unicodedata.normalize(NFKC, title) # 去掉非字母数字字符 title re.sub(r[^\w\s], , title, flagsre.UNICODE) title re.sub(r\s, , title).strip() return title def normalize_author(author: str) - str: 作者名归一化只用于粗聚类不做深度实体消歧。 if not isinstance(author, str): return author author.strip().lower() # 把 Garcia Marquez, Gabriel 转为 Gabriel Garcia Marquez if , in author: parts author.split(,) if len(parts) 2: author f{parts[1].strip()} {parts[0].strip()} # 去掉多余标点与重音 author unicodedata.normalize(NFKD, author) author author.encode(ascii, ignore).decode(ascii) author re.sub(r[^a-z\s], , author) author re.sub(r\s, , author).strip() return author def extract_year(raw_year: str) - str: if not isinstance(raw_year, str): return year re.search(r(19|20)\d{2}, raw_year) return year.group(0) if year else 这里把“García”归一化成了“garcia”能帮助规则层匹配到同作者的不同拼写。但这种 ASCII 化会丢失字符信息所以在后续 LLM 判断时我们还会使用原始作者字段而不是归一化后的字段。4.5 规则粗聚类把明显相同的记录放一起粗聚类这步我们可以直接使用清洗后的字段做分组。最简单的实现方式是以语言分组因为同一个超级作品的“内容表达”首先按语种区分。然后每个语种内部再按规范化作者和规范化题名精确匹配。# 文件路径scripts/cluster_rules.py import pandas as pd from clean import normalize_author, normalize_title def rule_based_cluster(df: pd.DataFrame) - pd.DataFrame: df df.copy() df[norm_title] df[title].apply(normalize_title) df[norm_author] df[author].apply(normalize_author) # 注意这里只做示例不合并同书名的跨语种版本 df[rule_cluster_id] df.groupby([language, norm_title, norm_author]).ngroup() return df def main(): df pd.read_csv(output/extracted_fields.csv) df rule_based_cluster(df) print(df[[source_id, language, norm_title, norm_author, rule_cluster_id]]) df.to_csv(output/rule_clustered.csv, indexFalse) if __name__ __main__: main()规则层跑完后你会看到 rec001中文版被分到自己一个簇rec002、rec003、rec004 如果语言识别正确则应该分别落在 en/es/en 不同簇里。也就是说规则层无法把《百年孤独》四份不同语言的记录合并到同一个超级作品簇。这正是下一步 LLM 语义判断要解决的。4.6 用 LLM 判断跨语言记录是否属于同一超级作品为了避免对全量数据两两组合做判断会产生笛卡尔积爆炸我们只对规则聚类后“看起来可能相关”的候选对做判断。这里定义一个简化的筛选策略规范化作者相同但语言或题名不同的记录对进入 LLM 判断队列。# 文件路径scripts/llm_judge.py import json import requests import pandas as pd from itertools import combinations OLLAMA_URL http://localhost:11434/api/generate MODEL_NAME qwen2.5:7b def judge_same_work(rec_a: dict, rec_b: dict) - dict: prompt f 你是一名书目编目专家。请判断下面两条书目记录是否属于同一个“作品”Work。 “作品”定义不考虑语种、出版社、版本、物理载体差异 如果两条记录只是同一位作者的同一部作品的不同翻译/版本请判断为 same_worktrue。 如果作者相同但确实是两部不同作品请判断为 false。 记录A - title: {rec_a[title]} - author: {rec_a[author]} - language: {rec_a[language]} - publication_year: {rec_a[publication_year]} 记录B - title: {rec_b[title]} - author: {rec_b[author]} - language: {rec_b[language]} - publication_year: {rec_b[publication_year]} 只输出JSON格式不要输出其他内容 {{same_work: true或false, confidence: high/medium/low, reason: 简要原因}} resp requests.post( OLLAMA_URL, json{ model: MODEL_NAME, prompt: prompt, stream: False, temperature: 0.1, }, timeout120, ) resp.raise_for_status() text resp.json()[response].strip() try: start text.find({) end text.rfind(}) 1 return json.loads(text[start:end]) except json.JSONDecodeError: return {same_work: None, confidence: low, reason: parse error} def main(): df pd.read_csv(output/rule_clustered.csv) # 按规范化作者分组先找出来自同一作者的 candidates author_groups df.groupby(norm_author) pair_results [] for author, group in author_groups: if len(group) 2: continue records group.to_dict(records) for rec_a, rec_b in combinations(records, 2): # 跳过规则层已经判定完全一致的组其实已经被 group 完全隔开了 if rec_a[language] rec_b[language] and rec_a[norm_title] rec_b[norm_title]: continue judgment judge_same_work(rec_a, rec_b) pair_results.append( { source_a: rec_a[source_id], source_b: rec_b[source_id], same_work: judgment.get(same_work), confidence: judgment.get(confidence), reason: judgment.get(reason), } ) print(pair_results[-1]) pd.DataFrame(pair_results).to_csv(output/llm_pair_judgments.csv, indexFalse) if __name__ __main__: main()理论上rec001 和 rec002 会被模型判定为 same_worktrue因为模型能理解“百年孤独”和“One Hundred Years of Solitude”是同一部小说的中文名和英文名。rec001 和 rec005 则会被判定为 false因为《百年孤独》与《霍乱时期的爱情》是不同的作品。5. 结果合并从“记录对”生成最终作品簇5.1 用连通分量合并候选对llm_judge 的输出是两两判断的结果。我们需要把判定为 same_work 的记录对合并成簇。这个操作在算法上可以建模为无向图连通分量问题每条书目记录是节点如果两两判断为同一作品则节点之间有边最终每个连通分量对应一个超级作品簇。# 文件路径scripts/merge_output.py import json import pandas as pd from collections import defaultdict, deque def build_clusters(pairs_df: pd.DataFrame, extracted_df: pd.DataFrame) - pd.DataFrame: # 只取判定为 same_work 的边 edges pairs_df[pairs_df[same_work] True] # noqa: E712 graph defaultdict(set) for _, row in edges.iterrows(): graph[row[source_a]].add(row[source_b]) graph[row[source_b]].add(row[source_a]) visited set() clusters [] def bfs(start): queue deque([start]) visited.add(start) members [] while queue: node queue.popleft() members.append(node) for neighbor in graph[node]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) return members all_nodes list(graph.keys()) for node in all_nodes: if node not in visited and node in graph: clusters.append(bfs(node)) # 对所有 source_id 建立 superwork_id 映射 id_to_cluster {} for cluster_id, members in enumerate(clusters, start1): for member in members: id_to_cluster[member] fSW{cluster_id:04d} extracted_df[superwork_id] extracted_df[source_id].map(id_to_cluster).fillna(SW0000) return extracted_df def main(): extracted_df pd.read_csv(output/extracted_fields.csv) pairs_df pd.read_csv(output/llm_pair_judgments.csv) result build_clusters(pairs_df, extracted_df) result.to_csv(output/final_results.csv, indexFalse) print(result[[source_id, title, language, superwork_id]]) if __name__ __main__: main()注意有可能出现图中有多个孤立节点它们没有与任何其他记录建立 same_work 边所以会得到默认的 SW0000。实际项目中这种记录可能是新作品也可能是规则层遗漏的候选需要人工复审。5.2 运行流程回顾整个原型运行顺序如下cd bibliographic-superwork python scripts/generate_sample.py python scripts/extract_with_llm.py python scripts/cluster_rules.py python scripts/llm_judge.py python scripts/merge_output.py如果你只想先测试一部分流程也可以手动把 raw_records.csv 精简成两三条记录减少 LLM 请求次数。6. 进阶思路Embedding、向量相似度与超大规模聚类6.1 用 Embedding 模型辅助召回如果你处理的是几十万条甚至上百万条书目数据让 LLM 两两判断候选对显然不可行。一个更合理的设计是先用规则完成确定性聚类。对未归并的记录或簇用本地 Sentence Embedding 模型生成题名作者的稠密向量。通过向量相似度召回 Top-K 候选对。再用 LLM 对 Top-K 候选对做精准裁决。这样可以把 LLM 调用量控制在一个可接受的范围。你甚至可以使用同一套向量索引来做跨语种召回因为在多语种 embedding 模型中“百年孤独”与“One Hundred Years of Solitude”的向量距离通常会比较近。6.2 LLM 输出规范化与容错本地 LLM 与云端模型相比输出稳定性会差一点。尤其是一些小参数量模型在要求输出严格 JSON 时偶尔会生成多余的说明、混入 markdown 代码块或把布尔值写成字符串。我们在生产代码里至少要做三层容错第一层尝试 json.loads 完整解析。第二层用正则截取第一个 { 到最后一个 } 之间的内容后解析。第三层如果解析失败过滤出 yes/no/true/false 等关键词做规则回退或者标记为人工复核。在实际项目中个人更推荐采用 Outlines、Instructor 这类结构化生成库在解码阶段就约束模型只能输出合法 JSON能大幅减少解析问题。6.3 从“记录对判断”升级到“簇合并判断”两两判断虽然简单但因为 LLM 需要反复调用成本较高。你也可以直接让模型“阅读”一个候选簇的全部成员尝试为这个簇生成一个代表特征描述再判断新记录是否与簇代表匹配。这种方式的优点是调用次数少缺点是对长上下文要求更高且模型更容易忽略个别冲突记录。更稳妥的做法是保留两两判断但只对边界记录对做判断。7. 常见问题与排查思路7.1 LLM 请求超时或失败如果你使用的是 Ollama默认的 keep_alive 机制在第一次请求时会把模型加载进显存耗时可能达到几十秒程序容易超时。问题现象常见原因解决思路requests 抛出 timeout模型首轮加载慢把 timeout 从 60 秒改到 120 秒或提前请求一次模型预热请求成功但返回空响应模型被 Ollama 自动卸载后重新加载检查显存占用并一次性连续请求GPU 显存不足模型参数量太大切换到更小模型或使用 4bit 量化版7.2 模型输出 JSON 解析失败可能原因包括模型在 JSON 前后增加了 markdown 代码块标记或字段值中混入换行符。建议处理方式import json import re text json\n{\title\: \x\}\n # 去掉 markdown 代码块标记 clean re.sub(r^(?:json)?|$, , text, flagsre.MULTILINE).strip() try: data json.loads(clean) print(data) except json.JSONDecodeError: # 二级兜底截取第一个 { 到最后一个 } start clean.find({) end clean.rfind(}) 1 data json.loads(clean[start:end]) print(data)7.3 聚类结果出现“过度合并”如果 LLM 把不同作品也合并到一起通常是因为 prompt 对“作品”的边界定义不够严格。你需要在 prompt 中强调系列丛书中的每一本书算独立作品即使作者相同。全集、选集不属于单一作品不要参与超级作品合并。改编作品影视、游戏是否归入需要由业务规则明确默认不归入。如果反复出现同一类误判可以把错误案例作为 few-shot 示例放到 prompt 中或者在规则层增加“不能合并”的硬性约束。7.4 实际数据字段缺失严重很多书目数据没有作者字段只有团体作者有些古籍甚至只有题名和年代。面对这种情况建议不要把规则聚类的阈值设得太严。你可以先按“题名归一化 语言”做候选召回然后把缺失字段交给 LLM 从 raw_text 里补全。如果 raw_text 本身就不完整那只能降低期望目录数据缺失条件下的作品聚类本身是编目领域的难题任何模型都不能凭空造出信息。8. 最佳实践与工程化建议8.1 数据安全与隐私边界书目数据通常不涉及读者个人隐私但在很多图书馆场景中仍然属于内部数据。使用本地 LLM 能天然满足数据不出内网的要求不过仍需注意不要把整个 MARC 库无差别导出到个人电脑。对导出的字段做最小化处理只保留需要聚类的字段。如果后续想切换到云端 API 做对比实验必须经过数据脱敏与合规审查。8.2 成本控制与任务拆分大模型不是万能的。能用规则解决的就不要用模型用小型 embedding 模型能解决的就不要用大型生成模型。推荐按以下优先级设计硬规则ISBN、控制号精确匹配。软规则规范化题名 作者 语种组合。向量召回对未归并记录做近似匹配。LLM 判断只处理少量边界案例。另外所有 LLM 请求都应该有缓存。同一段 raw_text 不需要重复抽取两次同一对记录也不需要反复判断。可以用 Redis 或者简单的磁盘缓存解决。8.3 聚类结果可解释性与普通业务推荐系统不同书目聚类结果往往需要被编目员审核甚至要写进馆藏系统的维护文档。因此每条聚类关系都应记录合并依据类型规则、向量、LLM 判断还是人工确认。LLM 给出的置信度与原因。执行聚类的系统版本和提示词版本。这样当发现某次合并错误时可以追查是规则问题、模型问题还是提示词问题。8.4 用“知识库工作流”管理提示词与结果如果你熟悉 Karpathy 提到的“LLM 知识库”方法会发现本书目聚类项目其实也适合用类似思路管理为不同类型的书目记录准备 Markdown 格式的说明文档让 LLM 在判断前先基于这些说明文档回答。比如# 作品合并规则 ## 作品定义 一个作品是智识或艺术创作的核心。 ## 特例 - 丛书中的单册是独立作品。 - 同一作品的不同翻译版本应合并到同一超级作品簇。这种方式比在 prompt 中反复堆叠规则更容易维护也便于后续把审核案例不断补充进去。你可以在 prompt 中告诉模型“请先阅读关于作品合并规则的知识库文档再回答问题”。8.5 增量聚类策略书目数据不是静态的每天都会有新书到馆、新记录导入。全量重建聚类成本很高建议设计为增量模式已经固定的超级作品簇保存到“作品簇主表”。新记录先进规则层入库计算它与现有作品簇的特征相似度。只有新增记录与已有作品簇冲突时才需要让 LLM 裁决并更新作品簇主表。这种设计能保证系统长期稳定运行而不至于每次更新数据都让所有书目重新聚类一遍。9. 总结与下一步建议本文从“超级作品聚类”这个偏图书馆信息学的需求出发拆解了如何使用本地 LLM 辅助完成书目字段抽取、跨语种作品识别、候选对判断和聚类合并。整套流程的核心思路可以概括如下不要把 LLM 当成全自动聚类工具而是把它嵌入到“规则召回 → LLM 裁决”的流程里。本地部署让数据留在可控环境内也为后续接入更多隐私敏感数据提供了基础。输出结果必须保留可解释信息让编目员能够审核并持续优化规则与提示词。如果在本地跑通原型下一步可以考虑用更大的真实书目数据集验证聚类精确率与召回率。接入本地向量模型实现百万级记录的快速召回。用结构化解码库如 Instructor、Outlines替代手工 JSON 解析。设计一个基于 Web 的人工复核界面展示聚类簇内的记录对比和 LLM 判断理由。如果你手头正有书目数据或者知识库类的文本建议先用一个很小的子集把上面流程跑通观察模型输出的不稳定点在哪里。每一次输出错误都是优化 prompt 和规则边界的最好素材。希望这套方法能在你自己的发现系统或知识库建设中派上用场。
