
Knowledge Base Manager
- 87 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
knowledge-base-manager is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- knowledge-base-manager
- AI & Agent Building
- AI-coding skill
Knowledge Base Manager by the numbers
- 87 all-time installs (skills.sh)
- +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #4,949 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill knowledge-base-managerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 87 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Knowledge Base Manager
Overview
Provides a structured methodology for selecting, designing, and governing knowledge bases. Covers architecture decisions (document-based vs entity-based vs hybrid), content curation, quality metrics, versioning strategies, and maintenance governance. Use when choosing a KB architecture, establishing curation workflows, or building governance processes for organizational knowledge.
When NOT to use: Static documentation suffices, fewer than 50 FAQ items cover all questions, or no maintenance resources are available. For implementing retrieval pipelines (chunking, embeddings, vector stores), use the rag-implementer skill. For implementing knowledge graphs (ontology, entity extraction, graph databases), use the knowledge-graph-builder skill.
Quick Reference
| Aspect | Options | Key Considerations |
|---|---|---|
| Architecture | Document-based (RAG), Entity-based (Graph), Hybrid | Match to query patterns; start simple, add complexity when needed |
| Document-based | Vector DB (Pinecone, Weaviate, pgvector) | Best for docs, FAQs, manuals; semantic search; easy to add content |
| Entity-based | Graph DB (Neo4j, ArangoDB) | Best for org charts, catalogs, networks; relationship traversal |
| Hybrid | Both + linking layer | Enterprise, medical, legal; combined queries; highest complexity |
| When to skip KB | Static docs, <50 FAQ items | No maintenance resources, information never changes |
| Implementation | 6 phases | Audit, Curation, Storage, Quality, Versioning, Governance |
| Accuracy target | >90% on test questions | Create 100+ test questions with known correct answers |
| Coverage target | >80% questions answerable | Validate against real user queries continuously |
| Freshness target | <30 days average age | Automated freshness monitoring + scheduled updates |
| Consistency target | >95% conflict-free | Deduplication + single source of truth |
| Query latency | <100ms median | Caching and optimization for common access patterns |
| Storage tech | pgvector, Pinecone, Weaviate, Chroma | pgvector for existing Postgres; Pinecone for managed scale |
| Index types | HNSW, IVFFlat | HNSW for recall; IVFFlat for frequently rebuilt indexes |
| Ingestion pipeline | Load, clean, chunk, embed, store | Chunk at semantic boundaries; 512 tokens max; 10-15% overlap |
| Deduplication | Content hashing, semantic similarity | Hash for exact dupes; cosine similarity >0.95 for semantic dupes |
| Quality testing | Recall@K, MRR, accuracy sampling | 100+ test questions; measure recall@10 >0.8 and MRR >0.7 |
| Drift detection | Embedding distribution monitoring | Track mean shift; alert when >0.1 threshold |
| Versioning | Snapshot, Event-sourced, Git-style | Snapshot for simple; event-sourced for audit; git-style for teams |
| Maintenance | Daily, Weekly, Monthly, Quarterly | Establish schedule from day 1; monitor errors and user feedback |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
| Ingesting raw data without curation or normalization | Curate, clean, and deduplicate before ingesting; quality over quantity |
| Skipping version control for KB content | Implement versioning from day one with rollback and audit trail |
| Building a KB without validating against user questions | Start with user research and test against real queries for >90% accuracy |
| Choosing hybrid architecture when document-based suffices | Match architecture to actual query patterns; start simple, add complexity when needed |
| Launching without freshness monitoring or update schedules | Set up automated freshness checks and scheduled content reviews |
| No provenance tracking on knowledge entries | Always track source URL, timestamp, author, and confidence score |
| Duplicate information across sources | Establish single source of truth; merge similar entries with conflict resolution rules |
| Perfectionism delaying launch | Launch at 80% coverage and iterate based on real usage data |
Delegation
- Audit existing knowledge sources and classify content types: Use
Exploreagent to inventory documents, assess quality, and identify gaps - Implement end-to-end KB pipeline with storage and retrieval: Use
Taskagent to deploy database, configure search, and run quality checks - Design KB architecture and governance model: Use
Planagent to select between document-based, entity-based, or hybrid approaches
For implementing document retrieval pipelines (chunking, embeddings, vector stores, hybrid search), use therag-implementerskill. For implementing knowledge graphs (ontology design, entity extraction, graph databases), use theknowledge-graph-builderskill.
References
- Architecture and Types -- KB types, decision framework, knowledge classification
- Curation and Ingestion -- extraction, cleaning, deduplication, provenance tracking
- Storage and Retrieval -- database selection, interfaces, technology stacks
- Quality Control -- metrics, validation strategies, continuous monitoring
- Versioning -- snapshot, event-sourced, and git-style approaches
- Governance -- maintenance schedules, roles, change processes
Architecture and Types
When to Use a Knowledge Base
Use when: factual questions need consistent answers, information changes frequently, multiple sources need unification, provenance tracking is critical, building AI systems needing grounded information, complex domain with interconnected concepts.
Don't use when: static documentation suffices, no maintenance resources available, simple FAQ covers all questions (<50 items), information never changes.
Document-Based KB (RAG)
Collection of documents, chunked and embedded for semantic search.
Best for: technical documentation, support articles, policy documents, research papers, user manuals.
Strengths: easy to add documents, preserves full context, natural for text-heavy content.
Weaknesses: hard to query relationships, duplicate information across documents, difficult to keep facts consistent.
Implementation: rag-implementer skill + vector database.
Entity-Based KB (Knowledge Graph)
Network of entities (people, places, things) connected by relationships.
Best for: org charts, product catalogs, social networks, recommendation systems, fraud detection, supply chain.
Strengths: excellent for relationship queries, consistent facts (one source of truth), powerful traversal.
Weaknesses: upfront modeling required, harder to add unstructured info, graph query learning curve.
Implementation: knowledge-graph-builder skill + graph database.
Hybrid KB (RAG + Graph)
Documents for unstructured knowledge + graph for structured entities/relationships.
Best for: enterprise knowledge management, research with citations, medical systems, legal systems, e-commerce.
Implementation: both rag-implementer + knowledge-graph-builder skills.
Knowledge Classification
| Type | Description | Example |
|---|---|---|
| Factual | Verifiable facts | "Product X costs $50" |
| Procedural | How-to knowledge | "How to deploy to production" |
| Conceptual | Definitions and explanations | "What is microservices architecture" |
| Relationship | Connections between entities | "Team A reports to Division B" |
Curation and Ingestion
Knowledge Sources
| Source Type | Examples |
|---|---|
| Internal | Databases, documents, wikis, Slack, emails |
| External | Public data, APIs, third-party sources |
| Tribal | SME interviews, recorded conversations |
Document Ingestion Pipeline
End-to-end flow: load, clean, chunk, embed, store.
interface RawDocument {
content: string;
mimeType: string;
sourceUrl: string;
fetchedAt: string;
}
interface ProcessedChunk {
content: string;
embedding: number[];
metadata: ChunkMetadata;
contentHash: string;
}
interface ChunkMetadata {
sourceUrl: string;
chunkIndex: number;
totalChunks: number;
category?: string;
tags: string[];
fetchedAt: string;
processedAt: string;
}
async function ingestDocument(
raw: RawDocument,
embedFn: (text: string) => Promise<number[]>,
): Promise<ProcessedChunk[]> {
const cleaned = cleanContent(raw.content, raw.mimeType);
const chunks = chunkText(cleaned, { maxTokens: 512, overlap: 64 });
const now = new Date().toISOString();
const processed = await Promise.all(
chunks.map(async (text, i) => ({
content: text,
embedding: await embedFn(text),
contentHash: await hashContent(text),
metadata: {
sourceUrl: raw.sourceUrl,
chunkIndex: i,
totalChunks: chunks.length,
tags: [],
fetchedAt: raw.fetchedAt,
processedAt: now,
},
})),
);
const deduplicated = await deduplicateChunks(processed);
return deduplicated;
}Chunking Strategy
interface ChunkOptions {
maxTokens: number;
overlap: number;
separators?: string[];
}
function chunkText(text: string, opts: ChunkOptions): string[] {
const { maxTokens, overlap, separators = ['\n\n', '\n', '. ', ' '] } = opts;
const chunks: string[] = [];
let remaining = text;
while (remaining.length > 0) {
if (estimateTokens(remaining) <= maxTokens) {
chunks.push(remaining.trim());
break;
}
let splitPoint = -1;
const searchWindow = remaining.slice(0, maxTokens * 4);
for (const sep of separators) {
const lastIndex = searchWindow.lastIndexOf(sep);
if (lastIndex > 0) {
splitPoint = lastIndex + sep.length;
break;
}
}
if (splitPoint <= 0) splitPoint = maxTokens * 4;
chunks.push(remaining.slice(0, splitPoint).trim());
const overlapStart = Math.max(0, splitPoint - overlap * 4);
remaining = remaining.slice(overlapStart);
}
return chunks.filter((c) => c.length > 0);
}
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}Content Cleaning
HTML Stripping
function stripHtml(html: string): string {
return html
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<nav[\s\S]*?<\/nav>/gi, '')
.replace(/<footer[\s\S]*?<\/footer>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/ /g, ' ')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\s+/g, ' ')
.trim();
}Cleaning by MIME Type
function cleanContent(content: string, mimeType: string): string {
switch (mimeType) {
case 'text/html':
return stripHtml(content);
case 'text/markdown':
return normalizeMarkdown(content);
case 'text/plain':
return content.replace(/\s+/g, ' ').trim();
default:
return content.trim();
}
}
function normalizeMarkdown(md: string): string {
return md
.replace(/^#{1,6}\s+/gm, (match) => match)
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
.replace(/(`{3}[\s\S]*?`{3})/g, '$1')
.replace(/\s+/g, ' ')
.trim();
}Table Detection
interface ExtractedTable {
headers: string[];
rows: string[][];
}
function extractMarkdownTables(md: string): ExtractedTable[] {
const tablePattern = /\|(.+)\|\n\|[-| :]+\|\n((?:\|.+\|\n?)*)/g;
const tables: ExtractedTable[] = [];
let match: RegExpExecArray | null;
while ((match = tablePattern.exec(md)) !== null) {
const headers = match[1]
.split('|')
.map((h) => h.trim())
.filter(Boolean);
const rowLines = match[2].trim().split('\n');
const rows = rowLines.map((line) =>
line
.split('|')
.map((c) => c.trim())
.filter(Boolean),
);
tables.push({ headers, rows });
}
return tables;
}Deduplication
Content Hashing
import { createHash } from 'node:crypto';
async function hashContent(text: string): Promise<string> {
const normalized = text.toLowerCase().replace(/\s+/g, ' ').trim();
return createHash('sha256').update(normalized).digest('hex');
}
async function deduplicateChunks(
chunks: ProcessedChunk[],
): Promise<ProcessedChunk[]> {
const seen = new Map<string, ProcessedChunk>();
for (const chunk of chunks) {
const existing = seen.get(chunk.contentHash);
if (!existing) {
seen.set(chunk.contentHash, chunk);
continue;
}
mergeMetadata(existing.metadata, chunk.metadata);
}
return Array.from(seen.values());
}Semantic Similarity Threshold
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
async function findSemanticDuplicates(
chunks: ProcessedChunk[],
threshold = 0.95,
): Promise<Array<[ProcessedChunk, ProcessedChunk]>> {
const duplicates: Array<[ProcessedChunk, ProcessedChunk]> = [];
for (let i = 0; i < chunks.length; i++) {
for (let j = i + 1; j < chunks.length; j++) {
const sim = cosineSimilarity(chunks[i].embedding, chunks[j].embedding);
if (sim >= threshold) {
duplicates.push([chunks[i], chunks[j]]);
}
}
}
return duplicates;
}Metadata Enrichment
Auto-Tagging with LLM
interface TagResult {
tags: string[];
category: string;
confidence: number;
}
async function autoTag(
content: string,
llmFn: (prompt: string) => Promise<string>,
): Promise<TagResult> {
const prompt = `Classify this knowledge base content.
Return JSON: { "tags": string[], "category": string, "confidence": number }
Tags: 3-5 descriptive keywords.
Category: one of [policy, procedure, reference, tutorial, faq, api-docs].
Confidence: 0-1 how certain you are.
Content:
${content.slice(0, 2000)}`;
const raw = await llmFn(prompt);
return JSON.parse(raw) as TagResult;
}Source Tracking and Freshness
interface SourceRecord {
url: string;
lastChecked: string;
lastModified: string;
contentHash: string;
checkIntervalHours: number;
}
async function checkSourceFreshness(
source: SourceRecord,
): Promise<{ stale: boolean; newHash?: string }> {
const response = await fetch(source.url, { method: 'HEAD' });
const lastModified = response.headers.get('last-modified');
if (lastModified && new Date(lastModified) > new Date(source.lastModified)) {
const full = await fetch(source.url);
const newHash = await hashContent(await full.text());
return { stale: newHash !== source.contentHash, newHash };
}
return { stale: false };
}Provenance Tracking
Every knowledge entry needs:
- Source URL or reference
- Last updated timestamp
- Author/contributor
- Confidence score (if applicable)
function mergeMetadata(target: ChunkMetadata, source: ChunkMetadata): void {
const targetDate = new Date(target.fetchedAt);
const sourceDate = new Date(source.fetchedAt);
if (sourceDate > targetDate) {
target.fetchedAt = source.fetchedAt;
target.sourceUrl = source.sourceUrl;
}
target.tags = [...new Set([...target.tags, ...source.tags])];
}Curation Best Practices
| Practice | Description |
|---|---|
| Single source of truth | One canonical answer per question |
| Deduplication | Merge similar knowledge entries |
| Conflict resolution | When sources disagree, establish priority rules |
| Metadata richness | More metadata = better filtering and search |
| Chunking at boundaries | Split at paragraphs/sections, not mid-sentence |
| Overlap between chunks | 10-15% overlap preserves context at boundaries |
Validation Checklist
- Knowledge extracted and structured
- Content cleaned and normalized per MIME type
- Chunks sized within token limits with semantic boundaries
- Duplicate content detected and merged
- Quality metrics above threshold (accuracy >95%)
- Provenance tracked for all entries
- Sample queries return relevant results
Governance
Maintenance Schedule
| Frequency | Tasks |
|---|---|
| Daily | Monitor errors, review user feedback, address urgent corrections |
| Weekly | Review new submissions, update time-sensitive knowledge, run quality checks |
| Monthly | Audit freshness, resolve conflicts, analyze usage, update stale content |
| Quarterly | Full quality audit, schema review, performance optimization, user survey |
Roles and Responsibilities
| Role | Responsibility |
|---|---|
| Knowledge owners | Domain experts responsible for content accuracy |
| Curators | Review and approve changes |
| Contributors | Submit new knowledge |
| Consumers | Use knowledge and provide feedback |
Change Process
Submit → Review → Approve → Publish → MonitorQuality Standards
- Minimum source quality requirements
- Citation requirements for all factual claims
- Update frequency requirements per content type
- Conflict resolution process when sources disagree
Common Pitfalls
| Pitfall | Impact | Solution |
|---|---|---|
| Build it and they will come | Doesn't meet user needs | Start with user research, validate continuously |
| Perfectionism | Never launches | Launch at 80% coverage, iterate on usage |
| Over-engineering | Wasted complexity | Start simple, add complexity only when needed |
| Maintenance neglect | Knowledge rot | Establish maintenance schedule from day 1 |
| No provenance | Can't verify accuracy | Always track source + timestamp + author |
| Data dump without curation | Low signal-to-noise | Curate before ingesting, quality > quantity |
Quality Control
Quality Metrics
| Metric | Description | Target |
|---|---|---|
| Accuracy | % correct answers to test questions | >90% |
| Coverage | % user questions answerable | >80% |
| Freshness | Average age of knowledge | <30 days |
| Consistency | % without conflicts/contradictions | >95% |
| Source quality | % from authoritative sources | >90% |
Automated Quality Metrics
Accuracy Sampling
interface TestQuestion {
question: string;
expectedAnswer: string;
category: string;
difficulty: 'easy' | 'medium' | 'hard';
}
interface AccuracyResult {
totalQuestions: number;
correctCount: number;
accuracy: number;
failures: Array<{
question: string;
expected: string;
actual: string;
similarity: number;
}>;
}
async function measureAccuracy(
testSet: TestQuestion[],
queryFn: (question: string) => Promise<string>,
similarityFn: (a: string, b: string) => Promise<number>,
threshold = 0.8,
): Promise<AccuracyResult> {
const failures: AccuracyResult['failures'] = [];
let correctCount = 0;
for (const test of testSet) {
const actual = await queryFn(test.question);
const similarity = await similarityFn(test.expectedAnswer, actual);
if (similarity >= threshold) {
correctCount++;
} else {
failures.push({
question: test.question,
expected: test.expectedAnswer,
actual,
similarity,
});
}
}
return {
totalQuestions: testSet.length,
correctCount,
accuracy: correctCount / testSet.length,
failures,
};
}Coverage Measurement
interface CoverageResult {
totalQueries: number;
answeredCount: number;
coverage: number;
unanswered: Array<{
query: string;
topScore: number;
}>;
}
async function measureCoverage(
userQueries: string[],
searchFn: (
query: string,
) => Promise<Array<{ content: string; score: number }>>,
relevanceThreshold = 0.7,
): Promise<CoverageResult> {
const unanswered: CoverageResult['unanswered'] = [];
let answeredCount = 0;
for (const query of userQueries) {
const results = await searchFn(query);
const topScore = results[0]?.score ?? 0;
if (topScore >= relevanceThreshold) {
answeredCount++;
} else {
unanswered.push({ query, topScore });
}
}
return {
totalQueries: userQueries.length,
answeredCount,
coverage: answeredCount / userQueries.length,
unanswered,
};
}Freshness Scoring
interface FreshnessResult {
totalDocuments: number;
averageAgeDays: number;
staleCount: number;
staleDocuments: Array<{
id: string;
title: string;
ageDays: number;
lastUpdated: string;
}>;
}
async function measureFreshness(
documents: Array<{ id: string; title: string; updatedAt: string }>,
maxAgeDays = 30,
): Promise<FreshnessResult> {
const now = Date.now();
const staleDocuments: FreshnessResult['staleDocuments'] = [];
let totalAge = 0;
for (const doc of documents) {
const ageDays = (now - new Date(doc.updatedAt).getTime()) / 86_400_000;
totalAge += ageDays;
if (ageDays > maxAgeDays) {
staleDocuments.push({
id: doc.id,
title: doc.title,
ageDays: Math.round(ageDays),
lastUpdated: doc.updatedAt,
});
}
}
return {
totalDocuments: documents.length,
averageAgeDays: Math.round(totalAge / documents.length),
staleCount: staleDocuments.length,
staleDocuments,
};
}Retrieval Quality Testing
Test suite pattern: query known answers and measure recall at K.
interface RetrievalTestCase {
query: string;
relevantDocIds: string[];
}
interface RetrievalMetrics {
meanRecallAtK: number;
meanPrecisionAtK: number;
meanReciprocalRank: number;
perQuery: Array<{
query: string;
recallAtK: number;
precisionAtK: number;
reciprocalRank: number;
}>;
}
async function evaluateRetrieval(
testCases: RetrievalTestCase[],
searchFn: (query: string) => Promise<Array<{ id: string; score: number }>>,
k = 10,
): Promise<RetrievalMetrics> {
const perQuery: RetrievalMetrics['perQuery'] = [];
for (const tc of testCases) {
const results = await searchFn(tc.query);
const topK = results.slice(0, k);
const retrievedIds = new Set(topK.map((r) => r.id));
const relevantSet = new Set(tc.relevantDocIds);
const hits = topK.filter((r) => relevantSet.has(r.id)).length;
const recallAtK = hits / relevantSet.size;
const precisionAtK = hits / k;
let reciprocalRank = 0;
for (let i = 0; i < topK.length; i++) {
if (relevantSet.has(topK[i].id)) {
reciprocalRank = 1 / (i + 1);
break;
}
}
perQuery.push({
query: tc.query,
recallAtK,
precisionAtK,
reciprocalRank,
});
}
const n = perQuery.length;
return {
meanRecallAtK: perQuery.reduce((s, q) => s + q.recallAtK, 0) / n,
meanPrecisionAtK: perQuery.reduce((s, q) => s + q.precisionAtK, 0) / n,
meanReciprocalRank: perQuery.reduce((s, q) => s + q.reciprocalRank, 0) / n,
perQuery,
};
}Drift Detection
Embedding Distribution Monitoring
interface DriftReport {
meanShift: number;
varianceChange: number;
drifted: boolean;
details: string;
}
function computeMeanEmbedding(embeddings: number[][]): number[] {
const dim = embeddings[0].length;
const mean = new Array<number>(dim).fill(0);
for (const emb of embeddings) {
for (let i = 0; i < dim; i++) {
mean[i] += emb[i];
}
}
return mean.map((v) => v / embeddings.length);
}
function embeddingVariance(embeddings: number[][], mean: number[]): number {
let totalDist = 0;
for (const emb of embeddings) {
let dist = 0;
for (let i = 0; i < mean.length; i++) {
dist += (emb[i] - mean[i]) ** 2;
}
totalDist += Math.sqrt(dist);
}
return totalDist / embeddings.length;
}
function detectDrift(
baselineEmbeddings: number[][],
currentEmbeddings: number[][],
shiftThreshold = 0.1,
): DriftReport {
const baselineMean = computeMeanEmbedding(baselineEmbeddings);
const currentMean = computeMeanEmbedding(currentEmbeddings);
let shift = 0;
for (let i = 0; i < baselineMean.length; i++) {
shift += (baselineMean[i] - currentMean[i]) ** 2;
}
const meanShift = Math.sqrt(shift);
const baselineVar = embeddingVariance(baselineEmbeddings, baselineMean);
const currentVar = embeddingVariance(currentEmbeddings, currentMean);
const varianceChange = Math.abs(currentVar - baselineVar) / baselineVar;
const drifted = meanShift > shiftThreshold;
return {
meanShift,
varianceChange,
drifted,
details: drifted
? `Mean shifted by ${meanShift.toFixed(4)} (threshold: ${shiftThreshold})`
: 'No significant drift detected',
};
}Schema Change Detection
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'kb_documents'
ORDER BY ordinal_position;Compare against a stored baseline and alert on added, removed, or type-changed columns.
Quality Dashboard
Key metrics to track and their alert thresholds:
| Metric | Green | Yellow | Red |
|---|---|---|---|
| Accuracy | >90% | 80-90% | <80% |
| Coverage | >80% | 70-80% | <70% |
| Freshness (avg age) | <30 days | 30-60 days | >60 days |
| Consistency | >95% | 90-95% | <90% |
| Recall@10 | >0.8 | 0.6-0.8 | <0.6 |
| MRR | >0.7 | 0.5-0.7 | <0.5 |
| Query latency (p50) | <100ms | 100-500ms | >500ms |
| Query latency (p99) | <500ms | 500ms-2s | >2s |
| Embedding drift | <0.05 | 0.05-0.1 | >0.1 |
Metrics Collection
interface KBHealthMetrics {
accuracy: number;
coverage: number;
freshness: { averageAgeDays: number; stalePercent: number };
consistency: number;
retrieval: { recallAt10: number; mrr: number };
latency: { p50Ms: number; p99Ms: number };
drift: { meanShift: number; drifted: boolean };
collectedAt: string;
}
async function collectHealthMetrics(
testQuestions: TestQuestion[],
userQueries: string[],
queryFn: (q: string) => Promise<string>,
searchFn: (
q: string,
) => Promise<Array<{ id: string; score: number; content: string }>>,
similarityFn: (a: string, b: string) => Promise<number>,
): Promise<KBHealthMetrics> {
const [accuracy, coverage] = await Promise.all([
measureAccuracy(testQuestions, queryFn, similarityFn),
measureCoverage(userQueries, async (q) =>
(await searchFn(q)).map((r) => ({ content: r.content, score: r.score })),
),
]);
return {
accuracy: accuracy.accuracy,
coverage: coverage.coverage,
freshness: { averageAgeDays: 0, stalePercent: 0 },
consistency: 0,
retrieval: { recallAt10: 0, mrr: 0 },
latency: { p50Ms: 0, p99Ms: 0 },
drift: { meanShift: 0, drifted: false },
collectedAt: new Date().toISOString(),
};
}Validation Strategies
Human Review
- Sample random knowledge entries
- Subject matter expert validation
- User feedback loops
Automated Checks
| Check | Purpose |
|---|---|
| Duplicate detection | Find near-identical entries |
| Conflict detection | Find contradictory facts |
| Staleness detection | Flag outdated information |
| Citation validation | Verify sources still exist |
User Satisfaction Targets
| Metric | Target |
|---|---|
| Query relevance (user-rated) | >85% |
| Users finding KB valuable | >80% |
| Median query time | <100ms |
| Uptime | >99.9% |
Storage and Retrieval
Technology Stacks
| Stack | Vector DB | Graph DB | Embeddings | Search |
|---|---|---|---|---|
| Document-based | Pinecone, Weaviate, pgvector | — | OpenAI, Cohere | Semantic + keyword hybrid |
| Entity-based | — | Neo4j, ArangoDB | — | Cypher, AQL |
| Hybrid | Any vector DB | Any graph DB | OpenAI, Cohere | Combined queries |
Selection Criteria
Choose storage based on the architecture decision from Architecture and Types:
| Architecture | Primary concern | Key trade-off |
|---|---|---|
| Document-based | Query latency, embedding cost | Easy to add content vs hard to query relationships |
| Entity-based | Graph modeling complexity | Powerful traversal vs upfront schema design |
| Hybrid | Synchronization between stores | Combined strengths vs operational complexity |
Technology Comparison
| Feature | pgvector | Pinecone | Weaviate | Chroma |
|---|---|---|---|---|
| Hosting | Self-hosted | Managed | Both | Self-hosted |
| Max vectors | Limited by Postgres | Billions | Billions | Millions |
| Cost model | Infrastructure only | Per-vector/query | Per-node or managed | Free / open |
| Metadata | Full SQL filtering | Key-value filters | GraphQL-style | Key-value filters |
| Best for | Existing Postgres | Managed scale | Multi-modal search | Local dev / POC |
| Index types | HNSW, IVFFlat | Proprietary | HNSW | HNSW |
| Backup/Restore | Postgres native | Managed | Snapshots | Persistence dir |
| Transactions | Full ACID | None | None | None |
Vector Store Setup: pgvector
Schema
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE kb_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
embedding vector(1536),
metadata JSONB NOT NULL DEFAULT '{}',
source_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON kb_documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
CREATE INDEX ON kb_documents USING gin (metadata);
CREATE INDEX ON kb_documents USING gin (to_tsvector('english', content));Insert and Query Functions
import { pool } from './db';
interface KBDocument {
id: string;
content: string;
embedding: number[];
metadata: Record<string, unknown>;
sourceUrl?: string;
}
async function insertDocument(doc: Omit<KBDocument, 'id'>): Promise<string> {
const { rows } = await pool.query(
`INSERT INTO kb_documents (content, embedding, metadata, source_url)
VALUES ($1, $2::vector, $3, $4)
RETURNING id`,
[doc.content, JSON.stringify(doc.embedding), doc.metadata, doc.sourceUrl],
);
return rows[0].id;
}
async function searchByVector(
queryEmbedding: number[],
limit = 10,
metadataFilter?: Record<string, unknown>,
): Promise<(KBDocument & { similarity: number })[]> {
let whereClause = '';
const params: unknown[] = [JSON.stringify(queryEmbedding), limit];
if (metadataFilter) {
whereClause = 'WHERE metadata @> $3';
params.push(JSON.stringify(metadataFilter));
}
const { rows } = await pool.query(
`SELECT id, content, metadata, source_url AS "sourceUrl",
1 - (embedding <=> $1::vector) AS similarity
FROM kb_documents
${whereClause}
ORDER BY embedding <=> $1::vector
LIMIT $2`,
params,
);
return rows;
}Document Store Schema
For structured documents with full-text search:
interface DocumentEntry {
id: string;
title: string;
content: string;
contentHash: string;
category: string;
tags: string[];
source: {
url: string;
author: string;
fetchedAt: string;
};
version: number;
createdAt: string;
updatedAt: string;
}Full-Text Search Query
SELECT id, title,
ts_rank(to_tsvector('english', content), query) AS rank
FROM kb_documents, plainto_tsquery('english', $1) query
WHERE to_tsvector('english', content) @@ query
ORDER BY rank DESC
LIMIT $2;Hybrid Storage Pattern
Combine vector similarity, full-text search, and metadata filtering in a single query using Reciprocal Rank Fusion (RRF):
interface HybridSearchOptions {
query: string;
queryEmbedding: number[];
metadataFilter?: Record<string, unknown>;
limit?: number;
vectorWeight?: number;
textWeight?: number;
}
async function hybridSearch(opts: HybridSearchOptions) {
const {
query,
queryEmbedding,
metadataFilter,
limit = 10,
vectorWeight = 0.7,
textWeight = 0.3,
} = opts;
const filterClause = metadataFilter ? 'AND metadata @> $4::jsonb' : '';
const params: unknown[] = [JSON.stringify(queryEmbedding), query, limit];
if (metadataFilter) params.push(JSON.stringify(metadataFilter));
const { rows } = await pool.query(
`WITH vector_results AS (
SELECT id, content, metadata,
ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) AS vrank
FROM kb_documents
WHERE embedding IS NOT NULL ${filterClause}
LIMIT 50
),
text_results AS (
SELECT id, content, metadata,
ROW_NUMBER() OVER (
ORDER BY ts_rank(to_tsvector('english', content),
plainto_tsquery('english', $2)) DESC
) AS trank
FROM kb_documents
WHERE to_tsvector('english', content) @@ plainto_tsquery('english', $2)
${filterClause}
LIMIT 50
)
SELECT COALESCE(v.id, t.id) AS id,
COALESCE(v.content, t.content) AS content,
COALESCE(v.metadata, t.metadata) AS metadata,
(COALESCE($5::float / (60 + v.vrank), 0) +
COALESCE($6::float / (60 + t.trank), 0)) AS rrf_score
FROM vector_results v
FULL OUTER JOIN text_results t ON v.id = t.id
ORDER BY rrf_score DESC
LIMIT $3`,
[...params, vectorWeight, textWeight],
);
return rows;
}Index Tuning
HNSW Parameters
| Parameter | Default | Effect | Guidance |
|---|---|---|---|
m | 16 | Max connections per node | 12-48; higher = better recall |
ef_construction | 64 | Build-time search width | 100-400; higher = slower build |
ef_search | 40 | Query-time search width (SET at runtime) | 100-400; higher = slower query |
SET hnsw.ef_search = 200;IVFFlat Trade-offs
| Parameter | Effect | Guidance |
|---|---|---|
lists | Number of clusters | sqrt(row_count) to row_count / 1000 |
probes | Clusters searched at query time | 1-20; higher = better recall |
IVFFlat is faster to build but less accurate than HNSW. Use IVFFlat for datasets that change frequently (rebuild is cheaper). Use HNSW for stable datasets where recall matters.
CREATE INDEX ON kb_documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
SET ivfflat.probes = 10;Implementation Steps
1. Choose database(s) based on KB type 2. Create schema with vector, full-text, and metadata indexes 3. Implement insert and query functions with proper parameterization 4. Add caching and optimization for common access patterns 5. Target <100ms median query time 6. Monitor index performance and tune parameters based on recall benchmarks
For document-based implementation details (chunking, embeddings, vector store configuration), use therag-implementerskill. For entity-based implementation details (ontology design, entity extraction, graph database setup), use theknowledge-graph-builderskill.
Versioning
Knowledge changes over time. Versioning enables audit trails, rollback, and historical queries.
Snapshot Versioning
Each entry stores its version and link to prior version:
interface KnowledgeEntry {
id: string;
content: string;
version: number;
created_at: string;
updated_at: string;
updated_by: string;
changelog: string;
previous_version?: string;
}Event Sourcing
Track every change as an immutable event:
interface KnowledgeEvent {
event_id: string;
entity_id: string;
event_type: 'created' | 'updated' | 'deleted';
timestamp: string;
changes: {
field: string;
old_value: any;
new_value: any;
}[];
author: string;
}Reconstruct any point-in-time state by replaying events.
Git-Style Versioning
Treat knowledge like code:
- Commit-based changes with messages
- Branch for experimental knowledge
- Merge when validated
- Pull request-style review for changes
Best for teams familiar with git workflows.
Choosing a Strategy
| Strategy | Best For | Complexity |
|---|---|---|
| Snapshot | Simple KB, few updates | Low |
| Event sourcing | High-frequency updates, audit requirements | Medium |
| Git-style | Team collaboration, review processes | High |