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>
389 lines
13 KiB
Go
389 lines
13 KiB
Go
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
|
||
}
|