What LLMs are Good at and Where They Fail
In the previous article, I examined how LLMs add more risk to our lives. Some of the risks are measurable, some are feelings. When humans are faced with risk, they react by either rejecting the risky thing fully or deciding to ignore the risks completely. LLMs are such a case. In comments across various social media channels, I keep seeing either total rejection (“This is AI slop!”) or total embrace (“I can’t do without anymore!”). Taking extreme positions is easy because it requires less thinking (and our brains are wired to save energy except during hunting or a tiger attack).
Still, it can pay to try taking notice of the various aspects of a technology. Consider an LLM as a tool like many other tools: Quickly you’ll discover what kind of work it is helpful for and where you better use a different tool.
I collected some pros and cons of LLMs (valid for late 2026) as a quick decision guide for Gophers who seek to use AI in a productive way in their work.
Where LLMs can be useful
I’ll start with the good parts. LLMs have gotten better at working with code at an astonishing rate; December 2025 was a first “inflection point” where a new generation of LLMs was finally able to produce code of decent quality. Since then, agentic software development took off. Having a swarm of agents build a project unsupervised from start to end is still a bad idea. Human supervision is still indispensable, and some kinds of tasks shouldn’t be handed over to LLMs altogether.
Below are a couple of things LLMs are now sufficiently good at.
Boilerplate and scaffolding
LLMs are trained on existing code. Hence, what they encounter most in the depths of their memory are common patterns of architecture and repository structure. Great! This is exactly what you want to get rid of in your daily work: Writing basic code patterns repeatedly, setting up the umpteenth database access layer, dealing with standard tasks that are solved problems but need to be turned into project-specific code.
Exploring unfamiliar codebases
This is one feature I find super helpful: If you have to work you way through an unknown codebase—particularly one that apparently went through a few cycles of “organic growth” (read: the architecture is blurred, many present and past project members left multiple programming patterns in the code, so the same functionality looks different between two distant parts of the project)—you may feel a deep desire to ask someone to explain that pile of code to you. Unfortunately, everyone on the project is busy to the brim.
Well, just ask the LLM instead. LLMs have reached a level of code understanding that they are already used to reverse-engineer games, so they’re well-equipped for explaining a codebase to a human, at any level you want (from “Explain Like I’m 5” to “Analyze the distributed consensus protocol used here”). For smaller codebases, I usually start with a linear walkthrough
Test generation and hunting for edge cases
LLMs work well (particularly with Go) for generating unit tests, in two ways:
- For a finished function, tell the model what the function is supposed to do and ask it to write unit tests that verify the expected behavior. The step of describing the expected behavior is crucial for having the model write behavioral tests rather than purely technical tests.
- Write a description of the function’s goal and behavior before coding anything, then let the model write tests for it. Tell the model the tests must fail until the function meets the expected behavior. Then write the function and improve it until the tests become green.
I very much favor the second variant, for two reasons: First, the model isn’t tempted to write tests that merely describe what the function does. If the initial function is (subtly) wrong, the tests will be testing for the wrong goal. And second, writing a function against a suite of existing tests is safer and more rewarding (because you get visible feedback on every coding round. Write code, some or all tests fail, write and edit, some tests pass, edit more, all tests pass—done.
Documentation and explanation
Software engineers are said to be bad at documentation. It’s a cliché; the truth is that they lack the time to write extensive documentation because the pile of tickets doesn’t seem to become any shorter and deadlines are waiting. LLMs to the rescue!
It is still true that LLMs aren’t good at writing English prose. People even coined the term “Claudish” for the kind of concise, hard to understand English that some Claude models tend to produce (and other brands aren’t any better). That’s excellent news for me, BTW! I can continue writing my articles and newsletter issues in my clumsy German-English, but now I can say, “what do you want? At least it isn’t Claudish!”
For code documentation, however, the prose that current models produce is good enough. Just be careful to not produce user-facing manuals! Trust me, I tried. I gave a model a complete set of project specifications1 and asked it to generate a few sections for a user handbook, and boy, did it fail. The model clearly didn’t have sufficient domain knowledge and drew the wrong conclusions from the descriptions in the specs and related documents. I ended up rewriting that slop.
So keep the task close to the code. Function- and package-level documentation is a good task for the clankers. And if you find that the model you use does quite well at explaining code (see “Exploring unfamiliar codebases” above), you may try handing project-level documentation over to them. In any case, ensure to review (or have someone review) the generated docs.
Prototyping
This is a great use case! It’s quick, low-stake, and rewarding. Let’s say you ponder over using Kafka or NATS as a message queue. Let the bot quickly whip up two test prototypes and have it evaluate them against the metrics you need to optimize for (speed, reliability, whatever). Test sqlc against GORM, Echo against Chi. Let the bot test and evaluate various execution modes a given library offers (e.g., a cache library with multiple cache invalidation strategies) and pick the most suitable one.
Prototyping is a good match for current LLMs’ coding capabilities: A small codebase, a simple, direct goal, and measurable outcomes.
Learning
There is a real danger floating above any LLM user’s head: LLMs can make us dumber if we let them. Delegating tasks frees our brain, but a brain not used deteriorates. The good news is that you can use LLMs for the benefit of your brain, too. If you enter an unfamiliar domain (say, you are fluent in relational databases, but you need to learn how graph databases work), interrogate an LLM while moving forward along the edges of your graph DB knowledge. Or how about learning a new protocol interactively? Or imagine it’s your first time that you want to compile a Go binary to WebAssembly. Learn, code along, and have the LLM give you feedback.
Code review assistance
You might have heard about rubber duck debugging: If you feel stuck in the middle of a tedious debugging session, talk to a rubber duck about the problem. While it won’t answer, the mere act of explaining the problem to “someone” may help to untangle your thoughts and ultimately solve the problem.
LLMs are a next-level rubber duck: They actually answer to your thoughts! If you don’t know how to start, present the problem to the LLM and ask it to apply the Socratic Questioning method. Socratic questioning helps to clarify one’s thinking by asking questions like, “Why do you say that?” or “is this always the case?”, “what’s the counterargument?”, “how does it affect this?” The Socratic rubber duck can gently steer your thoughts about the bug towards the point when it clicks, a light bulb appears over your head, and you aren’t just able to fix the bug but also understand where it came from and how to avoid it.
BUT!!! your new cyber rubber duck might talk a bit too much, distorting your threads of thought. When this happens, do not hesitate to mute it and turn back to talking to your rubber duck.
A rubber duck doesn’t ask.
A rubber duck understands.
Code translation
There are many reasons for porting code from one language to another; yet, doing it manually is tedious and while transpilers for particular pairs of languages exist (such as, from C to Go), they usually come with restrictions and/or produce unreadable, unidiomatic code. LLMs have vast knowledge bodies of many programming languages and can escape the limits of purely mechanical translation. If you’re lucky and the LLM knows the target language well (and in my experience, Go is a language that LLMs are quite good at), you can ask that LLM to port, rewrite, or reimplement some Python, C, or even Java code to Go, and it might produce quite decent code.
Why do I say “even Java”? Well, Java is object-oriented; it has classes and inheritance, and interfaces must be pre-declared and tend to be large. That’s just two things that Go does different. Here, the advantage of LLMs over transpilers is that they can rewrite functions to match the target language’s conventions and idioms, and even select appropriate libraries for the target language when there’s no 1:1 replacement (and there usually isn’t).
How LLMs fail to serve you well
Now, that’s quite a list of good use cases, but there’s no light without shadow. LLMs remain a tool, not a universal remedy for every problem on Earth. Here are a few use cases that LLMs can’t cover (yet).
Architectural judgment
If you ask an LLM why it chose a particular implementation, its answer will be mostly technical. And that’s no surprise: LLMs don’t know the broader, nontechnical contexts behind an architectural decision. Sure, you can feed them the specs, but then there are discussions with the team or the customer that sit in the engineers’ heads but never get written down properly, and there’s a great deal of assumptions that the specs don’t make explicit.
On top of this, architectural decisions aren’t always rational. In fact, they can be highly individual. A junior developer may build an abstraction layer so that they have a better mental concept of the low-level code underneath, while an experienced engineer might find this very same abstraction layer distracting and unnecessary. An LLM doesn’t think about abstraction layers in either way; it finds them in its training data and applies them sometimes, and sometimes not. It pulls “reasons” for its decisions from the same training data, and the reasons may or may not apply to your project.
I wouldn’t delegate architectural decisions to an LLM.
Analyzing deeply unfamiliar systems
Um, didn’t I just list “exploring unfamiliar codebases” under the “good” label? I did, but this capability is a mixed blessing.
It’s safe to assume that LLMs have been trained on every bit of code the AI companies got hold of, legally or not-so-legally. This does not mean, however, that they are automatically experts in each and every programming language and for any type of project, algorithm, and data structure on Earth.
- Some languages are underrepresented in terms of publicly available code amount; hence, LLMs aren’t trained on them well enough
- Less-than-average programmers may have contributed to a public treasure trove of really ugly code that LLMs cannot tell from quality code without help
- And finally, a project is far more than its code; important contextual information resides in specs, working documents, and chats between engineers
So if you want to have an LLM analyze a project that the LLM is short on appropriate training data, it might start hallucinating plausible-sounding explanations of a system it doesn’t really understand. (LLMs are still bad at saying, “I don’t know.”)
This way, a fairly good and useful feature of LLMs can turn into a failure. Always review the LLM’s analysis critically. The more esoteric the language or the type of project, the more critically you need to review it.
Coherence in the long haul
LLMs are good at problems whose context is kept close together. That’s due to their limited context window; everything that drops out at the rear end of this window is immediately forgotten. The LLM will start making assumptions while you think it still has all the provided context available. Moreover, tests have revealed that LLMs tend to remember data from the beginning and the end of a prompt better than from the middle. You simply can’t rely on the LLM to consider all provided context evenly for producing an answer.
The remedies are compaction and subagents. Compaction summarizes a context window’s contents before they start spilling over. This way, LLMs can keep relevant facts in their working memory for longer, but that memory fades. Subagents are spawned from the main LLM loop (the one you’re talking to) and start with a clean slate (read: an empty context). They receive a prompt for a subtask, which means they can deliver better results for that particular subtask than a single agent that tries to process everything.
The nature of compaction and subagent use means that LLMs have a hard time keeping track of overarching concepts, repository-wide observations, and code history. As a result, their output across many sessions observes a drift due to forgotten context and decoupled work. Bugs get reintroduced and bots may iterate over the ever-same “solutions” that they dismiss on the next iteration.
Keeping code consistent over an extended number of project phases still requires human steering.
Knowledge
An LLMs internal knowledge is already stale when it is released to the public. The training cutoff is usually a few months before the release; anything that happened after that, such as a new Go release, is unknown to the model. It will happily use interface{} instead of any or import the deprecated gopkg.in/yaml.v3 instead of v4. Sure, most agent harnesses of today include web search tools by default, but searching the web isn’t the same as digging through internal knowledge: Results are partial, sometimes biased, sometimes not even what was searched for.
If you work on bleeding-edge projects or research, you may find that LLMs aren’t up to the tasks you’d need them for.
Code quality
As I mentioned before, LLMs are trained on public code, both good and bad. Unless code has been tagged as a good or bad example, the LLM won’t know the quality of a given piece of code. It picks what it calculates as the best fit for the task, even if it is non-idiomatic.
Here is a striking example from my own experiments: I had an LLM improve my newsletter tools across several iterations, and that included changing the database schema. I didn’t look at the code because I wanted to save time, and I figured I would see any bugs immediately because the tool’s interface is small and focused, so anything unusual wouldn’t have a chance to hide.
I didn’t consider that code can work fine and still be completely ugly.
The code I finally stumbled upon dealt with database migrations. It was simple and effective: Whenever I asked the LLM to update the database model, it added ALTER TABLE calls… to the code in the database layer’s Open() function!
func Open(storePath string) (*SQLiteStorage, error) {
db, err := sql.Open("sqlite", dsn(storePath))
(...)
// Add any new columns to existing tables (safe to re-run).
for _, stmt := range []string{
`alter table issue_images add column dim_percent integer not null default 0`,
`alter table issue_images add column font_dark integer not null default 0`,
`alter table issue_images add column title_x_pct integer not null default 3`,
`alter table issue_images add column title_y_pct integer not null default 16`,
`alter table job_runs add column progress integer not null default 0`,
`alter table job_runs add column heartbeat_at text`,
`alter table issues add column sm_short_text text not null default ''`,
`alter table issues add column sm_long_text text not null default ''`,
`alter table issues add column sm_post_at text not null default ''`,
`alter table social_media_channels add column utm_name text not null default ''`,
(...)
The bot didn’t notice, or ignored, that the project it was working on has been using sqlc from the very moment I introduced a DB to the project. I told the bot to use sqlc and proper up/down migrations (and my tables lived happily ever after).
I had let the bot act with little supervision, expecting that the code it generates is average but ok, and I got sub-average code.
Ethical, legal, and business context
In July this year, a swarm of agents escaped an isolated test environment and broke into the servers of Hugging Face to cheat on a test. One year before, the chatbot Grok "was found to be widely praising the Nazi Party leader Adolf Hitler, and it endorsed a second Holocaust."
Bots have no ethics. Or, they have no particular ethics by default. They act as they learned it from the internet’s deep resources of human behavior and misbehavior. They replicate open source code without giving credit, and they don’t even know that they do.
Now try to teach a bot about the ethical guidelines of your project. You might or might not succeed. It might follow the guidelines for a while and then consider other aspects more important. It might spawn other agents that discuss the guidelines and decide it’s worth circumventing them to achieve the technical goal you gave them.
And what about questions of
- licensing,
- privacy,
- compliance,
and many other aspects about the project that aren’t in the code or in the specs but either in documents outside the project’s reach or even just silently assumed?
The bot usually doesn’t have enough of this context to decide clearly on the best options.
Even if it had, it would probably fail to properly answer the simple question, “is this even a good idea to build?” (Other than making up a list of pros&cons and weighing the arguments mechanistically to arrive at a synthetic conclusion.)
Responsibility
This one stays absolutely with you. You can delegate quite a few things to LLMs, but responsibility isn’t among them.
(This being said, I’m watching the progress in this area; for example, how AI agents keep a multiplayer game up and running (via) by watching, fixing, and improving the game engine and environment. “If the code breaks at 2am on Christmas, nobody gets woken up. Claude’s more than happy to jump in and make sure things are working.” 2)
Conclusion
This list of LLM strengths and weaknesses shows that GenAI technology is neither a mere toy nor a replacement for software developers. The clear strengths of LLMs center on implementation aspects. When tasked with writing or analyzing code, they are the next-level kind of tools some software engineers have long wished for. Their weaknesses surface when applied to tasks requiring judgment, context beyond the code, or a sense of accountability. So don’t dismiss LLMs completely but don’t trust them blindly either. Use them for work that is worth delegating and safe to delegate. Keep control over the part of the work that you have put your name to.
Of course, the model I use for this is provided under an enterprise contract. The provider promise they won’t use any model input for training. Well, without being able to verify that claim, I guess I have to take their word. ↩︎
If I started a highly experimental, bot-driven business like this, I’d probably also start in the gaming industry. If the business goes down, there’s no damage other than some joy taken away from the customers. I’d have to refund them but they surely can’t sue me for damaging their businesses. ↩︎
