loader

Token Optimization in RAG

Token Optimization in RAG
RAG engineering Beginner to production Long read

Token Optimization in RAG

A complete guide to reducing LLM costs, improving latency, and building efficient AI applications — chunking, retrieval, reranking, context compression and real-world examples.

The objective
Send the smallest sufficient amount of relevant information to the LLM while preserving answer accuracy, completeness, security, and reliability.
01 · The business problem

A correct answer that costs too much and arrives too late

A steel manufacturer has built a RAG chatbot for its dealers and employees. The knowledge base holds product specifications, steel price policies, technical manuals, dealer FAQs, warranty documents, delivery policies, and thousands of PDF and Word files collected over many years.

A dealer asks:

“What is the minimum order quantity for TMT bars, and what documents are required to become an authorized dealer?”

The application retrieves several large document chunks and sends them to an LLM. The answer is correct. The system still has problems:

  • Too many input tokens per question, so every answer is expensive.
  • High monthly LLM API bills that grow linearly with traffic.
  • Slow responses, because the model must read thousands of tokens before it writes the first word.
  • Irrelevant retrieved content, such as warranty clauses and southern-region delivery rules.
  • Repeated information: the same eligibility rule appears in three documents and in overlapping chunks.
  • Large conversation histories resent on every turn.
  • Poor performance when many dealers ask questions at once, because long prompts consume rate limits faster.

Why this is not a prompt-writing problem

Most of the tokens in this request were not written by a prompt engineer. They came from upstream decisions: how documents were extracted and cleaned, how large the chunks are, how many chunks the retriever returns, whether filters remove irrelevant regions, whether duplicates are removed, how much chat history is attached, which model is called, and how long the answer is allowed to be. Rewording the system prompt touches only a few hundred of the eight thousand input tokens. Token optimization is therefore a pipeline problem spanning retrieval, chunking, context selection, conversation memory, model choice, and generation.

Fewer tokens is not the goal by itself If the optimized system drops the exception that allows small orders for project supplies, or omits one required onboarding document, it has failed — even if it is 70% cheaper. A dealer who receives a wrong answer calls the sales office, escalates, or places an invalid order. Those costs are larger than any API saving. Every optimization in this guide is judged on two axes at once: token cost and answer quality.
02 · Tokens from first principles

What a token actually is

Language models do not read characters or words. They read tokens: integer IDs that each represent a piece of text from a fixed vocabulary. A token can be a whole common word (“steel”), part of a word (“author” + “ized”), a single punctuation mark, a space followed by a word, or a single digit.

A tokenizer converts text into these IDs. Most modern tokenizers use a variant of byte-pair encoding (BPE): training starts from individual bytes and repeatedly merges the most frequent adjacent pairs into new vocabulary entries. Frequent English words become single tokens; rare words, product codes and unfamiliar scripts are split into several smaller pieces.

UnitWhat it counts“Steel prices increased by 5% in Delhi.”
BytesRaw storage (UTF-8)38 bytes
CharactersVisible symbols and spaces38 characters
WordsSpace-separated strings7 words
SentencesGrammatical units1 sentence
TokensVocabulary entries the model readsDepends on the tokenizer — roughly 9–11

An illustrative split — not verified against any specific tokenizer — might look like this. Note how the leading space is usually attached to the following word, and how “5” and “%” become separate tokens:

Steel ·prices ·increased ·by · 5 % ·in ·Delhi .

Illustrative only. “·” marks a leading space. Run the code below to see the real split for your tokenizer.

Why counts differ across text and models

  • Different vocabularies. Each model family trains its own tokenizer. The same sentence can produce different counts on different models, so a budget tuned for one model does not transfer exactly to another.
  • Numbers and codes. “12,500.75 MT” or a grade such as “Fe 550D” often splits into many small tokens. Spec sheets and price tables are token-dense.
  • Punctuation and special characters. Bullets, pipes from table extraction, repeated dashes and OCR garbage each cost tokens and carry no meaning.
  • Code and JSON. Braces, quotes and indentation all count. Pretty-printed JSON uses more tokens than compact JSON.
  • Multilingual text. Many tokenizers are trained mostly on English, so Hindi, Tamil or Marathi text commonly needs more tokens per word. A bilingual dealer base can make average prompts noticeably longer.

A rule of thumb often quoted for English is about four characters per token. It is useful for back-of-envelope sizing and wrong for budgets, billing, and non-English text.

Input, output and cached tokens

  • Input tokens (prompt tokens) are everything you send: system instructions, the question, history, retrieved context, tool definitions, and formatting the provider adds around messages.
  • Output tokens (completion tokens) are what the model generates. They are produced one at a time, so they dominate latency, and providers commonly price them higher than input tokens.
  • Cached input tokens are input tokens that a provider has recognized as an exact repeat of a recently processed prompt prefix. Where supported, they may be billed at a lower rate and processed faster. They are still input tokens; caching changes their price, not their count.

Embedding models and generation models are separate. The model that turns chunks into vectors may use a different tokenizer, a different maximum input length and a different price than the model that writes answers. Count each with its own tokenizer and read each provider’s usage report separately.

Counting tokens in Python

For OpenAI models supported by tiktoken, you can count locally. Verify that the encoding matches your model; other providers publish their own tokenizers or token-counting endpoints.

# pip install tiktoken
import tiktoken

# o200k_base is the encoding used by several recent OpenAI models.
# tiktoken.encoding_for_model("model-name") raises KeyError for models it does not know.
enc = tiktoken.get_encoding("o200k_base")

text = "Steel prices increased by 5% in Delhi."
ids = enc.encode(text)

print(len(ids))                          # token count for this encoding
print([enc.decode([i]) for i in ids])    # the actual split

Local counts are estimates. Chat APIs wrap messages in role markers and may add tokens for tool schemas or images, so the provider-reported usage field on each response is the authoritative number for billing.

03 · The pipeline

Where a RAG system spends tokens

Retrieval-Augmented Generation has three phases. Ingestion runs once per document version and prepares a searchable index. Retrieval runs for every question and selects evidence. Generation runs for every question and produces the answer. Each phase has different costs.

Generation — every question
Retrieval — every question
Ingestion — once per document version
13 LLM generation
12 Prompt construction
14 Output validation
15 Delivery and monitoring
8 Query embedding
7 User question processing
9 Retrieval
10 Reranking (optional)
11 Filtering and compression
2 Text extraction and OCR
1 Document collection
3 Cleaning and normalization
4 Chunking
5 Embedding generation
6 Vector / hybrid index

Two kinds of token usage must be kept apart:

Embedding token usage
Text processed by the embedding model: every chunk during ingestion, and every query (or rewritten query) at question time. Usually cheap per token and paid once per chunk version.
Generation token usage
Instructions, question, history, retrieved context and tool definitions sent to the answer-generation model, plus the answer it writes. Paid on every request; usually the largest recurring cost.

Storing vectors is not token consumption. A vector database charges for storage, memory and queries. Keeping a million embedding vectors does not consume a single generation token; only the chunk text you later paste into a prompt does.

StageTokens?Consumer · phaseOptimizeDon’t optimize blindly
1 CollectionNoStorage · ingestionExclude obsolete, draft and duplicate filesDeleting old versions still needed for historical contracts
2 Extraction / OCRNo (OCR may use a vision model)Parser or OCR · ingestionPreserve table structure; avoid garbage charactersSkipping OCR on scanned annexures
3 CleaningNoCode · ingestionStrip headers, footers, navigation, whitespaceRemoving dates, units, qualifiers, clause numbers
4 ChunkingIndirectlyCode · ingestionStructure-aware sizes measured in tokensShrinking chunks without testing recall
5 EmbeddingYesEmbedding model · ingestionEmbed only new or changed chunks; batchChanging embedding model without re-indexing everything
6 IndexingNoVector / keyword index · ingestionStore metadata for filtersDropping fields you will later need to filter on
7 Question processingSometimesCode or small LLM · retrievalRoute deterministic questions away from RAGRewriting away product codes
8 Query embeddingYes (small)Embedding model · retrievalCache repeated query vectorsUsing a different embedding model than the index
9 RetrievalNoIndex · retrievalFilters, hybrid search, tuned top-KTop-K = 1 everywhere
10 RerankingYes (reranker’s own)Reranker · retrievalRerank a bounded candidate setReranking hundreds of candidates per query
11 Filter / compressSometimes (if LLM-based)Code or LLM · retrievalDedupe; extract relevant sentencesSummaries that drop exceptions
12 Prompt constructionDefines inputCode · generationBudgeted, stable prefix firstRemoving safety and grounding rules
13 GenerationYes (largest)LLM · generationModel routing, output limits, cachingWeak model for high-risk questions
14 ValidationSometimesCode or LLM judge · generationSchema and rule checks in codeSkipping citation checks
15 Delivery / monitoringNoObservability · generationLog usage, latency, outcomeLogging only averages

