diff --git a/docs/buglist.md b/docs/buglist.md index a352a2d..7bc6bda 100644 --- a/docs/buglist.md +++ b/docs/buglist.md @@ -24,7 +24,7 @@ | BUG-013 | P1 | ✅ 已修复 | 刷新页面后 AI 不记得对话历史(sessionId 未持久化) | | BUG-014 | P2 | ✅ 已修复 | ContextPicker 抢焦点,按 `@` 后无法在主输入框继续输入 | | BUG-015 | P2 | ✅ 已修复 | @设计/@测试 等 Profile 标签被 ContextPicker 拦截,无法路由到后端 | -| BUG-016 | P2 | ⏸ 挂起 | Agentic loop 8 轮耗尽后 AI 无文字输出(safety net 未生效) | +| BUG-016 | P2 | ✅ 已修复 | Agentic loop 轮数耗尽后 AI 无文字输出(safety net 增强 + fallback 机制) | | BUG-017 | P1 | ✅ 已修复 | Knowledge Services 页面白屏崩溃(ServiceType/ServiceStatus 枚举大小写) | | BUG-018 | P2 | ✅ 已修复 | Context Picker Knowledge tab 无内容(空字符串未 fallback 到通配搜索) | | BUG-019 | P2 | ✅ 已修复 | 代码块 Apply/Copy 按钮不可见(CSS opacity-0 隐藏) | @@ -153,15 +153,17 @@ 2. `AiChatSidebar.tsx`: `handleContextSelect` 判断 `item.type === "profile"` 时,将 `@标签名 ` 插入到输入框文本前缀,而非添加为 context attachment。这样 `@设计` 等标签保留在消息文本中,后端 ProfileRouter 可以正常检测 - **文件**: `ContextPicker.tsx`, `AiChatSidebar.tsx` -### BUG-016: Agentic loop 耗尽后无文字输出(⏸ 挂起) +### BUG-016: Agentic loop 耗尽后无文字输出(✅ 已修复) - **发现**: Session 16, Phase 1.6 验收测试 TC-2.3 - **症状**: `@设计 帮我看下这个系统的架构`,AI 调了 8 轮工具(workspace_list_files, workspace_read_file, search_knowledge 等),全部 stopReason=TOOL_USE,最终无文字总结输出 -- **已尝试修复**: - 1. MAX_AGENTIC_TURNS 5 → 8(仍不够) - 2. Safety net:轮数耗尽后注入 user message + 无工具最终轮 → 产出 0 chars,未生效 -- **根因推测**: Claude API 在长工具链后的空工具调用可能不产生 ContentDelta;或 conversation 过长导致模型输出为空 -- **状态**: ⏸ 挂起,后续排查 -- **文件**: `ClaudeAgentService.kt` +- **根因**: 原 safety net 触发条件为 `finalContent.isBlank()`,但: + 1. 当 `stopReason == TOOL_USE` 但 `currentToolUses` 为空时,循环直接 break,safety net 没机会检查 + 2. Summary turn 本身可能因对话历史过长而不产生 ContentDelta +- **修复**: + 1. 扩展 safety net 触发条件:`finalContent.isBlank() && allToolCalls.isNotEmpty()` 或 `turn > MAX_AGENTIC_TURNS` + 2. 增强 summary prompt:包含工具调用摘要和明确的指令 + 3. 添加 fallback 机制:若 summary turn 仍为空,基于 `allToolCalls` 生成合成回复 +- **文件**: `web-ide/backend/src/main/kotlin/com/forge/webide/service/ClaudeAgentService.kt` ### BUG-017: Knowledge Services 页面白屏崩溃 - **发现**: Session 16, Phase 1.6 验收测试 TC-5.4 @@ -202,8 +204,8 @@ ## 统计 - **总计**: 20 个 Bug -- **已修复**: 19 个 -- **挂起**: 1 个 (BUG-016) +- **已修复**: 20 个 +- **挂起**: 0 个 - **P0 (阻塞)**: 2 个 (BUG-008, BUG-012) - **P1 (严重)**: 4 个 (BUG-001, BUG-005, BUG-013, BUG-017) - **P2 (一般)**: 14 个 diff --git a/web-ide/backend/src/main/kotlin/com/forge/webide/service/ClaudeAgentService.kt b/web-ide/backend/src/main/kotlin/com/forge/webide/service/ClaudeAgentService.kt index c0a5000..0ff863f 100644 --- a/web-ide/backend/src/main/kotlin/com/forge/webide/service/ClaudeAgentService.kt +++ b/web-ide/backend/src/main/kotlin/com/forge/webide/service/ClaudeAgentService.kt @@ -427,16 +427,19 @@ class ClaudeAgentService( break } - // Safety net: if all turns exhausted and AI still wanted to use tools, + // Safety net: if all turns exhausted or AI didn't produce text output, // inject a user message requesting summary and do one final turn WITHOUT tools - if (finalContent.isBlank()) { - logger.info("All $MAX_AGENTIC_TURNS turns exhausted with tool_use. Forcing final summary turn.") + val needsSummaryTurn = finalContent.isBlank() && allToolCalls.isNotEmpty() + if (needsSummaryTurn || turn > MAX_AGENTIC_TURNS) { + val reason = if (finalContent.isBlank()) "AI produced no text output" else "Max turns exhausted" + logger.info("Safety net triggered: $reason. Forcing final summary turn. allToolCalls=${allToolCalls.size}") onEvent(mapOf("type" to "ooda_phase", "phase" to "complete")) // Add a user message to explicitly instruct the AI to summarize + // Enhanced prompt with multiple fallback options currentMessages.add(Message( role = Message.Role.USER, - content = "你已经收集了足够的信息。请基于以上工具调用的结果,直接给出完整、详细的回复。不要再调用任何工具。" + content = buildSummaryPrompt(allToolCalls) )) var summaryText = StringBuilder() @@ -451,6 +454,13 @@ class ClaudeAgentService( } finalContent = summaryText.toString() logger.debug("Summary turn produced ${finalContent.length} chars") + + // If still no content, generate a fallback response based on tool calls + if (finalContent.isBlank()) { + logger.warn("Summary turn produced no content. Generating fallback response based on ${allToolCalls.size} tool calls.") + finalContent = generateFallbackFromToolCalls(allToolCalls) + onEvent(mapOf("type" to "content", "content" to finalContent)) + } } return AgenticResult(content = finalContent, toolCalls = allToolCalls) @@ -584,6 +594,61 @@ class ClaudeAgentService( """.trimMargin() } + /** + * Build an enhanced summary prompt for the safety net turn. + * + * The prompt is designed to: + * 1. Force the AI to produce text output + * 2. Provide clear instructions for summarizing tool results + * 3. Offer fallback options if summarization is not possible + */ + private fun buildSummaryPrompt(toolCalls: List): String { + val toolNames = toolCalls.map { it.name }.distinct().joinToString(", ") + val toolCount = toolCalls.size + + return """ + |你已经完成了 $toolCount 次工具调用,使用的工具包括:$toolNames。 + | + |请基于工具调用的结果,为用户提供一个完整、详细的回复。回复应该: + |1. 总结你通过工具发现或获取的关键信息 + |2. 直接回答用户的问题,而不是描述你做了什么 + |3. 如果信息不完整,明确说明你能确认的和不能确认的部分 + | + |如果无法基于工具调用结果给出回复,至少提供一个基于你的理解的合理回答。 + | + |请现在开始给出你的回复: + """.trimMargin() + } + + /** + * Generate a fallback response based on tool call results. + * + * This is used when the safety net summary turn also produces no content, + * which can happen in edge cases with long conversation histories. + */ + private fun generateFallbackFromToolCalls(toolCalls: List): String { + if (toolCalls.isEmpty()) { + return "已完成工具调用,但未能生成回复。" + } + + val toolSummaries = toolCalls.map { call -> + val status = when (call.status) { + "complete" -> "成功" + "error" -> "失败" + else -> call.status + } + "- ${call.name}: $status" + }.joinToString("\n") + + return """ + |根据工具调用结果: + | + |$toolSummaries + | + |已执行完成所有操作。如果需要进一步分析或有其他问题,请重新提问。 + """.trimMargin() + } + private fun collectStreamResult(events: List): AgenticResult { val text = StringBuilder() val toolCalls = mutableListOf()