## 主要改进 ### 🚀 Gemini 3 Pro 集成 - 支持通过 OpenRouter API 调用 Google Gemini 3 Pro Preview 模型 - 针对数学试卷优化 prompt,增强数学公式识别能力 - 自动检测 Gemini 模型并启用数学优化模式 - 设置 Gemini 3 Pro 为默认解析模型(AI_MODEL 环境变量) ### 🔧 JSON 转义修复 - 实现 fix_json_escaping() 函数处理 LaTeX 公式中的反斜杠 - 智能识别并转义 LaTeX 命令(\frac, \sqrt, \pm 等) - 保留有效的 JSON 转义序列(\", \\, \/, \uXXXX) - 解决 Gemini 返回的 LaTeX 公式 JSON 解析失败问题 ### 📐 数学解析增强 - 详细的 LaTeX 公式转换说明(分数、根式、积分、矩阵等) - 特殊数学符号支持(不等式、集合、逻辑、箭头等) - 几何图形识别和描述 - 完整的解题步骤和思路分析 ### 📝 新增文件 - GEMINI_INTEGRATION.md: 完整的集成文档 - test_gemini_math.py: Claude vs Gemini 对比测试脚本 - test_gemini_fixed.py: JSON 修复验证测试 - example_gemini_usage.py: 使用示例和最佳实践 - quick_test_gemini.sh: 快速测试脚本 ### 🎯 测试结果 - ✅ 成功解析数学试卷(5 道题,100 分) - ✅ 正确提取所有 LaTeX 公式 - ✅ 详细的解题步骤和知识点标注 - ✅ 准确的难度评估(难度 2-5) ## 配置说明 在 .env 文件中设置: ```bash AI_MODEL=google/gemini-3-pro-preview USE_CLAUDE_PARSER=true ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
240 lines
8.1 KiB
Python
Executable File
240 lines
8.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
测试 Gemini 3 Pro 模型解析数学试卷的能力
|
|
对比 Claude Sonnet 4.5 和 Gemini 3 Pro 在数学试卷解析上的表现
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import time
|
|
import logging
|
|
from pathlib import Path
|
|
from dotenv import load_dotenv
|
|
from claude_exam_parser import ClaudeExamParser
|
|
|
|
# 配置日志
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 加载环境变量
|
|
load_dotenv()
|
|
|
|
|
|
def test_single_model(file_path: str, model: str, subject: str = "数学", grade: str = None):
|
|
"""
|
|
使用单个模型测试试卷解析
|
|
|
|
Args:
|
|
file_path: 试卷文件路径
|
|
model: 模型名称
|
|
subject: 学科
|
|
grade: 年级
|
|
|
|
Returns:
|
|
解析结果和性能指标
|
|
"""
|
|
logger.info(f"\n{'='*80}")
|
|
logger.info(f"开始测试模型: {model}")
|
|
logger.info(f"文件: {file_path}")
|
|
logger.info(f"学科: {subject}")
|
|
logger.info(f"{'='*80}\n")
|
|
|
|
try:
|
|
# 创建解析器
|
|
parser = ClaudeExamParser(model=model)
|
|
|
|
# 开始计时
|
|
start_time = time.time()
|
|
|
|
# 解析试卷
|
|
result = parser.parse_exam_file(file_path, subject=subject, grade=grade)
|
|
|
|
# 结束计时
|
|
end_time = time.time()
|
|
elapsed_time = end_time - start_time
|
|
|
|
if result["success"]:
|
|
parsed_data = result["result"]
|
|
|
|
logger.info(f"\n✅ 解析成功!")
|
|
logger.info(f" 耗时: {elapsed_time:.2f} 秒")
|
|
logger.info(f" 学科: {parsed_data.get('subject', 'N/A')}")
|
|
logger.info(f" 年级: {parsed_data.get('grade', 'N/A')}")
|
|
logger.info(f" 题目总数: {parsed_data.get('total_questions', 0)}")
|
|
|
|
# 题型统计
|
|
stats = parsed_data.get("stats", {})
|
|
logger.info(f"\n 📊 题型统计:")
|
|
logger.info(f" 选择题: {stats.get('choice_count', 0)}")
|
|
logger.info(f" 填空题: {stats.get('fill_blank_count', 0)}")
|
|
logger.info(f" 计算题: {stats.get('calculation_count', 0)}")
|
|
logger.info(f" 解答题: {stats.get('essay_count', 0)}")
|
|
|
|
# 难度分布(如果有)
|
|
if "difficulty_distribution" in stats:
|
|
logger.info(f"\n 📈 难度分布:")
|
|
for level, count in stats["difficulty_distribution"].items():
|
|
logger.info(f" 难度 {level}: {count} 题")
|
|
|
|
# 分析公式数量
|
|
formula_count = 0
|
|
questions_with_formulas = 0
|
|
for q in parsed_data.get("questions", []):
|
|
formulas = q.get("formulas", [])
|
|
if formulas:
|
|
formula_count += len(formulas)
|
|
questions_with_formulas += 1
|
|
|
|
logger.info(f"\n 📐 公式统计:")
|
|
logger.info(f" 总公式数: {formula_count}")
|
|
logger.info(f" 包含公式的题目: {questions_with_formulas}/{parsed_data.get('total_questions', 0)}")
|
|
|
|
# 显示前3道题作为示例
|
|
logger.info(f"\n 📝 前3道题目示例:")
|
|
for i, q in enumerate(parsed_data.get("questions", [])[:3], 1):
|
|
logger.info(f"\n 题目 {q.get('number', i)}:")
|
|
logger.info(f" 类型: {q.get('type', 'N/A')}")
|
|
logger.info(f" 题干: {q.get('question', 'N/A')[:100]}...")
|
|
if q.get("formulas"):
|
|
logger.info(f" 公式: {q.get('formulas', [])[:2]}")
|
|
if q.get("has_image"):
|
|
logger.info(f" 图片: {q.get('image_description', 'N/A')[:80]}...")
|
|
logger.info(f" 难度: {q.get('difficulty', 'N/A')}")
|
|
logger.info(f" 知识点: {q.get('knowledge_points', [])}")
|
|
|
|
return {
|
|
"success": True,
|
|
"model": model,
|
|
"elapsed_time": elapsed_time,
|
|
"total_questions": parsed_data.get('total_questions', 0),
|
|
"formula_count": formula_count,
|
|
"questions_with_formulas": questions_with_formulas,
|
|
"stats": stats,
|
|
"result": parsed_data
|
|
}
|
|
else:
|
|
logger.error(f"\n❌ 解析失败: {result.get('error', 'Unknown error')}")
|
|
return {
|
|
"success": False,
|
|
"model": model,
|
|
"error": result.get('error', 'Unknown error')
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"\n❌ 测试失败: {str(e)}", exc_info=True)
|
|
return {
|
|
"success": False,
|
|
"model": model,
|
|
"error": str(e)
|
|
}
|
|
|
|
|
|
def compare_models(file_path: str, subject: str = "数学", grade: str = None):
|
|
"""
|
|
对比多个模型的解析效果
|
|
|
|
Args:
|
|
file_path: 试卷文件路径
|
|
subject: 学科
|
|
grade: 年级
|
|
"""
|
|
models = [
|
|
"anthropic/claude-sonnet-4.5",
|
|
"google/gemini-3-pro-preview"
|
|
]
|
|
|
|
results = {}
|
|
|
|
for model in models:
|
|
results[model] = test_single_model(file_path, model, subject, grade)
|
|
|
|
# 生成对比报告
|
|
logger.info(f"\n\n{'='*80}")
|
|
logger.info(f"对比报告")
|
|
logger.info(f"{'='*80}\n")
|
|
|
|
comparison_table = []
|
|
|
|
for model, result in results.items():
|
|
if result["success"]:
|
|
comparison_table.append({
|
|
"模型": model,
|
|
"耗时(秒)": f"{result['elapsed_time']:.2f}",
|
|
"题目数": result['total_questions'],
|
|
"公式数": result['formula_count'],
|
|
"含公式题目": f"{result['questions_with_formulas']}/{result['total_questions']}",
|
|
"选择题": result['stats'].get('choice_count', 0),
|
|
"填空题": result['stats'].get('fill_blank_count', 0),
|
|
"计算题": result['stats'].get('calculation_count', 0),
|
|
"解答题": result['stats'].get('essay_count', 0),
|
|
})
|
|
else:
|
|
comparison_table.append({
|
|
"模型": model,
|
|
"状态": f"❌ 失败: {result.get('error', 'Unknown')}",
|
|
})
|
|
|
|
# 打印对比表格
|
|
logger.info("对比结果:")
|
|
logger.info(json.dumps(comparison_table, ensure_ascii=False, indent=2))
|
|
|
|
# 保存详细结果到文件
|
|
output_dir = Path("test_results")
|
|
output_dir.mkdir(exist_ok=True)
|
|
|
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
for model, result in results.items():
|
|
if result["success"]:
|
|
model_name = model.replace("/", "_")
|
|
output_file = output_dir / f"result_{model_name}_{timestamp}.json"
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
|
json.dump(result["result"], f, ensure_ascii=False, indent=2)
|
|
logger.info(f"\n💾 {model} 详细结果已保存到: {output_file}")
|
|
|
|
logger.info(f"\n{'='*80}\n")
|
|
|
|
|
|
def main():
|
|
"""主函数"""
|
|
if len(sys.argv) < 2:
|
|
print("用法:")
|
|
print(f" {sys.argv[0]} <试卷文件路径> [学科] [年级]")
|
|
print()
|
|
print("示例:")
|
|
print(f" {sys.argv[0]} math_exam.pdf 数学 高一")
|
|
print(f" {sys.argv[0]} physics_test.pdf 物理")
|
|
print()
|
|
print("环境变量:")
|
|
print(" OPENROUTER_API_KEY - OpenRouter API Key(必需)")
|
|
print()
|
|
print("支持的模型:")
|
|
print(" - anthropic/claude-sonnet-4.5")
|
|
print(" - google/gemini-3-pro-preview")
|
|
sys.exit(1)
|
|
|
|
file_path = sys.argv[1]
|
|
subject = sys.argv[2] if len(sys.argv) > 2 else "数学"
|
|
grade = sys.argv[3] if len(sys.argv) > 3 else None
|
|
|
|
# 检查文件是否存在
|
|
if not os.path.exists(file_path):
|
|
logger.error(f"文件不存在: {file_path}")
|
|
sys.exit(1)
|
|
|
|
# 检查 API Key
|
|
if not os.getenv("OPENROUTER_API_KEY"):
|
|
logger.error("未设置 OPENROUTER_API_KEY 环境变量")
|
|
logger.error("请在 .env 文件中设置或使用: export OPENROUTER_API_KEY='your-key'")
|
|
sys.exit(1)
|
|
|
|
# 运行对比测试
|
|
compare_models(file_path, subject, grade)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|