From 38603eb4f9473606475ee0b63b79b9589c17ac59 Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Sun, 10 May 2026 09:09:45 +0800 Subject: [PATCH 1/2] fix(llm): normalize native tool call metadata --- .../LLM/OpenAIToolCallNormalizationTests.cs | 65 +++++++++++++++++++ .../Service/AI/LLM/OpenAIService.cs | 50 ++++++++++---- .../TelegramSearchBot.LLM.csproj | 4 ++ 3 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs diff --git a/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs b/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs new file mode 100644 index 00000000..f82c3938 --- /dev/null +++ b/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs @@ -0,0 +1,65 @@ +using TelegramSearchBot.Service.AI.LLM; +using Xunit; + +namespace TelegramSearchBot.LLM.Test.Service.AI.LLM { + public class OpenAIToolCallNormalizationTests { + [Fact] + public void NormalizeToolCallId_EmptyValue_GeneratesNonEmptyFallback() { + var id = OpenAIService.NormalizeToolCallId(string.Empty); + + Assert.False(string.IsNullOrWhiteSpace(id)); + Assert.StartsWith("call_", id); + } + + [Fact] + public void NormalizeToolCallId_NonEmptyValue_TrimsAndPreservesValue() { + var id = OpenAIService.NormalizeToolCallId(" call_123 "); + + Assert.Equal("call_123", id); + } + + [Fact] + public void NormalizeToolCallName_EmptyValue_ReturnsUnknown() { + var name = OpenAIService.NormalizeToolCallName(" "); + + Assert.Equal("unknown", name); + } + + [Fact] + public void NormalizeToolCallName_NonEmptyValue_TrimsAndPreservesValue() { + var name = OpenAIService.NormalizeToolCallName(" tool_1 "); + + Assert.Equal("tool_1", name); + } + + [Fact] + public void NormalizeToolCallArguments_EmptyValue_ReturnsEmptyJsonObject() { + var arguments = OpenAIService.NormalizeToolCallArguments(""); + + Assert.Equal("{}", arguments); + } + + [Fact] + public void NormalizeToolCallArguments_WhitespaceValue_ReturnsEmptyJsonObject() { + var arguments = OpenAIService.NormalizeToolCallArguments(" "); + + Assert.Equal("{}", arguments); + } + + [Fact] + public void DeserializeToolArgumentsForDisplay_ConvertsNonStringValues() { + var arguments = OpenAIService.DeserializeToolArgumentsForDisplay("{\"count\":2,\"enabled\":true,\"text\":\"hello\"}"); + + Assert.Equal("2", arguments["count"]); + Assert.Equal("True", arguments["enabled"]); + Assert.Equal("hello", arguments["text"]); + } + + [Fact] + public void DeserializeToolArgumentsForDisplay_InvalidJson_ReturnsEmptyDictionary() { + var arguments = OpenAIService.DeserializeToolArgumentsForDisplay("{not json"); + + Assert.Empty(arguments); + } + } +} diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs index b10a2406..c2cbcb1d 100644 --- a/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs +++ b/TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs @@ -44,6 +44,35 @@ private class ToolCallAccumulator { public StringBuilder Arguments { get; } = new StringBuilder(); } + internal static string NormalizeToolCallId(string toolCallId) { + return string.IsNullOrWhiteSpace(toolCallId) + ? $"call_{Guid.NewGuid():N}" + : toolCallId.Trim(); + } + + internal static string NormalizeToolCallName(string toolCallName) { + return string.IsNullOrWhiteSpace(toolCallName) + ? "unknown" + : toolCallName.Trim(); + } + + internal static string NormalizeToolCallArguments(string argumentsJson) { + return string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson; + } + + internal static Dictionary DeserializeToolArgumentsForDisplay(string argumentsJson) { + try { + var normalized = NormalizeToolCallArguments(argumentsJson); + var values = JsonConvert.DeserializeObject>(normalized); + return values?.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.ToString() ?? string.Empty) + ?? new Dictionary(); + } catch { + return new Dictionary(); + } + } + private readonly ILogger _logger; public static string _botName; public string BotName { @@ -1022,13 +1051,16 @@ private async IAsyncEnumerable ExecWithNativeToolCallingAsync( // Build the assistant message with tool calls chatToolCalls = new List(); foreach (var (index, acc) in toolCallAccumulators) { - if (acc.Id == null) { + if (string.IsNullOrWhiteSpace(acc.Id)) { _logger.LogWarning("{ServiceName}: Tool call at index {Index} has no ID, generating fallback.", ServiceName, index); } + var toolCallId = NormalizeToolCallId(acc.Id); + var toolName = NormalizeToolCallName(acc.Name); + var argumentsJson = NormalizeToolCallArguments(acc.Arguments.ToString()); chatToolCalls.Add(ChatToolCall.CreateFunctionToolCall( - acc.Id ?? $"call_{Guid.NewGuid():N}", - acc.Name ?? "unknown", - BinaryData.FromString(acc.Arguments.Length > 0 ? acc.Arguments.ToString() : "{}"))); + toolCallId, + toolName, + BinaryData.FromString(argumentsJson))); } var assistantMessage = new AssistantChatMessage(chatToolCalls); @@ -1041,15 +1073,13 @@ private async IAsyncEnumerable ExecWithNativeToolCallingAsync( var toolIndicators = new StringBuilder(); foreach (var toolCall in chatToolCalls) { - var argsJson = toolCall.FunctionArguments?.ToString() ?? "{}"; - var argsDict = JsonConvert.DeserializeObject>(argsJson) - ?? new Dictionary(); + var argsDict = DeserializeToolArgumentsForDisplay(toolCall.FunctionArguments?.ToString()); toolIndicators.Append(McpToolHelper.FormatToolCallDisplay(toolCall.FunctionName, argsDict)); } currentMessageContentBuilder.Append(toolIndicators.ToString()); } catch (Exception ex) { _logger.LogError(ex, "{ServiceName}: Error building tool calls, returning error to LLM for self-correction", ServiceName); - var errorMsg = $"Error processing tool call: {ex.Message}. Please check your tool call parameters and try again."; + const string errorMsg = "Tool call failed before execution due to malformed tool metadata. Please verify the tool name and parameters, then try again."; providerHistory.Add(new UserChatMessage(errorMsg)); continue; } @@ -1065,9 +1095,7 @@ private async IAsyncEnumerable ExecWithNativeToolCallingAsync( string toolResultString; try { // Parse arguments from JSON - var argsJson = toolCall.FunctionArguments?.ToString() ?? "{}"; - var argsDict = JsonConvert.DeserializeObject>(argsJson) - ?? new Dictionary(); + var argsDict = DeserializeToolArgumentsForDisplay(toolCall.FunctionArguments?.ToString()); var toolContext = new ToolContext { ChatId = ChatId, UserId = message.FromUserId, MessageId = message.MessageId }; object toolResultObject = await McpToolHelper.ExecuteRegisteredToolAsync(toolName, argsDict, toolContext); diff --git a/TelegramSearchBot.LLM/TelegramSearchBot.LLM.csproj b/TelegramSearchBot.LLM/TelegramSearchBot.LLM.csproj index 310b65aa..0c443575 100644 --- a/TelegramSearchBot.LLM/TelegramSearchBot.LLM.csproj +++ b/TelegramSearchBot.LLM/TelegramSearchBot.LLM.csproj @@ -6,6 +6,10 @@ warnings + + + + From 009db4077a9b74fee8ff406b9b0f3c86c9d4453f Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Sun, 10 May 2026 09:23:15 +0800 Subject: [PATCH 2/2] test(vector): warm up search performance benchmark --- .../Service/Vector/VectorPerformanceTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TelegramSearchBot.Test/Service/Vector/VectorPerformanceTests.cs b/TelegramSearchBot.Test/Service/Vector/VectorPerformanceTests.cs index a4ab0a22..005f3cdb 100644 --- a/TelegramSearchBot.Test/Service/Vector/VectorPerformanceTests.cs +++ b/TelegramSearchBot.Test/Service/Vector/VectorPerformanceTests.cs @@ -151,6 +151,10 @@ public async Task MultipleSearches_ShouldMaintainConsistentPerformance() { SearchType = SearchType.Vector }; + // Warm up FAISS/index loading so the measured loop tracks steady-state search performance. + var warmupResult = await _faissVectorService.Search(searchOption); + Assert.NotNull(warmupResult); + // Act - 执行多次搜索 for (int i = 0; i < searchCount; i++) { var stopwatch = Stopwatch.StartNew();