fix: 标准答案图先发+提示词优化+JSON强化+重试3次
This commit is contained in:
+66
-48
@@ -354,7 +354,7 @@ async function callAiApi(imageBase64: string, rubric: string, config: { apiKey:
|
||||
deductions: string[]
|
||||
comment: string
|
||||
}> {
|
||||
const systemRole = `你是高校阅卷专家,严格按照评分标准逐项评分。只输出JSON,不包含任何其他文字。`
|
||||
const systemRole = `你是高校阅卷专家,严格按照评分标准逐项评分。你必须只输出JSON,严禁包含\`\`\`markdown代码块、\`\`\`json标记、注释、说明文字或任何其他非JSON内容。输出必须以{开头,以}结尾,且必须是合法的JSON。`
|
||||
|
||||
const hasRefImage = !!extras?.referenceAnswerImage
|
||||
|
||||
@@ -368,10 +368,10 @@ ${extras?.referenceAnswer || '无'}
|
||||
【满分】
|
||||
${extras?.maxScore ?? 100}
|
||||
|
||||
图片1:学生答案
|
||||
图片2:标准答案
|
||||
图片1:标准答案
|
||||
图片2:学生答案
|
||||
|
||||
请按照标准答案(图片2)给学生答案(图片1)评分。
|
||||
请根据评分标准和标准答案(图片1)对学生答案(图片2)评分。
|
||||
|
||||
---
|
||||
|
||||
@@ -382,13 +382,14 @@ ${extras?.maxScore ?? 100}
|
||||
4. 部分正确时按评分标准酌情给分
|
||||
|
||||
【输出要求】
|
||||
只输出以下JSON,不要包含markdown代码块、\`\`\`标记、注释或任何其他文字:
|
||||
你必须严格输出以下格式的纯JSON,严禁包含\`\`\`、\`\`\`json、markdown代码块、注释或任何说明文字。整个回复必须是合法的JSON对象,以{开始,以}结束:
|
||||
{
|
||||
"score": <总分>,
|
||||
"confidence": <置信度0~1>,
|
||||
"deductions": ["扣分项说明1", "扣分项说明2"],
|
||||
"comment": "总评语及逐项得分说明"
|
||||
}`
|
||||
}
|
||||
再次强调:只输出JSON,禁止任何其他内容。`
|
||||
: `${extras?.questionTitle ? `【题目】\n${extras.questionTitle}\n` : ''}【评分标准】
|
||||
${rubric || '无'}
|
||||
|
||||
@@ -398,8 +399,8 @@ ${extras?.referenceAnswer || '无'}
|
||||
【满分】
|
||||
${extras?.maxScore ?? 100}
|
||||
|
||||
【学生答案】
|
||||
请看图片中的内容。
|
||||
图片:学生答案
|
||||
请根据评分标准对学生答案评分。
|
||||
|
||||
---
|
||||
|
||||
@@ -410,65 +411,82 @@ ${extras?.maxScore ?? 100}
|
||||
4. 部分正确时按评分标准酌情给分
|
||||
|
||||
【输出要求】
|
||||
只输出以下JSON,不要包含markdown代码块、\`\`\`标记、注释或任何其他文字:
|
||||
你必须严格输出以下格式的纯JSON,严禁包含\`\`\`、\`\`\`json、markdown代码块、注释或任何说明文字。整个回复必须是合法的JSON对象,以{开始,以}结束:
|
||||
{
|
||||
"score": <总分>,
|
||||
"confidence": <置信度0~1>,
|
||||
"deductions": ["扣分项说明1", "扣分项说明2"],
|
||||
"comment": "总评语及逐项得分说明"
|
||||
}`
|
||||
}
|
||||
再次强调:只输出JSON,禁止任何其他内容。`
|
||||
|
||||
const axios = require('axios')
|
||||
|
||||
const userContent: any[] = hasRefImage
|
||||
? [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
||||
{ type: 'image_url', image_url: { url: extras!.referenceAnswerImage } }
|
||||
{ type: 'image_url', image_url: { url: extras!.referenceAnswerImage } },
|
||||
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } }
|
||||
]
|
||||
: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } }
|
||||
]
|
||||
|
||||
const response = await axios.post(`${config.baseUrl}/chat/completions`, {
|
||||
model: config.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemRole
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: userContent
|
||||
}
|
||||
],
|
||||
max_tokens: 4096,
|
||||
temperature: 0.3
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.apiKey}`
|
||||
},
|
||||
timeout: ((getStore().get('timeout') as number) ?? 120) * 1000
|
||||
})
|
||||
let lastError: Error | null = null
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
const response = await axios.post(`${config.baseUrl}/chat/completions`, {
|
||||
model: config.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemRole
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: userContent
|
||||
}
|
||||
],
|
||||
max_tokens: 4096,
|
||||
temperature: 0.3
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.apiKey}`
|
||||
},
|
||||
timeout: ((getStore().get('timeout') as number) ?? 120) * 1000
|
||||
})
|
||||
|
||||
const content = response.data?.choices?.[0]?.message?.content
|
||||
if (!content) {
|
||||
log.error('AI API 返回为空或格式异常:', JSON.stringify(response.data).slice(0, 1000))
|
||||
throw new Error(`API 返回为空,请检查模型名是否正确 (当前: ${config.model})`)
|
||||
}
|
||||
const parsed = tryParseJson(content)
|
||||
if (!parsed) {
|
||||
log.error('AI 返回 JSON 解析失败:', content.slice(0, 500))
|
||||
throw new Error(`AI 返回格式异常,无法解析为 JSON。原始返回:\n${content}`)
|
||||
}
|
||||
return {
|
||||
score: parsed.score,
|
||||
confidence: parsed.confidence ?? 0.5,
|
||||
deductions: parsed.deductions ?? [],
|
||||
comment: parsed.comment ?? parsed.commit ?? ''
|
||||
const content = response.data?.choices?.[0]?.message?.content
|
||||
if (!content) {
|
||||
lastError = new Error(`API 返回为空 (尝试 ${attempt}/3)`)
|
||||
log.warn(`AI API empty response, attempt ${attempt}/3`)
|
||||
if (attempt < 3) continue
|
||||
throw lastError
|
||||
}
|
||||
const parsed = tryParseJson(content)
|
||||
if (!parsed) {
|
||||
lastError = new Error(`AI 返回 JSON 解析失败 (尝试 ${attempt}/3)`)
|
||||
log.warn(`AI JSON parse failed, attempt ${attempt}/3:`, content.slice(0, 200))
|
||||
if (attempt < 3) continue
|
||||
throw lastError
|
||||
}
|
||||
return {
|
||||
score: parsed.score,
|
||||
confidence: parsed.confidence ?? 0.5,
|
||||
deductions: parsed.deductions ?? [],
|
||||
comment: parsed.comment ?? parsed.commit ?? ''
|
||||
}
|
||||
} catch (err: any) {
|
||||
lastError = err
|
||||
if (attempt < 3) {
|
||||
log.info(`AI API attempt ${attempt}/3 failed, retrying...`)
|
||||
await new Promise(r => setTimeout(r, 2000))
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError || new Error('AI API 调用失败')
|
||||
}
|
||||
|
||||
function tryParseJson(text: string): any {
|
||||
|
||||
Reference in New Issue
Block a user