package services import ( "ankao/pkg/config" "context" "encoding/json" "fmt" "io" "net/http" "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. 必须与标准答案进行逐项对比 2. 答案要点完全覆盖标准答案且表述准确的,给85-100分 3. 答案要点基本覆盖但有缺漏或表述不够准确的,给60-84分 4. 答案要点缺失较多或有明显错误的,给40-59分 5. 答案完全错误或离题的,给0-39分 6. 判断标准:60分及以上为正确(is_correct: true),否则为错误(is_correct: false) 评分要求: 1. 给出一个0-100的精确分数 2. 判断答案是否正确(is_correct: 60分及以上为true,否则为false) 3. 给出简短的评语(说明得分和失分原因,不超过50字) 4. 给出具体的改进建议(如果答案满分可以为空,否则必须指出具体改进方向,不超过50字) 请按照以下JSON格式返回结果: { "score": 85, "is_correct": true, "feedback": "答案覆盖了主要要点,但XXX部分描述不够准确", "suggestion": "建议补充XXX内容,并完善XXX的描述" } 注意:只返回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, // 温度为0,获得最确定、最一致的评分结果 }, ) 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 interface{}) 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 } // AIExplanationResult AI解析结果 type AIExplanationResult struct { Explanation string `json:"explanation"` // 题目解析 } // ExplainQuestionStream 生成题目解析(流式输出) // writer: HTTP响应写入器 // question: 题目内容 // standardAnswer: 标准答案 // questionType: 题目类型 func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, question, standardAnswer, questionType string) error { // 构建解析提示词(直接输出,不使用JSON格式) prompt := fmt.Sprintf(`你是一位严谨、专业的老师,请对以下题目进行详细解析。 题目类型:%s 题目内容:%s 标准答案:%s 请提供一个详细的解析,要求: 1. **必须实事求是**:只基于题目内容和标准答案进行解析,不要添加题目中没有的信息 2. **不要胡编乱造**:如果某些信息不确定或题目没有提供,请如实说明,不要编造 3. **使用Markdown格式**:使用标题、列表、加粗等markdown语法使内容更清晰易读 解析内容要求: - **知识点**:说明题目考查的核心知识点 - **解题思路**:提供清晰的解题步骤和方法 %s 示例输出格式: ## 知识点 题目考查的是... ## 解题思路 1. 首先分析... 2. 然后判断... %s ## 总结 %s 请使用markdown格式输出解析内容。`, questionType, question, standardAnswer, // 根据题目类型添加特定要求 func() string { if questionType == "single-selection" || questionType == "multiple-selection" { return `- **选项分析**:对于选择题,必须逐项分析每个选项的对错及原因 - **记忆口诀**:如果适用,提供便于记忆的口诀或技巧` } return "- **答案解析**:详细说明为什么这个答案是正确的" }(), // 根据题目类型添加示例格式 func() string { if questionType == "single-selection" || questionType == "multiple-selection" { return `## 选项分析 - **A选项**:[分析该选项] - **B选项**:[分析该选项] - **C选项**:[分析该选项] - **D选项**:[分析该选项] ## 正确答案 正确答案是... 因为...` } return `## 答案解析 正确答案是... 因为...` }(), // 根据题目类型添加总结要求 func() string { if questionType == "single-selection" || questionType == "multiple-selection" { return "对于选择题,可以提供记忆口诀或关键要点总结" } return "总结本题的关键要点和注意事项" }(), ) // 调用AI API(流式) stream, err := s.client.CreateChatCompletionStream( context.Background(), openai.ChatCompletionRequest{ Model: s.config.Model, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleSystem, Content: "你是一位严谨、专业的老师,擅长深入浅出地讲解题目。你必须实事求是,只基于题目和标准答案提供解析,不编造任何不确定的信息。你使用Markdown格式输出,让学生更容易理解。", }, { Role: openai.ChatMessageRoleUser, Content: prompt, }, }, Temperature: 0, // 温度为0,获得最确定、最一致的输出 Stream: true, }, ) if err != nil { return fmt.Errorf("创建流式请求失败: %w", err) } defer stream.Close() flusher, ok := writer.(http.Flusher) if !ok { return fmt.Errorf("响应写入器不支持Flush") } // 读取流式响应并发送给客户端 for { response, err := stream.Recv() if err == io.EOF { // 流结束 fmt.Fprintf(writer, "data: [DONE]\n\n") flusher.Flush() break } if err != nil { return fmt.Errorf("接收流式响应失败: %w", err) } // 获取增量内容 if len(response.Choices) > 0 { delta := response.Choices[0].Delta.Content if delta != "" { // 使用SSE格式发送 fmt.Fprintf(writer, "data: %s\n\n", delta) flusher.Flush() } } } return nil } // ExplainQuestion 生成题目解析 // question: 题目内容 // standardAnswer: 标准答案 // questionType: 题目类型 func (s *AIGradingService) ExplainQuestion(question, standardAnswer, questionType string) (*AIExplanationResult, error) { // 构建解析提示词 prompt := fmt.Sprintf(`你是一位经验丰富的老师,请对以下题目进行详细解析。 题目类型:%s 题目内容:%s 标准答案:%s 请提供一个详细的解析,包括: 1. 题目考查的知识点 2. 解题思路和方法 3. 为什么选择这个答案 4. 相关的重要概念或注意事项 请按照以下JSON格式返回结果: { "explanation": "这道题考查的是...(200字以内的详细解析)" } 注意:只返回JSON格式的结果,不要有其他内容。`, questionType, question, standardAnswer) // 调用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.7, // 中等温度以获得有创造性的解释 }, ) 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 AIExplanationResult if err := parseAIResponse(content, &result); err != nil { return nil, fmt.Errorf("解析AI响应失败: %w", err) } return &result, 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] }