Issue #13
The Wrong Way to Code Your Projects Using LLMs
Any tool or framework looks its best when we use it for the first time. Be it trying out Haskell, getting your hands dirty with Rust, or even firing up OpenCode and hooking it up to your GLM Coding Plan subscription.
Any tool or framework looks its best when we use it for the first time. Be it trying out Haskell, getting your hands dirty with Rust, or even firing up OpenCode and hooking it up to your GLM Coding Plan subscription.
As an engineer, I saw a lot of potential in coding agents. As programmers, we’ve built scaffolds, code generators, and even complex abstractions to avoid writing the same code twice. Early on, I remember saving code snippets in files so I could reuse them later. Many of us still do this.
LLMs made those scaffolds powerful because you’re no longer tied to a rigid framework. Moreover, you don’t have to fight the language by trying to turn everything into a framework or a library. And LLMs are very fast at churning out code, which is a bliss compared to handwriting a bunch of boilerplate.
But all that looks wonderful when you first do it. Try repeating it, and you’re in for trouble. And that’s what we’re going to talk about today.
The Website Incident
I used LLMs to code the website for my book, How AI Thinks. It’s here at HowAIThinks.dev. Believe me, it did not come out this way from scratch.
Instead, I had to create a mock in Illustrator, code some initial parts of the design token and directory structures, and only then did I let the LLM add to the codebase. This was based on several prior experiences of trying to start from scratch and having the LLM go all over the place.
After that, within the constraints of the codebase, the LLM’s code was pretty much standard. (I used Cursor with Composer instead of GLM 5.2 because the latter can’t read image files.)
But I did have to nudge the LLM in the right direction every once in a while, or even throw away changes it made and start over.
That’s when I stumbled upon an important fact about using LLMs.
Changing how you approach each project just for the sake of it adds cognitive load. And the LLM always does the same thing in different ways for the sake of it (temperature).
For example, if I start another project that needs a similar kind of website, I would have to repeat the whole process with the LLM. I would also have to correct the same mistakes it happily makes again in this new codebase. And if the model version updates because a new version came out, the behavior can sometimes completely change in these narrow use cases.
In short, if we just use it project to project, it’s going to do something different each time and will also repeat most of the same mistakes. It’s a lose-lose if you’re working on many projects.
The Template Approach
In the “olden days,” many engineers would save starter projects, such as a base project with authentication and basic database models, from which they would start. This was true even as frameworks became stronger, because the boilerplate simply appeared at a higher level of abstraction.
For example, a data scientist might have a project that’s copied each time a new project has to be started, with the new data replaced and local adjustments made to the codebase as needed. This helped override changes without having to contribute back to some “base framework.”
This allows engineers not to waste time making the same decisions again and again, which is a huge deal.
The Template Approach with LLMs
Now, the wrong way to code with LLMs is to always start from scratch, to always start with an empty repository.
Well, when you’re just starting out, that’s exactly how you might do it. But the general instinct is to standardize “instructions” so that things are reproducible. Instructions are often extremely painful to tune, especially when you’re creating something highly complex. And you find yourself fighting the LLM on every new project, trying to get it to do what you’ve done hundreds of times already.
Instead, there’s another approach. We can use the “Template Approach” with LLMs as well.
When I had to create another website with a similar style to my How AI Thinks site, I copied the existing site’s folder, added a new spec document, and told Cursor that I wanted to create a new project based on the existing ones, but with the new specs. It could delete files it no longer needed and add what it did. For the rest, it got the context of all my decisions without me having to be very explicit with instructions.
Compared with starting from scratch, I saw the following benefits:
-
The typography was perfect. Even when images are provided, the LLM does not get the rhythm right and always does something different. It would add Serif when only Sans-serif existed in the screenshot.
-
Also, the LLM didn’t toggle random properties that make the font render differently, which you might have to go back and fix.
-
Certain tricky aspects, like navigation, where the LLM almost always over-engineers what needs to be done rather than properly using grids and aligning everything vertically, don’t have to be repeated by me.
-
Certain components, even if not used, served as references for new components that had to be created. This saved me a huge amount of time in specifying exactly what I’m looking for.
-
The plan and the code did not deviate. Otherwise, the LLM would plan for things I never asked for or do it in a manner that is not in the spirit of what I want.
In short, an existing project plus great prompts = great new projects.
Collection of Decisions
Just as we say that life is the sum of our decisions, so is a repository the sum of our decisions. Instead of repeating those decisions, it’s easier to just give the LLM the repository with a smattering of the right prompts for it to infer.
That’s why I think the older practice of maintaining a collection of reference repositories is going to be a big thing again. You can just give them as a stable starting point for the LLM rather than starting from scratch.
That’s also why software engineers have a distinctive advantage over non-tech folks. When you’re non-technical, you only try to build that one project. And when it comes to another project, you start from scratch. However, engineers often build reusable blocks rather than the final software directly. And that makes subsequent projects both faster and gives room to be more ambitious.
Conclusion
In short, using LLMs on your personal collection of starter repos can significantly improve the reproducibility of your creations. Otherwise, you’ll find yourself repeating yourself and correcting the same types of mistakes.
The wrong way is to always start from scratch. The right way is to use past artifacts to make future ones easier to build.
That’s all for today. See you in Issue 14!
Get the next issue in your inbox
If you want technical depth, applicability, fun, and profit all at once but have never found them together before, The Invariant is the place to be.
Subscribe today if you have not already.
Free. No spam. Unsubscribe anytime.
