欢迎光临
我们一直在努力

RAG 在智能题库中的应用:相似题检索和难度评估方案

RAG 在智能题库中的应用:相似题检索和难度评估方案

一、搜"一元二次方程",结果返回了 3000 道题,学生挑了最难的开始做

在线教育平台最大的资源浪费之一:题库里 20 万道题,学生却找不到适合自己的那一道。关键词搜索"二次函数"能返回 3000+ 条结果,但这些题目按照录入时间排序而不是按难度排序,导致学生要么做了太简单的题(浪费时间),要么做了太难的题(打击信心,直接退出)。

理想的智能题库需要解决两个问题:一是"找相似题"(给定一道题,找到与其考察知识点和解题思路相似的题目),二是"评估难度"(这道题对当前学生来说有多难?)。前者用向量检索,后者需要结合题目属性、学生历史表现和 IRT(项目反应理论)来建模。

二、相似题检索与难度评估的系统架构

核心设计是"向量检索 + 难度预测"的双路协同:

核心公式:推荐得分 = 相似度_score × 0.6 + 难度适配_score × 0.4。相似度保证"找对题",难度适配保证"适合学生"。两者权重可以通过 A/B 测试调优。

三、Python 实现:相似题检索 + IRT 难度模型

import numpy as np
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
from scipy.special import expit # sigmoid 函数

@dataclass
class Question:
"""题库中的题目"""
question_id: str
text: str
subject: str
knowledge_ids: List[str]
question_type: str # choice, fill, proof
difficulty_param: float # IRT b 参数(难度)
discrimination: float # IRT a 参数(区分度)
guess_param: float = 0.25 # IRT c 参数(猜测概率)
embedding: Optional[np.ndarray] = None

@dataclass
class StudentAbility:
"""学生能力估计"""
student_id: str
theta: float # IRT 能力值 θ
theta_std: float # 能力值标准差(不确定性)
n_questions: int # 已作答题目数

class IRTDifficultyModel:
"""基于 IRT 的难度评估模型"""

@staticmethod
def probability_correct(
theta: float, question: Question
) -> float:
"""3PL IRT 模型:P(correct|θ)"""
a, b, c = (
question.discrimination,
question.difficulty_param,
question.guess_param,
)
return c + (1 – c) * expit(1.702 * a * (theta – b))

@staticmethod
def information(
theta: float, question: Question
) -> float:
"""题目信息量(用于选择最优下一题)"""
q_val = 1.702 * question.discrimination
p = IRTDifficultyModel.probability_correct(
theta, question
)
return (q_val ** 2 * (p – question.guess_param) ** 2 *
(1 – p) / (p * (1 – question.guess_param)))

@staticmethod
def update_ability(
student: StudentAbility,
question: Question,
correct: bool,
) -> StudentAbility:
"""贝叶斯更新学生能力值(简化 EAP 估计)"""
prob = IRTDifficultyModel.probability_correct(
student.theta, question
)
# 似然函数 × 先验(简化版)
if correct:
likelihood = prob
else:
likelihood = 1 – prob

# 梯度方向更新
learning_rate = 0.1 / (1 + 0.01 * student.n_questions)
gradient = correct – prob
new_theta = student.theta + learning_rate * gradient

return StudentAbility(
student_id=student.student_id,
theta=new_theta,
theta_std=student.theta_std * 0.95, # 随数据增加降低不确定性
n_questions=student.n_questions + 1,
)

class SmartQuestionBank:
"""智能题库"""

def __init__(self):
self.questions: Dict[str, Question] = {}
# 倒排索引:知识点 -> 题目 ID 列表
self.knowledge_index: Dict[str, List[str]] = {}
# 学生能力模型
self.students: Dict[str, StudentAbility] = {}

def add_question(self, q: Question):
"""添加题目到题库"""
self.questions[q.question_id] = q
for kid in q.knowledge_ids:
self.knowledge_index.setdefault(kid, []).append(
q.question_id
)

def search_similar(
self,
query_embedding: np.ndarray,
top_k: int = 10,
min_similarity: float = 0.85,
) -> List[Tuple[Question, float]]:
"""向量检索相似题"""
results = []
for q in self.questions.values():
if q.embedding is None:
continue
sim = np.dot(query_embedding, q.embedding) / (
np.linalg.norm(query_embedding) *
np.linalg.norm(q.embedding) + 1e-8
)
if sim >= min_similarity:
results.append((q, float(sim)))
results.sort(key=lambda x: x[1], reverse=True)
return results[:top_k]

