AX Agent 진행 이력 파편 메시지 정제 및 렌더링 안정화
- 스트리밍 TextDelta와 Thinking summary에 공통 정제기를 적용해 1, [, file_read] 같은 저품질 파편 문구가 이벤트와 카드에 쌓이지 않도록 개선 - V2 라이브 진행 카드와 이력 렌더링에서 정제된 thinking summary만 표시하고 low-signal 조각은 숨기며 process feed는 안전한 기본 문구로 폴백 - AgentProgressSummarySanitizerTests와 AgentLoopResponseClassificationServiceTests를 추가/확장하고 dotnet build 경고 0 오류 0, 지정 테스트 22건 통과를 확인
This commit is contained in:
@@ -10,17 +10,12 @@ internal sealed record AgentLoopResponseClassificationResult(
|
||||
{
|
||||
public string BuildThinkingSummary(int maxLength = 150)
|
||||
{
|
||||
if (string.IsNullOrEmpty(TextResponse))
|
||||
return string.Empty;
|
||||
|
||||
return TextResponse.Length > maxLength
|
||||
? TextResponse[..maxLength] + "…"
|
||||
: TextResponse;
|
||||
return AgentProgressSummarySanitizer.NormalizeThinkingSummary(TextResponse, maxLength: maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LLM 응답 블록을 텍스트와 tool_use로 분리하고, 무도구 응답 연속 횟수를 계산한다.
|
||||
/// Splits LLM response blocks into text and tool_use items and tracks consecutive no-tool replies.
|
||||
/// </summary>
|
||||
internal static class AgentLoopResponseClassificationService
|
||||
{
|
||||
|
||||
@@ -582,10 +582,14 @@ public partial class AgentLoopService
|
||||
var now = DateTime.UtcNow;
|
||||
if ((now - lastStreamUiUpdateAt).TotalMilliseconds >= 450 && streamedTextPreview.Length > 0)
|
||||
{
|
||||
var preview = streamedTextPreview.ToString();
|
||||
preview = preview.Length > 140 ? preview[..140] + "…" : preview;
|
||||
EmitEvent(AgentEventType.Thinking, "", preview);
|
||||
lastStreamUiUpdateAt = now;
|
||||
var preview = AgentProgressSummarySanitizer.NormalizeThinkingSummary(
|
||||
streamedTextPreview.ToString(),
|
||||
maxLength: 140);
|
||||
if (!string.IsNullOrWhiteSpace(preview))
|
||||
{
|
||||
EmitEvent(AgentEventType.Thinking, "", preview);
|
||||
lastStreamUiUpdateAt = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -4952,6 +4956,13 @@ public partial class AgentLoopService
|
||||
long elapsedMs = 0, int inputTokens = 0, int outputTokens = 0,
|
||||
string? toolInput = null, int iteration = 0, bool? successOverride = null)
|
||||
{
|
||||
if (type == AgentEventType.Thinking)
|
||||
{
|
||||
summary = AgentProgressSummarySanitizer.NormalizeThinkingSummary(summary, toolName);
|
||||
if (string.IsNullOrWhiteSpace(summary))
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == AgentEventType.Thinking && ShouldSuppressPostCompactionThinking(summary))
|
||||
{
|
||||
_runPostCompactionSuppressedThinkingCount++;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal static class AgentProgressSummarySanitizer
|
||||
{
|
||||
private static readonly Regex s_previousToolCallRegex =
|
||||
new(@"\[이전 도구 호출[^\]\n]*(?:\]|$)", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex s_previousToolBundleRegex =
|
||||
new(@"\[이전 도구 호출 묶음 축약[^\]\n]*(?:\]|$)", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex s_listPrefixRegex =
|
||||
new(@"^\s*(?:\d+\s*[\.\)]|[-*•])\s*", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex s_punctuationOnlyRegex =
|
||||
new(@"^[\[\]\(\)\{\}/\\<>\.\-_:;,`'""*]+$", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex s_numericFragmentRegex =
|
||||
new(@"^\d+\s*[\.\)]?$", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex s_toolTokenRegex =
|
||||
new(@"^[A-Za-z][A-Za-z0-9_\-]{1,40}\]?$", RegexOptions.Compiled);
|
||||
|
||||
public static string NormalizeThinkingSummary(string? summary, string? toolName = null, int maxLength = 0)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(summary))
|
||||
return string.Empty;
|
||||
|
||||
var text = summary
|
||||
.Replace("\r\n", "\n")
|
||||
.Replace('\r', '\n')
|
||||
.Replace("```", string.Empty)
|
||||
.Replace("**", string.Empty)
|
||||
.Replace('`', ' ');
|
||||
|
||||
text = s_previousToolCallRegex.Replace(text, string.Empty);
|
||||
text = s_previousToolBundleRegex.Replace(text, string.Empty);
|
||||
|
||||
var cleanedLines = text
|
||||
.Split('\n')
|
||||
.Select(CleanThinkingLine)
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||||
.Where(line => !IsLowSignalFragment(line))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
if (cleanedLines.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var normalized = string.Join(" ", cleanedLines);
|
||||
normalized = Regex.Replace(normalized, @"\s{2,}", " ").Trim();
|
||||
|
||||
if (maxLength > 0 && normalized.Length > maxLength)
|
||||
normalized = normalized[..maxLength].TrimEnd() + "…";
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static string CleanThinkingLine(string line)
|
||||
{
|
||||
var cleaned = s_listPrefixRegex.Replace(line.Trim(), string.Empty).Trim();
|
||||
cleaned = cleaned.Trim('*', '-', '•', '[', ']', '(', ')', '{', '}', ':', ';');
|
||||
cleaned = Regex.Replace(cleaned, @"\s{2,}", " ").Trim();
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static bool IsLowSignalFragment(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
return true;
|
||||
|
||||
if (s_numericFragmentRegex.IsMatch(line))
|
||||
return true;
|
||||
|
||||
if (s_punctuationOnlyRegex.IsMatch(line))
|
||||
return true;
|
||||
|
||||
if (s_toolTokenRegex.IsMatch(line))
|
||||
return true;
|
||||
|
||||
if (line.Length <= 3 && line.Any(ch => char.IsPunctuation(ch)))
|
||||
return true;
|
||||
|
||||
if (line.Length <= 4 && line.All(ch => char.IsPunctuation(ch) || char.IsWhiteSpace(ch)))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user