← Back to home WHY IS ASKING A COLLEAGUE STILL FASTER THAN SEARCHING?

Why is company knowledge always faster to ask for than to search?

AI Tool · Mar 2026 – present · Product, RAG architecture, deployment and operations

Most small companies look alike: knowledge scattered across Wiki, Google Docs and Notion — three search boxes, three permission systems. New hires bounce between systems looking for an SOP; senior colleagues get interrupted all day answering questions that were written down long ago; the KM becomes a write-only system. The e-commerce company I work at was exactly that — 30 people, HR policies in Word files, SOPs in Wiki.js, product specs in Notion. When a colleague came to ask me for the third time in a month "where's the complaint-escalation flowchart?", I decided to build something: an AI bot living inside Microsoft Teams that searches all three sources at once, always cites its source, and can write knowledge back.

0+ Indexed documents
0+ Knowledge chunks
< 3s Response time
NT$2,500 Monthly Azure cost

APPROACH

The idea

Put a conversational interface inside the tool everyone already uses — Microsoft Teams — and have it search every source at once. The bot needed to:

  1. Sync content from Wiki.js, Google Docs and Notion on a schedule
  2. Answer questions with RAG (retrieval-augmented generation), citing the original source
  3. Make contributing knowledge as easy as sending a message — jot something down → one command publishes it as a KM page

The bar I set: it had to be better than asking a senior colleague directly, not just "barely usable".

ONE CHAT, EVERY SOURCE WIKI.JS GOOGLE DOCS NOTION HOURLY SYNC VECTOR INDEX 9,600+ CHUNKS TEAMS CHAT ANSWERS CITE THEIR SOURCES — VERIFYING THE AI COSTS ONE CLICK

CHALLENGE 01

How do you get people to remember to use it?

Knowledge tools usually die not from missing features, but because nobody remembers to open them. The other route was a standalone Q&A website — far more interface freedom. As PM I didn't take it: living inside the Teams everyone already has open all day drops the cost of asking to almost zero; the bonus is that authentication rides on company accounts, so I never had to build a login system — home-made credentials usually only get half of it right.

THREE DECISIONS FIRST ROADS NOT TAKEN WHERE A NEW WEBSITE → LIVE INSIDE TEAMS TRUST “TRUST THE AI” → EVERY ANSWER CITES SOURCES WRITES READ-ONLY KB → WRITE-BACK + AUDIT TRAIL KNOWLEDGE TOOLS DIE BECAUSE NOBODY REMEMBERS TO OPEN THEM

CHALLENGE 02

How do you get people to trust an AI's answers?

The premise: AI will say wrong things. That's a premise, not an accident. So every answer carries links to the source passages, and users can check the original in one click — drive the cost of verifying the AI to a minimum, and trust can be built.

And the retrieval itself was sharpened by real users. The naive RAG pipeline (embed the question → cosine search → generate) looks great in a slide demo and falls apart on contact with real users. Real users type vague fragments like "LINE push notification setting location?" — the correct document, "LINE push notification settings", gets dragged by the word "location" towards UI-settings documents and doesn't even make the top five. The fix: before searching, use GPT to expand the question into 2–3 semantic variants, search each, then merge and rank by each chunk's best score. For the short fragments users actually type, recall improved noticeably.

Later I added hybrid search (semantic + weighted keyword). It started with a gap I hit myself: a requirements doc that was definitely in Notion, and a plain-language question just couldn't find it. Pure semantic retrieval is genuinely weak on proper nouns and product codes — "Zhou Baobao" is just a string of characters to the model. With keyword weighting, the same question moved from rank 27 to rank 5, and clearly-phrased questions settled at rank one.

But there's an honest limitation here: query expansion is done by an LLM, so it isn't deterministic, and asking the same question twice shifts the ranking. The truly stable approach isn't tuning retrieval to perfection — it's giving the AI both code and documents to cross-check. That idea later became another project (the FruitWeb Diagnostic MCP).

