I. Vì sao có wiki này — vấn đề mà tools hiện tại không giải
Tháng 2/2024, Andrej Karpathy tweet một idea: thay vì chunk-and-embed tài liệu vào vector DB rồi cosine-search, LLM distill nguồn vào page có cấu trúc, retrieve bằng title + tag + [[wikilink]] thuần. Page là đơn vị tri thức nén, không phải đoạn text 200-token.
Vấn đề mà tweet này giải quyết — và tools hiện tại không giải:
- Vector RAG hallucinated citation. Cosine-similarity retrieve chunk gần đúng → LLM tổng hợp → trả lời nghe-hợp-lý-nhưng-sai. Citation trả về là chunk ID không readable. User không verify được.
- Knowledge dùng-một-lần. Bạn ingest 50 PDF, chat 1 lần, kiến thức không “compound” — không có page-level concept để link tới, không có decay khi outdated. Lần sau ingest lại từ đầu.
- Vendor lock-in. Pinecone / Weaviate / OpenAI Assistants — đẹp đến khi giá tăng / API breaking change. Personal KB không cần scale đó.
LLM wiki theo Karpathy pattern bỏ cả 3 cùng lúc bằng cách distill, không retrieve. Bài này dẫn bạn qua toàn bộ implementation choices — 2 trục compounding:
- Knowledge (sections II-V): page schema, forgetting, trust, scale
- Integration (sections VI-VII): wiki như tool cho agent khác + agent đọc code của bạn
Mỗi section là 1 load-bearing decision. Skip 1 section = chấp nhận alternative + hậu quả của nó.
II. Distill, không chunk-and-vector — foundations
Karpathy pattern in 1 sentence
LLM đọc source (text / PDF / URL) → compile thành page có cấu trúc (title + summary + body + tags + domain), lưu file JSON. Khi hỏi, agent search title + tag, đọc page liên quan, trả lời + cite [[Page Title]].
So với vector RAG:
| Vector RAG | LLM Wiki |
|---|---|
| Embed chunk → vector DB | Compile source → page file |
| Retrieve top-K bằng cosine | Search title/tag/[[wikilink]] |
| Citation = chunk_id (opaque) | Citation = [[Page Title]] (human-readable, navigable) |
| Chunk 200 token, mất context | Page 500-2000 token, self-contained |
| Hallucination khó detect | Refuse-or-cite enforceable (section IV) |
Page schema
Mỗi page là 1 file JSON ở data/wiki/<owner>__<slug>.json:
{
"title": "RAG Hybrid Search",
"slug": "rag-hybrid-search",
"owner": "thuvt",
"summary": "BM25 + vector hybrid với reranker. Khi nào dùng, khi nào không.",
"body": "## Khi nào hybrid > pure vector ... [[Vector DB]] ... [[BM25]]",
"tags": ["rag", "retrieval", "hybrid"],
"domain": "ML",
"ingested_at": "2026-05-12T14:00:00Z",
"last_verified_at": "2026-05-12T14:00:00Z",
"source_authority": "blog",
"confidence": 0.85,
"valid_from": "2026-05-12T14:00:00Z",
"valid_until": null,
"superseded_by": null,
"supersedes": []
}
body chứa Markdown thuần với [[wikilinks]] — resolve runtime sang page khác.
Compile pipeline 2-pass
Source → page là 2-pass LLM call:
- Pass 1 — Compile: LLM nhận source raw + existing-domain list. Output JSON với
title,summary,body,tags,domainproposed. Prompt teaches tolerant JSON parsing (model như Gemma hay emit\$,\(— strip trước parse). - Pass 2 — Lint: another LLM call review pass-1 output. Check:
[[wikilinks]]valid, no fabricated facts, summary ≤ 2 câu, title concise. Return adjustments.
Final page save xuống disk + Notion mirror (optional).
3-folder layout
data/
├── wiki/ # compiled pages (the source of truth for queries)
│ ├── thuvt__rag-hybrid-search.json
│ ├── thuvt__vector-db.json
│ └── _index.json # title → slug map cho fast lookup
├── raw/ # original sources (PDF, HTML snapshot, raw markdown)
│ ├── rag-paper-2024.pdf
│ └── blog-post-2026-05-12.html
└── log.md # human-readable changelog: "2026-05-12 ingested X (5 pages)"
wiki/ là canonical. raw/ để audit (tại sao page này có dòng kia?). log.md là git-blame của tri thức.
[[Wikilink]] resolution
Khi render page hoặc khi chat-answer, mọi [[Page Title]] được resolve về URL /p/<slug> (FE) hoặc page object (backend). Wikilinks tạo graph KB tự nhiên — không cần “knowledge graph” ceremony.
III. Forget ≠ Delete — bi-temporal honest forgetting
Vấn đề: stale knowledge
Wiki chạy 6 tháng. Page về “Best LLM model 2024” giờ outdated. Bạn:
- (a) Delete? Mất audit “tôi từng tin điều này”.
- (b) Để nguyên? Chat trả lời sai dựa trên page cũ.
- (c) Edit? Mất context decision điểm A → điểm B.
Cần một pattern thứ 4: honest forgetting — biết kiến thức stale, biết khi nào stale, biết replace bằng cái gì, vẫn audit-able.
Bi-temporal schema
Mỗi page có 4 thời gian-track:
| Field | Ý nghĩa |
|---|---|
ingested_at |
System time — khi page lưu vào disk lần đầu |
valid_from / valid_until |
Decision time — page valid trong khoảng nào |
last_verified_at |
Lần cuối bạn xác nhận page vẫn đúng |
superseded_by / supersedes |
Chain — page này thay thế page nào / được thay bởi page nào |
Bi-temporal nghĩa là phân biệt “khi nào kiến thức được biết” (system time) vs “khi nào kiến thức là đúng” (decision time). Trong real-world KB, 2 cái này luôn lệch.
4 forget actions
soft_delete_page(slug): chuyển page sang_archive/. Không bị query default tìm thấy, nhưnglist_archivedra. Reversible. Dùng khi: “tôi không chắc đây có còn đúng, ẩn đi đã.”mark_superseded(old, new): gánold.superseded_by = new,new.supersedes = [old]. Old vẫn ởwiki/, queries trả lời “see [[New Page]]”. Dùng khi: “page B thay thế page A, nhưng audit chain A → B.”reverify_page(slug): updatelast_verified_at = now(). Page vẫn đúng. Dùng cho periodic review.decay_scan(): liệt kê pages cólast_verified_at> 90 ngày trước. UI prompt: re-verify / supersede / archive cho mỗi page. Trigger thủ công (cron optional).
Drift detection
Khi chat trả lời từ 2+ pages, nếu các pages mâu thuẫn (vd: “best model = X” vs “best model = Y” cùng tag “ml-2026”), agent flag drift. 2 cách handle:
- Auto: pages mới hơn (lớn
valid_from) override pages cũ - Manual: agent surface conflict, user quyết supersede
Bi-temporal làm cái này thấy được. Vector RAG cũng có drift nhưng hidden — chunk gần nhất “win” không transparent.
_archive/ + restore + hard-delete
wiki/
├── thuvt__rag-hybrid-search.json
└── _archive/
├── thuvt__llm-best-2024.json # soft-deleted, restorable
└── thuvt__obsolete-prompt.json
restore_page(slug) đưa page từ _archive/ về wiki/. hard_delete_page(slug) xoá hẳn — admin only, không expose qua MCP (xem section VI).
IV. Cite-or-Refuse — 2-pass trust mechanism
Vấn đề: LLM bịa citation
Vector RAG cơ bản: retrieve top-K → stuff vào context → LLM answer. Vấn đề: LLM có thể compose answer dùng training-data + retrieved chunk lẫn nhau, citation [doc 3] chỉ là cosmetic.
Wiki cần stricter contract: citation must be a real page that contains the cited claim. Otherwise refuse.
2-pass enforcement
User asks: "what's the best vector DB for personal KB?"
Pass 1 — Answer attempt:
Agent searches title/tag → finds [[Vector DB]], [[RAG Hybrid Search]]
Composes answer:
"For personal KB, pgvector via Postgres is sufficient.
See [[Vector DB]] for tradeoff vs Pinecone."
Pass 2 — Judge:
Independent LLM call (or same model, fresh context)
Input: answer + cited pages content
Check: does [[Vector DB]] actually say "pgvector sufficient for personal KB"?
If YES → return answer to user.
If NO → replace with refusal:
"Tôi không có wiki entry confirms 'pgvector cho personal KB'.
Available: [[Vector DB]] (covers tradeoffs but doesn't pick), [[RAG Hybrid Search]].
Bạn muốn ingest source mới về cái này?"
Judge có 3 verdict:
- Pass: claim truly cited
- Fail — citation doesn’t support: claim không có trong cited page
- Fail — claim outside wiki: claim nằm ngoài wiki, dù có citation gì cũng không OK (strict mode)
Strict-mode addendum
Default mode: judge pass nếu citation HỖ TRỢ claim. Strict mode: pass chỉ khi claim được PHÁT BIỂU TRỰC TIẾP trong page. Khác biệt:
- Default: page nói “pgvector handles 1M vectors”, agent claim “pgvector good for personal KB” → PASS (suy luận hợp lý)
- Strict: page không nói chữ “personal KB” → FAIL refusal
Strict mode cho compliance / regulated context. Default cho personal use.
Refusal heuristic
Refusal không phải lúc nào cũng “tôi không biết”. 4 dạng refusal:
- No relevant page: “wiki không có entry cho topic này”
- Page exists but doesn’t say claim: “[[Vector DB]] có nói về tradeoffs nhưng không pick winner. Bạn muốn rephrase?”
- Conflicting pages: “[] và [[Y]] mâu thuẫn về point này. Cần resolve trước.”
- Page stale: “[] last_verified 6 tháng trước, có thể outdated. Reverify?”
Mỗi dạng → next action khác.
Web search citations
Khi user enable enable_web=true, agent có thể search web (DuckDuckGo HTML scrape, không API key). Citations trả về phải bao gồm URL gốc, không bịa. Judge cũng check URL accessibility.
Web search KHÔNG bypass cite-or-refuse — chỉ thêm source nguồn ngoài wiki, vẫn phải cite + verify.
V. Scaling Out — multi-user + TOC ingest
Multi-user, single Notion DB
Khi 2+ người dùng cùng wiki, có 2 approach:
- (a) Database per user: complete isolation, nhưng cross-user sharing phức tạp.
- (b) Single DB + Owner + SharedWith fields: 1 Notion DB, mỗi page có
Owner(select, 1 user) vàSharedWith(multi-select, N users).
Wiki chọn (b) vì sharing là use case chính (Alice ingest page về RAG, share với Bob để cùng review).
Visibility rule trong dispatcher:
def visible_to(page, viewer):
if viewer == "": # admin context (server-side cron)
return True
if page.owner == viewer: # owner sees own
return True
if viewer in page.shared_with: # explicit share
return True
return False # default: hidden
Mọi read action (list_pages, get_page, chat_wiki) filter qua hàm này.
HMAC cookie auth (vs JWT)
User login → server trả cookie:
Set-Cookie: wiki_session=alice.<base64(HMAC_SHA256(secret, "alice"))>
Cookie format: <user>.<signature>. Verify: re-compute HMAC từ user part, so signature. Stateless — không cần DB session table.
So với JWT:
- JWT carry payload (exp, claims) → larger cookie, parsing complexity, secret rotation harder
- HMAC: chỉ có user, không có expiration trong cookie → expiry là cookie
Max-Age=30dở browser
Personal scale (1-10 users): HMAC đủ. JWT là over-engineering.
Anti-spoof rule: backend MUST derive viewer từ cookie HMAC, KHÔNG trust client-sent X-User-Id header. Trojan agent có thể đặt header bất kỳ; HMAC verify mới là source of truth.
TOC-driven ingest
Khi ingest PDF dày (50+ trang) hoặc Markdown nhiều ##, naive approach là 1 page = 1 document. Vấn đề:
- Page quá lớn (10k+ token) — không fit vào 1 LLM context
- Search “topic in chapter 7” returns toàn document, không section-specific
TOC-driven: extract document structure trước khi compile.
def parse_pdf_structured(path):
pdf = pypdf.PdfReader(path)
outline = pdf.outline # PDF bookmarks
if outline:
return outline_to_sections(outline, pdf)
else:
return [(pdf.pages_full_text, "Whole document")]
def detect_markdown_sections(md):
# Split on H2 (##) — H1 is doc title
sections = []
current = {"title": None, "body": []}
for line in md.split("\n"):
if line.startswith("## "):
if current["title"]:
sections.append(current)
current = {"title": line[3:].strip(), "body": []}
else:
current["body"].append(line)
sections.append(current)
return sections
Mỗi section → 1 page riêng. Search “vector DB tradeoffs in personal KB chapter” → page section cụ thể.
Trade-off: 1 PDF 100 trang có thể tạo 30 page → tốn LLM calls hơn. Bù lại: search precision tăng, citation specific hơn.
One-section-one-page
ingest book "Designing Data-Intensive Applications" PDF:
→ outline = [Ch1 Reliable, Ch2 Data Models, Ch3 Storage, ...]
→ 12 pages created, each titled "DDIA: ChapterN — TopicName"
→ each linkable: [[DDIA: Chapter 5 — Replication]]
Một book = một “domain” tự nhiên trong wiki.
VI. MCP — exposing wiki to other agents
Từ “wiki của tôi” → “tool cho any agent”
Khi đã có wiki, câu hỏi tiếp theo: agent khác của tôi (Claude Desktop, Cursor, cron task, custom GPT, AgentBase agent) — có dùng được wiki này không?
2 path:
- Custom REST API per agent. Mỗi integration làm lại từ đầu. Không reusable.
- MCP (Model Context Protocol). Standard interface. Mọi MCP client cắm vào dùng được.
MCP = USB-C cho AI ↔ tool/data. Spec mở do Anthropic công bố 11/2024, giờ cross-vendor (OpenAI, Google, AWS Bedrock, GreenNode). Client-server qua JSON-RPC 2.0. Transport: stdio (desktop) hoặc Streamable HTTP (remote).
Phân biệt nhanh với A2A (Agent-to-Agent Protocol, Google 2025): MCP = agent → tool/data. A2A = agent ↔ agent peer (negotiate, delegate). Wiki là data tool, không phải peer → MCP đủ, đơn giản hơn.
Thiết kế MCP server cho wiki — 3 quyết định đắt
Code ở src/wiki_mcp.py (~250 LOC) — tool definitions + adapters + JSON-RPC router, transport-agnostic. Mount qua POST /mcp route trong local_main.py (~25 LOC). Technically nothing novel. 3 design decisions mới là bài học.
Decision 1 — Tool nào expose, tool nào giữ internal?
Wiki có ~15 backend actions. Expose hết là sai.
- Destructive (
hard_delete_page,mark_superseded) — DO NOT expose. Risk: agent wipe data nhầm. Keep destructive sau UI confirm. - Admin (
notion_pull,github_resync,_debug_fs) — meaningless cho agent ngoài. - Health / debug (
stats,decay_scan) — internal.
Còn lại các tool thực sự matter:
| Tool | Backend | Purpose |
|---|---|---|
wiki_search |
chat_wiki |
Grounded Q&A với citations |
wiki_list_pages |
list_pages |
Discovery |
wiki_read_page |
get_page |
Read 1 page đầy đủ |
wiki_list_users |
list_users |
Cho share picker |
wiki_ingest_text |
ingest_text |
Distill 1 source |
wiki_save_page |
save_page |
Direct save (no LLM compile) |
wiki_archive_page |
soft_delete_page |
Soft delete — reversible |
LLM client đọc description để quyết khi nào call tool nào. Description là primary UX surface — viết kỹ. Vague description → agent pick sai tool / skip.
Decision 2 — Adapter pattern: wrapper mỏng, không logic mới
Mỗi MCP tool map 1-1 với backend action. Adapter chỉ transform args:
MCP_TOOL_MAP = {
"wiki_search": ("chat_wiki", _adapt_search),
"wiki_list_pages": ("list_pages", lambda a: a),
"wiki_save_page": ("save_page", lambda a: a),
}
def _adapt_search(a: dict) -> dict:
return {
"messages": [{"role": "user", "content": a.get("question", "")}],
"domain": a.get("domain", ""),
"enable_web": bool(a.get("enable_web", False)),
}
Backend không biết về MCP, MCP không biết backend internals. Bug fix backend → MCP benefit miễn phí.
Decision 3 — Auth: Bearer = wiki user token, không OAuth
MCP spec mới có OAuth 2.0 dance — clean nhưng overkill cho personal/small-team. Pragmatic: mỗi wiki user 1 token, vừa làm UI login password vừa làm MCP Bearer.
Agent calls: Authorization: Bearer <token>
↓
Proxy: reverse-lookup token → username
↓
Forward: X-User-Id: <username>
↓
Backend: filter pages bởi viewer=<username>
Token = 32 URL-safe chars (~192 bit entropy). Agent dùng token alice → act as alice → see alice’s KB + shared. Identity = token.
4 use case agent → wiki qua MCP
- A. Daily note-taking: User “save meeting today” → agent
wiki_ingest_text(content=notes, source_label="Meeting 2026-06-15")→ freshly-named page cite-able sau. - B. Research while writing blog: User “notes của tôi về vector DB?” → agent
wiki_search(question="notes vector DB")→ grounded answer + citations. - C. Co-writing: User “draft section về Mamba based on my notes” → agent
wiki_read_page(title="Mamba")→ đọc full → draft. - D. KB maintenance (cron): Weekly
wiki_list_pagesfilterlast_verified_at > 90d→ agent suggest verify / supersede / archive cho mỗi stale page.
Trade-offs + pitfalls
- Latency: mỗi tool call = 1 HTTP roundtrip + 1 backend. Interactive chat OK; batch ingest 100 page slow — dùng backend trực tiếp.
- Description quality > code quality: LLM client pick tool bằng description. Vague → agent pick sai / skip. Invest viết description.
- Destructive action: dễ slip thêm vào sau, khó undo. Default safe = không expose.
- Token leakage: token = full access KB. Commit
claude_desktop_config.jsonlên public repo → leaked. Tutorial Part 6 warn explicit. - Rate limit: agent có thể loop tool calls. Backend không có rate limit → spike risk. Production nên add 100 calls/min/user ở MCP layer.
MCP vs A2A — khi nào dùng cái nào
| MCP | A2A | |
|---|---|---|
| Relationship | Agent → tool/data | Agent ↔ agent peer |
| Server | Passive (data, no reasoning) | Agentic (internal reasoning) |
| Wiki use case |
Personal use: MCP đủ. Upgrade lên A2A khi wiki có “auto-organize agent” reason về KB và propose actions, hoặc external agents muốn delegate tasks.
VII. CODEMAP — agent navigation
Cold-start cost
Mỗi session AI mới, agent phải build mental model về codebase từ đầu:
Agent: "list directory" → 30 files
Agent: "read README" → 200 lines
Agent: "read main file" → 1000 lines
Agent: "grep cho concept X" → 5 results
...
10-15 tool call burn ~30-40% context budget trước khi viết được dòng code đầu tiên. Mỗi session lặp.
Giải pháp: file-by-file map ~300 dòng
CODEMAP.md ở root project. Cấu trúc 5 phần:
- Top-level tree — ASCII diagram thư mục + 1-line per item
- Per-file table — mỗi file đáng kể: path / role 1-line / key symbols / notes
- Reverse-lookup cookbook — “Nếu bạn đang làm X, đọc Y trước, edit Z”
- Gotchas section — production warnings, drift hazards, dual-codebase rules
- How-to-use (cho agent) — đọc CODEMAP trước, dùng reverse-lookup thay grep, update sau significant work
Anatomy example
## 🏭 Production stack
### `llm-wiki-agent-deploy/` — AgentBase Runtime backend
| File | What | Key symbols | Notes |
|---|---|---|---|
| `main.py` | AgentBase entrypoint + dispatcher (~1100 LOC) | `@app.entrypoint handler`, `dispatch(action, payload, viewer)`, `call_llm()`, `run_agent()` | LIVE. Refactor cần redeploy + smoke. |
| `wiki_store.py` | Persistent storage (JSON + index, bi-temporal) | `save_page()`, `get_page()`, `list_pages(viewer=)`, `mark_superseded()`, `decay_scan()` | Bi-temporal fields: `valid_from/until`, `last_verified_at`, `superseded_by/supersedes`. |
...
Reverse-lookup cookbook — chỗ đắt nhất
| Task | Files đọc FIRST | Files edit |
|---|---|---|
| Add backend action mới | main.py dispatch() |
main.py + matching MCP tool nếu expose |
| Change page schema | wiki_store.py _index_entry() + save_page() |
All: wiki_store.py, notion_sync.py, migration script |
| Touch frontend | llm-wiki.html — search section // ── X ── |
Same file. Test browser. |
| Deploy production | agentbase-deploy/SKILL.md |
Build + push vCR + runtime.sh update |
Agent đọc cookbook → biết exactly đụng file nào → skip 80% grep.
Gotchas — drift hazards
Production warnings section bắt buộc cho codebase có hazards real:
/datawipe khi AgentBase redeploy → re-hydrate từ Notion canonical- HF Space PUBLIC → MCP /mcp accept anyone với valid Bearer
save_pagecosmetic bug: returnsmerged=Truealways (existed_before check sau save)- Cookie HMAC ≠ JWT — format-specific
- Multi-user
viewerfilter apply mọi read; bypass =viewer=""
Mỗi item: hazard + cause + mitigation hoặc workaround.
Wire-in mechanism
CODEMAP chỉ valuable nếu agent thực sự đọc. 2 mechanism:
AGENTS.mdorient first: “Nếu CODEMAP.md tồn tại, read it BEFORE grepping. Saves 10+ tool calls per session.”- Update habit sau significant work: thêm dòng “Update CODEMAP.md nếu thêm/xoá top-level file” vào checklist review của bạn. Stale CODEMAP > absent CODEMAP, but try stay current.
Measured speedup
Trước (1 session refactor MCP layer): 14 tool call build mental model. ~22k token đọc xong tự thấy “OK giờ tôi hiểu”.
Sau CODEMAP.md tồn tại: 1 read CODEMAP (~7k token) + 2-3 targeted reads → đủ. 10-20× tool call reduction — measured trên 4 session khác nhau.
Anti-patterns
CODEMAP describe ý tưởng tổng quát (“kiến trúc microservice”). Cần file paths cụ thể.
CODEMAP refer file đã rename / xoá. Stale > absent. Update khi rename.
CODEMAP thay thế comments inline cho complex function. Đó là docstring’s job.
CODEMAP cover EVERY file <100 LOC. Focus file có business logic, skip config / boilerplate.
Wrap up
2 trục, 6 implementation pieces, 1 working wiki + ecosystem.
Bạn không cần adopt hết. Sweet spot vào việc của bạn. Pick section gần với pain point nhất, start ở đó. Mỗi section đứng vững một mình; nhưng adopt nhiều hơn = compound nhanh hơn.
Cost: ~10 giờ đọc + cuối tuần dựng. Payoff: compound suốt thời gian project sống.
Karpathy idea tweet ngắn (Feb 2024) chỉ là khởi điểm. Tất cả còn lại — bi-temporal, cite-or-refuse, MCP, CODEMAP — là implementation choices một project thực sự buộc bạn phải làm. Bài này là tổng kết những choice đã đi qua.
Wiki không phải app. Là second-brain readable, editable, shareable, auditable, pluggable into agent ecosystem. Không vector DB anywhere.
