You mentioned abstracting your test code from the framework. The page object pattern is one way that this can be done and is fairly common. It is popular with Selenium but can be applied to any UI automation: http://code.google.com/p/selenium/wiki/PageObjects.
You can avoid using xpath that contains the entire path (or even part of the path) to the UI elements so that if parent or ancestor elements change or are moved that your test will still work. You can identify elements by ID's so that even if the element type or class or other attributes change your automation won't break.
You can also educate developers about how their changes impact the automation. One thing I have noticed is that junior developers (or developers who are not used to much QA or process) will make way more unnecessary changes to UI than are normally needed. They should know to give all of the important elements ID's, to not change the ID's and to not change the basic behavior of those elements unless it is strictly necessary for new feature work. One good way to educate developers is to have them run automation prior to checking in new code. I guarantee if they were the ones who had to update the automation when they changed the UI they would get smart about not changing the UI so much.
Another thing that I have seen make UI automation unreliable is timing issues. Many people use explicit waits for elements, or just assume that the element is loaded and functional. I like to implicitly wait for any element to appear on a page which means if I try to click on the element, I will first poll for the element to make sure it exists, then once I know it exists attempt to click on it. I have done this automatically in my abstraction layer on top of Selenium so it will always wait until the element exists first.