loose leaf

writing, loosely

Intention Is All You Need

A philosophy for working effectively with LLMs

Contents
  1. Intention
  2. 1. What Do You Want?
  3. Mapping Prompts
  4. Enough Information
  5. A Perfect Fit
  6. Read My Mind?
  7. Should I Just Write Code Then?
  8. 2. Communication is Hard
  9. You Don't Know What You Want
  10. Minimizing Miscommunication
  11. Thread the Needle
  12. 3. Encapsulate Ambiguity
  13. Make Tools
  14. Ask Questions
  15. Get Data Not Conclusions
  16. 4. Space to Think
  17. Accuracy Not Velocity
  18. Iteration as Instruction
  19. Just Be Intentional
  20. Looking Forward

"How do you use AI effectively for coding?" Many discussions on this topic focus on how to generate higher quality code, understand more complex bugs, or generate more code faster. In my opinion, that's not the important problem to solve.

Modern models are "good enough" to produce high quality products. And they continue to get incrementally better. They are so fast, and they write functional code.

These patterns attempt to optimize how the user spends their time. That's the right idea. You can spin up more LLMs but you can't buy yourself more time. But the problems that cost you the most time aren't because the models couldn't write enough code. The worst problems happen when the model didn't understand you.

The most impactful issues start with misunderstanding intention. No matter how good an LLM[1] gets it will never be able to read your mind.

Therefore, working efficiently with an LLM is all about how you transfer your intention into the tool as quickly and accurately as possible.

Intention

What is intention?

Intention is: "what you want to do" and "how you want to do it". Intention includes all your assumptions. It includes your preferences and idiosyncrasies. Your style. Your taste.

Intention isn't static. It includes the half-formed ideas that exist solely as outlines in your mind. Outlines that you'll fill in without thought when it's time. Because it's obvious what they should be. Obvious to you.

Intention includes the way that you break down problems and the way you build solutions. It includes the decisions that you make and the patterns and values behind those choices.

This probably seems obvious. But it's also subtle, and easy to get wrong. And, even if it's obvious, looking at working with LLMs through the lens of "Intention" helps optimize our workflow.

Now, because we are communicating our intention to the LLM, we can't have those outlines and half formed ideas. We need to sharpen our intention before we express it. Building quickly and iterating until our intention is clear is necessary to express it accurately.

I'm going to start by explaining why this is more important than other metrics you can optimize LLM usage against. And why this problem will not go away. Then I'll explain some patterns that are helpful for efficiently expressing intention.

If you just want the summary, here are my recommendations:

  • Be specific enough
  • Communicate clearly
  • Break problems down
  • Think deeply

1. What Do You Want?

When asking someone else to do something you need to be specific.

Let's imagine that you have a task for an LLM to implement.

There is some "perfect", "ideal" answer to this task. Maybe the perfect answer is what you would produce yourself with infinite time. Maybe the perfect solution is provable with some objective test.

For our experiment we have something special: a "perfect" LLM. By whatever metric you'd like it's perfect. It's not "really good", it cannot be better. For any prompt it will produce an output that is perfect, and it will always produce an output. It functions in the same way current LLMs do, it's stochastic, its inputs and outputs are tokens, and it predicts its output tokens with "existing math".

Mapping Prompts

Let's give our perfect LLM a task. We will give it this task by writing a prompt. The ideal solution our perfect LLM will produce for this task is a series of tokens (text, or code in this case). I find it useful to visualize this ideal solution as a "shape" in the embedding space of the model.

There is some minimal, mathematical function[2] that represents that solution. The size of that function is the amount of information in the solution. We cannot "identify" or "compress" that series of tokens any further. In order to generate the ideal solution we need at least that amount of information.

Our prompt only has so much information in it. Is there only one solution in the entire universe that satisfies that prompt? Probably not, and we'll explore that more later. For now think of this: the ideal solution is surrounded by a region of "valid" solutions. I picture a larger, less defined shape, surrounding the shape of the ideal solution.

