From Vague Prompt to Working Code
James has been using Claude Code for weeks. He has pasted in function stubs and asked the AI to fill them in. He has copied error messages and asked for fixes. Every time, he followed a template: here is my stub, here are my tests, generate the body.
Today Emma changes the rules. "No stub. No template. You write the prompt yourself."
James blinks. "What do I say?"
"Start with what you want the function to do. Then add details until the prompt is specific enough that the AI cannot misinterpret it." She pulls up a blank terminal. "Think of it as a funnel. Wide at the top, narrow at the bottom."
Up to this point, you have been given prompts to copy or templates to fill in. This lesson is the first time you write your own prompt from scratch. The goal is not to write a perfect prompt on the first try. The goal is to learn the pattern for making prompts more specific when the first attempt produces imperfect code.
You may already write prompts regularly. This lesson formalizes the progression from vague to specific and connects it to test results. The key insight: your tests are the best possible context for a re-prompt, because they define exactly what "correct" means.
The Funnel Pattern
In Chapter 46 Lesson 4, you did a single re-prompt cycle: the AI generated code, one test failed, and you pasted the error back with a short instruction. That was one round, one fix, and someone else wrote the original prompt for you.
Now you write the prompt yourself. The funnel pattern has four levels of specificity:
Level 0 (vague): "Write a search function for notes."
Level 1 (typed): "Write a function search_notes(notes: list[Note], term: str) -> list[Note] that searches notes."
Level 2 (behavior): "Write search_notes(notes: list[Note], term: str) -> list[Note] that returns every note whose title or body contains the search term. The match should be case-insensitive."
Level 3 (edge cases): "Write search_notes(notes: list[Note], term: str) -> list[Note] that returns every note whose title or body contains the search term, case-insensitive. Return an empty list when no notes match. If the term is an empty string, return all notes."
Each level adds information that reduces ambiguity. A Level 0 prompt forces the AI to guess everything: the function name, the parameters, the return type, the matching rules. A Level 3 prompt leaves almost nothing to interpretation.
Why Vague Prompts Produce Partial Results
Consider what the AI must decide when you write "Write a search function for notes":
| Decision | What You Meant | What the AI Might Guess |
|---|---|---|
| Function name | search_notes | find_notes, search, query_notes |
| Parameter types | list[Note] and str | list[dict] and str, or list[str] |
| What to search | title and body | title only, or tags only |
| Case sensitivity | case-insensitive | case-sensitive |
| Empty term behavior | return all notes | raise an error, return empty list |
| No matches | return [] | return None, raise an error |
Six decisions, six chances for a mismatch. Each mismatch produces a test failure. The funnel pattern eliminates these mismatches one level at a time.
Writing Your First Prompt
Here is the scenario. You have these tests already written (from Chapter 52's parametrized testing):
from dataclasses import dataclass
@dataclass
class Note:
title: str
body: str
tags: list[str]
word_count: int
def test_search_exact_title_match() -> None:
notes: list[Note] = [
Note(title="Python Basics", body="Learn Python.", tags=["python"], word_count=2),
Note(title="Cooking Tips", body="Use fresh herbs.", tags=["cooking"], word_count=3),
]
result: list[Note] = search_notes(notes, "Python")
assert len(result) == 1
assert result[0].title == "Python Basics"
def test_search_case_insensitive() -> None:
notes: list[Note] = [
Note(title="Python Basics", body="Learn Python.", tags=["python"], word_count=2),
]
result: list[Note] = search_notes(notes, "python")
assert len(result) == 1
def test_search_in_body() -> None:
notes: list[Note] = [
Note(title="Cooking Tips", body="Use fresh herbs.", tags=["cooking"], word_count=3),
]
result: list[Note] = search_notes(notes, "herbs")
assert len(result) == 1
def test_search_no_matches() -> None:
notes: list[Note] = [
Note(title="Python Basics", body="Learn Python.", tags=["python"], word_count=2),
]
result: list[Note] = search_notes(notes, "JavaScript")
assert result == []
def test_search_empty_list() -> None:
result: list[Note] = search_notes([], "anything")
assert result == []
def test_search_empty_term() -> None:
notes: list[Note] = [
Note(title="Python Basics", body="Learn Python.", tags=["python"], word_count=2),
Note(title="Cooking Tips", body="Use fresh herbs.", tags=["cooking"], word_count=3),
]
result: list[Note] = search_notes(notes, "")
assert len(result) == 2
Your job: write a prompt that produces a search_notes function passing all six tests. You have never been given a template for this. You have to decide what to include.
The First Attempt
Start at Level 1. Here is a reasonable first prompt:
Write a Python function called search_notes that takes a list of Note
objects and a search term string. Return a list of matching Note objects.
Note is a dataclass with fields: title (str), body (str),
tags (list[str]), word_count (int).
This prompt specifies types and the dataclass structure. It does not mention case sensitivity, empty term behavior, or whether to search title, body, or both. The AI will make assumptions about those decisions.
Run the generated code against your tests. Suppose four of six pass: test_search_exact_title_match, test_search_no_matches, test_search_empty_list, and test_search_in_body pass. Two fail: test_search_case_insensitive and test_search_empty_term.
Reading the Failures
The pytest output for the case-insensitive failure looks like this:
FAILED test_search_case_insensitive - AssertionError: assert 0 == 1
The AI generated a case-sensitive search. Your prompt never mentioned case sensitivity, so the AI guessed wrong. This is not an AI bug; it is a prompt gap.
The empty-term failure:
FAILED test_search_empty_term - AssertionError: assert 0 == 2
The AI treated an empty string as "no match." Your prompt never specified what an empty term should do.
The Refined Prompt
Now you move to Level 2+. Instead of rewriting the whole prompt, you add the missing details:
The search_notes function I asked you to write has two issues:
1. The search should be case-insensitive. "python" should match
a note with title "Python Basics".
2. If the search term is an empty string, return all notes.
Here is the failing test output:
FAILED test_search_case_insensitive - assert 0 == 1
FAILED test_search_empty_term - assert 0 == 2
Please fix the function.
This re-prompt is specific. It names the two problems, states the expected behavior, and includes the test output as evidence. Compare this to a vague re-prompt like "some tests are failing, can you fix it?" The specific version gives the AI exactly what it needs.
The Pattern: Tests as Context
Notice what happened. Your tests defined "correct." When some tests failed, the failure messages told you exactly what the AI got wrong. You fed those messages back into the next prompt. This is the core loop:
- Write a prompt (as specific as you can)
- Run pytest
- Read which tests failed and why
- Re-prompt with the specific failures
The tests are not just verification. They are communication. They tell the AI what you meant in a language more precise than English.
In Chapter 52, you learned to write parametrized tests that cover multiple inputs in a single function. Those parametrized outputs are especially useful as re-prompt context because they show the AI a table of inputs and expected outputs, making the specification unambiguous.
PRIMM-AI+ Practice: Prompt Prediction
Predict [AI-FREE]
Before running any prompt, look at the Level 1 prompt above and predict:
- Will the AI search title only, body only, or both? (Confidence: 1-5)
- Will the match be case-sensitive or case-insensitive? (Confidence: 1-5)
- What will the AI do with an empty search term? (Confidence: 1-5)
Write your predictions down before continuing.
Check your predictions
Prediction 1: Most AI models search both title and body when you say "matching Note objects" without specifying which fields. But this is not guaranteed. If the AI searched only the title, test_search_in_body would fail.
Prediction 2: Most AI models default to case-sensitive string comparison unless told otherwise. The in operator in Python is case-sensitive, and the AI is likely to use if term in note.title.
Prediction 3: With an empty string, "" in "any string" evaluates to True in Python. If the AI uses an in check without a special case, empty terms might accidentally match everything. But some AI implementations add an early return for empty input, returning [].
The unpredictability is the point. Your tests catch whatever the AI guesses wrong.
Run
Open Claude Code in your SmartNotes project. Write your own Level 1 prompt (not the Level 2 version). Run uv run pytest against the six tests. Record how many pass.
Investigate
For each failing test, classify the error using the Error Taxonomy from Chapter 43:
- Omission: the AI left something out (e.g., never searched the body)
- Misinterpretation: the AI did something different from what you intended (e.g., case-sensitive instead of insensitive)
- Logic error: the AI implemented the right idea incorrectly (e.g., used
orinstead ofand)
Write one sentence per failure explaining which category it falls into.
Modify
Write a Level 2+ re-prompt that targets the specific failures. Include the pytest output. Run uv run pytest again. Did the pass count improve?
Make [Mastery Gate]
Without looking at any examples from this lesson, write a prompt from scratch for this function:
def filter_notes_by_tag(notes: list[Note], tag: str) -> list[Note]:
"""Return notes that contain the given tag. Case-insensitive match."""
...
Write three tests first. Then write a prompt, generate the code, and iterate until all tests pass. You should need no more than two rounds.
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Evaluate Your Own Prompt
I wrote this prompt to generate a Python function:
"Write a function search_notes that takes notes and a term
and returns matching notes."
What information is missing that could cause the AI to produce
code that does not match my intent? List each missing detail
and explain what the AI might guess instead.
Read the response. Compare its list of missing details to the funnel levels described in this lesson. Did the AI catch the same gaps you identified?
Prompt 2: Prompt Comparison
Here are two prompts for the same function. Which one will
produce more accurate code on the first try, and why?
Prompt A: "Write a search function for notes."
Prompt B: "Write search_notes(notes: list[Note], term: str) -> list[Note]
that returns notes whose title or body contains term,
case-insensitive. Return [] for no matches. If term is empty,
return all notes."
What you're learning: You are developing judgment about prompt quality before you run anything. This is the same skill you use when reading code: predicting behavior from text.
Key Takeaways
-
The funnel pattern moves from vague to specific. Start with what you want, then add types, behavior, and edge cases. Each level of detail removes a decision the AI has to guess.
-
Tests are your most precise prompt context. When tests fail, the failure output tells the AI exactly what it got wrong. Include pytest output in re-prompts rather than describing the problem in English.
-
Vague prompts produce partial results by design. The AI is not broken when 4 of 6 tests pass on the first try. Your prompt left room for interpretation, and the AI interpreted differently than you intended.
-
You write the prompt now. In Chapter 46, prompts were provided for you. From this chapter forward, you decide what to include. The funnel pattern gives you a structure for making that decision.
Looking Ahead
You got most tests passing with one round of refinement. But what happens when fixing one failure breaks something that was already working? Lesson 2 covers multi-round iteration, where a fix introduces a new bug and you have to manage the cycle across several rounds.