Topic 12.10
Playbook: Choosing an Advanced Pattern
In one line
A table that maps a measured symptom to a pattern, with recipes to copy for parent expansion within a budget, a stuck-loop guard, and where a real tool-calling search loop plugs in.
Think of it like this
A doctor does not prescribe by habit. She looks at the symptom, orders a test, then chooses the treatment. Advanced patterns are treatments. Pick them from a measured symptom, never from a catalogue.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Symptom
- A measured failure, such as "the right passage is retrieved but the answer lacks the surrounding facts".
- Control set
- Questions that already work, checked to make sure a change does not break them.
- Budget
- A cap on words, tokens, steps or money.
- Guard
- A check in code that stops something unwanted.
Step by step
01Step 1: name the symptom
Collect failing questions and say in one line what is wrong with each: wrong passage, right passage but thin, answer spread across documents, answer in a table.
02Step 2: look it up
Match the symptom to the table in the learn section. If nothing fits, the next step is measuring, not a new pattern.
03Step 3: add parents under a budget
The first recipe sends the parent when it fits, the chunk when only the chunk fits, and nothing when neither fits. It prints what it chose for three budgets.
04Step 4: guard the loop
The second recipe returns why a scripted loop ended: answered, repeated query, or step limit. Three scripts show the three outcomes.
05Step 5: connect a real model
The shown tool-calling code is the loop with the model in place of the script. Your guard logic from step 4 goes inside it.
06Step 6: measure before and after
Run the same questions on the old and new version, and keep the change only if the numbers justify the cost.
Try it yourself
- 1
Change the budgets
Try budgets 220 and 100 in the first recipe. What is sent? Is any answer left with no evidence?
- 2
Add a guard
Add a token budget to the loop guard: stop when the total query length exceeds 60 characters. Add a script that triggers it.
- 3
Write your own table
Write three symptoms from your own project and the pattern or measurement each calls for.
Code & diagrams
Sizes are in words to keep the example simple. In production count tokens (Topic 8.2).
Expected output
budget 500: D52, D50, D51 | 400 words
budget 350: D52, D50, D51.2 (chunk only) | 329 words
budget 150: D52.4 (chunk only), D50, D51.2 (chunk only) | 140 wordsThe three guards in order: a final answer, a repeated query, the step limit.
Expected output
script A: answered at step 2 after 1 search(es)
script B: stopped at step 2: repeated query
script C: stopped: step limit 3Shown, not run. Endpoint and model name are Groq's, as in the labs of this course. The Java tab shows one step with plain HTTP and Jackson; the loop around it is the same as in the Python tab. Names such as MAX_STEPS, search and format_passages come from your own code.
import json
import os
import httpx
# Your own code, not shown: question (the user's text), MAX_STEPS = 3, search(query, k) that
# returns passages, and format_passages(passages) that turns them into one string with ids.
SEARCH_TOOL = {
"type": "function",
"function": {
"name": "search_handbook",
"description": "Search the Lumen Bikes handbook. Returns up to 3 passages with ids.",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
},
}
messages = [
{"role": "system", "content": "Answer from the handbook only. Call search_handbook as often as you need, then cite ids like [D6]."},
{"role": "user", "content": question},
]
for step in range(MAX_STEPS): # the step limit lives in your code, not in the prompt
response = httpx.post(
"https://api.groq.com/openai/v1/chat/completions",
headers={"Authorization": "Bearer " + os.environ["GROQ_API_KEY"]},
json={"model": "openai/gpt-oss-20b", "messages": messages, "tools": [SEARCH_TOOL]},
timeout=30,
)
response.raise_for_status()
message = response.json()["choices"][0]["message"]
messages.append(message)
if not message.get("tool_calls"):
print(message["content"])
break
for call in message["tool_calls"]:
query = json.loads(call["function"]["arguments"])["query"]
messages.append({"role": "tool", "tool_call_id": call["id"], "content": format_passages(search(query, 3))})
Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Count a parent twice
In the Python tab of the first recipe, delete the if parent_id in handled: continue check and the handled.add(parent_id) line. Run.
Myth vs fact
Myth
Advanced patterns are upgrades to apply to every system.
Fact
Each is a fix for a symptom with a cost. Without the symptom it is only cost.
Myth
A pattern that helps once helps always.
Fact
Models and data change. Keep the baseline and re-measure.
Myth
Agentic search should be the default.
Fact
A fixed pipeline is cheaper, faster and easier to test. Use the loop where the question needs it.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Keep an architecture note listing each pattern, the symptom that justified it, the cost, and the metric that proves it still helps.
- ▸
Order of cheapness: better chunks and metadata, then hybrid and re-rank, then query rewriting, then these patterns.
- ▸
Every pattern here adds an ingestion step, a failure mode and a test. Count those before you add it.
Remember this
- 1
Symptom to pattern. The answer is right but needs the sentences around the match: parent-child (12.1). Questions are about a whole document: summaries as an index (12.2). Users paraphrase or ask in forms the passage never uses: question keys (12.3). The right passage is in the top 50 but not the top 5: late interaction or a cross-encoder re-rank (12.3, 6.4). The answer needs chains of facts across documents: a graph (12.4). The answer needs a number from a table: validated text-to-SQL (12.5). The answer is in a picture: captions, OCR or image embeddings (12.6). The answer needs several searches: a bounded search loop (12.7). The whole corpus is small: paste it (12.8). Anything else: measure first.
- 2
Recipe: expand to parents within a budget. When to use it: parent-child retrieval with a context budget. What it guards against: a big parent pushing out others, or the same parent sent twice. How to adapt it: pass real token counts in place of words, and decide whether a hit that does not fit sends its chunk alone or is skipped.
- 3
Recipe: guard a search loop. When to use it: any model-driven retrieval. What it guards against: stuck loops that repeat a query or never end. How to adapt it: also stop on a token or cost budget, log each step, and return what was found when stopped.
- 4
Recipe: plug a real model into the loop. The shown code gives the model the search tool and runs your search until it answers, with the step limit in your code. It uses the OpenAI-compatible tool-calling shape (LLM Engineering Topic 7.2): the request carries
tools, the reply carriestool_calls, and each result goes back as arole: "tool"message with the matchingtool_call_id. Many other providers accept the same shape at a different base URL. Check your provider's current fields. - 5
Before you add any pattern. Write the symptom down with three failing questions. Add the pattern behind the same
retrieveinterface. Measure the same questions and a control set. Keep it only if it helps, and record what it costs in storage, calls and updates. Remove a pattern that stops helping.
Explain it without notes
How would you choose between parent-child retrieval and summaries?
What do you do before adding any advanced pattern?
Why keep a baseline after adding a pattern?
Which guards belong in any model-driven search loop?
Practice
Write pick_pattern(symptom) that looks the symptom up in a dictionary and returns the pattern, or "measure first" for an unknown symptom. Include three symptoms of your own. Print the result for one known and one unknown symptom.
Write unique_in_order(items) that removes repeats and keeps the first occurrence of each item. Print it for ["D52", "D50", "D52", "D51", "D50"].
Trade-offs
- ↔
Adding a pattern: a fix for a symptom and a lasting cost.
- ↔
Measuring first: slower start, fewer wasted patterns.
- ↔
A shared interface: easy swaps and some discipline.
Done when you can
Done when you can map five symptoms to patterns.
Done when you can write the parent-budget recipe and the loop guard.
Done when you can say what to measure before and after a pattern.
Done when you can say where the step limit lives and why.