That region defines the possible valid solutions to our prompt. There are a few cases worth listing:

  1. The ideal solution is within the valid region, but the valid region is larger than the ideal solution. The prompt is ambiguous.
  2. The ideal solution is not within the valid region. The prompt is inaccurate.
  3. The ideal solution and the valid region are the same. The prompt is perfect.

The prompt defines a set of possible outputs. The solution you want may or may not be in that set.

Enough Information

Let's go back to our perfect LLM, our task, and our prompt.

The question we need to ask ourselves is: "Is there enough information in the prompt for a perfect LLM to arrive at the ideal solution?"

Obviously the answer depends on our prompt and the ideal solution. It's clearly not always "yes". It depends on the amount of information in the input, output, and how many possible solutions the LLM can produce [3]. I lack the "math" to formally prove this, how much information is needed in the prompt to ensure we can derive the solution. But intuitively it's obvious that for simple problems and complex ideal solutions it's mathematically impossible to get the ideal answer.

That's all very abstract. Let's assume the task is to code a video game. It's your "dream" game. You can picture it clearly in your head.

A prompt like "Build a fun game" does not have very much information. Our perfect LLM will produce a "fun game"[4]. If we run this prompt over and over again it will produce different "fun games". One of them might even be the ideal solution! Our ideal solution is certainly within the set of solutions. But the space defined by our prompt is so big, for all practical purposes, even our perfect LLM will never produce our dream game.

A Perfect Fit

We can do better. You write a detailed design document for the game. It's long and explains all of the important features in detail. Run it through our perfect LLM ... and we still don't get the dream game!

It's a better game! It's a lot closer to your intention! The details and requirements from the document removed many of the previous solutions from the set of solutions our perfect LLM will produce. But it didn't remove all of them. Some of the language in the document is ambiguous. Some assumptions are still missing.

We can do better. With enough iteration you refine the document until the dream game is the only output our perfect LLM produces.

Earlier we asked: "Is there enough information in the prompt for a perfect LLM to arrive at the ideal solution?". That wasn't the right question. The right question is "Does our prompt contain enough information that our ideal solution is the only valid solution".[5]

How much information is required to satisfy that requirement for our perfect LLM? I'm not sure it's possible to derive. Does every solution have an input that produces it and nothing else?

We can see it is possible to have a "perfect" prompt for some solutions. "Write a program that outputs all of the numbers between 0-10" for example. This is simple, and has a well defined ideal solution... Or does it? Which is it?

  • "1 2 3 4 5 6 7 8 9 10"
  • "1, 2, 3, 4, 5, 6, 7, 8, 9, 10"
  • "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10"
  • "Error: there are infinite numbers between 0 and 10"
  • "2,5,6,7,1,3,4,8,9,10"
  • "One, Two, Three..."

Fine, but this is fixable. Maybe: "Output all integers between 1 and 10, inclusive, in increasing numerical order, with a single space between each and nothing else". But the point is: it's not easy to be unambiguous[6].

Maybe you feel some of those responses are not "reasonable" solutions. Some of them certainly are ... antagonistic. The perfect LLM should only output the reasonable ones. But, they do logically satisfy the prompt.

You might also point out that some of those variations don't matter. If my goal with this prompt was to learn how to count to 10 then using spaces, commas, or whatever formatting doesn't stop me from doing that. The ambiguity that allows these multiple solutions is unimportant; any "reasonable" solution is acceptable.

But that is not always true, and certainly not universal. What is "reasonable" to one person might not be "reasonable" to another. A constraint that is "unimportant" to one user might not be "unimportant" to another. What is our perfect LLM to do? It can't know. All the LLM has is the input. How does it know your intention?

Read My Mind?

The prompt to ideal solution mapping is a simplification. The prompt is not the only input. LLMs have system prompts, a harness, skills, memories, and files on disk. They have tools to ask the user questions and search the web. That doesn't invalidate the point. Those are all bandaids on top of this same problem: the LLM only has so much information in its input. If that information doesn't define your output at best you are rolling the dice.

These limitations are inherent. It is not because the LLM is lacking in some way; it cannot be overcome with better models. They happen with our perfect LLM.