Token limits, pricing and accounting rules depend on the model, the service tier and the API configuration. Treat every number in this guide that is not derived from a formula as an illustration and replace it with the values from your provider’s documentation.

04 · Related concepts

Token optimization and its neighbours

These terms overlap heavily, and teams often mean different things by them. The distinctions below are the ones used in this guide.

ConceptQuestion it answersTypical lever
Token optimizationHow many tokens does each task process, and are they all necessary?Trim context, history, prompts, output
Prompt optimizationDo the instructions produce the desired behaviour reliably?Clear rules, schema, examples
Context engineeringWhat exact information should the model see for this request?Selection, ordering, memory, tool results
Retrieval optimizationDoes the retriever find the right evidence?Chunking, hybrid search, reranking, filters
Model optimizationIs this the right model for this task?Routing, fine-tuning, smaller models
Latency optimizationHow fast does the user get a useful answer?Streaming, parallelism, fewer output tokens
Cost optimizationWhat does a resolved question cost end to end?All of the above, plus caching and avoiding calls

A shorter prompt can cost more

Suppose prompt A uses 8,040 input tokens and succeeds on the first call. Prompt B is trimmed to 5,000 tokens, but in 40% of cases the answer fails validation and the system retries with the full 8,040-token prompt. Expected input per question for B is:

5,000 + 0.40 × 8,040 = 5,000 + 3,216 = 8,216 tokens (vs 8,040 for A)

B also doubles latency for 40% of users and generates two answers for them. The same trap appears when an LLM-based compression step saves 3,000 tokens in the answer call but itself consumes 6,000 tokens. Optimize the end-to-end workflow per resolved question, not a single API request.

05 · Baseline

Measure before you optimize

Without a baseline you cannot tell whether a change saved money, lost quality, or did both. Instrument the current system first: log the token count of each prompt component, the provider-reported usage, latency, and whether the answer was correct.

An illustrative baseline for one dealer request (these are not industry benchmarks):

ComponentIllustrative tokensShare of input
System instructions5006.2%
User question400.5%
Conversation history1,20014.9%
Retrieved context6,00074.6%
Formatting and tool instructions3003.7%
Total input8,040100%
Generated answer (output)400—

Retrieved context is three quarters of the input. That is where optimization should start.

Formulas

Total input = system + question + history + context + formatting/tools
Average tokens per request = Σ (input + output) ÷ number of requests
Tokens per successful answer = Σ (input + output) ÷ number of correct answers
Cost per request = ( uncached_input × P_in + cached_input × P_cached + output × P_out ) ÷ 1,000,000
Cost per resolved question = total cost of all calls (incl. retries, rewrites, judges) ÷ resolved questions
P50 / P95 latency = the 50th / 95th percentile of end-to-end response times

P_in, P_cached and P_out are prices per million tokens. Check how your provider reports cached tokens — some include them inside the input total, so uncached_input = input − cached_input.

Worked example. At hypothetical prices of $1.00 per million input tokens and $4.00 per million output tokens, with no caching:

Input cost = 8,040 × $1.00 ÷ 1,000,000 = $0.00804
Output cost = 400 × $4.00 ÷ 1,000,000 = $0.00160
Cost per request = $0.00964
1,000 requests, 820 resolved → $9.64 ÷ 820 = $0.01176 per resolved question

Quality metrics to record alongside tokens

  • Answer correctness — share of answers judged correct against a reference answer by a reviewer or a validated grader.
  • Retrieval relevance — precision@K (share of retrieved chunks that are relevant) and recall@K (share of required evidence chunks that were retrieved).
  • Groundedness — share of answer claims supported by the supplied context.
  • Citation accuracy — share of citations that point to a chunk that actually supports the cited statement.

Reconcile your local estimates with the provider’s usage fields and invoices weekly. Differences come from message wrappers, tool schemas, tokenizer mismatches and retries you did not log; the bill is computed from provider usage, not from your estimate.

06 · Techniques A–P

Sixteen techniques, from ingestion to the last output token

Each technique below explains what it is, why waste occurs, how it works on the steel dealer chatbot, and how to tell whether it helped. All before-and-after token counts are illustrative; your numbers depend on your documents, models and questions. The query-time path after applying the retrieval techniques looks like this:

price / order status
policy / spec
Dealer question
Router
Pricing or ERP API
Metadata filter: region, audience, version
Vector search
BM25 keyword search
RRF fusion: ~40 candidates
Cross-encoder rerank
Deduplicate
Pack to token budget
Prompt: stable prefix + memory + context
LLM
Validate schema and citations
A

Better document cleaning

PDF and Word extraction returns everything on the page: running headers, footers, page numbers, website navigation, repeated disclaimers, hyphenation breaks and OCR noise. All of it gets chunked, embedded, retrieved and paid for on every request. Cleaning removes text that carries no meaning before chunking, so it never reaches the index.

# Raw extraction (noisy)
Home | Products | Dealers | Contact Us
NorthBar Steel Ltd. | Confidential
Page 4 of 38


3.2  Minimum Order Quan-
tity
TMT bars (Fe 500D, Fe 550D): 5 MT
per delivery, North region.
Excep-tion: orders below 5 MT
for project supply require RM
approval.   ¦¦ ~~ ¦
NorthBar Steel Ltd. | Confidential
Page 4 of 38
# Cleaned
## 3.2 Minimum Order Quantity
TMT bars (Fe 500D, Fe 550D): 5 MT
per delivery, North region.
Exception: orders below 5 MT for
project supply require Regional
Manager (RM) approval.

Cleaning must preserve qualifiers (“North region”), units (“MT”), product identifiers (“Fe 550D”), dates, clause numbers, table structure and legal language. A cleaner that removes all lines under 10 characters would delete “5 MT” rows in a table; a cleaner that joins every hyphenated break would turn “Fe-500D” into “Fe500D”.

Before · 6,000After · 5,100Retrieved-context tokens, illustrative
Implement
Detect lines repeated on most pages of a document and drop them; normalise whitespace; strip known navigation patterns; keep a disclaimer once per document rather than per page; convert tables to Markdown or row-wise text; flag low-confidence OCR pages for review.
Benefits
Fewer chunks, cheaper embedding, cleaner retrieval, smaller prompts. Applies to every future request.
Risks
Over-aggressive rules silently delete facts. Errors propagate into the index until re-ingestion.
Use when
Always. It is the lowest-risk, highest-leverage step when rules are reviewed.
Measure
Token count per document before and after; diff samples by hand; retrieval recall on the evaluation set must not drop.
B

Smarter chunking

A chunk is the unit you embed and retrieve. Its size decides how much text enters the prompt each time it is selected. Oversized chunks waste tokens: a 1,200-token chunk retrieved for a one-line MOQ rule brings 1,100 tokens of unrelated text. Undersized chunks lose context: a 60-token chunk containing “5 MT per delivery” may lose the heading that says it applies only to the North region.

MethodHow it splitsGood for
Fixed-sizeEvery N tokens or charactersQuick baselines; uniform text
Sentence-basedOn sentence boundaries, grouped to a sizeFAQs, short policies
Paragraph-basedOn blank linesWell-written prose documents
Recursive characterTries paragraphs, then lines, then sentences, then characters until under the limitMixed documents; a sensible default
SemanticStarts a new chunk where embedding similarity between sentences dropsLong unstructured text; costs extra embedding calls at ingestion
Structure-awareFollows headings, clauses, table rows; keeps headings attachedManuals, policies, spec tables
Parent-childSmall child chunks are searched; the larger parent section is returnedPrecise matching with enough surrounding context

Overlap repeats the last part of one chunk at the start of the next so a rule split across a boundary is not lost. It helps for flowing prose; it hurts when adjacent chunks are both retrieved, because the overlapping text is then paid for twice. With structure-aware chunking, boundaries already fall at headings and overlap can usually be small or zero.

Characters are not tokens. LangChain’s RecursiveCharacterTextSplitter measures characters by default; a 2,000-character spec table full of numbers may be twice as many tokens as 2,000 characters of prose. Use a token-based length function when the budget matters.

Configuration (illustrative)Chunks sentContext tokensNote
1,200 tokens, 200 overlap, top-556,000Up to 800 duplicated tokens if neighbours retrieved
500 tokens, 100 overlap, top-552,500Check that headings stay attached
300 tokens, 50 overlap, top-882,400More chunks needed to cover the same evidence
Parent-child: 150-token children, 2 parents × 80021,600Precise match, full clause context
Before · 6,000After · 2,500Retrieved-context tokens, illustrative
Implement
Split on document structure first; prepend the section path (“Delivery Policy › North › 3.2 MOQ”) to each chunk; keep tables whole or split by row with the header repeated; measure in tokens.
Benefits
Smaller, more precise context; better retrieval precision.
Risks
Lost context, lower recall, more chunks needed per answer. Every chunking change requires re-embedding.
Use when
Retrieved chunks are mostly irrelevant text, or answers miss context that sat just outside the chunk.
Measure
Recall@K and answer correctness per configuration on the same evaluation set; context tokens per request.
C

Better top-K retrieval

Top-K is the number of highest-scoring chunks the retriever returns. Context tokens scale roughly linearly with K. The dealer’s question needs two pieces of evidence from two documents: the MOQ clause in the Delivery Policy and the document list in the Dealer Onboarding Guide.

KContext tokens (400 each)Recall@K (hypothetical eval)Observation
31,2000.71Often returns three MOQ chunks and no onboarding chunk
52,0000.90Usually both documents
104,0000.93Small gain, double the tokens
208,0000.94Mostly noise; relevant facts buried in the middle

More chunks do not guarantee better answers. Irrelevant chunks dilute attention, can contradict each other (an old MOQ next to the current one), and increase cost and time to first token. Precision is the share of retrieved chunks that are relevant; recall is the share of required evidence that was retrieved. Lowering K raises precision but risks omitting one critical piece of evidence, which makes the answer incomplete rather than slightly worse.

K=10 · 4,000K=5 · 2,000Retrieved-context tokens, illustrative
Implement
Retrieve a larger candidate pool (e.g. 30–50), rerank, then send a smaller K. Pick K per question type from evaluation, not from a blog post.
Benefits
Direct, linear reduction in context tokens.
Risks
Missing evidence for multi-part questions.
Use when
Recall plateaus while K keeps rising.
Measure
Plot recall@K and correctness against K; choose the smallest K above your acceptance threshold.
D

Metadata filtering

Metadata filters remove chunks before similarity search ranks them, so irrelevant text never competes for the top-K slots. Useful fields: product category, document type, region, effective date, customer authorization (dealer, distributor, internal), department, and document version.

A Delhi dealer asking about TMT delivery policy should not receive southern-region delivery chunks unless the question asks for a comparison. Without a filter, semantically similar southern policies fill two of five slots and add 800 tokens of wrong evidence.

filters = {
    "region": ["North", "All"],          # user's region plus company-wide documents
    "audience": ["dealer"],              # from the authenticated session, never from the prompt
    "status": "current",                 # excludes superseded versions
    "doc_type": ["policy", "faq"],
}
Before · 2,000After · 1,200Context tokens at K=5 vs K=3 after filtering, illustrative
Implement
Add metadata during ingestion; derive filters from the user’s session and explicit query terms; include “All” values for company-wide documents.
Benefits
Fewer, more relevant chunks; enforces document permissions as a security control.
Risks
A wrong or missing tag silently removes the correct evidence. A national policy tagged “South” disappears for northern dealers.
Use when
Documents vary by region, product, audience or version.
Measure
Recall with and without each filter; audit metadata coverage; test permission boundaries.
E

Hybrid search and reranking

Semantic vector search compares embeddings and finds passages with similar meaning, even with different wording. It can miss exact strings: “Fe 550D”, “IS 1786”, a document ID or “12 mm” may not be close in embedding space to the right chunk. BM25 is a keyword ranking function that scores exact term matches, weighting rare terms more. Hybrid retrieval runs both and merges the results.

Reciprocal Rank Fusion (RRF) merges ranked lists without needing comparable scores: each chunk scores Σ 1 ÷ (k + rank) across lists, with k commonly set to 60.

Chunk X: rank 1 in BM25, rank 3 in vector → 1/61 + 1/63 = 0.01639 + 0.01587 = 0.03226
Chunk Y: rank 2 in vector only → 1/62 = 0.01613
X ranks above Y because both retrievers agree on it.

A cross-encoder reranker reads the question and each candidate together and outputs a relevance score. It is more accurate than embedding similarity but slower, so it is applied to a bounded candidate set (say 40) to pick the final 3–5.

Rerankers consume their own compute, have their own input limits (long chunks may be truncated) and their own pricing if hosted. Those 40 candidates are read by the reranker, not by the generation model. Retrieval-model token usage and generation-model token usage are separate line items.

Vector top-10 · 4,000Hybrid + rerank top-4 · 1,600Generation context tokens, illustrative
Implement
Run BM25 and vector search in parallel with the same filters; fuse with RRF; rerank the top 30–50; keep a score threshold.
Benefits
Smaller, more relevant final set; exact codes and grades are found.
Risks
Added latency (often tens to hundreds of milliseconds, measure yours), extra infrastructure, reranker truncation of long chunks.
Use when
Queries contain product codes, standards or numbers, or vector results contain many near misses.
Measure
Precision@K and recall@K; P95 latency of the retrieval stage; reranker cost per query.
F

Context compression

Even a relevant chunk is mostly irrelevant to a specific question. Compression reduces each chunk to what the question needs. Approaches, from safest to riskiest:

  • Remove irrelevant sentences using a lightweight relevance score per sentence.
  • Extractive compression — keep original sentences verbatim, drop the rest.
  • Redundancy elimination — keep one copy of a rule stated in several places.
  • Query-focused extraction — an LLM copies only the sentences that answer the question.
  • Abstractive summarization — an LLM rewrites content in fewer words.
  • Contextual compression retrievers — frameworks such as LangChain wrap a retriever with a compressor that filters or extracts from each result.

The dealer eligibility rule appears in the Dealer Onboarding Guide, the Dealer FAQ and a circular, each with surrounding text — about 1,800 tokens. The model needs only the conditions and the exception:

