- Blaming the model before looking at the retrieved chunks.
- Tuning chunk size with no measurement.
- Fixing one question and not re-running the others.
- Calling a keyword miss a prompt problem.
Follow-up questions.
- How do you tell a paraphrase miss from a ranking miss?
- What do you change if the right chunk is retrieved but cut by the context budget?
- Two documents state different specs. What should the answer do?
Scoring rubric.
Q2. You added a visibility filter to retrieval. How do you prove a public user can never see partner-only content, and what does your evaluation tell the customer?
What it assesses. Whether you enforce access control where the data is selected, test it directly, and report a number with its limits. A strong candidate also says what the metric cannot see.
Answer framework.
- Take identity from a verified token, never from the request body or the model.
- Apply the filter inside the query, before ranking, and in every tool.
- Make "hidden" and "missing" look the same.
- Test the control directly, then measure leaks on questions designed to provoke them.
- Report the number with its sample size, who wrote the set, and what it does not cover.
Strong answer example. Start with where the filter sits.
The index receives a predicate and applies it before scoring. The top results are chosen only among allowed chunks, and the ranking statistics ignore hidden ones. A filter applied after the search would return fewer results than asked for and would let hidden documents influence the order.
Verified: capstone/fde-product-discovery-copilot/app/retrieval/filters.py
def chunk_filter(
chunks: Mapping[str, Chunk],
visibilities: Collection[str],
exclude_superseded: bool = False,
skus: Collection[str] | None = None,
) -> Callable[[str], bool]:
wanted = set(skus) if skus else None
def allowed(chunk_ref: str) -> bool:
c = chunks[chunk_ref]
return (
c.visibility in visibilities
and not (exclude_superseded and c.status == "superseded")
and (wanted is None or bool(wanted.intersection(c.skus)))
)
return allowed
Verified: capstone/fde-product-discovery-copilot/tests/test_retrieval.py
def test_bm25_where_filter_runs_before_ranking() -> None:
items = [(f"doc{i}", "pump sensor") for i in range(5)] + [("secret", "pump sensor pump sensor pump")]
idx = BM25Index(items)
assert idx.search("pump", 1)[0][0] == "secret"
hits = idx.search("pump", 3, where=lambda i: i != "secret")
assert len(hits) == 3 and all(h[0] != "secret" for h in hits)
The evaluation compares three strategies. The baseline has no filter and leaked partner-only content on 5 of 36 questions (q08, q10, q12, q33 and q34). Strategies A and B leaked on none. A test pins the same behaviour.
Verified: capstone/fde-product-discovery-copilot/tests/test_discovery.py
def test_without_the_filter_the_baseline_leaks_partner_content(services: Services) -> None:
q = "What is the partner price for the LUM-VS-900?"
leaked = run_discovery(q, PUBLIC, services, BASELINE)
assert any(ref.startswith("partner-programme-guide") for ref in leaked.context_refs)
assert not run_discovery(q, PUBLIC, services, STRATEGY_A).context_refs
To the customer I would say: zero leaks on 36 questions, all asked as a public user, by the author who also built the filter. That is a regression check, not a proof. It does not cover a second tenant, a person whose access was just removed, or a document with the wrong flag. Here visibility is one flag per document; in production it would come from the source system and lag behind it, so I would test a revoked user.
One more honesty point. Groundedness is 100% under all three strategies, because the mock composes answers only from the context it is given. A metric that cannot fail cannot separate options with this provider; it would be informative with a real model.
Common mistakes.
- Filtering after retrieval, or relying on "do not reveal partner data" in the prompt.
- Reading the role from the request body.
- Evaluating on questions that never try to leak anything.
- Reporting "100%" with no sample size.
Follow-up questions.
- A user loses access at 10:00. When does the index reflect that?
- How would you test isolation between three tenants?
- Can a leak happen through a citation, a clarifying question or an error message?
Scoring rubric.
Q3. Design an agent that can look up products and prepare a quote draft. What limits do you put on it, and where does a human step in?
What it assesses. Whether you keep authority in code. The interviewer looks for read-only versus state-changing actions, argument validation, a bounded loop, and an approval that cannot be reused for different arguments.
Answer framework.
- List the actions and split them: read-only, or anything that changes state.
- Give the model only read-only tools, and validate every argument against a schema in code.
- Bound the loop: step count, time and repeated calls. A tool failure becomes an observation, not a crash.
- Route the state-changing action through an approval gate bound to the exact arguments.
- Check permissions in code, and log each step.
Strong answer example. The model gets three read-only tools: search the catalogue, read a product record, check protocol support. The registry refuses to register anything else, and a request for an unregistered tool, including create_quote_draft, returns an error to the model.
Verified: capstone/fde-product-discovery-copilot/app/workflow/tools.py
def register(self, tool: Tool) -> None:
if not tool.read_only:
raise ValueError("only read-only tools may be registered; use the approval gate for actions")
self._tools[tool.name] = tool
Verified: capstone/fde-product-discovery-copilot/tests/test_workflow.py
def test_runner_cannot_be_talked_into_an_unregistered_tool(ctx: ToolContext) -> None:
evil = json_text({"action": "tool", "tool": "create_quote_draft", "args": {"sku": "LUM-VS-100", "quantity": 1, "customer_ref": "x"}})
result = WorkflowRunner(ScriptedProvider([evil, json_text({"action": "final"})]), default_registry(), ctx).run(Requirements())
assert result.observations[0]["ok"] is False and "Unknown tool" in result.observations[0]["error"]
The loop is bounded by default settings of 14 steps and 5 seconds, and a repeated identical call is skipped and recorded. The time limit is checked between steps, so it cannot interrupt a slow call. A real deployment needs a client timeout as well.
For the quote draft, the model may only ask. The gate hashes the action and arguments, allows one use, and needs a second person with the right role to approve. If the arguments change after approval, execution fails.
Verified: capstone/fde-product-discovery-copilot/tests/test_workflow.py
def test_approval_is_bound_to_exact_arguments_and_single_use() -> None:
gate = ApprovalGate()
approval = gate.request(ARGS, ALICE)
gate.approve(approval.approval_id, BOB)
with pytest.raises(ApprovalError, match="not the ones"):
gate.execute(approval.approval_id, ARGS.model_copy(update={"quantity": 400}))
gate.execute(approval.approval_id, ARGS)
with pytest.raises(ApprovalError, match="used"):
gate.execute(approval.approval_id, ARGS)
assert len(gate.quote_drafts) == 1
Limits of the reference build: approvals are in memory and per process, and create_quote_draft is a library function behind the gate, not an HTTP endpoint.
Common mistakes.
- Putting permissions in the prompt instead of the tool.
- One broad tool that can run anything.
- Approving a description instead of the exact arguments.
- No step or time bound, so a confused model loops until the budget ends.
Follow-up questions.
- Approvers click "approve" without reading. What changes?
- Which actions could skip approval, and who decides?
- How would approval work if the approver answers an hour later?
Scoring rubric.
Q4. A customer asks for "an agent" for product discovery. When would you say no, and what would you build instead?
What it assesses. Judgment about autonomy. The interviewer wants a decision backed by call counts, cost, risk and testability, not a preference for or against agents.
Answer framework.
- Ask whether the steps are known in advance. If they are, write the workflow in code.
- Count model calls per request and what grows with each step.
- List what can go wrong at each step and who carries the risk.
- Ask whether you can list the paths well enough to test them.
- Choose the least autonomy that meets the requirement, and say what would justify more.
Strong answer example. Measure first. In the capstone, the example request makes 9 model calls: 1 to extract requirements, 7 to plan the loop (six tool calls, then "final") and 1 to write the answer.
Verified: capstone/fde-product-discovery-copilot/lessons/REF-rag-agents/test_ref_rag_agents_checks.py
def test_one_request_makes_nine_model_calls_and_seven_of_them_plan_the_tool_loop() -> None:
counting = CountingProvider()
svc = Services(SETTINGS, SERVICES.catalog, SERVICES.store, counting)
run = run_discovery(PUMPS, PUBLIC, svc, STRATEGY_B)
assert dict(counting.calls) == {"extract_requirements": 1, "plan_next_step": 7, "compose_answer": 1}
assert run.response.usage.llm_calls == 9 and run.response.usage.workflow_steps == 6
plan_share = counting.tokens_in["plan_next_step"] / run.response.usage.input_tokens
assert 0.75 < plan_share < 0.80
The planning calls carry 2,162 of the 2,792 input tokens, about 77%, because each one re-sends every earlier observation. At the placeholder prices of 1 and 5 EUR per million input and output tokens, the request costs (2,792 x 1 + 296 x 5) / 1,000,000 = 0.004272 EUR. If code ran the same six tool calls from a fixed script, only extraction and writing would call a model: 630 input and 166 output tokens, or (630 x 1 + 166 x 5) / 1,000,000 = 0.00146 EUR, about 34% of the cost.
Illustrative: token counts are the mock's estimate (1 word is about 1.3 tokens) and the prices are placeholders. Fill in your provider's published prices.
The honest caveat: the mock plans by script, so this says nothing about how a real model would choose. A real model varies the path and the call count, and that variation is the price of agency. I would say no when the sequence is fixed, a step is regulated, or the customer cannot tolerate variance. I would say yes when the next step depends on what the last result says and the paths are too many to write down, with the bounds from Q3.
Common mistakes.
- Building an agent because the customer used the word.
- Quoting a price per call and forgetting the number of calls.
- Saying "agents are unreliable" with no measurement.
- Testing only the happy path of a loop whose path varies.
Follow-up questions.
- What evidence would make you move one step from workflow to agent?
- How do you test a path that changes between runs?
- How do you cap cost per request?
Scoring rubric.
Last verified: 2026-11-02
How to practise
- Run
./.venv/Scripts/python -m pytest lessons/REF-rag-agents -q in the capstone folder and read each test name as a claim.
- Open
eval/results/latest.md, pick one of the five questions listed as not behaving as expected, and say aloud which stage failed.
- Redo the Q4 cost arithmetic with your provider's published prices and explain the difference.
- Describe the approval gate to a friend in three sentences without saying "human in the loop".
- Score yourself with each rubric and repeat the criterion you scored lowest.
Next
Continue to Interview questions: security and reliability, which covers prompt injection, excessive agency, tenant leakage, retries and idempotency, and audit logging.