diff --git a/agent/prompt/builder.py b/agent/prompt/builder.py index 168018c4..44a4eee9 100644 --- a/agent/prompt/builder.py +++ b/agent/prompt/builder.py @@ -179,7 +179,7 @@ def _build_tooling_section(tools: List[Any], language: str) -> List[str]: tool_map = {} tool_descriptions = { "read": "读取文件内容", - "write": "创建新文件或完全覆盖现有文件(会删除原内容!追加内容请用 edit)", + "write": "创建新文件或完全覆盖现有文件(会删除原内容!追加内容请用 edit)。注意:单次 write 内容不要超过 10KB,超大文件请分步创建", "edit": "精确编辑文件(追加、修改、删除部分内容)", "ls": "列出目录内容", "grep": "在文件中搜索内容", diff --git a/agent/protocol/agent_stream.py b/agent/protocol/agent_stream.py index 8289c15a..7ea1eeb1 100644 --- a/agent/protocol/agent_stream.py +++ b/agent/protocol/agent_stream.py @@ -78,12 +78,15 @@ class AgentStreamExecutor: args_str = json.dumps(args, sort_keys=True, ensure_ascii=False) return hashlib.md5(args_str.encode()).hexdigest()[:8] - def _check_consecutive_failures(self, tool_name: str, args: dict) -> tuple[bool, str]: + def _check_consecutive_failures(self, tool_name: str, args: dict) -> tuple[bool, str, bool]: """ Check if tool has failed too many times consecutively Returns: - (should_stop, reason) + (should_stop, reason, is_critical) + - should_stop: Whether to stop tool execution + - reason: Reason for stopping + - is_critical: Whether to abort entire conversation (True for 8+ failures) """ args_hash = self._hash_args(args) @@ -99,7 +102,7 @@ class AgentStreamExecutor: break # Different tool or args, stop counting if same_args_failures >= 3: - return True, f"Tool '{tool_name}' with same arguments failed {same_args_failures} times consecutively. Stopping to prevent infinite loop." + return True, f"工具 '{tool_name}' 使用相同参数连续失败 {same_args_failures} 次,停止执行以防止无限循环", False # Count consecutive failures for same tool (any args) same_tool_failures = 0 @@ -112,10 +115,15 @@ class AgentStreamExecutor: else: break # Different tool, stop counting - if same_tool_failures >= 6: - return True, f"Tool '{tool_name}' failed {same_tool_failures} times consecutively (with any arguments). Stopping to prevent infinite loop." + # Hard stop at 8 failures - abort with critical message + if same_tool_failures >= 8: + return True, f"抱歉,我没能完成这个任务。可能是我理解有误或者当前方法不太合适。\n\n建议你:\n• 换个方式描述需求试试\n• 把任务拆分成更小的步骤\n• 或者换个思路来解决", True - return False, "" + # Warning at 6 failures + if same_tool_failures >= 6: + return True, f"工具 '{tool_name}' 连续失败 {same_tool_failures} 次(使用不同参数),停止执行以防止无限循环", False + + return False, "", False def _record_tool_result(self, tool_name: str, args: dict, success: bool): """Record tool execution result for failure tracking""" @@ -227,6 +235,12 @@ class AgentStreamExecutor: result = self._execute_tool(tool_call) tool_results.append(result) + # Check for critical error - abort entire conversation + if result.get("status") == "critical_error": + logger.error(f"💥 检测到严重错误,终止对话") + final_response = result.get('result', '任务执行失败') + return final_response + # Log tool result in compact format status_emoji = "✅" if result.get("status") == "success" else "❌" result_data = result.get('result', '') @@ -467,15 +481,19 @@ class AgentStreamExecutor: try: arguments = json.loads(tc["arguments"]) if tc["arguments"] else {} except json.JSONDecodeError as e: - logger.error(f"Failed to parse tool arguments: {tc['arguments']}") + args_preview = tc['arguments'][:200] if len(tc['arguments']) > 200 else tc['arguments'] + logger.error(f"Failed to parse tool arguments for {tc['name']}") + logger.error(f"Arguments length: {len(tc['arguments'])} chars") + logger.error(f"Arguments preview: {args_preview}...") logger.error(f"JSON decode error: {e}") + # Return a clear error message to the LLM instead of empty dict # This helps the LLM understand what went wrong tool_calls.append({ "id": tc["id"], "name": tc["name"], "arguments": {}, - "_parse_error": f"Invalid JSON in tool arguments: {tc['arguments'][:100]}... Error: {str(e)}" + "_parse_error": f"Invalid JSON in tool arguments: {args_preview}... Error: {str(e)}. Tip: For large content, consider splitting into smaller chunks or using a different approach." }) continue @@ -558,16 +576,25 @@ class AgentStreamExecutor: return result # Check for consecutive failures (retry protection) - should_stop, stop_reason = self._check_consecutive_failures(tool_name, arguments) + should_stop, stop_reason, is_critical = self._check_consecutive_failures(tool_name, arguments) if should_stop: logger.error(f"🛑 {stop_reason}") self._record_tool_result(tool_name, arguments, False) - # 返回错误给 LLM,让它尝试其他方法 - result = { - "status": "error", - "result": f"{stop_reason}\n\nThis approach is not working. Please try a completely different method or ask the user for more information/clarification.", - "execution_time": 0 - } + + if is_critical: + # Critical failure - abort entire conversation + result = { + "status": "critical_error", + "result": stop_reason, + "execution_time": 0 + } + else: + # Normal failure - let LLM try different approach + result = { + "status": "error", + "result": f"{stop_reason}\n\n当前方法行不通,请尝试完全不同的方法或向用户询问更多信息。", + "execution_time": 0 + } return result self._emit_event("tool_execution_start", { diff --git a/agent/tools/write/write.py b/agent/tools/write/write.py index 9836564d..49e01c8f 100644 --- a/agent/tools/write/write.py +++ b/agent/tools/write/write.py @@ -14,7 +14,7 @@ class Write(BaseTool): """Tool for writing file content""" name: str = "write" - description: str = "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories." + description: str = "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories. IMPORTANT: Single write should not exceed 10KB. For large files, create a skeleton first, then use edit to add content in chunks." params: dict = { "type": "object",