refactor(JournalTaskConsumer): 优化AI评分prompt与图片加载逻辑

1. 新增无效作答固定返回文案常量
2. 调整prompt提示词,补充无效作答评分规则与返回要求
3. 移除冗余的参考答案无结果提示与题目基础信息打印
4. 封装图片流获取方法,兼容OSS和HTTP链接
5. 新增结果标准化处理,统一无效作答返回格式
This commit is contained in:
glz
2026-07-08 09:13:48 +08:00
parent f7f505d109
commit a64d1b459a

View File

@ -25,6 +25,7 @@ public class JournalTaskReceiveConsumer(
private const long DefaultMaxImageBytes = 10 * 1024 * 1024; private const long DefaultMaxImageBytes = 10 * 1024 * 1024;
private const float DefaultCompletionThreshold = 80; private const float DefaultCompletionThreshold = 80;
private const float DefaultCommunityScoreThreshold = 90; private const float DefaultCommunityScoreThreshold = 90;
private const string InvalidAnswerResult = "作答内容不符合要求";
private const string AiProcessingMessage = "AI批阅中请稍后"; private const string AiProcessingMessage = "AI批阅中请稍后";
private static readonly object AiSemaphoreLock = new(); private static readonly object AiSemaphoreLock = new();
private static SemaphoreSlim? aiSemaphore; private static SemaphoreSlim? aiSemaphore;
@ -359,7 +360,7 @@ public class JournalTaskReceiveConsumer(
prompt.AppendLine(scoreUnit.Questions.Count > 1 prompt.AppendLine(scoreUnit.Questions.Count > 1
? "这是同一跨页题目的多页作答,请综合全部学生作答图片评分。" ? "这是同一跨页题目的多页作答,请综合全部学生作答图片评分。"
: "这是单页题目作答,请根据学生作答图片评分。"); : "这是单页题目作答,请根据学生作答图片评分。");
prompt.AppendLine("参考答案不是必有;若无参考答案,以题目 Prompt 和学生答案为准评分。"); prompt.AppendLine("以题目 Prompt 和学生答案为准评分,若有参考答案请结合参考答案。");
if (referenceAnswerTexts.Count > 0) if (referenceAnswerTexts.Count > 0)
{ {
prompt.AppendLine("参考答案文本:"); prompt.AppendLine("参考答案文本:");
@ -368,17 +369,10 @@ public class JournalTaskReceiveConsumer(
prompt.AppendLine($"{i + 1}. {referenceAnswerTexts[i]}"); prompt.AppendLine($"{i + 1}. {referenceAnswerTexts[i]}");
} }
} }
else
{
prompt.AppendLine("参考答案文本:无");
}
prompt.AppendLine(); prompt.AppendLine();
prompt.AppendLine("题目信息:"); prompt.AppendLine("题目信息:");
foreach (var context in scoreUnit.Questions) foreach (var context in scoreUnit.Questions)
{ {
prompt.AppendLine($"- TaskId{context.Task.Id}");
prompt.AppendLine($" 页码:{context.Page?.PageNum ?? 0}");
prompt.AppendLine($" 题号:{context.Task.No}");
prompt.AppendLine($" 题目内容:{context.Task.Task}"); prompt.AppendLine($" 题目内容:{context.Task.Task}");
prompt.AppendLine($" 评分Prompt{context.Task.Prompt}"); prompt.AppendLine($" 评分Prompt{context.Task.Prompt}");
prompt.AppendLine($" 成长值上限:{context.Task.GrowthPoint}"); prompt.AppendLine($" 成长值上限:{context.Task.GrowthPoint}");
@ -394,6 +388,8 @@ public class JournalTaskReceiveConsumer(
prompt.AppendLine("- 看不清、缺页、无法识别或答案明显不完整时,降低 Completion不要猜测高分。"); prompt.AppendLine("- 看不清、缺页、无法识别或答案明显不完整时,降低 Completion不要猜测高分。");
prompt.AppendLine("- Completion 表示作答完整度,范围 0-100。"); prompt.AppendLine("- Completion 表示作答完整度,范围 0-100。");
prompt.AppendLine("- Result 返回 50 字内中文评语。"); prompt.AppendLine("- Result 返回 50 字内中文评语。");
prompt.AppendLine("若作答内容与题目要求不符、答非所问、空白、仅抄题或无法形成有效答案Score/GrowthPoint/Points/Comprehension/Judgment/Expression/Persuasiveness 均返回 0。");
prompt.AppendLine("若作答内容不符合要求Result 必须且只能返回:作答内容不符合要求。不要补充原因、建议或其他文字。");
prompt.AppendLine(); prompt.AppendLine();
prompt.AppendLine("只返回如下 JSON 字段:"); prompt.AppendLine("只返回如下 JSON 字段:");
prompt.AppendLine("{"); prompt.AppendLine("{");
@ -424,7 +420,7 @@ public class JournalTaskReceiveConsumer(
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
await using var imageStream = ossService.GetObjectStream(imageUrl); await using var imageStream = await GetImageStreamAsync(imageUrl, cancellationToken);
if (imageStream == null) if (imageStream == null)
{ {
throw new InvalidOperationException($"答案图片读取失败TaskId: {question.Id}, Url: {imageUrl}"); throw new InvalidOperationException($"答案图片读取失败TaskId: {question.Id}, Url: {imageUrl}");
@ -474,7 +470,7 @@ public class JournalTaskReceiveConsumer(
try try
{ {
await using var imageStream = ossService.GetObjectStream(imageUrl); await using var imageStream = await GetImageStreamAsync(imageUrl, cancellationToken);
if (imageStream == null) if (imageStream == null)
{ {
throw new InvalidOperationException($"参考答案图片读取失败TaskId: {taskId}, Url: {imageUrl}"); throw new InvalidOperationException($"参考答案图片读取失败TaskId: {taskId}, Url: {imageUrl}");
@ -590,6 +586,18 @@ public class JournalTaskReceiveConsumer(
throw new InvalidOperationException($"AI评分调用失败TaskId: {taskId}", lastException); throw new InvalidOperationException($"AI评分调用失败TaskId: {taskId}", lastException);
} }
private async Task<Stream?> GetImageStreamAsync(string imageUrl, CancellationToken cancellationToken)
{
if (Uri.TryCreate(imageUrl, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
{
var httpClient = httpClientFactory.CreateClient();
return await httpClient.GetStreamAsync(uri, cancellationToken);
}
return ossService.GetObjectStream(imageUrl);
}
private static bool ShouldRetry(System.Net.HttpStatusCode statusCode) private static bool ShouldRetry(System.Net.HttpStatusCode statusCode)
{ {
var status = (int)statusCode; var status = (int)statusCode;
@ -732,10 +740,22 @@ public class JournalTaskReceiveConsumer(
Expression = Clamp(scoreResult.Expression, 0, task.Expression), Expression = Clamp(scoreResult.Expression, 0, task.Expression),
Persuasiveness = Clamp(scoreResult.Persuasiveness, 0, task.Persuasiveness), Persuasiveness = Clamp(scoreResult.Persuasiveness, 0, task.Persuasiveness),
Completion = Clamp(scoreResult.Completion, 0, 100), Completion = Clamp(scoreResult.Completion, 0, 100),
Result = TrimResult(scoreResult.Result) Result = NormalizeResult(scoreResult.Result)
}; };
} }
private static string NormalizeResult(string? result)
{
var trimmedResult = TrimResult(result);
if (trimmedResult.Contains("不符合要求", StringComparison.Ordinal) ||
trimmedResult.Contains("不符", StringComparison.Ordinal))
{
return InvalidAnswerResult;
}
return trimmedResult;
}
private static float Clamp(float value, float min, float max) private static float Clamp(float value, float min, float max)
{ {
if (float.IsNaN(value) || float.IsInfinity(value)) if (float.IsNaN(value) || float.IsInfinity(value))