# Grounding an answer without pretending to have a vector database

Connect grounds an answer in the Knowledge a workspace supplied and the Facts extracted from it, selected by relation — this workspace, this channel, this endpoint, this contact — rather than by embedding similarity. No vector store is deployed. What reaches the model is a budgeted block: four facts and 2,000 characters of knowledge, 700 of memory, 600 for the contact. The budget is the design, and the limit is paraphrase.

- **Status:** Available
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/research/grounding-without-a-vector-database/

## What actually reaches the model

The question a grounding design has to answer is not *which documents are relevant* but *which few hundred characters are worth the latency*. On the realtime voice path the brief is rendered once per call and the session is re-billed for its whole context on every turn — production calls are 92% carried context by input token — so a retrieval step that returns twenty passages in case one helps is not free. It is a second added to every reply.

| Block | Budget | Selected by |
|---|---|---|
| Knowledge | 4 facts, 2,000 characters | The line's purpose and the work in hand, out of `knowledge_bank` sources and the Facts extracted from them |
| Memory | 700 characters | `connect_memory.tiers()` — workspace, channel, endpoint, contact |
| Contact block | 600 characters | The resolved Person and their identities |
| Human voice profile | about 2,500 characters | One preset from `voice_profiles`, if the line uses one |

Those numbers are not taste. Instruction size is recorded on the call (`session.instructions_chars`), and 7,500 characters produced a first token in 1.2–1.8 s against 2.3–3.4 s at 9,600. Grounding that costs two seconds of silence is a worse answer than a shorter one that arrives.

## Selection by relation, not by distance

A similarity search answers *what looks like this question*. The selection here answers *what is true for this relationship*, which is a different question over a much smaller candidate set. Memory resolves narrowest-first through four tiers — workspace, channel, endpoint, contact — and `tiers()` returns an empty tier rather than dropping it, so "nothing is set at this level" is a visible answer rather than an absence you have to infer.

**Knowledge** — What the workspace supplied: documents, pages, pasted material. A source is the thing you can re-read when an answer looks wrong.
**Fact** — One grounded statement pulled out of Knowledge or a conversation, editable on `#/knowledge`. Facts are what the budget counts, because a fact is small and a document is not.
**Memory** — What Connect learned rather than what it was given, held at the tier that owns it. A block lives here as a `block:<channel>` tag, which is why it holds across channels.
**Directive** — Read from the tag list only — `directives()` never parses the body — so a standing instruction cannot be smuggled in as prose.

The Assistant reaches the same material through `search_knowledge`, one of its read-only tools, and writes through `add_knowledge`. There is no second index behind either: the tool calls the service that owns the record, so an answer given in chat and an answer given on a call are grounded in the same rows.

## Why this holds at the size it runs at

A workspace's knowledge is its own material — what it sells, how it quotes, what it will not promise. That corpus is small, curated by the business, and already related to the work by structure: this line, this contact, this channel. Embedding search earns its cost when the candidate set is large and unstructured. Neither is true here, and paying for a vector store would buy recall over a set a filter already narrows to a handful of rows.

The second reason is that a grounding failure has to be loud. Where Knowledge does not support a commercial claim, `safe_sales` refuses it and escalates to a person instead of improvising — the refusal is the behaviour, not a gap. A missing fact therefore surfaces as a held action in Needs You with a reason attached. A similarity search that returns the third-best passage produces a confident sentence instead, which is the failure nobody notices until a customer quotes it back.

## Limits, stated rather than implied

- **Paraphrase is the weak point.** Material stored in one vocabulary and asked about in another is exactly what embeddings are good at and what structural selection is not. A workspace whose Knowledge and whose customers use different words for the same product will feel this first.
- **Four facts is a hard cut, not a ranking guarantee.** If ten facts genuinely bear on a question, six are absent from that turn and nothing on the screen says which six.
- **Long documents are read, not chunked by meaning.** A PDF is read from its text layer where it has one and given to the model where it does not; a spreadsheet is read as a sheet. There is no passage-level index over either.
- **Retrieval precision has not been measured.** There is no recall figure, no answer-accuracy benchmark and no ablation against a vector baseline: UNKNOWN. What is measured is the cost of the prompt the selection produces.

> **Note** What would force a change is a workspace where the selected tier routinely holds more facts than the budget carries. That is a size signal, and it is visible before it becomes a quality problem — the same brief that renders the block knows how much it dropped.

## Questions

### Does Connect use embeddings or a vector database?

No vector database is deployed and no embedding index backs the answers described here. Grounding is Knowledge sources, the Facts extracted from them, and memory resolved through four tiers. A page telling you otherwise would be describing an architecture this one does not have.

### What happens when Knowledge does not contain the answer?

The honest outcome, which is a refusal rather than an invention. On a commercial question — a price, an SLA, a warranty — `safe_sales` refuses and escalates to a person. On the realtime voice path that rule lives in the instructions and a breach is detected after the fact rather than prevented, which is why `price_spoken` counts the lines the voice priced.

### Does adding more Knowledge make answers better?

Up to the budget, then not by itself. Four facts and 2,000 characters reach the model regardless of how much is stored, so a large unstructured upload mostly changes which four are chosen. Fewer, sharper sources attached to the right line usually beat a bigger pile.

## Related

- [Knowledge in Connect](https://connectbyjbrh.com/docs/knowledge/)
- [Memory in Connect](https://connectbyjbrh.com/docs/memory/)
- [Structured business memory instead of a longer prompt](https://connectbyjbrh.com/research/structured-business-memory/)
- [What every character of a prompt costs on a live call](https://connectbyjbrh.com/research/prompt-size-and-first-token/)
- [Why a sales agent should refuse to answer](https://connectbyjbrh.com/research/safe-refusals-in-sales/)
- [Why uncertainty is a valid answer from a business agent](https://connectbyjbrh.com/research/uncertainty-is-an-answer/)

## What this page is based on

- `docs-source/sources/GENERAL.md` §6 — memory tiers and `directives()`
- `docs-source/sources/PHONE.md` §4 and §7 — context billing and the instruction-size measurements
- `docs-source/sources/CHANNELS.md` §6 — the `safe_sales` refusal
- Connect capability registry (docs-source/facts.py) — `knowledge_sources`, `facts_editor`, `memory_tiers`
