Странный зависающий запрос в приложении .NET/CoreC#

Место общения программистов C#
Ответить
Anonymous
 Странный зависающий запрос в приложении .NET/Core

Сообщение Anonymous »

У меня запущено два приложения: веб-приложение на .NET 4.7.2 (управление версиями не находится под моим контролем...) и еще одно на .NET 9 (прокси-служба LLM). Прокси-сервис принимает запрос, маршрутизирует его и использует семантическое ядро ​​для вызова OpenAI. Веб-приложение – это внутренний инструмент с интерфейсом чат-бота.
Если я вызываю прокси-службу непосредственно из интерфейса JavaScript

Код: Выделить всё

const res = await fetch(API_URL ...
все работает как надо и запрос проходит. Если я вызываю прокси через веб-приложение, запрос поступает в прокси-службу, вплоть до вызова OpenAI, а затем просто... зависает и истекает время ожидания. Иногда это работает, но затем при перезапуске снова зависает.
Соответствующий код — веб-приложение (.NET 4.7.2), вызывающий объект

Код: Выделить всё

/// Controller
[RoutePrefix("LLMProxy")]
public class LLMProxyController : Controller
{
[HttpPost]
[Route("Chat")]
public async Task Query()
{
string prefix = ConfigurationManager.AppSettings["ParameterStorePrefix"];
string parsedPref = TenantHelper.NormalizeParameterStorePrefix(prefix);

Request.InputStream.Position = 0;
using var reader = new System.IO.StreamReader(Request.InputStream);
var body = await reader.ReadToEndAsync().ConfigureAwait(false);

var dto = Newtonsoft.Json.JsonConvert.DeserializeObject(body);

if (dto == null || string.IsNullOrWhiteSpace(dto.Prompt))
return new HttpStatusCodeResult(400, "Missing prompt");

var raw = dto.JsonBlob?.ToString(Newtonsoft.Json.Formatting.None);

var proxyBusinessModel = new LLMProxyBusinessModel();
var result = await proxyBusinessModel.PostLLMChat(new Entity.ChatInputEntity
{
// ...
}, dto.ChatbotId).ConfigureAwait(false);

var json = JsonConvert.SerializeObject(result);

return Content(json, "application/json");
}
}

/// LLMProxyBusinessModel

public class LLMProxyBusinessModel
{
public string LLMProxyUrl
{
get => System.Configuration.ConfigurationManager.AppSettings["LLMProxyBaseUrl"].TrimEnd('/');
}

public async Task PostLLMChat(ChatInputEntity input, string chatbotId)
{
var url = string.IsNullOrEmpty(chatbotId)
? $"{LLMProxyUrl}/llm/chat"
: $"{LLMProxyUrl}/llm/chat?chatbotId={Uri.EscapeDataString(chatbotId)}";

var handler = new HttpClientHandler { UseProxy = false }; // == Also tried without ==
using (var http = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(3) })
{
http.DefaultRequestHeaders.ExpectContinue = false; // == Added this to try ==
http.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

var json = Newtonsoft.Json.JsonConvert.SerializeObject(input);
using var content = new StringContent(json, Encoding.UTF8, "application/json");

using var resp = await http.PostAsync(url, content).ConfigureAwait(false);
var raw = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
resp.EnsureSuccessStatusCode();

return Newtonsoft.Json.JsonConvert.DeserializeObject(raw);
}
}
}
Прокси-сервер LLM (получатель)

Код: Выделить всё

/// Controller
[ApiController]
[Route("llm")]
public class LLMController(RespondService respondService, ChatService chatService, ILogger logger) : Controller
{
private readonly ILogger _logger = logger;
private readonly RespondService _respondService = respondService;
private readonly ChatService _chatService = chatService;

[EndpointName("Chat")]
[EndpointSummary("Handles a chat request that maintains conversation state.")]
[EndpointDescription("Can receive a system prompt to set the context for the chat session.")]
[HttpPost("chat")]
public async Task Chat([FromBody] ChatInput input, [FromQuery] string chatbotId = "", CancellationToken ct = default)
{
var linked = CancellationTokenSource.CreateLinkedTokenSource(ct);
linked.CancelAfter(TimeSpan.FromMinutes(3));  // == This was added as a test, no change ==

if (input is null)
{
_logger.LogWarning("Chat input is null.");
return BadRequest("Chat input cannot be null.");
}

_logger.LogInformation("Processing chat input: {Input}, chatbotId: {ChatbotId}", input, chatbotId);

try
{
var response = await _chatService.HandleChatInput(input, chatbotId, HttpContext, linked.Token);
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing chat request.");
return StatusCode(500, "Internal server error.");
}
}
}