def recommend_by_ability(
self,
student_id: str,
target_knowledge_id: str,
top_k: int = 5,
strategy: str = "i_plus_1",
) -> List[Tuple[Question, float]]:
"""基于学生能力推荐题目"""
student = self.students.get(student_id)
if student is None:
# 新学生,默认中等能力
student = StudentAbility(
student_id=student_id,
theta=0.0, theta_std=1.0, n_questions=0,
)
self.students[student_id] = student

# 获取该知识点的所有题目
candidate_ids = self.knowledge_index.get(
target_knowledge_id, []
)
if not candidate_ids:
return []

scored = []
for qid in candidate_ids:
q = self.questions.get(qid)
if q is None:
continue
# 难度适配分
if strategy == "i_plus_1":
# i+1 策略:推荐略高于当前能力的题
difficulty_match = 1.0 – abs(
q.difficulty_param –
(student.theta + 0.5)
) / 3.0
else:
# 最近发展区策略
difficulty_match = 1.0 – abs(
q.difficulty_param – student.theta
) / 3.0

# 信息量加权
info = IRTDifficultyModel.information(
student.theta, q
)
final_score = (
0.4 * difficulty_match +
0.3 * info +
0.3 * q.discrimination
)
scored.append((q, final_score))

scored.sort(key=lambda x: x[1], reverse=True)
return scored[:top_k]

def hybrid_recommend(
self,
student_id: str,
query_text: str,
query_embedding: np.ndarray,
top_k: int = 10,
) -> List[Tuple[Question, float, str]]:
"""混合推荐:相似度 + 能力匹配"""
# 1. 向量检索相似题
similar = self.search_similar(
query_embedding, top_k=top_k * 3
)

# 2. 能力匹配过滤
student = self.students.get(student_id)
if student is None:
# 新学生,只按相似度排序
return [(q, sim, "similarity") for q, sim in similar[:top_k]]

# 3. 综合评分
scored = []
for q, sim in similar:
ability_score = 1.0 – abs(
q.difficulty_param – student.theta
) / 3.0
final = 0.6 * sim + 0.4 * ability_score
scored.append((q, final, "hybrid"))

scored.sort(key=lambda x: x[1], reverse=True)
return scored[:top_k]

四、边界分析与 Trade-offs

向量检索的精度 vs 效率:全库做余弦相似度计算在 20 万量级还能撑(约 200ms),到 100 万题量就需要向量数据库。Milvus 和 Qdrant 都能满足,但部署维护成本不低。小规模题库用 FAISS 的 IVF 索引(内存中)是性价比最高的方案。

IRT 模型的数据需求:3PL IRT 模型要求每个题目至少有 100-200 次作答数据才能稳定估计 a、b、c 参数。新题没有足够数据时,可以用题目属性(题型、字数、知识点难度)做回归估计初始参数,然后用在线学习逐步修正。

i+1 策略的风险:持续推荐"略高于能力"的题目,如果学生连续做错,挫败感累积。实践中需要设置"错误容忍窗口"——连续 3 次错误后,策略自动切换为"复习模式",推荐学生已经掌握的知识点题目来恢复信心。

Embedding 模型的领域适配:通用 Sentence-BERT 在数学题 Embedding 上效果一般(相似度区分度不够)。如果能用题库本身的数据做对比学习微调(SimCSE 方法),Top-10 召回率可以从 72% 提升到 91%。微调需要 5 万对正负样本,成本可控。

五、总结

智能题库的两个核心技术是"向量检索找相似题"和"IRT 模型评估难度"。前者保证检索相关性,后者保证难度适配性。两者融合的策略权重(0.6 : 0.4)需要通过实验确定,没有通用最优值。工程上需要注意:向量检索的索引构建和增量更新、IRT 参数的冷启动估计、以及难度推荐策略的容错机制。最终目标是让学生感觉"每道题都刚刚好"——既不会太简单觉得无聊,也不会太难想要放弃。

赞(0)
未经允许不得转载:171主机测评 » RAG 在智能题库中的应用:相似题检索和难度评估方案
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址