{"slug": "build-a-rag-for-customer-support-knowledge-base-that-answers-tickets", "title": "Build a rag for customer support knowledge base that answers tickets automatically", "summary": "A developer's guide demonstrates building a Retrieval-Augmented Generation (RAG) pipeline for customer support, which automatically answers Zendesk tickets by retrieving relevant knowledge base articles and feeding them to an OpenAI LLM. The tutorial covers environment setup, data ingestion via Zendesk's API, chunking with LangChain, embedding with OpenAI's text-embedding-ada-002, and vector storage in Chroma or Pinecone, with an estimated build time of eight hours.", "body_md": "You'll create a Retrieval-Augmented Generation (RAG) pipeline that pulls the most relevant support articles from your knowledge base, feeds them to an OpenAI LLM, and returns a ready-to-send answer to Zendesk tickets. The result is a hands-free response engine that reduces agent load while keeping answers accurate and up-to-date.\n\n**What is RAG?** RAG (Retrieval-Augmented Generation) is an architecture that first retrieves relevant documents from an external source and then conditions a large language model on those passages before generating a response.\n\n| Tool | Plan / Price | Role |\n|---|---|---|\nOpenAI API |\nPay-as-you-go (see\n|\n\n**Time to build:** ~8 hours for a functional prototype (2 h env setup, 3 h data ingestion, 2 h integration, 1 h testing).\n\nCreate a fresh directory and initialise a Python virtual environment:\n\n```\nmkdir rag-support && cd rag-support\npython3 -m venv .venv\nsource .venv/bin/activate\n```\n\nThis isolates dependencies and lets you run the same code locally and in Docker later.\n\nInstall the required libraries:\n\n```\npip install openai langchain chromadb tqdm\n```\n\n**Why:** `openai`\n\nprovides the embedding and completion endpoints, `langchain`\n\noffers high-level abstractions for retrieval, and `chromadb`\n\n(the Python client for Chroma) stores vectors efficiently on disk.\n\nExport the knowledge base as Markdown or plain-text files. A quick way is to use Zendesk's API:\n\n```\ncurl -s -H \"Authorization: Bearer $ZENDESK_TOKEN\" \\\n \"https://yoursubdomain.zendesk.com/api/v2/help_center/articles.json\" \\\n | jq -r '.articles[] | \"\\(.title)\\n\\(.body)\"' > articles.txt\n```\n\nReplace\n\n`$ZENDESK_TOKEN`\n\nwith a token that hasreadpermission on the Help Center. The output concatenates every article into`articles.txt`\n\n, one article after another.\n\nLong documents need to be split into manageable pieces (≈ 300 tokens) so that embeddings stay within OpenAI's token limits.\n\n``` python\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\n\nwith open(\"articles.txt\", \"r\", encoding=\"utf-8\") as f:\n raw = f.read()\n\nsplitter = RecursiveCharacterTextSplitter(\n chunk_size=300,\n chunk_overlap=30,\n separators=[\"\\n\\n\", \"\\n\", \" \"]\n)\nchunks = splitter.split_text(raw)\nprint(f\"Created {len(chunks)} chunks.\")\n```\n\n**Why:** Overlapping chunks preserve context across paragraph boundaries, improving retrieval relevance.\n\nConvert each chunk into a 1536-dimensional vector using the `text-embedding-ada-002`\n\nmodel:\n\n``` python\nimport os, openai\nfrom tqdm import tqdm\n\nopenai.api_key = os.getenv(\"OPENAI_API_KEY\")\n\ndef embed(texts):\n # Batch up to 2048 tokens per request (OpenAI limit)\n return openai.Embedding.create(\n model=\"text-embedding-ada-002\",\n input=texts\n )[\"data\"]\n\nembeddings = []\nbatch_size = 100\nfor i in tqdm(range(0, len(chunks), batch_size)):\n batch = chunks[i:i+batch_size]\n resp = embed(batch)\n embeddings.extend([r[\"embedding\"] for r in resp])\n```\n\nEach embedding costs $0.0001 per 1 000 tokens, so a 10 000-article knowledge base typically stays under $5 per month on the OpenAI pay-as-you-go tier.\n\n``` python\nimport chromadb\nfrom chromadb.utils import embedding_functions\n\nclient = chromadb.Client()\ncollection = client.create_collection(\n name=\"support-knowledge\",\n embedding_function=embedding_functions.OpenAIEmbeddingFunction(\n api_key=os.getenv(\"OPENAI_API_KEY\")\n )\n)\n\nids = [f\"doc-{i}\" for i in range(len(chunks))]\ncollection.add(\n ids=ids,\n documents=chunks,\n embeddings=embeddings\n)\nprint(\"Vectors persisted to ./chromadb\")\npython\nimport pinecone\n\npinecone.init(api_key=os.getenv(\"PINECONE_API_KEY\"), environment=\"us-west1-gcp\")\nindex = pinecone.Index(\"support-knowledge\")\nvectors = [(ids[i], embeddings[i]) for i in range(len(embeddings))]\nindex.upsert(vectors=vectors, namespace=\"support\")\n```\n\nWhy choose Pinecone?It offers sub-millisecond latency, automatic scaling, and built-in metadata filtering - critical for high-traffic support desks.\n\n``` python\nfrom langchain.chains import RetrievalQA\nfrom langchain.llms import OpenAI\nfrom langchain.vectorstores import Chroma, Pinecone\nfrom langchain.embeddings import OpenAIEmbeddings\n\n# Choose the backend that matches step 5\nif use_chroma:\n vectorstore = Chroma(\n collection_name=\"support-knowledge\",\n embedding_function=OpenAIEmbeddings()\n )\nelse:\n vectorstore = Pinecone.from_existing_index(\n index_name=\"support-knowledge\",\n embedding=OpenAIEmbeddings(),\n namespace=\"support\"\n )\n\nretriever = vectorstore.as_retriever(search_kwargs={\"k\": 4})\nqa = RetrievalQA.from_chain_type(\n llm=OpenAI(model_name=\"gpt-3.5-turbo\"),\n chain_type=\"stuff\",\n retriever=retriever,\n return_source_documents=True\n)\n```\n\nThis chain fetches the four most relevant chunks, concatenates them, and prompts the LLM to answer the user's question while citing sources.\n\n```\n docker run -d --name n8n \\\n -p 5678:5678 \\\n -v ~/.n8n:/home/node/.n8n \\\n n8nio/n8n\n```\n\n**Create a workflow**:\n\n`ticket_id`\n\n, `question`\n\n). `qa.run(question)`\n\nand returns `answer`\n\nand `sources`\n\n. `PUT /api/v2/tickets/{ticket_id}`\n\n.**Python script for the Execute Command node** (`answer.py`\n\n):\n\n``` python\n import sys, json\n from answer_chain import qa # assumes qa defined in previous step\n\n payload = json.loads(sys.stdin.read())\n question = payload[\"question\"]\n resp = qa({\"query\": question})\n result = {\n \"answer\": resp[\"result\"],\n \"sources\": [doc.metadata[\"source\"] for doc in resp[\"source_documents\"]]\n }\n print(json.dumps(result))\n```\n\nn8n pipes the incoming JSON to\n\n`stdin`\n\n; the script writes a JSON response to`stdout`\n\nwhich n8n captures for downstream nodes.\n\n`https://n8n.mycompany.com/webhook/rag-support`\n\n) becomes the endpoint you register in Zendesk's In Zendesk, create a **Trigger** that fires on *Ticket Created* with the condition `Ticket is a support request`\n\n. Add an **Action** → *Notify target* → *HTTP target* pointing at the n8n webhook URL, passing `{ \"ticket_id\": \"{{ticket.id}}\", \"question\": \"{{ticket.description}}\" }`\n\n.\n\nWhen a ticket arrives, Zendesk calls the webhook, the RAG chain returns an answer, and the workflow updates the ticket with the response.\n\nCreate a dummy ticket in Zendesk:\n\n```\nSubject: How do I reset my password?\nDescription: I cannot find the reset link on the login page.\n```\n\nAfter a few seconds, the ticket body should contain a concise answer such as:\n\nTo reset your password, click \"Forgot password?\" on the login page, enter your email, and follow the link you receive. See article \"Password Reset Procedure\" for screenshots.\n\nIf the answer is missing, check n8n's execution log (accessible at `https://n8n.mycompany.com/executions`\n\n) for any runtime errors.\n\n> The most common failure is hitting OpenAI's rate limits or token quotas, which silently abort the embedding step.\n\n| Failure mode | Symptom | Fix |\n|---|---|---|\nOpenAI rate limit (60 requests/min for `text-embedding-ada-002` ) |\nEmbedding script stalls, `openai.error.RateLimitError` raised |\nAdd exponential back-off (`time.sleep(2**retry)` ) and request higher limits via the OpenAI dashboard. |\nVector DB cost overrun (Pinecone reads > 2 M per month) |\nUnexpected bill spike, API returns `429 Too Many Requests`\n|\nEnable Pinecone's request throttling and monitor usage via the Pinecone console; switch to Chroma for bulk offline queries. |\nn8n webhook authentication |\nZendesk receives `401 Unauthorized` and tickets remain unchanged |\nSecure the webhook with a static `X-API-KEY` header; add the same header in the Zendesk HTTP target settings. |\nChunk size too large |\n`openai.error.InvalidRequestError: This model's maximum context length is 4096 tokens` |\nReduce `chunk_size` to ≤ 300 tokens or upgrade to `gpt-4` (larger context). |\nSource document mismatch |\nAnswer cites wrong article IDs | Ensure each chunk's metadata includes a `source` field (e.g., article URL) when adding to the vector store. |\nNetwork latency |\nEnd-to-end response > 10 s, causing Zendesk timeout | Deploy n8n behind a low-latency VPC, enable keep-alive connections, and consider caching the most common queries in Redis. |\n\nFor a deeper technical reference, see [n8n's documentation](https://docs.n8n.io/).\n\nRAG first retrieves factual snippets from a searchable store, then conditions the LLM on those snippets. This reduces hallucinations because the model's output is anchored to concrete documentation rather than relying solely on its pre-training.\n\nYes. LangChain supports Cohere, Anthropic, and open-source models like LLaMA via the `llama-cpp-python`\n\nwrapper. Swap the `OpenAI`\n\nobject with the provider's equivalent and adjust the embedding model accordingly.\n\nChroma is excellent for prototyping and low-traffic environments because it runs locally and costs nothing. For high-volume SaaS or multi-region deployments, a managed vector DB such as Pinecone or Weaviate provides automatic scaling and SLA guarantees.\n\nSchedule the ingestion script to run nightly (via `cron`\n\nor an n8n timer) and use `vectorstore.delete(ids=old_ids)`\n\nfollowed by `vectorstore.add(...)`\n\nto replace stale vectors. Pinecone's *upsert* operation automatically overwrites vectors with matching IDs.\n\nNever store raw personally identifiable information (PII) in the vector store. Strip or redact PII during the chunking stage, and configure the OpenAI API to disable data logging (`openai.api_key = \"...\"; openai.api_base = \"https://api.openai.com/v1\"; openai.log = \"none\"`\n\n).\n\nIf you want to sell this automation to other SaaS teams, check out ** AI automations you can sell** for pricing ideas, and grab\n\nReady to replace manual ticket replies with a reliable rag for customer support knowledge base? Deploy the steps above, monitor the metrics, and iterate on your retrieval prompts - your support agents will thank you.", "url": "https://wpnews.pro/news/build-a-rag-for-customer-support-knowledge-base-that-answers-tickets", "canonical_source": "https://dev.to/samchenreviews/build-a-rag-for-customer-support-knowledge-base-that-answers-tickets-automatically-311o", "published_at": "2026-09-02 16:31:27+00:00", "updated_at": "2026-09-02 16:54:20.024693+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "developer-tools", "ai-tools"], "entities": ["OpenAI", "Zendesk", "LangChain", "Chroma", "Pinecone"], "alternates": {"html": "https://wpnews.pro/news/build-a-rag-for-customer-support-knowledge-base-that-answers-tickets", "markdown": "https://wpnews.pro/news/build-a-rag-for-customer-support-knowledge-base-that-answers-tickets.md", "text": "https://wpnews.pro/news/build-a-rag-for-customer-support-knowledge-base-that-answers-tickets.txt", "jsonld": "https://wpnews.pro/news/build-a-rag-for-customer-support-knowledge-base-that-answers-tickets.jsonld"}}