Building a Support Knowledge Base Agent

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:

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:

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:

The missed opportunity: 60% of support questions have been answered before, but agents can't find those answers.

The Context Problem

Documentation lacks context:

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

2. Embedding Generation

3. Semantic Search Engine

4. Answer Generation

5. Learning Loop

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

User Query
Query Embedding
Vector Search
Similarity Ranking
Top-K Results
Context Assembly
Answer Generation

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

Positive
Negative
Resolution
Agent Uses Answer
Track Usage
Collect Feedback
Feedback Type
Increase Relevance
Decrease Relevance
Update Content
Update Embeddings
Re-index Content

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:

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:

  1. Indexing is foundational: Extract content from all sources, normalize formats, chunk for optimal retrieval. Comprehensive indexing makes search possible.
  2. Semantic search enables discovery: Vector embeddings find relevant content even when keywords don't match. This is critical for natural language queries.
  3. Confidence scoring builds trust: Agents need to know when answers are reliable. Score confidence based on relevance, source quality, and recency.
  4. Learning improves over time: Track which answers work, collect feedback, update relevance. The knowledge base gets better with use.
  5. Integration with tickets creates a loop: Resolved tickets become new knowledge. Extract solutions, index them, make them searchable.