基于LLM的双维度题目附带内容相似度分析框架解析
这个项目的名字很直白:A Dual-Dimensional LLM Framework for Automated Item Incidental Content Similarity Analysis in Large-Scale Assessments。简单翻译过来,就是一套面向大规模测评场景、用 LLM 自动化分析“题目附带内容相似度”的双维度框架。它要解决的核心问题不是“这道题做对没有”,而是“这道题的附带材料跟其他题目、外部素材之间,有没有重复、改写、拼凑的嫌疑”。
在题库建设、试卷命制、出版内容合规审查这些环节里,这类相似度分析是真正的痛点。一套大规模考试试卷里,阅读文章、题干情境、选项干扰项都有可能是从历年题库或外部出版物中复用、改编过来的。人工抽审只能覆盖很小比例,字符串精确匹配又识别不了“同义改写”“段落重组”“跨材料拼凑”这类常见手法。这个框架的切入点,是把相似度拆成显式文本相似和隐式语义相似两个维度,先批量召回候选,再用 LLM 给出复核结论。
下面直接进入正题,从框架设计、技术选型、环境部署、核心代码、API 接入、批量任务和性能观察几个方面展开。看完之后,你可以照着搭出一套能跑通、能接 API、能处理批量文件的题目素材相似度分析系统。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 教育测评领域 NLP 分析框架,基于 LLM 的自动化相似度审查 |
| 核心思路 | 显式文本相似度 + 隐式语义向量相似度,双维度融合判定 |
| 输入内容 | 题目附带内容,包括阅读材料、题干、选项、情境材料等 |
| 主要功能 | 素材去重、题目查重、改写识别、跨材料拼凑检测、版权风险提示 |
| 依赖模型 | Embedding 模型 + 本地 LLM 或云端 LLM API |
| 硬件门槛 | Embedding 阶段可 CPU 运行;加入本地 LLM 复核建议配置独立显卡,具体显存取决于模型规模 |
| 启动方式 | Python 脚本、FastAPI 服务、批量目录处理 |
| 是否支持 API | 支持,接口设计可直接接入内部系统 |
| 是否支持批量任务 | 支持,按目录批量处理或按接口批量提交 |
| 适合读者 | 教育测评机构、题库建设团队、内容审核、NLP 工程开发人员 |
这个项目本身是一套研究性质的双维度分析框架,不是打包好的一键客户端。要落地,需要自己组合模型、向量库和接口服务。难度不高,关键是理解两个维度分别解决什么问题。
2. 这个框架要解决什么问题
2.1 题目附带内容指什么
从项目标题来看,“Item Incidental Content”指的是题目正文之外的辅助内容。在大规模测评里,一道题的构成通常不只是题干本身,还包括阅读篇章、图表说明、实验情境、选项段落等。这些内容被计入“附带内容”,是因为它们承载了题目要考查的信息背景,也是命题质量审查和版权审查的重点对象。
举例来说,一道阅读理解题里的英文短文,可能来自某本出版物;一道数学应用题里的情境素材,可能与另一场考试的材料高度相似;一道多选题的干扰项,可能是从旧题里换了个说法拼出来的。这些都属于题目附带内容的相似度问题,也是这个框架要自动识别的对象。
2.2 传统查重方案为什么不够
传统文本相似度方法主要分两类:一类是词面匹配,比如编辑距离、Jaccard 相似度、最长公共子序列、SimHash;另一类是统计向量化,比如 TF-IDF、BM25。它们的问题是只停留在“字面相同”的层。
字面相同的题目好查,难的是这些情况:
- 同义改写:把主动句改成被动句,把关键词换成近义词,字面相似度很低,但语义等同。
- 段落重组:把一篇材料切成几段,打乱顺序重新拼接,n-gram 匹配基本失效。
- 跨材料拼凑:从三篇不同文章里各取一段组成新材料,单独跟任何一篇比都不像,但整体语义密度很高。
- 跨语言或格式转换:中文素材翻译成英文,或者从 PDF 复制后格式混乱。
这些场景单靠传统文本相似度算法无法覆盖。于是需要引入 LLM 和语义向量,把“这段文本表达的是什么意思”纳入比较。
2.3 双维度融合的思路
只用语义向量也会误报。两篇材料讨论同一个话题,比如都是“全球变暖”,语义向量可能很接近,但实际内容来源并不相同。如果只按向量余弦相似度判定,会产生大量误报。
所以这个框架采用“双维度”融合的思路:
- 第一维度,显式文本相似度,捕捉字面上的复制、改写、局部拼接。
- 第二维度,隐式语义向量相似度,捕捉语义层面的等效表达、观点复现、段落信息重复。
两个维度不是二选一,而是互补。字面相似度高而语义相似度低,可能是“复制后插入大量干扰内容”;字面相似度低而语义相似度高,可能是“高质量改写”;两个维度都高,则高度疑似重复或素材复用。把所有情况合并成一个打分体系,再交给 LLM 做最终复核,比单纯用某一个维度的判定可靠得多。
3. 双维度框架整体设计
3.1 第一维度:显式文本相似度
显式文本相似度的目标,是识别“看起来就很像”的文本对。这一层适合用经典算法实现,速度快、可解释性强。
常用方法包括:
- 编辑距离或 Levenshtein 距离,衡量字符级差异。
difflib.SequenceMatcher,计算最长匹配块比例,适合检测局部复制。- Jaccard 相似度,基于分词后的集合交集。
- SimHash,适合大规模去重场景,可以先做候选集收缩。
在实际实现里,我会先用difflib取得一个大致的相似度分数,再对匹配块的长度进行统计。如果文本对之间存在大量连续匹配片段,说明存在明显的复制拼接行为。
import difflib from typing import Tuple def lexical_similarity(text_a: str, text_b: str) -> float: sm = difflib.SequenceMatcher(None, text_a, text_b) return sm.ratio() def longest_match_ratio(text_a: str, text_b: str) -> float: sm = difflib.SequenceMatcher(None, text_a, text_b) matching_blocks = sm.get_matching_blocks() total = sum(block.size for block in matching_blocks) base_len = max(len(text_a), len(text_b), 1) return total / base_len这个维度的特点是“严查字面”。它不会放过直接复制和低水平改写,但对高水平的语义复述无能为力。因此需要第二个维度补位。
3.2 第二维度:隐式语义向量相似度
语义向量相似度,是把文本用 Embedding 模型编码成向量,再计算余弦相似度。这一步解决的是“表面不同,实际同源”的问题。
实际落地时,可以用本地 Embedding 模型,也可以用云端 API。从数据保密角度考虑,本地部署更稳妥。适合做中英文题目素材的嵌入模型包括 BGE-M3、bge-large-zh、gte-large 等,这类模型对长文本、中英混合内容支持都不错。
from sentence_transformers import SentenceTransformer model = SentenceTransformer("BAAI/bge-m3") texts = [ "这是一段阅读理解材料的第一段内容。", "This is the first paragraph of a reading comprehension passage." ] embeddings = model.encode(texts, normalize_embeddings=True)编码完成后,计算两个向量之间的余弦相似度即可。关键点在于:Embedding 模型通常有最大输入长度限制,长阅读材料需要切块。切块后可以用段级向量均值或取最大相似度作为整篇材料的语义相似度。这部分对最终效果影响很大,后面第 6 节会专门给出代码。
3.3 融合判定与三级流水线
双维度框架的完整工作流程,建议按“召回、精排、复核”三级结构组织。
第一级,候选召回。大规模测评的题库可能有数万道题,不能对全部题目两两计算相似度,那样计算量是 O(n²)。正确做法是先对所有题目附带内容做向量化,建立向量索引。查询时用 FAISS 或 Milvus 做近似最近邻检索,把相似度较高的 Top-K 条作为候选。
第二级,精排。对候选对同时计算显式文本相似度和语义向量相似度,得到一个两维特征向量。再根据业务规则或者一个简单的加权公式,输出融合分数。
第三级,LLM 复核。对融合分数位于敏感区间的文本对,把两段内容拼成一个 Prompt,交给 LLM 判断“是否存在素材复用、改写、拼凑”,并输出判定理由。LLM 能理解语义和上下文,可以显著降低误报率。
输入材料 -> 预处理与题目切块 -> Embedding 向量化 -> ANN 召回 Top-K -> 双维度精排打分 -> LLM 复核判定 -> 输出相似度报告这条流水线既能处理批量任务,也能独立成接口服务。整个框架的核心实现,就是把各个阶段的代码串起来。
4. 技术选型与工具链
4.1 文本处理与预处理
题目附带内容通常来自 Word、PDF、Excel 或纯文本,原始数据里往往混有格式符、特殊字符、图片注记。预处理阶段要解决几件事:
- 统一换行和空格,去掉全角空白。
- 去除页眉页脚、题目编号、章节名这类与内容无关的信息。
- 按题目 ID 切分,把一整份试卷拆成“题号 + 附带内容”的结构。
- 对中文内容做分词,对英文内容做小写化,为后续计算做准备。
预处理的质量,直接影响相似度计算的准确性。建议在预处理之后,把切分结果保存成 JSON 或 CSV 中间文件,方便后续反复调试。
import json import re from pathlib import Path from typing import List, Dict def normalize_text(text: str) -> str: text = text.replace("\u3000", " ").replace("\r", "") text = re.sub(r"\s+", " ", text) return text.strip().lower() def parse_items(raw_text: str) -> List[Dict[str, str]]: blocks = re.split(r"\n(?=Q\d+|\d+[.、])", raw_text) results = [] for block in blocks: block = normalize_text(block) if len(block) < 10: continue results.append({"id": f"item_{len(results)}", "content": block}) return results def load_corpus(filepath: str) -> List[Dict[str, str]]: raw = Path(filepath).read_text(encoding="utf-8") items = parse_items(raw) with open("corpus.json", "w", encoding="utf-8") as f: json.dump(items, f, ensure_ascii=False, indent=2) return items4.2 嵌入模型选型
Embedding 模型决定了语义维度的上限。选择时主要看三点:
- 是否支持中英文混合文本。
- 最大输入长度是否够长。
- 本地推理的显存和内存消耗。
BGE-M3 是最常被推荐的选项之一,支持中英文、长文本,效果和性能比较均衡。如果只处理中文题目,bge-large-zh 也是一个不错的选择。如果使用云端 API,可以选择 text-embedding-3-small 这类服务,但需要评估题目素材是否允许传送到外部服务。
4.3 向量检索与索引
数万道题目的向量检索,不能靠暴力循环。常用方案:
- FAISS:Meta 开源的向量检索库,轻量级,适合单机批量分析。
- Chroma:接口简单,适合中小规模项目快速搭建。
- Milvus:分布式向量数据库,适合大规模生产环境。
对这个框架来说,如果数据量在十万条以下,FAISS 足够。重点是把全量向量导入索引,查询时设置相似度阈值,过滤低分结果。
import faiss import numpy as np def build_index(vectors: np.ndarray): dim = vectors.shape[1] index = faiss.IndexFlatIP(dim) index.add(vectors) return index def search_candidates(index, query_vector: np.ndarray, top_k: int = 10): scores, indices = index.search(query_vector.reshape(1, -1), top_k) return scores[0], indices[0]4.4 LLM 复核层
处理模糊判定时,需要 LLM 阅读两段文本,给出结构化结论。复核层可以部署本地模型,也可以调用企业内部已建好的 LLM 网关。对中文内容来说,Qwen、GLM、DeepSeek 这一系列模型都支持 OpenAI 兼容接口,接入成本很低。推荐使用 7B 到 14B 规模的模型,既保证判定质量,又不会太吃显存。如果只有 CPU 机器,也可以先用小参数模型,或者直接调用云端 API,前提是数据脱敏和合规审批通过。
5. 环境准备与本地部署
5.1 硬件与运行环境
先明确一条原则:Embedding 阶段和 LLM 复核阶段对硬件的要求不一样。
- 只跑 Embedding:普通 CPU 机器即可,内存建议 16GB 以上,适合先跑通流程。
- 加入本地 LLM 复核:如果用 7B 级别模型,建议配置至少 12GB 显存的独立显卡;显存不足时,可以使用量化版本或把部分层卸载到 CPU。
- 如果使用云端 API:本机只需要能联网,硬件门槛大幅降低。
操作系统以 Linux 或 macOS 最佳,Windows 也可以运行,但显存管理和一些原生依赖需要额外配置。
5.2 Python 环境与依赖
建议使用 Python 3.9 到 3.11 版本。新建虚拟环境后,安装以下依赖:
python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install -U pip pip install sentence-transformers pip install faiss-cpu # 有 GPU 可换成 faiss-gpu pip install fastapi uvicorn pip install openai # 用于调用 OpenAI 兼容接口 pip install python-multipart pip install jieba pip install scikit-learn安装完成后,先跑一句简单的模型加载测试,确认环境没有缺包。
from sentence_transformers import SentenceTransformer model = SentenceTransformer("BAAI/bge-m3") vectors = model.encode(["测试文本"], normalize_embeddings=True) print(vectors.shape)5.3 模型文件准备
Embedding 模型会自动下载到本地缓存目录。如果服务器无法直接访问模型仓库,可以提前在有网的机器上下载好,再复制到离线环境的~/.cache/huggingface或指定目录。
LLM 模型建议用 vLLM 或 Ollama 启动一个兼容 OpenAI 接口的服务。这里以 OpenAI 兼容接口为例,启动后记住base_url和模型名,后续代码只需要替换两个参数。
# vLLM 启动示例,实际参数需要按模型版本调整 python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --host 127.0.0.1 \ --port 80005.4 启动服务与验证
如果只是本地跑分析脚本,直接把函数串起来执行即可。如果要提供接口,就启动 FastAPI 服务,然后在浏览器或 curl 里验证。
uvicorn api_server:app --host 0.0.0.0 --port 7860启动后,用下面的命令快速验证接口是否可用:
curl -X POST http://127.0.0.1:7860/compare \ -H "Content-Type: application/json" \ -d '{"text_a": "这是一段阅读材料", "text_b": "这是一段类似的阅读材料"}'端口冲突时,改掉--port参数即可。服务进程不要用nohup裸跑,建议接 systemd 或 supervisor 做进程托管,至少在脚本里加日志输出。
6. 核心实现代码
6.1 文本预处理与题目切分
第一步是把原始试卷文件转换成“题目 ID + 附带内容”的结构。这里用正则粗切,实际项目里可以根据不同试卷格式定制切分规则。
import re from typing import List, Dict def split_items(raw_text: str) -> List[Dict[str, str]]: # 按题号切分,支持 "1." "1、" "Q1" 等常见格式 parts = re.split(r"\n(?=(?:\d+[.、]|Q\d+))", raw_text) items = [] for part in parts: lines = part.strip().split("\n") if not lines: continue item_id = re.match(r"(\d+[.、]|Q\d+)", lines[0]) content = " ".join(lines[1:]).strip() if content: items.append({"id": item_id.group(0) if item_id else f"item_{len(items)}", "content": content}) return items6.2 语义向量构建
对切分后的题目内容批量编码。注意,长文本要切块,否则会被模型截断。
from sentence_transformers import SentenceTransformer EMBEDDING_MODEL = SentenceTransformer("BAAI/bge-m3") MAX_CHARS = 500 def embed_text(text: str): if len(text) <= MAX_CHARS: return EMBEDDING_MODEL.encode([text], normalize_embeddings=True)[0] chunks = [text[i:i + MAX_CHARS] for i in range(0, len(text), MAX_CHARS)] chunk_vecs = EMBEDDING_MODEL.encode(chunks, normalize_embeddings=True) return chunk_vecs.mean(axis=0)切块后取平均向量,是一种简单但有效的策略。如果材料本身段落结构清晰,也可以按段落切块,再逐段做向量召回,最后取最大值作为相似度。
6.3 双维度相似度计算
把显式文本相似度和语义相似度合并成一个分数。融合方式可以简单加权,也可以用规则分组。
import difflib import numpy as np def cosine_sim(a: np.ndarray, b: np.ndarray) -> float: return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9)) def dual_dimension_score(text_a: str, text_b: str, vec_a: np.ndarray, vec_b: np.ndarray) -> dict: lexical = difflib.SequenceMatcher(None, text_a, text_b).ratio() semantic = cosine_sim(vec_a, vec_b) fused = 0.4 * lexical + 0.6 * semantic return { "lexical_score": round(lexical, 4), "semantic_score": round(semantic, 4), "fused_score": round(fused, 4) }融合权重需要根据业务数据校准。如果误报偏高,就增加 lexical 的权重;如果漏报偏高,就增加 semantic 的权重。不要把阈值固定死,要观察实际分数分布。
6.4 LLM 复核调用
对融合分数处于敏感区间的内容,调用 LLM 做最终判定。这里使用 OpenAI 兼容接口,方便对接本地 vLLM 或云端服务。
import openai client = openai.OpenAI( base_url="http://127.0.0.1:8000/v1", api_key="EMPTY" ) def llm_review(text_a: str, text_b: str) -> str: prompt = f"""你是一名试卷素材审查专家。请判断以下两段文本是否存在素材复用、改写或拼凑关系。 材料 A: {text_a[:800]} 材料 B: {text_b[:800]} 请输出 JSON,包含两个字段: - "verdict": "same" / "rewrite" / "partial" / "different" - "reason": 简要说明判断理由 """ resp = client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role": "user", "content": prompt}], temperature=0.1, max_tokens=256 ) return resp.choices[0].message.content这里只取前 800 字去复核,是为了控制 Token 消耗。实际使用中,可以按段落抽取最相似片段,把片段交给 LLM,而不是把整篇长文都塞进去。
7. 接口 API 与批量任务
7.1 FastAPI 服务示例
如果要把双维度分析能力开放给题库系统或内容管理平台,可以封装成 API 服务。下面的代码实现两个接口:/compare用于单对文本对比,/analyze用于单篇文本检索全库相似内容。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() class CompareRequest(BaseModel): text_a: str text_b: str class AnalyzeRequest(BaseModel): text: str top_k: int = 10 @app.post("/compare") def compare(req: CompareRequest): vec_a = embed_text(req.text_a) vec_b = embed_text(req.text_b) result = dual_dimension_score(req.text_a, req.text_b, vec_a, vec_b) return result @app.post("/analyze") def analyze(req: AnalyzeRequest): vec = embed_text(req.text) scores, indices = search_candidates(index, vec, req.top_k) results = [] for score, idx in zip(scores, indices): item = corpus[idx] results.append({ "id": item["id"], "content": item["content"][:200], "semantic_score": round(float(score), 4) }) return {"results": results}这里省略了index和corpus的全局初始化代码,实际部署时要提前加载。接口启动后,用下面的 Python 脚本测试:
import requests response = requests.post( "http://127.0.0.1:7860/compare", json={ "text_a": "这是第一段阅读材料", "text_b": "这是第二段阅读材料" }, timeout=30 ) print(response.json())7.2 批量目录处理脚本
大规模分析场景下,更适合用批量脚本处理目录文件。输入目录放待分析材料,输出目录生成 JSON 报告。
import json from pathlib import Path input_dir = Path("./data/input") output_dir = Path("./data/output") output_dir.mkdir(parents=True, exist_ok=True) def batch_analyze(): report = [] files = list(input_dir.glob("*.txt")) for file in files: content = file.read_text(encoding="utf-8") vec = embed_text(content) scores, indices = search_candidates(index, vec, top_k=5) file_result = {"file": file.name, "candidates": []} for score, idx in zip(scores, indices): item = corpus[idx] file_result["candidates"].append({ "id": item["id"], "score": round(float(score), 4) }) report.append(file_result) with open(output_dir / "report.json", "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2) batch_analyze()批量任务最关键的是日志和断点续跑。建议每个文件处理成功后,把结果单独写一个result_xxx.json,最后再合并汇总。这样中途失败无需重新计算全部内容。
7.3 任务与日志建议
在 API 和批量任务中,建议做好三件事:
- 每个请求或文件记录开始时间、结束时间、处理状态。
- 失败任务设置重试机制,Embedding 调用或 LLM 调用偶尔会有超时。
- LLM 复核结果要缓存,避免同一对文本重复调用模型产生额外成本。
创建一张简单的结果表,字段包括item_id_a、item_id_b、lexical_score、semantic_score、fused_score、llm_verdict、created_at。后续做阈值调整和人工抽检都很方便。
8. 性能观察与资源占用
8.1 关键性能指标
这套框架的性能瓶颈通常有三个位置:
- Embedding 编码阶段。
- 向量索引查询阶段。
- LLM 复核阶段。
Embedding 阶段的执行速度取决于模型大小和是否使用 GPU,批量编码比单条循环快很多,所以要尽量减少逐条调用。向量索引阶段在万级规模下耗时很低,但索引构建需要一次性完成。LLM 复核是成本最高的部分,建议只对融合分数超过阈值的文本对调用。
8.2 显存与内存观察
本地运行 LLM 时,用nvidia-smi查看显存占用。不同量化级别、不同上下文长度的显存占用差异非常大,不能直接引用别人的数值代替本地测试。
如果显存不足,可以按优先级调整:
- 改用 4bit 或 8bit 量化版本。
- 缩短输入文本,只取相似片段。
- 降低 batch size,分批推理。
- 用 CPU 跑小参数模型,速度慢但稳定。
Embedding 模型通常占用的显存或内存较少,CPU 也能跑完万级向量化任务。如果数据量到十万级以上,建议先把向量缓存到磁盘,避免每次启动重复计算。
8.3 优化手段
从工程角度看,优化点集中在几个地方:
- 向量结果落盘缓存。同一批题目向量算一次,之后直接加载。
- 索引使用 IVF 或其他近似检索方式,降低查询耗时。
- 文本对去重,不重复计算已经判断过的组合。
- LLM 复核加流控,避免同时打爆 GPU。
- 长文本优先做片段级相似度定位,再做整篇融合。
观察资源时,应该记下三个数字:全量向量化的耗时、构建索引的耗时、单次 LLM 复核的耗时。调优后再对比,能明显看到瓶颈在哪。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型加载失败 | 模型文件缺失或路径错误 | 检查模型缓存目录,确认网络可访问 | 重新下载或手动指定模型路径 |
| 显存不足 | 模型参数过大或 batch size 过高 | 查看nvidia-smi当前占用 | 换量化模型,减小 batch size |
| 接口返回超时 | LLM 复核阶段 token 生成过长 | 查看服务日志和耗时分布 | 限制 max_tokens,减少复核文本长度 |
| 相似度误报高 | 阈值设置不合理 | 输出分数分布,统计误报样本 | 调整权重和阈值,结合 LLM 复核 |
| 长文本结果不准确 | Embedding 截断导致信息丢失 | 检查切块策略和长度统计 | 分段编码取均值,或片段级比对 |
| 批量任务卡住 | 某个文件异常或进程崩溃 | 查看任务日志,定位失败文件 | 加入 try-except 和重试机制 |
| 向量召回结果为空 | 阈值设置过高或索引未构建 | 检查索引是否加载,查看查询阈值 | 降低阈值或重新构建索引 |
| 端口冲突 | 服务被其他进程占用 | lsof -i :7860查看占用情况 | 更换端口或关闭旧进程 |
整套流程最容易出现的问题,不在代码本身,而在数据预处理和阈值设置。建议第一批跑出来的结果不要直接采信,先人工抽查 50 对文本,确认判定是否符合预期,再调整参数。
10. 合规边界与内容安全
题目附带内容相似度分析,处理的是考试材料、出版物片段、题库内容,这些数据通常涉及版权和保密要求。以下几个边界必须注意。
数据授权与保密。大规模测评材料在正式发布前属于敏感数据,不能随意上传到外部服务。优先使用本地部署的 Embedding 模型和 LLM。如果必须使用云端 API,需要经过数据安全评估,并确认数据传输链路加密。
隐私与个人信息。如果题目素材中包含考生作答数据、学生画像等个人信息,必须在分析前完成脱敏。相似度分析系统只应该处理去标识化后的文本内容。
版权合规。从公开出版物中提取的段落,用于相似度比对和查重是合理的;但批量复制、长期存储或对外展示需要获得授权。分析报告里的素材来源信息,宜只做内部使用。
使用范围限制。API 服务要设置访问控制,避免未授权调用。批量任务结束后,及时清理临时文件,中间数据按保密等级管理。
11. 最佳实践建议
根据实际项目落地经验,总结几条可复用的建议。
先跑小规模验证集。准备 100 到 200 道题,人工标注出“重复、改写、拼凑、正常”四类结果。用这批数据校准双维度权重和阈值,再全量运行。
保留最小可运行配置。把模型路径、阈值、权重、LLM 接口地址写进配置文件,不要硬编码在代码里。这样不同环境切换成本低。
建立输入、中间结果、输出目录。原始试卷放input,切分后的题目内容和向量缓存放intermediate,报告放output。跑完一段就落盘一段,避免内存和进程失效导致数据丢失。
LLM 复核结果必须有格式校验。调用 LLM 输出 JSON 时,可能会遇到格式不完整的情况。要加一个解析后校验逻辑,解析失败就重试一次或标记为待人工复核。
阈值不是一次定死。不同学科、不同题型,附带内容风格差异很大。理科题干的重复度高但正常相似也高,文科材料的语义相近更常见。建议按科目分别设置阈值。
合规与审核前置。部署前确认数据来源合法、授权清晰。分析结果用于辅助审查,不能直接当作侵权或抄袭的最终结论,涉及外部版权的内容应提交人工复核。
12. 总结与下一步
这个双维度 LLM 框架最值得尝试的点,是把“字面相似”和“语义相似”分开计算,再通过流水线召回和 LLM 复核降低误报。这种设计比单用 Embedding 或单用字符串匹配都更适合大规模测评场景。
第一次验证时,建议先准备一批已知存在重复或改写的题目,跑一遍完整流程,重点看几件事:语义向量能否召回被改写的材料,融合分数是否能把不同关系区分开,LLM 复核在模糊样本上是否稳定。这三个点验证通过,再扩大到全量题库。
最容易踩的坑是长文本直接进 Embedding 模型被截断,以及阈值设得太死导致误报或漏报。解决方案也不复杂:长文本先分段,阈值用数据分布去校准。
再往下扩展,可以加两条路线。一条是接入 OCR,把扫描版试卷和旧书素材纳入分析范围;另一条是增加溯源能力,把相似片段定位到具体段落,输出类似“第 3 段与另一篇材料第 2 段存在连续匹配”的报告。这两条路都属于同一套框架的延伸,核心的双维度判定逻辑不需要大改。
如果你的业务里也面临题库查重、内容审核、素材版权风险识别这类问题,用这个框架先跑一个最小原型,比直接上人工审核要快得多。
