Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.Service/AiChatService.cs

174 lines
7.6 KiB
C#
Raw Normal View History

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.Runtime.CompilerServices;
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聊天逐块返回AI生成的内容
/// </summary>
public async IAsyncEnumerable<string> ChatAsync(
AiChatInput input,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
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,
stream = true
};
var jsonContent = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
// 创建 HttpClient 并发起请求(使用 ResponseHeadersRead 提前获取响应头以流式读取)
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");
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
logger.LogError("AI聊天服务调用失败状态码{StatusCode},响应:{Response}", response.StatusCode, errorContent);
throw new BusinessException($"AI服务调用失败{response.StatusCode}", 500);
}
// 流式读取响应体
using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var reader = new StreamReader(responseStream);
while (!reader.EndOfStream)
{
cancellationToken.ThrowIfCancellationRequested();
var line = await reader.ReadLineAsync(cancellationToken);
if (line == null) break;
// 只处理 data: 开头的行
if (!line.StartsWith("data: ")) continue;
var data = line.Substring(6);
if (data == "[DONE]") break;
// 解析 SSE chunk提取 delta.content
using var chunkDoc = JsonDocument.Parse(data);
var choicesEl = chunkDoc.RootElement.GetProperty("choices");
if (choicesEl.GetArrayLength() == 0) continue;
var delta = choicesEl[0].GetProperty("delta");
if (delta.TryGetProperty("content", out var contentEl))
{
var content = contentEl.GetString();
if (!string.IsNullOrEmpty(content))
{
yield return content;
}
}
}
logger.LogInformation("AI聊天服务流式调用完成");
}
}