新增功能: 1. AI智能评分系统 - 集成OpenAI兼容API进行简答题评分 - 提供分数、评语和改进建议 - 支持自定义AI服务配置(BaseURL、APIKey、Model) 2. 题目列表页面 - 展示所有题目和答案 - Tab标签页形式的题型筛选(选择题、多选题、判断题、填空题、简答题) - 关键词搜索功能(支持题目内容和编号搜索) - 填空题特殊渲染:****显示为下划线 - 判断题不显示选项,界面更简洁 3. UI优化 - 答题结果组件重构,支持AI评分显示 - 首页新增"题目列表"快速入口 - 响应式设计,适配移动端和PC端 技术改进: - 添加AI评分服务层(internal/services/ai_grading.go) - 扩展题目模型支持AI评分结果 - 更新配置管理支持AI服务配置 - 添加AI评分测试脚本和文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
157 lines
3.9 KiB
Go
157 lines
3.9 KiB
Go
package services
|
||
|
||
import (
|
||
"ankao/pkg/config"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/sashabaranov/go-openai"
|
||
)
|
||
|
||
// AIGradingService AI评分服务
|
||
type AIGradingService struct {
|
||
client *openai.Client
|
||
config *config.AIConfig
|
||
}
|
||
|
||
// NewAIGradingService 创建AI评分服务实例
|
||
func NewAIGradingService() *AIGradingService {
|
||
cfg := config.GetAIConfig()
|
||
|
||
// 创建OpenAI客户端配置
|
||
clientConfig := openai.DefaultConfig(cfg.APIKey)
|
||
clientConfig.BaseURL = cfg.BaseURL + "/v1" // 标准OpenAI API格式需要/v1后缀
|
||
|
||
client := openai.NewClientWithConfig(clientConfig)
|
||
|
||
return &AIGradingService{
|
||
client: client,
|
||
config: cfg,
|
||
}
|
||
}
|
||
|
||
// AIGradingResult AI评分结果
|
||
type AIGradingResult struct {
|
||
Score float64 `json:"score"` // 得分 (0-100)
|
||
IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确)
|
||
Feedback string `json:"feedback"` // 评语
|
||
Suggestion string `json:"suggestion"` // 改进建议
|
||
}
|
||
|
||
// GradeShortAnswer 对简答题进行AI评分
|
||
// question: 题目内容
|
||
// standardAnswer: 标准答案
|
||
// userAnswer: 用户答案
|
||
func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) {
|
||
// 构建评分提示词
|
||
prompt := fmt.Sprintf(`你是一位专业的阅卷老师,请对以下简答题进行评分。
|
||
|
||
题目:%s
|
||
|
||
标准答案:%s
|
||
|
||
学生答案:%s
|
||
|
||
请按照以下要求进行评分:
|
||
1. 给出一个0-100的分数
|
||
2. 判断答案是否正确(60分及以上为正确)
|
||
3. 给出简短的评语(不超过50字)
|
||
4. 如果答案不完善,给出改进建议(不超过50字,如果答案很好可以为空)
|
||
|
||
请按照以下JSON格式返回结果:
|
||
{
|
||
"score": 85,
|
||
"is_correct": true,
|
||
"feedback": "答案基本正确,要点全面",
|
||
"suggestion": "可以补充一些具体的例子"
|
||
}
|
||
|
||
注意:只返回JSON格式的结果,不要有其他内容。`, question, standardAnswer, userAnswer)
|
||
|
||
// 调用AI API
|
||
resp, err := s.client.CreateChatCompletion(
|
||
context.Background(),
|
||
openai.ChatCompletionRequest{
|
||
Model: s.config.Model,
|
||
Messages: []openai.ChatCompletionMessage{
|
||
{
|
||
Role: openai.ChatMessageRoleSystem,
|
||
Content: "你是一位专业的阅卷老师,擅长对简答题进行公正、客观的评分。",
|
||
},
|
||
{
|
||
Role: openai.ChatMessageRoleUser,
|
||
Content: prompt,
|
||
},
|
||
},
|
||
Temperature: 0.3, // 较低的温度以获得更稳定的评分结果
|
||
},
|
||
)
|
||
|
||
if err != nil {
|
||
return nil, fmt.Errorf("AI评分失败: %w", err)
|
||
}
|
||
|
||
if len(resp.Choices) == 0 {
|
||
return nil, fmt.Errorf("AI未返回评分结果")
|
||
}
|
||
|
||
// 解析AI返回的JSON结果
|
||
content := resp.Choices[0].Message.Content
|
||
|
||
var result AIGradingResult
|
||
if err := parseAIResponse(content, &result); err != nil {
|
||
return nil, fmt.Errorf("解析AI响应失败: %w", err)
|
||
}
|
||
|
||
return &result, nil
|
||
}
|
||
|
||
// parseAIResponse 解析AI返回的JSON响应
|
||
func parseAIResponse(content string, result *AIGradingResult) error {
|
||
// 移除可能的markdown代码块标记
|
||
jsonStr := removeMarkdownCodeBlock(content)
|
||
|
||
// 使用json包解析
|
||
if err := json.Unmarshal([]byte(jsonStr), result); err != nil {
|
||
return fmt.Errorf("JSON解析失败: %w, 原始内容: %s", err, content)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// removeMarkdownCodeBlock 移除markdown代码块标记
|
||
func removeMarkdownCodeBlock(s string) string {
|
||
// 去除可能的```json和```标记
|
||
s = strings.TrimSpace(s)
|
||
|
||
// 移除开头的```json或```
|
||
if strings.HasPrefix(s, "```json") {
|
||
s = s[7:]
|
||
} else if strings.HasPrefix(s, "```") {
|
||
s = s[3:]
|
||
}
|
||
|
||
// 移除结尾的```
|
||
if strings.HasSuffix(s, "```") {
|
||
s = s[:len(s)-3]
|
||
}
|
||
|
||
s = strings.TrimSpace(s)
|
||
|
||
// 查找第一个{的位置
|
||
startIdx := strings.Index(s, "{")
|
||
if startIdx == -1 {
|
||
return s
|
||
}
|
||
|
||
// 查找最后一个}的位置
|
||
endIdx := strings.LastIndex(s, "}")
|
||
if endIdx == -1 || endIdx <= startIdx {
|
||
return s
|
||
}
|
||
|
||
return s[startIdx : endIdx+1]
|
||
}
|