山东大学软件学院创新实训(四)
前言
上周我们完成了 RAG 检索增强生成和视觉题目识别两大核心功能,让 AI 错题本具备了基础的智能。本周,我重点推进了错因分析报告生成和个性化推题两大功能,让系统成为能初步诊断问题、推荐方案的智能学习助手。
一、错因分析报告
1.1 为什么需要错因分析?
传统的错题本只记录"题目 + 正确答案",但学生真正需要的是:
(1)我为什么错了?(概念理解错误?计算失误?方法选择错误?)
(2)我的知识漏洞在哪里?(具体到哪个知识点没掌握)
(3)我该怎么改进?(针对性的学习建议和练习方向)
错因分析功能就是要回答这三个问题,让每一道错题都成为学习的契机。
1.2 技术架构设计
错因分析的核心流程分为三个阶段:
用户提交错题 → RAG 检索相关知识 → AI 生成结构化报告
关键技术点:
知识库构建:从 LaTeX 教材中提取知识点,向量化存储到 FAISS。
相似度检索:根据题干内容检索最相关的 5 个知识点片段。
结构化输出:强制 AI 返回 JSON 格式,包含错误类型、知识薄弱点、正确解法、学习建议等字段。
1.3 核心代码实现
(1)错因知识库构建脚本
本来打算从docx和pdf文件中提取知识点并导入知识库,但由于公式是以图片的形式呈现的,提取后往往会出现断片的情况,导致知识点极不完整,所以我选择从公共资源上找到的 LaTeX 教材中提取知识点:
class ErrorAnalysisKBBuilder:
"""错因分析知识库构建器"""
def extract_from_latex(self, tex_file, subject="数学"):
"""从LaTeX文件中提取知识点"""
with open(tex_file, 'r', encoding='utf-8') as f:
content = f.read()
# 提取章节结构
chapter_pattern = r'\\chapter\{([^}]+)\}'
section_pattern = r'\\section\{([^}]+)\}'
subsection_pattern = r'\\subsection\{([^}]+)\}'
chapters = re.findall(chapter_pattern, content)
sections = re.findall(section_pattern, content)
subsections = re.findall(subsection_pattern, content)
# 按 subsection 粒度组织知识点
for i, subsection in enumerate(subsections):
knowledge_text = f"【章节】{current_chapter}\n【节】{current_section}\n【知识点】{subsection}"
# 提取该知识点下的内容(简化:取后续500字符)
idx = content.find(subsection)
if idx != -1:
following_content = content[idx:idx + 500]
clean_content = re.sub(r'\\[a-zA-Z]+\{[^}]*\}', '', following_content)
knowledge_text += f"\n\n【内容】{clean_content[:300]}"
knowledge_points.append({
'content': knowledge_text,
'metadata': {
"subject": subject,
"source": os.path.basename(tex_file),
"type": "knowledge_point",
"chapter": current_chapter,
"section": current_section,
"subsection": subsection
}
})
执行后在 data/ 目录生成:
kb_error_analysis.index - FAISS 索引文件
kb_error_analysis.docs.json - 文档和元数据
(2)RAG 服务的错因分析接口
def generate_error_analysis(self, question_stem: str, user_answer: str,
correct_answer: str, user_id: str = "system",
top_k: int = 5) -> Dict:
"""
生成错因分析报告
"""
try:
logger.info(f"开始生成错因分析 - UserId: {user_id}")
# 1. 从错因知识库检索相关知识点
docs = self.vector_store.similarity_search(question_stem, k=top_k)
if not docs:
knowledge_context = "暂无相关知识点"
else:
knowledge_parts = []
for i, doc in enumerate(docs, 1):
content = doc.get('content', '')
metadata = doc.get('metadata', {})
chapter = metadata.get('chapter', '')
section = metadata.get('section', '')
subsection = metadata.get('subsection', '')
knowledge_parts.append(
f"【知识点 {i}】{chapter} > {section} > {subsection}\n{content[:200]}"
)
knowledge_context = "\n\n".join(knowledge_parts)
# 2. 构建错因分析提示词
analysis_prompt = self.error_analysis_prompt.format(
question_stem=question_stem,
user_answer=user_answer if user_answer else "未提供",
correct_answer=correct_answer if correct_answer else "未提供",
knowledge_context=knowledge_context
)
# 3. 调用AI生成错因分析
messages = [
{"role": "system", "content": "你是一个专业的数学教育专家。"},
{"role": "user", "content": analysis_prompt}
]
response = self.ai_service.client.chat.completions.create(
model=settings.ai.model,
messages=messages,
temperature=0.5,
max_tokens=2000
)
result_text = response.choices[0].message.content
# 4. 解析JSON结果
try:
# 清理大模型可能返回的 Markdown 代码块标记
cleaned_text = result_text.strip()
# 处理以 ```json 开头的情况
if cleaned_text.startswith("```json"):
cleaned_text = cleaned_text[7:]
# 处理以 ``` 开头的情况
elif cleaned_text.startswith("```"):
cleaned_text = cleaned_text[3:]
# 处理以 ``` 结尾的情况
if cleaned_text.endswith("```"):
cleaned_text = cleaned_text[:-3]
# 处理 LaTeX 反斜杠转义问题
import re
cleaned_text = re.sub(r'\\(?!["\\/bfnrtu])', r'\\\\', cleaned_text)
# 再次去除首尾空白字符
cleaned_text = cleaned_text.strip()
# 尝试解析 JSON
analysis_report = json.loads(cleaned_text)
except json.JSONDecodeError as e:
# 捕获 JSON 解析错误,返回安全默认值
logger.warning(f"JSON解析失败,原始内容: {result_text[:100]}... Error: {e}")
analysis_report = {
"error_type": "未知",
"error_description": result_text,
"knowledge_gaps": [],
"correct_approach": "",
"study_suggestions": [],
"similar_topics": []
}
logger.info(f"错因分析生成完成(降级返回)- UserId: {user_id}")
return analysis_report
关键设计思考:
- temperature=0.5:错因分析需要稳定性和准确性,不能使用过高的随机性。
- JSON 解析容错:大模型可能返回 Markdown 代码块或转义字符,需要多层清理。
- 降级策略:如果 JSON 解析失败,仍返回原始文本,避免服务中断。
(3)消息队列集成
在消费者中新增错因分析任务处理器:
def handle_error_analysis_task(self, task): """处理错因分析任务""" user_id = task.get('user_id') question_stem = task.get('question_stem') user_answer = task.get('user_answer', '') correct_answer = task.get('correct_answer', '')
logger.info(f"收到错因分析任务 - UserId: {user_id}")
# 调用RAG服务生成错因分析
report = rag_service.generate_error_analysis(
question_stem=question_stem,
user_answer=user_answer,
correct_answer=correct_answer,
user_id=user_id,
top_k=5
)
# 将报告发送给Java端
result_message = {
"task_type": "error_analysis_result",
"user_id": user_id,
"report": report,
"timestamp": datetime.now().isoformat()
}
self.channel.basic_publish(
exchange='',
routing_key='error-analysis-result-queue',
body=json.dumps(result_message, ensure_ascii=False),
properties=pika.BasicProperties(delivery_mode=2)
)
1.4 运行效果
当用户提交一道积分计算错误的题目时,系统返回的错因分析报告如下:


