You’ve probably noticed that LLM coding assistants are great at some things and weirdly bad at others. They can write a sorting algorithm from scratch but fumble a refactor that seems obvious to you. That gap isn’t random. It comes from a fundamental difference in how LLMs perceive code versus how you do. Understanding that difference will make you dramatically better at working with these tools.

1. Your Codebase Is a Flat Document, Not a Project

When you open a repository, your brain immediately builds a mental model: this module owns authentication, that directory handles payments, this class is the one everyone inherits from. You navigate by meaning. An LLM navigates by what’s in its context window.

If you paste a single file into a chat, the model has no idea that the function you’re asking about is called from twelve other places. It doesn’t know that the variable name config means something specific in your codebase because you have a company-wide convention. It sees text. The project structure, the dependency graph, the history of why things are named what they are — none of that exists unless you explicitly provide it.

This is why “here’s a function, make it better” produces mediocre results while “here’s the function, here’s the interface it has to satisfy, here’s the calling code” produces something actually useful. You’re not writing a better prompt. You’re reconstructing the context the model needs to reason correctly.

2. Naming Is Your Most Underrated Communication Channel

LLMs are trained on enormous amounts of code, which means they’ve absorbed strong statistical associations between names and behaviors. A function called validateUser is expected to return a boolean or raise an exception. A class called UserManager probably has CRUD methods. When your code follows these conventions, the model fills in enormous amounts of correct assumptions automatically.

When your naming diverges, things break down fast. A function called processData that actually sends emails and updates a database is genuinely confusing to the model, for the same reason it’s confusing to a new team member. Except the new team member can ask questions. The model will confidently proceed on its best guess.

Good naming has always mattered, but working with LLMs makes the cost of bad naming immediate and visible. If your codebase is full of helper.js files and utils directories with no clear scope, you’ll feel it every time you ask for assistance with that code.

A spotlight illuminating a small portion of a large dark grid of code blocks
The context window doesn't see your project. It sees what you chose to show it.

3. Comments Are Context, Not Clutter

Many codebases treat comments as a legacy habit, something you write when code isn’t self-explanatory. The actual best use of comments is to record intent: why this approach was chosen, what alternatives were rejected, what invariant this block assumes will hold.

For an LLM, that intent information is gold. The model can read code well enough to reconstruct what something does. What it cannot reconstruct is why a seemingly-simpler approach was rejected three years ago because it caused a race condition in production. If that context lives in a comment, the model won’t suggest the simpler approach and break everything. If it lives only in someone’s memory, or in a closed Jira ticket, the model will confidently suggest exactly the wrong thing.

This isn’t about writing comments for AI consumption. It’s about recognizing that comments encoding decision rationale are exactly the kind of high-signal text that makes models useful rather than dangerous in your codebase.

4. Inconsistency Destroys Pattern Matching

LLMs are extremely good at continuing patterns. If your codebase has a consistent pattern for error handling, the model will pick it up and use it without being told. If your codebase has four different error handling patterns from four different eras of the project, the model will pick one, and you won’t know which until you review the output carefully.

Inconsistency doesn’t just make codebases harder for humans to maintain. It actively degrades the quality of AI assistance because the model is trying to infer rules from noisy data. Every place where you’ve mixed conventions is a place where the model’s confident suggestions become less reliable.

This is a real argument for paying down certain kinds of technical debt before you lean heavily on AI tooling. Not all debt, but the kind that produces surface-level inconsistency: mixed naming conventions, multiple patterns for the same operation, config that lives in three different places. Cleaning that up pays dividends for your team and for every AI tool you use on the code.

5. The Context Window Is a Spotlight, Not a View

Professional codebases are large. Context windows, even generous ones, are not. This means every LLM interaction with real code involves a selection problem: what gets included determines what the model can reason about. If the relevant constraint is in a file you didn’t include, the model doesn’t know it exists.

This has a practical implication for how you structure your requests. Bigger isn’t always better. Pasting your entire codebase (even if it fits) often produces worse results than carefully selecting the three files that actually matter for the task. More text means more noise, and the model’s attention isn’t uniformly distributed across everything you provide. Research on transformer attention has consistently shown that information in the middle of a long context receives less reliable attention than information at the beginning or end.

When you’re asking for help with a specific change, think about what a thorough human reviewer would need to see to give good feedback. That’s roughly what the model needs, and probably less than you’d instinctively include.

6. Tests Are a Description of Your Intent

Your test suite is one of the most information-dense parts of your codebase for an LLM. Tests describe what functions are supposed to do, what edge cases matter, what inputs are valid, and what behavior is expected under failure conditions. All of that is exactly the context that lets a model suggest changes that don’t break things.

If you include relevant tests when asking for help with a function, you’re giving the model a machine-readable specification to work against. If you ask for help without tests, you’re asking it to guess your intent from implementation alone. That’s a much harder problem, and the model will sometimes guess wrong in ways that look completely plausible.

The inverse is also true: if your tests are sparse or test only the happy path, you’re handing the model an incomplete spec. It’ll satisfy the tests you have, which may not be the tests you need.

What To Do With This

None of this requires a new workflow from scratch. A few targeted habits will close most of the gap. Include the calling context, not just the function in question. Add comments where you record a non-obvious decision. Pick one pattern and apply it consistently. When you ask for help, select what you include deliberately rather than defaulting to a whole file.

LLMs aren’t going to become more like the human developers who deeply understand your system over time. They’re going to keep working from what you give them. The codebases that get the most useful output from AI tooling will be the ones that were already well-organized, well-named, and well-documented. That’s a good reason to care about code quality even if you were somehow unconvinced before.