[onboarding-guide#4 | effective 2026-04-01]
Eligibility: registered business with GST registration, minimum 2 years of
trading in construction materials, and covered storage of at least 500 sq ft.
Exception: the 2-year trading requirement does not apply to applicants
referred by an existing authorized dealer in the same district.
Required documents: GST registration certificate, PAN card, shop and
establishment licence, cancelled cheque, last 2 years' audited financials.

The compressed version keeps the exception and every unit. Compression is most dangerous exactly where enterprise answers matter: exceptions (“does not apply to”), negations (“not eligible”), units (MT vs kg), and qualifiers (“in the same district”). Abstractive summaries paraphrase these and can invert them. Prefer extractive compression for policy, legal and numeric content; reserve summarization for narrative background.

An LLM compressor is another model call. If it reads 6,000 tokens to save 4,000 in the answer call, total tokens rise, and latency increases by a full model round-trip. It pays off only with a much cheaper compressor model, cached compressed results, or a very expensive answer model.

Before · 1,800After · 420Eligibility evidence tokens, illustrative
Implement
Start with sentence-level extractive filtering; always keep sentences containing “except”, “unless”, “not”, “only”, dates and numbers near a match; never compress below the clause level for contracts.
Benefits
Large context reductions on verbose documents.
Risks
Dropped exceptions and inverted meaning; extra calls and latency for LLM compressors.
Use when
Chunks are long and relevant facts are a small part of each.
Measure
Exception-coverage tests; groundedness; total tokens across all calls, not just the answer call.
G

Remove duplicate retrieved content

Duplicates enter through overlapping chunks, the same PDF uploaded twice, a policy copied into an FAQ, and one page published at several URLs. Each copy consumes tokens and pushes distinct evidence out of the top-K.

  • Document ID and canonical URL — drop repeated chunks from the same source and version.
  • Content hash — SHA-256 of normalised text catches exact copies across files.
  • Near-duplicate detection — Jaccard similarity over word shingles, MinHash, or embedding cosine similarity catches copies with small edits.

Similar is not identical. The 2025 and 2026 MOQ clauses may differ by one number; North and South delivery rules may share 95% of their text. Compare near-duplicates only within the same region, version and effective date, and keep the newest version when two are truly redundant.

Before · 2,400After · 1,700Context tokens, illustrative
Implement
Deduplicate at ingestion (hashes) and again at query time after reranking (near-duplicates). See the Python section.
Benefits
Lower tokens and more distinct evidence in the same budget.
Risks
Merging versions that differ in a critical number.
Use when
You see the same sentence twice in logged prompts.
Measure
Share of prompt tokens that are duplicate shingles; recall must stay flat.
H

Optimize system prompts

System prompts grow by accretion: each incident adds a sentence, and nobody removes the old ones. Remove repeated instructions, redundant examples, formatting rules the schema already enforces, policy text that belongs in retrieved documents, and explanations written for humans.

# Before (~500 tokens, abridged)
You are a helpful, friendly, professional
assistant for NorthBar Steel, a leading
manufacturer of TMT bars since 1987...
Always be polite. Always be accurate.
Please make sure you are accurate.
Only use the provided documents. Do not
make things up. Never make things up.
Our MOQ policy is 5 MT for North...
Format answers nicely with headings and
bullet points and bold text where useful.
Example 1: ... Example 2: ... Example 3: ...
If unsure, say you are unsure.
# After (~300 tokens)
Role: NorthBar Steel dealer-support assistant.
Grounding: answer only from CONTEXT. If
CONTEXT is insufficient, say so and set
escalate=true. Cite chunk ids for each fact.
Include applicable exceptions and dates.
Access: never reveal content not in CONTEXT;
never discuss other dealers' accounts.
Prices: never state a price; the pricing
service provides prices.
Escalate: legal disputes, credit limits,
complaints -> escalate=true.
Output: JSON matching the provided schema.
One example: {...}

The optimized prompt keeps every rule that changes behaviour: grounding, citation, access control, price handling, escalation and the output schema. It removes the MOQ policy because policy text should come from retrieval, where it can be versioned. Cutting further — for example removing the grounding rule to save 30 tokens — can increase hallucinations, unsafe tool calls and missing escalations.

Before · 500After · 300System-prompt tokens, illustrative
Implement
Version the prompt in source control; delete one rule at a time and run the evaluation set; keep the stable part at the start for caching.
Benefits
Savings on every request; clearer behaviour.
Risks
Safety and compliance regressions that a small eval set does not catch.
Use when
The prompt contains duplicates, embedded policy, or many examples.
Measure
Correctness, refusal and escalation rates, red-team cases, schema-valid rate.
I

Control conversation history

Chat APIs are stateless: the full history is resent on every turn, so a conversation’s cumulative input grows roughly quadratically with its length. A dealer has exchanged 30 messages about grades, districts and delivery dates. The current question needs only three facts: the product grade, the delivery district, and the last clarification.

StrategyWhat is sentTrade-off
Full historyAll 30 messagesComplete but expensive; old, irrelevant turns compete for attention
Recent-turn windowLast N turnsCheap and simple; forgets facts stated early
Rolling summarySummary of older turns + recent turnsCompact; summary can drop or distort details
Structured memoryTyped fields extracted per turnPrecise and auditable; requires a schema
Stable preferencesStored profile (language, district)Never resent as chat; must stay current
Retrieved historyPast turns relevant to the current questionGood for long sessions; adds retrieval work
{
  "product_grade": "Fe 500D",
  "delivery_district": "South West Delhi",
  "dealer_status": "applicant, not yet authorized",
  "last_clarification": "Dealer asked about 12 mm and 16 mm bars only",
  "recent_turns": ["user: And what about the documents?"]
}

Structured memory protects details summaries tend to lose: numbers, codes and negations (“not yet authorized”). Update fields with code or a small extraction model after each turn, and keep the last one or two turns verbatim for pronoun resolution.

Before · 1,200After · 250History tokens, illustrative
Implement
Define a memory schema per use case; persist it server-side; send memory + last 1–2 turns.
Benefits
History cost stops growing with conversation length.
Risks
Forgotten constraints; stale memory after the user changes their mind.
Use when
Sessions regularly exceed a few turns.
Measure
Multi-turn test conversations with facts introduced early and needed late.
J

Query rewriting and query expansion

Query rewriting turns a long or ambiguous message into one concise search query. “Hi, so as I mentioned earlier we’re in Delhi and wanted to check the thing about how much we need to order minimum for the bars and also the paperwork” becomes “TMT bar minimum order quantity North region; authorized dealer required documents”. It also resolves pronouns from memory (“what about 16 mm?” → “Fe 500D 16 mm MOQ North region”).

Multi-query expansion generates several variants (“MOQ TMT”, “minimum order TMT bars tonnes”, “dealer onboarding documents”) and retrieves for each. It improves recall when users and documents use different vocabulary, but costs an LLM call, one embedding per variant, more retrieval work, and a larger candidate pool.

Keep the final context compact by merging the variant results with RRF, deduplicating, reranking against the original question, and packing to the same token budget. Expansion should widen the candidate pool, not the prompt.

Search query · 85Rewritten · 18Final generation context unchanged at the budget, illustrative
Implement
Rewrite with a small model only when the query is long or refers back; never drop product codes or numbers; expand only for low-confidence retrievals.
Benefits
Better retrieval, which lets you use a smaller K.
Risks
Rewrites that change intent; extra latency.
Use when
Conversational queries retrieve poorly.
Measure
Recall with and without rewriting; added latency and cost per query.
K

Dynamic context budgets

A fixed context size is too large for simple questions and too small for complex ones. Classify the question (with rules or a small model) and assign a budget for retrieved context:

Question typeExampleContext budget (illustrative)
Simple factual“What is the MOQ for TMT bars?”≈ 800
Multi-document policyMOQ plus onboarding documents≈ 1,500–2,500
Technical comparisonFe 500D vs Fe 550D for coastal projects≈ 3,000–5,000
High-risk contractualWarranty claim after improper storageEnough for all applicable clauses and exceptions

The ceiling comes from the model: context budget = input limit − system − question − history − tool schemas − reserved output − safety margin. Section 09 works through the numbers. The ceiling is a limit, not a target; a model advertising a very large context window should not be filled for every request.

Implement
Route question types to budgets and K values; for high-risk types, include whole clauses and their cross-references.
Benefits
Small average cost without starving hard questions.
Risks
Misclassification gives a hard question a small budget.
Use when
Question complexity varies widely.
Measure
Correctness per question type; classifier accuracy.
L

Structured output and concise answers

Without guidance, models add greetings, restate the question and append offers of further help. An explicit structure removes that filler and makes answers checkable:

{"answer": "MOQ for TMT bars in the North region is 5 MT per delivery. Orders below 5 MT for project supply need Regional Manager approval. To become an authorized dealer you need: GST registration certificate, PAN card, shop and establishment licence, cancelled cheque, last 2 years' audited financials.",
 "evidence": ["delivery-policy#2", "onboarding-guide#4"],
 "effective_date": "2026-04-01",
 "escalate": false}

Compact JSON is efficient for downstream systems and validators; readable prose is better for people. A common pattern is JSON from the model, rendered to prose by the UI. Savings depend on your actual format: verbose field names, pretty-printing and fields nobody consumes all cost output tokens. Include a field only if something downstream uses it.

Before · 400After · 250Output tokens, illustrative
Implement
Use the provider’s structured-output or JSON mode where available; validate with a schema; render in the client.
Benefits
Fewer output tokens (often the priciest), lower latency, automatic validation.
Risks
Over-terse answers that omit context users need; schema failures.
Use when
Answers feed a UI or another system.
Measure
Output tokens, schema-valid rate, user satisfaction.
M

Model selection and task routing

One large model for every step is the simplest design and rarely the cheapest. Route each task to the cheapest component that does it reliably:

TaskSuitable component
Intent classification, query rewriting, memory extractionSmall, fast LLM or a classifier
Multi-document synthesis, contractual reasoningStronger LLM
Current prices, stock, order status, calculationsDeterministic API or database
Turning text into vectorsDedicated embedding model
Choosing the best candidatesReranker

Routing reduces cost when most traffic is simple. Smaller models may need more retries, follow schemas less reliably and fail on complex synthesis, so a cheaper per-call model can produce a more expensive per-resolution system. Route by evaluated capability, and escalate to the stronger model on low confidence or validation failure.

Measure
Cost per resolved question per route; retry rate; correctness per question type for each model.
N

Prompt caching and reusable prefixes

Some providers can reuse the internal computation for a prompt prefix they processed recently. When a new request begins with exactly the same tokens, those tokens may be served from cache: often billed at a reduced rate and processed faster. The logical token count does not change; the price and latency of the cached portion may.

  • Exact prefix reuse. Caches match from the first token. One changed character early in the prompt invalidates everything after it.
  • Dynamic content placement. Put stable content first (system prompt, tool schemas, fixed examples) and variable content last (memory, retrieved context, question). A timestamp at the top of the system prompt defeats caching.
  • Eligibility and minimums. Providers set minimum prefix lengths; a 300-token system prompt may be too short to qualify.
  • Lifetime. Cached prefixes expire after a period of inactivity; low-traffic routes may rarely hit.
  • Provider differences. At the time of writing, OpenAI’s guide describes caching applied automatically to sufficiently long prompts, while Anthropic’s requires marking cache breakpoints in the request and prices cache writes separately. Minimum lengths, lifetimes and discounts vary by model and change over time — see the references.

Caching is never guaranteed for a given request. Read the cached-token field in the usage response to see what actually hit, and budget as if it might not.

O

Output token limits

Output tokens are generated sequentially, so each one adds latency, and they are commonly priced above input. The API’s maximum-output parameter is a hard stop, not a length target: the model does not plan to fit it, and an answer that reaches it is cut mid-sentence (or mid-JSON, which then fails to parse).

  • Set the limit from observed lengths: for example the P99 of good answers for that route plus a margin.
  • Control typical length with instructions (“at most 120 words”) and structure, not with the limit.
  • Detect truncation from the finish reason in the response and retry or escalate instead of showing a broken answer.
  • Remember that reasoning-style models may spend hidden output tokens before the visible answer; check how your provider counts them.
P

Avoid unnecessary LLM calls

The cheapest generation call is the one you do not make. Options: deterministic routing (keywords, intents), direct database queries, cached answers for stable FAQs, rule-based validation instead of an LLM judge, reusing stored embeddings, batch processing for offline work, and early exits for off-topic or unsupported questions. A query embedding is not an answer-generation call; it is small and cheap, and it can be cached for repeated queries.

“What is today’s price of Fe 500D 12 mm in Delhi?” should never be answered from retrieved PDFs. Prices change, and an LLM may reconstruct a plausible but wrong number. Route it to the authorized pricing API:

import re

PRICE_INTENT = re.compile(r"\b(price|rate|cost)\b.*\b(today|current|now)\b", re.I)
GRADE = re.compile(r"\bFe\s?(500D?|550D?)\b", re.I)
SIZE = re.compile(r"\b(8|10|12|16|20|25|32)\s?mm\b", re.I)

def route(question: str, session: dict, pricing_api) -> dict | None:
    """Return a direct answer, or None to continue to RAG."""
    if not PRICE_INTENT.search(question):
        return None
    grade, size = GRADE.search(question), SIZE.search(question)
    if not (grade and size):
        return None                                     # let RAG ask a clarifying question
    quote = pricing_api.get_price(                      # your authorized internal service
        grade=f"Fe {grade.group(1).upper()}",
        size_mm=int(size.group(1)),
        district=session["district"],
        dealer_id=session["dealer_id"],                 # dealer-specific price, permission-checked
    )
    return {"answer": f"{quote['grade']} {quote['size_mm']} mm: {quote['price']} {quote['unit']} "
                      f"(valid until {quote['valid_until']})",
            "source": "pricing-api", "llm_called": False}

Caching answers needs the same care as caching data: invalidate when the source document changes, key the cache by permissions and region so one dealer never sees another’s terms, set short lifetimes for anything time-sensitive, and never cache user-specific responses in a shared cache.

RAG answer · ≈ 8,440API answer · 0Generation tokens for a current-price question, illustrative
07 · Case study

Before and after, for one dealer request

The optimized scenario applies cleaning and structure-aware chunking, metadata filtering, hybrid retrieval with reranking, deduplication and extractive compression, structured history, and a revised system prompt. These numbers are an illustration of the method, not a promise of achievable savings.

Baseline8,440 tokens
Optimized2,440 tokens
System Question History Retrieved context Formatting Output
ComponentBaselineOptimizedMain technique
System prompt500300H
Question4040—
History1,200250I
Retrieved context6,0001,500A, B, D, E, F, G
Formatting / tools300100H, L
Input total8,0402,190
Output400250L, O
Total8,4402,440

Calculations

1. Baseline input = 500 + 40 + 1,200 + 6,000 + 300 = 8,040
2. Optimized input = 300 + 40 + 250 + 1,500 + 100 = 2,190
3. Baseline total = 8,040 + 400 = 8,440
4. Optimized total = 2,190 + 250 = 2,440
5. Absolute reduction = 8,440 − 2,440 = 6,000 tokens (input only: 5,850)
6. Percentage reduction = (8,440 − 2,440) ÷ 8,440 × 100 = 71.1% (input only: 5,850 ÷ 8,040 = 72.8%)

Costs below use hypothetical prices of $1.00 per million input tokens and $4.00 per million output tokens, no caching, and 300,000 requests per month. They are not any provider’s prices; adjust them in the Tweaks panel.

Cost lineBaselineOptimizedSaving
Input cost per request$0.00804$0.00219
Output cost per request$0.00160$0.00100
7. Cost per request$0.00964$0.00319$0.00645
8. Cost per 1,000 requests$9.64$3.19$6.45
9. Monthly (300,000 requests)$2,892.00$957.00$1,935.00

Why cost fell by 66.9% while tokens fell by 71.1%. Input and output are priced differently. Input fell by 72.8% but output only by 37.5% (400 → 250), and output carries the higher price, so the blended cost reduction is different from the token reduction. With prompt caching, cached input would be priced lower again and the percentages would shift once more. Always compute savings from priced token categories, not from total tokens.

The full picture includes added costs: the reranker, any query-rewrite calls, and extra embedding calls for expansion. Subtract them from the saving.

Success criteria The optimized system is successful only if, on the same evaluation set, answer correctness, retrieval recall, groundedness and critical-exception coverage stay within the thresholds agreed with the business (for example: correctness no more than 1 point below baseline, 100% of exception cases still mention the exception). If resolution drops from 82% to 70%, cost per resolved question and downstream support costs may cancel the saving.
08 · Python implementation

A token-aware RAG pipeline you can run locally

The file below runs end to end with only the Python standard library (Python 3.10+); tiktoken is optional. To keep it runnable without accounts or GPUs, three components are local stand-ins behind clearly defined interfaces: a lexical retriever in place of vector/hybrid search, an overlap scorer in place of a cross-encoder, and an extractive “LLM” that assembles an answer from the context. A provider adapter for a real chat API follows the listing.

"""token_aware_rag.py — minimal, runnable, token-aware RAG pipeline.

Python 3.10+. Optional: pip install tiktoken
Local token counts are estimates; provider-reported usage is authoritative for billing.
"""
from __future__ import annotations

import hashlib
import json
import logging
import math
import re
import time
from dataclasses import dataclass
from typing import Protocol

logging.basicConfig(level=logging.INFO, format="%(message)s")
log = logging.getLogger("rag")

# HYPOTHETICAL prices in USD per 1M tokens. Replace with your provider's current prices.
PRICES = {"input": 1.00, "cached_input": 0.25, "output": 4.00}

# ---------------------------------------------------------------- 1. Token counting
try:
    import tiktoken
    _ENC = tiktoken.get_encoding("o200k_base")   # verify this matches your generation model
except Exception:                                 # not installed, or encoding download failed
    _ENC = None


def count_tokens(text: str) -> int:
    """Estimate tokens. Falls back to ~4 chars/token (English-biased, rough)."""
    if _ENC is not None:
        return len(_ENC.encode(text))
    return math.ceil(len(text) / 4)


# ---------------------------------------------------------------- 2. Cleaning
BOILERPLATE = [
    re.compile(r"^page \d+ of \d+$", re.I),
    re.compile(r"^northbar steel ltd\. \| confidential$", re.I),
    re.compile(r"^(home|products|dealers|contact us)( \| (home|products|dealers|contact us))+$", re.I),
]


def clean_text(raw: str) -> str:
    """Remove boilerplate and noise; keep single blank lines as paragraph breaks."""
    lines: list[str] = []
    for line in raw.splitlines():
        line = re.sub(r"[ \t]+", " ", line.replace("\u00ad", "")).strip()
        line = re.sub(r"[¦~]{2,}", "", line).strip()      # OCR artifacts seen in this corpus
        if any(p.match(line) for p in BOILERPLATE):
            continue
        if not line:
            if lines and lines[-1] != "":
                lines.append("")
            continue
        lines.append(line)
    text = "\n".join(lines).strip()
    # Re-join words hyphenated across lines, lowercase only so codes like "Fe-500D" survive.
    return re.sub(r"([a-z])-\n([a-z])", r"\1\2", text)


# ---------------------------------------------------------------- 3. Chunking
@dataclass
class Chunk:
    chunk_id: str
    doc_id: str
    text: str
    metadata: dict
    tokens: int


def chunk_document(doc_id: str, text: str, metadata: dict,
                   max_tokens: int = 400, overlap_paragraphs: int = 0) -> list[Chunk]:
    """Paragraph-aware chunking bounded by token count.

    Limitation: a single paragraph longer than max_tokens becomes one oversized chunk.
    """
    paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
    chunks: list[Chunk] = []
    current: list[str] = []

    def flush() -> None:
        if current:
            body = "\n".join(current)
            chunks.append(Chunk(f"{doc_id}#{len(chunks)}", doc_id, body,
                                dict(metadata), count_tokens(body)))

    for para in paragraphs:
        if current and count_tokens("\n".join(current + [para])) > max_tokens:
            flush()
            current = current[-overlap_paragraphs:] if overlap_paragraphs else []
        current.append(para)
    flush()
    return chunks


# ---------------------------------------------------------------- 4. Metadata filtering
def matches(meta: dict, filters: dict) -> bool:
    for key, allowed in filters.items():
        value = meta.get(key)
        if isinstance(allowed, (list, tuple, set)):
            if value not in allowed:
                return False
        elif value != allowed:
            return False
    return True


# ---------------------------------------------------------------- 5. Candidate retrieval
def words(text: str) -> list[str]:
    return re.findall(r"[a-z0-9]+", text.lower())


class Retriever(Protocol):
    def search(self, query: str, filters: dict, k: int) -> list[tuple[float, Chunk]]: ...


class LexicalRetriever:
    """LOCAL STAND-IN for vector or hybrid search: IDF-weighted term overlap."""

    def __init__(self, chunks: list[Chunk]):
        self.chunks = chunks
        self.df: dict[str, int] = {}
        for c in chunks:
            for w in set(words(c.text)):
                self.df[w] = self.df.get(w, 0) + 1

    def search(self, query: str, filters: dict, k: int = 20) -> list[tuple[float, Chunk]]:
        q, n = set(words(query)), len(self.chunks)
        scored = []
        for c in self.chunks:
            if not matches(c.metadata, filters):        # filter BEFORE ranking
                continue
            shared = q & set(words(c.text))
            score = sum(math.log(1 + n / self.df[w]) for w in shared)
            if score > 0:
                scored.append((score, c))
        scored.sort(key=lambda s: s[0], reverse=True)
        return scored[:k]


# ---------------------------------------------------------------- 6. Reranking
class Reranker(Protocol):
    def score(self, query: str, texts: list[str]) -> list[float]: ...


class OverlapReranker:
    """LOCAL STAND-IN for a cross-encoder: share of query terms present in the text."""

    def score(self, query: str, texts: list[str]) -> list[float]:
        q = set(words(query))
        return [len(q & set(words(t))) / (len(q) or 1) for t in texts]


# ---------------------------------------------------------------- 7. Deduplication
def shingles(text: str, n: int = 5) -> set[str]:
    w = words(text)
    return {" ".join(w[i:i + n]) for i in range(max(1, len(w) - n + 1))}


def deduplicate(chunks: list[Chunk], threshold: float = 0.85) -> list[Chunk]:
    """Drop exact copies (hash) and near copies (Jaccard), keeping input order.

    Near-duplicates are compared only within the same region and effective date,
    so similar-looking but distinct policies are both kept.
    """
    kept: list[Chunk] = []
    hashes: set[str] = set()
    sigs: list[tuple[tuple, set[str]]] = []
    for c in chunks:
        h = hashlib.sha256(" ".join(words(c.text)).encode()).hexdigest()
        if h in hashes:
            continue
        key = (c.metadata.get("region"), c.metadata.get("effective_date"))
        s = shingles(c.text)
        if any(k == key and len(s & t) / len(s | t) >= threshold for k, t in sigs):
            continue
        hashes.add(h)
        sigs.append((key, s))
        kept.append(c)
    return kept


# ---------------------------------------------------------------- 8. Context budgeting
def context_budget(window: int, max_output: int, fixed_text: str,
                   margin_ratio: float = 0.05, task_cap: int = 3000) -> int:
    """Tokens available for retrieved context = window - output - margin - fixed parts."""
    available = window - max_output - int(window * margin_ratio) - count_tokens(fixed_text)
    return max(0, min(available, task_cap))


def format_chunk(c: Chunk) -> str:
    return f"[{c.chunk_id}] effective {c.metadata.get('effective_date', 'n/a')}\n{c.text}"


def pack_context(ranked: list[Chunk], budget: int) -> tuple[list[Chunk], int]:
    """Greedy packing in rank order. Whole chunks only: never cut a clause mid-way."""
    selected, used = [], 0
    for c in ranked:
        cost = count_tokens(format_chunk(c)) + 2      # +2 for the blank-line separator
        if used + cost > budget:
            continue                                   # a smaller, lower-ranked chunk may still fit
        selected.append(c)
        used += cost
    return selected, used


# ---------------------------------------------------------------- 9. Prompt construction
SYSTEM_PROMPT = (
    "Role: NorthBar Steel dealer-support assistant.\n"
    "Grounding: answer only from CONTEXT. If CONTEXT is insufficient, say so and set escalate=true.\n"
    "Cite chunk ids for each fact. Include applicable exceptions and effective dates.\n"
    "Prices: never state a price; the pricing service provides prices.\n"
    'Output JSON: {"answer": str, "sources": [str], "effective_date": str or null, "escalate": bool}'
)


def build_messages(question: str, memory_json: str, chunks: list[Chunk]) -> list[dict]:
    context = "\n\n".join(format_chunk(c) for c in chunks)
    user = f"MEMORY: {memory_json}\n\nCONTEXT:\n{context}\n\nQUESTION: {question}"
    # Stable system prompt first (cache-friendly); variable content last.
    return [{"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user}]


# ---------------------------------------------------------------- 10. Generation interface
@dataclass
class LLMResult:
    text: str
    input_tokens: int
    output_tokens: int
    cached_input_tokens: int = 0
    finish_reason: str = "stop"


class LLMClient(Protocol):
    def generate(self, messages: list[dict], max_output_tokens: int) -> LLMResult: ...


class ExtractiveLocalLLM:
    """LOCAL STAND-IN, not a language model: answers with the first sentence of each
    context block so the pipeline runs offline. Token counts are local estimates."""

    BLOCK = re.compile(r"^\[([^\]]+)\] effective (\S+)\n(.+?)(?=\n\n\[|\n\nQUESTION|\Z)", re.S | re.M)

    def generate(self, messages: list[dict], max_output_tokens: int) -> LLMResult:
        prompt = "\n".join(m["content"] for m in messages)
        blocks = self.BLOCK.findall(prompt)
        answer = " ".join(body.split(". ")[0].rstrip(".") + "." for _, _, body in blocks)
        dates = sorted(d for _, d, _ in blocks if d != "n/a")
        payload = {"answer": answer or "The available documents do not cover this question.",
                   "sources": [cid for cid, _, _ in blocks],
                   "effective_date": dates[-1] if dates else None,
                   "escalate": not blocks}
        text = json.dumps(payload)
        return LLMResult(text, count_tokens(prompt), count_tokens(text))


# ---------------------------------------------------------------- 11. Cost, usage, latency
def estimate_cost(r: LLMResult, prices: dict = PRICES) -> float:
    uncached = r.input_tokens - r.cached_input_tokens
    return (uncached * prices["input"] + r.cached_input_tokens * prices["cached_input"]
            + r.output_tokens * prices["output"]) / 1_000_000


# ---------------------------------------------------------------- 12. Evaluation
def evaluate(result_text: str, required_facts: list[str], allowed_ids: set[str]) -> dict:
    try:
        data = json.loads(result_text)
    except json.JSONDecodeError:
        return {"valid_json": False}
    answer = data.get("answer", "").lower()
    found = [f for f in required_facts if f.lower() in answer]
    cited = set(data.get("sources", []))
    return {"valid_json": True,
            "fact_recall": len(found) / len(required_facts) if required_facts else 1.0,
            "missing_facts": [f for f in required_facts if f not in found],
            "citations_valid": bool(cited) and cited <= allowed_ids,
            "escalate": data.get("escalate")}


# ---------------------------------------------------------------- Orchestration
def answer_question(question: str, filters: dict, memory: dict, retriever: Retriever,
                    reranker: Reranker, llm: LLMClient, *, window: int = 16_000,
                    max_output: int = 600, task_cap: int = 1_500,
                    required_facts: tuple[str, ...] = ()) -> dict:
    t0 = time.perf_counter()
    candidates = retriever.search(question, filters, k=20)
    if not candidates:                                         # early exit: no LLM call
        return {"answer": "No authorized document covers this question.",
                "escalate": True, "llm_called": False}

    chunks = [c for _, c in candidates]
    try:
        scores = reranker.score(question, [c.text for c in chunks])
    except Exception as exc:                                   # degrade, don't fail
        log.warning("reranker failed, using retrieval order: %s", exc)
        scores = [s for s, _ in candidates]
    ranked = [c for _, c in sorted(zip(scores, chunks), key=lambda p: p[0], reverse=True)]
    ranked = deduplicate(ranked)

    memory_json = json.dumps(memory, separators=(",", ":"))   # compact JSON
    budget = context_budget(window, max_output, SYSTEM_PROMPT + memory_json + question,
                            task_cap=task_cap)
    selected, context_tokens = pack_context(ranked, budget)
    messages = build_messages(question, memory_json, selected)
    t1 = time.perf_counter()

    try:
        result = llm.generate(messages, max_output)
    except Exception as exc:
        log.error("generation failed: %s", exc)
        return {"answer": None, "escalate": True, "error": str(exc), "llm_called": True}
    t2 = time.perf_counter()

    if result.finish_reason == "length":
        log.warning("answer truncated at max_output=%d", max_output)

    record = {
        "candidates": len(candidates), "after_dedup": len(ranked), "sent_chunks": len(selected),
        "context_budget": budget, "context_tokens_est": context_tokens,
        "input_tokens": result.input_tokens, "cached_input_tokens": result.cached_input_tokens,
        "output_tokens": result.output_tokens, "cost_usd_est": round(estimate_cost(result), 6),
        "retrieval_ms": round((t1 - t0) * 1000, 1), "generation_ms": round((t2 - t1) * 1000, 1),
    }
    log.info(json.dumps(record))
    evaluation = evaluate(result.text, list(required_facts), {c.chunk_id for c in selected})
    return {"response": result.text, "usage": record, "evaluation": evaluation, "llm_called": True}


# ---------------------------------------------------------------- Demo (fictional data)
RAW_DOCS = [
    ("delivery-policy", """Home | Products | Dealers | Contact Us
NorthBar Steel Ltd. | Confidential
Page 4 of 38

3.2 Minimum Order Quantity
TMT bars minimum order quantity is 5 metric tonnes per delivery for dealers in the North region. Orders below 5 metric tonnes for project supply require Regional Manager approval.   ¦¦ ~~

3.3 Delivery Window
Deliveries within Delhi NCR are scheduled within 3 working days of order confirmation.""",
     {"region": "North", "audience": "dealer", "doc_type": "policy", "effective_date": "2026-04-01"}),

    ("delivery-policy-south", """3.2 Minimum Order Quantity
TMT bars minimum order quantity is 8 metric tonnes per delivery for dealers in the South region.""",
     {"region": "South", "audience": "dealer", "doc_type": "policy", "effective_date": "2026-04-01"}),

    ("onboarding-guide", """4 Becoming an Authorized Dealer
Required documents to become an authorized dealer: GST registration certificate, PAN card, shop and establishment licence, cancelled cheque, and the last 2 years of audited financial statements. Applicants referred by an existing authorized dealer in the same district are exempt from the 2-year trading requirement.""",
     {"region": "All", "audience": "dealer", "doc_type": "policy", "effective_date": "2026-04-01"}),

    ("dealer-faq-copy", """4 Becoming an Authorized Dealer
Required documents to become an authorized dealer: GST registration certificate, PAN card, shop and establishment licence, cancelled cheque, and the last 2 years of audited financial statements. Applicants referred by an existing authorized dealer in the same district are exempt from the 2-year trading requirement.""",
     {"region": "All", "audience": "dealer", "doc_type": "faq", "effective_date": "2026-04-01"}),

    ("credit-terms-internal", """Internal: dealer credit limits are reviewed quarterly by finance.""",
     {"region": "All", "audience": "internal", "doc_type": "policy", "effective_date": "2026-01-01"}),
]

if __name__ == "__main__":
    all_chunks: list[Chunk] = []
    for doc_id, raw, meta in RAW_DOCS:
        all_chunks += chunk_document(doc_id, clean_text(raw), meta, max_tokens=120)

    out = answer_question(
        "What is the minimum order quantity for TMT bars, and what documents are "
        "required to become an authorized dealer?",
        filters={"region": ["North", "All"], "audience": ["dealer"]},  # from the session
        memory={"product_grade": "Fe 500D", "delivery_district": "South West Delhi",
                "dealer_status": "applicant"},
        retriever=LexicalRetriever(all_chunks),
        reranker=OverlapReranker(),
        llm=ExtractiveLocalLLM(),
        required_facts=("5 metric tonnes", "GST registration certificate"),
    )
    print(json.dumps(out, indent=2))

Running it shows the southern MOQ and the internal credit document removed by filters, the FAQ copy of the onboarding text removed by deduplication, and an evaluation confirming both required facts and valid citations. Change task_cap, max_tokens or the filters to see the effect on context tokens.

Provider adapter for real generation

Provider-specific sketch. This adapter targets the OpenAI Python SDK’s Chat Completions interface. Parameter and field names change between SDK versions and models; verify them against the current documentation before use. Note that the SDK reports cached tokens inside the prompt-token total, which matches estimate_cost above.

# PROVIDER-SPECIFIC — verify against your SDK version. pip install openai
from openai import OpenAI

class OpenAIChatAdapter:
    def __init__(self, model: str, client: OpenAI | None = None):
        self.model, self.client = model, client or OpenAI()   # reads OPENAI_API_KEY

    def generate(self, messages: list[dict], max_output_tokens: int) -> LLMResult:
        resp = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            max_completion_tokens=max_output_tokens,
            response_format={"type": "json_object"},
        )
        usage = resp.usage                                       # authoritative for billing
        details = getattr(usage, "prompt_tokens_details", None)
        cached = getattr(details, "cached_tokens", 0) or 0
        choice = resp.choices[0]
        return LLMResult(choice.message.content or "", usage.prompt_tokens,
                         usage.completion_tokens, cached, choice.finish_reason)

A production vector or hybrid retriever implements the same search(query, filters, k) method; a cross-encoder implements score(query, texts). The orchestration does not change.

Limitations and production controls

  • The lexical retriever and overlap reranker are teaching stand-ins; they do not understand synonyms.
  • The fallback token estimate is crude; the tiktoken encoding may not match a non-OpenAI model at all.
  • The extractive evaluator checks fact strings, not meaning. Use human review or a validated grader for correctness and groundedness.
  • Add before production: timeouts and retries with backoff, rate-limit handling, request IDs, permission checks in the retriever (not just filters passed by the caller), PII redaction in logs, schema validation with a retry on failure, and metrics export to your observability stack.
09 · Token budgeting

A worked budget for a 16,000-token window

Assume a hypothetical generation model with a 16,000-token context window that covers input and output together. Replace this with the documented limits of your model; some models state input and output limits separately.

LineTokensReasoning
Context window (hypothetical)16,000From model documentation
− Reserved output1,000P99 answer length plus margin
− Safety margin (5%)800Tokenizer mismatch, message wrappers, provider overhead
= Usable input14,200
− System instructions400Measured
− Tool schemas and instructions600Function definitions are serialized into input
− User question (cap)200Longer messages are rewritten or rejected
− Conversation memory (cap)1,000Structured memory + last turns
= Maximum retrieved context12,000A ceiling, not a target

The usable budget is smaller than the window because output shares the window, providers add formatting tokens around each message, tool-call payloads and tool results are counted as input, and a local tokenizer may disagree with the provider’s by a few percent. If the input reaches the window, the request fails or the output is cut short.

Then apply a task cap below the ceiling: about 800 tokens for a simple factual question, 1,500–2,500 for a multi-document policy question, 3,000–5,000 for a technical comparison, and whatever the applicable clauses require for a high-risk contractual question, up to the 12,000 ceiling. If even the ceiling cannot hold the required evidence, split the task (answer per document, then combine) or escalate rather than truncating clauses.

10 · Common mistakes

Fifteen ways token optimization goes wrong

01Reducing chunk size without testing recall
Problem. Smaller chunks cut tokens per chunk but separate rules from the headings that scope them.
Example. Chunks drop from 800 to 150 tokens; “5 MT per delivery” loses its “North region” heading.
Fix. Measure recall@K before and after; prepend section paths; consider parent-child retrieval.
02Setting top-K to one for every query
Problem. Multi-part questions need evidence from several documents.
Example. K=1 returns the MOQ clause and never the onboarding document list.
Fix. Choose K per question type from evaluation; use adaptive K with a reranker.
03Removing critical exceptions during compression
Problem. Summaries and aggressive filters drop “except”, “unless” and “not”.
Example. The referral exemption from the 2-year trading rule disappears.
Fix. Use extractive compression; always keep exception sentences; test exception cases.
04Truncating at arbitrary token boundaries
Problem. Cutting text at token N splits clauses, tables and numbers.
Example. A warranty clause ends at “void if stored”, losing “…uncovered for more than 30 days”.
Fix. Pack whole chunks; split on structure; drop lower-ranked chunks rather than cutting one.
05Using character counts as token counts
Problem. Ratios vary by language, numbers and code.
Example. A 4,000-character spec table is budgeted as 1,000 tokens and is actually far more.
Fix. Count with the model’s tokenizer; keep a safety margin.
06Shortening safety prompts too aggressively
Problem. Grounding, access and escalation rules are what keep answers safe.
Example. Removing “never state prices” leads the bot to quote last year’s price list.
Fix. Treat behavioural rules as protected; delete only with eval and red-team evidence.
07Summarizing every document before every request
Problem. An extra LLM call per chunk often costs more than it saves.
Example. Summarizing 10 chunks with a model to shrink a 4,000-token context.
Fix. Precompute summaries at ingestion if needed; prefer extractive filtering at query time.
08Re-embedding unchanged documents
Problem. Full re-ingestion pays embedding cost for text that did not change.
Example. Nightly job re-embeds 40,000 chunks to add 12 new circulars.
Fix. Hash chunk text; embed only new or changed hashes; track model version.
09Caching answers without freshness or permission checks
Problem. Cached answers outlive their sources and cross user boundaries.
Example. An answer with old MOQ is served for weeks; a distributor sees dealer-only terms.
Fix. Key caches by permissions, region and document versions; invalidate on update; set TTLs.
10Using an LLM when a query is enough
Problem. LLMs are slower, costlier and less exact than a lookup.
Example. Order status reconstructed from emails instead of read from the ERP.
Fix. Route deterministic intents to APIs and databases.
11Measuring only token counts
Problem. Token reduction says nothing about correctness.
Example. A 75% token cut ships while resolution falls from 82% to 70%.
Fix. Report cost per resolved question with quality metrics alongside.
12Treating estimates as billed usage
Problem. Wrappers, tool schemas and tokenizer differences change real counts.
Example. Local estimates are 8% below the invoice every month.
Fix. Log provider-reported usage per request and reconcile.
13Assuming prompt caching is guaranteed
Problem. Eligibility, minimum lengths and lifetimes vary.
Example. A timestamp at the start of the system prompt means no request ever hits the cache.
Fix. Put stable content first; read cached-token fields; budget without cache.
14Optimizing for the average request
Problem. Long-tail questions carry the most risk and the largest prompts.
Example. Budget fits typical FAQs; warranty disputes lose key clauses.
Fix. Track P95 tokens and quality per category; give high-risk types larger budgets.
15Ignoring multilingual text and OCR noise
Problem. Non-English text and OCR garbage inflate tokens and hurt retrieval.
Example. Scanned Hindi circulars produce noisy chunks that crowd out clean ones.
Fix. Clean OCR output; test multilingual questions; measure tokens per language.
11 · Evaluation

Proving the optimization did not break anything

Build an evaluation set from real, anonymised dealer and employee questions, labelled with the reference answer and the chunk IDs that contain the required evidence. A few hundred questions spread across categories is more useful than thousands of easy ones. Cover each category deliberately:

CategorySteel company exampleWhat it catches
Simple factualDelivery window in Delhi NCRBasic regressions
Multi-documentMOQ and onboarding documentsTop-K and budget too small
Exact numeric / product codeElongation spec for Fe 550D 16 mmSemantic search misses, unit loss
ExceptionsMOQ for a project supply under 5 MTCompression dropping “except”
Insufficient evidencePolicy for a product not soldHallucination instead of escalation
MultilingualThe MOQ question in HindiToken inflation, retrieval gaps
Permission-restrictedDealer asking about internal credit rulesLeaks through filters or caches
Outdated / conflicting2025 and 2026 MOQ both indexedVersion filtering, dedup merging

Metrics, measured together

Efficiency
Input tokens per request · output tokens per answer · cost per request · cost per resolved question
Retrieval
Precision@K and recall@K where evidence labels exist
Answer quality
Correctness · groundedness · citation accuracy
Operations
P50 and P95 latency · error rate · escalation rate

Running a controlled comparison

  • Run baseline and candidate on the same questions, same index snapshot, same model version, and low or zero temperature.
  • Change one technique at a time so you can attribute gains and losses.
  • Repeat runs or use enough questions to separate real differences from noise; report differences per category, not only overall.
  • Agree acceptance thresholds before running, for example: correctness within 1 point of baseline, recall@K ≥ 0.90, zero permission leaks, all exception cases pass, P95 latency not worse.
  • After offline acceptance, shadow or A/B test on live traffic and compare billed usage, not just logged estimates.

There is a real trade-off curve between tokens and quality. Early cuts (boilerplate, duplicates, irrelevant regions) are usually free; later cuts start removing evidence. Plot tokens per request against correctness for each configuration and pick the point where further savings start costing accuracy. Token reduction is never the only success criterion.

12 · Advanced techniques

Production techniques and what each one actually improves

These techniques address different goals. Some reduce the tokens processed, some reduce the price paid for them, some mainly reduce latency, and some mainly improve quality. Few applications need all of them.

TechniqueWhat it doesPrimary effect
Query-aware retrieval budgetsBudget and K chosen per question typeFewer tokens
Adaptive top-KStop adding chunks when reranker scores drop sharplyFewer tokens
Parent-child retrievalMatch small chunks, return their sectionQuality
Contextual compressionExtract relevant sentences per chunkFewer tokens
Semantic / near-duplicate detectionRemove repeated evidenceFewer tokens
Document versioning, incremental indexingRe-embed only changed documents; retire old versionsIngestion cost Quality
Embedding cacheStore vectors keyed by content hash and modelIngestion cost
Semantic answer cacheReuse answers to similar questions, keyed by permissions and document versionsBillable cost Latency
Batching offline ingestionEmbed in batches; use batch APIs where offeredBillable cost
Model routingCheaper model for simple tasksBillable cost
Prompt-prefix cachingStable prefix reused across requestsBillable cost Latency
Streaming responsesShow tokens as they are generatedPerceived latency
Parallel retrievalRun BM25, vector and API lookups concurrentlyLatency
Token and cost observabilityPer-component token logs, per-route cost dashboardsEnables all others
Rate-limit-aware schedulingQueue and prioritise by tokens-per-minute limitsReliability
Multi-tenant cost controlsPer-tenant budgets, quotas and alertsBillable cost
Regression testingRun the eval set on every chunking, prompt or model changeQuality

Streaming does not reduce tokens or cost; it reduces how long the user waits to see something. Semantic caching reduces cost but is the riskiest item on the list: two similar questions from dealers in different regions may need different answers, so the cache key must include region, permissions and the versions of the source documents.

13 · Decision framework

From symptom to technique

Observed problemLikely causeTechniqueExpected benefitQuality riskValidate by
High input tokensLarge chunks, high K, boilerplateA, B, C, GSmaller contextLost evidenceRecall@K, correctness
High output tokensVerbose answers, no structureL, OLower cost and latencyTruncation, terse answersFinish reasons, user feedback
Poor retrieval relevanceVector-only search, no filtersD, E, JFewer, better chunksWrong filtersPrecision@K by category
Slow first-token latencyLong prompts, sequential stepsC, F, N, streaming, parallel retrievalFaster responseLow if quality heldP50/P95 time to first token
Repeated API costsSame questions and prefixesN, P, answer cacheLower billed costStale or leaked answersCache hit rate, freshness tests
Excessive historyFull transcript resentIFlat history costForgotten constraintsMulti-turn tests
Wrong answers after compressionExceptions or units droppedExtractive F, keep-rulesRestored accuracyFewer savingsException test set
Too many reranker callsLarge candidate pools, every query rerankedSmaller pools, skip rerank on high-confidence hitsLower latency and costLower precisionPrecision@K, retrieval P95
Outdated cached responsesNo invalidation on document changeVersion-keyed cache, TTLsCorrect answersLower hit rateUpdate a doc, re-ask

Prioritization

  1. Measure. Log tokens per prompt component and build the evaluation set.
  2. Remove waste with no evidence in it. Cleaning, deduplication, history control, prompt clean-up, output structure.
  3. Route away from the LLM where a database or API is the correct source.
  4. Improve retrieval with filters, hybrid search and reranking, then lower K.
  5. Add caching and model routing once traffic patterns are known.
  6. Introduce compression last, with exception tests, because it carries the highest quality risk.
14 · Production checklist

Checklist

Before optimization
During optimization
Before production rollout
15 · Summary

Fewer tokens, or better RAG?

Fewer tokens is a measurement. Better RAG is an outcome: the dealer gets the correct MOQ, the applicable exception, the complete document list and a citation, quickly and at a sustainable cost. The two usually move together when you remove waste, and diverge when you start removing evidence.

Principles

  • Optimize the pipeline, not the prompt: most tokens come from retrieval and history.
  • Measure cost per resolved question, not per request.
  • Count with the right tokenizer and reconcile with provider-reported usage.
  • Remove noise and duplicates before removing evidence.
  • Use deterministic systems for prices, stock and calculations.
  • Treat budgets as ceilings and context windows as limits, not targets.
  • Protect exceptions, negations, units and permissions in every transformation.

Implementation order

Instrument → evaluation set → cleaning → deduplication → history control → system prompt and output structure → deterministic routing → metadata filters → hybrid search and reranking → tuned K and dynamic budgets → prompt caching and model routing → extractive compression.

Metrics to watch

Input tokens per request by component, output tokens, cost per resolved question, recall@K, correctness, groundedness, citation accuracy, exception coverage, P95 latency, escalation rate and cache hit rate.

For tomorrow

  1. Log token counts per prompt component for one day of traffic.
  2. Pull 50 real questions, label answers and evidence, and include exceptions.
  3. Read ten logged prompts end to end and mark every duplicate and boilerplate line.
  4. Replace full history with structured memory plus the last two turns.
  5. Route current-price questions to the pricing API.
  6. Re-run the 50 questions and compare tokens, cost and correctness against the baseline.

“Optimize the evidence sent to the model, not just the words in the prompt. The goal is the minimum sufficient context that preserves the maximum reliable answer quality.”

References

Provider features, limits and prices change frequently. Check these pages for current details before relying on any behaviour described above.

All companies, documents, token counts, prices and evaluation scores in this guide are fictional or hypothetical and are used only to illustrate methods.

Leave a Reply

Your email address will not be published. Required fields are marked *

chatbotaiassist programmed to generate human-like responses and provide information on a wide range of topics.
Integration
subscribe us
Get hot news and benefits delivered to your inbox
For Any Question/Query Email: support@chatbotaiassist.com
Copyright 2024 Chat Bot AI assist All Rights Reserved.