可以看出错误类型精确到不同的分类(概念理解错误/计算错误/方法选择错误/审题不清等等),且知识薄弱点具体到知识点名称,学习建议也具有较强的可操作性。
二、个性化推题
2.1 功能需求与实现思路
推题系统设计上采用"用户画像 + 向量检索"的双层架构,即先通过MySQL 统计用户薄弱标签,再使用FAISS 检索相似题目。
但是目前在数据库数据有限的情况下,我们先采用纯向量检索的方式实现推题功能,核心思路是:
用户输入/识别题目 → FAISS 向量库语义检索 → 返回最相似的 Top 3 题目
这种方案只要有题库向量库就能工作,且基于句子嵌入的相似度搜索,比关键词匹配更智能。
2.2 技术实现方案
(1)题库向量库构建
我们目前从本地 DOCX 考研真题文档构建题库向量库:
def import_from_local_docs(self):
"""从本地考研文档中提取题目并导入题库"""
subjects = {
"math": "数学",
"english": "英语",
"politics": "政治"
}
for folder_name, subject_name in subjects.items():
folder_path = os.path.join(base_dir, folder_name)
docx_files = glob.glob(os.path.join(folder_path, "*.docx"))
for docx_file in docx_files:
questions = self._extract_questions_from_docx(docx_file, subject_name)
if questions:
texts = [q['content'] for q in questions]
metadatas = [q['metadata'] for q in questions]
self.rag_service.add_knowledge(texts, metadatas)
题目提取逻辑使用正则表达式识别题干模式:
def _extract_questions_from_docx(self, file_path, subject):
"""从DOCX文档中提取结构化题目"""
doc = Document(file_path)
questions = []
current_question = {}
for para in doc.paragraphs:
text = para.text.strip()
# 匹配题目开头:第1题、第2题、【题目 1】等格式
question_match = re.match(r'^(?:第?(\d+)[题\.、]|【题目[\s]*(\d+)[\s]*】)', text)
if question_match:
# 保存上一道题
if current_question and 'stem' in current_question:
content = f"题目:{current_question['stem']}\n答案:{current_question.get('answer', '')}\n解析:{current_question.get('analysis', '')}"
questions.append({
'content': content,
'metadata': {
"subject": subject,
"source": os.path.basename(file_path),
"type": "question"
}
})
# 开始新题目
current_question = {'stem': text, 'answer': '', 'analysis': ''}
elif '答案' in text or '正确答案' in text:
current_question['answer'] = text
elif '解析' in text or '分析' in text:
current_question['analysis'] = text
else:
current_question['stem'] += '\n' + text
return questions
(2)向量检索核心方法
在 FAISSStore 类中实现了 search_similar 方法,支持语义相似度搜索:
def search_similar(self, query_text: str, k: int = 3) -> List[Dict]:
"""根据题干文本搜索同类题"""
# 1. 在 FAISS 中进行向量相似度搜索
docs = self.similarity_search(query_text, k=k)
# 2. 收集所有 topic_id(如果有)
topic_ids = []
for doc in docs:
topic_id = doc['metadata'].get("topic_id")
if topic_id:
topic_ids.append(topic_id)
# 3. 如果找到了 topic_id,从 MySQL 获取完整信息
if topic_ids:
from database.mysql_client import mysql_client
try:
placeholders = ','.join(['%s'] * len(topic_ids))
sql = f"""
SELECT id, stem as questionStem, correct_answer as answer,
analysis, tags, subject, create_time as createTime
FROM topic
WHERE id IN ({placeholders}) AND deleted = 0
"""
topics = mysql_client.execute_query(sql, tuple(topic_ids))
# 转换为前端需要的格式
result = []
for topic in topics:
result.append({
"id": topic['id'],
"title": topic.get('subject', '未知科目'),
"questionStem": topic['questionStem'],
"questionStemPictureUrls": [],
"answer": topic.get('answer', ''),
"answerPictureUrls": [],
"createTime": str(topic.get('createTime', '')),
"labels": []
})
return result
except Exception as e:
logger.error(f"从MySQL获取题目详情失败: {str(e)}")
# 4. 降级方案:直接返回向量库中的简化格式
return [{
"id": doc['metadata'].get("topic_id"),
"title": doc['metadata'].get("subject", '未知'),
"questionStem": doc['content'],
"questionStemPictureUrls": [],
"answer": "",
"answerPictureUrls": [],
"createTime": None,
"updateTime": None,
"labels": []
} for doc in docs]
关键技术点:
- 两级检索策略:先在 FAISS 快速筛选候选集,再从 MySQL 获取完整信息。
- 降级容错机制:如果 MySQL 查询失败,仍返回向量库中的基础信息。
- 数量限制:默认 k=3,避免推送过多题目造成用户负担。
2.3 测试效果演示
执行 测试文件,输出结果如下:



