ARTICLE DETAIL

资讯详情

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

双维度LLM框架用于试题附带内容相似度分析

双维度LLM框架用于试题附带内容相似度分析 在大规模教育测评场景中试题查重与相似度分析一直是一个“既重要又难落地”的问题。过去我们讨论试题相似度大多停留在关键词重合、文本编辑距离或向量余弦值这些层面但真实考试命题中真正让命题专家头疼的往往是另一类相似性题干背景材料相近、图表条件相似、选项编排雷同甚至只是“换了学科壳内核考察点一样”。这类相似性有一个共同特点——它们藏在试题的附带内容里而不是核心测量内容里。这篇文章要讲清楚一个更系统的解法双维度 LLM 框架用于大规模测评中试题附带内容相似度分析。它是一个把传统相似度计算与大语言模型理解能力结合起来的自动化筛查框架核心不是用一个模型替换人工而是用“语义召回 条件判断”两个维度分层处理在可控成本下完成海量试题的高精度相似性筛查。读完这篇文章你会理解这个框架要解决的三个问题一是为什么传统方法看不住“附带内容”这一类相似二是双维度框架的设计逻辑是什么两个维度分别承担什么职责三是如何在 Python 工程里跑通这套流程包括代码实现、测试集构造、阈值校准和人工复核兜底。如果手头正好有真题库、模拟题库或教辅资源的去重需求文中这套思路可以直接改造后接入。1. 这篇文章真正要解决的问题先说一个实际场景。某教育测评机构每年要处理数万道试题这些题来自不同命题人、不同年份、不同渠道。为了防止考试中反复出现“换汤不换药”的题目也为了避免版权纠纷命题部门必须做相似度筛查。但筛查工作长期处于一种尴尬状态。传统文本查重工具能抓出“文字几乎一样”的题却抓不出“材料不同但条件等价”的题。举个具体例子一道物理题用“小车在光滑斜面下滑”考查牛顿第二定律另一道题用“木块在无摩擦水平面加速”考查同一个定律。两题文字完全不同向量相似度可能很低但命题专家一眼就能看出这是“同类题”。反过来两题都用“某物体在粗糙平面上运动”文字高度重合但一题考摩擦力方向的判断一题考做功的计算考察目标完全不同。前者是“表面不同、实质相似”后者是“表面相似、实质不同”。这两类误判暴露了传统方法的共同短板它们只关注文本的表层结构不能理解试题的测量意图和条件约束。而附带内容相似度分析要处理的恰恰是这类藏在题干、材料、图表选项里的隐性重复。所以这个框架真正回答的问题是在人力有限、题库规模不断扩大的情况下如何用 LLM 自动完成“附带内容相似度”的初筛与精排让命题专家把精力集中在少数真正可疑的题目对上而不是面对几万道题束手无策。从工程角度看它还解决了另一个痛点成本与精度的平衡。如果所有题对都交给大模型做深度判断成本不可控如果只做向量相似度精度不够。双维度设计的本质是用低成本方式先缩小范围再用高成本方式做精细判断。2. 附带内容相似度概念、难点与之前路线的局限2.1 什么是附带内容相似度要理解附带内容相似度先要区分试题中的两类信息。第一类是核心测量内容也就是这道题真正要考的知识技能点。比如数学里考“二次函数顶点坐标”物理里考“动量守恒”语文里考“文言文实词理解”。这类信息决定了试题的测量目标。第二类是附带内容它不直接决定测量目标但影响试题的呈现形式。这包括背景材料、情境描述、数据表、示意图、图表选项、示例主体等。比如英语阅读理解中的一篇科普文章、生物题中的实验流程图、历史材料题中一段文字材料都属于附带内容。附带内容相似度分析要做的事情是判断两道题的附带内容是否存在“高度可互换”的相似性。这种相似性的危险之处在于它可能不被核心测量内容的相似所覆盖。两道题完全可以考点不同但附带内容高度雷同造成考试中“题目看着眼熟”的不良体验或者构成洗稿式侵权。2.2 为什么传统方法在这里失灵传统相似度分析有三个常用方案每个都有明确盲区。第一种是字符串匹配如编辑距离、公共子串。它只能发现文本近乎一致的题对只要改动几个关键词或语序就会被绕过。第二种是词频统计与 TF-IDF 向量。它比字符串匹配灵活一些但无法处理同义替换和抽象表达。材料中大量使用“无摩擦表面”和“光滑表面”这类同义说法时词面重合度并不高。第三种是基于词向量的语义相似度比如 Word2Vec 和 Sentence-BERT 嵌入的余弦相似度。它能解决一部分同义表达问题但仍然存在两个不足。一方面向量相似度无法可靠区分“语义相似但条件不同”的情况。题 A 说“物体从高度 h 自由落下”题 B 说“物体以初速度 v0 竖直下抛”两者词向量可能很近但一个是自由落体模型一个是抛体运动模型解法完全不同。另一方面向量相似度不能对“物理情境不同但解题结构一致”的情况给出稳定结果很多跨领域类比题在向量空间里距离很远。还有一个更麻烦的问题向量相似度是黑盒相似度它给出一个 0 到 1 的分数却不说清楚相似在哪里、哪个条件重合。在大规模测评这种需要审计和申诉的场景里这种“无解释判断”很难直接作为处理依据。2.3 LLM 在这里扮演的角色LLM 的引入改变了这个问题的边界。它带来的不是“更精准的文本向量”而是“可解释的结构化判断”。与传统方法不同LLM 能理解题目中哪些信息是测量目标、哪些是附带内容能识别图表中条件参数的等价关系能输出“相似点出现在哪一部分、属于什么类型”的结构化结论。这种能力正是附带内容相似度分析最需要的东西。但这不等于可以放弃传统方法。大模型推理成本高、速度慢把几万道题两两配对后全部交给 LLM 判断既不经济也不必要。正确的思路是把两者组合成双维度流水线向量维度做召回LLM 维度做精排。3. 双维度框架的整体设计这个框架之所以叫“双维度”是因为它从两个互补的维度判断一道题对的附带内容相似程度。第一个维度是语义表征维度。它把试题中的附带内容文本截取出来用嵌入模型转成向量再计算余弦相似度。这一层的目的是“尽量不漏”通过相对较低的阈值把可能相似的题对召回出来。它的优点是有明确的数学定义、速度快、成本低适合海量题库粗筛。它的缺点是不会解释“为什么相似”也不擅长捕捉条件级等价。第二个维度是结构化条件判断维度。它把召回出来的题对交给 LLM通过设计好的提示模板让模型按照测评专家的分析框架逐项判断比如“背景材料是否同源”“题干条件是否等价”“图表数据是否一致”“选项编排是否存在明显对应关系”最后输出结构化 JSON 结果和判定依据。这一层的目的是“尽量不错”在大幅缩小候选集的基础上做精细判断。两个维度是流水线关系不是并列关系。语义维度负责把十万级题对压缩到百级LLM 维度负责在百级候选里给出高质量判断。从架构上看这很像信息检索领域的“召回 精排”两阶段范式但它针对的是附带内容相似度这个具体的教育测评子问题。这个设计背后有一个关键观察附带内容相似度并不是一个单一的“相似度分数”能表达的问题它包含不同层级的相似需要不同成本的手段分层处理。如果只用向量精度不够如果只用 LLM成本太高、速度太慢。双维度正是对这两个条件的工程折中。4. 环境准备与前置条件在搭建代码之前先把环境说清楚。以下版本只作为示例实际以你的项目为准。4.1 推荐的运行环境操作系统Linux / macOS / Windows 均可建议 Linux 服务器处理大批量数据Python3.10 及以上包管理器pip 或 poetry需要重点说明的是模型选型。在实际项目中LLM 服务可以有两种接入方式使用 OpenAI 兼容接口的在线 API优点是配置简单、效果稳定适合小批量试点部署本地模型如通义千问 Qwen 系列、ChatGLM 系列或 Llama 系列优点是数据不出内网适合教育测评这类高敏感数据场景由于教育测评数据通常涉及未公开试题和考生个人隐私更稳妥的做法是私有化部署或使用机构内部模型服务。代码层面只需要保证接口兼容 OpenAI 格式后续切换模型时改动很小。4.2 Python 依赖pip install openai numpy pandas scikit-learn如果你的嵌入模型不使用 OpenAI 接口而是用本地模型库可以额外安装 sentence-transformerspip install sentence-transformers4.3 数据格式约定整个分析流程以两个输入为基础试题表包含题号、题干、选项、答案、学科、题型等字段题对候选表由全量两两配对或按题型分桶配对产生为了让流程易于扩展建议先统一输入格式{ item_id: MATH-2024-0001, subject: 数学, type: 选择题, stem: 某物体从静止开始沿光滑斜面下滑已知斜面倾角为30度斜面长度为5米求物体滑到底端时的速度大小。, options: A. 5m/s B. 7m/s C. 10m/s D. 14m/s, material: 本题附有斜面示意图图中斜面倾角为30度长度标尺为5米。 }其中material字段专门用来存试题中的图表说明与背景材料。如果原始题库没有该字段可以在预处理阶段把“题干中的非公式部分”和“图片的 OCR 描述”拼接进去。5. 核心流程拆解整个框架的在线服务流程可以拆成五个步骤。5.1 题目解析与附带内容抽取第一步不是计算相似度而是把每道题拆解成可分析的结构化字段。这一步直接决定后续分析质量。推荐的做法是先用程序解析题库文件尽量抽取题干、选项、答案、解析、图表信息对于图片类内容用 OCR 加图像描述模型生成文本描述再存入material字段。做完字段抽取后生成两个文本版本text_embed用于语义向量化的文本一般拼接题干、题干中的背景材料、选项文本不需要答案text_judge用于 LLM 精排的完整文本包含题干、选项、图片描述材料注意这里要刻意把答案排除在分析文本之外。因为两道题答案相同并不代表附带内容相似反过来答案不同也不代表题目不相似。附带内容相似度分析的核心对象是“题目的皮肤”而不是“题目的答案”。5.2 全量题对生成与分桶对于一万道题全量两两配对会产生约五千万个题对。直接全部计算嵌入相似度虽然可行但会浪费大量算力。更稳妥的做法是分层缩小配对空间。按学科、题型分桶是最常用的一级分桶。学科和题型差异过大的题对即使附带内容相似通常也不是测评意义上的重复可以优先排除。分桶后每个桶内部的题对数量会大幅下降。如果某个桶仍然过大可以继续按章节或知识点分桶。这一步不是分析框架的核心但工程上很有价值。5.3 维度一语义向量召回对每个桶内的题对使用嵌入模型生成text_embed的向量然后计算余弦相似度。具体实现在下一节给出。这一步要设置一个召回阈值。阈值设置有两个目标既要尽量把真正的相似题对召回进来又要控制进入 LLM 阶段的题对数量避免成本失控。从实践来看阈值选取不能凭感觉。更稳妥的做法是先从题库中抽样两到三百题人工标注出“确定相似”“确定不相似”“不确定”三类题对用这批样本做阈值扫描观察不同阈值下的召回率和候选集规模再结合预算选择工作点。5.4 维度二LLM 结构化判断对于召回出来的题对送入 LLM 判断。提示模板需要覆盖附带内容相似度的常见类型并要求输出结构化 JSON。LLM 判断环节的稳定性直接决定整个框架的可用性。建议在提示中明确模型需要完成的任务边界并要求输出包含相似类型、相似维度、风险等级和建议处理方式。5.5 人工复核与反馈LLM 输出结果后将风险等级较高的题对导出为审核列表由命题专家进行最终判断。人工复核的意见需要落库并定期作为反馈样本微调提示模板或校准阈值。这五步合起来就是双维度框架从数据到决策的完整闭环。6. 完整示例代码实现下面给出一个最小可运行的示例。这个示例不依赖具体的题库系统只演示双维度分析的 Pipeline 核心逻辑。6.1 维度一嵌入向量相似度计算# 文件路径semantic_dimension.py import numpy as np from openai import OpenAI from sklearn.metrics.pairwise import cosine_similarity # 兼容本地部署的 OpenAI 风格服务 client OpenAI( base_urlhttp://localhost:8000/v1, # 或你的模型服务地址 api_keyEMPTY # 本地服务通常不需要真实密钥 ) def get_embedding(text: str, model: str text-embedding-3-small) - list[float]: resp client.embeddings.create(modelmodel, inputtext) return resp.data[0].embedding def semantic_similarity(text1: str, text2: str) - float: emb1 np.array(get_embedding(text1)).reshape(1, -1) emb2 np.array(get_embedding(text2)).reshape(1, -1) return float(cosine_similarity(emb1, emb2)[0][0])这段代码的关键点是base_url和api_key。如果你使用在线 API替换成对应配置即可如果使用本地服务保持这个结构就能完成切换。代码输出的是 0 到 1 之间的余弦相似度分数作为召回阶段的排序依据。6.2 维度二LLM 结构化精排# 文件路径llm_judge_dimension.py import json from openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY ) JUDGE_PROMPT 你是一名教育测评命题质量分析专家。 请判断以下两道试题的附带内容是否存在高度相似性。 附带内容包括背景材料、情境描述、图表数据、题干条件、选项编排方式。 不包括知识点、解答思路、标准答案。 请按以下维度逐项判断 1. 背景材料是否同源或高度相近 2. 题干条件是否等价数值可不同但物理关系/逻辑关系一致算等价 3. 图表数据是否指向同一结构 4. 选项内容是否存在明显对应或改写关系 题1{item1} 题2{item2} 请以 JSON 格式输出 {{ material_same_source: true/false, condition_equivalent: true/false, chart_same_structure: true/false, option_corresponding: true/false, similarity_type: 同源材料/等价条件/图表一致/选项对应/组合情况, risk_level: high/medium/low, reason: 判断依据不超过200字 }} def llm_judge(item1: str, item2: str, model: str qwen2.5-14b-instruct) - dict: prompt JUDGE_PROMPT.format(item1item1, item2item2) resp client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], response_format{type: json_object} ) content resp.choices[0].message.content return json.loads(content)这里有两个工程细节值得注意。第一个是response_format。请求中显式要求 JSON 输出能大幅减少模型返回非结构化文本的概率。第二个是提示模板中的维度设计。四个布尔维度不是随便写的它们对应附带内容相似度最常见的四种表现模型逐项判断比笼统问“是否相似”要稳定得多。6.3 双维度融合与决策召回和精排完成后需要把两个维度的结果融合成一个可执行的决策。# 文件路径dual_dimension_pipeline.py from semantic_dimension import semantic_similarity from llm_judge_dimension import llm_judge def dual_dimension_analysis( item1: dict, item2: dict, recall_threshold: float 0.82, judge_model: str qwen2.5-14b-instruct ): # 维度一语义召回 sem_score semantic_similarity(item1[text_embed], item2[text_embed]) if sem_score recall_threshold: return { item_pair: (item1[item_id], item2[item_id]), stage: recall_filtered, semantic_score: round(sem_score, 4), risk_level: low, llm_result: None } # 维度二LLM 精排 full_text_1 f题干{item1[stem]}\n选项{item1[options]}\n材料{item1[material]} full_text_2 f题干{item2[stem]}\n选项{item2[options]}\n材料{item2[material]} judge_result llm_judge(full_text_1, full_text_2, modeljudge_model) # 决策逻辑 booleans [ judge_result.get(material_same_source, False), judge_result.get(condition_equivalent, False), judge_result.get(chart_same_structure, False), judge_result.get(option_corresponding, False) ] hit_count sum(booleans) if judge_result.get(risk_level) high or hit_count 2: final_risk high elif judge_result.get(risk_level) medium or hit_count 1: final_risk medium else: final_risk low return { item_pair: (item1[item_id], item2[item_id]), stage: llm_judged, semantic_score: round(sem_score, 4), llm_result: judge_result, risk_level: final_risk }决策逻辑做了一件重要的事不把 LLM 输出当成绝对真理。risk_level字段来自模型hit_count表示命中的相似类型数量。只有当两者出现至少一个强信号时才给出高风险的最终判断。这种“模型输出 规则兜底”的组合比把决策完全交给模型可靠得多。6.4 批量题对筛选管道在实际项目中不可能把全量题对一次性读进内存。推荐写成生成器分批处理# 文件路径batch_processor.py import itertools from typing import Generator def item_pairs(items: list[dict]) - Generator[tuple[dict, dict], None, None]: for item1, item2 in itertools.combinations(items, 2): yield item1, item2 def batch_analysis(items: list[dict], batch_size: int 200): batch [] for pair in item_pairs(items): result dual_dimension_analysis(pair[0], pair[1]) batch.append(result) if len(batch) batch_size: yield batch batch [] if batch: yield batch这里每一批输出一个结果列表方便你写进 CSV 或数据库。item_pairs是组合生成器能避免一次性生成全量题对导致的内存溢出。7. 运行结果与效果验证7.1 构造一个小型验证集在正式接入题库前建议先构造一个微型测试集。测试集不需要很大但必须覆盖两类关键样本正样本人工确认过“附带内容相似”的题对负样本人工确认过“附带内容不相似”的题对从实际经验看正样本至少要有 30 到 50 对负样本 50 到 100 对。正样本可以通过改写真题得到也可以由命题专家从已发现的重复题目中整理。给测试集标注时两个标注者独立判断不一致的由第三人仲裁。这样得到的标签一致性更高后续评估才有意义。7.2 评估指标对召回阶段主要看两个指标召回率真实相似题对中被召回的比例候选集压缩比进入 LLM 阶段的题对占全部题对的比例对精排阶段主要看精确率LLM 标记为高风险且人工确认属实的比例人工复核节约比例经过管道后真正需要专家看的题对占全部题对的比例下面是一段简单的评估代码# 文件路径evaluate.py def evaluate_recall(pairs, y_true, sem_scores, threshold): y_pred [1 if score threshold else 0 for score in sem_scores] tp sum(1 for p, t in zip(y_pred, y_true) if p 1 and t 1) fn sum(1 for p, t in zip(y_pred, y_true) if p 0 and t 1) recall tp / (tp fn) if (tp fn) 0 else 0 candidate_ratio sum(y_pred) / len(y_pred) return {recall: recall, candidate_ratio: candidate_ratio}7.3 判断成功的标准一个管道是否能上线可以从三个信号判断召回率能到 90% 以上。这意味着真正相似的题对基本不会被初筛漏掉。LLM 阶段输出的高风险题对中人工确认命中率明显高于随机抽查。比如随机抽查 100 题对可能只有 2 对相似但 LLM 标记的 100 题对有 60 对相似说明精排有区分力。单道题平均分析成本在预算范围内。向量阶段成本几乎可以忽略LLM 阶段只处理召回子集成本模型是可控的。如果管道运行后LLM 输出的reason字段经常出现自相矛盾或答非所问说明提示模板或模型选型需要调整。这时候不要急着调代码先抽查 20 条输出找出失败模式再针对性改提示词。8. 常见问题与排查思路双维度框架在落地时会遇到几类高频问题。下面整理成排查表。问题现象可能原因排查方式解决方案语义维度召回率过低嵌入模型对教育领域专业术语理解不足抽样统计未召回题对的文本特征换领域微调嵌入模型或改用更强的通用嵌入模型文本向量 API 未配置环境变量未设置或 base_url 指向错误检查 API 配置和网络连通性设置正确的 base_url 和 api_key本地服务确认已启动LLM 输出不是合法 JSON模型不支持 response_format或上下文过长查看原始返回内容确认模型能力换支持 JSON 输出的模型或在后处理时增加 JSON 提取与修复逻辑LLM 判断不稳定同一题对多次结果不同采样温度过高或提示模板中任务边界不清晰固定 temperature0多次调用对比稳定性将 temperature 设为 0并在提示中增加“没有依据不要猜测”的约束长题干超出模型上下文窗口题目包含大量材料或图片 OCR 文本统计输入 token 长度定位超限字段只送入关键区段如题干前 500 字和图表描述前 300 字人工复核发现大量误报阈值设置过松或 LLM 提示导致过度判相似统计误报样本中哪些维度命中最多收紧阈值或在提示中增加“仅基于附带内容不基于知识点”的强调图片信息无法参与分析当前流程只处理文本检查材料字段是否为空增加 OCR 和图像描述模块把视觉信息转成文本描述成本超预算LLM 阶段候选集过大查看进入 LLM 阶段的题对数量提高召回阈值或增加分桶粒度缩小题对总数遇到问题时最忌讳直接改代码而跳过日志分析。建议为每一道题对保留完整的处理记录包括输入文本、模型输出、命中维度、最终风险等级。这些记录既是排查问题的依据也是后续迭代提示模板的语料。9. 最佳实践与工程建议9.1 安全与数据合规教育测评数据属于敏感数据尤其涉及未公开试题时数据泄露可能直接影响考试安全。因此有几点红线必须守住。第一如果条件允许优先本地私有化部署模型不要将原始试题直接发送到外部 API。本地部署虽然前期成本高但从长期合规角度看更稳妥。第二即使使用本地模型也要在流程中增加数据脱敏。分析完成后原始文本不应该长期存在于日志中。可以只保留脱敏后的文本指纹和判定结果。第三人工复核结果必须留痕。每次判定都要记录模型版本、提示词版本、输入文本摘要、输出结果和复核人意见保证整个分析过程可追溯。9.2 提示模板的迭代节奏LLM 应用的难点不在第一次写好提示词而在于持续迭代。建议按以下节奏迭代每两周抽样一次未命中样本分析是召回层漏掉还是判断层误判每次调整提示词后跑同一批回归测试集对比前后指标变化把历史误判案例整理成负样本提示加入提示词作为“反例约束”例如如果发现模型常常把“同考点的不同情境题”误判为相似可以在提示中增加一句“如果两题只是知识点相同但背景材料、条件设置、选项结构均不同不应判为附带内容相似。”这类反例提示通常比抽象规则更有效。9.3 阈值校准的工程方法阈值不要拍脑袋定。推荐在正式运行前做一个阈值扫描实验。做法是从题库中随机抽取 300 题两两配对生成约 45000 个题对对其中随机 500 个题对做人工标注计算不同阈值下模型的召回率和候选集规模这个实验能画出一条召回-开销曲线你可以根据业务需要选择工作点。如果预算紧张就选候选集更小的阈值如果漏判代价高就选召回率更高的阈值。9.4 与人工审核的协作模式这套系统定位不是“替代命题专家”而是“帮命题专家节省时间”。因此产品设计上要保证两点。一是必须有审核界面。LLM 判定为高风险的题对应以列表形式呈现展示相似点摘要、命中的相似类型和关键文本片段而不是只给一个风险分数。二是审核意见要回传系统。专家确认或否决 LLM 的判断后这些反馈数据可以用于后续评估和模型微调。这样系统会越用越准而不是停留在初始水平。10. 总结与后续学习方向至此双维度 LLM 框架的完整链路已经讲清楚了从附带内容的概念拆解到语义召回与 LLM 精排的协作设计再到 Python 代码实现、测试集评估和工程落地实践。这套框架的价值在于它没有试图让一个模型包办所有事情而是把“相似度分析”拆成两个难度不同的子问题用最合适的工具处理最合适的问题。如果你想继续深入建议从三个方向展开探索。第一个方向是多模态分析。当前的流程把图片转成文本描述后再分析会损失一部分视觉结构信息。未来的框架升级方向是让 LLM 直接理解题目中的图片在图表结构层面做对比判断。第二个方向是领域自适应。通用嵌入模型和通用大模型在教育测评场景下的表现始终不如专门调优过的模型。可以尝试用历史人工标注数据微调一个测评领域的嵌入模型或者用指令微调让小模型在特定学科上的判断能力接近大模型。第三个方向是知识增强。把题库中的知识图谱、教材章节、命题大纲等信息注入判断过程让系统不只判断“文本像不像”还能结合“这个考点是否允许附带内容重复”等业务规则做判断。最后提醒一句这套框架的每一个环节都要在测试集上验证后再放量不要一上来就处理全量题库。先用小批量跑出可信指标再逐步扩大范围才是这类自动化分析系统最稳妥的上线路径。建议收藏本文搭建时按章节顺序对照实现可以少走不少弯路。
返回列表