Skip to content

    Forward Deployed Engineer

    Context Assembly and Citations for RAG Answers

    Fit retrieved chunks into a token budget, attach citation ids, validate every citation in code, and see why the coverage check missed the q36 refusal.

    By Published 9 min read

    • fde
    • rag
    • context-window
    • citations
    • refusal
    • retrieval
    Share

    This lesson is part of Forward Deployed Engineer: RAG, Search and Data. Enroll for free, then sign in, to read it in full.

    What you'll learn: how to assemble retrieved chunks into a token-budgeted context with citation ids, how code (not the model) validates every citation, and how the capstone refuses a question the documents cannot answer, including the real q36 case where that is wrong.

    At a glance

    Figure 1. From retrieved chunks to a validated, cited answer

    flowchart LR accTitle: From retrieved chunks to a validated, cited answer accDescr: A request first passes a coverage gate that can refuse or ask. Allowed requests retrieve chunks with the access filter inside the query. The chunks are assembled into a context within a token budget and each gets a citation id. The model drafts recommendations with citations. Code then keeps only recommendations with a known product and citations that are in the context and are about that product. A["Request"] --> B{"Coverage gate"} B -- "not in the documents" --> R["Refuse"] B -- "too vague" --> Q["Ask questions"] B -- "ok" --> C["Retrieve with filter"] C --> D["Assemble context within budget, add ids"] D --> E["Model drafts answer with citation ids"] E --> F["Code validates SKU and citations"] F --> G["Cited answer"]

    How to read this: three places can stop or change the answer, the gate before retrieval, the budget during assembly and the validator after the model, and all three are code.

    • The context is a ranked list of chunks labelled [doc_id#n], cut to a token budget; the best chunk is always kept.
    • The model proposes citations; code keeps only those that exist in the context and mention the product.
    • "No citation, no claim": a recommendation without a valid citation is dropped.
    • Refusal is a cheap word-coverage heuristic, and sometimes wrong. q36 is the capstone's own miss.

    Scenario

    Composite scenario, not a real company. A Lumen sales engineer pastes the assistant's answer into a customer email. The customer replies that the source ds-gw-200#9 does not exist, and that a question about a white-label version got no clear answer.

    Nothing checked the model's sources, and nothing said the documents were silent. The sales director wants no invented sources and a clear refusal when the documents say nothing. Figures are illustrative.

    What you will be able to do

    • Fit ranked chunks into a token budget and read the cost of each budget.
    • Explain why citations are checked in code and what the check adds beyond "the id exists".
    • Compute a coverage score by hand and show where the refusal rule fails.
    • Find the smallest budget that keeps an answer unchanged, and say why production should not copy it.

    Prerequisites

    Metadata filters and tenant-aware access control for the filter that decides which chunks can enter the context, and Structured output for why model JSON is validated.

    Core ideas

    The context is a ranked list with a budget

    The Lumen request ("monitor vibration on 40 pumps in a humid plant, budget around 30k, must work with our existing OPC UA setup") retrieves 10 public chunks, at most 3 per document (the capstone's default is 6; 10 makes the budget easier to see). Tokens are the capstone's estimate: words times 1.3, rounded up.

    | Rank | Chunk | Tokens | Running total | |---|---|---|---|---| | 1 | compat-opcua#5 | 54 | 54 | | 2 | ds-gw-200#5 | 29 | 83 | | 3 | faq-licensing#2 | 34 | 117 | | 4 | ds-gw-200#2 | 47 | 164 | | 5 | ds-gw-500#2 | 64 | 228 | | 6 | ds-gw-200#3 | 43 | 271 | | 7 | ds-gw-300#2 | 62 | 333 | | 8 | faq-vs-200#4 | 37 | 370 | | 9 | ds-gw-410#3 | 54 | 424 | | 10 | ds-ct-400#2 | 51 | 475 |

    Predict first. The default budget is 700 tokens (context_budget_tokens in Settings). How many of these 10 chunks does it drop?

    Answer

    None. The total is 475 tokens, under 700, so all 10 fit. The budget did not bind here, nor on any of the 36 golden questions asked as a public user. It matters with long chunks, a larger k or a small model window.

    The assembler:

    • walks the hits in rank order;
    • keeps each that fits and counts the rest as dropped;
    • always keeps the best hit.

    Verified: capstone/fde-product-discovery-copilot/app/retrieval/context.py

    def assemble_context(hits: list[Hit], budget_tokens: int | None, words_to_tokens: float = 1.3) -> Context:
        """Take hits in rank order and keep each one that still fits. The best hit is always kept.
        budget_tokens=None means no budget (every hit goes in)."""
        ctx = Context()
        for hit in hits:
            cost = estimate_tokens(hit.chunk.text, words_to_tokens)
            fits = budget_tokens is None or ctx.tokens + cost <= budget_tokens
            if fits or not ctx.chunks:
                ctx.chunks.append(hit.chunk)
                ctx.tokens += cost
            else:
                ctx.dropped += 1
        return ctx

    budget_table in the lesson code runs it for several budgets (pinned by a test). At budget 10 the best hit is still kept:

    Budget (tokens) Chunks kept Dropped Tokens used
    none 10 0 475
    700 10 0 475
    400 8 2 370
    200 4 6 164
    100 2 8 83
    10 1 9 54

    Order may matter too: "Lost in the Middle" (linked below) reports that models use the start and end of the input best. The capstone keeps rank order; edges_first in the lesson code tries best first, second best last. Its effect is not measured here.

    Citations: the model proposes, code disposes

    Each chunk is rendered as [doc_id#n] text. The model must cite only those ids; the pipeline checks what comes back.

    Figure 2. Where a citation is checked

    sequenceDiagram accTitle: Where a citation is checked accDescr: The orchestrator asks the retriever for chunks, assembles the context and gives each chunk an id. It sends the question, candidate products and the labelled context to the model gateway, which calls the LLM provider. The provider returns draft recommendations with SKUs and citation ids. The orchestrator then keeps a draft only when the SKU is a known candidate and at least one citation is in the context and is about that product. participant O as Orchestrator participant R as Retriever participant G as Model gateway participant L as LLM provider O->>R: search with allowed visibilities R-->>O: ranked hits O->>O: assemble within budget, label [doc_id#n] O->>G: question, candidates, labelled context G->>L: complete with JSON schema L-->>G: drafts with SKU and citation ids G-->>O: validated JSON O->>O: keep known SKU, citations in context and about it

    How to read this: the two self-arrows on the orchestrator are the only places a citation is trusted or dropped, and neither involves the model.

    Verified: capstone/fde-product-discovery-copilot/app/discovery.py

    def _validated(out: ComposeOutput, cands: list[Candidate], ctx: Context) -> list[Recommendation]:
        """Code, not the model, decides what survives: known SKU, real chunk ids that mention it."""
        by_sku = {c.product.sku: c.product for c in cands}
        chunks = {c.ref: c for c in ctx.chunks}
        recs: list[Recommendation] = []
        for draft in out.recommendations:
            product = by_sku.get(draft.sku)
            if product is None or any(r.sku == draft.sku for r in recs):
                continue
            refs = [r for r in dict.fromkeys(draft.citations) if r in chunks and is_about(product, chunks[r])][:3]
            if refs:  # no citation, no claim
                recs.append(Recommendation(sku=product.sku, name=product.name, why=draft.why, citations=refs))
        return recs

    Three rules: a draft for an unknown SKU is dropped; a citation not in the context is dropped (this removes invented ids such as ds-gw-200#9); a citation survives only if is_about says the chunk names the product or belongs to its own document. If nothing survives, the draft is dropped.

    NOTE

    As of 2026-11-02 Anthropic's citations documentation says that, because the API extracts the cited text itself, citations are guaranteed to contain valid pointers to the provided documents. A valid pointer says where the text is, not that it supports the claim. Keep your own check.

    Refusing what the documents cannot answer

    Before retrieval, the gate asks: is the request about this domain, and what share of its topic words appear in the documents and catalogue this caller may see?

    Verified: capstone/fde-product-discovery-copilot/app/retrieval/support.py

    def coverage(text: str, vocabulary: set[str]) -> float:
        terms = significant_terms(text)
        if not terms:
            return 1.0
        return sum(t in vocabulary for t in terms) / len(terms)

    min_coverage is 0.6. A request below it is refused with "I cannot answer this from the documents."

    Word of q36 In the public vocabulary?
    white no
    label yes
    version yes
    lumen yes
    monitor yes
    own yes
    logo no

    The question is "Do you sell a white label version of Lumen Monitor with our own logo?" The expected outcome is a refusal, because the white-label product exists only in the partner guide.

    • Five of seven words are in the vocabulary: 5 / 7 = 0.714, above 0.6.
    • The gate does not fire; the user gets clarifying questions, not a refusal.
    • Nothing partner-only reaches the model (a test asserts it): a quality miss, not a leak.

    Can a better threshold fix it? The test sweeps the rule over the 36 golden questions:

    Threshold Questions the rule gets wrong
    0.5 q27, q29, q30, q36
    0.6 (production) q36
    0.72 q08
    0.8 q08, q10
    • At 0.72 the rule fixes q36 and breaks q08, an answerable question whose coverage is exactly 0.6.
    • No single threshold works: q36 scores 0.714 and q08 0.6.
    • The threshold was also chosen while looking at these questions: the in-sample problem of the next lesson.

    Options are a phrase check, a retrieval-score floor or a model judge; each needs fresh questions. Do not tune on q36 and report it.

    Worked example

    The email has two defects. Assumptions: the Lumen request above, public user, default settings.

    1. Retrieval returns the chunks in the table; the 700-token budget drops none.
    2. Suppose the model cites ds-gw-200#3 and an invented ds-gw-200#9.
    3. _validated finds the first in the context and about the product, the second not in it. The recommendation survives with one citation.
    4. Had the invented id been the only citation, the recommendation would be dropped and "I found matching products but no document evidence I can cite for them." would appear.
    5. For the white-label question, coverage is 5 / 7 = 0.714. The gate passes: a gate defect, not a citation defect.

    Build it

    ./.venv/Scripts/python -m pytest lessons/M3-L5 -v

    The budget table comes from a small function over the capstone's assembler.

    Verified: capstone/fde-product-discovery-copilot/lessons/M3-L5/m3_l5_context.py

    def budget_table(hits: Sequence[Hit], budgets: Sequence[int | None]) -> list[dict[str, Any]]:
        """For each budget: how many chunks fit, how many were dropped, how many tokens were used."""
        rows = []
        for budget in budgets:
            ctx = assemble_context(list(hits), budget)
            rows.append({"budget": budget, "kept": len(ctx.chunks), "dropped": ctx.dropped, "tokens": ctx.tokens})
        return rows

    The threshold sweep is a test, so "no threshold works" is checked on every run.

    Verified: capstone/fde-product-discovery-copilot/lessons/M3-L5/test_m3_l5_context.py

    def test_no_threshold_gets_all_36_questions_right(svc: Services) -> None:
        gold = load_golden()
        assert len(gold) == 36
        assert gate_errors(gold, svc, 0.5) == ["q27", "q29", "q30", "q36"]
        assert gate_errors(gold, svc, 0.6) == ["q36"]
        assert gate_errors(gold, svc, 0.72) == ["q08"]  # fixes q36, breaks q08 (coverage exactly 0.6)
        assert gate_errors(gold, svc, 0.8) == ["q08", "q10"]
        assert all(gate_errors(gold, svc, t) for t in (0.4, 0.5, 0.6, 0.65, 0.7, 0.72, 0.76, 0.8, 0.9))

    The mock never invents a citation, so test_code_side_validation_drops_unsupported_claims builds invented drafts by hand.

    Exercise

    Task. Write smallest_budget(question, step=50, ceiling=700) (a reference solution is lessons/M3-L5/solution_m3_l5_budget.py; write yours first) that returns the smallest budget, in multiples of step, at which run_discovery returns the same SKUs and citations as at the ceiling. Run it on q02 ("Which gateway connects 12 Modbus RTU devices to our OPC UA server? Budget 2000 euros."). Done when your function returns 300 for q02 and a test shows that 250 changes the answer.

    Hint 1: how do you change the budget?

    dataclasses.replace(Settings(), context_budget_tokens=250) copies the frozen settings with another budget; Services.build(settings, MockProvider()) builds services that use it.

    Hint 2: what do you compare?

    Compare (sku, tuple(citations)) pairs, not the whole response: trace ids differ on every run.

    Would you set production to 300? No. It holds for the mock only, and the edge leaves no margin. Pick the budget from your window size and cost target.

    Common mistakes

    • Trusting an id because it looks well-formed. Check membership in the context, not the format.
    • Checking that the cited chunk exists, not that it speaks about the claim's subject.
    • Setting the budget from the average chunk instead of the worst case.
    • Treating a coverage threshold as a truth. It is a heuristic tuned on a small set; keep a list of known misses.

    At the customer: "Can it make up a source?" A good answer: the model proposes ids, code removes any id not in the context or not about the product, and a claim with no valid citation is not shown. The residual risk is a real but weak citation, which the next lesson measures.

    Knowledge check

    1. Why does `assemble_context` always keep the best hit?

    If the first chunk alone exceeds the budget, dropping it would send the model an empty context and invite an unsupported answer. Keeping it shows the failure as an over-long chunk.

    2. The model cites `ds-gw-200#9`, which is not in the context. What happens?

    The validator drops it, because only ids in the supplied context are accepted. If it was the only citation, the recommendation goes too: no citation, no claim.

    3. A provider guarantees valid citation pointers. Why keep the `is_about` check?

    A valid pointer says where a passage is, not that it supports the claim. A shared compatibility note can be a valid source and say nothing about the recommended product.

    4. Why does q36 pass the coverage gate?

    Five of its seven topic words appear in the public documents, so coverage is 0.714, above 0.6. Only "white" and "logo" are absent, and a word count cannot see that the phrase "white label" is missing.

    5. Why not raise the threshold to 0.72 and call q36 fixed?

    At 0.72 the rule refuses q08, an answerable question with coverage 0.6: one error traded for another. Any gain would also be in-sample.

    You can now

    • Assemble ranked chunks into a token budget and read the cost of each budget.
    • Explain why citations are validated in code and what is_about adds.
    • Compute a coverage score and state why a single threshold cannot fix q36.
    • Find the smallest budget that keeps an answer unchanged, and say why not to copy it.

    Further reading

    Last verified: 2026-11-02

    Next

    Evaluating retrieval and answers; failure modes

    Enroll to keep reading

    Lesson 5 of 10 · Forward Deployed Engineer: RAG, Search and Data

    Fit retrieved chunks into a token budget, attach citation ids, validate every citation in code, and see why the coverage check missed the q36 refusal.

    In the rest of this lesson

    • Worked example
    • Build it
    • Exercise
    • Common mistakes
    • Knowledge check
    • You can now

    Enrolling is free. It unlocks every lesson in Forward Deployed Engineer: RAG, Search and Data, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments