做本地 LLM 应用,真正难的不是跑一个模型,而是找到一个“值得用 LLM 来做”的任务。书目超级作品聚类就是一个很典型的例子:同一部作品在不同馆藏、不同出版社、不同语言版本里,可能被记录成完全不同的条目;靠精确匹配很难把它们合并到一起,而人工合并又非常耗时。标题里提到的 Building Bibliographic Superwork Clusters for Discovery with Local LLMs,本质上是把“本地大语言模型”用于书目数据的语义理解、聚类判断和发现索引生成。本文不讨论概念堆砌,而是直接拆开两件事:一是“超级作品聚类”到底要解决什么问题,二是怎么用本地 LLM 把它落地成一条可运行的工程链路。
先说结论:这个场景并不是让 LLM 把几百上千万条书目一次性读完,那既不现实,也不经济。更稳妥的路线是混合式管线——先用规则和文本嵌入做低成本预聚类,缩小范围;再把边界模糊的候选组交给本地 LLM 做语义审核。这样既保留了 LLM 对自然语言的理解能力,又避免了全量推理带来的资源浪费。文章后面会给出从环境准备、数据预处理、嵌入聚类到 LLM 审核、批量处理、结果验证的完整代码示例。
如果你手头有馆藏数据、开放书目数据、机构知识库,或者只是想把内部知识资产按“同一作品的不同版本/相关改编”整理成更清晰的结构,这篇文章可以直接收藏。整条链路用 Python 就能搭完,本地 LLM 运行时不要求固定型号,重点是把接口调用和工作流设计讲清楚。
1. 本地 LLM 构建书目超级作品聚类的核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目目标 | 将分散、异构的书目记录聚合为“超级作品”簇,用于文献发现和知识导航 |
| 技术路线 | 规则归一化 + 文本嵌入预聚类 + 本地 LLM 边界审核 + LLM 规范化命名 |
| LLM 主要角色 | 判断多条书目是否属于同一超级作品、给作品簇生成权威题名和范围描述 |
| 推荐硬件 | 中低配置即可开始;具体取决于所选 LLM 模型大小和量化精度 |
| 显存需求 | 不确定,需以实际部署的模型和推理参数为准 |
| 是否支持 CPU | 取决于本地运行时与量化模型;CPU 可跑小模型,但速度会低于 GPU |
| 启动方式 | 本地 LLM 运行时 + Python 脚本,命令行启动 |
| 是否支持 API | 支持,通常通过本机 HTTP 服务调用 |
| 是否支持批量任务 | 支持,文章提供批量提示词与重试机制示例 |
| 输出格式 | CSV / JSONL / SQLite 等,便于接入发现系统 |
| 适合场景 | 数字图书馆、机构知识库、图书元数据清洗、出版社内容组织 |
需要强调的数据入口有三类:MARC 记录、Excel/CSV 导出、来自 API 的 JSON 数据。凡是包含“题名 + 责任者 + 版本说明 + 出版年”的记录,都可以进入这条管线。
2. 超级作品聚类要解决的核心问题
在图书馆信息学里,FRBR 把书目实体拆成四个层级:作品(Work)、内容表达(Expression)、载体表现(Manifestation)和单件(Item)。常规编目已经解决了“表现层”的记录,但发现系统需要的往往是更高一层的聚合:一个叫“超级作品”的逻辑单元,把某部作品的各种版本、翻译、不同出版社的载体现,甚至相关改编都归到同一个可发现的节点下。
举一个常见例子。《白鲸》(Moby-Dick; or, The Whale) 在数据库里可能出现这些形式:
- Herman Melville 写的 1851 年首版记录;
- 某个出版社的 Norton Critical Edition;
- 某个中文译本,题名为《白鲸》;
- 手机上的一部有声书版本。
如果检索系统只做关键词匹配,这几个记录一般是散开的。用户搜“白鲸”时,结果页可能挤满了同一个作品的不同版本,却缺少一个能把它们聚合起来说明“这些都属于同一部作品”的入口。超级作品聚类要做的,就是把这些记录合并成一个簇,并给这个簇一个稳定、可读的标识。
传统方法不是不能做,但很困难。题名归一化能处理标点、大小写和“第 2 版 / revised edition”之类的后缀,却很难处理《The Three-Body Problem》和《三体》之间的跨语言关系,也很难判断某个续作、改编或同名书是否真的属于同一簇。规则要写的话,会无穷无尽。
本地 LLM 的价值在于它把“判断”变成了一个问题回答任务:给定若干条书目,让模型判断它们是否属于同一个超级作品,并输出理由。模型能看到题名中“同名但不同作品”的细微差别,也能捕捉翻译、副题名和版本说明里的语义线索。而且使用本地部署,书目数据不需要发送到外部服务,适合馆藏数据、未公开资源和机构内部资料。
3. 适用场景、使用边界与合规提醒
这个方案最适合以下场景:
- 机构知识库或图书馆盘点中,需要把不同来源的重复书目合并。
- 出版社或内容平台整理“作品族”,给编辑和推荐系统提供聚合单元。
- 数字人文研究者做文献计量或版本分析,第一阶段先把语料按作品簇切分。
- 内部知识系统做知识图谱时,通过超级作品簇串联作者、翻译、改编和评论文献。
不适合的场景也要说清楚。如果你的数据量只有几百条,人工清点比搭管线更划算;如果数据字段极不规范,连题名和作者都混在一个字段里,第一优先级不是 LLM,而是数据治理;如果要求 100% 的聚类准确率,那必须引入人工复审,LLM 只能做召回候选,不能直接作为最终发布依据。
合规边界需要特别注意。使用本地 LLM 并不代表可以随便处理版权材料。聚类结果如果对外发布,需要确认原始书目数据是否有合法的再分发授权。涉及受版权保护的作品内容时,本地 LLM 只处理元数据,不要让模型去生成受版权保护的正文摘要。内部数据做聚类时,建议先在隔离环境测试;不要将包含个人信息的读者借阅记录与书目记录关联后随意输出。部署本地 LLM 服务后,默认只监听 127.0.0.1 或内网地址,不要直接暴露到公网。
4. 混合式技术路线:不是让 LLM 硬读全部数据
把几百万条记录交给 LLM 判断,单靠提示词轮询显然不行。合理的工程化设计是分阶段缩小数据规模,让 LLM 只处理真正需要语义判断的边界样本。
整体流程可以拆成四个阶段:
- 数据清洗与归一化:统一字段格式,提取题名、作者、出版年、语言、类型。
- 低成本候选聚合:使用文本嵌入向量 + 聚类算法,把高度相似的记录预分组。
- 本地 LLM 审核:对候选组做“是否属于同一超级作品”的二分类判断,并给出理由。
- 发现层输出:给通过审核的簇生成标准化题名、作品范围描述和检索标签。
为什么不能跳过第二步?因为 LLM 推理成本远高于向量相似度计算。嵌入模型在 CPU 上也能批量跑,一条书目记录可以很快转成向量;DBSCAN 这类聚类算法处理十万条记录也比较从容。预聚类完成后,只剩边界组需要 LLM 参与,调用量会小很多。
举个例子。假设某库有 100 万条书目记录,第一步清洗后剩 80 万条有效记录,向量预聚类后可能会产生 40 万个候选簇,但其中大量是单记录簇,不需要审核。真正需要 LLM 判断的可能是那些带翻译、副题名、版本混杂的中等簇,数量可能只有几千个。把几千次推理放在本地 LLM 上做,完全是可接受的规模。
5. 本地 LLM 环境准备与启动
环境准备分成两部分:本地 LLM 运行时和 Python 数据管线。
操作系统方面,Windows、Linux、macOS 都能运行,但 Python 依赖安装会有细微差异。Python 建议使用 3.10 或更新的版本,并创建一个单独的虚拟环境,避免和系统 Python 包互相干扰。
5.1 安装本地 LLM 运行时
这里以 Ollama 为例,因为它安装简单、默认提供本机 HTTP 接口,并且支持 CPU / GPU 推理。实际使用中,LM Studio、llama.cpp、vLLM 等也能达到同样目的,接口地址需要按各自的文档调整。
# 示例:在 Linux / macOS 下安装 ollama,实际命令以官网文档为准 curl -fsSL https://ollama.com/install.sh | sh # 启动服务,默认监听 127.0.0.1:11434 ollama serve服务启动后,拉取一个对话模型作为示例,名称可以根据本机资源和任务需求替换:
# 拉取 7B 级对话模型,尺寸和量化方式以实际输出为准 ollama pull qwen2.5:7b然后验证模型是否能正常调用:
curl http://127.0.0.1:11434/api/tags如果返回 JSON 中能看到模型列表,说明服务已经可用。
5.2 创建 Python 工作目录
建议按下面的结构组织文件:
bibliographic-superwork/ ├── data/ │ ├── raw_records.csv │ ├── normalized_records.csv │ └── clusters.jsonl ├── scripts/ │ ├── 01_normalize.py │ ├── 02_embed_cluster.py │ ├── 03_llm_review.py │ └── 04_export_discovery.py ├── logs/ └── requirements.txtrequirements.txt 里的核心依赖包括 pandas、scikit-learn、requests,以及用于生成嵌入向量的 sentence-transformers:
pandas>=2.0 scikit-learn>=1.3 requests>=2.31 sentence-transformers>=2.7安装命令:
pip install -r requirements.txt如果只需要做轻量嵌入,也可以使用本地 LLM 运行时自带的 embedding 接口,但不同运行时接口差异较大。为了减少不确定性,本文示例采用 sentence-transformers,在第一次运行时它会下载模型文件,后续就可以离线使用。
6. 数据预处理与候选聚合:代码示例
6.1 演示数据集
先准备一份最小化的演示数据。为了避开版权和隐私问题,这里使用经典公版书目做结构演示。实际数据请替换为自己的合法来源。
record_id,title,author,publisher,publication_year,language rec001,Moby-Dick; or, The Whale,Melville Herman,Harper & Brothers,1851,en rec002,Moby-Dick,Norton Critical Edition,Herman Melville,W. W. Norton,2017,en rec003,白鲸,赫尔曼·梅尔维尔,上海译文出版社,2007,zh rec004,Moby-Dick (Unabridged),Herman Melville,Audible,2019,en rec005,Moby Dick 漫画版,Herman Melville,某个漫画出版社,2015,zh rec006,The Whale,Herman Melville,Random House,2020,en从人眼角度,rec001 到 rec006 基本都属于同一个超级作品族。但 rec006 的题名是 The Whale,如果没有作者信息和出版背景,算法层面很可能漏掉它。这正是需要嵌入相似度和 LLM 配合的地方。
6.2 题名归一化
题名归一化的目标是去除不影响语义判断的版本标记和符号,代码不需要复杂:
import re import pandas as pd def normalize_title(title: str) -> str: if not isinstance(title, str): return "" text = title.lower() # 保留中英文和数字,去掉多余标点 text = re.sub(r"[^\w\s\u4e00-\u9fff]", " ", text) # 去掉常见的版本和载体说明词,可按数据情况增删 text = re.sub(r"\b(edition|ed|vol|volume|unabridged|paperback|hardcover)\b", " ", text) return " ".join(text.split()) def build_candidate_key(row: pd.Series) -> str: title_part = normalize_title(row["title"]) author_part = "" if isinstance(row.get("author"), str): author_part = re.sub(r"\s+", "", row["author"].lower()) return f"{author_part}:{title_part}" df = pd.read_csv("data/raw_records.csv") df["title_norm"] = df["title"].apply(normalize_title) df["candidate_key"] = df.apply(build_candidate_key, axis=1) df.to_csv("data/normalized_records.csv", index=False) print(df[["record_id", "title_norm", "candidate_key"]].to_string())这个阶段并不是为了直接得到最终簇,而是生成一个粗粒度的分桶键,方便后续做精确比较。实际项目中,一个易错点在于作者字段:不同来源里“Melville, Herman”和“Herman Melville”顺序不一致,所以通常需要对作者名做倒序归一或去空格处理。
6.3 嵌入向量 + DBSCAN 候选聚类
对题名和作者做归一化之后,还可以用嵌入向量对标准化题名做语义聚类。这里使用 sentence-transformers,它会同时处理好中英文语料的统一向量表示。模型名称可以根据局域网可达情况替换成已下载的本地 embedding 模型。
import numpy as np import pandas as pd from sklearn.cluster import DBSCAN from sentence_transformers import SentenceTransformer df = pd.read_csv("data/normalized_records.csv") # 本地 embedding 模型,第一次运行会下载,后续离线可用 embedder = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") titles = df["title_norm"].fillna("").tolist() vectors = embedder.encode(titles, normalize_embeddings=True, show_progress_bar=True) df["vector"] = list(vectors) # eps 参数需要在真实数据上调参;min_samples=1 表示即使只有一个记录也保留 clustering = DBSCAN(eps=0.25, min_samples=1, metric="cosine") df["cluster_id"] = clustering.fit_predict(np.array(vectors)) # 查看预聚类结果 for cid, group in df.groupby("cluster_id"): print(f"cluster {cid}: {group['title'].tolist()}")eps 控制聚类的疏密程度。如果 eps 太大,不同作品会被错误合并;如果太小,同一个作品的不同版本又会被拆散。可以先用小批量样本来回测试,找到一个能让《白鲸》的几条记录聚到一起、又不至于把无关书目混进来的取值。
这一步做完之后,得到的 cluster_id 只是候选簇。它的准确率通常足以把大量明显重复的数据去掉,但处理跨语言、版本杂糅的边界时还不够。下一步交给本地 LLM。
7. 本地 LLM 聚类审核与超级作品命名
7.1 本地 LLM 调用封装
先写一个通用的本地 LLM 调用函数。这里用 Ollama 的 HTTP API,地址http://127.0.0.1:11434,模型名qwen2.5:7b只是一个示例。实际项目里,可以用你本机已经部署好的模型名替换。
import json import time import requests LLM_BASE_URL = "http://127.0.0.1:11434" LLM_MODEL = "qwen2.5:7b" def ask_llm(system_prompt: str, user_prompt: str, temperature: float = 0.1) -> str: payload = { "model": LLM_MODEL, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], "stream": False, "options": {"temperature": temperature}, } resp = requests.post(f"{LLM_BASE_URL}/api/chat", json=payload, timeout=180) resp.raise_for_status() return resp.json()["message"]["content"]temperature 设低一点,因为聚类审核是判断型任务,不希望模型发挥太多创造性。
7.2 判断同一超级作品的提示词
给模型每组候选记录时,提示词要明确“超级作品”的定义,并让模型输出 JSON,方便后续程序解析。一次请求里带上 5 到 10 条记录比较合适,太多会超出小模型的上下文窗口。
def build_review_user_prompt(records: list) -> str: numbered = "\n".join( [f"{i + 1}. 题名: {r['title']} / 作者: {r['author']} / 出版年: {r['publication_year']} / 语言: {r['language']}" for i, r in enumerate(records)] ) return f"请判断下面 {len(records)} 条书目是否属于同一个超级作品:\n{numbered}"系统提示词可以这样设计:
SYSTEM_PROMPT_REVIEW = """ 你是一名图书编目专家。所谓“超级作品”,是指同一部文学或学术作品及其各种版本、翻译、载体现的聚合。 你的任务是判断给定的多条书目记录是否应该归入同一个超级作品簇。 判断规则: 1. 同一原著的不同语言翻译,通常属于同一个超级作品。 2. 同一原著的不同版本、不同出版社、精装/平装,属于同一个超级作品。 3. 如果题名相同但作者不同、内容明显不同,则不属于同一个超级作品。 4. 如果记录是对原著的改编,例如漫画、影视剧本,需要谨慎:如果目标簇定义包含相关改编,可以归入;如果只做原作品聚合,则不归入。 请只输出 JSON,不要输出多余解释,JSON 格式: {"same_superwork": true, "reason": "简要判断理由", "risk_level": "low/medium/high"} """这里把 risk_level 作为额外输出,是因为自动聚类不可能达到 100% 准确,高风险簇需要后续人工复审。risk_level 是给工作人员做优先级队列用的,不是模型凭空保证的事实。
7.3 多候选组遍历
遍历预聚类结果时,不需要对所有簇都调用 LLM。一个簇只有一条记录,直接跳过;一个簇里的题名经过归一化后完全相同而且作者一致,可以直接接受,不调用模型。只有那些记录条数大于 1 且存在题名差异或语言差异的簇,才提交给 LLM。
def process_cluster_group(cluster_id, group_df, cache): cache_key = f"cluster_{cluster_id}" if cache_key in cache: return cache[cache_key] records = group_df[["title", "author", "publication_year", "language"]].to_dict("records") if len(records) <= 1: result = {"same_superwork": True, "reason": "单条记录无需判断", "risk_level": "low"} else: user_prompt = build_review_user_prompt(records) raw = ask_llm(SYSTEM_PROMPT_REVIEW, user_prompt) result = parse_llm_json(raw) time.sleep(0.2) # 不要对本地服务造成过大压力 cache[cache_key] = result return resultparse_llm_json 里要注意,有些本地模型会在 JSON 外面加 markdown 代码块标记,程序里要剥离:
import re def parse_llm_json(raw: str) -> dict: if raw.startswith("```"): raw = re.sub(r"^```(?:json)?|```$", "", raw, flags=re.MULTILINE).strip() try: return json.loads(raw) except json.JSONDecodeError: # 如果解析失败,宁可标记为高风险,让后续人工处理 return {"same_superwork": False, "reason": "JSON 解析失败", "risk_level": "high"}这里的处理原则是:解析失败的簇不做自动合并,留给人工。安全第一。
7.4 超级作品命名与范围描述
判断通过之后,还可以让 LLM 给每个超级作品簇生成一个规范化名称和简短的描述,方便后面做发现入口。
SYSTEM_PROMPT_NAME = """ 你是图书编目数据整理助手。请为一个已经确认的书目聚合簇生成一个稳定的超级作品名称和范围说明。 要求: 1. 名称优先使用最权威的原始题名,中文资料可附中文通用译名。 2. 范围说明不超过 60 个字,说明该簇包含哪些类型的版本。 3. 只输出 JSON。 """ def generate_superwork_metadata(group_df): titles = group_df["title"].tolist() authors = group_df["author"].dropna().unique().tolist() user_prompt = ( f"该簇包含以下书目记录:\n{chr(10).join(titles)}\n" f"责任者:{', '.join(authors)}\n" "请生成超级作品名称和范围说明。" ) raw = ask_llm(SYSTEM_PROMPT_NAME, user_prompt) return parse_llm_json(raw)生成的 JSON 建议保持成类似{"name": "白鲸", "scope": "赫尔曼·梅尔维尔原著及其主要中英文版本、全本有声书与无删节版", "author": "Herman Melville"}的结构。
8. 批量任务、缓存与接口调用注意事项
当书目数据量大到几千组时,建议增加三层控制:
第一层是缓存。已审核的簇 ID 和审核结果写入本地 JSON 文件,避免脚本中断后重头再跑。第二层是分批和限速。本地 LLM 通常可以连续处理,但并发过高还是会导致显存溢出或接口超时。第三层是失败重试。网络请求会因为模型加载、系统内存不足等原因失败,需要捕获异常并进行指数退避。
下面是一个批量脚本的骨架:
import json import time import requests from typing import Dict, List def batch_review_clusters(cluster_groups: Dict[str, object], output_path: str): cache = {} try: with open("logs/review_cache.json", "r", encoding="utf-8") as f: cache = json.load(f) except FileNotFoundError: cache = {} results = [] for cluster_id, group_df in cluster_groups.items(): try: result = process_cluster_group(cluster_id, group_df, cache) results.append({"cluster_id": cluster_id, **result}) with open(output_path, "a", encoding="utf-8") as f: f.write(json.dumps({"cluster_id": cluster_id, **result}, ensure_ascii=False) + "\n") except requests.exceptions.Timeout: print(f"[timeout] cluster {cluster_id}, sleep 10s retry later") time.sleep(10) except Exception as exc: print(f"[error] cluster {cluster_id}: {exc}") results.append({"cluster_id": cluster_id, "same_superwork": False, "reason": f"exception: {exc}", "risk_level": "high"}) # 回写缓存 with open("logs/review_cache.json", "w", encoding="utf-8") as f: json.dump(cache, f, ensure_ascii=False, indent=2) print(f"done, total {len(results)} clusters")要注意,打印输出不要包含原始书目全文;日志里只保留 record_id 或 cluster_id,降低敏感信息扩散风险。
9. 输出发现数据与效果验证
经过 LLM 审核和命名后,可以生成一个便于检索的发现文件。每行 JSON 对应一个超级作品簇:
{ "cluster_id": 12, "superwork_name": "白鲸", "superwork_scope": "赫尔曼·梅尔维尔原著及其主要中英文版本、全本有声书与无删节版", "record_ids": ["rec001", "rec002", "rec003", "rec004", "rec005", "rec006"], "representative_title": "Moby-Dick; or, The Whale", "languages": ["en", "zh"], "risk_level": "low" }如果需要支持前端模糊搜索,可以导入 SQLite 并用 FTS5 建全文索引。示例过程:
sqlite3 discovery.db " CREATE VIRTUAL TABLE superwork_fts USING fts5(superwork_name, superwork_scope, representative_title); "验证聚类效果时,不要只看准确率,还要看召回。对一批手工标注的数据,可以计算两个数字:
- 准确率:生成的簇里真正属于同一超级作品的比例。
- 召回率:原本属于同一超级作品的记录,有多少被成功归到同一个簇里。
实际项目中,不可能对全量数据做人工标注,所以建议抽样:随机抽 300 到 500 个簇,人工判断簇边界是否正确。抽样策略要覆盖简单簇、双语簇、高风险簇,不能只挑容易样本。
判断成功的标准可以定义为:同一作品的不同版本被合并为一个簇,且簇内没有混入无关作品;每个簇有可读的超级作品名称;风险标记为 high 的簇不超过可接受比例。如果失败,先看是预聚类阶段漏召回了,还是 LLM 审核阶段误判了,再针对性地调整 eps 或提示词。
10. 资源占用与性能观察方法
在真实书目数据集上跑,需要关注两个计算环节的性能:嵌入向量生成和 LLM 推理。
嵌入阶段,使用 sentence-transformers 时,如果机器有 NVIDIA GPU,sentence-transformers 会自动使用 CUDA;如果只有 CPU,也能跑,但十万条记录可能需要几十分钟到几小时,具体耗时取决于 CPU 核心数和向量化模型。执行时可以观察 htop、任务管理器,或者用nvidia-smi查看 GPU 占用。
LLM 推理阶段,显存占用主要由模型大小和量化精度决定。7B 级模型全精度和 4bit 量化差别很大,具体显存占用要以本地运行时的ollama ps或 nvidia-smi 输出为准。如果你的显卡显存不够,可以考虑使用更小的模型、更低的量化精度、或者直接切到 CPU 推理。CPU 推理不是不能用,但对响应时间和并发能力影响明显。
降低显存占用和提升吞吐量的常用手段:
- 把温度参数调低,默认 0.1 即可。
- 一次 prompt 里多放几条候选记录,减少请求总次数。
- 模型加载后保持常驻,避免重复重启。
- 调整 DBSCAN 的 eps,让预聚类更准,减少 LLM 请求量。
- 如果提示词太长,做截断,只保留每条记录的题名、作者、年份和语言。
对 LLM 推理性能,一个更稳妥的判断是:实际调用一定以本机测试为准。不要照搬别的文章里的“占用 X GB”结论,因为模型版本、上下文长度和并发策略都会影响结果。
11. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 本地 LLM 接口请求返回 404 | 运行时不同,接口路径不一致 | 查看本地运行时文档或服务日志 | 换成对应运行时的接口路径,例如 OpenAI 兼容接口 |
| 模型未下载或名称错误 | pull 的模型名与调用名不一致 | 先调用/api/tags查看已安装模型 | 使用正确的模型名重新 pull |
| 脚本请求超时 | 模型首次加载慢 / 系统负载高 / 上下文过长 | 观察日志和 GPU 占用 | 加大 timeout,先预热模型,缩短 prompt |
| 显存或内存不足 | 模型过大或并发请求过多 | nvidia-smi / htop 查看占用 | 切换到更小模型或量化版本,降低并发 |
| 聚类结果不理想,版本被拆散 | eps 设定过小 | 抽样查看候选簇 | 增大 eps,或调整 embedding 模型 |
| 不同作品被误合并 | eps 设定过大 | 抽样查看高风险簇 | 调小 eps,或让 LLM 提高 risk_level 阈值 |
| LLM 输出 JSON 解析失败 | 小模型指令遵循能力偏弱 | 打印原始返回 | 剥离 markdown 标记,增加 fallback 逻辑 |
| 预处理后大量记录为空 | 原始字段缺失严重 | 检查 CSV/API 来源 | 补全字段,或将该部分数据排除后再处理 |
一个特别需要注意的问题是:Ollama 服务默认监听 127.0.0.1,这个配置不要随意改成 0.0.0.0。如果确实需要在局域网内其他机器访问,建议加鉴权,并且只在可信网络环境里开启。
12. 最佳实践与工程化建议
先小规模跑通,再扩大数据量。第一次实验不要直接处理百万级数据,建议导出一万条左右的样本,覆盖多语言、多版本、高重复几种类型。先验证向量聚类和 LLM 提示词是否稳定,再决定是否要做全量批处理。
数据目录最好分成三层:原始数据、特征数据、结果数据。原始数据只读,不允许程序直接改写;特征数据包括归一化字段和向量;结果数据每次生成都要带时间戳或版本号,方便回溯。比如输出文件可以是results_20250101.jsonl,不要把旧结果直接覆盖。
对风险记录一定要人工复审通道。LLM 给出的 risk_level 为 high 的记录不要自动发布,至少做二次确认。二次确认可以是人工,也可以用一批更大的提示词重新审核两次,取一致结果。自动合并必须可回滚,不要让程序直接改动原始书目数据库。
涉及版权的材料,要坚持三不原则:不在未授权情况下擅自对外聚合发布;不把受版权保护的作品正文放入 LLM 提示词去生成摘要或重写;不把内部聚类结果用作未授权模型的训练语料。书目元数据的再分发,也需要查看数据供应商的许可条款。
如果将来要接入第三方工具或做成服务,建议把本地 LLM 接口封装成独立的微服务,数据层只通过 JSON 交互。这样即使底层模型换掉,上层聚类脚本也不需要重写。
13. 总结与下一步
可以先去本机部署一个本地 LLM 运行时,准备几百条测试书目,然后按文章里的三步走:归一化、嵌入聚类、LLM 审核。验证完准确率和召回率以后,再决定是否把规模扩大。最容易踩的坑是 eps 参数没有调好就贸然全量跑,以及本地模型输出 JSON 不稳定没有做容错。建议把日志、缓存、风险标记一起做完整,再进入批量环节。下一步可以考虑的方向是把聚类结果导出成知识图谱结构,把作品、作者、翻译者和改编者关系合并成网络,形成真正的发现系统。