Build Your Own Perplexity in About a Hundred Lines of Python A developer demonstrates that building a functional answer engine like Perplexity requires only about a hundred lines of Python by combining three steps: searching the web, reading top results, and prompting a language model to answer with citations. The architecture relies on a search API and a language model instructed to use only provided sources, making the system transparent and improvable. An answer engine, the kind of tool that reads the web and hands you a written answer with citations instead of a list of blue links, sounds like it must be a huge system. It isn’t. Underneath, the whole idea is three steps you can write in an afternoon, search the web, read the top results, and ask a language model to answer using only what it found, with citations. This walks through a complete, working version you can run yourself, explains why each piece is there, and shows exactly how to make it better once the basic one is running. The first time you use one of the AI answer engines, the ones that respond to a question with a written paragraph and little numbered citations rather than ten links, it feels like a fundamentally different kind of software than a search engine. It isn’t, really. It’s a search engine with a language model bolted onto the end, and the clever part isn’t any single component but the way three simple pieces fit together. Once you see the shape of it, the mystery evaporates, and you realize you can build a working version of the core yourself in about a hundred lines of code. That’s what this piece does. We will build a small but genuinely working answer engine, the kind of thing Perplexity popularized, from scratch. It will take a question, search the web, read the top few results, and produce an answer that cites its sources. It won’t have the polish, the speed, or the scale of a production system, but it’ll do the real thing, and every piece will be visible and swappable. By the end you’ll understand not just how to build it but why answer engines are designed the way they are, and what separates a good one from a chatbot that makes things up. Here’s the entire architecture, and it’s worth holding in your head before any code, because everything else is detail hung on this frame. A question comes in. You send it to a search API and get back the top handful of web results. You fetch those pages and pull out their readable text. You hand that text to a language model along with the original question and a firm instruction, answer using only these sources, and cite them. The model writes the answer with little numbered references pointing back at the source list. That’s it. Search, read, then answer from what was read, with citations. Every answer engine, no matter how sophisticated, is a version of this loop. The fancy ones add cleverness at each step, better search queries, smarter selection of which text to keep, streaming output, follow-up questions. But the spine is always these three moves, and the single most important one, the thing that makes it an answer engine rather than a chatbot, is that last instruction. The model isn’t answering from its own memory. It’s answering from fresh sources it was just handed, and it’s citing them, which is what makes the answer both current and checkable. You don’t build a web crawler. You use a search API, a service that takes a query and returns ranked results, and there are several built for exactly this purpose. Some are designed specifically to feed language models and return clean, readable content alongside each result. Others are general web-search APIs. For our purposes, the only thing that matters is that you send a query and get back a list of results with URLs and titles. Here’s the search step. python def search web query, num results=4 : resp = requests.post "https://api.tavily.com/search", json={ "api key": SEARCH API KEY, "query": query, "max results": num results, "search depth": "basic", }, timeout=30, resp.raise for status data = resp.json results = for item in data.get "results", : results.append { "title": item.get "title", "" , "url": item.get "url", "" , "snippet": item.get "content", "" , } return results The key design decision here is that the rest of the program only needs a list of results with URLs back from this function. That means the search provider is completely swappable. If you want to change which search service you use, you rewrite this one function and touch nothing else. That kind of clean seam, where one component can be replaced without disturbing the others, is worth building in deliberately, because you’ll want to experiment with different search backends and different models as you go. How many results should you read. Three to five is a sensible default. More sources give the model a fuller picture, but each one costs tokens and time, so there’s a balance. Start with four. The search results come with short snippets, but snippets are thin. To get a real answer, you want the actual content of the pages. So you fetch each result and extract its readable text. python def fetch sources results : sources = for i, r in enumerate results, start=1 : text = r "snippet" fallback if the page will not load try: resp = requests.get r "url" , timeout=15, headers={"User-Agent": "Mozilla/5.0 answer-engine example "} if resp.ok and resp.text: extracted = extract text resp.text if len extracted 200: text = extracted except requests.RequestException: pass keep the snippet fallback sources.append { "id": i, "title": r "title" , "url": r "url" , "text": text :PER SOURCE CHARS , } return sources Two things in here matter more than they look. First, the fallback. Web pages fail to load all the time, they time out, they block you, they return junk. So if a fetch fails, the code falls back to the search snippet rather than crashing. One bad URL should never sink the whole answer, and building in that resilience from the start saves a lot of frustration. Second, the length cap, that PER SOURCE CHARS trim at the end. A small model has a limited context window, and even a large one costs more the more you feed it, so you keep a bounded slice of each page rather than dumping entire articles into the prompt. Trimming each source to a couple of thousand characters keeps the whole thing affordable and fast while still giving the model plenty to work with. The extraction itself, turning messy HTML into plain text, can be as simple or as fancy as you like. Here is a deliberately simple version that strips scripts and styles, removes tags, and collapses whitespace. python def extract text html : html = re.sub r"< script|style ^ . ?