The input to optimisation is always how real users phrase things, never how I imagine they would. Every failed search is a piece of training material; I feed it back into the system, and the next person finds it.

HYBRID SEARCH, MEASURED SAME QUESTION · SAME DOCUMENT SEMANTIC ONLY RANK 27 SEMANTIC + KEYWORD RANK 5 SHORTER BAR = FOUND SOONER A PRODUCT NAME IS JUST A CHARACTER SEQUENCE TO THE MODEL — KEYWORDS CATCH WHAT EMBEDDINGS MISS

CHALLENGE 03

How do you keep the knowledge base from rotting?

A read-only knowledge base slowly rots, because the person who spots the typo has no permission to fix it. So the truly critical feature isn't search — it's that the bot can also edit, append, and publish content back to all three sources.

#edit lets anyone fix a typo without learning the Wiki.js back office. #append lets them add a step an SOP missed. #publish turns a quick note into a properly formatted KM page (AI fills in the title and structure). Every Notion edit also writes an audit annotation automatically — Notion has no per-block edit history, so that annotation is the only trail for tracing things later. Capabilities can be opened up, as long as what happens can be traced back.

The effect: the knowledge base is continuously corrected by the people who actually notice the problems, instead of waiting for some editor to be notified. Contributing knowledge went from "something you schedule time for" to "something you do in passing".

FEATURES

Two features that grew on the side

Personal assistant. Since the UI was already there, I added notes and reminders — the bot doubles as a personal sticky-note board. The daily 9am / 2pm / 5pm reminder pushes have a small engineering story behind them (see "What I learned") — it should have been simple, and wasn't.

End-to-end image support. Users can attach images to notes and reminders. Images live in private Azure Blob storage with a short-lived SAS token generated for every display — no public URLs anywhere. When a note is published the image is embedded into the KM (base64) or uploaded to Google Docs (via Drive), and the original blob is deleted immediately — the temporary storage lifecycle is deliberately short.

Data you don't keep can never leak from you. The shorter temporary files live, the less there is to protect.

A PERSONAL ASSISTANT ON THE SIDE THE UI WAS ALREADY THERE NOTES JOT → KEEP → #PUBLISH REMINDERS 09:00 · 14:00 · 17:00 IMAGES ATTACH TO EITHER THE DAILY PUSHES HIDE AN ENGINEERING STORY — SEE LESSONS BELOW
SHORT-LIVED BY DESIGN IMAGE PIPELINE UPLOAD PRIVATE BLOB NO PUBLIC URL SHORT-LIVED SAS TOKEN PUBLISH → DELETE BLOB DATA YOU DIDN'T KEEP CAN NEVER LEAK FROM YOU

RESULTS

How it actually runs

Live since March 2026. Steadily holding 690+ documents, about 9,600 indexed chunks, three sources, hourly sync, sub-3-second answers, and roughly NT$2,500 a month in Azure costs. Usage telemetry was added in July, so real usage patterns are now visible rather than just my observations.

The most interesting part isn't the technical metrics, it's the usage patterns:

  • Customer service uses it to look up policy thresholds live, while on the phone
  • New hires treat it as self-serve onboarding — HR gets far fewer "where's the form" interruptions
  • A group of power users has internalised the #publish flow, and the bot has become their main channel for contributing knowledge

The knowledge base itself is growing faster than before. Not because anyone was told to write, but because the barrier dropped low enough that contributing became a side effect of using it.

IN PRODUCTION SINCE MARCH 2026 MEASURED, NOT OBSERVED 690+ DOCUMENTS ~9,600 CHUNKS < 3S ANSWERS NT$2,500 / MONTH HOURLY SYNC · 3 SOURCES · USAGE TELEMETRY SINCE JULY

LESSONS