To summarize:

  • There is a minimum amount of information required to uniquely specify a particular output
  • The input must contain enough information to specify the ideal output
  • The less ambiguous an input is the greater the chance the ideal output is the only valid output

Luckily, when building with LLMs we don't need to write a perfect prompt every time. We only need our ideal solution once. But, we'll need a different ideal solution for the next problem, and the one after that. We need to figure out how to get the ideal output efficiently.

Again: You cannot get around specifying your intention. The LLM cannot do it for you. No matter what, the quality of your input determines the quality of your output.

Should I Just Write Code Then?

"Natural language is ambiguous" is not a novel point. Especially to anyone who writes code. In some ways resolving this ambiguity is the reason coding languages exist in the first place. Code is deterministic. It's unambiguous[7]. The compiler forces you to be precise about what you intended the code to do.

The ambiguity problem still exists at the next level of abstraction. Just because the code does one thing doesn't mean it matches the spec. And it certainly doesn't mean that it solves the problem it was written to solve. When writing code you still have the same "specificity problem".

Writing "perfect" prompts isn't the goal. The goal is to write "Good Enough" prompts and to be intentional how you trade ambiguity for speed. If it takes 10 minutes to write a perfect spec for a feature, but only 1 minute to write a "good enough" spec write the good enough one! The exact point where it pays to write a better spec depends on how good your model is, how novel the output you're looking for is, and how costly it will be if the spec is misinterpreted.

2. Communication is Hard

The strongest evidence that this problem is not going away is how prevalent it is in analogous human relationships. People misunderstand each other all of the time. One person thought an option was obvious and assumed the other would do the same. A word or phrase meant something different to different people. LLMs cannot avoid this potential for misunderstanding [8].

You Don't Know What You Want

Looking at how these misunderstandings happen can give us insights into what to avoid when writing prompts. Anyone who builds something for someone else is familiar with this problem. Their customer comes to them with a request for a feature. The feature is built and given back to the customer. The customer is unhappy. It wasn't what they wanted. Even though they asked for it!

The root cause with this type of issue is often that the customer didn't know what they wanted. The person building the feature should have asked more questions. What are you trying to do? What is the problem you are trying to solve? Instead of the problem the customer gave their solution.

This doesn't happen because the customer wasn't smart enough to solve the problem. It happens because of a difference in perspective. Users of a system don't see it the same way as the people who build it. The internal and external representations of the system are different. A solution derived from the external representation of the system has no guarantee of integrating cleanly with the internal representation of the system.

It's very hard to know if something will work without trying it. Sometimes the only way to know is to try. You don't always know what you want until you see it. That's another way to find yourself in this situation: if you see it and it's not what you want.

When working with an LLM you are now the customer. You approach the LLM and ask for a feature. You need to be a good customer. Make sure you understand what you want, how the system works, and how to modify the system. If you aren't sure what you want make sure you can figure out what you want as soon as possible.

Minimizing Miscommunication

Good communication can prevent these problems. The real root cause of the previous issue was a miscommunication. The customer said "Here's what I want the system to do". What they meant was "This is what I think the right solution to this problem is". Those are different. The customer knows what they need the system to do. But they asked for how they want the system to do it. We need to communicate precisely with the LLM to avoid this class of problem.

To do that prompts should be concise and unambiguous.

Concise: You need to give the model enough information to define your request and no more. Each word you add is a fresh opportunity for you to be misunderstood. Fewer, more carefully chosen words carry more information than long rambling sentences.

Don't add irrelevant details. If the way the feature you're building will be used helps clarify the requirements include it. But if the feature is generic or the larger project context is irrelevant that extra detail is just noise.

Unambiguous: Think about what you write adversarially. Write like you're writing a contract, and the other party is going to try to misinterpret it. LLMs are trained to be helpful, but writing defensively will still give better results than writing ambiguously.

Don't use words that are ambiguous. When you say "Optimize this" is there only one way that can be done? Did you have a specific metric to optimize around? Write that instead! "Make no mistakes". What is a "mistake"? Do you mean it compiles? Or that the logic follows a requirement? Without context these words are just noise. Make them meaningful or leave them out.

