Files
duocthu/docs/v1-delivery-plan.md
T

367 lines
24 KiB
Markdown

# Kế hoạch giao bản v1 chạy được — 2 tuần
**Lập ngày 2026-08-03. Hạn: ~2026-08-17.**
## Cập nhật bắt buộc 2026-08-04 — gate trước embedding
Phần hiện trạng ngày 2026-08-03 bên dưới được giữ làm lịch sử, nhưng không còn
được dùng để quyết định chạy embedding. Candidate schema v4 đã được tạo và đo
trên toàn corpus: **15.100 chunks** (14.949 prose + 151 block descriptor),
4.105.382 token `cl100k_base`, 0 chunk quá 800 token. Candidate chưa phải artifact
canonical cho đến khi vượt toàn bộ gate và thay thế `data/processed/chunks.jsonl`.
Thứ tự bắt buộc từ đây:
1. khóa an toàn nội dung: label/liều không tách rời; `source_text` ghép lại đúng
section; toàn bộ header bảng chưa kiểm chứng bị embargo khỏi text embedding;
2. khóa provenance: range vật lý và range trang in phải chính xác theo từng chunk;
attachment phải mang `block_id`, `bbox`, trang vật lý và trang in;
3. khóa consumer: loader chỉ nhận đúng schema v4, từ chối schema cũ/mới và metadata
sai kiểu hoặc sai miền;
4. chạy test + `chunk-ready` trên candidate; chỉ khi mọi gate bằng 0 mới tái sinh
artifact canonical và ghi SHA-256;
5. smoke-test local bằng vector giả để kiểm plumbing/idempotency; xóa collection test;
6. **chỉ sau phê duyệt riêng của chủ dự án** mới gọi provider có chi phí hoặc chạy
embedding toàn corpus. Bedrock chỉ dùng để tìm hiểu/benchmark, không phải runtime
dependency.
Định nghĩa **READY TO EMBED**: canonical là schema v4; toàn bộ readiness gate bằng
0; test ingestion và AI service liên quan đều pass; SHA corpus đã ghi; Qdrant không
còn collection test; không có header/cell chưa kiểm chứng trong embedding text.
Trạng thái này chỉ cho phép bước chuẩn bị kỹ thuật, không tự động cấp phép phát sinh
chi phí.
Quy ước của tài liệu này, theo đúng luật trong `CLAUDE.md`:
- **(đo)** = đã chạy thật trong phiên 2026-08-03, lệnh và kết quả ghi trong
`docs/progress-log.md`.
- **(ước lượng)** = phỏng đoán, chưa đo, có thể sai. Mọi con số thời gian
trong tài liệu này đều là ước lượng — không có ngoại lệ.
- `[chờ xác nhận]` = phụ thuộc quyết định của người chủ dự án, không được tự
chọn thay.
Ước lượng thời gian giả định **1 người, ~6 giờ làm việc hiệu quả/ngày, 10
ngày công**. Nếu thực tế là bán thời gian thì mục §8 (ngoài phạm vi) phải
dài thêm, chứ không phải ép các mục còn lại chạy nhanh hơn.
---
## 0. Hiện trạng — đo, không phải nhớ
| Thành phần | Trạng thái |
|---|---|
| `ingestion/` extract → segment → chunk | Baseline 2026-08-03 đã hoàn thành; candidate schema v4 ngày 2026-08-04 có 15.100 chunks và đang chờ gate cuối trước khi trở thành canonical |
| `ingestion/embed/` | Đã có provider ports, cache, local BGE-M3 và adapter Bedrock; chưa được phép chạy provider trả phí/full corpus |
| `ingestion/load/` | Đã có validation fail-closed schema v4, manifest/hash, upsert idempotent và adapter Qdrant; còn nghiệm thu artifact canonical mới |
| `apps/ai-service/` | Đã có FastAPI/RAG, adapter Qdrant/Postgres, guardrails và citation theo region; còn nghiệm thu tích hợp trên corpus canonical mới |
| `apps/api-gateway`, `auth-service`, `chat-service`, `user-service` | **0 file `.ts`** mỗi service |
| `apps/web/` | 18 file, chat UI + PDF split-view chạy được, backend là mock (`sendChatMessage` = `setTimeout(400ms)` + fixture) |
| `packages/shared-types` | DTO `ChatMessage` / `Citation` đã có |
| Dockerfile | **0 cái trong toàn repo** |
| `infra/helm/medical-chatbot/templates/` | **rỗng**, chỉ có `.gitkeep`; `values.yaml` chỉ có 2 dòng comment |
| `infra/argocd/applications/{dev,staging,prod}/app.yaml` | Có sẵn, trỏ `path: infra/helm/medical-chatbot`, `targetRevision: master`; còn 3 `TODO` (project, repoURL, destination cluster) |
| `infra/docker/docker-compose.yml` | Chỉ có `postgres`, `qdrant`, `redis` — không có service ứng dụng |
| CI | Chỉ có `infra/ci/github-actions/README.md` |
| Tracing | Không có gì |
**Tài sản không nằm trong repo nhưng có thật**: quyền truy cập k3s của team,
ArgoCD (admin), kubeconfig đã hoạt động. Đây là lý do phần deploy không bắt
đầu từ số 0.
---
## 1. Phạm vi v1 — cắt gì, và vì sao đó không phải "ăn bớt"
### Cắt khỏi v1: `api-gateway`, `auth-service`, `user-service`, `chat-service`
Bốn service này cộng lại đang là **0 dòng code (đo)**. Viết cả bốn bằng
NestJS trong 2 tuần, song song với mọi việc khác, là thứ giết deadline — và
không service nào trong bốn cái đó **thêm năng lực** cho bản chạy được:
gateway là định tuyến, auth là đăng nhập, user là hồ sơ, chat là lịch sử.
Thay thế trong v1:
| Nhu cầu | Cách làm trong v1 | Nợ kỹ thuật để lại |
|---|---|---|
| Chặn người ngoài | Basic-auth ở ingress (hoặc header token dùng chung) | Không có tài khoản cá nhân, không phân quyền |
| Lịch sử hội thoại | `ai-service` ghi thẳng Postgres, bảng `conversation` / `message` | Không có service riêng, không có sync đa thiết bị |
| Hồ sơ người dùng | Không có | Toàn bộ |
| Định tuyến | `web` gọi thẳng `ai-service` | Không có rate-limit/gateway policy tập trung |
Điều này **không mâu thuẫn** với Clean Architecture đã ghi trong `CLAUDE.md`:
domain là retrieval + grounding, còn auth/history/profile là hạ tầng. Tách
chúng ra service riêng sau này không phải sửa domain — nếu domain được viết
đúng ngay từ đầu (xem §4.C).
### Ba thứ tuyệt đối không cắt, dù trễ
1. **Vector không bao giờ được chọn *thuốc*.** Danh tính thuốc resolve tất
định. Lý do đo được: cặp `PANTOPRAZOL ↔ OMEPRAZOL` có cosine chống chỉ
định **1,000**, `DIGOXIN ↔ DIGITOXIN` 0,891/0,911, `NATRI NITRIT ↔ NATRI
THIOSULFAT` 0,631 ở phần liều. Để cosine chọn thuốc là chấp nhận rủi ro
trả nhầm liều của thuốc khác.
2. **Trả về cả section, không phải top-k mảnh.** Trả 2/5 chống chỉ định
nguy hiểm hơn trả 0, vì thiếu sẽ bị đọc thành "không có chống chỉ định".
Đã có bảo chứng: gate `section_not_reassemblable_from_chunks` = 0.
3. **Không đọc số liều từ 167 block quarantine** (129 block = 77% nằm trong
`lieu_luong_va_cach_dung`) — phải hiện ảnh crop trang gốc.
---
## 2. Giả định phải xác nhận trước khi bắt đầu
| # | Giả định mặc định của kế hoạch này | Nếu khác thì đổi gì |
|---|---|---|
| GĐ-1 | Đích deploy là **k3s của team qua ArgoCD** | Nếu chỉ cần `docker-compose` demo: bỏ §4.E5-E8, tiết kiệm ~2 ngày (ước lượng) |
| GĐ-2 | "Tracing" = **trace LLM/RAG** (câu hỏi → thực thể resolve → chunk lấy ra → prompt → câu trả lời → latency/token) | Nếu là distributed tracing OTel giữa các service: v1 chỉ có 2 service nên giá trị thấp; xem §4.F |
| GĐ-3 | Runtime giữ **provider-agnostic**; Bedrock chỉ để benchmark embedding, không là dependency bắt buộc | Không gọi Bedrock/full corpus hoặc tạo chi phí nếu chưa có phê duyệt riêng; local smoke vector chỉ kiểm tra plumbing, không dùng làm số đo retrieval |
| GĐ-4 | Dùng **bản 2018** đang có | Chuyển sang bản 2022 = chạy lại toàn bộ ingestion + validate lại từ đầu; **không khả thi trong 2 tuần** |
| GĐ-5 | Câu hỏi runtime **có thể chứa thông tin bệnh nhân** | Nội dung sách là tài liệu công khai nên embedding offline không rò rỉ gì; nhưng **câu hỏi của bác sĩ thì có thể** — cần quyết định chính sách trước khi mở cho người thật dùng `[chờ xác nhận]` |
---
## 3. Kiến trúc v1
```
[ web (Next.js) ] ──HTTP──> [ ai-service (FastAPI) ] ──> Qdrant (chunk + vector)
│ └─> Postgres (hội thoại + trace)
└──> provider cấu hình (không bắt buộc Bedrock)
[ ingestion CLI ] (offline, chạy tay) ──> Qdrant
```
Luồng trả lời, chế độ A (biết tên thuốc — chiếm phần lớn câu hỏi):
```
câu hỏi
→ resolve thực thể (khớp chính xác dài nhất trên bảng tên+alias) → drug_id
→ phân loại ý định → section_key (+ population nếu là câu hỏi liều)
→ LẤY TẤT CẢ chunk của (drug_id, section_key) từ Qdrant bằng FILTER, không phải bằng vector
→ ghép lại thành section đầy đủ
→ nếu section có attachment quarantine → kèm ảnh crop, và cấm mô hình đọc số từ đó
→ LLM soạn câu trả lời, bắt buộc trích: tên thuốc + tên mục + số trang IN
```
Luồng chế độ B (biết khái niệm, không biết thuốc — "thuốc nào trị tăng huyết áp"):
```
câu hỏi → embedding → vector search CHỈ trên section_key ∈ {chi_dinh, duoc_ly}
→ gom theo drug_id → trả DANH SÁCH thuốc ứng viên, không trả một thuốc
→ người dùng chọn → quay về chế độ A
```
Lý do chế độ B trả danh sách chứ không trả một thuốc: `chi_dinh` là field có
độ giống chéo cao nhất (median 0,408, 27,5% số thuốc có hàng xóm > 0,5). Với
nhóm PPI thì omeprazol và pantoprazol trùng chỉ định là **đúng y học** — trả
cả nhóm mới đúng.
---
## 4. Công việc chi tiết
Ký hiệu kích thước (ước lượng): **S** ≈ nửa buổi · **M** ≈ 1 buổi · **L**
1 ngày · **XL** ≈ 2 ngày.
### A. `ingestion/embed/` + `ingestion/load/`
Chunk record candidate là **schema v4**. `text` là văn bản retrieval có thể lặp
nhãn ngữ cảnh an toàn; `source_text` là đoạn nguồn liên tục dùng cho kiểm chứng và
reassembly. Payload còn có `context_labels`, `source_page_range`,
`printed_page_range`; mỗi attachment mang trang vật lý, trang in, `block_id`, `bbox`
và crop nếu có. Loader không tự suy luận provenance và từ chối mọi schema khác v4.
| # | Việc | File | Nghiệm thu | Size |
|---|---|---|---|---|
| A1 | Cổng embedding (interface) + adapter OpenAI, batch + retry + backoff | `ingestion/embed/ports.py`, `embed/openai_provider.py` | Test với provider giả, không gọi mạng | M |
| A2 | Cache embedding ra đĩa theo `chunk_id` + sha256(text) | `embed/cache.py`, `data/processed/embeddings.jsonl` | Chạy lần 2 không gọi lại API; đếm cache-hit = 100% | M |
| A3 | `cli embed` | `ingestion/cli.py` | In: số chunk, số token thật, số call, chi phí; ghi file | S |
| A4 | Schema collection Qdrant + adapter | `load/qdrant_repo.py` | Tạo collection, index payload cho `drug_id`, `section_key`, `atc_codes`, `chunk_kind` | M |
| A5 | `cli load` — upsert idempotent, point id sinh tất định từ `chunk_id` | `ingestion/cli.py`, `load/upsert.py` | Chạy 2 lần → số point không đổi | M |
| A6 | **Gắn corpus vào collection**: lưu sha256 của `chunks.jsonl` vào metadata collection | `load/qdrant_repo.py` | Gate: sha256 lệch → `cli load` từ chối chạy, không upsert lẫn lộn hai đời corpus | S |
**Khối lượng candidate**: 4.105.382 token (đo bằng `cl100k_base`). Đơn giá phải
tra bảng giá hiện hành trước khi chạy — không trích từ trí nhớ. Đây là hạng
mục phải có phê duyệt riêng dù ước tính nhỏ.
Hai thiếu hụt từng chặn embedding — trang in và ngữ cảnh đối tượng/đường dùng —
đã được xử lý trong schema v4. Chỉ được coi là xong khi audit toàn corpus trên
artifact canonical xác nhận range chính xác và mọi chunk continuation giữ đủ
nhãn ngữ cảnh.
### B. Tầng thực thể / alias — **làm sớm nhất, zero-regret**
| # | Việc | File | Nghiệm thu | Size |
|---|---|---|---|---|
| B1 | Trích 344 dòng `X - xem Y` từ back index thành bảng alias | `ingestion/validation/back_index.py` (thêm hàm mới, **không** đổi `parse_back_index` đang dùng cho validate) | Đếm ra đúng 344 (đo); test hồi quy | M |
| B2 | Gom tên biệt dược từ 492 mục `ten_thuong_mai` | `ingestion/segment/` hoặc module mới `entities/` | Đếm được số alias thu thêm | M |
| B3 | Xuất `data/verified/drug_entities.json`: 683 tên chuẩn + alias + 1.043 mã ATC → `drug_id` | mới | Mọi `drug_id` phải tồn tại trong `monographs.jsonl`; 0 alias mồ côi | M |
| B4 | Bộ resolve **khớp chính xác dài nhất**, có test cho **19 cái bẫy substring** | `entities/resolver.py` | `HYDROCLOROTHIAZID` không ra `CLOROTHIAZID`; `PSEUDOEPHEDRIN` không ra `EPHEDRIN`; `DESLORATADIN` không ra `LORATADIN`; `HOMATROPIN HYDROBROMID` không ra `ATROPIN` | L |
### C. `apps/ai-service`
Cấu trúc theo Clean Architecture (`CLAUDE.md`): domain không import SDK.
| # | Việc | File | Nghiệm thu | Size |
|---|---|---|---|---|
| C1 | Khung FastAPI + `/health` + config qua env | `main.py`, `config.py` | `curl /health` | S |
| C2 | Cổng (interface): `VectorStore`, `Embedder`, `Chat`, `PageRenderer` | `domain/ports.py` | Domain test chạy không cần dịch vụ sống | M |
| C3 | Adapter Qdrant / OpenAI embed / OpenAI chat / PyMuPDF render | `adapters/` | Test tích hợp riêng, đánh dấu `@pytest.mark.integration` | L |
| C4 | Hiểu truy vấn: tách thực thể thuốc (B4) + phân loại `section_key` + nhận diện đối tượng | `rag/understand.py` | Bộ test câu hỏi mẫu; ca không resolve được phải trả "không chắc", không đoán | L |
| C5 | Chế độ A: lấy theo **filter**, ghép section đầy đủ | `rag/retrieve.py` | Ghép lại đúng text section (so với `monographs.jsonl`) | M |
| C6 | Chế độ B: vector search giới hạn `section_key`, gom theo thuốc, trả danh sách | `rag/discover.py` | Trả ≥1 ứng viên cho câu hỏi chỉ định mẫu | M |
| C7 | Soạn câu trả lời + trích dẫn bắt buộc + từ chối khi không có căn cứ | `rag/answer.py` | Không có chunk → trả "không tìm thấy trong Dược thư", **không** để LLM tự bịa | L |
| C8 | Xử lý block quarantine: trả `attachment` + endpoint `/crop?page=&bbox=` render ảnh | `routers/crop.py` | Crop đúng vùng của `p109_t0` (ACETAZOLAMID, trang vật lý 109) | M |
| C9 | Lưu hội thoại + trace vào Postgres | `adapters/pg.py`, migration | Hỏi 1 câu → 1 hàng trace đọc lại được | M |
### D. `apps/web`
| # | Việc | Nghiệm thu | Size |
|---|---|---|---|
| D1 | Bỏ mock, gọi thật `ai-service` (giữ nguyên DTO trong `shared-types`) | Chat trả lời thật | M |
| D2 | Mở rộng `Citation`: thêm `printedPage`, `chunkId`, `attachment?` | Type check pass | S |
| D3 | Click trích dẫn → nhảy đúng trang PDF (trang **in**, không phải trang vật lý) | Kiểm bằng mắt 5 ca | M |
| D4 | Hiện ảnh crop cho block quarantine + nhãn cảnh báo "không trích số từ bảng này" | Kiểm bằng mắt trên 1 ca có bảng liều | M |
### E. Deploy
| # | Việc | Nghiệm thu | Size |
|---|---|---|---|
| E1 | `Dockerfile` cho `ai-service` | Build + chạy local | M |
| E2 | `Dockerfile` cho `web` (Next.js standalone) | Build + chạy local | M |
| E3 | Bổ sung 2 service vào `docker-compose.yml` | `docker compose up` ra bản chạy đầy đủ local | M |
| E4 | Nạp dữ liệu Qdrant: chạy `cli embed` + `cli load` qua port-forward, viết runbook | `docs/runbooks/load-qdrant.md` (thư mục đang rỗng) | M |
| E5 | Helm templates: deployment/service/ingress cho 2 app + Qdrant (statefulset + PVC) | `helm template` render sạch | XL |
| E6 | `values-dev.yaml` thật + Secret cho OpenAI key (**không commit key**) | Secret tạo bằng tay hoặc sealed-secret | M |
| E7 | Gỡ 3 `TODO` trong ArgoCD Application (project, repoURL, destination) | ArgoCD sync xanh | M |
| E8 | CI: build + test + push image + bump tag trong values | 1 lần chạy thật xanh | L |
**Ràng buộc đã ghi trong bộ nhớ dự án**: repo gitops nội bộ
(`git.vinmec.tech/ai-team/gitops`) là chỉ-đọc, **không đẩy gì lên đó**.
ArgoCD Application trong repo này trỏ về chính repo này.
### F. Tracing
Theo GĐ-2 (trace LLM/RAG). Đề xuất **làm theo 2 mức, mức 1 trước**:
| Mức | Nội dung | Size |
|---|---|---|
| **1 — bắt buộc** | Mỗi request sinh `trace_id`; ghi Postgres: câu hỏi, thực thể resolve được, `section_key`, danh sách `chunk_id` lấy ra, prompt gửi đi, câu trả lời, token in/out, latency từng bước, có/không dùng block quarantine. Kèm endpoint nội bộ `/traces/{id}` đọc lại | L |
| **2 — nếu còn thời gian** | Self-host Langfuse hoặc export OTel sang stack sẵn có của team | XL |
Nói thẳng: **mức 2 không phải một buổi chiều.** Langfuse bản mới cần thêm
Clickhouse + Redis + object storage — đó là một hạng mục triển khai riêng.
Mức 1 phục vụ đúng mục đích thật (debug một câu trả lời y khoa sai thì truy
ngược được tới chunk và trang nào), và nó là thứ hợp với văn hoá provenance
của dự án này.
### G. Eval + gate
Tách đôi, không gộp:
| # | Việc | Nghiệm thu | Size |
|---|---|---|---|
| G1 | **Eval định tuyến** — sinh tự động từ chính corpus: với mỗi (thuốc, field) tạo truy vấn mẫu, kiểm hệ có trả đúng `drug_id` + `section_key`. Ground truth suy ra từ dữ liệu, **không bịa một câu nào** | Báo cáo % đúng; không đặt mục tiêu giả | L |
| G2 | **Tập đối kháng** — các cặp confusable đã đo (PPI, penicilin, digoxin/digitoxin, estriol/estron, contrast media, nitrit/thiosulfat) | Gate `wrong_drug_returned` = **0** | M |
| G3 | Truy vấn bằng **tên biệt dược** trên 344 alias | Gate `brand_name_query_unresolved` = 0 | M |
| G4 | **Eval nội dung** — cần dược sĩ/bác sĩ chấm | **Không tự làm được.** Xem §8 | — |
---
## 5. Lịch 2 tuần (ước lượng, không phải cam kết)
Nguyên tắc xếp lịch: **sau mỗi ngày phải luôn có thứ demo được**, để nếu
trễ thì trễ ở phần đuôi chứ không phải mất trắng.
| Ngày | Nội dung | Cuối ngày có gì |
|---|---|---|
| 1 | Gate chunk v4: seam label/liều, embargo descriptor, provenance, schema fail-closed | Mọi readiness gate bằng 0; artifact canonical + SHA được chốt |
| 2 | B1-B4 (thực thể/alias) + smoke A4-A6 bằng vector giả | Gõ "Panadol" ra `paracetamol`; local Qdrant load đủ 15.100 point, idempotent, rồi dọn collection test |
| 3 | C1-C3 (khung + cổng + adapter) | `/health`, gọi được Qdrant + OpenAI |
| 4 | C4-C5 (hiểu truy vấn + chế độ A) | Hỏi "chống chỉ định metformin" ra đúng section qua HTTP |
| 5 | C7 + C9 (soạn câu trả lời + trace mức 1) | Câu trả lời có trích dẫn, có trace đọc lại được |
| 6 | D1-D3 (web nối thật) | **Demo đầu tiên end-to-end trên máy local** |
| 7 | C6 + C8 + D4 (chế độ B + crop bảng) | Hỏi theo chỉ định ra danh sách; bảng liều hiện ảnh |
| 8 | G1-G3 (eval + 3 gate) | Có số thật về độ đúng định tuyến |
| 9 | E1-E4 | `docker compose up` ra bản đầy đủ; runbook nạp dữ liệu |
| 10 | E5-E7 | Chạy trên k3s qua ArgoCD |
| Dự phòng | E8 (CI), F mức 2, vá lỗi | |
Embedding thật không được gắn cứng vào “ngày 2”: chỉ chạy sau khi gate ngày 1
đã pass và chủ dự án phê duyệt provider, model, phạm vi và chi phí.
**Không có ngày trống trong 10 ngày.** Đây là rủi ro số 1 của kế hoạch: mọi
sự cố đều ăn thẳng vào phần đuôi (CI, tracing mức 2).
---
## 6. Gate nghiệm thu v1
Theo phong cách sẵn có của dự án — có tên, có mục tiêu bằng 0.
| Gate | Mục tiêu | Đo bằng |
|---|---|---|
| `wrong_drug_returned` (tập đối kháng) | **0** | G2 |
| `answer_without_citation` | 0 | G1 |
| `dose_stated_from_quarantined_block` | 0 | rà tay trên các ca có attachment |
| `citation_uses_physical_page` (phải là trang **in**) | 0 | G1 |
| `brand_name_query_unresolved` (344 alias) | 0 | G3 |
| `qdrant_point_count ≠ chunk_count` | 0 | A5 |
| `collection_corpus_sha_mismatch` | 0 | A6 |
| Độ đúng định tuyến (thuốc, field) | **báo số thật**, không đặt ngưỡng giả | G1 |
| p95 latency | **đo rồi báo**, không hứa trước | tracing mức 1 |
---
## 7. Rủi ro, xếp theo mức độ
1. **Segmentation đang bị viết lại (Codex, ngay lúc này).** Nếu `assembler/
detector/vocab` đổi thì `chunks.jsonl` đổi, và **mọi embedding đã trả
tiền phải tính lại**. → Không chạy `cli embed` cho tới khi bản mới qua
đủ: 164 test, 18/18 gate, `cli validate` ≥ 96,0%/99,1%, và so sha256
output với mốc đã lưu. Mốc: `monographs 84f41d96…`, `chunks 63472db4…`.
2. **Helm viết từ trống (E5).** Không có gì để copy trong repo. Đây là hạng
mục dễ vỡ tiến độ nhất sau #1.
3. **Tracing mức 2 phình ra.** → Chốt cứng: mức 1 là bắt buộc, mức 2 chỉ
làm nếu ngày dự phòng còn trống.
4. **Một người, 10 ngày, không có slack.** → Thứ tự trong §5 đã xếp sao cho
ngày 6 đã có demo; nếu trễ thì trễ ở CI/tracing chứ không mất demo.
5. **Chưa có ai chấm nội dung y khoa.** Gate ở §6 chứng minh hệ *lấy đúng
mục của đúng thuốc* — **không** chứng minh câu trả lời đúng về y học.
---
## 8. Ngoài phạm vi v1 — nói thẳng, không giấu
- `api-gateway`, `auth-service`, `user-service`, `chat-service` (§1).
- **Các chương tổng quát (in tr. 37-98) và phụ lục (in tr. 1497-1528)** vẫn
chưa vào corpus. Hỏi "Kê đơn thuốc", "Ngộ độc và thuốc giải độc" sẽ **không
ra gì**. Cần nói trước với người dùng thử.
- Benchmark chọn embedding model (bge-m3 vs multilingual-e5 vs provider khác).
Runtime vẫn provider-agnostic; chưa chọn provider/model cho full corpus và
không được gọi dịch vụ có chi phí khi chưa có phê duyệt riêng.
- Tái dựng bảng 2D và nomogram — vẫn quarantine, chỉ hiện ảnh.
- Đánh giá nội dung y khoa (G4): **bắt buộc có dược sĩ/bác sĩ chấm.** Tôi tự
viết câu hỏi rồi tự chấm thì chỉ đo được trí tưởng tượng của mình, không
đo được thực tế lâm sàng — đúng loại bằng chứng giả mà `CLAUDE.md` cấm.
- Bản Dược thư 2022 (xuất bản lần 3). Bản đang dùng là 2018.
- Mobile app.
---
## 9. Số nào đo, số nào đoán
**Baseline lịch sử đã đo (2026-08-03, không dùng để load/embedding):** toàn bộ
bảng §0; 15.076 chunk; 4.072.725 token
`cl100k_base`; 683/11.966/8.212.880; recall 96,0% (677/705), precision
99,1%; 18/18 gate; 164 test; 167 block quarantine (129 trong phần liều);
344 alias `- xem`; 401 cụm cross-reference; 492 mục `ten_thuong_mai`; 1.043
mã ATC; 19 tên thuốc là substring của tên khác; bảng cosine chéo giữa các
thuốc; schema chunk v2. Các số này đã bị candidate schema v4 ở đầu tài liệu
thay thế và chỉ còn giá trị đối chiếu lịch sử.
**Chưa đo, là phỏng đoán:** mọi ước lượng thời gian ở §4 và §5; chi phí
embedding; p95 latency; độ khó thật của E5 (Helm) và F mức 2 (Langfuse);
tỷ lệ câu hỏi rơi vào chế độ A so với chế độ B.
**Chưa biết, chờ người quyết:** GĐ-1, GĐ-2, GĐ-5 ở §2.