AX Agent 코워크·코드 후속 호환 과제 정리 및 IBM Qwen 도구호출 경로 보강

이번 커밋은 claude-code 기준 후속 과제를 이어서 반영해 Cowork/Code의 도구 선택 강도와 IBM 배포형 vLLM(Qwen) 호환 경로를 정리했다.

핵심 변경 사항:

- IBM 전용 tool body에서 과거 assistant tool_calls 및 role=tool 이력을 OpenAI 형식으로 재전송하지 않고 평탄한 transcript로 직렬화하도록 변경

- Cowork 프롬프트에서 document_review 및 format_convert를 기본 단계처럼 강제하지 않고 file_read/document_read 중심의 가벼운 검증 흐름으로 완화

- unknown/disallowed tool recovery에서 tool_search를 항상 강제하지 않고 alias 후보나 활성 도구 예시로 바로 선택 가능하면 직접 사용하도록 조정

- Code 탐색에서 정의/참조/구현/호출관계 의도는 lsp_code_intel을 더 우선하도록 보강하고 LSP 결과 요약 품질 개선

- 문서 검증 근거는 file_read/document_read면 충분하도록 단순화

문서 반영:

- README.md, docs/DEVELOPMENT.md에 2026-04-09 22:48 (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-09 22:50:27 +09:00
parent 3c6d2f1ce4
commit 1fab215344
8 changed files with 967 additions and 72 deletions

View File

@@ -11,6 +11,8 @@ public partial class AgentLoopService
TopicBased,
RepoWide,
OpenEnded,
/// <summary>문서 생성 요청 — 탐색 단계를 건너뛰고 바로 document_plan/생성 도구 사용.</summary>
DirectCreation,
}
private sealed class ExplorationTrackingState
@@ -24,6 +26,196 @@ public partial class AgentLoopService
public bool CorrectiveHintInjected { get; set; }
}
private static IReadOnlyCollection<IAgentTool> FilterExplorationToolsForCurrentIteration(
IReadOnlyCollection<IAgentTool> tools,
ExplorationTrackingState state,
string userQuery,
string? activeTab,
int totalToolCalls)
{
if (tools.Count == 0)
return tools;
// 문서 생성 모드: 생성 도구를 최우선, 탐색 도구를 뒤로 배치
if (state.Scope == ExplorationScope.DirectCreation)
{
var creationFirst = new List<IAgentTool>(tools.Count);
creationFirst.AddRange(tools.Where(IsDocumentCreationTool));
creationFirst.AddRange(tools.Where(t => !IsDocumentCreationTool(t)
&& !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
// 탐색 도구는 마지막에 (필요 시에만 사용)
creationFirst.AddRange(tools.Where(t =>
string.Equals(t.Name, "glob", StringComparison.OrdinalIgnoreCase)
|| string.Equals(t.Name, "grep", StringComparison.OrdinalIgnoreCase)));
return creationFirst
.DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
.ToList()
.AsReadOnly();
}
var allowFolderMap = ShouldAllowFolderMapForCurrentIteration(state, userQuery, activeTab, totalToolCalls);
var ordered = new List<IAgentTool>(tools.Count);
ordered.AddRange(tools.Where(IsSelectiveDiscoveryTool));
ordered.AddRange(tools.Where(t => !IsSelectiveDiscoveryTool(t) &&
(allowFolderMap || !string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase))));
if (allowFolderMap)
{
ordered.AddRange(tools.Where(t => string.Equals(t.Name, "folder_map", StringComparison.OrdinalIgnoreCase)));
}
return ordered
.DistinctBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
.ToList()
.AsReadOnly();
}
private static bool IsDocumentCreationTool(IAgentTool tool)
{
return tool.Name is "document_plan" or "docx_create" or "html_create" or "excel_create"
or "markdown_create" or "csv_create" or "pptx_create" or "chart_create"
or "document_assemble" or "file_write";
}
private static bool ShouldAllowFolderMapForCurrentIteration(
ExplorationTrackingState state,
string userQuery,
string? activeTab,
int totalToolCalls)
{
// 문서 생성 모드에서는 folder_map 차단 — 탐색 없이 바로 생성
if (state.Scope == ExplorationScope.DirectCreation)
return false;
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
return true;
if (HasExplicitFolderIntent(userQuery))
return true;
if (string.Equals(activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)
&& IsExistingMaterialReferenceRequest(userQuery))
return true;
return totalToolCalls >= 2 && state.TotalFilesRead == 0 && state.FolderMapCalls == 0;
}
private static bool HasExplicitFolderIntent(string userQuery)
{
if (string.IsNullOrWhiteSpace(userQuery))
return false;
return ContainsAny(
userQuery,
"folder",
"directory",
"tree",
"structure",
"list files",
"workspace layout",
"work folder",
"project structure",
"폴더",
"디렉터리",
"폴더 구조",
"디렉터리 구조",
"파일 목록",
"작업 폴더",
"폴더 안",
"구조를 보여",
"구조 확인");
}
private static bool IsExistingMaterialReferenceRequest(string userQuery)
{
if (string.IsNullOrWhiteSpace(userQuery))
return false;
return ContainsAny(
userQuery,
"existing file",
"existing files",
"existing document",
"existing documents",
"reference files",
"reference docs",
"existing materials",
"기존 파일",
"기존 문서",
"기존 자료",
"참고 파일",
"참고 문서",
"폴더 내 자료",
"안의 자료",
"작업 폴더 파일");
}
private static bool IsSelectiveDiscoveryTool(IAgentTool tool)
{
return tool.Name is "glob" or "grep" or "file_read" or "document_read" or "multi_read" or "lsp_code_intel";
}
private static bool HasSemanticNavigationIntent(string userQuery)
{
if (string.IsNullOrWhiteSpace(userQuery))
return false;
return ContainsAny(
userQuery,
"definition",
"reference",
"references",
"implementation",
"implementations",
"caller",
"callers",
"callee",
"call hierarchy",
"symbol",
"symbols",
"interface",
"override",
"정의",
"참조",
"구현",
"호출부",
"호출 관계",
"심볼",
"인터페이스",
"오버라이드");
}
private static string BuildPreferredInitialToolSequence(
ExplorationTrackingState state,
TaskTypePolicy taskPolicy,
string? activeTab,
string userQuery)
{
// 문서 생성 의도가 감지되면 탐색 없이 바로 문서 계획/생성 도구 사용
if (state.Scope == ExplorationScope.DirectCreation)
return "document_plan -> docx_create/html_create/excel_create -> self-review";
if (HasExplicitFolderIntent(userQuery))
return "folder_map -> glob/grep -> targeted read";
if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase))
return "glob/grep -> document_read/file_read -> drafting tool";
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
&& HasSemanticNavigationIntent(userQuery))
return "lsp_code_intel -> targeted file_read -> file_edit/file_write -> build_run/test_loop as needed";
if (string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase))
return "targeted file_read or grep/glob/lsp -> file_edit/file_write -> build_run/test_loop as needed";
if (state.Scope == ExplorationScope.TopicBased)
return "glob/grep -> targeted read -> next action";
return "glob/grep -> file_read/document_read -> next action";
}
private static ExplorationScope ClassifyExplorationScope(string userQuery, string? activeTab)
{
if (string.IsNullOrWhiteSpace(userQuery))
@@ -32,6 +224,11 @@ public partial class AgentLoopService
var q = userQuery.Trim();
var lower = q.ToLowerInvariant();
// Cowork 탭에서 문서 생성 의도가 있으면 탐색을 건너뛰고 바로 생성 도구 사용
if (!string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase)
&& HasDocumentCreationIntent(lower))
return ExplorationScope.DirectCreation;
if (lower.Contains("전체") || lower.Contains("전반") || lower.Contains("코드베이스 전체") ||
lower.Contains("repo-wide") || lower.Contains("repository-wide") || lower.Contains("전체 구조") ||
lower.Contains("아키텍처") || lower.Contains("전체 점검"))
@@ -52,14 +249,47 @@ public partial class AgentLoopService
: ExplorationScope.OpenEnded;
}
/// <summary>
/// 사용자가 문서를 새로 생성/작성하려는 의도인지 판별합니다.
/// "보고서 작성해줘", "문서 만들어줘" 등 생성 동사가 포함된 경우 true.
/// </summary>
private static bool HasDocumentCreationIntent(string lowerQuery)
{
// 생성 동사 키워드
var hasCreationVerb = ContainsAny(lowerQuery,
"작성해", "써줘", "써 줘", "만들어", "생성해", "작성 해",
"만들어줘", "만들어 줘", "생성해줘", "생성해 줘",
"write", "create", "draft", "generate", "compose",
"작성하", "작성을", "생성하", "생성을",
"리포트 써", "보고서 써", "문서 써",
"작성 부탁", "만들어 부탁");
if (!hasCreationVerb)
return false;
// 생성 대상이 문서/보고서/자료 등인지 확인
return ContainsAny(lowerQuery,
"보고서", "문서", "제안서", "리포트", "분석서", "기획서",
"report", "document", "proposal", "analysis",
"요약서", "발표자료", "ppt", "pptx", "docx", "xlsx", "excel", "word",
"표", "차트", "스프레드시트", "프레젠테이션",
"정리해", "정리 해");
}
private static void InjectExplorationScopeGuidance(List<ChatMessage> messages, ExplorationScope scope)
{
var guidance = scope switch
{
ExplorationScope.DirectCreation =>
"Exploration scope = direct-creation. The user wants to CREATE a new document/report/file. " +
"Do NOT search for existing files with glob/grep/folder_map — skip exploration entirely. " +
"Call document_plan first to outline the document structure, then immediately call the appropriate creation tool " +
"(docx_create, html_create, excel_create, markdown_create, etc.) to produce the actual file. " +
"The output MUST be a real file on disk, not a text response.",
ExplorationScope.Localized =>
"Exploration scope = localized. Start with grep/glob and targeted file reads. Avoid folder_map unless structure is unclear.",
"Exploration scope = localized. Start with lsp_code_intel when the request is about definitions/references/implementations/call hierarchy; otherwise use targeted file_read or grep/glob. Avoid folder_map unless the user explicitly asks for folder structure or file listing.",
ExplorationScope.TopicBased =>
"Exploration scope = topic-based. Identify candidate files by topic keywords first, then read only a small targeted set.",
"Exploration scope = topic-based. Identify candidate files by topic keywords first with glob/grep, then read only a small targeted set.",
ExplorationScope.RepoWide =>
"Exploration scope = repo-wide. Broad structure inspection is allowed when needed.",
_ =>
@@ -93,6 +323,15 @@ public partial class AgentLoopService
string toolName,
string argsJson)
{
// 문서 생성 모드: 탐색 도구가 1회라도 호출되면 즉시 교정
if (state.Scope == ExplorationScope.DirectCreation)
{
return string.Equals(toolName, "folder_map", StringComparison.OrdinalIgnoreCase)
|| string.Equals(toolName, "glob", StringComparison.OrdinalIgnoreCase)
|| string.Equals(toolName, "grep", StringComparison.OrdinalIgnoreCase)
|| state.TotalFilesRead >= 1;
}
if (state.Scope is ExplorationScope.RepoWide or ExplorationScope.OpenEnded)
return false;