AX Agent 계획 선행 잔재 제거 및 최종 후속 권유 조건부 완화

이번 커밋은 claude-code와 비교했을 때 AX에 남아 있던 계획 선행 흐름과 과한 최종 후속 권유를 줄이는 데 집중했다.

핵심 변경 사항:

- AgentLoopService에서 사용되지 않는 plan prelude/승인용 계획 생성 블록 제거

- FinalReportGate를 review 작업 및 고영향 변경에만 적용하도록 축소

- FinalReportQuality 프롬프트에서 remaining risk/next action은 실제 미해결 사항이 있을 때만 쓰도록 완화

- Cowork 프롬프트에서 document_plan을 기본 선행 단계처럼 밀지 않고 필요 시만 사용하도록 조정

- Code REPORT 단계도 변경 내용과 검증 요약 중심으로 정리하고 미해결 리스크만 선택적으로 언급하도록 수정

문서 반영:

- README.md, docs/DEVELOPMENT.md에 2026-04-10 00:08 (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-10 08:39:52 +09:00
parent 6bd8d5bb2c
commit d24c1f9edc
5 changed files with 33 additions and 154 deletions

View File

@@ -239,9 +239,6 @@ public partial class AgentLoopService
var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType);
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
// 플랜 prelude는 현재 정책상 비활성
var shouldGeneratePlanPrelude = false;
var context = BuildContext();
InjectTaskTypeGuidance(messages, taskPolicy);
InjectExplorationScopeGuidance(messages, explorationState.Scope);
@@ -340,148 +337,6 @@ public partial class AgentLoopService
workFolder = context.WorkFolder
}));
// ── 과거 plan mode 잔재. 현재 정책상 비활성 ──
if (shouldGeneratePlanPrelude)
{
iteration++;
EmitEvent(AgentEventType.Thinking, "", "실행 계획 생성 중...");
// 계획 생성 전용 시스템 지시를 임시 추가
var planInstruction = new ChatMessage
{
Role = "user",
Content = "[System] 도구를 호출하지 마세요. 먼저 실행 계획을 번호 매긴 단계로 작성하세요. " +
"각 단계에 사용할 도구와 대상을 구체적으로 명시하세요. " +
"계획만 제시하고 실행은 하지 마세요."
};
messages.Add(planInstruction);
// 도구 없이 텍스트만 요청
string planText;
try
{
planText = await _llm.SendAsync(messages, ct);
}
catch (Exception ex)
{
EmitEvent(AgentEventType.Error, "", $"LLM 오류: {ex.Message}");
return $"⚠ LLM 오류: {ex.Message}";
}
// 계획 지시 메시지 제거 (실제 실행 시 혼란 방지)
messages.Remove(planInstruction);
// 계획 추출
planSteps = TaskDecomposer.ExtractSteps(planText);
planExtracted = true;
if (planSteps.Count > 0)
{
EmitEvent(AgentEventType.Planning, "", $"작업 계획: {planSteps.Count}단계",
steps: planSteps);
// 사용자 승인 대기
if (UserDecisionCallback != null)
{
EmitEvent(
AgentEventType.Decision,
"",
$"계획 확인 대기 · {planSteps.Count}단계",
steps: planSteps);
var decision = await UserDecisionCallback(
planText,
new List<string> { "승인", "수정 요청", "취소" });
EmitPlanDecisionResultEvent(decision, planSteps);
if (decision == "취소")
{
EmitEvent(AgentEventType.Complete, "", "작업이 중단되었습니다");
return "작업이 중단되었습니다.";
}
else if (TryParseApprovedPlanDecision(decision, out var approvedPlanText, out var approvedPlanSteps))
{
planText = approvedPlanText;
planSteps = approvedPlanSteps;
}
else if (decision != null && decision != "승인")
{
// 수정 요청 — 피드백으로 계획 재생성
messages.Add(new ChatMessage { Role = "assistant", Content = planText });
messages.Add(new ChatMessage { Role = "user", Content = decision + "\n위 피드백을 반영하여 실행 계획을 다시 작성하세요." });
// 재생성 루프 (최대 3회)
for (int retry = 0; retry < 3; retry++)
{
try { planText = await _llm.SendAsync(messages, ct); }
catch { break; }
planSteps = TaskDecomposer.ExtractSteps(planText);
if (planSteps.Count > 0)
{
EmitEvent(AgentEventType.Planning, "", $"수정된 계획: {planSteps.Count}단계",
steps: planSteps);
}
EmitEvent(
AgentEventType.Decision,
"",
$"수정 계획 확인 대기 · {planSteps.Count}단계",
steps: planSteps);
decision = await UserDecisionCallback(
planText,
new List<string> { "승인", "수정 요청", "취소" });
EmitPlanDecisionResultEvent(decision, planSteps);
if (decision == "취소")
{
EmitEvent(AgentEventType.Complete, "", "작업이 중단되었습니다");
return "작업이 중단되었습니다.";
}
if (TryParseApprovedPlanDecision(decision, out var revisedPlanText, out var revisedPlanSteps))
{
planText = revisedPlanText;
planSteps = revisedPlanSteps;
break;
}
if (decision == null || decision == "승인") break;
// 재수정
messages.Add(new ChatMessage { Role = "assistant", Content = planText });
messages.Add(new ChatMessage { Role = "user", Content = decision + "\n위 피드백을 반영하여 실행 계획을 다시 작성하세요." });
}
}
}
// 승인된 계획을 컨텍스트에 포함하여 실행 유도
// 도구 호출을 명확히 강제하여 텍스트 응답만 반환하는 경우 방지
messages.Add(new ChatMessage { Role = "assistant", Content = planText });
// 1차 계획의 단계들을 document_plan의 sections_hint로 전달하도록 지시
// → BuildSections() 하드코딩 대신 LLM이 잡은 섹션 구조가 문서에 반영됨
var planSectionsHint = planSteps.Count > 0
? string.Join(", ", planSteps)
: "";
var sectionInstruction = !string.IsNullOrEmpty(planSectionsHint)
? $"document_plan 도구를 호출할 때 sections_hint 파라미터에 위 계획의 섹션/단계를 그대로 넣으세요: \"{planSectionsHint}\""
: "";
messages.Add(new ChatMessage { Role = "user",
Content = "계획이 승인되었습니다. 지금 즉시 1단계부터 도구(tool)를 호출하여 실행을 시작하세요. " +
"텍스트로 설명하지 말고 반드시 도구를 호출하세요." +
(string.IsNullOrEmpty(sectionInstruction) ? "" : "\n" + sectionInstruction) });
}
else
{
// 계획 추출 실패 — assistant 응답으로 추가하고 일반 모드로 진행
if (!string.IsNullOrEmpty(planText))
messages.Add(new ChatMessage { Role = "assistant", Content = planText });
}
}
while (iteration < maxIterations && !ct.IsCancellationRequested)
{
iteration++;
@@ -3538,8 +3393,8 @@ public partial class AgentLoopService
private static string BuildFinalReportQualityPrompt(TaskTypePolicy taskPolicy, bool highImpact)
{
var taskLine = taskPolicy.FinalReportTaskLine;
var riskLine = highImpact
? "고영향 변경이므로 남은 리스크나 추가 확인 필요 사항도 반드시 적으세요.\n"
var riskLine = taskPolicy.IsReviewTask || highImpact
? "남은 리스크나 추가 확인 필요 사항이 실제로 남아 있을 때만 짧게 적으세요.\n"
: "";
return "[System:FinalReportQuality] 최종 답변을 더 구조적으로 정리하세요.\n" +
@@ -3551,7 +3406,7 @@ public partial class AgentLoopService
"6. review 작업이면 이슈별로 수정 완료/미수정 상태를 분리해 적으세요\n" +
taskLine +
riskLine +
"가능하면 짧고 명확하게 요약하세요.";
"후속 권유는 실제 미해결 위험이나 추가 확인이 남았을 때만 포함하세요. 가능하면 짧고 명확하게 요약하세요.";
}
private static string BuildFinalReportQualityPrompt(string taskType, bool highImpact)

View File

@@ -55,9 +55,11 @@ public partial class AgentLoopService
var hasDiffEvidence = HasDiffEvidenceAfterLastModification(messages);
var hasRecentBuildOrTestEvidence = HasBuildOrTestEvidenceAfterLastModification(messages);
var hasSuccessfulBuildAndTestEvidence = HasSuccessfulBuildAndTestAfterLastModification(messages);
var hasLightweightCompletionEvidence = hasCodeVerificationEvidence
|| hasDiffEvidence
|| hasRecentBuildOrTestEvidence;
if (executionPolicy.CodeVerificationGateMaxRetries > 0
&& !hasCodeVerificationEvidence
&& !(hasDiffEvidence && hasRecentBuildOrTestEvidence && !requireHighImpactCodeVerification)
&& !(requireHighImpactCodeVerification ? hasCodeVerificationEvidence : hasLightweightCompletionEvidence)
&& runState.CodeVerificationGateRetry < executionPolicy.CodeVerificationGateMaxRetries)
{
runState.CodeVerificationGateRetry++;
@@ -93,9 +95,12 @@ public partial class AgentLoopService
return true;
}
var hasBlockingCodeEvidenceGap = !hasCodeVerificationEvidence
var hasBlockingCodeEvidenceGap = !(requireHighImpactCodeVerification ? hasCodeVerificationEvidence : hasLightweightCompletionEvidence)
|| (requireHighImpactCodeVerification && !hasSuccessfulBuildAndTestEvidence);
var shouldRequestStructuredFinalReport =
taskPolicy.IsReviewTask || requireHighImpactCodeVerification;
if (executionPolicy.FinalReportGateMaxRetries > 0
&& shouldRequestStructuredFinalReport
&& !hasBlockingCodeEvidenceGap
&& !HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages)
&& runState.FinalReportGateRetry < executionPolicy.FinalReportGateMaxRetries)