Dynamic Few-Shot Example Selection

AILLMMachine LearningArchitecture
Share on LinkedIn

The same four few-shot examples in every prompt worked until users started asking about enterprise billing — a category none of the examples covered. The model defaulted to consumer pricing patterns and sounded confident doing it. Static few-shot is a shotgun; dynamic selection picks the right examples for each input, improving accuracy without stuffing 20 examples into every prompt.

Example store design

@dataclass
class FewShotExample:
    id: str
    input: str
    output: str
    embedding: list[float]
    metadata: dict  # intent, language, difficulty, tenant
    quality_score: float  # from human review or eval performance

Store in vector index + Postgres for metadata filtering. Curate examples from:

Quality over quantity — 200 excellent examples beat 2,000 noisy ones.

Similarity-based selection

Retrieve examples closest to the current input:

async def select_examples(
    query: str,
    k: int = 5,
    filters: dict | None = None,
) -> list[FewShotExample]:
    embedding = await embed(query)
    candidates = await index.search(
        embedding,
        filter=filters,
        top_k=k * 3,  # over-fetch for diversity reranking
    )
    return diversify(candidates, k)

Use the same embedding model as your semantic router for consistency.

Diversity sampling

Similarity alone picks near-duplicates. After retrieval, diversify:

def diversify(candidates: list[FewShotExample], k: int) -> list[FewShotExample]:
    selected = [candidates[0]]  # best match always included
    while len(selected) < k:
        remaining = [c for c in candidates if c not in selected]
        # pick candidate maximally different from selected set
        next_ex = max(remaining, key=lambda c: min_distance(c, selected))
        selected.append(next_ex)
    return selected

Maximal marginal relevance (MMR) balances relevance and diversity:

def mmr_score(candidate, query_embed, selected, lambda_param=0.7):
    relevance = cosine(candidate.embedding, query_embed)
    if not selected:
        return relevance
    max_sim = max(cosine(candidate.embedding, s.embedding) for s in selected)
    return lambda_param * relevance - (1 - lambda_param) * max_sim

Metadata filtering

Constrain examples to relevant subset:

filters = {
    "intent": classified_intent,
    "language": detect_language(query),
    "quality_score": {"$gte": 0.8},
}

Don't show billing examples for technical queries even if embedding similarity is high — intent filter first, similarity second.

Prompt assembly

def build_few_shot_prompt(examples: list[FewShotExample], query: str) -> str:
    shots = "\n\n".join(
        f"Input: {ex.input}\nOutput: {ex.output}"
        for ex in examples
    )
    return f"""Follow the pattern in these examples:

{shots}

Input: {query}
Output:"""

Place examples after system instructions, before the actual query. Consistent formatting across examples matters — the model learns the pattern from structure.

Quality maintenance

Examples degrade as product changes:

async def validate_example_set(examples: list, eval_set: list) -> list:
    eval_inputs = {e.input for e in eval_set}
    return [ex for ex in examples if ex.input not in eval_inputs]

Active learning for example curation

When the model fails on a query type:

  1. Human labels correct output
  2. Add to example store with embedding
  3. Similar future queries automatically get this example

This closes the loop between failures and improved few-shot coverage.

Cost and latency

Dynamic selection adds one embedding call (~50ms) and one vector search (~10ms). Cache selected examples for identical queries. Pre-compute embeddings for the example store at ingest time.

For latency-sensitive paths, pre-classify intent and maintain per-intent static subsets (5 examples × 10 intents = hybrid approach).

Semantic similarity selection

Select examples most similar to the current query:

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("all-MiniLM-L6-v2")

def select_examples(query: str, example_store: list[dict], k: int = 5) -> list[dict]:
    query_emb = model.encode(query)
    example_embs = np.array([model.encode(ex["input"]) for ex in example_store])
    similarities = example_embs @ query_emb / (
        np.linalg.norm(example_embs, axis=1) * np.linalg.norm(query_emb)
    )
    top_k_indices = similarities.argsort()[-k:][::-1]
    return [example_store[i] for i in top_k_indices]

Semantic selection outperforms random selection by 10–20% on domain tasks. Pre-compute example embeddings at ingest — selection adds ~50ms at query time.

Maximal Marginal Relevance (MMR)

Avoid selecting redundant similar examples:

def mmr_select(query_emb, example_embs, examples, k=5, lambda_=0.7):
    selected, selected_embs = [], []
    candidates = list(range(len(examples)))

    while len(selected) < k and candidates:
        scores = []
        for i in candidates:
            relevance = cosine(query_emb, example_embs[i])
            redundancy = max(
                (cosine(example_embs[i], s) for s in selected_embs),
                default=0
            )
            mmr_score = lambda_ * relevance - (1 - lambda_) * redundancy
            scores.append((mmr_score, i))
        best_idx = max(scores)[1]
        selected.append(examples[best_idx])
        selected_embs.append(example_embs[best_idx])
        candidates.remove(best_idx)
    return selected

MMR balances relevance to query with diversity among selected examples. Five similar examples teach one pattern; five diverse examples teach five patterns.

Example quality scoring

Not all examples are equally valuable — weight by demonstrated effectiveness:

@dataclass
class ScoredExample:
    input: str
    output: str
    intent: str
    success_rate: float  # downstream task success when this example used
    usage_count: int
    last_validated: datetime

def select_weighted(query: str, examples: list[ScoredExample], k: int = 5) -> list:
    candidates = semantic_filter(query, examples, k=k * 3)
    # Boost high success rate, penalize stale
    scored = sorted(
        candidates,
        key=lambda e: e.success_rate * freshness_weight(e.last_validated),
        reverse=True,
    )
    return scored[:k]

Retire examples with success_rate <0.6 after 50+ usages. Add production failures as new examples after human labeling.

Failure modes

Production checklist

Resources

Frequently asked questions

How many few-shot examples should I include?

Typically 3–8 examples depending on example length and task complexity. More examples help up to a point — then context bloat hurts and the model confuses patterns. Measure on your eval set: plot accuracy vs example count and stop where marginal gain drops below 1%.

Static vs dynamic few-shot — when does dynamic win?

Dynamic wins when your task has diverse subtypes (support intents, document formats, code patterns) and no single static set covers them well. Static is fine for homogeneous tasks with stable patterns. Dynamic adds latency (embedding lookup) and infrastructure.

Should few-shot examples include chain-of-thought?

Include reasoning in examples only when the task requires multi-step reasoning and you've confirmed CoT helps on evals. For classification and extraction, input→output pairs without reasoning are cleaner and use fewer tokens.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →