Building a Support Knowledge Base Agent: From Documentation to Answers
Hoshang Mehta
Building a Support Knowledge Base Agent: From Documentation to Answers
Support teams have documentation scattered across wikis, FAQs, and internal docs. Finding the right answer takes too long. Documentation is outdated. Agents can't quickly surface relevant solutions for similar tickets. The result: support agents spend 10 minutes searching for answers, customers wait longer, and knowledge doesn't get reused.
The problem: A customer asks "How do I export my data?" The support agent searches the wiki, checks the FAQ, looks through internal docs. After 8 minutes, they find a relevant article but it's outdated. They piece together an answer from multiple sources. The customer is frustrated. The next time someone asks the same question, the agent starts from scratch.
The solution: A knowledge base agent that indexes all support documentation and resolved tickets, uses semantic search to find relevant answers, generates responses with confidence scores, and learns from resolutions. This post walks through building a repeatable architecture that transforms scattered documentation into an intelligent assistant.
The Problem with Scattered Support Documentation
Support documentation lives everywhere. Here's what agents face:
The Search Problem
Documentation is scattered across:
- Internal Wiki: Product guides, troubleshooting steps
- FAQ Pages: Common questions, quick answers
- Internal Docs: Team-specific procedures, escalation paths
- Resolved Tickets: Solutions that worked, but aren't documented
- Product Docs: Feature documentation, API references
The math: If searching each source takes 2 minutes and you handle 30 questions per day, that's 60 minutes (1 hour) just searching—before answering any questions.
The Outdated Content Problem
Documentation gets stale:
- Product features change, but docs aren't updated
- Procedures change, but old docs remain
- Solutions that worked last month no longer apply
- Links break, screenshots become outdated
The impact: Agents find documentation, but it's wrong. They waste time following outdated steps, then have to figure out the correct process.
The Knowledge Loss Problem
Solutions exist in resolved tickets, but they're not accessible:
- A ticket was resolved 3 months ago with a perfect solution
- The same issue comes up again, but the agent doesn't know about the previous solution
- Knowledge is trapped in ticket history, not reusable
The missed opportunity: 60% of support questions have been answered before, but agents can't find those answers.
The Context Problem
Documentation lacks context:
- A solution works for one customer tier, but not another
- Steps vary by product version
- Procedures depend on customer configuration
- Documentation doesn't explain when to use which solution
The cost: Agents apply solutions that don't fit the context, causing escalations and customer frustration.
A Repeatable Knowledge Base Architecture
The solution is a pipeline architecture that indexes documentation, embeds content for semantic search, retrieves relevant answers, and generates responses with context.
Architecture Components
1. Content Ingestion Layer
- Connects to documentation sources (wikis, FAQs, docs)
- Captures resolved tickets from support system
- Extracts text from various formats (Markdown, HTML, PDF)
- Normalizes content structure
2. Embedding Generation
- Converts text to vector embeddings using LLMs
- Chunks content for optimal retrieval
- Generates metadata (source, date, relevance tags)
- Stores embeddings in vector database
3. Semantic Search Engine
- Embeds user queries
- Searches vector store for similar content
- Ranks results by relevance
- Returns top-k most relevant documents
4. Answer Generation
- Combines retrieved documents with query
- Generates natural language response
- Includes source citations
- Formats for agent consumption
5. Learning Loop
- Tracks which answers agents use
- Collects resolution feedback
- Updates relevance scores
- Improves retrieval over time
Indexing Documentation and Resolved Tickets
The first stage is ingesting and indexing all documentation sources. This includes wikis, FAQs, internal docs, and resolved tickets.
Content Ingestion Process
1. Connect to Documentation Sources
-- Documentation sources table
CREATE TABLE knowledge_base_sources (
source_id UUID PRIMARY KEY,
source_name VARCHAR(255) NOT NULL,
source_type VARCHAR(50) NOT NULL, -- 'wiki', 'faq', 'docs', 'tickets'
connection_config JSONB,
last_synced_at TIMESTAMP,
sync_frequency VARCHAR(50) -- 'realtime', 'hourly', 'daily'
);
-- Example sources
INSERT INTO knowledge_base_sources VALUES
('wiki-001', 'Internal Wiki', 'wiki', '{"url": "https://wiki.company.com", "api_key": "..."}', NULL, 'hourly'),
('faq-001', 'Public FAQ', 'faq', '{"url": "https://faq.company.com", "format": "markdown"}', NULL, 'daily'),
('docs-001', 'Product Docs', 'docs', '{"url": "https://docs.company.com", "format": "markdown"}', NULL, 'daily'),
('tickets-001', 'Resolved Tickets', 'tickets', '{"system": "zendesk", "status": "resolved"}', NULL, 'realtime');
2. Extract and Normalize Content
-- Knowledge base content table
CREATE TABLE knowledge_base_content (
content_id UUID PRIMARY KEY,
source_id UUID REFERENCES knowledge_base_sources(source_id),
title VARCHAR(500),
content_text TEXT,
content_type VARCHAR(50), -- 'article', 'faq', 'ticket', 'procedure'
metadata JSONB, -- tags, categories, product versions, etc.
created_at TIMESTAMP,
updated_at TIMESTAMP,
is_active BOOLEAN DEFAULT true
);
-- Extract content from sources
CREATE OR REPLACE FUNCTION ingest_documentation()
RETURNS void AS $$
DECLARE
source_record RECORD;
BEGIN
FOR source_record IN SELECT * FROM knowledge_base_sources LOOP
-- Extract content based on source type
CASE source_record.source_type
WHEN 'wiki' THEN
-- Extract from wiki API
INSERT INTO knowledge_base_content (source_id, title, content_text, content_type, metadata)
SELECT
source_record.source_id,
wiki.title,
wiki.content,
'article',
jsonb_build_object('category', wiki.category, 'tags', wiki.tags)
FROM wiki_api_get_pages() wiki;
WHEN 'faq' THEN
-- Extract from FAQ pages
INSERT INTO knowledge_base_content (source_id, title, content_text, content_type, metadata)
SELECT
source_record.source_id,
faq.question,
faq.answer,
'faq',
jsonb_build_object('category', faq.category)
FROM faq_api_get_items() faq;
WHEN 'tickets' THEN
-- Extract from resolved tickets
INSERT INTO knowledge_base_content (source_id, title, content_text, content_type, metadata)
SELECT
source_record.source_id,
ticket.subject,
ticket.resolution_notes || E'\n\n' || ticket.description,
'ticket',
jsonb_build_object(
'ticket_id', ticket.id,
'customer_tier', ticket.customer_tier,
'product_version', ticket.product_version,
'resolution_date', ticket.resolved_at
)
FROM support_tickets ticket
WHERE ticket.status = 'resolved'
AND ticket.resolution_notes IS NOT NULL
AND ticket.resolved_at > CURRENT_TIMESTAMP - INTERVAL '90 days';
END CASE;
-- Update sync timestamp
UPDATE knowledge_base_sources
SET last_synced_at = CURRENT_TIMESTAMP
WHERE source_id = source_record.source_id;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Content Chunking Strategy
Break long documents into chunks for better retrieval:
-- Content chunks table
CREATE TABLE knowledge_base_chunks (
chunk_id UUID PRIMARY KEY,
content_id UUID REFERENCES knowledge_base_content(content_id),
chunk_text TEXT,
chunk_index INTEGER,
embedding VECTOR(1536), -- OpenAI embedding dimension
metadata JSONB,
created_at TIMESTAMP
);
-- Chunk content into 500-token pieces with 50-token overlap
CREATE OR REPLACE FUNCTION chunk_content()
RETURNS void AS $$
DECLARE
content_record RECORD;
chunk_text TEXT;
chunk_index INTEGER := 0;
BEGIN
FOR content_record IN
SELECT * FROM knowledge_base_content
WHERE content_text IS NOT NULL
LOOP
-- Split content into chunks (simplified - use proper tokenization in production)
chunk_index := 0;
FOR chunk_text IN
SELECT unnest(string_to_array(
regexp_split_to_string(content_record.content_text, E'\\n\\n', 'g'),
E'\\n\\n'
))
WHERE length(chunk_text) > 100 -- Minimum chunk size
LOOP
INSERT INTO knowledge_base_chunks (
content_id,
chunk_text,
chunk_index,
metadata
) VALUES (
content_record.content_id,
chunk_text,
chunk_index,
jsonb_build_object(
'title', content_record.title,
'source', content_record.source_id,
'content_type', content_record.content_type
)
);
chunk_index := chunk_index + 1;
END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Semantic Search and Retrieval
Once content is indexed, implement semantic search to find relevant answers. Use vector embeddings to match queries to content.
Embedding Generation
Generate embeddings for content chunks:
-- Generate embeddings using OpenAI API (via function)
CREATE OR REPLACE FUNCTION generate_embedding(text_content TEXT)
RETURNS VECTOR(1536) AS $$
DECLARE
embedding_result JSONB;
BEGIN
-- Call OpenAI embedding API
SELECT openai_embedding_api_call(text_content) INTO embedding_result;
-- Convert JSON array to vector
RETURN (embedding_result->>'embedding')::vector;
END;
$$ LANGUAGE plpgsql;
-- Generate embeddings for all chunks
UPDATE knowledge_base_chunks
SET embedding = generate_embedding(chunk_text)
WHERE embedding IS NULL;
Semantic Search Query
Search for relevant content using vector similarity:
-- Semantic search function
CREATE OR REPLACE FUNCTION search_knowledge_base(
query_text TEXT,
top_k INTEGER DEFAULT 5,
min_similarity FLOAT DEFAULT 0.7
)
RETURNS TABLE (
chunk_id UUID,
content_id UUID,
chunk_text TEXT,
title TEXT,
source_name TEXT,
similarity FLOAT,
metadata JSONB
) AS $$
DECLARE
query_embedding VECTOR(1536);
BEGIN
-- Generate embedding for query
query_embedding := generate_embedding(query_text);
-- Search for similar chunks
RETURN QUERY
SELECT
kb.chunk_id,
kb.content_id,
kb.chunk_text,
kb.metadata->>'title' as title,
ks.source_name,
1 - (kb.embedding <=> query_embedding) as similarity,
kb.metadata
FROM knowledge_base_chunks kb
JOIN knowledge_base_content kbc ON kb.content_id = kbc.content_id
JOIN knowledge_base_sources ks ON kbc.source_id = ks.source_id
WHERE kbc.is_active = true
AND 1 - (kb.embedding <=> query_embedding) >= min_similarity
ORDER BY kb.embedding <=> query_embedding
LIMIT top_k;
END;
$$ LANGUAGE plpgsql;
Search Flow
Answer Generation and Confidence Scoring
Combine retrieved documents with the user query to generate natural language answers. Score confidence based on relevance and source quality.
Answer Generation Process
1. Retrieve Relevant Content
-- Retrieve top-k relevant chunks
SELECT * FROM search_knowledge_base(
query_text := 'How do I export my data?',
top_k := 5,
min_similarity := 0.7
);
2. Generate Answer with LLM
def generate_answer(query: str, retrieved_chunks: list) -> dict:
# Assemble context from retrieved chunks
context = "\n\n".join([
f"Source: {chunk['source_name']}\n{chunk['chunk_text']}"
for chunk in retrieved_chunks
])
# Generate answer using LLM
prompt = f"""Based on the following documentation, answer the user's question.
Documentation:
{context}
Question: {query}
Provide a clear, concise answer. Include source citations."""
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0.3
)
answer = response.choices[0].message.content
# Calculate confidence score
confidence = calculate_confidence(retrieved_chunks, answer)
return {
"answer": answer,
"confidence": confidence,
"sources": [chunk['source_name'] for chunk in retrieved_chunks],
"similarity_scores": [chunk['similarity'] for chunk in retrieved_chunks]
}
3. Confidence Scoring
def calculate_confidence(chunks: list, answer: str) -> float:
# Factors affecting confidence:
# 1. Average similarity score of retrieved chunks
avg_similarity = sum(chunk['similarity'] for chunk in chunks) / len(chunks)
# 2. Number of relevant sources
source_diversity = len(set(chunk['source_name'] for chunk in chunks))
diversity_score = min(source_diversity / 3, 1.0) # Normalize to 0-1
# 3. Recency of content (newer = more confident)
recent_score = calculate_recency_score(chunks)
# 4. Answer completeness (check if answer addresses query)
completeness_score = check_answer_completeness(answer, chunks)
# Weighted confidence score
confidence = (
avg_similarity * 0.4 +
diversity_score * 0.2 +
recent_score * 0.2 +
completeness_score * 0.2
)
return min(confidence, 1.0)
MCP Tool for Knowledge Base
{
"name": "search_knowledge_base",
"description": "Searches the knowledge base for answers to support questions. Returns relevant documentation, FAQs, and solutions from resolved tickets. Use when customers ask how-to questions, troubleshooting questions, or need help with features.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The question or topic to search for"
},
"content_type": {
"type": "string",
"enum": ["all", "article", "faq", "ticket", "procedure"],
"description": "Filter by content type. Defaults to 'all'.",
"default": "all"
},
"min_confidence": {
"type": "number",
"description": "Minimum confidence score (0-1). Defaults to 0.7.",
"minimum": 0,
"maximum": 1,
"default": 0.7
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return",
"minimum": 1,
"maximum": 10,
"default": 5
}
},
"required": ["query"]
},
"query": "SELECT * FROM search_knowledge_base(query_text := $1, top_k := $4, min_similarity := $3)",
"policies": ["authenticated", "role:support"]
}
Continuous Learning from Resolutions
The knowledge base improves over time by learning from which answers agents use and how customers respond.
Learning Loop Architecture
Usage Tracking
-- Track knowledge base usage
CREATE TABLE knowledge_base_usage (
usage_id UUID PRIMARY KEY,
query_text TEXT,
chunk_id UUID REFERENCES knowledge_base_chunks(chunk_id),
agent_id VARCHAR(255),
ticket_id UUID,
used_at TIMESTAMP,
feedback_score INTEGER, -- 1-5 rating
was_helpful BOOLEAN,
resolution_notes TEXT
);
-- Update relevance scores based on usage
CREATE OR REPLACE FUNCTION update_relevance_scores()
RETURNS void AS $$
BEGIN
-- Increase relevance for frequently used, highly-rated content
UPDATE knowledge_base_chunks
SET metadata = jsonb_set(
COALESCE(metadata, '{}'::jsonb),
'{relevance_score}',
to_jsonb(
COALESCE((metadata->>'relevance_score')::float, 0.5) +
(SELECT COUNT(*) * 0.1 FROM knowledge_base_usage
WHERE chunk_id = knowledge_base_chunks.chunk_id
AND was_helpful = true) -
(SELECT COUNT(*) * 0.05 FROM knowledge_base_usage
WHERE chunk_id = knowledge_base_chunks.chunk_id
AND was_helpful = false)
)
)
WHERE chunk_id IN (
SELECT DISTINCT chunk_id FROM knowledge_base_usage
WHERE used_at > CURRENT_TIMESTAMP - INTERVAL '30 days'
);
END;
$$ LANGUAGE plpgsql;
Content Updates from Resolutions
When tickets are resolved, extract new knowledge:
-- Extract knowledge from resolved tickets
CREATE OR REPLACE FUNCTION extract_knowledge_from_resolution()
RETURNS void AS $$
BEGIN
INSERT INTO knowledge_base_content (
source_id,
title,
content_text,
content_type,
metadata
)
SELECT
(SELECT source_id FROM knowledge_base_sources WHERE source_type = 'tickets'),
ticket.subject || ' - Resolution',
ticket.resolution_notes,
'ticket',
jsonb_build_object(
'ticket_id', ticket.id,
'customer_tier', ticket.customer_tier,
'product_version', ticket.product_version,
'resolution_date', ticket.resolved_at,
'satisfaction_score', ticket.satisfaction_score
)
FROM support_tickets ticket
WHERE ticket.status = 'resolved'
AND ticket.resolution_notes IS NOT NULL
AND ticket.resolved_at > CURRENT_TIMESTAMP - INTERVAL '7 days'
AND NOT EXISTS (
SELECT 1 FROM knowledge_base_content
WHERE metadata->>'ticket_id' = ticket.id::text
);
-- Re-chunk new content
PERFORM chunk_content();
-- Generate embeddings for new chunks
UPDATE knowledge_base_chunks
SET embedding = generate_embedding(chunk_text)
WHERE embedding IS NULL;
END;
$$ LANGUAGE plpgsql;
Schema Overview
The knowledge base architecture uses a hierarchical schema that traces from source content to generated answers.
Key relationships:
knowledge_base_sources→ Documentation sources (wikis, FAQs, tickets)knowledge_base_content→ Extracted and normalized contentknowledge_base_chunks→ Chunked content with embeddingsknowledge_base_usage→ Usage tracking and feedback
Traceability: Every answer can be traced back to source content, retrieval process, and usage feedback.
Closing Thoughts
Building a support knowledge base agent transforms documentation from static text into an intelligent assistant. The key is treating knowledge as a searchable, learnable resource—not a static collection.
Key takeaways:
-
Indexing is foundational: Extract content from all sources, normalize formats, chunk for optimal retrieval. Comprehensive indexing makes search possible.
-
Semantic search enables discovery: Vector embeddings find relevant content even when keywords don't match. This is critical for natural language queries.
-
Confidence scoring builds trust: Agents need to know when answers are reliable. Score confidence based on relevance, source quality, and recency.
-
Learning improves over time: Track which answers work, collect feedback, update relevance. The knowledge base gets better with use.
-
Integration with tickets creates a loop: Resolved tickets become new knowledge. Extract solutions, index them, make them searchable.
The architecture described here is repeatable. Start with one documentation source, build the indexing pipeline, implement semantic search, then add more sources incrementally.
Most importantly: this isn't about replacing human knowledge—it's about making knowledge accessible. Support agents still need expertise and judgment. The knowledge base agent provides instant access to documented solutions, freeing agents to handle complex, novel issues.
This architecture pattern is implemented in Pylar, but the concepts apply to any system building a knowledge base. The key is the indexing → embedding → retrieval → generation → learning pipeline.