Generating an FAQ with a neural network from tickets and a knowledge base
A good FAQ is built on what customers really ask. A pipeline: group the tickets, answer from the knowledge base with a source reference, and verify the result.
5 min · · автоматизация · бизнес
If you ask a neural network to "write an FAQ for our service", it produces polite questions nobody asks and answers that exist nowhere in your documentation. Generating an FAQ with a neural network works when it has two sources: real customer tickets (where the questions come from) and your knowledge base (where the answers come from). Below is a four-step pipeline with Python code, checks and a cost estimate. Along the way we cover how to get website copy out of it.
Step 0. Prepare the tickets
Export the ticket texts for the last 3–6 months from your helpdesk, mail or chat. Keep only the customer's own questions, not the support replies. Strip personal data before sending to a model: names, phones, emails, order numbers. Questions do not need them, and a leak from logs is expensive; see the separate piece on logging without leaking data.
One CSV with an id and the text is enough. A thousand short tickets of 40 tokens each is about 40,000 tokens, a job for the cheapest model.
Step 1. Clustering the questions
The goal is to turn "where is my order", "when will the parcel arrive" and "tracking number does not work" into one canonical question and learn how many tickets stand behind it. Do it in batches, because there is no need to put 5,000 tickets into a single request.
import json
import os
import re
from openai import OpenAI
client = OpenAI(
base_url="https://api.zukko.pro/v1",
api_key=os.environ["ZUKKO_API_KEY"],
)
def ask(model, system, user, max_tokens=3000):
r = client.chat.completions.create(
model=model,Split the tickets into batches of 100–150 and call cluster_batch for each. Then collect the canonical questions from all batches into one list and give it to the model once more, asking it to merge duplicates and sum the ids. You get a list of questions with frequencies. Sort in descending order and take the top 20–40: a tail of single tickets does not belong in an FAQ.
Check this part by eye: open three tickets from each group and make sure they are really about one thing. A cheap model sometimes glues together close but different topics, such as "refund" and "cancel the order".
Step 2. Find a source for every answer
Answers must come from your knowledge base: help articles, regulations, terms. Cut the documents into chunks of 200–500 words and give each an id such as kb-12. A vector database is not required: for a hundred documents, word-overlap search is enough, and the scheme is described in the article on RAG without a vector DB.
def stems(text):
return {w[:5] for w in re.findall(r"\w{4,}", text.lower())}
def pick_chunks(question, chunks, k=4):
q = stems(question)
scored = sorted(chunks, key=lambda c: len(q & stems(c["text"])), reverse=True)
return scored[:k]Cutting words to five letters is crude, but it partly smooths over Russian word endings. If chunks are found badly, that is a sign the knowledge base has no answer to a frequent question. That result is valuable in itself: you found a hole in the documentation.
Step 3. An answer with a source reference
The rule that separates a working FAQ from an invented one: the model answers only from the supplied chunks, lists their ids and honestly says when there is no answer.
ANSWER_PROMPT = (
"You write an answer for the FAQ section of a website. Use ONLY the supplied chunks. "
"Answer in 2-4 sentences, calm tone, no promises beyond the chunk text. "
"At the end list the ids of the chunks used in square brackets, e.g. [kb-12][kb-3]. "
"If the chunks contain no answer, return exactly NO_ANSWER."
)
def make_answer(question, chunks):
context = "\n\n".join(f"[{c['id']}] {c['text']}" for c in chunks)
user = f"Question: {question}\n\nChunks:\n{context}"
return ask("claude-sonnet-5-5", ANSWER_PROMPT, user, max_tokens=500)
def check_sources(answer, chunks):
allowed = {c["id"] for c in chunks}
cited = set(re.findall(r"\[(kb-\d+)\]", answer))
return bool(cited) and cited <= allowedcheck_sources catches two kinds of defect: an answer with no source reference and a reference to a chunk the model was never given. Send everything that failed to a manual queue. A stronger model suits answers than grouping: in the Zukko table claude-sonnet-5-5 has a strength index of 48 and deepseek-v4.1-flash 39, but choose by the result on 20 of your own questions, not by the index.
Do not publish the [kb-12] marks in the final text: they are for your verification. Before publishing replace them with a link to the help article or remove them.
Step 4. Verification before publishing
- A human reads every answer. An FAQ is short, it is an hour of work, and a mistake in refund terms or a price costs more.
- Compare with the source. Open the chunk and compare numbers, deadlines and plan names. Check numbers with special care: an error in them is the hardest to spot on a quick read.
- A second model as a checker. Optional: give another model the answer and the chunk and ask whether the answer contains claims absent from the chunk. This supplements the human check, it does not replace it.
- One style. Set tone and length in the system prompt so answers do not sound stitched from different sources.
The finished FAQ can be marked up for search engines in JSON-LD:
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [{
"@type": "Question",
"name": "How do I check my order status?",
"acceptedAnswer": {"@type": "Answer", "text": "The status is shown in your account under Orders."}
}]
}A rich snippet in search is not guaranteed: search engines show such blocks selectively. The markup stays useful because it structures the page. Website copy assembled this way is closer to real customer speech than copy written from scratch.
What FAQ generation with a neural network costs
Prices on October 18, 2026, per 1M tokens. Example: 2,000 tickets and 30 final questions.
- Grouping on
deepseek-v4.1-flash(input $0.013, output $0.038). Input: 2,000 × 40 = 80,000 tokens, output about 20,000. Cost = 80,000 × 0.013 / 1,000,000 + 20,000 × 0.038 / 1,000,000 = 0.00104 + 0.00076 ≈ $0.002. - Answers on
claude-sonnet-5-5(input $0.4, output $2). Per question 3,000 tokens of chunks and prompt and 200 tokens of answer: input 30 × 3,000 = 90,000, output 30 × 200 = 6,000. Cost = 90,000 × 0.4 / 1,000,000 + 6,000 × 2 / 1,000,000 = 0.036 + 0.012 = $0.048.
About $0.05 per run in total, not counting reruns. The real bill depends on the length of your chunks; current prices are at GET https://api.zukko.pro/v1/models or on the "Models" page in the dashboard. Request parameters are described in the chat completions reference.
FAQ
Can I build an FAQ without tickets, from documentation only?
You can, but the model will have to invent the questions, and they will be about what is logical to ask rather than what people actually ask. Even 200–300 real tickets noticeably change the result.
What if the model returned NO_ANSWER?
That is a useful signal. Either the knowledge base has no answer and you must write one by hand, or the search missed the right chunk: raise k or improve how documents are split.
How do I get stable JSON from a model?
Ask for "JSON only", trim the text at the first and last bracket (as parse_json does) and retry on a parse error. More in the article on a structured JSON response.
How often should the FAQ be refreshed?
Rebuild it once a quarter or after terms change: a fresh slice of tickets shows which questions grew and which went away.
If you want to try it on your data, create a key in the Zukko dashboard and give it a small daily limit.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- AI for testers: test cases, checklists and bug reportsHow a QA engineer can use a model for test cases, checklists and bug reports, check coverage, and avoid requirements the model made up.
- Anonymizing data before sending it to a neural network: a practical wayYou do not have to send personal data to a model for it to process a document. How to replace it with labels, restore it in the answer and check that nothing leaked.
- A neural network for HR: job posts, candidate emails and resume reviewA model can draft job posts and emails and do a first-pass resume review. What to hand over, how to treat candidates' personal data and how to prevent biased decisions.
- Website copy with AI: homepage, services and FAQ without fillerAI writes smoothly but does not know your business. How to build a brief with facts, get page structure, edit the result before publishing, and what it costs.