Previously, I’ve written that Schrödinger’s LLMs are both overly impressive and underwhelming. This three-part series cuts through the noise to separate what’s genuinely valuable from what’s mostly just hype. In the first post, I share four practical, low-risk ways I incorporate LLMs into my daily routine while pointing out their limitations.
Accelerating With Code Completion
One of the least controversial ways I use LLMs is for code completion. From predictive text on phones to Language Server Protocols (LSP), we’ve been primed for this workflow since well before the existence of LLMs. It feels intuitive, and the experience is quite satisfying when it seems to read your mind. As one of the first useful applications of LLMs, code completion has rightly received a lot of attention. Consequently, many models now exist that execute this well.
Deceptively simple extensions to the basic workflow like multi-line edit and tab jump (made famous by Cursor) make it even more useful. I remember how eye-opening these features were the first time I encountered them. The tedium of adding docstrings to functions instantly vanished. Adding test cases was a simple tab away. Forgetting to update that one last change no longer happened. It’s hard to unsee how useful this workflow can be. When not available, it’s painfully obvious just what is missing. Adding comments and having relevant files open in your IDE can elicit better multi-line prediction. I find it’s generally not worth the added effort: the baseline performance is already superb.
I get the most out of code completion when I’m working on low-complexity, repetitive, or boilerplate code. That’s not to say it can’t work in other instances, but I’m much more likely to accept suggestions when the stakes are low and there’s a clear pattern that’s easy to verify. When things get intricate, I’m more inclined to just open the chat interface to explore the solution a bit more. (Un)luckily, much of development involves the low-complexity code that completions are great for.
And yet, code completion is not a panacea. Sometimes an LSP is just better. The most glaring example of this is for package imports. LLMs are frighteningly good at importing plausible-sounding modules. However, unlike writing new code, there’s no room for creativity with imports. Whenever I accept LLM predictions for imports, I inevitably have to go back and fix something. LSPs, on the other hand, are the perfect tool for this. They are completely deterministic and only suggest existing modules. Similarly, LLMs are great at suggesting methods that sound like they should exist on your classes. In contrast, LSPs offer a superior discoverability workflow by listing only valid, existing methods.
One tip to make using both tools easier: change the shortcuts for accepting suggestions. Tab is overloaded in most IDEs. If there is both an LLM completion and an LSP completion, Tab will use the LLM suggestion by default. By creating separate shortcuts, you can easily control which suggestion to incorporate and prevent the frustration of accidentally accepting the wrong one.
Squashing Bugs
There’s usually a gap between generated code and production code. LLMs can help close that gap. For popular languages like Python, many errors no longer require a trip to Stack Overflow. In the past, if your exact error trace didn’t show up in search results, you needed to abstract the issue you were facing to search more generally. With modern LLMs, simply copying and pasting the error trace is enough to get you back on track. For trivial or simple bugs, this is great and a huge speed-up: no need to leave the IDE, context switch, or engage in deep problem-solving.
At the same time, there’s value in periodically thinking deeply and banging your head against the wall. If you stop thinking critically about why things are broken, you may find yourself giving up sooner and be even more lost when an LLM can’t fix your small-batch, artisanal bugs. To combat this failure mode, I make a point to either confirm I’m familiar with the technique, or I spend extra time to fully understand the solution.
For hairy bugs, I’ve found two maxims particularly useful: keep it simple and context is king. The longer the LLM or agent session is, the more needlessly complex the solutions become. LLMs tend to solve bugs iteratively, and when the first solution doesn’t work, they add special conditions instead of starting over. Without oversight, this leads to brittle and unmaintainable code. To keep it simple, I ruthlessly cut unneeded parts. I challenge the LLM to simplify its recommendations and question what is absolutely necessary.
The context you provide largely constrains an LLM’s ability to solve an issue. Copying the full error output as well as pointing to relevant code is crucial to getting pertinent solutions. Oversharing is also a risk because exhausting the context window can make it hard for the LLM to determine what’s important (though this is becoming less true as context windows continue to grow). When the LLM is getting distracted or pursuing a path I don’t think will be fruitful, I use specific context to redirect it, in addition to explicitly prompting it to focus on smaller sub-problems. Completely restarting the session can also help an LLM get out of a rut by pruning the counterproductive conversation history. Even if an LLM ultimately can’t fix the issue, it’s a great rubber duck to solidify my own understanding of the problem.
I’m also worried about the risk of incumbency bias which is exacerbated by the meteoric rise in the use of LLMs as a debugging tool. Because LLMs are trained on historical data, existing packages and frameworks have a huge advantage over new ones. Users are more likely to get working code if they leverage popular packages. I’ve been particularly interested in marimo lately, but as a new framework, it is more difficult to debug with an LLM. They often don’t fully understand its execution model, its API, or how it defines functions and variables. It’s much easier to one-shot a streamlit app than a marimo app, even though I think marimo apps are likely a better choice for many people. I’ve found a few techniques can help get better suggestions from LLMs for newer packages: supplying links to documentation, providing prototypical recipes, and explicitly stating rules or standards it consistently gets wrong. While not perfect, modern LLM interfaces often allow you to crystallize this extra context in system prompts or reusable instruction files, so you don’t have to include it with each prompt.
Asking Dumb Questions
Beyond debugging, I use the LLM chat interface for more general Q&A as well. I’m a firm believer in asking questions in public channels as a form of documentation. The fact that you have a question likely means other people have or will have it as well. I’ve often searched Slack only to find a question my past self had already asked. At the same time, no matter how often we’ve been told there’s no such thing as a stupid question, we’re all humans with our own insecurities. LLMs give us a perfect outlet for getting answers to these “dumb” questions without fear of judgement.
I still think public channels are the right choice for questions about processes or internal tooling specific to your job, but for anything more general, LLMs offer fantastic value. Search engines are becoming increasingly difficult to use, especially when all you need is a specific answer. LLMs (for now) directly surface answers that would otherwise be buried under promoted content and SEO-driven articles.
I frequently find myself asking questions of the form how to do X in Y? Despite using pandas for the past decade, I cut my teeth on R and the tidyverse. While packages like polars and ibis are bridging the gap with more thoughtful dataframe APIs, I can’t always avoid using pandas. When there’s a transformation that I know is one or two lines in dplyr but becomes verbose in pandas, I ask an LLM for the pandas equivalent. Search engines of the past allowed us to know more by knowing less: we stopped memorizing as many facts and started memorizing how to retrieve the facts. As LLMs replace search engines, they are becoming an extension of our working memory. It’s no longer efficient for me to learn the long tail of pandas’ API when an LLM can produce code for tricky transformations instantly.
LLMs are also infinitely patient, personalized tutors. I often work on projects that have some aspect I’m completely unfamiliar with. It’s hard to even know where to begin. Traditional resources usually involve lengthy videos, multipart online courses, or pay-walled articles. Now, I just ask an LLM “explain modern authentication to me like I’m five.” If there are terms I don’t understand, I dig in deeper. As I build my high-level view, follow-up questions become clearer. All along the way, the LLM keeps track of our conversation, doesn’t get annoyed at explaining the same thing multiple times, and can get as specific or general as needed. The cost of indulging my curiosity is lower than it’s ever been. Instead of passively consuming content, I’m learning new topics by taking a more active role: formulating specific questions and choosing which paths to explore.
Seeking Instant Critique
Learning isn’t just about absorbing information: at some point it requires application. At this stage, specific feedback on your execution becomes more valuable than general information. LLMs make it easier than ever to obtain feedback. In the same way that spell check has become a basic requirement for producing professional writing, LLM critiques will be a marker of thoughtfulness and attention to detail for ICs and management alike. You can wield this fountain of critique to speed up feedback loops and ship faster by anticipating what your stakeholders will say.
For any written work, especially highly visible content, I make sure to ask an LLM for a pedantic review of the text. I often write documents non-linearly, rewrite and prune paragraphs, and reorder sections. Depending on the length of the document, it becomes impossible to hold everything in my mind at once. And it’s easy to read what I meant to say instead of what I actually wrote. I find the impersonal, direct feedback from LLMs easier to incorporate and often more thorough than human reviews.
For coding specifically, I ask LLMs for a critical review of the complexity of my solution. Don’t be clever is not an ideal that LLMs follow by default, but they can be prompted to enforce it. I ask for brutal simplification and pruning of unnecessary code. I try to imagine the poor soul who will debug my code in 6 months when something breaks (most likely myself), and how much they’ll thank me for choosing readability and simplicity over cleverness.
I also ask LLMs to poke holes in my plans. Whether for project-level prioritization or a concrete piece of code, LLMs are an excellent way to get another perspective. The Endowment effect makes it easy to grow too attached to things you’ve built yourself, and I find getting quick feedback early and often from an LLM helps separate ego from the work. The goal of seeking critique is not perfection but simply to catch obvious oversights and proactively address predictable questions.
Crucially, many LLMs have some level of sycophancy by default. This tendency may exist because models are trained to be agreeable, reflecting human conversations where positive reinforcement and compliment sandwiches are common. However, with clear instructions you can compel the LLM to move past flattery and identify concrete improvements.
Context Length Exceeded
How you use LLMs will depend on the problems you face. For me, these four techniques have had the most significant impact on my workflow. LLMs have enabled me to produce predictable code faster, resolve bugs more efficiently, learn more actively, and ask for feedback more often. In the next post in this series, I’ll explore several more advanced LLM patterns that I’ve started to experiment with.