使用知识库分析答案

This commit is contained in:
燕陇琪 2025-11-13 07:17:30 +08:00
parent 59364700bc
commit 69ae78b009
9 changed files with 126 additions and 461 deletions

1
go.mod
View File

@ -11,6 +11,7 @@ require (
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/baidubce/app-builder/go/appbuilder v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect

2
go.sum
View File

@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/baidubce/app-builder/go/appbuilder v1.1.1 h1:mPfUGmQU/Vi4KRJca6m34rWH/YWuQWOiPLmjtVjPhuA=
github.com/baidubce/app-builder/go/appbuilder v1.1.1/go.mod h1:mHOdSd9TJ52aiUbRE2rW1omu4A0U7H32xN39ED+etmE=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=

View File

@ -79,7 +79,11 @@ func gradeExam(recordID uint, examID uint, userID uint) {
// 评分 // 评分
totalScore := 0.0 totalScore := 0.0
aiService := services.NewAIGradingService() aiService, err := services.NewAIGradingService()
if err != nil {
log.Printf("AI服务初始化失败: %v将跳过AI评分", err)
// 不返回错误继续评分流程只是跳过AI评分
}
for _, question := range questions { for _, question := range questions {
userAnswerRaw, answered := answersMap[question.ID] userAnswerRaw, answered := answersMap[question.ID]
@ -195,9 +199,16 @@ func gradeExam(recordID uint, examID uint, userID uint) {
// AnswerData 直接存储答案文本 // AnswerData 直接存储答案文本
correctAnswer := question.AnswerData correctAnswer := question.AnswerData
userAnswerStr := fmt.Sprintf("%v", userAnswerRaw) userAnswerStr := fmt.Sprintf("%v", userAnswerRaw)
aiResult, err := aiService.GradeShortAnswer(question.Question, correctAnswer, userAnswerStr)
if err != nil { // 检查AI服务是否可用
log.Printf("AI评分失败: %v", err) if aiService == nil {
log.Printf("AI服务不可用无法评分问题 %d", question.ID)
isCorrect = false
score = 0
} else {
aiResult, aiErr := aiService.GradeShortAnswer(question.Question, correctAnswer, userAnswerStr)
if aiErr != nil {
log.Printf("AI评分失败: %v", aiErr)
isCorrect = false isCorrect = false
score = 0 score = 0
} else { } else {
@ -219,6 +230,7 @@ func gradeExam(recordID uint, examID uint, userID uint) {
} }
} }
} }
}
totalScore += score totalScore += score

View File

@ -259,10 +259,17 @@ func SubmitPracticeAnswer(c *gin.Context) {
} }
// 调用AI评分服务带重试机制 // 调用AI评分服务带重试机制
aiService := services.NewAIGradingService() aiService, err := services.NewAIGradingService()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": fmt.Sprintf("AI评分服务初始化失败: %v", err),
})
return
}
var aiResult *services.AIGradingResult var aiResult *services.AIGradingResult
var err error maxRetries := 3 // 减少重试次数从5次改为3次
maxRetries := 5
// 区分简答题和论述题的评分方式 // 区分简答题和论述题的评分方式
if strings.HasSuffix(question.Type, "-essay") { if strings.HasSuffix(question.Type, "-essay") {
@ -276,8 +283,8 @@ func SubmitPracticeAnswer(c *gin.Context) {
} }
log.Printf("论述题AI评分失败 (第 %d 次尝试): %v", attempt, err) log.Printf("论述题AI评分失败 (第 %d 次尝试): %v", attempt, err)
if attempt < maxRetries { if attempt < maxRetries {
// 等待一小段时间后重试(指数退避) // 减少等待时间使用固定延迟500ms而不是指数退避
time.Sleep(time.Second * time.Duration(attempt)) time.Sleep(500 * time.Millisecond)
} }
} }
} else { } else {
@ -301,8 +308,8 @@ func SubmitPracticeAnswer(c *gin.Context) {
} }
log.Printf("简答题AI评分失败 (第 %d 次尝试): %v", attempt, err) log.Printf("简答题AI评分失败 (第 %d 次尝试): %v", attempt, err)
if attempt < maxRetries { if attempt < maxRetries {
// 等待一小段时间后重试(指数退避) // 减少等待时间使用固定延迟500ms而不是指数退避
time.Sleep(time.Second * time.Duration(attempt)) time.Sleep(500 * time.Millisecond)
} }
} }
} }
@ -980,8 +987,16 @@ func ExplainQuestion(c *gin.Context) {
c.Header("X-Accel-Buffering", "no") c.Header("X-Accel-Buffering", "no")
// 调用AI服务生成流式解析 // 调用AI服务生成流式解析
aiService := services.NewAIGradingService() aiService, err := services.NewAIGradingService()
err := aiService.ExplainQuestionStream(c.Writer, question.Question, standardAnswerStr, question.Type) if err != nil {
log.Printf("AI服务初始化失败: %v", err)
// SSE格式的错误消息
fmt.Fprintf(c.Writer, "data: [ERROR] AI服务暂时不可用请稍后再试\n\n")
c.Writer.(http.Flusher).Flush()
return
}
err = aiService.ExplainQuestionStream(c.Writer, question.Question, standardAnswerStr, question.Type)
if err != nil { if err != nil {
log.Printf("AI流式解析失败: %v", err) log.Printf("AI流式解析失败: %v", err)
// SSE格式的错误消息 // SSE格式的错误消息

View File

@ -1,37 +1,27 @@
package services package services
import ( import (
"ankao/pkg/config"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"github.com/sashabaranov/go-openai"
) )
// AIGradingService AI评分服务 // AIGradingService AI评分服务接口(使用百度云AppBuilder)
type AIGradingService struct { type AIGradingService struct {
client *openai.Client baiduService *BaiduAIGradingService
config *config.AIConfig
} }
// NewAIGradingService 创建AI评分服务实例 // NewAIGradingService 创建AI评分服务实例
func NewAIGradingService() *AIGradingService { func NewAIGradingService() (*AIGradingService, error) {
cfg := config.GetAIConfig() baiduService, err := NewBaiduAIGradingService()
if err != nil {
// 创建OpenAI客户端配置 return nil, fmt.Errorf("创建百度云AI服务失败: %w", err)
clientConfig := openai.DefaultConfig(cfg.APIKey) }
clientConfig.BaseURL = cfg.BaseURL + "/v1" // 标准OpenAI API格式需要/v1后缀
client := openai.NewClientWithConfig(clientConfig)
return &AIGradingService{ return &AIGradingService{
client: client, baiduService: baiduService,
config: cfg, }, nil
}
} }
// AIGradingResult AI评分结果 // AIGradingResult AI评分结果
@ -40,104 +30,18 @@ type AIGradingResult struct {
IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确) IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确)
Feedback string `json:"feedback"` // 评语 Feedback string `json:"feedback"` // 评语
Suggestion string `json:"suggestion"` // 改进建议 Suggestion string `json:"suggestion"` // 改进建议
ReferenceAnswer string `json:"reference_answer"` // 参考答案(论述题) ReferenceAnswer string `json:"reference_answer"` // 参考答案(论述题)
ScoringRationale string `json:"scoring_rationale"` // 评分依据 ScoringRationale string `json:"scoring_rationale"` // 评分依据
} }
// GradeEssay 对论述题进行AI评分(不需要标准答案) // GradeEssay 对论述题进行AI评分(不需要标准答案)
// question: 题目内容 // question: 题目内容
// userAnswer: 用户答案 // userAnswer: 用户答案
func (s *AIGradingService) GradeEssay(question, userAnswer string) (*AIGradingResult, error) { func (s *AIGradingService) GradeEssay(question, userAnswer string) (*AIGradingResult, error) {
// 构建评分提示词(论述题评分模式) if s.baiduService == nil {
prompt := fmt.Sprintf(`你是一位专业的保密领域阅卷老师请对以下论述题进行评分 return nil, fmt.Errorf("百度云AI服务未初始化")
题目%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": "建议补充《保密法》相关条文,加强论点之间的逻辑联系"
} }
return s.baiduService.GradeEssay(question, userAnswer)
注意
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评分 // GradeShortAnswer 对简答题进行AI评分
@ -145,89 +49,38 @@ func (s *AIGradingService) GradeEssay(question, userAnswer string) (*AIGradingRe
// standardAnswer: 标准答案 // standardAnswer: 标准答案
// userAnswer: 用户答案 // userAnswer: 用户答案
func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) { func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) {
// 构建评分提示词(严格评分模式) if s.baiduService == nil {
prompt := fmt.Sprintf(`你是一位专业的保密领域阅卷老师请严格按照标准答案对以下简答题进行评分 return nil, fmt.Errorf("百度云AI服务未初始化")
}
题目%s return s.baiduService.GradeShortAnswer(question, standardAnswer, userAnswer)
标准答案%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的描述"
} }
注意 // AIExplanationResult AI解析结果
1. 只返回JSON格式的结果不要有其他内容 type AIExplanationResult struct {
2. 必须严格对照标准答案评分不要过于宽松 Explanation string `json:"explanation"` // 题目解析
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 { // ExplainQuestionStream 生成题目解析(流式输出)
return nil, fmt.Errorf("AI未返回评分结果") // writer: HTTP响应写入器
// question: 题目内容
// standardAnswer: 标准答案
// questionType: 题目类型
func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, question, standardAnswer, questionType string) error {
if s.baiduService == nil {
return fmt.Errorf("百度云AI服务未初始化")
}
return s.baiduService.ExplainQuestionStream(writer, question, standardAnswer, questionType)
} }
// 解析AI返回的JSON结果 // ExplainQuestion 生成题目解析
content := resp.Choices[0].Message.Content // question: 题目内容
// standardAnswer: 标准答案
var result AIGradingResult // questionType: 题目类型
if err := parseAIResponse(content, &result); err != nil { func (s *AIGradingService) ExplainQuestion(question, standardAnswer, questionType string) (*AIExplanationResult, error) {
return nil, fmt.Errorf("解析AI响应失败: %w", err) if s.baiduService == nil {
return nil, fmt.Errorf("百度云AI服务未初始化")
} }
return s.baiduService.ExplainQuestion(question, standardAnswer, questionType)
return &result, nil
} }
// parseAIResponse 解析AI返回的JSON响应 // parseAIResponse 解析AI返回的JSON响应
@ -243,223 +96,6 @@ func parseAIResponse(content string, result interface{}) error {
return nil 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. **不要胡编乱造**如果某些信息不确定或题目没有提供请如实说明不要编造法规条文
解析内容要求
- **知识点**说明题目考查的核心知识点指出涉及哪些保密法规
- **法规依据**明确引用相关法律法规的具体条文保密法第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代码块标记 // removeMarkdownCodeBlock 移除markdown代码块标记
func removeMarkdownCodeBlock(s string) string { func removeMarkdownCodeBlock(s string) string {
// 去除可能的```json和```标记 // 去除可能的```json和```标记

18
main.go
View File

@ -5,6 +5,8 @@ import (
"ankao/internal/handlers" "ankao/internal/handlers"
"ankao/internal/middleware" "ankao/internal/middleware"
"log" "log"
"net/http"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -98,9 +100,21 @@ func main() {
// 当没有匹配到任何 API 路由时,尝试提供静态文件 // 当没有匹配到任何 API 路由时,尝试提供静态文件
r.NoRoute(gin.WrapH(handlers.StaticFileHandler("./web"))) r.NoRoute(gin.WrapH(handlers.StaticFileHandler("./web")))
// 启动服务器 // 创建自定义HTTP服务器设置超时时间
port := ":8080" port := ":8080"
if err := r.Run(port); err != nil { server := &http.Server{
Addr: port,
Handler: r,
ReadTimeout: 5 * time.Minute, // 读取超时5分钟
WriteTimeout: 5 * time.Minute, // 写入超时5分钟
IdleTimeout: 10 * time.Minute, // 空闲连接超时10分钟
MaxHeaderBytes: 1 << 20, // 最大请求头1MB
}
log.Printf("服务器启动在端口 %s超时配置读/写 5分钟", port)
// 启动服务器
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic("服务器启动失败: " + err.Error()) panic("服务器启动失败: " + err.Error())
} }
} }

View File

@ -21,6 +21,7 @@ type AIConfig struct {
BaseURL string BaseURL string
APIKey string APIKey string
Model string Model string
BaiduAppID string // 百度云AppBuilder应用ID
} }
// GetDatabaseConfig 获取数据库配置 // GetDatabaseConfig 获取数据库配置
@ -79,12 +80,14 @@ func (c *DatabaseConfig) GetDSN() string {
// 优先使用环境变量,如果没有设置则使用默认值 // 优先使用环境变量,如果没有设置则使用默认值
func GetAIConfig() *AIConfig { func GetAIConfig() *AIConfig {
baseURL := getEnv("AI_BASE_URL", "http://172.20.0.117") baseURL := getEnv("AI_BASE_URL", "http://172.20.0.117")
apiKey := getEnv("AI_API_KEY", "sk-OKBmOpJx855juSOPU14cWG6Iz87tZQuv3Xg9PiaJYXdHoKcN") apiKey := getEnv("AI_API_KEY", "bce-v3/ALTAK-TgZ1YSBmbwNXo3BIuzNZ2/768b777896453e820a2c46f38614c8e9bf43f845")
model := getEnv("AI_MODEL", "deepseek-v3") model := getEnv("AI_MODEL", "deepseek-v3")
baiduAppID := getEnv("BAIDU_APP_ID", "7b336aaf-f448-46d6-9e5f-bb9e38a1167c")
return &AIConfig{ return &AIConfig{
BaseURL: baseURL, BaseURL: baseURL,
APIKey: apiKey, APIKey: apiKey,
Model: model, Model: model,
BaiduAppID: baiduAppID,
} }
} }

View File

@ -1,18 +0,0 @@
-- 修复 practice_progress 表的唯一索引
-- 问题:之前的唯一索引只在 user_id 上,导致同一用户只能有一条进度记录
-- 修复:改为 (user_id, type) 联合唯一索引,允许同一用户有多种题型的进度
-- 1. 删除旧的唯一索引(如果存在)
DROP INDEX IF EXISTS idx_user_question;
-- 2. 创建新的联合唯一索引
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_type ON practice_progress(user_id, type);
-- 3. 验证索引
SELECT
indexname,
indexdef
FROM
pg_indexes
WHERE
tablename = 'practice_progress';

View File

@ -3,7 +3,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
// 创建 axios 实例 // 创建 axios 实例
const instance: AxiosInstance = axios.create({ const instance: AxiosInstance = axios.create({
baseURL: '/api', // 通过 Vite 代理转发到 Go 后端 baseURL: '/api', // 通过 Vite 代理转发到 Go 后端
timeout: 10000, timeout: 300000, // 5分钟超时300秒适应AI评分长时间处理
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },