367 lines
24 KiB
Markdown
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.
|