- VisionAsync 新增 Temperature=0.7, TopP=0.8 - system prompt 用专业营养识别指令 - userText 用简短"请看图识别食物"配合图片 - 修复重复 prompt 导致 VLM 误读文本的 bug
234 lines
7.2 KiB
C#
234 lines
7.2 KiB
C#
using System.Net.Http.Headers;
|
||
|
||
namespace Health.Infrastructure.AI;
|
||
|
||
/// <summary>
|
||
/// OpenAI 兼容协议 HTTP 客户端,统一调用 DeepSeek / 千问 VL
|
||
/// </summary>
|
||
public sealed class OpenAiCompatibleClient(string baseUrl, string apiKey, string model)
|
||
{
|
||
private readonly HttpClient _http = CreateHttpClient(baseUrl, apiKey);
|
||
private readonly string _model = model;
|
||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||
{
|
||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||
PropertyNameCaseInsensitive = true
|
||
};
|
||
|
||
private static HttpClient CreateHttpClient(string baseUrl, string apiKey)
|
||
{
|
||
var client = new HttpClient
|
||
{
|
||
BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"),
|
||
Timeout = TimeSpan.FromSeconds(60)
|
||
};
|
||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||
return client;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 流式 Chat Completions(SSE)
|
||
/// </summary>
|
||
public async IAsyncEnumerable<string> ChatStreamAsync(
|
||
List<ChatMessage> messages,
|
||
List<ToolDefinition>? tools = null,
|
||
int maxTokens = 2048,
|
||
float temperature = 0.7f)
|
||
{
|
||
var request = new ChatCompletionRequest
|
||
{
|
||
Model = _model,
|
||
Messages = messages,
|
||
Stream = true,
|
||
MaxTokens = maxTokens,
|
||
Temperature = temperature,
|
||
Tools = tools,
|
||
};
|
||
|
||
if (tools?.Count > 0)
|
||
request.ToolChoice = "auto";
|
||
|
||
var json = JsonSerializer.Serialize(request, _jsonOptions);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
|
||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "chat/completions")
|
||
{
|
||
Content = content
|
||
};
|
||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||
|
||
var response = await _http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
|
||
response.EnsureSuccessStatusCode();
|
||
|
||
using var stream = await response.Content.ReadAsStreamAsync();
|
||
using var reader = new StreamReader(stream);
|
||
|
||
string? line;
|
||
while ((line = await reader.ReadLineAsync()) != null)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||
if (!line.StartsWith("data: ")) continue;
|
||
|
||
var data = line["data: ".Length..];
|
||
if (data == "[DONE]") break;
|
||
|
||
yield return data;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 非流式 Chat Completions
|
||
/// </summary>
|
||
public async Task<ChatCompletionResponse> ChatAsync(
|
||
List<ChatMessage> messages,
|
||
List<ToolDefinition>? tools = null,
|
||
int maxTokens = 2048,
|
||
float temperature = 0.7f)
|
||
{
|
||
var request = new ChatCompletionRequest
|
||
{
|
||
Model = _model,
|
||
Messages = messages,
|
||
Stream = false,
|
||
MaxTokens = maxTokens,
|
||
Temperature = temperature,
|
||
Tools = tools,
|
||
};
|
||
|
||
if (tools?.Count > 0)
|
||
request.ToolChoice = "auto";
|
||
|
||
var json = JsonSerializer.Serialize(request, _jsonOptions);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
|
||
var response = await _http.PostAsync("chat/completions", content);
|
||
response.EnsureSuccessStatusCode();
|
||
|
||
var body = await response.Content.ReadAsStringAsync();
|
||
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Vision 图片理解(非流式)
|
||
/// </summary>
|
||
public async Task<ChatCompletionResponse> VisionAsync(
|
||
string systemPrompt,
|
||
List<string> imageUrls,
|
||
string? userText = null,
|
||
int maxTokens = 2048)
|
||
{
|
||
var messages = new List<ChatMessage>();
|
||
|
||
if (!string.IsNullOrEmpty(systemPrompt))
|
||
messages.Add(new ChatMessage { Role = "system", Content = systemPrompt });
|
||
|
||
// 构建多模态消息内容
|
||
var contentParts = new List<object>();
|
||
foreach (var url in imageUrls)
|
||
contentParts.Add(new { type = "image_url", image_url = new { url } });
|
||
if (!string.IsNullOrEmpty(userText))
|
||
contentParts.Add(new { type = "text", text = userText });
|
||
|
||
var userMessage = new ChatMessage
|
||
{
|
||
Role = "user",
|
||
Content = JsonSerializer.Serialize(contentParts, _jsonOptions)
|
||
};
|
||
|
||
messages.Add(userMessage);
|
||
|
||
var request = new ChatCompletionRequest
|
||
{
|
||
Model = _model,
|
||
Messages = messages,
|
||
MaxTokens = maxTokens,
|
||
Stream = false,
|
||
};
|
||
|
||
var json = JsonSerializer.Serialize(request, _jsonOptions);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
var response = await _http.PostAsync("chat/completions", content);
|
||
response.EnsureSuccessStatusCode();
|
||
var body = await response.Content.ReadAsStringAsync();
|
||
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
|
||
}
|
||
}
|
||
|
||
#region 请求/响应模型
|
||
|
||
public sealed class ChatCompletionRequest
|
||
{
|
||
public string Model { get; set; } = string.Empty;
|
||
public List<ChatMessage> Messages { get; set; } = [];
|
||
public bool Stream { get; set; }
|
||
public int MaxTokens { get; set; } = 2048;
|
||
public float Temperature { get; set; } = 0.7f;
|
||
public float? TopP { get; set; }
|
||
public List<ToolDefinition>? Tools { get; set; }
|
||
public string? ToolChoice { get; set; }
|
||
}
|
||
|
||
public sealed class ChatMessage
|
||
{
|
||
public string Role { get; set; } = string.Empty;
|
||
public string Content { get; set; } = string.Empty;
|
||
public string? ToolCallId { get; set; }
|
||
public List<ToolCall>? ToolCalls { get; set; }
|
||
}
|
||
|
||
public sealed class ToolDefinition
|
||
{
|
||
public string Type { get; set; } = "function";
|
||
public ToolFunction Function { get; set; } = new();
|
||
}
|
||
|
||
public sealed class ToolFunction
|
||
{
|
||
public string Name { get; set; } = string.Empty;
|
||
public string Description { get; set; } = string.Empty;
|
||
public object Parameters { get; set; } = new();
|
||
}
|
||
|
||
public sealed class ToolCall
|
||
{
|
||
public string Id { get; set; } = string.Empty;
|
||
public string Type { get; set; } = "function";
|
||
public ToolCallFunction Function { get; set; } = new();
|
||
}
|
||
|
||
public sealed class ToolCallFunction
|
||
{
|
||
public string Name { get; set; } = string.Empty;
|
||
public string Arguments { get; set; } = string.Empty;
|
||
}
|
||
|
||
public sealed class ChatCompletionResponse
|
||
{
|
||
public string Id { get; set; } = string.Empty;
|
||
public List<Choice> Choices { get; set; } = [];
|
||
}
|
||
|
||
public sealed class Choice
|
||
{
|
||
public int Index { get; set; }
|
||
public ResponseMessage? Message { get; set; }
|
||
public ResponseDelta? Delta { get; set; }
|
||
public string? FinishReason { get; set; }
|
||
}
|
||
|
||
public sealed class ResponseMessage
|
||
{
|
||
public string Role { get; set; } = string.Empty;
|
||
public string? Content { get; set; }
|
||
public List<ToolCall>? ToolCalls { get; set; }
|
||
}
|
||
|
||
public sealed class ResponseDelta
|
||
{
|
||
public string? Content { get; set; }
|
||
public string? Role { get; set; }
|
||
}
|
||
|
||
#endregion
|