hello all, this is an article I wrote up on my interaction with an agent, Claude, in fixing a bug in the hyperscript parser
it was a rather mundane bug, but i thought the interaction was interesting and worth analyzing to show where AI is very strong and where it is not as strong
I very much love your work Carson, it has always been and remain a fresh breath of air.
The example is mundane but to the point; and I very much enjoyed this article. It's a concrete example which is rare to read when it comes to using LLMs.
To the risk of being told that we "hold it wrong", it resonates with my experience of using LLMs.
maybe slightly unrelated but the new htmx homepage (https://four.htmx.org/) feels a little ironic, seemingly written with tailwindcss and a full JS ecosystem Astro build system. It also has the ‘vibey’ ‘hypey’ landing page design that’s hard to describe but you’ll find on any web framework, rather than dropping you to docs like the old site.
Compared to the original simple HTML site it’s really surprising to see from the grugbrain.dev author!
Interesting read! Creating tests is highlighted as something Claude did well, but it strikes me that all the weaker rejected solutions could have been avoided if it were really good at designing intelligent tests for itself. For example, the first solution “was very specific to the reported bug and wouldn’t have fixed the general case” and the third suggestion “prevented the perfectly valid use of as conversion expressions in go commands as well”. I imagine both of these cases could have been noticed and avoided by the agent if it had planned out adequate tests ahead of time.
This is kind of what coding with LLMs feels like. Gradually increase guard rails "outside of it's context (automated)" to get the results you want out of it. Static typing, quick compilation, not having nulls, and lints are a great start (I would also argue for managed side effects and functional, but to each their own).
It gets pretty far to the solution on it's own and quickly, but then you spend time adjacent to the problem, building out it's cage while iterating through the remainder of the solution.
As humans we have a concept of viscosity. That resistance, like being in quicksand or a swamp, is how you “easily” identify a code smell, something that needs to be refactored, etc. Part of it is human laziness, part of it some concept of elegance, an itch of being not quite tidy as it can be, etc.
LLM, being a tiresome little helper, will gladly output hundreds of lines, hacks, and what have you.
I don’t think any amount of tests, prompts, harnesses and other “my shaman is a better shaman” will help it to acquire this trait. Some other AI architecture someday maybe — just not today.
And that’s why it is good at what it is and really bad at stuff like code “design” (unless it is a well-known solution being baked in the training set)
I disagree with the trope -- (AI effects) "the slow dulling of our intellects". I am old enough to remember my career change, being a developer in the Apple ecosystem, confident with Objective-C and native system libraries in iOS and MacOS. I changed direction using a very different software stack in cloud services as a data engineer with deep utilization of Clojure. I have personal projects that I occasionally would return to in the former world -- often a decade or more later. I saw what I forgot immediately; but soon after, with engagement, I saw how quickly I was able to remember. Extended use of AI for me has exactly this footprint. Even "use it or lose it" is wrong -- "use it when you need to" is honestly more like it -- the brain is plastic. Some AI fears are warranted, this isn't one of them.
Carson’s experience matches mine: AI is good at analysis and boilerplate, but not good at the kind of critical thinking necessary for good designs. If it were human, I would say that it jumps to solutions to quickly, rather than stepping back to consider the big picture and how everything should fit together to make a cohesive whole.
It’s not human, of course, and I think this problem actually relates to the fact that LLMs don’t have a world model. They don’t study and think through a design in the way that humans do. They don’t form a mental model of how everything fits together and how that design can be tweaked to most elegantly support a change.
I suspect that this is a fundamental limitation of LLMs, and that design will remain a weak point until some sort of bespoke design AI is bolted onto the side. In the meantime, we’ve got a lot of people producing a lot of code very quickly, and I think the debt in that code is going to be a millstone around our necks for a long time to come.
I don't think this problem is related to the fact that they don't have a world model, or because they don't form a mental model of how everything fits together, or a fundamental limitation of LLMs. These claims are often meaningless, and the boring answer is usually something like "software architecture is harder to verify than code/maths so RLing on it is harder, and it's harder writing good evals/benchmarks for it".
The author admits that the logic of the language and the design of the parser are idiosyncratic. Even the solution the author likes is an extension of an existing hacky trap door. He could be more open-minded about the solutions the AI proposed and in fact, I think AI could potentially rearchitect this in a more structured, sustainable, and legible way.
Many developer criticism of AI coders could be easily directed at 95%+ of human developers. Much coding is monkey see, monkey do and keep trying until it does the things we want it to do. AI can certainly do that cheaper and faster and really this is why automated testing became such an important software discipline with or without AI.
It's a good write up, but it's lacking some details, the most important one is: which Claude model was used?
The second issue is: what was tooling and the prompt approach?
(To be clear, I have no problem with the premise of the write up. But without some details like this, it's sort of like saying "I had a bad board on my deck, and my tape measure wasn't able to help me remove the nails. What a bad tape measure."
> Technical debt, I assert without evidence1, grows exponentially, and therefpre it is very important to minimize it in your projects.
This actually seems like a really important idea absolutely deserving of its own blog post.
I'd have to think about the exact argument for why this feels so right, but the kernel would go something like this: whatever you build on those parts of the codebase where you have technical debt incurs new technical debt, because you're building on top of abstractions you'll remove later. The reason you have to remove the new abstractions, too, is that abstractions are like puzzle pieces: their structure determines which other abstractions they can connect with. So, as a rule (there are some exceptions), you can't take out one bad part, replace it with another, and leave everything around it untouched.
And, of course, it's easier to build on top of something creaky but currently serviceable than it would be to first rip that out and replace it, so that's what you do in most cases ... and the whole codebase gets more creaky and less serviceable; you increase the amount of abstractions you'd have to rip out and replace before building something new. The problem does, indeed, grow exponentially.
The argument is free to a good home -- I don't have the time for a full, meticulous elaboration, but I'd love to read one if someone is interested in making it.
I agree, it is an interesting thing to ponder. I often phrased it to myself that the cost of technical debt compounds the lower in the code stack you go.
Said another way, tech debt has a multiplicative factor the farther away from the end user you get. Tech debt in the database is worse than in the data layer. It is worse in the data layer than in the business logic. It is worse in the business logic than in the UI code. etc.
This is related to the fact that it gets exponentially more difficult to refactor code the farther away you get from the end user. Changing the database is usually more difficult and impacts more things than the data layer code. And on and on we go back up.
Ward Cunningham, who coined technical debt, describes it as having interest, which is exponential:
> Shipping first time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite.... The danger occurs when the debt is not repaid. Every minute spent on not-quite-right code counts as interest on that debt. Entire engineering organizations can be brought to a stand-still under the debt load of an unconsolidated implementation, object-oriented or otherwise.
24 comments
[ 4.4 ms ] story [ 41.0 ms ] threadit was a rather mundane bug, but i thought the interaction was interesting and worth analyzing to show where AI is very strong and where it is not as strong
The example is mundane but to the point; and I very much enjoyed this article. It's a concrete example which is rare to read when it comes to using LLMs.
To the risk of being told that we "hold it wrong", it resonates with my experience of using LLMs.
Compared to the original simple HTML site it’s really surprising to see from the grugbrain.dev author!
It gets pretty far to the solution on it's own and quickly, but then you spend time adjacent to the problem, building out it's cage while iterating through the remainder of the solution.
LLM, being a tiresome little helper, will gladly output hundreds of lines, hacks, and what have you.
I don’t think any amount of tests, prompts, harnesses and other “my shaman is a better shaman” will help it to acquire this trait. Some other AI architecture someday maybe — just not today.
And that’s why it is good at what it is and really bad at stuff like code “design” (unless it is a well-known solution being baked in the training set)
It’s not human, of course, and I think this problem actually relates to the fact that LLMs don’t have a world model. They don’t study and think through a design in the way that humans do. They don’t form a mental model of how everything fits together and how that design can be tweaked to most elegantly support a change.
I suspect that this is a fundamental limitation of LLMs, and that design will remain a weak point until some sort of bespoke design AI is bolted onto the side. In the meantime, we’ve got a lot of people producing a lot of code very quickly, and I think the debt in that code is going to be a millstone around our necks for a long time to come.
Many developer criticism of AI coders could be easily directed at 95%+ of human developers. Much coding is monkey see, monkey do and keep trying until it does the things we want it to do. AI can certainly do that cheaper and faster and really this is why automated testing became such an important software discipline with or without AI.
The second issue is: what was tooling and the prompt approach?
(To be clear, I have no problem with the premise of the write up. But without some details like this, it's sort of like saying "I had a bad board on my deck, and my tape measure wasn't able to help me remove the nails. What a bad tape measure."
Shameless plug: https://open.substack.com/pub/deimos28/p/the-friction-collap...
i tried it before with sonnet and the results weren't very good
went back to react
This actually seems like a really important idea absolutely deserving of its own blog post.
I'd have to think about the exact argument for why this feels so right, but the kernel would go something like this: whatever you build on those parts of the codebase where you have technical debt incurs new technical debt, because you're building on top of abstractions you'll remove later. The reason you have to remove the new abstractions, too, is that abstractions are like puzzle pieces: their structure determines which other abstractions they can connect with. So, as a rule (there are some exceptions), you can't take out one bad part, replace it with another, and leave everything around it untouched.
And, of course, it's easier to build on top of something creaky but currently serviceable than it would be to first rip that out and replace it, so that's what you do in most cases ... and the whole codebase gets more creaky and less serviceable; you increase the amount of abstractions you'd have to rip out and replace before building something new. The problem does, indeed, grow exponentially.
The argument is free to a good home -- I don't have the time for a full, meticulous elaboration, but I'd love to read one if someone is interested in making it.
Said another way, tech debt has a multiplicative factor the farther away from the end user you get. Tech debt in the database is worse than in the data layer. It is worse in the data layer than in the business logic. It is worse in the business logic than in the UI code. etc.
This is related to the fact that it gets exponentially more difficult to refactor code the farther away you get from the end user. Changing the database is usually more difficult and impacts more things than the data layer code. And on and on we go back up.
> Shipping first time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite.... The danger occurs when the debt is not repaid. Every minute spent on not-quite-right code counts as interest on that debt. Entire engineering organizations can be brought to a stand-still under the debt load of an unconsolidated implementation, object-oriented or otherwise.