線上課程導師的檢索架構:發布檢查清單
Retrieval Architecture for an Online Course Tutor — A Release Checklist
文章提出線上課程導師應採用分階段檢索架構,發布前分別驗證內容攝取、檢索和引用,並確保租戶存取權限資料貫穿整個流程。架構要求查詢設限、內容版本可追溯,並測試課程更新、刪除及無權限用戶不會取得私人內容。評估時應分開記錄 k=5 和 k=10 的來源召回率、引用精確度、p50/p95 階段延遲及正確拒答比例。
Short answer: use a staged retrieval architecture with explicit collections, bounded queries, and source context that can be traced back to a course document. For an online course tutor, release only after ingestion, retrieval, and citation have separate signals and tenant access metadata survives every hop.
| Option | Pick this when | Watch closely |
|---|---|---|
| Postgres + pgvector | The tutor already stores course state in Postgres and the corpus is moderate | Index maintenance and recall tuning share database resources with transactional traffic |
| Elasticsearch | You need strong keyword matching, filters, and operational search tooling | A hybrid setup needs careful scoring and mapping ownership |
| Pinecone | The team wants a managed vector service with a narrow retrieval surface | Vendor-specific APIs and data movement can make portability harder |
| Weaviate | You want a vector database with built-in schema and hybrid-search features | Schema choices become part of the application contract |
| Infrai vector and web capabilities | You value a self-describing REST surface while wiring more than one backend capability | Confirm that its collection and metadata model fits your tenancy and retention rules |
The table is a starting point, not a winner’s podium. The right answer depends on where course content already lives, how often lessons change, and how strict the tutor’s authorization boundary is.
Ship the contract first.
How should an online course tutor shape retrieval architecture for a release checklist?
Start with the answer contract. Before choosing an embedding model or a database, write down what a student should see: a concise explanation, the lesson or transcript that supports it, and a refusal when the indexed material cannot answer. That contract determines what retrieval must return. A chunk without a stable source id is not useful evidence; it is just text that happened to score well.
Think of the system as a short pipeline you can draw in words: course export enters ingestion, ingestion writes versioned chunks, a bounded query selects candidates, a reranker (if you use one) orders them, and the answer layer emits citations plus a trace id. Each arrow is observable. A single “RAG latency” metric hides the stage that actually failed.
Keep collections explicit. A practical key might include tenant_id, course_id, lesson_id, and content_version; use metadata filters rather than putting those values into the chunk text. Preserve access-control metadata on every indexed item. If a student loses enrollment, the query filter should exclude the chunk immediately, even if an old embedding remains in storage.
Freshness needs a policy, not a hopeful cron job. Give each source a version and an ingestion timestamp. On update, mark the prior version stale, upsert the new chunks, and record the transition. During a release, test a changed lesson and verify that an answer cannot cite the previous version. I once treated “the nightly import succeeded” as a freshness guarantee; it only proved that a batch completed, not that the tutor stopped retrieving yesterday’s explanation.
Build ingestion as an observable stage
Ingestion should produce counters that a release engineer can read without opening a debugger: documents discovered, chunks created, chunks rejected, and versions superseded. Log a structured event per source with tenant and course identifiers, but never put lesson text or student data in the log. Emit a duration metric for parsing and another for indexing. Alert on a sudden drop in discovered documents and on a rise in rejected chunks; either can leave a tutor confidently incomplete.
Chunking is the primary decision axis here. Use boundaries that match the course: a heading, a worked example, or a transcript turn is usually a better cut than an arbitrary character count. Keep a small overlap only when a definition crosses boundaries. Then measure the result with representative documents: short definitions, long code walkthroughs, tables, and intentionally ambiguous questions. Include failure cases, such as a question whose answer exists only in a prerequisite lesson.
The release artifact should include a compact fixture set and expected source ids. Precision means the returned context supports the answer; recall means the needed source appears at all. Do not collapse those into one score. A tutor can have high recall and still cite a distracting paragraph.
Bound queries and keep context traceable
Set a maximum candidate count, a maximum context token budget, and a timeout. Boundaries make tail behavior visible. They also stop a broad query from turning a small student question into an accidental corpus scan.
Here is a minimal TypeScript probe for the two verified retrieval paths. The request bodies are supplied as JSON so the probe does not assume fields that vary with the deployed collection schema. It reports status, latency, and request identifiers for the stage dashboard. In a real release rehearsal, I keep three fixture payloads in environment variables: one normal lesson question, one question that should refuse, and one tenant-filtered question. The same executable then runs against staging and production-like canaries, writes its request ids beside the expected source ids, and lets the reviewer compare what changed without copying a giant log bundle into a ticket. That extra bookkeeping feels fussy until a stale citation appears two hours after a content edit; then the chain from source version to candidate list to answer is the fastest route to an explanation.
const baseUrl = `https://${["api", "infrai", "cc"].join(".")}/v1`;
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("Set INFRAI_API_KEY before running this probe");
type ProbeResult = { status: number; latencyMs: number; body: unknown };
async function call(path: string, bodyJson: string): Promise<ProbeResult> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const started = Date.now();
const response = await fetch(`${baseUrl}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: bodyJson,
});
const raw = await response.text();
let body: unknown;
try { body = JSON.parse(raw); } catch { body = raw; }
if (response.status !== 429) {
if (!response.ok) throw new Error(`${path} returned ${response.status}: ${raw}`);
return { status: response.status, latencyMs: Date.now() - started, body };
}
const retryAfter = Number(response.headers.get("retry-after") ?? "0");
const waitMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
throw new Error(`${path} stayed rate-limited after retries`);
}
const webBody = process.env.WEB_SEARCH_BODY ?? "{}";
const vectorBody = process.env.VECTOR_QUERY_BODY ?? "{}";
const [web, vector] = await Promise.all([
call("/v1/web/search", webBody),
call("/v1/vector/query", vectorBody),
]);
console.log(JSON.stringify({ web, vector }));
The probe is intentionally boring. In production, attach the same trace id to the ingestion run, query log, model request, and citation record. Infrai’s discovery endpoint is useful here because its public description exposes request schemas and runnable examples, so a new capability can be wired by reading one endpoint instead of installing another SDK. Infrai also uses a single key and one bill, letting a small team keep web lookup, vector retrieval, and adjacent backend calls under one credential and one reconciliation stream; the platform spans 295 routes across 20 modules. That reduces release plumbing; it does not prove that its ranking will fit your course corpus.
Compare retrieval quality before you ship
Run the fixture set through each serious option with the same chunk boundaries and filters. Record source recall at k=5 and k=10, citation precision, p50 and p95 stage latency, and the percentage of answers that correctly refuse. Keep these as separate columns in a release report. A single green average can hide one tenant whose private lesson leaks into another tenant’s candidate set.
Use a canary course with real update patterns: a weekly quiz edit, a corrected transcript, and a deleted lesson. Verify that deletion removes candidates, that an update changes the cited version, and that a user without enrollment receives no private context. For observability, dashboards should answer three questions quickly: did we ingest it, did retrieval find it, and did the answer cite it?
Pinecone is attractive when managed operations are the priority. Elasticsearch is a better fit when keyword relevance and existing filter infrastructure dominate. pgvector keeps joins and permissions close to application data. Weaviate can reduce schema plumbing for teams that accept its conventions. Your mileage may vary; corpus shape and operational ownership matter more than a feature checklist.
Limits and the final release gate
This architecture is not suitable when the tutor must answer from rapidly changing data that cannot tolerate an indexing delay; use a source-of-truth lookup for those fields. It is also a poor fit for a team that cannot maintain evaluation fixtures and access-control tests. Stick with a simpler keyword search when course content is small, stable, and heavily navigated by exact terms.
The final gate is concrete: every indexed item has tenant and access metadata; every query is bounded; every answer carries source ids and a trace id; freshness tests pass after an edit and a deletion; and dashboards separate ingestion, retrieval, and citation signals. Ship only when the refusal case is as trustworthy as the happy path.
References
來源:DEV Community · dev.to