feat: 新增AI聊天服务,重构任务提示词相关逻辑
1. 新增AI聊天服务接口、实现、控制器及相关DTO、配置类 2. 调整JournalPageTaskTypeEnum枚举值修正 3. 合并任务的基础Prompt和自定义Prompt为单个Prompt字段 4. 精简任务输出DTO冗余属性
This commit is contained in:
16
QYZH.InteractiveMagazine.IService/IAiChatService.cs
Normal file
16
QYZH.InteractiveMagazine.IService/IAiChatService.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天服务接口
|
||||
/// </summary>
|
||||
public interface IAiChatService
|
||||
{
|
||||
/// <summary>
|
||||
/// 发送消息进行AI对话
|
||||
/// </summary>
|
||||
/// <param name="input">聊天输入</param>
|
||||
/// <returns>AI返回的提示词内容</returns>
|
||||
Task<AiChatOutput> ChatAsync(AiChatInput input);
|
||||
}
|
||||
23
QYZH.InteractiveMagazine.Models/Dto/Ai/AiChatDto.cs
Normal file
23
QYZH.InteractiveMagazine.Models/Dto/Ai/AiChatDto.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天输入
|
||||
/// </summary>
|
||||
public class AiChatInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户需求文本
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天输出
|
||||
/// </summary>
|
||||
public class AiChatOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// AI返回的内容(Markdown格式提示词)
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
@ -19,11 +19,6 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
/// </summary>
|
||||
public long JournalPageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 学科Id
|
||||
/// </summary>
|
||||
public long SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题号
|
||||
/// </summary>
|
||||
@ -36,33 +31,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
|
||||
public string ShowType => Type.GetDescription();
|
||||
|
||||
private string _task;
|
||||
/// <summary>
|
||||
/// 问题
|
||||
/// </summary>
|
||||
public string Task
|
||||
{
|
||||
get { return _task.AddDomain(DomainHelper.OssDomain); }
|
||||
set { _task = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 答案列表
|
||||
/// </summary>
|
||||
public List<JournalTaskAnswerOutput> Answers { get; set; }
|
||||
|
||||
private string _imageUrl;
|
||||
/// <summary>
|
||||
/// 图片地址
|
||||
/// </summary>
|
||||
public string ImageUrl
|
||||
{
|
||||
get { return DomainHelper.OssFullUrl(_imageUrl); }
|
||||
set { _imageUrl = value; }
|
||||
}
|
||||
|
||||
public List<string> ImageUrls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 积分
|
||||
/// </summary>
|
||||
@ -98,55 +70,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
/// </summary>
|
||||
public int AnswerTime { get; set; }
|
||||
|
||||
private string _audioUrl;
|
||||
/// <summary>
|
||||
/// 音频地址
|
||||
/// Prompt配置
|
||||
/// </summary>
|
||||
public string AudioUrl
|
||||
{
|
||||
get { return DomainHelper.OssFullUrl(_audioUrl); }
|
||||
set { _audioUrl = value; }
|
||||
}
|
||||
|
||||
private string _videoUrl;
|
||||
/// <summary>
|
||||
/// 视频地址
|
||||
/// </summary>
|
||||
public string VideoUrl
|
||||
{
|
||||
get { return DomainHelper.OssFullUrl(_videoUrl); }
|
||||
set { _videoUrl = value; }
|
||||
}
|
||||
|
||||
private string _pointsUrl;
|
||||
/// <summary>
|
||||
/// 点位数据地址
|
||||
/// </summary>
|
||||
public string PointsUrl
|
||||
{
|
||||
get { return DomainHelper.OssFullUrl(_pointsUrl); }
|
||||
set { _pointsUrl = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 音频开始时间
|
||||
/// </summary>
|
||||
public long AudioStartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 音频结束时间
|
||||
/// </summary>
|
||||
public long AudioEndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 知识点
|
||||
/// </summary>
|
||||
public string KnowledgePointIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 年级
|
||||
/// </summary>
|
||||
public int Grade { get; set; }
|
||||
public string Prompt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -67,18 +67,9 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
/// </summary>
|
||||
public int AnswerTime { get; set; }
|
||||
/// <summary>
|
||||
/// Desc:基础Prompt配置ID(为空则使用IsDefault=1的模板)
|
||||
/// Default:
|
||||
/// Nullable:True
|
||||
/// Prompt配置
|
||||
/// </summary>
|
||||
public string BasePrompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:个性化Prompt(追加到基础Prompt后面,可为空)
|
||||
/// Default:
|
||||
/// Nullable:True
|
||||
/// </summary>
|
||||
public string CustomPrompt { get; set; }
|
||||
public string Prompt { get; set; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -122,18 +122,11 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
||||
public bool NeedAiProcess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:基础Prompt配置ID(为空则使用IsDefault=1的模板)
|
||||
/// Desc:Prompt配置
|
||||
/// Default:
|
||||
/// Nullable:True
|
||||
/// </summary>
|
||||
public string BasePrompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:个性化Prompt(追加到基础Prompt后面,可为空)
|
||||
/// Default:
|
||||
/// Nullable:True
|
||||
/// </summary>
|
||||
public string CustomPrompt { get; set; }
|
||||
public string Prompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:作答参考时间
|
||||
|
||||
@ -16,5 +16,5 @@ public enum JournalPageTaskTypeEnum
|
||||
/// 问答题
|
||||
/// </summary>
|
||||
[Description("主观题")]
|
||||
TaskAnswer = 4
|
||||
TaskAnswer = 0
|
||||
}
|
||||
|
||||
37
QYZH.InteractiveMagazine.Models/Settings/AiChatSettings.cs
Normal file
37
QYZH.InteractiveMagazine.Models/Settings/AiChatSettings.cs
Normal file
@ -0,0 +1,37 @@
|
||||
namespace QYZH.InteractiveMagazine.Models.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天配置
|
||||
/// </summary>
|
||||
public class AiChatSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// API密钥
|
||||
/// </summary>
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// API基础地址(OpenAI兼容)
|
||||
/// </summary>
|
||||
public string? BaseUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型名称
|
||||
/// </summary>
|
||||
public string? Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求超时时间(秒)
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// 最大生成Token数
|
||||
/// </summary>
|
||||
public int MaxTokens { get; set; } = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// 采样温度(0-2,值越低输出越确定)
|
||||
/// </summary>
|
||||
public double Temperature { get; set; } = 0.5;
|
||||
}
|
||||
154
QYZH.InteractiveMagazine.Service/AiChatService.cs
Normal file
154
QYZH.InteractiveMagazine.Service/AiChatService.cs
Normal file
@ -0,0 +1,154 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Service;
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天服务实现(OpenAI API规范)
|
||||
/// </summary>
|
||||
public class AiChatService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IConfiguration configuration,
|
||||
ILogger<AiChatService> logger) : IAiChatService
|
||||
{
|
||||
/// <summary>
|
||||
/// 预设系统提示词
|
||||
/// </summary>
|
||||
private const string SystemPrompt = """
|
||||
# 提示词:AI 优化“题目评分/报告提示词”
|
||||
|
||||
你是一个专业的提示词优化专家。你的任务是**将用户输入的粗糙、口语化的“题目评分/报告提示词”重构为结构清晰、指令明确、便于 AI 直接执行的标准化提示词**。
|
||||
|
||||
用户(题目制作者)当前输入的内容可能包含:题目描述、作答要求、打分规则、评分维度、报告层级、示例等,但组织混乱、表述模糊。你需要**保留所有原始信息,不增删任何实质性规则**,仅优化其结构、逻辑层次和表达精度。
|
||||
|
||||
---
|
||||
|
||||
## 优化原则
|
||||
|
||||
1. **信息完整**:用户原文中的所有要点、示例、层级区分必须全部保留。
|
||||
2. **结构清晰**:将内容归入标准模块(如 `任务目标`、`输入格式`、`评分标准`、`报告生成要求`)。
|
||||
3. **指令明确**:将模糊表述(如“要写得好一点”)转化为可操作的判定条件(如“必须包含三层结构:解释原因 + 表达情绪 + 提出方案”)。
|
||||
4. **语言精练**:去除冗余修饰,保留关键限定词(如“仅”“必须”“至少”)。
|
||||
5. **格式统一**:最终输出为 Markdown 格式,使用标题(`#` `##` `###`)、列表(`-`)、加粗/斜体等提升可读性。
|
||||
|
||||
---
|
||||
|
||||
## 优化步骤
|
||||
|
||||
1. **解析原文**:通读用户输入,标注出:
|
||||
- 题目背景 / 情境
|
||||
- 学生作答要求
|
||||
- 评分维度(优秀/一般/薄弱)及其具体判定条件
|
||||
- 报告层级及其对应的评语模板或生成逻辑
|
||||
- 任何示例答案
|
||||
2. **重组结构**:按以下标准模板组织内容(可灵活调整章节名称):
|
||||
- `## 任务概述`(题目 + 要求)
|
||||
- `## 输入数据`(学生作答的内容)
|
||||
- `## 评分标准`(分档描述,含判定规则)
|
||||
- `## 报告生成规范`(层级名称、触发条件、评语示例/生成规则)
|
||||
- `## 注意事项`(如有特殊约束)
|
||||
3. **润色表达**:
|
||||
- 将“可能”“大概”等不确定词改为明确的“必须”“至少”;
|
||||
- 将并列条件用编号列表(1. 2. 3.)拆分;
|
||||
- 将示例单独以代码块或引用块呈现,避免与规则混淆。
|
||||
4. **保持原意**:不得添加用户未提及的新评分维度或报告层级,也不得删除任何已有规则。
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
|
||||
- 只输出优化后的完整提示词(即 AI 可直接使用的评分/报告 prompt),不要输出分析过程。
|
||||
- 使用 Markdown 语法,标题层级从 `#` 开始。
|
||||
- 所有原有示例必须原样保留,并标明“示例”或放在引用块中。
|
||||
|
||||
---
|
||||
|
||||
现在,请对**当前用户输入的原始提示词**执行上述优化过程,仅输出优化后的 Markdown 格式提示词。
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天,根据用户需求生成提示词
|
||||
/// </summary>
|
||||
public async Task<AiChatOutput> ChatAsync(AiChatInput input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.Message))
|
||||
{
|
||||
throw new BusinessException("消息内容不能为空", 400);
|
||||
}
|
||||
|
||||
var apiKey = configuration["AiChat:ApiKey"];
|
||||
var baseUrl = configuration["AiChat:BaseUrl"];
|
||||
var model = configuration["AiChat:Model"];
|
||||
var timeoutSeconds = configuration.GetValue<int>("AiChat:TimeoutSeconds");
|
||||
var maxTokens = configuration.GetValue<int>("AiChat:MaxTokens");
|
||||
var temperature = configuration.GetValue<double>("AiChat:Temperature");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
throw new BusinessException("AI聊天服务配置不完整,请检查 AiChat 配置节", 500);
|
||||
}
|
||||
|
||||
logger.LogInformation("开始调用AI聊天服务,消息长度:{Length}", input.Message.Length);
|
||||
|
||||
// 构建 OpenAI Chat Completions 请求体
|
||||
var requestBody = new
|
||||
{
|
||||
model = model,
|
||||
messages = new[]
|
||||
{
|
||||
new { role = "system", content = SystemPrompt },
|
||||
new { role = "user", content = input.Message }
|
||||
},
|
||||
max_tokens = maxTokens,
|
||||
temperature = temperature
|
||||
};
|
||||
|
||||
var jsonContent = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
});
|
||||
|
||||
// 创建 HttpClient 并发起请求
|
||||
var client = httpClientFactory.CreateClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(timeoutSeconds > 0 ? timeoutSeconds : 300);
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl.TrimEnd('/')}/chat/completions");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
logger.LogError("AI聊天服务调用失败,状态码:{StatusCode},响应:{Response}", response.StatusCode, responseContent);
|
||||
throw new BusinessException($"AI服务调用失败:{response.StatusCode}", 500);
|
||||
}
|
||||
|
||||
// 解析 OpenAI 响应
|
||||
using var doc = JsonDocument.Parse(responseContent);
|
||||
var choices = doc.RootElement.GetProperty("choices");
|
||||
if (choices.GetArrayLength() == 0)
|
||||
{
|
||||
throw new BusinessException("AI未返回有效内容", 500);
|
||||
}
|
||||
|
||||
var content = choices[0]
|
||||
.GetProperty("message")
|
||||
.GetProperty("content")
|
||||
.GetString();
|
||||
|
||||
logger.LogInformation("AI聊天服务调用成功,返回内容长度:{Length}", content?.Length ?? 0);
|
||||
|
||||
return new AiChatOutput
|
||||
{
|
||||
Content = content ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
using Mapster;
|
||||
using Mapster;
|
||||
using QYZH.InteractiveMagazine.Common.Extensions;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.OSS;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
@ -66,8 +66,7 @@ public class JournalPageTaskService(BaseRepository<Journal> journalRepository, O
|
||||
task.Expression = input.Expression;
|
||||
task.Persuasiveness = input.Persuasiveness;
|
||||
task.AnswerTime = input.AnswerTime;
|
||||
task.BasePrompt = input.BasePrompt;
|
||||
task.CustomPrompt = input.CustomPrompt;
|
||||
task.Prompt = input.Prompt;
|
||||
|
||||
|
||||
var no = input.No.Split('-').Select(int.Parse).ToArray();
|
||||
@ -97,7 +96,6 @@ public class JournalPageTaskService(BaseRepository<Journal> journalRepository, O
|
||||
JournalPageId = task.JournalPageId,
|
||||
No = task.No,
|
||||
Type = task.Type,
|
||||
Task = task.Task,
|
||||
Points = task.Points,
|
||||
GrowthPoint = task.GrowthPoint ?? 0,
|
||||
Comprehension = task.Comprehension,
|
||||
@ -105,6 +103,7 @@ public class JournalPageTaskService(BaseRepository<Journal> journalRepository, O
|
||||
Expression = task.Expression,
|
||||
Persuasiveness = task.Persuasiveness,
|
||||
AnswerTime = task.AnswerTime,
|
||||
Prompt = task.Prompt
|
||||
};
|
||||
|
||||
// 查询答案列表
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.WebApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天控制器
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))]
|
||||
public class AiChatController : BaseController
|
||||
{
|
||||
private readonly IAiChatService _aiChatService;
|
||||
private readonly ILogger<AiChatController> _logger;
|
||||
|
||||
public AiChatController(IAiChatService aiChatService, ILogger<AiChatController> logger)
|
||||
{
|
||||
_aiChatService = aiChatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天,根据需求生成提示词
|
||||
/// </summary>
|
||||
/// <param name="input">用户需求消息</param>
|
||||
/// <returns>AI生成的提示词(Markdown格式)</returns>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<AiChatOutput>> ChatAsync([FromBody] AiChatInput input)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _aiChatService.ChatAsync(input);
|
||||
return Success(result, "AI聊天成功");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "AI聊天业务异常: {Message}", ex.Message);
|
||||
return BaseResponse<AiChatOutput>.Fail(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "AI聊天系统异常,参数:{Input}", input);
|
||||
return BaseResponse<AiChatOutput>.Fail("AI聊天失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,6 +47,14 @@
|
||||
]
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AiChat": {
|
||||
"ApiKey": "Ollama",
|
||||
"BaseUrl": "http://172.16.10.130:11434/v1/",
|
||||
"Model": "qwen2.5vl:7b",
|
||||
"TimeoutSeconds": 300,
|
||||
"MaxTokens": 2000,
|
||||
"Temperature": 0.5
|
||||
},
|
||||
"AliyunOSSConfigs": {
|
||||
"AccessKeyID": "LTAI5tEBXGewpHSLiSxyx6Bf",
|
||||
"AccessKeySecret": "w29b8wkw6XQVL8GWXgp3ZesgYeDKvf",
|
||||
|
||||
Reference in New Issue
Block a user