LLM Systems Engineering · Chapter 13 of 20
RAG in the enterprise
Permissions, real data sources, keeping the index honest, and an unsentimental look at building versus buying.
The previous chapter built a working system. Everything here is what separates that from something you can put in front of an organisation — and almost none of it is about models.
Permissions, which is the whole problem
In a demo everyone sees everything. In a company, an HR investigation, a redundancy list, a salary band and a board pack all sit in systems your index would happily read. Retrieval does not inherit your access control. Somebody implements it, per query, per user.
Why post-filtering fails in a way that is easy to miss in testing
The obvious failure is a bad answer. Search returns ten results, seven get dropped, and the user is answered from three — a quietly worse answer than a colleague gets for the same question, with nothing on screen to indicate why.
The subtle failure is that counts leak. “No results found” and “3 results, 7 hidden” are different pieces of information, and the second one tells the asker that documents about their query exist. For a query like an employee name plus ‘investigation’, the existence is the sensitive fact.
Neither shows up in functional testing, because functional testing is done by someone with broad access. Both show up in an audit.
Pre-filtering is the correct default, and it has one requirement: every chunk must carry the groups permitted to read it, captured at ingestion and indexed so it can be filtered on cheaply. The query then intersects the asking user's group memberships with that field, as in the SQL in the previous chapter.
- Store group identifiers, not user identifiers. Groups change far less often than membership, so a joiner or leaver becomes a change in your identity provider rather than a re-index of the corpus.
- Resolve the user's groups at query time, from the identity provider or a short-lived cache — not from a session token issued hours ago.
- Deny rules need explicit handling. Many source systems have them; a simple ‘allowed groups’ array cannot express them. Decide deliberately whether to model denials or to exclude documents that carry them, and write the choice down.
- A late authority check — re-verifying the final few chunks against the source system before they enter the prompt — is the belt-and-braces option. It costs latency on a handful of documents rather than thousands, and it closes the window between a permission changing and your index learning about it.
Real data sources, and what each one costs you
Demos ingest a folder of PDFs. Production ingests systems that were not designed to be ingested.
| Source | How you get it | What actually bites |
|---|---|---|
| SharePoint / OneDrive | Microsoft Graph API, with delta queries for changes | Permissions are per-item and inherited through a hierarchy that has been edited for a decade. Resolving effective permissions is harder than reading the files. |
| Confluence / Jira | REST APIs, with space and project scoping. Worked end to end in APIs, webhooks and MCP — including the chatbot pattern. | Wildly mixed quality — current documentation and five-year-old drafts look identical to a retriever. Filter on last-modified date or you will confidently serve obsolete policy. |
| Email and Teams | Graph API | Enormous, extremely personal, and rarely worth it. The privacy conversation is longer than the engineering. |
| File shares | Direct read, plus the Windows file permissions (NTFS) attached to each folder | Duplicates everywhere. The same policy in nine versions, and retrieval will cheerfully cite the wrong one. |
| Databases and warehouses | SQL, one chunk per record | Usually the highest-value and lowest-effort source, and routinely overlooked because it is not a document. |
| Ticketing systems | REST API | Excellent material — real questions with real resolutions. Strip customer names and identifiers at ingest. |
The failure that damages trust fastest is not a wrong answer. It is a confidently cited obsolete one.
A retriever has no idea that a 2019 expenses policy was superseded in 2024. Both are relevant; both match; the old one may match better because it is longer and more detailed.
Two mitigations, and you want both. Filter or down-weight by last-modified date at retrieval, and show the document date in the citation so a human can catch what the system cannot.
The second one is nearly free and it is skipped constantly.
The most common single request: a chatbot over the company wiki
It is worth knowing that this has three implementations, not one, and the good systems use all three:
1. Index it — the pipeline in the previous chapter, pulling pages through its REST API — the ordinary web interface every such system exposes — on a schedule, with each page’s read-restrictions stored alongside the text. Fast, searchable across thousands of pages, and the only option that supports the permission pre-filtering above.
2. A webhook so an edit reaches the index in seconds instead of waiting for the nightly sync. Keep the scheduled sweep as well — webhooks get missed and you will not know which.
3. A live tool call, over MCP — the Model Context Protocol, a standard way to describe a tool to a model — so it can fetch the current version of a specific page it has already found — guaranteeing freshness where the index can only promise last-sync.
And the question that decides whether the live tool can ship: whose credentials does it use? A shared service account hands every user the permissions of the most privileged one. It is the pre-filter/post-filter problem in different clothes.
Fully worked, with code and diagrams, in APIs, webhooks and MCP.
Keeping the index honest
An index is a copy of your data, and copies drift. Four things need an owner, and if you can answer these four questions you have a real system:
- Freshness. How long between a document changing and the index reflecting it? Nightly is usually fine for policy; near-real-time is needed for anything operational. Use the source system's change feed rather than re-crawling everything — Graph delta queries, database change tracking, webhooks.
- Deletion. When a document is deleted or reclassified, what removes it, and how fast? This is the question that fails audits, because deletion is almost never implemented at the same time as ingestion.
- Reindexing. Changing the embedding model means re-embedding everything. Plan for it: build so you can index into a new table and switch over, rather than mutating the live one.
- Right-to-erasure. If a person can demand deletion of their data, you must be able to find and remove every chunk derived from it. That requires the link from chunk back to source record — which is a schema decision, made at the start or not at all.
Where it sits, architecturally
A production retrieval system is usually four services, and naming them separately is worth doing because they scale and fail differently:
- The ingestion workers — scheduled, batch, tolerant of being slow. This is where most of the code lives and most of the bugs.
- The store — vectors, text, metadata and access control together.
- The retrieval service — takes a question and a user identity, returns ranked permitted chunks. Keep this separate from generation; it is independently testable, independently useful, and the thing you will replace least often.
- The generation service — assembles the prompt and calls the model. Behind an AI gateway if you have one.
Log the retrieved chunk identifiers alongside every answer. Without them you cannot reproduce a bad answer, and reproducibility is the fifth of the five audit questions in the foundations chapter. It is one column, added at the beginning or painfully later.
Build or buy
Every major cloud sells a managed version, and there are dedicated products. The decision turns almost entirely on connectors and permissions rather than on retrieval quality.
| Option | What you get | When it is right |
|---|---|---|
| Azure AI Search | Managed indexing and retrieval, connectors into the Microsoft estate, permission-aware retrieval at the platform layer | A Microsoft 365 estate. The SharePoint permission problem above is the hard part, and this is where it is already solved. |
| Amazon Bedrock Knowledge Bases | Managed ingestion, indexing, retrieval, reranking and citations, with metadata-based access filtering | An AWS-centred estate, or when you want the pipeline managed but the data staying in your account. |
| Vertex AI Search | Managed semantic search with generative answers and data-source access controls | A Google Cloud estate. |
| Glean and similar | Very many pre-built connectors, permission-enforced search across the whole workplace | Broad workplace search across many systems. The connector library is the product; building that yourself is the multi-year part. |
| Build it | Complete control of chunking, ranking, prompts and cost | One or two well-understood sources, unusual retrieval requirements, or data that cannot leave your network. |
The question that actually decides it
Not “can we build retrieval?” — the previous chapter shows you can, in an afternoon.
It is: how many source systems, and how hard are their permission models?
One or two systems with simple permissions → build. You will get better results because you can tune chunking to your actual documents, and you will spend less.
Eight systems, each with its own permission model → buy. The connectors and the effective-permission resolution are the product. Writing those yourself is a multi-year commitment that has nothing to do with AI, and it is the part teams systematically underestimate.
A hybrid is common and sensible: buy the broad workplace search, build the narrow high-value system over the two sources that matter most.
Cost, roughly
Retrieval costs are usually dominated by things people do not expect:
- Embedding the corpus once is a real but one-off cost, and it is small compared with re-embedding it every time someone changes the model.
- Query-time embedding is trivial. One short embedding per question.
- The generation call dominates, and its cost is driven by how much context you paste in. Retrieving 20 chunks instead of 5 quadruples your bill and usually makes answers worse, because the relevant passage gets buried. Reranking is a cost reduction as well as a quality gain.
- Storage is cheap; RAM is not. See the internals chapter — keeping a graph index resident is the constraint that decides your hosting bill.
What goes wrong, in order of frequency
- Permissions bolted on afterwards. The pilot ignores them, then adding them means re-ingesting everything and the project restarts.
- Obsolete documents cited confidently. No date filtering, no date shown.
- No evaluation set, so every change is argued about rather than measured.
- Retrieval quality blamed on the model. Months spent switching models when recall@10 was 0.6 the whole time.
- Deletion never implemented. Discovered by an auditor.
- Everything indexed because it was possible. A larger index is not a better one — it is more chances to retrieve something irrelevant, and a larger surface to govern. The best-performing systems are usually the ones with the most restraint about what went in.
- Retrieved text treated as trusted input. It arrives in the same channel as your instructions, so a document containing instructions is an attack — see prompt injection. If the corpus contains anything user-submitted, this stops being theoretical.
Check yourself