-
Notifications
You must be signed in to change notification settings - Fork 9
fix: BUG-016 Agentic loop 轮数耗尽后无输出(safety net 增强 + fallback) #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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")) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // 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<ToolCallRecord>): String { | ||
| val toolNames = toolCalls.map { it.name }.distinct().joinToString(", ") | ||
| val toolCount = toolCalls.size | ||
|
|
||
| return """ | ||
| |你已经完成了 $toolCount 次工具调用,使用的工具包括:$toolNames。 | ||
| | | ||
| |请基于工具调用的结果,为用户提供一个完整、详细的回复。回复应该: | ||
| |1. 总结你通过工具发现或获取的关键信息 | ||
| |2. 直接回答用户的问题,而不是描述你做了什么 | ||
| |3. 如果信息不完整,明确说明你能确认的和不能确认的部分 | ||
| | | ||
| |如果无法基于工具调用结果给出回复,至少提供一个基于你的理解的合理回答。 | ||
| | | ||
| |请现在开始给出你的回复: | ||
| """.trimMargin() | ||
| } | ||
|
Comment on lines
+605
to
+621
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
|
||
| /** | ||
| * 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<ToolCallRecord>): 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") | ||
|
Comment on lines
+634
to
+641
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The val toolSummaries = toolCalls.map { call ->
val status = when (call.status) {
"complete" -> "成功"
"error" -> "失败"
else -> call.status
}
val safeName = call.name
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
"- ${safeName}: $status"
}.joinToString("\n") |
||
|
|
||
| return """ | ||
| |根据工具调用结果: | ||
| | | ||
| |$toolSummaries | ||
| | | ||
| |已执行完成所有操作。如果需要进一步分析或有其他问题,请重新提问。 | ||
| """.trimMargin() | ||
| } | ||
|
|
||
| private fun collectStreamResult(events: List<StreamEvent>): AgenticResult { | ||
| val text = StringBuilder() | ||
| val toolCalls = mutableListOf<ToolCallRecord>() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里存在一个严重的编译错误:变量
turn是在for循环内部定义的(第 255 行),在循环外部无法访问。此外,由于循环范围是1..MAX_AGENTIC_TURNS,条件turn > MAX_AGENTIC_TURNS在逻辑上也是永远不会成立的。建议在循环外部定义一个变量来记录是否达到了最大轮数,或者检查最后的stopReason是否仍为TOOL_USE。