Thread the Needle

There's a tension between writing clearly and concisely and specifying exhaustively and completely. Finding the balance between those is the skill of writing good instructions. To make things harder, there's a third factor to balance: time to write. Prompts that are concise and comprehensive are difficult to write. If it wasn't (and I had more time...) I would have written a shorter essay.

I don't have a one-size-fits-all solution for how to do that. How you trade off between those three different metrics will depend on your patience, tolerance for risk, and the type of code that you're working with. But being aware that you're making that tradeoff is a good start.

3. Encapsulate Ambiguity

Expressing intention precisely is difficult and time consuming. By following simple patterns we can isolate misunderstandings to areas that don't matter.

LLMs will produce better solutions more consistently on smaller and simpler problems. Even if the change is perfect, more complex changes take longer. There's more code to be read and to update.

Smaller and simpler problems are easier to specify.

We can express our intention more accurately if we make our tasks small.

Make Tools

Building tools is one way to make a big problem into a small problem.

LLMs are wonderful for building tools. By "tool" I mean: "a secondary program that allows you to efficiently produce an artifact used by your primary program".

Examples of tools might be:

  • A debugging tool that allows you to easily set state or trigger actions for testing your program
  • An editing tool that allows you to create templates for your program
  • A dashboard that allows you to visualize performance and internal state of your program

I want to be clear about what the alternative to this pattern is and how it relates to intention.

Let's use a game as an example: a racing game that we're building the track system for. We could just ask our LLM to build the tracks. We'd have to describe each track, how difficult it should be, how that difficulty is implemented, the theme of the track, how it fits into progression of tracks ... and so on. It's going to be extremely difficult to clearly articulate our intention to the model.

It's much easier to specify a tool for building tracks. We need some sort of path editor, a way to specify assets, and whatever other features make sense for our game. We can then use that tool to build the tracks. We don't have to explain our intention for each track to the LLM. We just need the editor to give us all the options we need to create it directly.

Tools also create a layer of encapsulation. It matters less if your intention for the tool was not expressed perfectly. As long as the tool produces the correct output it doesn't matter. That "deviation" stays in the tool and the primary program gets the tracks that match your intent exactly.

Ask Questions

Intention applies to understanding and fixing bugs.

When fixing an issue, one way to go about it would be to prompt "Fix the bug where the car falls through the track on turn 3". With the context of the existing code there's enough information to identify the bug. But we didn't specify how it should be fixed. Presumably we don't know why it's broken, so it's understandable that we didn't propose a fix. That might be fine. The LLM may do what you expect. It may ask how you want the bug to be fixed. But its still ambiguous.

