Capstone Project and Retrospective: A Multi-Tenant Enterprise Knowledge-Base Q&A, a RAG Decision Map, and an Interview Deep Dive
Turn thirteen days of work into a portfolio-ready project: a multi-tenant, cited, evaluation-dashboarded enterprise knowledge-base Q&A system. Then compress the whole course into one decision map that answers the questions interviewers most often follow up on.
Today's Goals
- Assemble the previous thirteen days' modules into one complete multi-tenant project with citations and an evaluation dashboard, and explain its architecture
- Draw a RAG decision map that, given a new requirement, points to which configuration to use and why
- Handle interview follow-up questions, explaining every technical choice using this course's data and trade-offs
No new theory today. Every one of the thirteen days left a part and a set of numbers, and today does three things only: assemble the parts, gather the numbers into a map, and turn that map into something you can say out loud in an interview. Come back and tick off the three goals.
Plain-Language Walkthrough
One: three criteria for a portfolio project
An unwelcome fact first: "I built a RAG project" carries nearly zero information in an interview. Everybody has built one. What separates people is the three follow-ups: how do you know it works, which decision would you change now, and if you could do only three more things, which three?
So a project fit for a portfolio is judged not by feature count but by these three:
One, it demonstrates. Within five minutes you must be able to feed in a document, ask a question, click a citation to see the original, and open a dashboard showing metrics. Without that sequence, your project is a README in somebody else's eyes. That is also why today's lab ends by asking you to write a demo script — the demo path is itself a design review, and where it stumbles is usually where the design is wrong.
Two, it explains its trade-offs. Every configuration entry needs a "because of this, therefore that," and that because is best measured by you. Thirteen days already gathered the raw material: why BM25 came before vectors, why reranking applies only to the top twenty, why the gate sits on the raw score rather than the fused one — each has a measurement behind it.
Three, it has reproducible data. Not "it works well" but "on this thirty-document corpus, these twenty questions, and a six-hundred-token budget: 93.8% recall, 75.0% on the multi-hop tier, 0.0% refusal rate on unanswerable questions." That last 0.0% especially must be written down — a project description reporting only good numbers is more suspicious than one reporting none.
The hardest of the three is the second. The raw material for trade-offs is already yours, and how do dozens of conclusions scattered across thirteen days become something portable, opened whenever a new requirement arrives? That takes a map.
Two: multi-tenancy is one thing landing in three places
Convert the project from single-tenant to multi-tenant first, since it is the minimum bar for the words enterprise knowledge base and the easiest thing to get wrong.
The wrong form is highly consistent: treating a tenant as a sieve at query time. Retrieval runs over the whole store as usual and the results are then filtered down to this customer's. D13 already argued why that is wrong — unauthorized chunks were read into memory, participated in ranking, and most likely entered a log; and the more practical consequence is diluted results, where the user sees nothing found and your log shows a normal retrieval.
The right approach splits one thing across three places, and missing one makes the other two ineffective:
Mermaid source
flowchart LR
A[Source documents] -->|Landing one: label at ingestion| B[Tenant-labeled chunks]
B --> C[Index]
D[User question] --> E{Landing two: filter at retrieval}
C --> E
E -->|predicate before scoring| F[Candidates and answer]
F -->|Landing three: report per tenant| G[Dashboard]Landing one, labeling at ingestion. A chunk id goes from doc-001#c01 to t-skyladder/doc-001#c01: two workspaces' chunks are physically different rows. The lab projects the same thirty-document corpus into two workspaces — headquarters subscribes to four departments and gets 134 chunks (verbatim identical to day seven's system), and a trial customer imported only the product manual and gets 44. Labeling happens at ingestion, not at query time, or the other two landings have no field to work with.
Landing two, filtering at retrieval. The predicate goes into the same statement as the ordering and the LIMIT, and the keyword path likewise — chunks outside the scope never participate in scoring once. The difference is measurable: restricted to the support scope (23 chunks), pre-filtering has the vector path honestly returning 23; switch to ordering over the whole store, taking the top 50 and then filtering, and only single digits return, with no error raised.
One easily missed detail: the keyword index is built per tenant, not per department. Inverse document frequency is a corpus statistic, and sharing one index means tenant A importing ten thousand documents changes the term weights tenant B sees; conversely, building per department makes term weights depend on support happening to be the asker. Permission narrowing happens at candidate filtering, not at the statistics layer.
// Landing one: project at ingestion. A document is chunked for a tenant only if it falls in that tenant's subscription
export function ingest(docs, tenants) {
const chunks = []
for (const tenant of tenants) {
for (const doc of docs) {
if (!tenant.subscribes.includes(doc.department)) continue
chunks.push(...chunkDoc(doc, tenant.id))
}
}
return chunks
}
// Landing two: the scope test is written once and shared by the in-memory implementation, the SQL
// implementation, and evaluation — three separate tests are the most common source of permission incidents
export function inScope(chunk, scope) {
if (chunk.tenantId !== scope.tenantId) return false
return scope.departments ? scope.departments.includes(chunk.department) : true
}def ingest(docs: list[ParsedDoc], tenants: list[Tenant]) -> list[Chunk]:
"""Landing one: project at ingestion. A document is chunked for a tenant only if subscribed"""
chunks: list[Chunk] = []
for tenant in tenants:
for doc in docs:
if doc.department not in tenant.subscribes:
continue
chunks.extend(chunk_doc(doc, tenant.id))
return chunks
def in_scope(chunk: Chunk, scope: Scope) -> bool:
"""Landing two: the scope test is written once and shared by the in-memory implementation,
the SQL implementation, and evaluation — three separate tests cause permission incidents"""
if chunk.tenant_id != scope.tenant_id:
return False
return chunk.department in scope.departments if scope.departments else TrueThree: the evaluation dashboard turns a scale into an instrument panel
Landing three is reporting per tenant, and it deserves its own section because the lab's most striking phenomenon appears here.
Day eight built a scale: run it, look once, done. A dashboard does three more things, or nobody runs it again in three weeks — split by tenant, classify failures, and compare against an archived baseline in a way that can fail the pipeline.
Take splitting by tenant first. The same twenty-question golden set is not the same set of questions across two workspaces: the trial customer has only product documentation, so how many batches a canary rolls out in has no answer there and refusing is the correct behavior, the same class as the four with no answer in the corpus at all. Reprojected on that criterion, the lab's dashboard looks like this:
| Workspace | Indexed chunks | Answerable | Should refuse | Recall | multi | nDCG@10 | Refusal rate |
|---|---|---|---|---|---|---|---|
| Headquarters | 134 | 16 | 4 | 93.8% | 75.0% | 0.6445 | 0.0% |
| Trial | 44 | 5 | 15 | 100.0% | no such tier | 0.7649 | 20.0% |
| Global average | — | 21 | 19 | 95.2% | 75.0% | 0.6732 | 15.8% |
The third row is this section's whole point. Not one of its numbers is wrong, and every one of them lies: 95.2% recall looks higher than headquarters and a 15.8% refusal rate looks like a system that stays quiet — when headquarters' refusal rate is 0.0% and the trial workspace answered twelve of the fifteen it should have refused. A global average smooths one tenant's collapse across a headcount, and the tenant who will actually complain in production is exactly that one.
Two implementation disciplines while we are here. One, an empty collection's average is 0, and "0.0%" and "there are no questions in this tier" are two different things — the trial workspace has no multi-hop question at all, and printing 0.0% suggests multi-hop collapsed entirely, so the dashboard must distinguish them with another symbol. Two, fix the report's timestamp and diff one line per run, since nobody will want to read that diff two weeks later.
As for headquarters' 0.0% refusal rate, it is day eight's baseline's largest debt and not one cent of it was repaid in fourteen days. Putting it in the dashboard's main table today is meant to make it impossible to ignore any longer — which is itself the dashboard's reason for existing.
Four: a RAG decision map
Now the day's real product. The map has four inputs: data scale, update frequency, latency budget, and accuracy requirement. The output is ten knobs, each shaped as "because a given day measured something, therefore this."
| Knob | Decided by | Criterion (with the day it came from) |
|---|---|---|
| Chunking | Document shape | Heading levels mean cutting by structure, free and level with semantic chunking; only unstructured long text is worth that full embedding pass (D4) |
| Vector index | Data scale | Under fifty thousand chunks a sequential scan beats an approximate index on speed and accuracy; the criterion is not row count but whether the index fits in memory (D5) |
| Quantization | Data scale | Half precision's recall loss falls inside rerun noise with a 40% smaller index, essentially free; binary quantization must pair with original-vector reranking (D5) |
| Retrieval paths | Latency budget | The vector path costs one embedding round trip; what it buys is fetching things when the wording changes (D9) |
| Reranking | Latency budget | Buys ranking quality, not recall, and cannot rescue refusal — admission is gated on raw scores (D9) |
| Query rewriting | Always | Barely moves single-turn metrics and is the line between working and not in multi-turn (D10) |
| Agentic | Accuracy requirement | The gain concentrates in multi-hop with the cost spread across every question; route first, and never make it the default (D12) |
| Incremental sync | Update frequency | A full rebuild rebuys the knowledge base daily (D13) |
| Refusal | Accuracy requirement | A prompt cannot cover it; only code verification keeps fabrications out of the response (D6) |
| Evaluation cadence | Accuracy requirement | Retrieval metrics are free and run on every commit; a model judge costs money and belongs pre-merge and nightly (D8) |
Written as code it is one pure function. Note that it returns not only values but each value's basis — advice without a basis is no different from the best-practice lists online.
export function decide(req) {
const chunks = Math.round(req.docs * CHUNKS_PER_DOC) // about 4.5 chunks per document, measured on this corpus
const decisions = []
const put = (knob, value, driver, because) => {
decisions.push({ knob, value, driver, because })
return value
}
// The criterion is not how many rows I have but whether the index fits in memory
const index = put(
'index',
chunks < 50_000 ? 'seqscan' : chunks < 5_000_000 ? 'hnsw' : 'dedicated',
'docs',
'D5: under fifty thousand chunks a sequential scan beats HNSW on speed and accuracy',
)
// Reranking is one synchronous round trip between retrieval and generation, with the user waiting
const rerank = put(
'rerank',
req.accuracy !== 'best-effort' && req.latencyBudgetMs >= 800,
'latencyBudgetMs',
'D9: modeled at 180 ms, buying nDCG (0.6438 to 0.7218) and not recall',
)
return { config: { index, rerank }, decisions }
}def decide(req: Requirement) -> tuple[Config, list[Decision]]:
chunks = round(req.docs * CHUNKS_PER_DOC) # about 4.5 chunks per document, measured on this corpus
decisions: list[Decision] = []
def put(knob: str, value, driver: str, because: str):
decisions.append(Decision(knob, value, driver, because))
return value
# The criterion is not how many rows I have but whether the index fits in memory
index = put(
"index",
"seqscan" if chunks < 50_000 else "hnsw" if chunks < 5_000_000 else "dedicated",
"docs",
"D5: under fifty thousand chunks a sequential scan beats HNSW",
)
# Reranking is one synchronous round trip between retrieval and generation
rerank = put(
"rerank",
req.accuracy != "best-effort" and req.latency_budget_ms >= 800,
"latencyBudgetMs",
"D9: modeled at 180 ms, buying nDCG (0.6438 to 0.7218) and not recall",
)
return Config(index=index, rerank=rerank), decisionsBut the map's more valuable use is the second: checking a configuration already running. The lab checks this project's ten knobs against the map and reports two disagreements — query rewriting is off, and evaluation runs only at merge. The assignment is not making them agree but explaining both away: this project runs single-turn question answering only, where day ten measured rewriting to gain nothing; and a model judge costs money that a twenty-question teaching project does not yet warrant nightly. What can be explained stays, and what cannot is what genuinely needs changing.
That exercise is a rehearsal for the interview's "which decision would you change now." Failing to answer it usually means not that the project is poor but that nobody ever made you list configurations and reasons side by side.
Five: a quick reference for common failure modes
When a user says the answers are inaccurate, those words hold four entirely different ailments. The quick reference's value is separating them, because the fixes do not transfer:
| Symptom | Check which step first | Criterion |
|---|---|---|
| Answers the wrong question | Retrieval side | Is the answer document in the candidate pool? If not, it is a retrieval debt and relaxing the gate does nothing |
| Answers only a fragment | Gate and budget | In the pool but not past the gate is a gate debt; past the gate and not into the budget is chunks too large or budget too small |
| Misaligned citation | Generation side | Citation verification's pass rate. Numbers are a closed set and a mismatch is one line of code |
| Updates not taking effect | Sync and cache | First whether reconciliation recognized the change, then whether the cache key holds the index version (D13) |
The first three are classified automatically in the dashboard, and one detail of that classification is easy to get wrong: a multi-hop question is diagnosed on the documents that are missing, not on whether any one was fetched. In the lab q06 must hit two documents, and the first ranks first steadily while the second never enters the pool; judging by any-one classifies it as a budget problem, and you can tune the budget all day for nothing.
// Diagnose only the missing ones. The order is the investigation order: start upstream, since upstream errors amplify
function classify(hit, rankedDocIds, contextDocIds, answerDocIds, candidates, result) {
if (!hit) {
const missing = answerDocIds.filter((id) => !contextDocIds.includes(id))
if (missing.some((id) => !rankedDocIds.includes(id))) return 'not-retrieved'
const admitted = candidates.filter((c) => c.admitted).map((c) => c.chunk.docId)
if (missing.some((id) => !admitted.includes(id))) return 'gated-out'
return 'budget-squeezed'
}
return result.rejected.length > 0 ? 'citation-misaligned' : undefined
}def classify(hit, ranked_doc_ids, context_doc_ids, answer_doc_ids, candidates, result):
"""Diagnose only the missing ones. The order is the investigation order: start upstream"""
if not hit:
missing = [d for d in answer_doc_ids if d not in context_doc_ids]
if any(d not in ranked_doc_ids for d in missing):
return "not-retrieved"
admitted = [c.chunk.doc_id for c in candidates if c.admitted]
if any(d not in admitted for d in missing):
return "gated-out"
return "budget-squeezed"
return "citation-misaligned" if result.rejected else NoneThe fourth lies outside question answering — the sync did not run, reconciliation missed the change, or the cache key lacks the index version, all three presenting as "I clearly edited the document." Day thirteen's discipline is the quick reference here: when must this invalidate is equivalent to whether that thing is in the key.
Six: what fourteen days accumulated, and what to learn next
To close. What this course genuinely gives you is not fourteen techniques but the following — every one written down only after being contradicted by its own experiment:
- Metrics fight each other, so say what you are optimizing. At 5 per path, recall is 93.8% and nDCG 0.6281; at 50, 87.5% and 0.7186, pointing opposite ways (D9).
- Metrics saturate, and a column of full marks means the ruler is broken. Mean reciprocal rank is a constant 1.0000 because the questions were written backwards from the corpus (D8, D10).
- An end-to-end metric blends two opposite effects.
q07is a keyword hit that hybrid search broke and reranking restored — not "hybrid search raised the metric" but "reranking fixed the question fusion broke" (D9). - Two failures need two medicines. Not in the candidate pool needs multi-hop; fetched and thrown away by your own filter needs relaxation. Misclassify and the remedy is inverted (D12).
- Not every technique should default to on. All-on and the default configuration have identical recall, at 2.5 times the model calls and 4.3 times the retrievals (D10).
- Conclusions have boundaries. "Headers harm BM25" did not reproduce under structure-based chunking — the accurate statement is that headers harm it only when chunk boundaries do not align with structure (D4, D11).
- Failure cases first, index structures second. Not one of five advanced index structures beat the baseline (D11).
- What cannot be measured offline must be admitted as unmeasurable. The hypothetical document embeddings effect bill was written as "we are not entitled to fill this in" rather than as a number with a disclaimer (D10).
One more was bought with blood. Day eleven's first draft said the vector side is a genuine gain, which looked measurement-backed; a later count found that only 5.8% of the fused top twenty were unique to the vector path, and the number of questions where those contained the answer document was 0 of 20 — the vector path brought in not one new answer document and only reshuffled ranks. That rise was a literal-overlap retriever's rise, not a semantic retriever's. So it overturned its own conclusion.
The lesson: measure MOCK's boundary rather than guessing it. The criterion is one sentence —
Is this conclusion something this experiment measured, or something this structure necessarily implies?
The former must be labeled with its boundary (a real model may invert it) and the latter can be stated freely. "Reranking changes order and not admission, so it cannot rescue refusal" is the latter, and a real cross-encoder does not change that conclusion. Sort your own experiment reports the same way, and that one habit is worth more than any percentage.
Where next? The two nearest roads are both on this site: stuffing retrieval results into a finite context and deciding what to keep and delete is day three of the context engineering course; wrapping this retrieval as a standard tool other agents can call is day two of the MCP course. Beyond that, two areas are worth researching yourself: evaluation and observability (today's dashboard is the minimal version, and production also needs sampling, percentiles, and alerting) and agent security (today's tenant predicate blocks only retrieval, while prompt injection and tool privilege escalation are another matter).
Source Reading
Hands-On Lab
Confirm one thing before starting: change no retrieval parameter today. The corpus, the twenty questions, the six-hundred-token budget, fifty per path, twenty kept after fusion, and both gates all carry over from earlier days — change any one and today's numbers cannot sit beside days eight through twelve, and this project's persuasiveness rests exactly on being able to sit beside them. The starter has five exercise points, all on this day's three new landings and two ledgers.
- Complete labeling at ingestion and watch the two workspaces' indexed chunk counts split into 134 and 44, with tenant-prefixed chunk ids.
- Move the scope predicate before scoring, and watch the vector path under the support scope go from returning single digits to honestly returning 23.
- Complete reciprocal rank fusion, and watch candidates unique to the vector path appear in the fused results rather than a verbatim keyword ranking.
- Change the dashboard's grouping key to tenant, watch the main table go from one row to three, and explain to yourself how the global average row lies.
- Complete the cost conversion, run the decision map's check, and explain both disagreements away — the one you cannot explain is what genuinely needs changing.
Interview Questions
Today's 5 questions are in the question bank below, the only day with five, weighted toward end-to-end scheme design, trade-offs under constraints, and stringing fourteen days of material together. Expand each one and read the analysis before the key points — practicing the derivation beats memorizing bullets. The high-frequency labels for the domestic and overseas markets are there so you can pick by target market.
Checklist and Tomorrow
- Assemble the previous thirteen days' modules into one complete multi-tenant project with citations and an evaluation dashboard, and explain its architecture
- Draw a RAG decision map that, given a new requirement, points to which configuration to use and why
- Handle interview follow-up questions, explaining every technical choice using this course's data and trade-offs
- Name multi-tenancy's three landings, and explain why filtering at retrieval alone is not enough
- Sort every number in your own report with the question of whether it was measured or is structurally necessary
- All 5 acceptance criteria of the lab pass, with a dashboard ready to paste into the project description
- Answer at least 4 of the 5 interview questions without looking at the key points
Fourteen days end here. Look back at day 1's minimal BM25-only system — it still lives in the project today as the keyword path and as the baseline of every controlled experiment. What these fourteen days genuinely added is not vectors, reranking, or loops but a habit of demanding evidence for every decision. Put today's project in your repository, paste the dashboard into the README, and print the decision map for the edge of your monitor — the next time somebody asks whether a scheme will work, your first reaction will no longer be "it should" but "let me go run it."
Interview questions
You are handed a knowledge base of five million documents that must answer in about a second, with accuracy as the top priority. How would you design it?给你一个五百万文档、要求秒级响应、准确率优先的知识库场景,你会怎么设计这套系统?
Common in ChinaCommon overseasDeep dive#system-design#scaling#latency-budgetHow to reason about it · think before answering
- The real subject here is not which technologies you know, it is whether you have a repeatable way to derive a configuration from constraints. Opening with an architecture diagram reads as a memorized answer; the way to score is to turn each constraint into a number first, then let every choice be forced by one of those numbers.
- Quantify the three constraints. Five million documents at roughly four or five chunks each is over twenty million chunks; at 1536 float dimensions that is hundreds of gigabytes, so the index does not fit in one machine's memory — that alone settles storage. A one-second budget to first token, with generation typically eating seven or eight hundred milliseconds, leaves only two or three hundred for retrieval. Accuracy first means you may trade latency and money for metrics, but only within that remaining budget.
- Now derive each knob from one of those numbers: a dedicated vector store or partitioning, plus half precision (its recall loss usually sits inside run-to-run noise while the index shrinks by about forty percent — essentially free); keep both keyword and vector routes with reciprocal rank fusion, because exact matches on document ids, error codes and names are a permanent blind spot for embeddings; rerank only the top twenty after fusion, since it buys ranking quality at the cost of one synchronous round trip, and a one-second budget affords exactly one.
- Then state two things you deliberately do not build, which is the part that reads as field experience. Agentic retrieval is not the default path: its gains concentrate on multi-hop questions while its cost is spread over every question, and it blows a one-second budget outright — the right move is a cheap classifier that routes only the multi-hop minority into the loop. Contextual chunk headers and similar tricks also wait, because they dilute the keyword route while helping the vector route; the directions are opposite, so measure on your own embeddings before committing.
- Accuracy first has to become something you can sign off on. That means a golden set of at least a hundred questions with multi-hop and unanswerable each above ten percent, recall and ranking quality read separately, abstention rate on unanswerable questions as its own column, and citations verified by code rather than trusted from the model. Reporting the ugliest column alongside the headline number is far more credible than reporting a single score.
- Expected follow-up: how do you build the first index over five million documents? It is a one-off large expense, so batch it, make it resumable, and put content-hash incremental sync in from day one, or every config change means buying the whole corpus again. Push further and you get to rollout: dual-write the new embeddings into a second column, evaluate both columns on the same golden set, then shift traffic, so rollback is a config flip rather than an eight-hour rebuild.
分析过程 · 先想清楚再作答
- 这题的题眼不在「你会用什么技术」,而在「你有没有一套从约束推配置的方法」。开口就报架构图和技术栈的答案会被判成背方案;拿到分的答法是先把约束翻译成数字,再让每个选择被某个数字逼出来。
- 先把三个约束量化:五百万文档按一篇四五块估,是两千多万块,单精度 1536 维就是上百 GB,**索引塞不进单机内存**,这一条直接决定了存储选型;秒级响应意味着从收到问题到第一个字的预算大约一秒,而生成本身通常就吃掉七八百毫秒,检索侧只剩两三百毫秒;准确率优先意味着可以拿延迟和钱换指标,但只能换到那两三百毫秒为止。
- 然后逐项落地,每一项都挂在上面某个数字上:存储上专用向量库或分区加半精度量化(半精度的召回损失通常落在重跑噪声里,索引却小四成,这是白捡的);检索保留关键词与向量两路加倒数排名融合,因为精确匹配的文档号、错误码、人名是向量的固定盲区;重排只作用于融合后的前二十条——它买的是排序质量,一次同步往返,秒级预算里放得下一次,放不下两次。
- 接着讲两个「不上」的决定,这一段比上面更能显出做过工程:**Agentic 检索不作为默认路径**,它的收益集中在多跳题上而代价摊给全部问题,秒级预算下更是直接超支——正确做法是先用一次便宜的分类把多跳分流出来,只让那一小部分进循环;**上下文块头之类的手法先不上**,因为它对关键词一路是稀释、对向量一路才是补位,方向相反,得在自己的真实 embedding 上测过再说。
- 准确率优先必须落成可验收的东西,否则是空话:一份不少于一百题的标准答案集(其中多跳与无答案各占一成以上)、召回率与排序质量分开看、无答案题的拒答率单独一栏、引用由代码回查而不是靠提示词自觉。**报数字时把最难看的那一栏也报出来**,比只报总分可信得多。
- 可预期的追问:五百万文档怎么建第一版索引?答案是这笔钱是一次性大额支出,要按批做、可断点续跑,并且从第一天就上基于内容指纹的增量同步——否则每次改配置都等于把整个知识库重买一遍。再追问就谈灰度:新旧两套向量双写在两列上,用同一份标准答案集在两列上各跑一遍再切流量,回滚只是改一个配置项。
Key points
- Translate constraints into numbers first: twenty million chunks means the index will not fit one machine, and a one-second budget leaves retrieval two to three hundred milliseconds.
- Dedicated store or partitions plus half precision; keep keyword and vector routes with RRF, and rerank only the top twenty after fusion.
- Name the two things you will not ship: agentic only for a routed multi-hop minority, and chunk headers only after measuring on your own embeddings.
- Turn accuracy-first into a hundred-plus question golden set, abstention rate as its own column, and code-verified citations.
- First index build is a one-off large expense: batch it, make it resumable, add incremental sync on day one, and dual-write columns for model swaps.
答题要点
- 先把约束翻译成数字:两千多万块决定索引塞不进单机内存,一秒预算里检索侧只剩两三百毫秒。
- 存储用专用库或分区加半精度;检索保留关键词与向量两路加倒数排名融合,重排只作用于前二十条。
- 明确说出「不上」的两项:Agentic 只对分流出来的多跳开,块头这类方向相反的手法先测再说。
- 准确率优先要落成一百题以上的标准答案集、拒答率单独一栏、引用由代码回查。
- 第一版建索引是一次性大额支出:分批可续跑,并从第一天就上增量同步;换模型走双写切列。
Looking back at the RAG project you built, which decision would you change now, and why?你做过的这个 RAG 项目里,哪个决定你现在会改?为什么?
Common in ChinaCommon overseasDeep dive#retrospective#evidence#chunkingHow to reason about it · think before answering
- This looks like a soft question but it separates people sharply. Saying 'nothing yet' admits you never ran a retrospective; a long list of self-criticism reads as poor judgment. What the interviewer is listening for is whether you can chain four things together: the decision, the evidence you had then, the evidence you got later, and your current call.
- How to pick: choose a decision that was justified at the time and later overturned by data, not one you always knew was a shortcut. The first proves you measure; the second only proves you were behind schedule. So the answer has a fixed four-part shape — what you chose, on what basis, what you measured later, and what you now believe.
- This course supplies a ready example. One day measured that prepending a heading-path header to every chunk left hit rate unchanged, grew index tokens by about ten percent, and pushed the answer document's mean rank from 2.88 to 3.25 — hence 'headers hurt keyword retrieval'. A later day re-ran the same comparison under structure-aware chunking and the rank regression did not reproduce. The reason was the chunker: with fixed-length cuts, chunk boundaries do not line up with section boundaries, so the header injects heading terms that do not belong to that chunk; with structure-aware cuts, each chunk already sits inside one section and the header largely restates what is already there. The correct statement is therefore not 'headers hurt' but 'headers hurt when chunk boundaries are misaligned with document structure'.
- The value of the chain is that it demonstrates a reusable habit: attach the premises to every conclusion. Change a premise and you owe a re-run; you may not pair new settings with an old conclusion. The same reasoning yields a second example: an earlier claim that 'the vector route is clearly a net gain' collapsed once the two routes were counted separately before fusion — the vector-only candidates were a small share and contained the answer document zero times, so the improvement was never semantic at all.
- Expected follow-up: how will you avoid this class of error in future? Give two concrete practices. Write the bound premises next to every number — corpus, question set, budget, chunker. And before publishing any conclusion, ask whether it was measured by this experiment or forced by the structure of the implementation; the first needs its boundaries stated, only the second can be asserted flatly.
分析过程 · 先想清楚再作答
- 这题看着是软性问题,其实区分度极高。答「暂时没有」等于承认没做过复盘;答成一长串自我批评又会显得没有判断力。面试官真正在听的是:你能不能把一个决定、它当时的依据、后来的证据、以及新的判断,四样东西串成一条链子说清楚。
- 怎么拆:挑一个**当时有理由、后来被数据推翻**的决定,而不是一个「当时就知道是凑合」的决定。前者证明你有量化的习惯,后者只证明你赶过工期。所以答案的骨架固定是四段——当时选了什么、依据是什么、后来量到了什么、现在的判断是什么。
- 本课里有一个现成的样本:某一天先量到「给每个块拼上标题块头之后,命中率不变、索引 token 涨一成、答案文档平均名次从 2.88 退到 3.25」,据此写下「块头对关键词检索是负收益」。后来换成按文档结构切块再复核,这条名次退化**没有复现**。原因是切法变了:固定长度硬切时块边界跟小节边界不对齐,块头会把不属于这一块的标题词塞进来;按结构切时块本身就落在一个小节里,块头补的信息跟块里已有的高度重合。所以正确的表述不是「块头有害」,而是「**块头在块边界与结构不对齐时才有害**」。
- 这条链子的价值在于它演示了一个可复用的动作:**给每个结论标出它绑定的前提**。前提变了就要重跑,不能拿新配置去配旧结论。顺着这个思路还能给出第二个例子:曾经写过「向量侧确实是正收益」,后来把融合前的两路拆开数了一遍才发现,向量路独有的候选只占很小一部分,其中含答案文档的次数是零——那个「涨」根本不是语义检索带来的,于是这条结论被自己推翻。
- 可预期的追问:那你以后怎么避免这类错误?答两条具体的:一是每个数字旁边写清它绑定了哪几个前提(语料、题集、预算、切法),二是报结论前先问自己一句「这是这次实验测出来的,还是这个结构必然导致的」——前者要标边界,后者才能直接讲。
Key points
- Structure the answer in four beats: the choice, the evidence then, the evidence later, the call now.
- Pick a decision that was defensible at the time and later overturned by data, not one you knew was a shortcut.
- Worked example: 'headers hurt keyword retrieval' was corrected to 'headers hurt when chunk boundaries misalign with structure', because the chunker premise changed.
- Record the premises bound to every number; when a premise changes you owe a re-run rather than a reinterpretation.
- Classify before asserting: measured by this experiment, or forced by the implementation's structure — the former needs its boundaries stated.
答题要点
- 答案要串成四段:当时选了什么、依据是什么、后来量到了什么、现在的判断是什么。
- 挑一个当时有理由、后来被数据推翻的决定,而不是一个当时就知道在凑合的决定。
- 样本:块头从「对关键词检索有害」修正成「块边界与结构不对齐时才有害」,因为切法这个前提变了。
- 每个数字旁边写清它绑定的前提;前提变了就必须重跑,不能新配置配旧结论。
- 报结论前先分类:这是实验测出来的,还是实现结构必然导致的——前者要标边界。
How do you convince a non-technical stakeholder that your retrieval system actually got better?怎么向不懂技术的业务方证明你的检索系统真的变好了?
Common in ChinaCommon overseasBasic#evaluation#stakeholder-communication#abstentionHow to reason about it · think before answering
- This is a communication question whose scoring hinges on technical judgment: which numbers you choose to show reveals whether you understand the metrics yourself. Dumping recall, nDCG and MRR on a business stakeholder reads as tone-deaf; saying 'user feedback improved' reads as unmeasured.
- Start from a principle: show them something they can adjudicate themselves. They cannot judge normalized discounted cumulative gain, but they can absolutely judge 'out of these hundred real questions, how many did it answer correctly, how many wrongly, and how many did it honestly decline'. So the external framing is three numbers — correct, wrong, declined — and they sum to one hundred.
- The crucial move is separating wrong from declined, and it is the fastest way to earn trust: saying 'not found' is a correct output, not a failure; the failure is inventing an answer when nothing was found. Teams that report a single 'accuracy' number can be gamed by a system that learns to decline everything, which is why all three must appear side by side.
- Then supply checkable evidence rather than only numbers: take ten real questions and show before-and-after answers with clickable citations on every claim. A stakeholder who opens the source and verifies one claim is more convinced than by any percentage, and the exercise doubles as the human spot-check you need anyway to calibrate whether your model judge is trustworthy.
- There is a lesson from this course worth volunteering: a column of perfect scores means the ruler is broken. Our questions were written backwards from the corpus, lexical overlap is unusually high, and mean reciprocal rank sits at exactly 1.0000. Showing that to a stakeholder only invites the misreading that you are already perfect, when in fact the metric has saturated. When a metric hits the ceiling, the response is to make the questions harder.
- Expected follow-up: how do you get the business side involved? One very practical answer: let them supply questions. Every production miss gets appended to the golden set, so the evaluation set grows rather than being built once. Then each release can point at 'the question you raised last month now answers correctly', which lands better than any status report.
分析过程 · 先想清楚再作答
- 这题在考沟通,但拿分点在技术判断上:你选哪几个数字给业务方看,暴露了你自己有没有看懂这些指标。把召回率、nDCG、MRR 一股脑摊出去的答法会被判成不懂受众;只说「用户反馈变好了」又会被判成没有度量。
- 先立一条原则:**给业务方看的必须是他们能自己判断对错的东西**。归一化折损累计增益他们没法判断,而「这一百个真实问题里,系统答对了多少、答错了多少、老老实实说查不到了多少」他们一眼就能判断。所以对外的口径应该是三个数:答对率、答错率、拒答率,而且三个加起来是一百。
- 关键是把**答错和拒答分开**。这一条最能建立信任:查不到就说查不到不是故障,是正确输出;真正的故障是查不到还编一段。很多团队只报「准确率」,结果一个学会了一直拒答的系统能刷出满分——所以这三个数必须并排出现,缺一个都能被骗。
- 然后给可核对的证据,而不是只给数字:**挑十条真实问题做前后对照**,各贴出改动前和改动后的回答,每句结论后面挂着可点开的引用。业务方点开原文核对一遍,比看任何百分比都有说服力,而且这个动作顺带完成了一次人工抽检——你自己也需要它来校准模型裁判靠不靠谱。
- 本课里有一条要主动说的教训:**一列全是满分说明尺子坏了**。我们的题目是从语料反向出的,字面重合度过高,平均倒数排名恒为 1.0000。这个数字拿给业务方看,只会换来一次「那你们已经完美了」的误会,而它其实是指标饱和。指标撞天花板时该做的是把题目出难一点。
- 可预期的追问:那怎么让业务方参与进来?答一条很实用的:让他们提供题目。把线上答错的问题一条条补进标准答案集,评估集是长出来的,而不是一次性造好的;这样每一次改进都能指着「你上次提的那个问题现在答对了」,比任何汇报都直接。
Key points
- Externally report three numbers they can adjudicate: correct, wrong, declined — summing to one hundred.
- Keep wrong and declined separate; a single accuracy number is gamed by a system that learns to decline everything.
- Pair it with ten before-and-after real questions, every claim carrying a citation they can open and verify.
- Volunteer the saturation caveat: a column of perfect scores means a broken ruler, and the fix is harder questions.
- Let stakeholders contribute questions; append every production miss to the golden set so it grows over time.
答题要点
- 对外只用三个他们能自己判断的数:答对率、答错率、拒答率,三者相加为一百。
- 答错和拒答必须分开——查不到就说查不到是正确输出,只报一个准确率会被「一直拒答」刷满分。
- 配十条真实问题的前后对照,每句结论挂可点开的引用,让他们自己核对原文。
- 主动说明指标饱和:某一列恒为满分是尺子坏了,不是系统完美,该做的是把题目出难一点。
- 让业务方提供题目,把线上答错的问题补进标准答案集——评估集是长出来的。
Users report that your live RAG system 'answers inaccurately'. What is your triage order?RAG 系统上线后用户反馈「答得不准」,你的排查顺序是什么?
Common in ChinaCommon overseasIntermediate#debugging#failure-modes#observabilityHow to reason about it · think before answering
- This one is almost guaranteed to be asked, and most people answer with a flat list of possibilities: maybe chunking, maybe the prompt, maybe the model. A list is not triage. Triage means an order, a decision rule at each step, and each step eliminating half the search space.
- First decompose the complaint. 'Inaccurate' hides at least four distinct failures whose fixes do not transfer: off-topic answers, partial answers, misaligned citations, and stale content. So the first action is not to change a setting, it is to obtain the specific question and answer and classify it into one of those four.
- Then give the order along with its justification: read the pipeline right to left, fix it left to right. Right to left because the generated answer is what you see first; left to right because upstream errors are amplified downstream — no prompt can recover a document retrieval never fetched. Concretely: dump the candidate pool and the final context for that question, and check whether the answer document is in the pool at all. Absent means a retrieval debt; present but below the admission gate means a gate debt; admitted but never packed into the context budget means chunks too large or budget too small; all present and still unused means it is finally a generation problem.
- One detail worth volunteering because it is easy to get wrong: for multi-hop questions, diagnose the documents that are missing, not whether any one of them was retrieved. In our experiment one question needed two documents; the first ranked first every time and the second never entered the candidate pool at all. Judging by 'any of them' labels it a budget problem, and you can spend a full day tuning budgets to no effect. This distinction only occurs to someone who has actually triaged question by question.
- The fourth class, stale content, happens outside the question path and has its own rule: first check whether reconciliation even noticed the edit (was the content hash computed after line-ending normalization?), then check whether the cache key includes the index version and the permission scope. 'When must this expire' is equivalent to 'is that thing part of the key' — leave something out of the key and changes to it will never invalidate the entry.
- Expected follow-up: how do you stop relying on manual triage? Build the classification into the evaluation panel so every missed question is automatically labeled with one of the four classes, and report it per tenant. A global average dilutes one customer's collapse across the whole population, and that customer is exactly the one who will file the complaint.
分析过程 · 先想清楚再作答
- 这题几乎是必考题,而绝大多数人答成一堆并列的可能性:可能是切块问题、可能是提示词问题、可能是模型不行。并列不是排查,排查的意思是**有顺序、有判据、每一步能把可能性砍掉一半**。
- 先把「答得不准」这四个字拆开——它至少塞了四种病,而且修法互不通用:答非所问、只答得出片段、引用错位、更新不生效。所以第一个动作不是改配置,是**拿到具体的问题和回答,把它归到这四类里的一类**。
- 然后给顺序,而且要说清顺序的理由:**排查从右往左看、修复从左往右修**。从右往左是因为你最先看到的是生成结果;从左往右是因为上游的错会被下游放大——检索没捞到的东西,再好的提示词也救不回来。具体走法是:打印这一问的候选池和最终上下文,先看答案文档在不在候选池里。不在,是检索的债;在候选池但没过准入门槛,是门槛的债;过了门槛却没装进上下文预算,是块太大或预算太小;都进了而模型没用上,才轮到生成侧。
- 这里有一个容易写错的细节值得主动讲:**多跳题的诊断对象是缺的那几篇,不是「有没有捞到任意一篇」**。我们实验里有一道题要同时命中两篇,第一篇稳稳排第一、第二篇一次都没进候选池;用「任意一篇」去判会把它归成预算问题,然后你去调预算,调一整天也没用。这一条区分度很高,因为它只有真的按题排查过才想得到。
- 第四类「更新不生效」发生在问答之外,判据是另一条:先看对账认没认出这篇改了(内容指纹算之前有没有做换行归一化),再看缓存的 key 里有没有把索引版本和权限范围算进去。「什么时候必须失效」等价于「key 里有没有把那样东西算进去」,key 少放一样,那样东西变了缓存就不会失效。
- 可预期的追问:怎么让这套排查不靠人肉?答案是把分类做进评估面板——每一道没中的题自动标出它属于四类中的哪一类,并按租户分开统计。全局平均会把单个客户的塌方按人头摊薄,而线上会投诉的恰恰是那个客户。
Key points
- Classify the complaint into four failures first — off-topic, partial, misaligned citation, stale — because their fixes do not transfer.
- Read right to left, fix left to right: dump the candidate pool and final context and find which layer the answer document stalls at.
- The four rules in order: never retrieved, retrieved but below the gate, admitted but squeezed out of the budget, packed but unused by the model.
- For multi-hop, diagnose only the missing documents; judging by 'any one retrieved' mislabels a never-retrieved case as a budget problem.
- For stale content, check reconciliation and the cache key: what must expire is exactly what the key must contain.
答题要点
- 先把「答得不准」归类成四种病:答非所问、只答得出片段、引用错位、更新不生效——修法互不通用。
- 排查从右往左看、修复从左往右修:先打印候选池与最终上下文,看答案文档卡在哪一层。
- 四层判据依次是:没进候选池、进了没过门槛、过了没装进预算、都进了模型没用上。
- 多跳题只诊断缺的那几篇;用「有没有捞到任意一篇」会把「根本没捞到」误判成预算问题。
- 「更新不生效」查对账与缓存 key:什么时候必须失效,等价于 key 里有没有算进那样东西。
If you could only fund three changes to improve an existing RAG system, which three would you pick and why those three?如果预算只够做三件事来提升一个已有 RAG 系统的效果,你选哪三件?为什么是这三件?
Common in ChinaCommon overseasIntermediate#prioritization#evaluation#abstentionHow to reason about it · think before answering
- This tests prioritization, not breadth. Answering with a list of techniques — add reranking, add hybrid retrieval, add query rewriting — almost always loses points, because it skips a prerequisite: how do you know those three help your system? That is precisely the sentence the interviewer is waiting for.
- So the first item has to be building evaluation, with a reason specific enough to be unarguable: without a scale, you cannot tell whether the other two helped or hurt; with one, every subsequent spend has a measurable return. It is also cheap — the three retrieval metrics are pure local computation, run in seconds, cost nothing, and can gate every commit; the only real effort is labeling answer documents once. Include the composition rule: multi-hop and unanswerable each above ten percent, because without the unanswerable class a system that only ever guesses scores perfectly on your report.
- Second, move abstention out of the prompt and into code — usually the best return per unit of effort, and the item most often skipped. Writing 'say you don't know' ten times in a prompt buys almost nothing. Citation numbers are a closed set, so checking existence is one line, and adding a substantive-overlap check catches the harder forgery where the number is real but the content is not. Our baseline abstention rate was 0.0 percent: four questions with no answer in the corpus, zero of them declined — a defect that is completely invisible on a report that only shows recall.
- Third, look at the failure cases before deciding, which is the actual answer to this question. After reading the panel you land on one of a few branches: a high share of multi-hop means bridging retrieval or a different index structure; queries that miss when phrased differently mean you need the vector route or hybrid retrieval; answers retrieved but never packed into context means reranking or budget. Failure cases first, technique second — we tried five advanced index structures and not one beat the baseline, because our system simply did not have the weakness they address.
- Why not the flashier options: agentic retrieval concentrates its gains on multi-hop while spreading cost across every question, and in our measurements turning on every query-side technique produced exactly the same recall as the default configuration while using 2.5 times the model calls and 4.3 times the retrievals. Stacking techniques is easy; explaining why you switched several off is the skill.
- Expected follow-up: once the three are done, how do you prove the money was well spent? Toggle each one individually and report three ledgers — how much the metric moved, how much latency moved, how much cost moved. A proposal that reports only the first should not be approved, including your own.
分析过程 · 先想清楚再作答
- 这题在考优先级判断,而不是知识面。答成「上重排、上混合检索、上查询改写」这类手法清单几乎必然掉分——因为它跳过了一个前提:**你凭什么知道这三件对你的系统有用?** 面试官等的就是这句话。
- 所以第一件必须是**建评估**,而且理由要具体到不可反驳:没有秤,剩下两件做完你也说不清是变好还是变坏;有了秤,后面每一笔钱都能算回报。而且它便宜——检索侧三个指标是纯本地计算、几秒钟、零成本,能挂进每次提交;花时间的只是给题目标答案文档那一次。顺带说清评估集的配比:多跳与无答案各占一成以上,缺了无答案那一类,一个只会硬答的系统在报表上就是满分。
- 第二件是**把拒答从提示词搬进代码**,这一件的性价比通常最高而最容易被跳过。提示词里写十遍「找不到就说找不到」增益接近于零;而引用编号是一个闭集,判它存不存在只要一行代码,再加一道「这句话与被引块的实质重合度」就能拦住「编号是真的、内容是假的」那一类。我们实验里的基线拒答率是 0.0%——四道语料里根本没有答案的题一道都没闭嘴,这类缺陷在只报召回率的报表上完全不可见。
- 第三件要**先看失败案例再决定**,这才是这道题真正的答案。看完面板你会落到其中之一:多跳题占比高就补桥接检索或改索引结构;换个说法就捞不到,说明该上向量那一路或混合检索;答案捞到了却排不进上下文,那是重排或者预算的活。**先有失败案例,再有手法**——我们试过五种高级索引结构,没有一种跑赢基线,因为我们的系统压根没有那些结构要补的短板。
- 为什么不选那些看起来更亮的:Agentic 检索的收益集中在多跳题上而代价摊给全部问题;「全开」所有查询侧手法在我们的实测里召回率和默认配置一模一样,模型调用却是 2.5 倍、检索次数 4.3 倍。**堆手法很容易,说清楚为什么关掉某几项才是本事。**
- 可预期的追问:三件做完怎么证明钱花对了?答:每一项单独开关各跑一遍,报三笔账——指标涨了多少、延迟涨了多少、钱涨了多少。只报第一笔的提案不该被批准,包括你自己的。
Key points
- First, build evaluation: without a scale the other two changes are unverifiable, and the retrieval metrics are cheap enough to gate every commit.
- The golden set must include unanswerable questions, or a system that only ever guesses scores perfectly on your report.
- Second, move abstention from the prompt into code: citation numbers are a closed set, and a substantive-overlap check catches real-number-fake-content forgeries.
- Third is chosen by the failure cases, not by a list of techniques — failure cases first, index structure or retrieval trick second.
- Toggle each change individually and report three ledgers: metric, latency, cost. A proposal reporting only the first should not be approved.
答题要点
- 第一件是建评估:没有秤,另外两件做完也说不清变好还是变坏;检索侧指标零成本可挂进每次提交。
- 评估集必须含无答案那一类,否则一个只会硬答的系统在报表上就是满分。
- 第二件是把拒答从提示词搬进代码:编号是闭集,再加实质重合度就能拦住「编号真、内容假」。
- 第三件由失败案例决定,不由手法清单决定——先有失败案例,再有索引结构或检索手法。
- 每一项单独开关跑一遍并报三笔账:指标、延迟、钱。只报第一笔的提案不该被批准。