Things only production can teach you

  • node-cron fails silently. In a long-running process the hourly schedule "just stopped" while the short-interval one (*/5) kept going. My first fix "worked in theory" but only took effect on restart — and the bot never restarted. The real fix: a 10-minute polling safety net that doesn't depend on cron at all.
  • Azure App Service defaults to a 32-bit worker, capping the Node heap at 90MB regardless of --max-old-space-size. 6,000 chunks and it OOMs. One CLI command switches it.
  • wwwroot is wiped on zip-deploy, including data/ if that's where you put the vector store. The whole knowledge base vanished once because of this. Fix: point the path at a persistent directory via an env variable.
  • Teams sends content-type: image/* (a wildcard, not the actual MIME type), and contentUrl needs Bearer auth to download. A chain of wrong assumptions to dismantle one by one.
  • Incremental deploys to save upload time took production down twice. The full package is 110MB and the incremental one 1.2MB, so I only uploaded the changed parts and reused node_modules on the server. The app wouldn't start — a ten-minute crash loop. Only the iisnode crash stack from Kudu showed the real culprit: the telemetry package, while loading a third-party module, read a file that had been written halfway. The root cause wasn't my code — it was inconsistent package state on the server caused by incremental deployment. Back to a full zip replaced in one go and it was fine. The upload time saved was paid back with two production outages.
  • A full deploy deletes whatever isn't in the package. After fixing the above, the full package didn't include the web/ folder, so deployment deleted the web chat interface's homepage (Teams was fine, so I almost didn't notice). A web-only package restored it.

I ran this list by the incident-handling rules: every outage got a root-cause analysis, and the lessons from the two deployment incidents became one hard rule — this service always deploys as a full package, replaced in one go, never incrementally. The rule doesn't rely on memory; it's written into the deploy script.

The full postmortem is in the repo, including the architecture choices and what I'd do differently.

ONLY PRODUCTION TEACHES THIS SIX BUGS THAT SURVIVED TESTING CRON DIES SILENTLY 32-BIT HEAP CAP · OOM WWWROOT WIPED ON DEPLOY image/* WILDCARD MIME INCREMENTAL DEPLOY ×2 OUTAGES FULL DEPLOY DELETES WHAT'S MISSING EVERY OUTAGE → WRITTEN RCA → A RULE IN THE DEPLOY SCRIPT FULL PACKAGE · SINGLE SWAP · NEVER INCREMENTAL — THE RULE DOESN'T RELY ON MEMORY

NEXT

What's next

  • Image OCR: many SOPs keep the key information in screenshots that text-only RAG can't see. A vision pass over images during sync.
  • Swappable LLM provider: being tied to Azure OpenAI has a practical reason (direct calls to OpenAI / Anthropic from Asia return 403), but one interface abstraction would allow switching to OpenAI / Anthropic / Bedrock.
NEXT IMAGE OCR AT SYNC SCREENSHOTS → SEARCHABLE SWAPPABLE LLM PROVIDER ONE INTERFACE LAYER AWAY THE AZURE LOCK-IN HAS A PRACTICAL REASON — REGIONAL 403S — NOT AN ARCHITECTURAL ONE

STACK

Stack

TypeScript, Node.js, Azure OpenAI (gpt-4.1 + text-embedding-3-small), Microsoft Bot Framework, Wiki.js GraphQL, Google Drive API + Docs API, Notion API, Azure Blob Storage, Azure SQL, Application Insights. Deployed on Azure App Service.

Source is on GitHub (jean199801/teams-knowledge-assistant) — the README has the full architecture write-up if you want the technical detail.

ARCHITECTURE TEAMS BOT · NODE/TS AZURE APP SERVICE RAG + WRITE-BACK AZURE OPENAI · GPT-4.1 + EMBEDDINGS PRIVATE BLOB · SHORT-LIVED SAS WIKI.JS · GDOCS · NOTION SYNC IN · AUDITED WRITES OUT HOURLY SYNC · EVERY WRITE LEAVES AN AUDIT NOTE