/// Business logic
public async Task HandleChatInput(
ChatInput input,
string chatbotId,
HttpContext http,
CancellationToken ct = default)
{
// setup code, no HTTP, both go beyond this
// ...

var response = await GetChatResponseAsync(input, chatbotId, session, messageId, ct);

return response;
}

private async Task GetChatResponseAsync(
ChatInput input,
string chatbotId,
ChatSession session,
string messageId,
CancellationToken ct = default)
{
var chatKernel = _router.GetKernelForModel(input, chatbotId);
var chat = chatKernel.GetRequiredService();

// ==== CRITICAL PART ====
// == In both cases, we reach this part. But when calling from the web app, I timeout here.
var reply = await chat.GetChatMessageContentAsync(
session.History,
executionSettings: ChatHelper.GetExecutionSettings(chatbotId),
kernel: chatKernel,
cancellationToken: ct);

// == When calling from the web app, we never reach the return and just time out above ==

return new LLMOutput
{
Response = reply.Content,
ReturnValue = session.ReturnValue,
MessageId = messageId
};
}
Код внешнего интерфейса, осуществляющий вызов:

Код: Выделить всё

/// When I call via the web app, whos Razor page I am within:

const llmPostUrl = '@Url.Action("Chat", "LLMProxy", new { area = "" })';

const res = await fetch(llmPostUrl, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
UserId: userId,
Prompt: txt,
OrganisationId: organisationId,
WorkGroupId: workGroupId,
ChatbotId: chatbotId,
NewSession: createNewSession,
JsonBlob: jsonBlob, })
});

/// When I call via the same JS file but call the proxy directly

var API_URL = "http://localhost:5183/llm"

const res = await fetch(API_URL + "/chat?chatbotId=" + chatbotId, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
prompt: txt,
jsonBlob: jsonBlob,
reasoning: true,
model: null
})
});
JSON различается, поскольку веб-приложение добавляет их в исходящий запрос.
Тайм-ауты большие (веб-приложение 3 минуты, LLM Proxy 150 секунд). Вызов модели обычно выполняется быстро, когда он работает.
Журналы http показывают:

Код: Выделить всё

Start processing HTTP request POST https://api.openai.com/v1/chat/completions
Sending HTTP request POST https://api.openai.com/v1/chat/completions

...  and then nothing until timeout.
Предварительная проверка GET на https://api.openai.com/v1/models с использованием клиента с тем же именем немедленно возвращает 200 OK примерно за 1,3 секунды — даже если /chat/completions зависает.
Перезапуск прокси-сервера LLM может изменить поведение (работает > перезапуск > зависает).
/>
  • Переключен на именованный клиент IHttpClientFactory («LLMProvider») и внедрен в SK.
  • Принудительный HTTP/1.1:

Код: Выделить всё

http.DefaultRequestVersion = HttpVersion.Version11;
http.DefaultVersionPolicy  = HttpVersionPolicy.RequestVersionOrLower;
  • Добавлена ​​перезапуск соединения + сбой быстрого подключения через SocketsHttpHandler:

Код: Выделить всё

ConnectTimeout = 10s
PooledConnectionLifetime = 5m
PooledConnectionIdleTimeout = 2m
DnsRefreshTimeout = 5m
MaxConnectionsPerServer = 256
ExpectContinue = false
UseProxy = false
Нет изменений, все еще зависает на /chat/completions.
  • Одноразовое соединение: закрыть (для обхода сокетов пула) — без изменений.
  • Временно отключены проверки отзыва TLS/закреплен TLS 1.2 — без изменений.
Я также попробовал запустить его внутри докер-контейнера, просто чтобы посмотреть, изменится ли что-нибудь. Неа. Простой запрос cURL непосредственно к нему также работает нормально.

Подробнее здесь: https://stackoverflow.com/questions/797 ... pplication
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»