Instead we could start with a question: "When going through turn 3 the car falls through the track. Explain why this is happening". This is a smaller problem. Our first prompt was "Find the bug" and "fix it". We gave the LLM the same amount of information. Assuming understanding the issue is as hard as writing the fix (it's often harder) we've cut the amount of work per prompt in half!

"Explain" and "why" are still ambiguous. But since we're just gathering information that ambiguity is encapsulated. We can follow up on the explanation if it isn't detailed enough. Once we understand we can prompt the LLM to address the bug. In that prompt we can be mindful of our specificity. But we've both simplified the problem (resulting in a higher quality result) and hidden half of the ambiguity.

Get Data Not Conclusions

The conclusion is the most important part of the decision. When you ask an LLM to give you a conclusion there's a huge problem. The process that it used to arrive at that conclusion is a black box to you. It may explain its reasoning. You can ask it follow up questions to explain. But because of how LLMs actually work those things do not have to be related. Its explanation for how it arrived at that conclusion will seem plausible. But it didn't follow those steps to arrive at it.

I'll try to give a concrete example to ground this. Try to play "Hangman" with an LLM. It'll likely give you a bunch of empty letters and start to fill them in as you guess. When you get to the end of the game, if you're able to reveal all of the letters it will probably be wrong!

Why does this happen? The blank board it gave you at the beginning might look something like this "_ _ _ _ _ _ _ _ _ _". That's our conclusion. We asked it to pick a word and give us spaces to represent that word without telling us what that word is[9]. It didn't do that! It produced a grid that seems plausible. There is no "hidden word". And as we reveal that grid its going to try to make it's choice seem plausible retroactively.

In more practical terms when you have a problem and you'd like an LLM to help you solve it don't ask it to solve your problem. Ask "When solving this problem what are the types of tradeoffs that have to be made?". If you want it to do some abstract "pick the best" type task don't ask "Which area needs to be refactored?". Ask or "Use this tool to generate a summary of all of our bugs for the last year" and "For each bug in this summary attribute a functional area of code". Keep yourself in control of making the decision and use the LLM to gather and synthesize data to help you make that decision.

4. Space to Think

You cannot offload your intention to the LLM, and your intention being achievable depends on understanding what you are building.

Writing code gave us an intimate understanding of the problem for free. Writing code forces you to think deeply about what you are actually trying to do. The precise order that operations happen, how the system represents real world concepts in data, and exactly what logic to use to move that data around are all required by the language and compiler.

Natural language doesn't have any of that. You're allowed to gloss over the tricky parts of a problem. There's no syntax checker. Most of these tools don't even have spellcheckers (which I desperately need).

In some ways this is fine. Models are good. Often it's fine to let the model figure out how to represent your data, or which algorithm to use, or any of the hundreds of other fiddly bits that go into these systems. But not always. It depends on your intention.

If it truly doesn't matter, leave the decision up to the LLM.

For experienced developers writing code is time to think. Yes, you need to think about the mechanics of what you're doing. But some of it is just busy work. Not enough that you can turn off your brain or do something else. But enough to think. You have space to turn over what you're doing in your head and consider if it's actually what you should be doing. You're testing your intention against the reality of the existing system.

LLMs inherently make focusing difficult. They're so fast. You can always spin up another session. You can bounce between different sessions, ask for anything and everything, and just wait for what it gives you. You don't have to watch it run. You can context switch to anything else. When they're done they give you back so much output. You may think that you know how the system works. But if you don't have that first hand knowledge, you don't know for sure.

Writing detailed specs or designs for your product takes time and effort. The point of writing like this is not the spec itself. The point is to clarify your intention proactively. By working through the problem step by step, at a finer level of detail you can uncover the assumptions before the code is written.

Accuracy Not Velocity

LLMs let you create code unimaginably fast. They give you so much velocity. This is wonderful! It means you can get to where you want to be in no time at all.

It also means you get somewhere you don't want to be in no time at all. And you can get lost, without a clear path back to places that you know. Misunderstandings are expensive. Adapting something to do something new is always a larger, more complex problem than doing it that way from the beginning. Because of how LLMs manage context and read files they are particularly vulnerable to this type of complexity.

This is not an issue just for "vibe coded" apps. This happens in carefully architected and well designed systems. If an abstraction has to be changed or expanded the rest of the system needs to be updated, and reevaluated. Decisions that were correct in the old way of thinking may not be correct now. What needs to be changed is hard to know, and tedious and error prone to do.

Make sure you can tell what was delivered versus what was asked for. Set yourself up so you know when misunderstandings happen as soon as possible.

When fixing this class of issues it's extremely easy to be ambiguous with intention. Did you ask for it to be "fixed"? In that case the LLM is likely just going to "make it work". It will pound the square peg of the old implementation into a round hole. But it will work. And if you aren't careful your system will end up as a stack of bandaids and adapters that falls over in a breeze.

Iteration as Instruction

Building can be a way of thinking. Everyone has had the experience of finishing something, looking at it, and thinking "If I did this again I would do so much better". LLMs make the cost of building with code so cheap we can embrace that as a working model.

If you can't know what you want until you see it we can instead ask the question: "How can I see it as quickly as possible". Build the smallest, quickest prototype you can. Not what you wanted? Build a different one. Close to what you wanted? Make some changes, evolve it.

Once you know what you want you can sit down to build it "for real". Having that prototype gives us something really useful: a well defined spec. The code might be a mess, it might have bugs, and it might be inefficient. But if it does what you want you can use it as a specification. Generating a spec from a prototype hides all of that ambiguity that crept in with all the iteration. The LLM can help you generate the spec! Building a prototype might be faster and more accurate than writing the perfect spec doc. It's certainly more fun.

Just Be Intentional

Let's return to my recommendations from the beginning.

  • Be specific enough: The LLM needs to be given enough information to arrive at your intended solution. It's extremely difficult to be unambiguous.
  • Communicate clearly: You must communicate your intent clearly; it cannot read your mind. Clarity and precision in your language will get the best results. Be a good customer.
  • Break problems down: Smaller problems produce better quality solutions and are easier to specify than larger problems. Layering small problems into larger solutions makes it easier to preserve intent. Asking questions and building tools are specific ways to encapsulate ambiguity. Make decisions yourself, let the LLM support you.
  • Think deeply: You are responsible for your intention, the LLM cannot define it for you. Your intention needs to acknowledge the reality of the system. You need to ensure your intention is actually what you want. It's only possible to do that by thinking deeply about what you are building. It's much harder to do that if you aren't writing code.

Looking Forward

The hardest part of solving any problem is defining the problem correctly. If we take optimizing the communication of intention as the problem to solve in AI coding tools we open up a wide open space to explore.

I want to see how we can design these tools to optimize for transferring intention. Through that lens I think we end up with a very different way of working than we have now. What does a tool that provides the lowest friction way to accurately express your intent to an LLM look like? I'm not sure it's a terminal or an IDE.

In this post I've presented intention as a flow in one direction: user to LLM. I touched on the importance of your intention being grounded in reality. Facilitating that grounding, making it easy to understand what the LLM did, at an appropriate level of detail is another area ripe for improvement. Is reading plans and diffs the best solution to that problem? I hope not, I hate reading LLM writing.


  1. I'm going to use "LLM" to refer to "AI Coding Tools", since "AI Coding Tools" is awkward and clunky. It's imprecise, but I have objections to the terms "Agents", and "AI". I'll use LLMs to refer to the whole product: the IDE, editor, or TUI as well as all of the tools, system prompts and other features that go with it. I'll use "model" to refer to the part of the tool that predicts tokens; or does the "thinking". ↩︎

  2. I had a much longer explanation of this, but it was distracting. It had some interesting ideas (does our perfect LLM get to the ideal solution in the smallest number of steps?). But what I mean by information is complexity. ↩︎

  3. The number of valid programs is not actually infinite. And the number of "useful" valid programs is still less. There's only so much computer. Infinite is fine for our purposes. ↩︎

  4. Not everyone will find it fun. Does that mean our perfect LLM cannot exist? ↩︎

  5. What happens if our input has no solutions? Maybe it contains contradictions or paradoxes? Does it break our perfect LLM? ↩︎

  6. I like to think about the exercise you might have done sometime about making a peanut butter and jelly sandwich. There are so many ways to interpret "spread the peanut butter on the bread". ↩︎

  7. Source code is ambiguous depending on the level of detail and how pedantic you want to be. If we take source code to byte code to be analogous to our LLM prompt to ideal solution example. The same source code can produce different byte-code when compiled differently. But the "behavior" that those operations produce when executed is the same. Even for something as detailed and precise as source code, multiple valid interpretations exist. If we want source code to only imply one possible binary output we either: need to specify the target platform, the compiler, and the compiler flags, or agree that those distinctions are unimportant. ↩︎

  8. Because LLMs are "computers" there's this assumption that, when functioning correctly, they are provably correct. Everyone knows LLMs hallucinate and can't count the number of "R"s in "Strawberry". But those are just bugs. When they are working they work like a computer, and a computer is always correct. Even if you know this isn't how they work this is a very tricky shortcut to break. ↩︎

  9. This is actually the "trick" to why the LLM cannot play hangman. If it doesn't tell you the word the tokens for that word are never generated. It never "picked" a word. If the harness you're using has a way for the LLM to generate private reasoning it might work for this test. ↩︎