Small-model selection
PRINCIPAL PATTERNA high topK prioritizes candidate recall. The large answer model receives only selected evidence, not every candidate. The selector is an application-provided model adapter. Knolo never calls it.
import { query, makeContextPatch, type Hit } from '@knolo/core';
type SelectionDecision = {
action: 'use_selected' | 'requery' | 'insufficient';
selectedIndexes: number[]; rewrittenQuery?: string; confidence: number;
reasonCodes: Array<'direct_match' | 'supports_answer' | 'duplicate' |
'off_topic' | 'missing_evidence'>;
};
// Application-provided model adapters. Knolo never calls a model.
type SelectorModel = { selectEvidence(input: { question: string; candidates: Array<{
index: number; score: number; source?: string; namespace?: string; text: string;
}> }): Promise<SelectionDecision> };
type AnswerModel = { answer(input: { question: string;
context: ReturnType<typeof makeContextPatch> }): Promise<string> };
function uniqueHits(hits: Hit[]) {
const seen = new Set<number>();
return hits.filter(hit => !seen.has(hit.blockId) && Boolean(seen.add(hit.blockId)));
}
export async function answerWithRetrievalGate(pack: Parameters<typeof query>[0],
question: string, selectorModel: SelectorModel, answerModel: AnswerModel) {
const candidates = query(pack, question, {
topK: 40, namespace: ['product', 'support', 'policies'],
queryExpansion: { enabled: true, docs: 4, terms: 6, weight: 0.3 },
});
if (!candidates.length) return 'I could not find grounded evidence for that question.';
let decision: SelectionDecision;
try {
decision = await selectorModel.selectEvidence({ question,
candidates: candidates.map((hit, index) => ({ index, score: hit.score,
source: hit.source, namespace: hit.namespace, text: hit.text.slice(0, 1200) })) });
} catch { return 'Evidence selection is unavailable; I cannot answer safely.'; }
const allowed = new Set(['use_selected', 'requery', 'insufficient']);
if (!allowed.has(decision.action) || !Array.isArray(decision.selectedIndexes) ||
decision.selectedIndexes.length > 8 || decision.confidence < 0 || decision.confidence > 1)
return 'The evidence decision was malformed; I cannot answer safely.';
const selected = [...new Set(decision.selectedIndexes)]
.filter(i => Number.isInteger(i) && i >= 0 && i < candidates.length)
.map(i => candidates[i]);
let finalHits = selected;
const rewritten = decision.rewrittenQuery?.trim().slice(0, 240);
if (decision.action === 'requery' && rewritten) {
const refined = query(pack, rewritten, { topK: 20,
namespace: ['product', 'support', 'policies'],
queryExpansion: { enabled: true, docs: 3, terms: 4, weight: 0.25 } });
finalHits = uniqueHits([...selected, ...refined]).slice(0, 8);
}
if (decision.action === 'insufficient' || !finalHits.length)
return 'The available knowledge does not contain enough evidence to answer confidently.';
const context = makeContextPatch(finalHits, { budget: 'small' });
try { return await answerModel.answer({ question, context }); }
catch { return 'The answer provider failed; please try again.'; }
}The decision includes selected indexes, whether a deterministic re-query helps, an optional rewritten query, confidence, and machine-readable reason codes. A rewritten string goes back through Knolo, so the re-query needs no second model call. If it adds no evidence, retained selected hits may still be used. Otherwise the application abstains.
Only selected or refined original hits, their source identifiers, and a bounded context patch reach the answer model. A privacy-sensitive application can mount and retrieve locally, run a small local selector, and send only selected excerpts to a remote answer model.