Most portfolio chatbots are a search box that answers in paragraphs. You ask what someone has worked on, you get prose, and then you scroll to find it yourself. The model knows the answer and the page it is sitting on stays exactly where it was.
The one on this site can move the page. Ask to see incident work and it opens the record filtered to incidents. Ask about a section and it scrolls there. That is not a different model, it is the same tool-calling mechanism everyone already has, pointed at the interface rather than at a knowledge base.
The key stays on the server, and that decides the architecture
The first real decision is not about the model. If the browser calls the model provider directly, the API key is in the browser, and anybody who opens devtools has your billing. Every "add AI to your site in five minutes" guide that keeps the key client-side is teaching a mistake that only shows up on an invoice.
So the request goes to a serverless function, which holds the key, attaches the system instruction and the tool declarations, calls the model, and streams the answer back. The client sends the conversation and nothing else. That single constraint determines most of the rest of the design, and it is worth accepting it up front rather than discovering it later.
It has a second effect that is easy to miss: because the system instruction is assembled server-side, it can be changed without shipping a new bundle, and a visitor cannot read it. The prompt is not secret, but it is also not something to hand out with the JavaScript.
Five tools, and what they are for
Four of them do something to the page: search the work, open the record with an optional category filter, scroll to a section, toggle the theme. One of them, saveLead, records that a visitor wants to be contacted.
The declarations are the interesting part, because they are prompt engineering wearing a schema. The description on openWork does not just say what it does, it says when to reach for it and names the one argument value that will not work. A tool whose description reads "opens the record" gets called at the wrong moments; a tool that explains its own preconditions gets called at the right ones. Most of the behaviour people attribute to the system prompt is actually in the tool descriptions.
Retrieval that is deliberately not a vector database
Before the model answers, a retrieval step pulls the entries most relevant to the question and puts them in the prompt. It is a substring matcher over the record. There are no embeddings and no vector store.
For a few dozen entries with distinctive names, that is the correct engineering. Embeddings would add an index to build, a service to depend on, a rebuild step to forget, and a class of failure where the nearest neighbour is confidently wrong. The substring matcher is inspectable, and when it misses, it misses for a reason you can read.
What it does badly is fail silently. Rename a record or change the category vocabulary and retrieval quietly returns less, and the bot gets vaguer rather than erroring. Nothing in the response tells you this happened. That is precisely why the retrieval layer has tests: not because the matching is subtle, but because its failure mode is degradation rather than a stack trace, and degradation does not page anyone.
The same reasoning covers the couplings between the bot and the site. The categories in the tool declarations have to match the categories in the data, and the section names it can scroll to have to match the sections that exist. Both are asserted in tests, because the alternative is a chatbot confidently offering to scroll somewhere that was renamed six commits ago.
The lead flow, and the gate before it
The tempting build fires saveLead the moment it has five fields. That produces a form wearing a conversation, and it submits while the visitor is still mid-thought.
Instead the model has to ask one closing question first, confirming there is nothing else to add, and only then calls the tool. The reply that follows carries a request ID, confirms where the transcript was sent, and gives an address to follow up on with that ID, so the visitor leaves with a reference rather than a hope.
That gate is one paragraph in the instruction and it is the difference between a conversation that captures a lead and an interrogation that happens to be written in sentences.
What I would keep
Tool calling for interface control rather than only for retrieval. The key on the server, without exception. The smallest retrieval that works, with tests around it precisely because it fails quietly. And a persona that positions the work honestly: the site argues that AI is one tool inside systems work rather than the identity, and a chatbot that oversold itself would contradict the page it lives on.

Top comments (0)