The path from a natural-language query to a token-budgeted context bundle.
Product-level explanations of the same behavior (with trade-offs) are in
business logic; this page is the
maintainer's view. Entry points: retrieval/orchestrator.py
(RetrievalOrchestrator), analysis/context_synthesizer.py.
llm/query_classifier.py scores weighted regex patterns per task type
(explain, debug, extend, review, locate, general);
confidence = min(1, score / (0.5 × max possible)), boosted up to +30% by
the gap to the runner-up. TASK_PROMPTS holds per-type expert personas
used by ask.
RetrievalOrchestrator.retrieve():
- Quoted queries (
"exact name") route to the exact-match engine (exact_query_engine.py). - Otherwise semantic retrieval through the hybrid index.
- On semantic failure → lexical fallback with keyword extraction; errors surfaced, mode recorded (never a silent empty result).
- Results deduplicated to
limit_entitiesunique entities. - Dependency expansion (
completeness.py): 1-hop callees pulled in, labeledsource="dependency", score 0.0 — completeness without polluting evidence ranking. - Per-entity token budget:
max(1, min(2000, max_tokens // limit_entities))— the 200-token floor was removed in BL-29 (src/knowcode/retrieval/orchestrator.py:185).
retrieval/hybrid_index.py:
- BM25 (sparse) and vector (dense) retrievers run in parallel, each
contributing
limit × 3candidates (CANDIDATE_FACTOR = 3). - Fused by Reciprocal Rank Fusion with
RRF_K = 60:
score += (1 − α) / (60 + sparse_rank + 1) # α = hybrid_alpha, default 0.2
score += α / (60 + dense_rank + 1)
- Candidate IDs are deduplicated by best rank before fusion; a dense ID unresolvable in the chunk repository is skipped without consuming a result slot.
HybridIndex.__init__'s own default foralphais 0.2, matching theAppConfigdefault; the service still passesalpha=self.app_config.hybrid_alphaexplicitly, so a config change never depends on the constructor default.
retrieval/reranker.py reranks the top limit × reranker_top_k_multiplier
(default 5) fused candidates with the VoyageAI cross-encoder
(rerank-2.5). Any provider error degrades to deterministic signal
scoring — multiplicative boosts: docstring ×1.2, modified < 7 days ×1.1,
query substring in content ×1.5, query == chunk kind ×2.0.
analysis/context_synthesizer.py:
TASK_TEMPLATESper task type define section priority order and budget boosts (e.g., DEBUG: source ×2.0, callers ×1.5).- Greedy packing in priority order under
min(max_tokens × boost, max_tokens); raw source capped at 50% of budget in task mode; relationship lists capped (10 callers/callees, 15 children, 200-char docstring previews). verbosityladder:minimal(context + sufficiency + tokens + reduction summary) →standard(+ query/mode/truncation) →verbose(+ evidence) →diagnostic(tests only).summarize=Trueomits raw source entirely.- Sufficiency (
_calculate_sufficiency): position-weighted coverage of the template priority list (weight = 1/(i+1)), +0.2 source bonus, +0.1 docstring > 50 chars, ×0.5 when the bundle is under 100 characters; normalized by max score.
Source inclusion is governed by
src/knowcode/retrieval/response_profiles.py: query/context responses
are summary-first, and raw source is included only when verbosity ≥
standard or the task type is source-hungry (debug, review — pinned by
tests/unit/retrieval/test_response_profiles.py:44). search/trace never
carry source; semantic_search always does. This supersedes the older
summarize=True knob described above.
llm/agent.py (smart_answer) escalates: 1500 tokens/1 entity minimal →
3000/3 + deps → same at standard verbosity → LLM. Local answering
requires the task type in local_answer_task_types (force-cleared at
config load; only a SHA-pinned machine-verified artifact repopulates it —
see routing_policy.py and retrieval evals) and
sufficiency ≥ max(sufficiency_threshold, routing_quality_floor). One
logical query = one telemetry query event across all attempts.
The LLM leg (agent.answer) iterates configured models with a
client-side RateLimiter (per-model RPM/RPD), failing over on provider
resource exhaustion. Prompts follow llm/prompt_contract.py: system
channel carries instructions; question + context travel as one
JSON-escaped untrusted-input envelope with per-field caps (context 120k
chars, question 8k chars, error echo 200 chars).