AnCao/internal/services/ai_grading.go
yanlongqi 3b7133d9de 添加论述题权限控制系统和AI评分功能
本次更新实现了基于用户类型的论述题访问权限控制,并为论述题添加了专门的AI评分功能。

后端更新:
- 添加论述题权限验证:根据用户类型(ordinary-person/management-person)控制不同论述题的访问权限
- 新增 GradeEssay 方法:为论述题提供专门的AI评分,不依赖标准答案,基于保密法规进行专业评分
- 优化AI评分提示词:增加法规依据要求,返回参考答案、评分依据等更详细的评分信息
- 添加用户类型管理:新增 UpdateUserType API,支持用户更新个人类型
- 路由调整:将练习题相关API移至需要认证的路由组

前端更新:
- 论述题答题界面优化:不显示标准答案,展示AI评分的参考答案和评分依据
- 用户类型选择:登录/注册时支持选择用户类型
- 权限控制适配:根据用户类型显示对应的论述题列表

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 16:47:37 +08:00

497 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"` // 改进建议
ReferenceAnswer string `json:"reference_answer"` // 参考答案(论述题)
ScoringRationale string `json:"scoring_rationale"` // 评分依据
}
// GradeEssay 对论述题进行AI评分不需要标准答案
// question: 题目内容
// userAnswer: 用户答案
func (s *AIGradingService) GradeEssay(question, userAnswer string) (*AIGradingResult, error) {
// 构建评分提示词(论述题评分模式)
prompt := fmt.Sprintf(`你是一位专业的保密领域阅卷老师,请对以下论述题进行评分。
题目:%s
学生答案:%s
评分依据:
请依据以下保密法律法规和管理制度进行分析和评分:
1. 《中华人民共和国保守国家秘密法》
2. 《中华人民共和国保守国家秘密法实施条例》
3. 《保密工作管理制度2025.9.9》
4. 《软件开发管理制度》
5. 《涉密信息系统集成资质保密标准》
6. 《涉密信息系统集成资质管理办法》
评分标准(论述题没有固定标准答案,请根据答题质量和法规符合度评分):
1. 论点是否明确是否符合保密法规要求30分
2. 内容是否充实论据是否引用相关法规条文30分
3. 逻辑是否严密分析是否符合保密工作实际25分
4. 语言表达是否准确、专业15分
评分等级:
- 85-100分论述优秀论点明确、论据充分、符合法规要求、分析专业
- 70-84分论述良好基本要素齐全符合保密工作要求
- 60-69分论述基本合格要点基本涵盖但不够深入
- 40-59分论述不够完整缺乏法规支撑或逻辑性较差
- 0-39分论述严重缺失或完全离题
判断标准60分及以上为正确is_correct: true否则为错误is_correct: false
评分要求:
1. 给出一个0-100的精确分数
2. 判断答案是否正确is_correct: 60分及以上为true否则为false
3. 生成一个专业的参考答案reference_answer150-300字必须引用相关法规条文
4. 给出评分依据scoring_rationale说明依据了哪些法规和条文80-150字
5. 给出简短的评语feedback说明得分情况不超过80字
6. 给出具体的改进建议suggestion如果分数在90分以上可以简短否则必须指出具体改进方向不超过80字
请按照以下JSON格式返回结果
{
"score": 75,
"is_correct": true,
"reference_answer": "根据《中华人民共和国保守国家秘密法》第XX条...",
"scoring_rationale": "依据《保密法》第XX条、《保密法实施条例》第XX条...",
"feedback": "论述较为完整,论点明确,但论据不够充分,缺少具体法规引用",
"suggestion": "建议补充《保密法》相关条文,加强论点之间的逻辑联系"
}
注意:
1. 只返回JSON格式的结果不要有其他内容
2. 参考答案必须专业、准确,体现保密法规要求
3. 评分依据必须具体引用法规条文`, question, 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
}
// GradeShortAnswer 对简答题进行AI评分
// question: 题目内容
// standardAnswer: 标准答案
// userAnswer: 用户答案
func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) {
// 构建评分提示词(严格评分模式)
prompt := fmt.Sprintf(`你是一位专业的保密领域阅卷老师,请严格按照标准答案对以下简答题进行评分。
题目:%s
标准答案:%s
学生答案:%s
评分依据:
请依据以下保密法律法规和管理制度进行分析和评分:
1. 《中华人民共和国保守国家秘密法》
2. 《中华人民共和国保守国家秘密法实施条例》
3. 《保密工作管理制度2025.9.9》
4. 《软件开发管理制度》
5. 《涉密信息系统集成资质保密标准》
6. 《涉密信息系统集成资质管理办法》
评分标准(请严格遵守):
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. 给出评分依据scoring_rationale说明依据了哪些法规和标准答案的哪些要点80-150字
4. 给出简短的评语feedback说明得分和失分原因不超过80字
5. 给出具体的改进建议suggestion如果答案满分可以为空否则必须指出具体改进方向不超过80字
请按照以下JSON格式返回结果
{
"score": 85,
"is_correct": true,
"scoring_rationale": "依据《保密法》第XX条和标准答案要点分析...",
"feedback": "答案覆盖了主要要点但XXX部分描述不够准确",
"suggestion": "建议补充XXX内容并完善XXX的描述"
}
注意:
1. 只返回JSON格式的结果不要有其他内容
2. 必须严格对照标准答案评分,不要过于宽松
3. 评分依据必须说明符合或违反了哪些法规要求`, 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. 《保密工作管理制度2025.9.9》
4. 《软件开发管理制度》
5. 《涉密信息系统集成资质保密标准》
6. 《涉密信息系统集成资质管理办法》
请提供一个详细的解析,要求:
1. **必须基于保密法规**:解析时必须引用相关法规条文,说明依据哪些具体法律法规
2. **必须实事求是**:只基于题目内容、标准答案和实际法规进行解析
3. **不要胡编乱造**:如果某些信息不确定或题目没有提供,请如实说明,不要编造法规条文
4. **使用Markdown格式**使用标题、列表、加粗等markdown语法使内容更清晰易读
解析内容要求:
- **知识点**:说明题目考查的核心知识点,指出涉及哪些保密法规
- **法规依据**明确引用相关法律法规的具体条文《保密法》第X条、《保密法实施条例》第X条等
- **解题思路**:提供清晰的解题步骤和方法,结合保密工作实际
%s
示例输出格式:
## 知识点
本题考查的是[知识点名称]涉及《XX法规》第XX条...
## 法规依据
- 《中华人民共和国保守国家秘密法》第XX条规定...
- 《保密工作管理制度2025.9.9》第X章第X节...
## 解题思路
1. 首先根据《XX法规》第XX条我们可以判断...
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选项**[分析该选项]根据《XX法规》第XX条...
- **B选项**[分析该选项]根据《XX法规》第XX条...
- **C选项**[分析该选项]根据《XX法规》第XX条...
- **D选项**[分析该选项]根据《XX法规》第XX条...
## 正确答案
正确答案是... 因为根据《XX法规》第XX条规定...`
}
return `## 答案解析
正确答案是... 根据《XX法规》第XX条的规定...`
}(),
// 根据题目类型添加总结要求
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.3, // 低温度,保证输出相对稳定和专业
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]
}