컨텍스트 tool_result 예산 규칙을 공용화해 query view와 압축 단계 정합성 강화

- AgentToolResultBudget helper를 추가해 오래된 tool_result를 최근 보호 구간과 aggregate budget 기준으로 공용 축약
- AgentQueryContextBuilder와 ContextCondenser가 같은 budget 규칙을 사용하도록 정리
- README와 DEVELOPMENT 문서에 2026-04-12 22:02 (KST) 기준 작업 이력 반영
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0, 오류 0)
This commit is contained in:
2026-04-12 21:47:02 +09:00
parent 0a2e5b2d49
commit 1dd10e0664
5 changed files with 168 additions and 137 deletions

View File

@@ -241,14 +241,14 @@ public static class ContextCondenser
}
/// <summary>
/// 1단계: 대용량 도구 결과를 축약합니다.
/// tool_result JSON이나 긴 파일 내용 등을 핵심만 남기고 자릅니다.
/// 1단계: 오래된 tool_result는 aggregate budget 기준으로 먼저 줄이고,
/// 그 외 긴 assistant/user 메시지는 경량 절단합니다.
/// </summary>
private static bool TruncateToolResults(List<ChatMessage> messages)
{
bool truncated = false;
var budgetResult = AgentToolResultBudget.Apply(messages, RecentKeepCount);
bool truncated = budgetResult.TruncatedCount > 0;
// 최근 RecentKeepCount개는 건드리지 않음 (방금 실행한 도구 결과는 유지)
var cutoff = Math.Max(0, messages.Count - RecentKeepCount);
for (int i = 0; i < cutoff; i++)
@@ -256,25 +256,20 @@ public static class ContextCondenser
var msg = messages[i];
if (msg.Content == null) continue;
// tool_result 메시지의 대용량 출력 축약
if (msg.Content.StartsWith("{\"type\":\"tool_result\"") && msg.Content.Length > MaxToolResultChars)
if (AgentMessageInvariantHelper.TryGetToolResultId(msg, out _))
{
// JSON 구조를 유지하되 output 부분만 축약
messages[i] = CloneWithContent(msg, TruncateToolResultJson(msg.Content));
truncated = true;
continue;
}
// assistant의 도구 호출 블록 내 긴 텍스트도 축약
else if (msg.Role == "assistant" && msg.Content.Length > MaxToolResultChars * 2 &&
if (msg.Role == "assistant" && msg.Content.Length > MaxToolResultChars * 2 &&
msg.Content.StartsWith("{\"_tool_use_blocks\""))
{
// 도구 호출 구조는 유지, 텍스트 블록만 축약
if (msg.Content.Length > MaxToolResultChars * 3)
{
messages[i] = CloneWithContent(msg, msg.Content[..(MaxToolResultChars * 2)] + "...[축약됨]\"]}");
truncated = true;
}
}
// 일반 assistant/user 메시지 중 비정상적으로 긴 것 (예: 파일 내용 전체 붙여넣기)
else if (msg.Content.Length > MaxToolResultChars * 3 && msg.Role != "system")
{
messages[i] = CloneWithContent(
@@ -658,40 +653,6 @@ public static class ContextCondenser
};
}
/// <summary>tool_result JSON 내의 output 값을 축약합니다.</summary>
private static string TruncateToolResultJson(string json)
{
// 간단한 문자열 처리로 output 부분만 축약 (JSON 파서 없이)
const string marker = "\"output\":\"";
var idx = json.IndexOf(marker, StringComparison.Ordinal);
if (idx < 0) return json[..Math.Min(json.Length, MaxToolResultChars)] + "...[축약됨]}";
var outputStart = idx + marker.Length;
// output 끝 찾기 (이스케이프된 따옴표 고려)
var outputEnd = outputStart;
while (outputEnd < json.Length)
{
if (json[outputEnd] == '\\') { outputEnd += 2; continue; }
if (json[outputEnd] == '"') break;
outputEnd++;
}
var outputLen = outputEnd - outputStart;
if (outputLen <= MaxToolResultChars) return json; // 이미 짧음
// 앞부분 + "...[축약됨]" + 뒷부분
var keepLen = MaxToolResultChars / 2;
var prefix = json[..outputStart];
var outputText = json[outputStart..outputEnd];
var suffix = json[outputEnd..];
return prefix +
outputText[..keepLen] +
"\\n...[축약됨: " + $"{outputLen:N0}" + "자 중 " + $"{MaxToolResultChars:N0}" + "자 유지]\\n" +
outputText[^keepLen..] +
suffix;
}
/// <summary>
/// 2단계: 오래된 메시지를 LLM으로 요약합니다.
/// 시스템 메시지 + 최근 N개는 유지하고, 나머지를 요약으로 교체합니다.