Case study — Knowledge Base docs-as-code + Context Graph cho portfolio 15 dự án
Case study này đã lược bỏ tên khách hàng và mọi thông tin định danh nội bộ. Nội dung giữ lại là thiết kế hệ thống, phương pháp đo và kết quả — phần an toàn để chia sẻ công khai.
Vai trò: Technical Lead, tự thiết kế và xây toàn bộ hệ thống (2026-08 → 2026-09, ~7 tuần làm song song với công việc quản lý dự án). Repo cá nhân:
tmt-projects-kb. Stack: Markdown + YAML frontmatter, Python (tree-sitter, networkx, SQLite), MCP server, viewer d3-force.
1. Bối cảnh
Tôi là Technical Lead quản lý nhiều dự án web cho khách hàng doanh nghiệp Nhật. Mỗi dự án gồm 3–4 repo kiểu microservice: web frontend, contact API, infra Terraform, CMS deployment. Tech stack và môi trường deploy khác nhau theo từng dự án và từng env: Cloud Run, Google App Engine, Firebase Hosting/Functions, Cloud Functions gen1/gen2, một số còn chạy CircleCI hoặc AWS S3 + CloudFront.
Tài liệu quản lý nằm rải rác trên Confluence, Google Sheets/Slides/Docs. Sau nhiều năm bàn giao qua nhiều thế hệ dev, phần lớn tài liệu lệch so với code đang chạy.
2. Pain point đo được (trước khi có hệ thống)
| Pain | Biểu hiện cụ thể |
|---|---|
| Cập nhật tính năng | Mỗi lần phải tự tìm và ráp thông tin từ Confluence, Sheets và 3–4 repo, không biết nguồn nào còn đúng. |
| Câu hỏi đơn giản của khách | "Dự án nào dùng Cloud Run ở prod?", "Có bao nhiêu dự án còn dùng Nuxt?" đòi mở từng repo để kiểm tra, mất hàng chục phút. |
| AI coding agent | Mỗi phiên phải "mớm" ngữ cảnh thủ công. Agent đọc lan man hàng chục file để tự ráp quan hệ giữa repo, tốn token và vẫn bỏ sót hop xuyên repo hoặc nhầm component trùng tên giữa dự án. |
| Quyết định kỹ thuật | Root cause, trade-off của các lần fix không được ghi lại; vài tháng sau lặp lại cùng câu hỏi "vì sao hồi đó chọn cách này". |
3. Mục tiêu và ràng buộc thiết kế
Mục tiêu: mọi câu hỏi tri thức dự án (hạ tầng, kiến trúc, tech stack, deploy, env, convention, quyết định) trả lời được ngay từ KB, không tốn thời gian và token scan repo nguồn. Người và agent dùng chung một nguồn. Mỗi loại câu hỏi có một tầng trả lời rẻ nhất, và agent phải biết chọn đúng tầng.
Ràng buộc tự đặt:
- Local-first, docs-as-code: Markdown + YAML frontmatter trong git, không phụ thuộc SaaS. Diff review được, commit prefix
kb:. - Code đang chạy là trọng tài: tài liệu import chỉ là gợi ý cho tới khi được đối chiếu với code và gắn commit hash.
- Không vector DB / embeddings ở quy mô hàng chục dự án × vài chục file; dùng progressive disclosure có cấu trúc thay cho tìm kiếm mờ.
- Tuyệt đối không lưu secret: chỉ lưu tên, vị trí (Secret Manager @ project / name), hint và tiền tố SHA-256 để đối chiếu.
- Chi phí build = 0 token LLM: mọi artifact dẫn xuất (index, graph) sinh tất định từ nguồn có sẵn, không bao giờ sửa tay.
4. Giải pháp — 4 tầng, mỗi tầng cho một loại câu hỏi
4.1 Tầng tri thức: KB docs-as-code theo project × component × env
Trả lời: fact đơn về một dự án (GCP project, URL, runtime, deploy flow, vị trí secret, convention) và kiến trúc nội bộ một component.
- Mỗi dự án một folder, mỗi component map 1:1 với một repo code. Mỗi doc mang frontmatter có kiểm soát: runtime, GCP project, access control, deploy flow, integrations, secrets (chỉ pointer), tags,
sourcevàverification. - Index sinh tự động
index/projects.yaml(project × component × env × thuộc tính): câu fact đơn trả lời bằng một lần đọc file, không cần MCP. - Controlled vocabulary (
shared/vocabulary.yaml): validator fail build nếu frontmatter dùng giá trị lạ (ví dụcloudrunthay chocloud-run). Đây là điều kiện để index và graph sinh ra tất định. - ADR theo MADR 4.0.0 cho mọi thay đổi có quyết định đằng sau:
status(proposed / accepted / superseded…),summary1–2 câu được index nguyên văn vàoindex/decisions.mdđể agent trả lời không cần mở body. Validator cảnh báo ADRproposedquá 30 ngày. - Quy tắc phạm vi rõ ràng: KB chỉ giữ kiến trúc, tech stack, spec tính năng, env/infra, convention, quyết định. Effort, schedule, phân công, meeting notes không bao giờ vào KB.
4.2 Tầng import và verification
Trả lời: "fact này còn đúng không, đúng tới commit nào".
- Pipeline export Confluence (theo hub page, follow links) và Google Drive vào
_imports/raw/<slug>/, giữ bản gốc nguyên vẹn, ghi nguồn từng file. - Quy trình 4 bước cho mỗi dự án mới: khai quật repo code (tạo doc
verifiedtừ code) → import tài liệu sẵn có → chưng cất vào component đúng (tất cảunverified) → phiên verify đối chiếu code, ghi danh sách lệch. - Mỗi doc mang
verification.statusvàverified_against: <commit hash>. Dashboardverification-report.mdsinh tự động: độ phủ verified theo dự án, file stale theo ngày nguồn, doc verified quá 90 ngày. - Verify phát hiện lệch doc-vs-code thật: Confluence ghi "purge theo URL" nhưng code purge toàn bộ theo CP code (Akamai CCU v3); Confluence chứa credential plaintext, KB từ chối chép.
4.3 Tầng graph: context graph 2 lớp, phân vùng theo dự án
Trả lời: câu hỏi quan hệ — form X gọi vào đâu xuyên repo, rotate secret Y ảnh hưởng file nào, dự án nào dùng package / middleware / host Z, "X có gọi Y không".
- Knowledge layer sinh từ frontmatter đã validate: Project, Component, EnvInstance, GcpProject, Runtime, Integration, Secret, ADR, SourceDoc.
- Code layer sinh từ repo local bằng tree-sitter (10 extractor: TypeScript/JS, Next.js, Vue SFC, Express, Firebase Functions, Terraform HCL, Python, Contentful, env scan, HTTP call): CodeFile, Symbol, Route, Middleware, EnvVar, HttpCall, TfResource.
- Bridge edges nối hai lớp: HAS_CODE, RESOLVES_TO (env var ↔ secret), CALLS (HTTP call trong frontend resolve qua env var URL tới route của API khác), PROVISIONS (Terraform ↔ GCP project).
- Phân vùng theo project ngay từ đầu:
_global.jsonchỉ chứa knowledge layer + catalog layer (package + version, middleware, host gọi ra, Terraform resource type, tên env var theo component) để trả lời câu cross-project mà không bao giờ lộ internals code của dự án khác. Edge nối hai project khác nhau là lỗi build. - Token budget là first-class: mỗi response tool ≤ ~2.000 token; vượt budget thì gom edge theo loại, collapse file theo thư mục, ưu tiên giữ Route / Symbol / EnvVar trong chuỗi trace trước khi cắt node xa.
- Một lệnh
kb_refresh.pybuild lại đúng phần đang cũ (component có commit mới, index khi doc đổi), 1–2 giây khi không có gì cũ, vài chục giây khi ép build lại toàn bộ 46 component. Có git hook để tự chạy sau commit.
4.4 Tầng truy cập: MCP server đa client + viewer trực quan
- MCP server
kb-graph8 tool, mỗi tool cho một dạng câu hỏi:kb_trace(form → route → controller → service xuyên repo),kb_impact(đổi/rotate X ảnh hưởng gì),kb_neighbors(láng giềng một node, kiểm phủ định),kb_find/kb_compare(cross-project trên catalog: "bao nhiêu dự án dùng Nuxt", "so sánh version Express"),kb_sql(SQL chỉ đọc trên SQLite của partition cho câu chưa có tool),kb_entity,kb_status. Server tự reload theo mtime, báocode_graph_stalekhi HEAD repo local khác commit graph được sinh. - Đăng ký cho Claude Code, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat với cùng một binary. Quy tắc chọn tầng của agent (fact đơn → index; quan hệ → graph; không suy "không có edge = không có quan hệ") viết một lần trong
AGENTS.md;CLAUDE.md,GEMINI.md,copilot-instructions.mdchỉ là con trỏ. - Viewer force-directed (canvas + d3-force, đọc thẳng partition JSON): chọn partition, tìm node, highlight láng giềng hop 1/2, panel evidence và edge gom theo loại, legend ẩn/hiện type, URL giữ trạng thái để chia sẻ. Dùng để trình bày kiến trúc cho khách và để debug extractor.
4.5 Bảng chọn tầng: câu hỏi nào → tầng nào
| Loại câu hỏi | Tầng trả lời | Vì sao (theo benchmark §5) |
|---|---|---|
| Fact đơn về một dự án: GCP project, URL, runtime, deploy, vị trí secret | index/projects.yaml, không MCP | Một lần đọc file, rẻ nhất, verified theo commit |
| Kiến trúc nội bộ một component: route → handler → middleware | KB docs (architecture.md), mở code khi cần dòng cụ thể | KB docs đủ (completeness 0.72); graph hiện thua ở đây (0.64) vì chưa có edge route → tên hàm handler và middleware theo từng route |
| Trace xuyên repo, impact khi rotate secret, cách ly giữa dự án | kb_trace, kb_impact | Graph lấy lại completeness KB docs làm mất (0.44 → 0.72), có file:line, rẻ hơn đọc source |
| Cross-project: "bao nhiêu dự án dùng Nuxt / package X / middleware Y", so sánh version | kb_find, kb_compare với project="*" | Rẻ nhất và nhanh nhất cả ba chế độ ($0.05 / 11 s), không grep 35 repo, không lộ code dự án khác |
| Phủ định: "component X có gọi Y không" | kb_neighbors + kết luận "theo graph tĩnh" | Không graph, agent nhầm component trùng tên giữa dự án (2/3 và 1/3 run); có graph, 3/3 đúng component |
| Quyết định / ADR | index/decisions.md | Summary index nguyên văn, không cần mở body |
| Câu chưa có tool nào cover | kb_sql | SQL chỉ đọc trên bản SQLite của partition |
5. Kết quả
Quy mô hiện tại (2026-09-25)
| Chỉ số | Giá trị |
|---|---|
| Dự án / repo component | 15 / 51 (50 component đã có doc) |
| Doc tri thức đã chưng cất | 153 file, 135 verified theo commit hash (75% của 180 file có status) |
| ADR có index | 27 ADR dự án + 4 ADR về chính KB |
| Tài liệu gốc đã import | 184 file Confluence / Drive |
| Code graph | 46 component, 10 extractor tree-sitter, 0 token LLM |
| Graph | ~25.000 node / ~45.500 edge qua 15 partition; _global 1.026 node / 2.679 edge |
| MCP | 8 tool, 4 client, budget ≤ 2.000 token/response |
| Test | 33 pytest (fixture KB mini + repo mini) |
Benchmark v2 — đo bằng phiên agent thật, chấm mù
Cách đo (2026-09-24): 9 câu hỏi đại diện cho 5 nhóm use case, mỗi câu chạy 3 chế độ × 3 run cold (81 phiên), cộng 3 phiên dài 5 câu liên tiếp × 3 chế độ (45 phiên). Model claude-opus-5-5, Claude Code 2.1.281, 35 repo ghim HEAD. Ground truth đóng băng trước run đầu; completeness (0–1) chấm mù bởi phiên agent riêng chỉ đọc truth và answer ẩn danh; đếm hallucination. Tổng chi phí đo và chấm: $40.74.
Ba chế độ: A Bare (chỉ đọc source repo), B KB docs (KB, không MCP), C KB + Graph (MCP). B − A là giá trị riêng của KB docs; C − B là giá trị graph cộng thêm.
| Nhóm câu | Chỉ số | A Bare | B KB docs | C KB + Graph | Đọc kết quả |
|---|---|---|---|---|---|
| Nội bộ 1 component (3 câu) | cost / time / compl | $0.19 / 48 s / 0.93 | $0.14 / 25 s / 0.72 | $0.13 / 25 s / 0.64 | KB docs đủ; graph không giúp, còn thua |
| Trace / impact / cách ly (3 câu) | cost / time / compl | $0.19 / 51 s / 0.72 | $0.12 / 23 s / 0.44 | $0.15 / 28 s / 0.72 | Graph lấy lại completeness bằng đọc source, rẻ hơn, nhanh hơn |
| Phủ định trung thực (1 câu) | compl | 0.11 | 0.11 | 0.33 | Không graph: nhầm component trùng tên 2/3 và 1/3 run |
| Cross-project (2 câu) | cost / time / payload | $0.20 / 61 s / 5.604 tok | $0.14 / 23 s / 5.698 tok | $0.11 / 19 s / 3.372 tok | Catalog thay grep 35 repo, payload −2.327 tok |
| Tất cả 9 câu (median) | cost / time / turn / compl / hallu | $0.17 / 49 s / 10 / 0.70 / 2/27 | $0.13 / 24 s / 4 / 0.55 / 3/27 | $0.14 / 25 s / 5 / 0.65 / 2/27 | |
| Phiên dài 5 câu | cost phiên / cost mỗi câu từ câu 2 / compl / hallu | $3.65 / $0.84 / 0.92 / 3 | $2.73 / $0.64 / 0.72 / 2 | $2.06 / $0.48 / 0.79 / 1 | Graph rẻ hơn đọc source 44%, tích lũy theo số câu |
Câu trace xuyên repo cụ thể (form liên hệ → route → controller → service qua 2 repo): KB docs completeness 0.25 vì có kiến trúc nhưng không có file:line; graph đạt 1.00 nhờ edge CALLS từ call site frontend tới route của API.
ContactForm (contact-form.tsx:105)
--CALLS_HTTP--> POST {NEXT_PUBLIC_CONTACT_API_URL}jp/contact
--CALLS (envvar-resolved, dev+prod)--> route POST /api/jp/contact
--ROUTES_TO--> contactController.sendContactEmailJp
--IMPORTS--> zendeskService.ts
caveats: trace tĩnh — middleware/DI runtime, dynamic dispatch không hiện ở đây
Kết luận trung thực từ số đo:
- KB docs mua toàn bộ phần tiết kiệm: thời gian −50%, turn 10 → 4, cost −25% so với đọc source, nhưng trả bằng −0.15 completeness vì agent dừng ở tài liệu, không mở code.
- Graph không đổi cost/time tổng nhưng lấy lại +0.10 completeness, tập trung đúng nhóm nó được thiết kế cho: trace / impact / cách ly (+0.28, bằng mức đọc source) và phủ định trung thực (+0.22). Ở cross-project, graph rẻ nhất và nhanh nhất mà không thua chất lượng.
- Graph thua thật ở kiến trúc nội bộ một component: edge route → handler dừng ở file/module, chưa có tên hàm và middleware validator theo từng route; agent tin graph nên không mở code. Đây là hạng mục backlog đã ghi, không phải điểm phân biệt của hệ thống.
- Phiên dài graph rẻ hơn rõ: 5 câu liên tiếp $2.06 so với $3.65 đọc source, hallucination 1 so với 3, và khi hỏi một dự án ngay sau một dự án khác không gán fact dự án này cho dự án kia.
- Sai số cần ghi nhận: n = 3 run mỗi ô, min–max rộng; một mục trong ground truth câu tổng hợp lệch với KB verified nên phần lớn cờ hallucination của câu đó là lỗi truth, không phải model bịa; kết quả chỉ đúng cho cặp Claude Code 2.1.281 + claude-opus-5-5.
Tác động vận hành
- Mọi phiên agent mở tại bất kỳ repo nào đều có cùng ngữ cảnh qua pointer
CLAUDE.local.mdvà MCP; không còn mớm tay. - Câu hỏi của khách kiểu "dự án nào dùng Cloud Run ở prod" trả lời từ index trong một lần đọc; "có bao nhiêu dự án dùng Nuxt / còn dùng Akamai" trả lời bằng một tool call trên catalog, không mở repo.
- 27 ADR ghi lại root cause và trade-off của các đợt security audit, vá CVE, migrate CI, kể cả các phát hiện lật ngược thiết kế ban đầu (ví dụ
npm audit = 0không đồng nghĩa sạch vì bỏ sót advisory chỉ có trên GitHub;min-release-agekhông áp chonpm ci). - Verification report chỉ ra chính xác dự án nào tài liệu còn stale để ưu tiên verify, thay vì "nghi ngờ tất cả".
6. Quyết định thiết kế đáng nói và lý do
| Quyết định | Phương án bị loại | Lý do |
|---|---|---|
| Frontmatter có vocabulary kiểm soát + index sinh tự động | Full-text search / vector DB | Quy mô nhỏ, câu hỏi phần lớn là fact có cấu trúc; tất định, không cần embedding, diff review được. |
| Tách tầng theo loại câu hỏi, agent phải chọn tầng | Một điểm truy cập duy nhất (chỉ MCP hoặc chỉ docs) | Benchmark cho thấy mỗi tầng thắng ở một nhóm câu khác nhau; gom một tầng thì hoặc mất completeness hoặc tốn turn. |
| Graph derive-only, 0 token LLM | Dùng LLM trích quan hệ từ code | Chi phí build bằng 0, tái lập được, sai thì sửa extractor chứ không sửa graph tay. |
| Phân vùng theo project + catalog layer riêng | Một graph toàn cục | Cách ly dữ liệu giữa khách hàng, câu cross-project vẫn trả lời được qua catalog không lộ code. |
| Token budget cứng trên mỗi response tool | Trả full subgraph | Agent nhận vừa đủ thông tin, không nổ context; collapse có thứ tự ưu tiên thay vì cắt ngẫu nhiên. |
| Kết luận phủ định phải nói "theo graph tĩnh" | Coi không có edge = không có quan hệ | Static analysis không thấy dynamic dispatch, URL nối chuỗi; build report liệt kê unmatched/dynamic call để kiểm. |
Quy tắc agent viết một lần trong AGENTS.md | Mỗi agent một file quy tắc | Đa client (Claude / Gemini / Copilot) dùng chung, tránh lệch quy tắc. |
Bỏ trường commits trong ADR, giữ prs | Ghi commit hash | Hash trên feature branch đổi sau squash/rebase, sinh ghi chú "fill in after" treo hàng tháng. |
7. Bài học
- Chưng cất và verify mua phần tiết kiệm; graph mua phần đúng. KB docs cắt nửa thời gian và turn cho mọi câu; graph chỉ đáng giá ở câu quan hệ và cross-project. Đừng xây graph trước khi có doc sạch, và đừng ép graph trả lời câu mà doc đã đủ.
- Đo bằng ablation và chấm mù trước khi khẳng định. Bản đo đầu (4 câu, không chấm mù) cho kết luận lạc quan hơn thực tế; bản v2 với ground truth đóng băng phơi ra điểm thua thật của graph ở kiến trúc nội bộ component. Kết quả bất lợi được ghi nguyên vào benchmark và thành backlog.
- Tổng usage phiên agent phản ánh số turn của harness, không phản ánh lượng thông tin tool trả về. Đại lượng so sánh đúng là cost, số turn, payload truy xuất và completeness chấm mù.
- Nguồn hallucination không phải "model bịa" mà là tên trùng giữa dự án và truth sai: không có graph, agent trả lời nhầm component cùng tên ở dự án khác; một cờ hallucination hàng loạt hoá ra là ground truth chốt theo code trong khi rate limit nằm ở Cloud Armor.
- Vocabulary có kiểm soát là khoản đầu tư rẻ nhất mang lại nhiều nhất: nó là thứ khiến index, graph, validator, dashboard đều sinh được tất định.
- Stale phải hiện ra được:
code_graph_stale,kb_index_stale, ADRproposedquá 30 ngày, doc verified quá 90 ngày. Hệ thống tri thức không tự báo cũ sẽ lại lệch như Confluence.
8. Tech stack
Markdown + YAML frontmatter · Python 3.13 · tree-sitter (language pack: TypeScript/TSX/JS, Python, HCL, Vue, YAML, Dockerfile) · networkx · SQLite · MCP Python SDK (stdio) · pytest · HTML canvas + d3-force · GitHub / GHES · GCP (Cloud Run, GAE, Firebase, Cloud Functions, Secret Manager) · Terraform · Contentful · Akamai.