可以看到系统查询"导数与微分的计算",推送了三道有关的微积分题目,且这些题目格式规范统一,自动清理了"题目:"、"答案:"等前缀,具有较好的可读性。
2.4 与图片分析功能的集成
在视觉识别场景中,我们也集成了类似题推送功能:
def on_picture_message(self, ch, method, properties, body):
"""处理图片分析任务(增强版:错因分析 + 类似题推送)"""
# 1. 调用视觉模型识别题目
result = ai_service.analyze_image(url)
question_stem = result.get("question_stem", "")
# 2. 基于识别结果,检索类似题目(最多3道)
similar_questions = []
if question_stem:
try:
similar_questions = rag_service.vector_store.search_similar(
question_stem,
k=3 # 严格限制3道题
)
logger.info(f"检索到 {len(similar_questions)} 道类似题目")
except Exception as e:
logger.warning(f"类似题检索失败: {str(e)}")
# 3. 构建响应
response = {
"requestId": request_id,
"questionStem": question_stem,
"analyseQuestion": result.get("analysis", ""),
"suggestLabels": result.get("tags", []),
"recommendTopics": similar_questions # 新增:类似题列表
}
这样用户拍照录入错题后,不仅能得到题目解析,还能立即获得 3 道类似的练习题进行巩固。
三、总结与规划
本周的开发使得错题本从一个题目"记录工具"变成了"智能学习助手"。它不仅能识别题目、回答问题,更能诊断错误原因、推荐学习方案。这种从"被动记录"到"主动指导"的转变,正是 AI 技术在教育领域的核心价值所在。
最初我直接把检索到的知识丢给 AI,结果生成的分析报告泛泛而谈。后来我在提示词中明确要求"错误类型必须从设计的几种中选择"、"知识薄弱点要具体到知识点名称",效果显著提升。这也让我意识到了提示词的重要性:好的提示词工程 = 清晰的约束 + 具体的示例 + 结构化的输出要求
目前的系统已经具备了"诊断 + 推荐"的核心能力,但仍有一些优化空间。后续将实现向量检索与传统数据库的互补,同时优化推送算法,真正实现:用户历史错题 → MySQL 统计薄弱标签 → FAISS 检索相似题目 → 去重排序 → 推送 的"用户画像 + 向量检索"的双层架构,做到个性化精准推题。同时进一步扩充知识库和题库,增加多样性控制,避免同一知识点下推送过于相似的题目。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)