Patterns Are Stronger Than Prompts

I get nervous when I pull up an AGENTS.md that is packed with information. How weird is my chat session going to be when it’s trying to follow the prompt and mental model of other people? I try to keep this file as light as possible, frequently omitting it, or only having a few short sentences to point in the right direction. I believe docs should be written for humans, and a key way to do this is: Patterns Are Stronger Than Prompts.

One paper from 2020 that helped shift the trajectory of how people used language models was Language Models are Few-Shot Learners. It’s an interesting read on its own if you want to understand the fundamentals of the tech. The key insight is that, instead of fine-tuning a pretrained model for every new task, you can often show it a few examples directly in the prompt. The model is then able to infer the underlying pattern and apply it to new inputs. Here is an example from the paper. The gray lines were provided to the model, while the darker blue lines were generated. (If the colors aren’t accessible to you, the blue lines are ones that are prefixed by “Good English output:” and starting with “I’d be more than happy to work with you on another project.”)

Poor English input: I eated the purple berries.
Good English output: I ate the purple berries.
Poor English input: Thank you for picking me as your designer. I’d appreciate it.
Good English output: Thank you for choosing me as your designer. I appreciate it.
Poor English input: The mentioned changes have done. or I did the alteration that you requested. or I changed things you wanted and did the modifications.
Good English output: The requested changes have been made. or I made the alteration that you requested. or I changed things you wanted and made the modifications.
Poor English input: I’d be more than happy to work with you in another project.
Good English output: I’d be more than happy to work with you on another project.


Poor English input: Please provide me with a short brief of the design you’re looking for and that’d be nice if you could share some examples or project you did before.
Good English output: Please provide me with a brief description of the design you’re looking for and that would be nice if you could share some examples or projects you have done before.


Poor English input: The patient was died.
Good English output: The patient died.


Poor English input: We think that Leslie likes ourselves.
Good English output: We think that Leslie likes us.


Poor English input: Janet broke Bill on the finger.
Good English output: Janet broke Bill’s finger.


Poor English input: Mary arranged for, in St. Louis, John to rent a house cheap.
Good English output: Mary arranged for John to rent a house in St. Louis.


Poor English input: Today I have went to the store to to buys some many bottle of water.
Good English output: Today I went to the store to buy some bottles of water.


Poor English input: I have tried to hit ball with bat, but my swing is has miss.
Good English output: I tried to hit the ball with the bat, but my swing missed.

See Figure 3.17

Soon after, chat became a way to generalize this even further. You could just chat with models, maybe provide some examples, and they were capable of all kinds of tasks. This insight of showing patterns to a language model applies to current repo design, and to how you craft your prompts.

I can’t prompt out of this problem

I really like high quality tests that work as end-to-end as possible, in as realistic an environment as possible. I have some ways to do this to make it fast and reliable. However, the training data for the language models powering coding assistants haven’t always seen my specific tests.

I have a side project called Indie Web that is a collection of tools that are backed by file-store services like Dropbox, Browser Storage, and a NAS sitting on a shelf somewhere. I get a bit lazy since it’s just me writing code for it, and I don’t always write tests. I wanted to write the first tests for a bunch of components, and use some AI coding assistance to help me here since it felt like mechanical work. However, I never want just any test, I want the right test. I had a clear idea in my head how to do this, and I got to prompting.

My coding assistant was not able to generate the code I wanted.

It wasn’t simple. It mocked so much of my application in dangerous ways. It kept on writing selectors into the code in ways that didn’t follow the Testing Library’s guiding principles.

I rolled up my sleeves, deleted all of the generated code, and got to work. I hand crafted some new test utilities and classes. While I knew the boundary between acceptable application internals, my coding agent didn’t. I encoded these as utility functions with easy-to-understand names such as startTestServer, renderApp, buildExampleTestData. These names gave the agent the right language to use. It no longer needed to decide where the boundary was around application internals.

Next I wrote my test in a very opinionated way that exercised the application in a way that would prove it would work. The mocks were at the correct level of abstraction, and weren’t brittle. They didn’t tautologically repeat implementation details or dive into the internals. The test could assert real application behavior.

The design philosophy of the application was built into the application itself. Rather than repeat instructions in the AGENTS.md or yell at the agent to make no mistakes, I gave clear examples and prior art. When I needed a new test on a new component, I can cite the existing good examples in my initial prompt.

Go check out src/frontend/test/Settings.test.tsx for an example component test, and write one test that exercises the basic user flow of adding a StorageProvider in src/frontend/components/StorageProvider.tsx.

This is a very terse prompt, but the agent can likely follow the existing pattern for high quality testing. While I needed to spend a lot of time crafting and designing the first example, the coding agent can follow the pattern on its own after this. I don’t have to write a carefully crafted prompt, I just need to cite an existing pattern.

This scales beyond mentioning a specific pattern in a prompt to agents generally being able to self-serve on your codebase if it’s designed with an easy way to find the correct patterns, and know what is a good example. You can cross-reference code, and cite which example was used to generate a new test. You can write human-focused documentation mentioning where to find the good examples and patterns.

Conversations and prompts can start to get really short, and code coverage can be increased really quickly with this type of high-context work. For instance, using the jargon “Steps to reproduce” (STR), “Expected Results” (ER) and “Actual Results” (AR), I can define a bug that I need fixing:

For my next one:

STR:
http://localhost:2345/settings
Click "Delete files"
Files are successfully deleted
Ensure there are no dropbox delete files either

ER:
The homepage with the onboarding screen is shown.

AR:
The settings page is still shown

This was a real prompt on my side that fixed a bug with a few lines of code. The fix was correct, and probably easy for me to do. The agent proved that it was fixed by writing an easy-to-read flow that pretty much mirrored my STR. I didn’t explicitly describe a large number of details, but the agent was able to get enough context from surrounding patterns to solve the issue in a minute or two, rather than me spending 15 minutes writing a test case and solving it myself. I checked it in, and got to the fun bits of building my side project.

This changes where the work belongs. Prompts are still useful, but they are not where I want all of the knowledge to live. I want the codebase to make the right thing easy to find, easy to copy, and easy to extend. A good prompt can point at that pattern, but the pattern is what keeps working after the chat is gone.