Knowledge base (RAG)
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
The knowledge base (KB) is server-side, group-scoped retrieval-augmented generation (RAG). When an assistant-class chat runs for a user whose group has a KB, the server transparently lets the model search that KB and grounds the answer in the retrieved passages — returning structured citations the client can render as sources. The model never receives a wider scope than the caller is entitled to.
A KB is modelled as a server plugin (Kind = "mcp_server", config {"type":"knowledge_base",…})
and is switched on for a group through the existing group-plugin allowlist. The feature is
behaviour-neutral until you opt in: with no vector DB configured the tool is never injected, the
ingestion worker idles, and the admin KB pages show a “vector DB required” notice.
The KB is guarded by two independent gates — both must be satisfied:
- License entitlement — the
Knowledgelicense feature must be granted. Without it the chat path simply runs normal (ungrounded) chat with no error, and the ingestion REST API returns403 feature_not_licensed. - Vector DB configured —
Knowledge:ConnectionStringmust point at a pgvector database. Without it the KB is operationally disabled (the ingestion API returns503 knowledge_disabled).
Storage — a dedicated vector database
Section titled “Storage — a dedicated vector database”KB content lives in its own PostgreSQL database (the pia-vectordb container, running the
pgvector extension), separate from the main application database.
| Store | Context | Holds |
|---|---|---|
| Main DB | PiaDbContext | Users, groups, the plugin catalog + group allowlist, token usage |
| Vector DB | KnowledgeDbContext | Documents, chunks + embeddings, the embedding-token ledger |
The two stores share no foreign keys — the vector DB references the KB catalog by PluginId
value only. The embedding dimension is fixed at compile time (one vector(N) column + one HNSW
index); startup validates the configured dimension against it and disables the KB on mismatch, because
changing it requires a migration and a full re-embed.
Ingestion pipeline
Section titled “Ingestion pipeline”Ingestion is asynchronous. A submitted document is hashed (SHA-256, for idempotent de-duplication),
persisted as Pending, and drained by a background worker:
Pending → extract text → semantic double-pass chunk → (contextual retrieval) → embed → store chunks → Ready- Chunking uses a semantic double-pass merging splitter (ported from the n8n recipe); the chunker and the embedding model are instance-level settings, never per-KB.
- Contextual retrieval (optional, on by default) prepends a one-sentence, LLM-generated description
of each chunk before embedding (
context + delimiter + raw chunk), which measurably improves recall. The raw chunk is what gets stored and cited. Contextualization needs its own chat provider; if none is configured it is auto-disabled at startup with a warning rather than failing ingestion. - Resilience: the embedding and contextualizer HTTP calls retry on rate-limit/transient errors and
time out; a per-chunk contextualization failure degrades gracefully (the raw chunk is still embedded)
instead of failing the whole document; and a worker that crashes mid-document resets stranded
Processingrows toPendingon its next start.
A document that cannot be processed is marked Failed with an error message, visible in the admin UI.
Retrieval
Section titled “Retrieval”Retrieval is hybrid and always scoped in SQL to the caller’s allowed KB ids:
- Semantic candidates via pgvector cosine distance over an HNSW index.
- Lexical candidates via PostgreSQL full-text search over the raw chunk.
- The two lists are fused with Reciprocal Rank Fusion (RRF) and re-ranked (identity re-ranker in v1, behind a swappable seam).
The store never returns a chunk outside the allowed set — the scope predicate is re-applied even on the final re-fetch (defense in depth).
The tool loop and streaming
Section titled “The tool loop and streaming”Retrieval is exposed to the model as a single search_knowledge_base tool, injected only when the mode
is assistant-class (Assistant / Research — never Optimize or voice) and the user’s group enables a
KB. KnowledgeChatOrchestrator runs the loop server-side:
- KB tool calls are resolved on the server and the conversation loops with the results appended (up to an iteration cap).
- Any client-declared tools the request carried are passed straight back to the client to execute. If a turn mixes a client tool call with a KB call, the KB call is resolved first and the client call deferred to the next round, so a KB lookup is never silently dropped.
- The KB tool and its call/result messages are stripped from everything the client sees; the
retrieved passages are returned as structured
knowledge_citations, never inlined as prose.
When the client requests stream:true, the tool-resolution rounds run buffered (a tool call can’t be
resolved before its arguments are complete), then the final grounded answer is streamed as Server-Sent
Events. A look-ahead guard guarantees no tool-call bytes leak mid-stream; citations arrive as a trailing
knowledge_citations event immediately before data: [DONE].
The number of search_knowledge_base calls resolved during a turn is recorded on that request’s token-usage
row, so KB activity surfaces alongside model spend in the KB column of the admin
token-usage view (/admin/token-usage). This is distinct from the embedding-token
ledger below, which tracks ingestion-time spend.
Quotas
Section titled “Quotas”Per-group quotas (GroupSettings.Quotas, edited in the group editor) bound KB usage:
| Quota | Enforced |
|---|---|
KnowledgeBases | When enabling KBs for a group |
KnowledgeDocuments / KnowledgeStorageBytes | Per KB at ingestion → 409 |
MonthlyEmbeddingTokens | Per group, against a dedicated embedding-token ledger |
The effective doc/byte limit for a KB is the most permissive among the groups that enable it (else the instance baseline). For monthly tokens, ingestion pre-checks an estimate against that most-permissive limit and the worker records the actual provider token count afterward; a KB shared across groups attributes its spend to a single deterministic owner (the lowest group id) so usage is never double-counted.
Privacy and tenant isolation
Section titled “Privacy and tenant isolation”Tenant scoping is server-resolved: the set of allowed KB ids comes from the authenticated user → group
→ group-plugin allowlist. The model only ever supplies the query text and topK; it cannot widen the
scope. Documentation: see Configuration → Knowledge base and the
admin setup guide.