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:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user