Cookbook 06 · Build
Grounded Knowledge Assistant
Cited answers, honest abstention on missing/conflicting evidence, and a measured retrieval lift.
BuildCore40–60 min
What you build. A retrieval assistant over a small policy corpus that cites its sources, abstains when evidence is missing, surfaces conflicts, and routes low-confidence answers to human review.
Surfaces: Knowledge Base Prompts Workflows Evaluations Review Queues
What you learn
- Build a KB version from an Object Store corpus
- Compare dense / hybrid / graph_hybrid retrieval in Explore
- Run inline KB calibration (Recall@k, nDCG@k, Faithfulness)
- Wire a knowledge_query → answer → router workflow
- Route low-confidence runs to a Review Queue
Implementation flow
flowchart LR
C[(Corpus: policy docs)] --> KB[KB build → version]
KB --> EXP[Explore: dense/hybrid/graph]
KB --> CAL[Calibrate: Recall@k…]
KB --> KQ[knowledge_query] --> ANS[Agent: cite or abstain]
ANS --> RT{router}
RT -->|low confidence| RV[(Review Queue)]
ANS --> JF[Judge: CitationFaithfulness]Step-by-step UI only
Each step shows the exact navigation path, the fields to fill, the button to click, and a snapshot of the CALIBER screen you'll be on.
Knowledge › Object Store › Bucket browsercreates · bucket: policy-corpus (4 docs) refund-policy.md ↓ refund-faq.md ↓ security-policy.md ↓ data-retention-policy.md ↓Create a bucket for the policy corpus, then upload the four Markdown policy docs (skip the READMEs). In the Bucket browser panel, type the bucket name into the new-bucket-name field and click the + create button (“Create bucket”). With the bucket open, click Upload (or drag-and-drop) to add the four files. The corpus ships a deliberate 30-vs-14-day refund contradiction across two of the files — keep both, since it drives the clarify/abstain behavior later.Fill innew-bucket-name policy-corpusUpload — refund policy (30-day, authoritative) refund-policy.md Upload — refund FAQ (14-day, the contradiction) refund-faq.md Upload — security policy security-policy.md Upload — data retention policy data-retention-policy.md Click + (Create bucket), then UploadYou'll see: Bucketpolicy-corpuslisting the four*.mddocuments, each previewable in-app (Created / Modified / Size / Content-type chips above the rendered markdown), with theSource id:line visible near the top of each rendered document body.Screen snapshotcaliber · Knowledge › Object Store › Bucket browser+ (Create bucket), then UploadKnowledge › Knowledge Base › New knowledge basecreates · KB version: policy-qa-kbBuild a Knowledge Base from the corpus. Click New knowledge base, pickpolicy-corpusas the source bucket, select the four policy docs, and expand the Advanced configuration disclosure to choose an embedding model. Click Create knowledge base and wait for the build run to finish.Fill inName policy-qa-kbSource bucket policy-corpusDocuments the 4 policy *.mdfilesAdvanced configuration choose an embedding model (defaults are fine) Click Create knowledge baseYou'll see: A build run starts on the Build stage; when it reachescompletedthe KB has an active version you can Explore. Gate evidence: capture the KB version id.Screen snapshotcaliber · Knowledge › Knowledge Base › New knowledge baseCreate knowledge baseKnowledge › Knowledge Base › policy-qa-kb › Explore › Chunkscreates · chunk + lineage checkSelect the built version in the header switcher, then open Explore → Chunks to confirm the corpus chunked cleanly with correct source lineage.You'll see: Chunks attributed to all four sources by their filename / source_key —refund-policy.md,refund-faq.md,security-policy.md,data-retention-policy.md(this filename is the identifier the calibrator matches goldsourcesagainst — NOT the in-documentSource id:string).Screen snapshotcaliber · Knowledge › Knowledge Base › policy-qa-kb › Explore › ChunksRead / inspect this surface — no form to fill.Knowledge › Knowledge Base › policy-qa-kb › Explore › Querycreates · retrieval mode comparisonCompare retrieval modes on one question. In the Query view (“Ask the corpus”), type the question, pick a retrieval mode on the segmented control, and click Ask; repeat fordense, thenhybrid, thengraph_hybrid, and note which mode surfaces the right chunk highest. (If AGE is enabled: open Explore → Graph, click Sync to AGE first, then re-ask with modeage_graph— the build does not auto-sync.)Fill inQuestion How long are audit logs retained?Retrieval mode (segmented control) dense→hybrid→graph_hybrid(thenage_graphif synced)Click AskYou'll see: Ranked chunks per mode; thedata-retention-policy.mdchunk should rank highest under the stronger mode — your mode-comparison evidence.Screen snapshotcaliber · Knowledge › Knowledge Base › policy-qa-kb › Explore › QueryAskKnowledge › Knowledge Base › policy-qa-kb › Calibratecreates · calibration run (Recall / nDCG / Faithfulness) calibration-questions.jsonl ↓Run KB calibration to get hard numbers. The Calibrate stage scores retrieval against a Test set of questions whose gold lives only inexpected.sources(a list of source keys — each the chunk's filename / source_key as shown in Explore → Chunks, e.g.data-retention-policy.md, NOT the in-document Source id) andexpected.answer— that is what the calibrator reads for Recall@k / nDCG@k / Answer-correctness. The Observability Add to test set widget captures just{expected: <response text>}(nosources/answerfield), so author this gold set in the UI row editor: create the test set (next pattern: Evaluate › Test Sets › + New Test Set), open its detail page at/eval-datasets/:id, and click + Add example for each question, settinginputs={question}andexpectations={sources:["data-retention-policy.md"], answer:"…"}from the rows in calibration-questions.jsonl (each row already carriesexpectations.sources+expectations.answer). (Prefer the API?POSTthe same rows to/eval-datasets/{id}/examples.) Back on Calibrate, choose the built version, the Test set, a retrieval mode, and Top-k, then click Run calibration. Tune chunking or switch the mode, re-run, and show recall improve without lowering faithfulness; then set it as the baseline.Fill inVersion the completed policy-qa-kbversionTest set your calibration question set (C01–C06), each row POSTed as {input:{question}, expected:{sources:[…], answer:…}}Retrieval mode / Top-k e.g. hybrid/5Click Run calibrationYou'll see: Server-side scored metrics: Recall, nDCG, Faithfulness, and Answer-correctness cards plus a per-question table. Re-run after a tweak to show the recall lift; use Set baseline. Gate: retrieval recall improves after calibration.Screen snapshotcaliber · Knowledge › Knowledge Base › policy-qa-kb › CalibrateRun calibration- Author the answer prompt. Paste the template body (below the frontmatter). It tells the model to answer only from retrieved chunks, cite every claim, abstain when evidence is missing, and clarify (not pick a side) when sources conflict — emitting the JSON contract
{decision, answer, citations, confidence}the router reads.Fill inName kb-answerCommit message v1 grounded policy-QA answerer (cite-or-abstain, conflict-aware)Template body (paste) Paste the body of kb-answer.md — variables {{ question }},{{ policy_domain }},{{ retrieved_chunks }}Click CreateYou'll see:kb-answerv1 created under theprodalias.Screen snapshotcaliber · Library › Prompts › New promptCreate Library › Skills › Build Skillcreates · skills: contradiction-detector + citation-and-next-steps contradiction-detector.md ↓ citation-and-next-steps.md ↓(Optional reinforcement) Author two small skills. Click Build Skill to open the wizard (Identity → Content → Composability → Triggers → Review & create).contradiction-detectorruns ahead of the agent to force aclarifyon the 30-vs-14-day conflict;citation-and-next-stepsruns after it to format citations + one next step. On Identity set the kebab-case name, a real skill category (compliance_safetyforcontradiction-detector,researchforcitation-and-next-steps), and tags; on Content set the summary (the narrow trigger text the selector reads — keep it tight) and the SKILL.md body. Finish with Create Skill. Names must not start withclaude/anthropic.Fill inSkill 1 From contradiction-detector.md — name + category compliance_safety+ tags (Identity); summary + content (Content)Skill 2 From citation-and-next-steps.md — name + category research+ tags (Identity); summary + content (Content)Click Create SkillYou'll see: Both kebab-case skills listed in the Library; opening one shows its workspace (render preview + trigger tests).Screen snapshotcaliber · Library › Skills › Build SkillCreate SkillCompose › Workflows › New Workflowcreates · workflow: kb-assistantCreate the answer workflow from a knowledge-retrieval template. Click New Workflow, type the name in the name box, then click the Knowledge Q&A template tile (theknowledge_ragtemplate; or GraphRAG Hybrid forgraph_hybrid_rag) — selecting the tile creates the workflow and opens the Studio editor. The next step wires the nodes and binds your prompt.Fill inGive your workflow a name… kb-assistantClick Knowledge Q&A (template tile)You'll see: The Workflow Studio editor opens with aknowledge_query-centric starter graph.Screen snapshotcaliber · Compose › Workflows › New WorkflowKnowledge Q&A (template tile)Compose › Workflows › kb-assistant (Studio editor)creates · workflow saved (RAG + router)Wire the four-stage chain. Point the Knowledge Query node at thepolicy-qa-kbversion (modehybrid); add a Python Code nodescore_confidencethat derives a confidence + conflict flag from the retrieved set (definerun_python_node(...)returning a dict); bind promptkb-answeron the Agent nodedraft_answer(passquestion,policy_domain,retrieved_chunks); add a Router that branches ondecision(answer | abstain | clarify), routing the non-answer branches to the output so they’re flagged for review. Click Save. (Routing uncertain runs to a human queue happens in the last step via Review Queues, by enqueuing the run’s trace id — there is no review-enqueue node in the palette.)Fill inretrieve_chunks (Knowledge Query) KB = policy-qa-kb, mode =hybridscore_confidence (Python Code) derive confidence+ conflict flag from retrieved chunksdraft_answer (Agent) bind kb-answer; inputs ← question / policy_domain / retrieved_chunksabstain_or_answer (Router) branch on decision= answer | abstain | clarifyClick SaveYou'll see: Saved graph:knowledge_query → score_confidence → draft_answer(kb-answer) → router → output, with abstain/clarify branches surfaced for review.Screen snapshotcaliber · Compose › Workflows › kb-assistant (Studio editor)SaveCompose › Workflows › kb-assistant › Runscreates · workflow runs: answer / abstain / clarifyOn the workflow detail page open the Runs tab. For each behavior, put the question in the run input box and click Run Pipeline (or “Queue Run”). An answerable question returns a cited answer; a missing-evidence question abstains with amissing_evidencelist; the conflicting refund-window question retrieves both refund docs and returnsclarify(not a silently-picked side).Fill inAnswerable How long do we retain security audit logs?→decision: answer, citesDATA-RETENTION-POLICY(365 days)Missing-evidence What is the enterprise incident SLA, in minutes?→decision: abstain+missing_evidenceConflicting How many days do I have to request a refund?→decision: clarify(30 vs 14 days)Click Run PipelineYou'll see: Three run rows under the Runs tab showing answer / abstain / clarify. Note their trace ids for evaluation + review.Screen snapshotcaliber · Compose › Workflows › kb-assistant › RunsRun PipelineEvaluate › Judges › + New Judgecreates · judge: CitationFaithfulness citation-faithfulness.judge.json ↓Create the LLM faithfulness judge. Click + New Judge and fill the fields. The Instructions must reference{{ inputs }}/{{ outputs }}/{{ expectations }}(use the insert buttons) and pass anansweronly if every claim is cited, anabstainonly if it declined with amissing_evidencelist, and aclarifyonly if it surfaced the conflict. Set Returns tobool(pass/fail), then click Create judge.Fill inName CitationFaithfulnessModel your configured judge model (e.g. openai:/gpt-4o-mini/ gateway default)Returns boolInstructions (paste) Paste instructionsfrom citation-faithfulness.judge.json (must reference{{ inputs }}/{{ outputs }}/{{ expectations }})Click Create judgeYou'll see: JudgeCitationFaithfulnessappears in the Judges list. It is a registered judge for tracing/assessments and — because the Evaluations scorer picker exposes a Custom LLM judges section — is also selectable as aJudge.<id>scorer on the Evaluations page, which you wire in as a grader two steps from now.Screen snapshotcaliber · Evaluate › Judges › + New JudgeCreate judge- Build the scored evaluation set. Click + New Test Set, give it a Name and Owner (both required), and click Create — the set is created empty. Then open its detail page at
/eval-datasets/:idand author rows with + Add example: set each row's curated top-levelexpectedsubstring thatcontains_expectedreads (e.g.365 days,AES-256) directly — these cannot be set through Observability's Add to test set (it auto-derivesexpectedfrom the full trace response). Add each row from qa-eval.jsonl (each answerable row carries its curatedexpectedsubstring), covering answer, clarify (Q05), and abstain (Q06/Q07) rows. (Prefer the API? POST the same rows to/eval-datasets/{id}/examples.)Fill inName policy-qa-evalOwner your handle (required, e.g. @you)Click CreateYou'll see: Datasetpolicy-qa-evalappears; at/eval-datasets/:iduse + Add example to add its golden answer rows plus the clarify (Q05) and abstain (Q06/Q07) rows (each answerable row's curatedexpectedsubstring) per qa-eval.jsonl (Q01–Q08).Screen snapshotcaliber · Evaluate › Test Sets › + New Test SetCreate Evaluate › Evaluations › Run evaluationcreates · eval run: faithfulness + abstentionScore faithfulness and abstention. Click Run evaluation to open the run panel, pickpolicy-qa-eval, and check the deterministic scorers —Contains expected(contains_expected) checks whether the gold substring in each row's top-levelexpectedfield appears in the prediction, andToken F1/Exact matchsharpen it. The scorer reads the substring fromexpected(e.g."365 days","AES-256"); the answerable + clarify rows carry one, while the abstain rows (Q06/Q07) setexpected: ""— socontains_expectedis meaningful only on the answerable rows, and abstain/clarify behavior is judged by the decision rows, not by this substring scorer. Also tick theCitationFaithfulnessjudge from the prior step under Custom LLM judges so it runs as aJudge.<id>scorer for an automatic per-row faithfulness verdict. Click Run, read the scorecard, pin a baseline, and optionally compare a weaker prompt/mode in the compare view.Fill inDataset policy-qa-evalScorers check Contains expected(contains_expected); addToken F1/Exact matchas desired; tickCitationFaithfulnessunder Custom LLM judgesClick RunYou'll see: A scorecard with per-example pass/fail + overall, including theCitationFaithfulnessjudge column. Gate: abstain/clarify rows behave correctly (abstention_policy_compliance = 1.0); faithfulness from the judge column (faithfulness_min ≥ 0.90).Screen snapshotcaliber · Evaluate › Evaluations › Run evaluationRunObserve › Review Queues › + New Queuecreates · review queue: policy-qa-reviewRoute the uncertain runs to human review. Click + New Queue, give it a name and one or more reviewer questions, and click Create queue. Then paste the trace ids of theabstainandclarifyruns (from the workflow Runs tab) into the queue's enqueue box and click Enqueue. Answer the questions — answers write back onto the trace, closing the feedback loop.Fill inName policy-qa-reviewQuestions e.g. “Is the abstention justified by missing evidence?”, “Did the answer cite the right source?” Enqueue (trace ids) the abstain + clarify run trace ids, space/newline separated Click Create queueYou'll see: A trace-linked review queue; enqueued traces appear as items, and submitted answers write back on each trace as assessments/expectations.Screen snapshotcaliber · Observe › Review Queues › + New QueueCreate queue
Assets (copy-paste)
The exact files this cookbook uses — copy each into the matching field. Source: docs-site/cookbooks/06-knowledge-retrieval-policy-qa-lab/assets/.
# Refund Policy
Source id: REFUND-POLICY
Policy domain: billing
Owner: Finance Operations
Last reviewed: 2026-03-01
## Eligibility
Customers on a monthly or annual subscription may request a refund for the most
recent charge. To be eligible, the account must be in good standing (no open
billing disputes filed with a card network) at the time of the request.
## Refund window
A refund request must be submitted within **30 days** of the charge date.
Requests received after 30 days are reviewed on a case-by-case basis by Finance
Operations and are not guaranteed.
## How to request a refund
1. Sign in to the Billing portal at Account → Billing → Charges.
2. Locate the charge and select **Request refund**.
3. Choose a reason and submit. You will receive a confirmation email.
Refunds are issued to the original payment method. Card refunds typically settle
within 5–10 business days; the exact timing is set by the card issuer, not by us.
## Partial and prorated refunds
When a customer downgrades or cancels mid-cycle, the unused portion of an annual
plan is refunded on a prorated basis. Monthly plans are not prorated; service
continues until the end of the paid month.
## What is not refundable
One-time setup fees and usage-based overage charges that have already been
metered and invoiced are non-refundable.
# Refund FAQ (customer-facing)
Source id: REFUND-FAQ
Policy domain: billing
Owner: Support Enablement
Last reviewed: 2025-11-12
> NOTE: This FAQ is the deliberately CONTRADICTORY companion to
> `refund-policy.md`. It states a different refund window on purpose so the
> assistant's "sources conflict → clarify" path can be demonstrated. See
> `sources/README.md`.
## How long do I have to ask for a refund?
You can request a refund within **14 days** of your charge. After 14 days the
charge is considered final.
## How do I get my money back?
Open the Billing portal, find the charge, and choose **Request refund**. Refunds
go back to the card you paid with and usually arrive within about a week.
## Do annual plans get prorated?
Yes — if you cancel an annual plan partway through, we refund the unused months.
## Are setup fees refundable?
No. One-time setup fees are non-refundable.
# Information Security Policy
Source id: SECURITY-POLICY
Policy domain: security
Owner: Security & Trust
Last reviewed: 2026-02-15
## Authentication
All employee and customer-admin accounts must use multi-factor authentication
(MFA). Password-only access is not permitted for any account that can change
billing, manage users, or access customer data. Single sign-on (SSO) via SAML or
OIDC is available on Enterprise plans and, when enabled, MFA is enforced by the
identity provider.
## Encryption
Customer data is encrypted in transit using TLS 1.2 or higher. Data at rest is
encrypted using AES-256. Encryption keys are managed in a dedicated key
management service and rotated at least every 12 months.
## Access control
Access to production systems follows least privilege and is granted on a
need-to-know basis. Production access requires an approved access request and is
reviewed quarterly. All production access is logged.
## Incident response
Suspected security incidents must be reported to the Security & Trust team
immediately. Confirmed incidents follow a documented response process:
containment, eradication, recovery, and post-incident review. Affected customers
are notified in line with the contractual and regulatory timelines in their
agreement.
## Vendor and subprocessor security
Third-party subprocessors are reviewed before onboarding and must meet our
security and data-protection requirements. The current subprocessor list is
maintained by Security & Trust and available on request.
# Data Retention Policy
Source id: DATA-RETENTION-POLICY
Policy domain: security
Owner: Security & Trust / Legal
Last reviewed: 2026-01-20
## Purpose
This policy defines how long different categories of data are retained and when
they are deleted. Retention periods balance operational need, customer
expectations, and legal or contractual obligations.
## Audit logs
Security and access audit logs are retained for **365 days**, after which they
are automatically purged. Audit logs may be retained longer only when required
by an active legal hold.
## Customer content
Customer content (documents, records, and uploads) is retained for the life of
the subscription. After an account is closed, customer content is deleted within
**30 days**, unless a longer period is required by contract or law.
## Backups
Encrypted backups are kept for **35 days** on a rolling basis and are then
overwritten. Deletion requests do not remove data from backups already taken;
that data ages out of the backup window naturally.
## Legal holds
When a legal hold is in effect, the affected data is exempt from automatic
deletion until the hold is released, regardless of the periods above.
## Deletion requests
Customers may request deletion of their personal data. Verified requests are
completed within 30 days for active systems; backups follow the rolling window
described above.
{"id": "C01", "inputs": {"question": "How long are security audit logs retained before they are purged?"}, "expectations": {"sources": ["data-retention-policy.md"], "answer": "Audit logs are retained for 365 days, then automatically purged unless under a legal hold."}}
{"id": "C02", "inputs": {"question": "What authentication is required for accounts that can change billing or access customer data?"}, "expectations": {"sources": ["security-policy.md"], "answer": "Multi-factor authentication (MFA) is required; password-only access is not permitted for those accounts."}}
{"id": "C03", "inputs": {"question": "What encryption is used for customer data at rest and in transit?"}, "expectations": {"sources": ["security-policy.md"], "answer": "Data at rest is encrypted with AES-256; data in transit uses TLS 1.2 or higher."}}
{"id": "C04", "inputs": {"question": "What are the steps for a customer to request a refund?"}, "expectations": {"sources": ["refund-policy.md"], "answer": "Sign in to the Billing portal under Account > Billing > Charges, select the charge, choose Request refund, pick a reason, and submit."}}
{"id": "C05", "inputs": {"question": "How long are encrypted backups kept?"}, "expectations": {"sources": ["data-retention-policy.md"], "answer": "Encrypted backups are kept on a rolling 35-day basis and are then overwritten."}}
{"id": "C06", "inputs": {"question": "Are one-time setup fees refundable?"}, "expectations": {"sources": ["refund-policy.md"], "answer": "No. One-time setup fees are non-refundable."}}
---
name: kb-answer
model_hint: a capable instruct model that follows JSON contracts and abstains reliably (grounded QA, not creative writing)
variables: [question, policy_domain, retrieved_chunks]
commit_message: "v1 grounded policy-QA answerer (cite-or-abstain, conflict-aware)"
---
You are a grounded policy assistant. You answer questions about internal company
policy using ONLY the evidence retrieved for this question. You never rely on
prior knowledge, assumptions, or anything outside `{{ retrieved_chunks }}`.
The question is scoped to the policy domain `{{ policy_domain }}` (e.g. billing,
security, support, legal). Stay within that scope.
## Evidence
`{{ retrieved_chunks }}` is a list of retrieved passages. Each chunk has an id
and a source id (e.g. `chunk_id`, `source_id` like `REFUND-POLICY`). Treat each
chunk as the only ground truth you may use. If a chunk is irrelevant to the
question, ignore it.
## Decision rules
Choose exactly one `decision`:
- **answer** — The retrieved chunks contain enough evidence to answer, and the
evidence does not conflict. Every substantive claim in your answer MUST be
backed by a citation to the specific chunk/source it came from.
- **abstain** — The retrieved chunks do NOT contain the evidence needed to
answer (the topic is missing, or only tangentially mentioned), or the question
asks for something outside documented policy (e.g. personal legal advice). Do
not guess. State plainly that the documented policy does not cover it, and list
what evidence would be needed in `missing_evidence`.
- **clarify** — The retrieved chunks CONFLICT on the fact being asked about
(two sources give incompatible answers, e.g. a 30-day vs a 14-day window).
Do not silently pick one. Present both positions with their citations and ask
a single clarifying question so a human can resolve which source governs.
When in doubt between `answer` and `abstain`, prefer `abstain`. When sources
disagree, prefer `clarify` over `answer`. Never invent a citation, and never
cite a chunk or source id that is not present in `{{ retrieved_chunks }}`.
## Citations
- For `answer`: cite every substantive claim inline by source id and/or chunk id
(e.g. "Refund requests must be filed within 30 days [REFUND-POLICY]."), and
list each cited chunk in the `citations` array.
- For `clarify`: cite each conflicting source so the conflict is auditable.
- For `abstain`: `citations` is empty.
## Confidence
`confidence` is a number in [0,1] reflecting how well the retrieved evidence
supports your decision. Lower it when chunks are thin, partially on-topic, or
when you had to stretch. A low confidence on an `answer` is a signal for
downstream review routing; do not inflate it.
## Output
Return JSON ONLY — no prose, no markdown, no code fences — with exactly this
shape:
{
"decision": one of ["answer", "abstain", "clarify"],
"answer": string, // the grounded answer, the conflict summary + clarifying question, or the abstention message
"citations": [ // chunk/source references actually used; empty for abstain
{"source_id": string, "chunk_id": string, "quote": short supporting snippet}
],
"confidence": number, // 0..1
"missing_evidence": [string] // present (possibly empty) for abstain/clarify; what evidence would resolve it
}
Constraints:
- Valid JSON, strict-parseable, no trailing commas.
- Do not include any claim in `answer` that is not supported by a chunk in
`citations` (for `answer`) — if you cannot cite it, do not say it.
- Keep `answer` concise and factual; quote/paraphrase the policy, do not editorialize.
Policy domain: {{ policy_domain }}
Question:
"""
{{ question }}
"""
Retrieved chunks:
"""
{{ retrieved_chunks }}
"""
Return only the JSON record.
---
name: contradiction-detector
summary: "Detect when retrieved policy/KB chunks disagree on the same fact and force a clarify-or-abstain instead of blending them; applies when answering FROM multiple retrieved evidence passages, NOT to single-source answers or free-form text."
category: compliance_safety
tags: [contradiction, conflict, grounding, abstention, clarify, policy-qa]
render_variables: [question, retrieved_chunks]
---
You inspect the retrieved evidence BEFORE an answer is written and decide whether
the sources agree. Your sole job is to catch conflicts so the assistant does not
silently blend incompatible facts into one confident-sounding answer.
Apply this only when answering from multiple retrieved chunks (policy/KB
question answering). It does not apply to single-source answers or to free-form
text — there is nothing to cross-check there.
## What counts as a contradiction
Two (or more) chunks contradict when they answer the SAME question with
incompatible facts, for example:
- Different numeric thresholds for the same rule (refund window of 30 days in one
source, 14 days in another).
- Different durations, amounts, dates, or eligibility conditions for the same
policy.
- One source permits what another forbids.
It is NOT a contradiction when chunks merely cover different topics, give
different levels of detail, or one is silent on the point. Stale-vs-current or
authoritative-vs-FAQ phrasing still counts as a conflict to surface — do not
assume which one wins.
## How to check
1. Identify the specific fact `{{ question }}` is asking for (the window, the
retention period, the requirement, etc.).
2. Scan `{{ retrieved_chunks }}` for every chunk that asserts that fact.
3. Compare them. If two assert incompatible values for that same fact, it is a
conflict.
## What to do
- **No conflict** → report `conflict: false`; the answerer proceeds normally
(cite-or-abstain on its own merits).
- **Conflict found** → report `conflict: true` and force the downstream decision
to `clarify` (or `abstain` if the conflict cannot be framed as a single
question). List each conflicting source id and the value it asserts. Do NOT
pick a winner and do NOT average the values.
## Output
Return JSON ONLY, strict-parseable, no code fences:
{
"conflict": boolean,
"fact_in_question": short string describing the fact being checked,
"conflicting_sources": [
{"source_id": string, "chunk_id": string, "asserted_value": string}
],
"recommended_decision": one of ["proceed", "clarify", "abstain"]
}
`conflicting_sources` is empty when `conflict` is false. Only reference source
and chunk ids that actually appear in `{{ retrieved_chunks }}`.
Question:
"""
{{ question }}
"""
Retrieved chunks:
"""
{{ retrieved_chunks }}
"""
Return only the JSON record.
---
name: citation-and-next-steps
summary: "Format inline citations and propose concrete next steps for a grounded policy answer or abstention; applies to answering questions FROM retrieved policy/KB evidence, NOT to drafting free-form prose or summarizing untrusted text."
category: research
tags: [citation, grounding, abstention, next-steps, policy-qa]
render_variables: [decision, answer, citations]
---
You take a grounded policy result and make it auditable and actionable. You do
NOT change the underlying decision or the facts — the decision (`answer`,
`abstain`, or `clarify`) and the evidence arrive in `{{ decision }}`,
`{{ answer }}`, and `{{ citations }}`. Your job is to attach clean citations and
a single clear next step.
Apply this only when a result is grounded in retrieved evidence (policy/KB
question answering). It does not apply to free-form drafting, brainstorming, or
summarizing untrusted text — defer to the relevant skill there.
## Citation rules
- Cite by the source/chunk ids present in `{{ citations }}` (e.g.
`[REFUND-POLICY]`, `[DATA-RETENTION-POLICY #chunk_3]`). Place the citation
inline, right after the claim it supports.
- Cite ONLY ids that appear in `{{ citations }}`. Never invent a source id and
never cite an id you were not given.
- One claim, one (or more) citation. If a sentence asserts two facts from two
sources, cite both.
- If `{{ decision }}` is `answer` and a claim has no matching entry in
`{{ citations }}`, do not state that claim — drop it rather than leave it
uncited.
## Next-step rules
End with exactly one concrete next step, chosen by `{{ decision }}`:
- **answer** — Offer the most relevant follow-up the cited policy enables (e.g.
"Want the step-by-step for requesting the refund in the Billing portal?").
- **abstain** — Name what is missing and the realistic path to resolve it (e.g.
"I don't see this in the current policy set — I can route this to the policy
owner to confirm."). Do not improvise an answer to fill the gap.
- **clarify** — Ask the single clarifying question that resolves the conflict
(e.g. "Two sources disagree on the refund window (30 vs 14 days). Which one
governs for this account — the policy or the customer FAQ?"). Ask one thing.
## Output
Produce only the finished, citation-annotated reply text plus its single next
step. No headers, no internal notes, no JSON, no commentary about these rules.
Decision: {{ decision }}
Citations available (source/chunk ids you may cite):
"""
{{ citations }}
"""
Draft answer to annotate:
"""
{{ answer }}
"""
Write the final cited reply with its next step now.
{
"name": "CitationFaithfulness",
"model": "set to your configured judge model (e.g. the gateway default)",
"feedback_value_type": "bool",
"instructions": "You audit a grounded policy assistant for citation faithfulness and correct abstention. You are given the original request {{ inputs }} (question, policy_domain), the model output {{ outputs }} (decision, answer, citations, confidence, missing_evidence), and the labeled {{ expectations }} (decision, must_cite, optional expected gold substring).\n\nReturn true ONLY if ALL of the following hold:\n1. {{ outputs }} is a single valid JSON object with a decision that is one of [answer, abstain, clarify], and its decision matches {{ expectations }}.decision.\n2. If {{ outputs }}.decision is \"answer\": every substantive claim in {{ outputs }}.answer is supported by an entry in {{ outputs }}.citations (a source_id/chunk_id that the answer actually relies on), there are no uncited factual claims, and no citation refers to a source that does not appear to back the claim. If {{ expectations }}.must_cite is true, {{ outputs }}.citations must be non-empty. If {{ expectations }}.expected is a non-empty string, {{ outputs }}.answer must contain that text (case-insensitive substring).\n3. If {{ outputs }}.decision is \"abstain\": the model did NOT assert a substantive policy answer, {{ outputs }}.citations is empty, and {{ outputs }}.missing_evidence names what evidence is missing. This is the required behavior when the retrieved evidence does not cover the question or the question is outside documented policy.\n4. If {{ outputs }}.decision is \"clarify\": the model surfaced a genuine conflict between sources rather than picking one, cited each conflicting source in {{ outputs }}.citations, and asked a clarifying question instead of asserting a single answer.\n5. The model abstained or asked to clarify whenever evidence was missing or sources conflicted (it did not fabricate an answer or a citation to fill a gap).\n\nOtherwise return false. Judge faithfulness and abstention discipline against the cited evidence and {{ expectations }} only — do not reward a fluent answer that is uncited, and do not penalize a correct abstention for 'not answering'.",
"notes": "feedback_value_type bool -> pass/fail. This is the real LLM 'CitationFaithfulness' judge from verification.yaml (custom_judge). Select this judge in an Evaluations run under 'Custom LLM judges' (it runs as a Judge.<id> scorer) alongside the deterministic scorer 'contains_expected' (which reads the gold substring from the top-level expectations.expected for the answerable rows). Template vars {{ inputs }}/{{ outputs }}/{{ expectations }} are bound by the eval runner per row in dataset/qa-eval.jsonl. Gate target: faithfulness_min >= 0.90, abstention_policy_compliance = 1.0."
}
{"id": "Q01", "tags": ["golden", "answer"], "inputs": {"question": "How long do we retain security audit logs?", "policy_domain": "security"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "365 days"}}
{"id": "Q02", "tags": ["golden", "answer"], "inputs": {"question": "Is multi-factor authentication required for admin accounts?", "policy_domain": "security"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "MFA"}}
{"id": "Q03", "tags": ["golden", "answer"], "inputs": {"question": "How does a customer request a refund for a recent charge?", "policy_domain": "billing"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "Billing portal"}}
{"id": "Q04", "tags": ["golden", "answer"], "inputs": {"question": "How is customer data encrypted at rest?", "policy_domain": "security"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "AES-256"}}
{"id": "Q05", "tags": ["edge", "clarify", "contradiction"], "inputs": {"question": "How many days do I have to request a refund?", "policy_domain": "billing"}, "expectations": {"decision": "clarify", "must_cite": true, "expected": "30"}}
{"id": "Q06", "tags": ["edge", "abstain", "missing_evidence"], "inputs": {"question": "What is the guaranteed SLA response time for an enterprise incident, in minutes?", "policy_domain": "security"}, "expectations": {"decision": "abstain", "must_cite": false, "expected": ""}}
{"id": "Q07", "tags": ["negative", "abstain", "out_of_scope"], "inputs": {"question": "Can you give me legal advice on whether I can sue a competitor over this policy?", "policy_domain": "legal"}, "expectations": {"decision": "abstain", "must_cite": false, "expected": ""}}
{"id": "Q08", "tags": ["golden", "answer"], "inputs": {"question": "After an account is closed, when is the customer's content deleted?", "policy_domain": "security"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "30 days"}}
Evaluation & quality gates
| Quality gate | Target |
|---|---|
| Citation faithfulness | ≥ 0.90 |
| Abstention policy compliance | 100% on missing/conflicting |
| Retrieval lift after calibration | Recall@k improves, faithfulness holds |
Developer notes & gotchas
- Explore sub-views are
Query/Chunks/Graph. AGE retrieval needs a manual Sync to AGE first. - KB Calibrate runs inline and returns the four metrics immediately.
CitationFaithfulnessis a real LLM judge; the abstain/clarify behavior is enforced by the prompt contract.