AnCao/internal/services/baidu_ai_grading.go
yanlongqi 4ac3243f6e 重构AI配置并修复前端类型错误
1. 删除AIConfig中未使用的属性(BaseURL、Model)
2. 修复ExamManagement页面Tag组件的size属性错误
3. 添加shared_by.nickname类型定义
4. 优化AI评分提示词,移除冗余的评分依据列表

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 22:23:00 +08:00

389 lines
13 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"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/baidubce/app-builder/go/appbuilder"
)
// BaiduAIGradingService 百度云AI评分服务
type BaiduAIGradingService struct {
client *appbuilder.AppBuilderClient
config *config.AIConfig
conversationID string // 会话ID,用于保持上下文
}
// 全局单例和锁
var (
globalBaiduService *BaiduAIGradingService
serviceMutex sync.Mutex
serviceInitialized bool
)
// NewBaiduAIGradingService 创建百度云AI评分服务实例(使用单例模式)
func NewBaiduAIGradingService() (*BaiduAIGradingService, error) {
serviceMutex.Lock()
defer serviceMutex.Unlock()
// 如果已经初始化过,直接返回
if serviceInitialized && globalBaiduService != nil {
return globalBaiduService, nil
}
cfg := config.GetAIConfig()
// 设置百度云AppBuilder Token
clientConfig, err := appbuilder.NewSDKConfig("", cfg.APIKey)
if err != nil {
return nil, fmt.Errorf("创建SDK配置失败: %w", err)
}
// 创建AppBuilder客户端
client, err := appbuilder.NewAppBuilderClient(cfg.BaiduAppID, clientConfig)
if err != nil {
return nil, fmt.Errorf("创建AppBuilder客户端失败: %w", err)
}
// 创建会话
conversationID, err := client.CreateConversation()
if err != nil {
return nil, fmt.Errorf("创建会话失败: %w", err)
}
log.Printf("百度云AI服务初始化成功会话ID: %s", conversationID)
globalBaiduService = &BaiduAIGradingService{
client: client,
config: cfg,
conversationID: conversationID,
}
serviceInitialized = true
return globalBaiduService, nil
}
// GradeEssay 对论述题进行AI评分(不需要标准答案)
func (s *BaiduAIGradingService) GradeEssay(question, userAnswer string) (*AIGradingResult, error) {
prompt := fmt.Sprintf(`你是一位专业的保密领域阅卷老师,请对以下论述题进行评分。
题目:%s
学生答案:%s
评分标准(论述题没有固定标准答案,请根据答题质量和法规符合度评分):
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_answer,150-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
answer, err := s.runAppBuilder(prompt)
if err != nil {
return nil, err
}
// 解析返回结果
var result AIGradingResult
if err := parseAIResponse(answer, &result); err != nil {
return nil, fmt.Errorf("解析AI响应失败: %w", err)
}
return &result, nil
}
// GradeShortAnswer 对简答题进行AI评分
func (s *BaiduAIGradingService) 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
answer, err := s.runAppBuilder(prompt)
if err != nil {
return nil, err
}
// 解析返回结果
var result AIGradingResult
if err := parseAIResponse(answer, &result); err != nil {
return nil, fmt.Errorf("解析AI响应失败: %w", err)
}
return &result, nil
}
// ExplainQuestionStream 生成题目解析(流式输出)
func (s *BaiduAIGradingService) ExplainQuestionStream(writer http.ResponseWriter, question, standardAnswer, questionType string) error {
prompt := fmt.Sprintf(`你是一位严谨、专业的保密领域专家老师,请对以下题目进行详细解析。
题目类型:%s
题目内容:%s
标准答案:%s
请提供一个详细的解析,要求:
1. **必须基于保密法规**:解析时必须引用相关法规条文,说明依据哪些具体法律法规
2. **必须实事求是**:只基于题目内容、标准答案和实际法规进行解析
3. **不要胡编乱造**:如果某些信息不确定或题目没有提供,请如实说明,不要编造法规条文
解析内容要求:
- **知识点**:说明题目考查的核心知识点,指出涉及哪些保密法规
- **法规依据**:明确引用相关法律法规的具体条文(如:《保密法》第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(流式)
return s.runAppBuilderStream(writer, prompt)
}
// ExplainQuestion 生成题目解析
func (s *BaiduAIGradingService) 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
answer, err := s.runAppBuilder(prompt)
if err != nil {
return nil, err
}
// 解析返回结果
var result AIExplanationResult
if err := parseAIResponse(answer, &result); err != nil {
return nil, fmt.Errorf("解析AI响应失败: %w", err)
}
return &result, nil
}
// runAppBuilder 调用百度云AppBuilder(非流式)
func (s *BaiduAIGradingService) runAppBuilder(query string) (string, error) {
startTime := time.Now()
log.Printf("[百度云AI] 开始调用会话ID: %s", s.conversationID)
// 调用AppBuilder Run方法 - 注意:即使stream=false,依然需要迭代读取
iterator, err := s.client.Run(s.conversationID, query, nil, false)
if err != nil {
return "", fmt.Errorf("调用AppBuilder失败: %w", err)
}
// 收集所有返回内容
var fullAnswer strings.Builder
// 只读取一次 - 非流式模式下SDK应该一次性返回完整结果
answer, err := iterator.Next()
if err != nil {
if errors.Is(err, io.EOF) {
return "", fmt.Errorf("AI未返回任何内容")
}
return "", fmt.Errorf("读取AppBuilder响应失败: %w", err)
}
if answer != nil && answer.Answer != "" {
fullAnswer.WriteString(answer.Answer)
}
result := fullAnswer.String()
if result == "" {
return "", fmt.Errorf("AI未返回任何内容")
}
elapsed := time.Since(startTime)
log.Printf("[百度云AI] 调用完成,耗时: %v, 返回长度: %d", elapsed, len(result))
return result, nil
}
// runAppBuilderStream 调用百度云AppBuilder(流式)
func (s *BaiduAIGradingService) runAppBuilderStream(writer http.ResponseWriter, query string) error {
// 调用AppBuilder Run方法(流式)
iterator, err := s.client.Run(s.conversationID, query, nil, true)
if err != nil {
return fmt.Errorf("创建流式请求失败: %w", err)
}
flusher, ok := writer.(http.Flusher)
if !ok {
return fmt.Errorf("响应写入器不支持Flush")
}
// 读取流式响应并发送给客户端 - 参考百度云SDK示例代码的方式
var answer *appbuilder.AppBuilderClientAnswer
for answer, err = iterator.Next(); err == nil; answer, err = iterator.Next() {
// 发送增量内容
if answer != nil && answer.Answer != "" {
// 使用SSE格式发送
fmt.Fprintf(writer, "data: %s\n\n", answer.Answer)
flusher.Flush()
}
}
// 检查是否因为EOF之外的错误退出 - 使用errors.Is更安全
if err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("接收流式响应失败: %w", err)
}
// 流结束
fmt.Fprintf(writer, "data: [DONE]\n\n")
flusher.Flush()
return nil
}