Fix the F3 out-of-scope gate, close out the V1 feature audit, and clean up project docs
Also drop .github/ (GitHub-specific CI/CD workflows and ArgoCD operational scripts) from this mirror -- Gitea auto-picked up .github/workflows/*.yml as Actions and queued a run against secrets that don't exist here. Not meaningful outside the GitHub-hosted repo anyway.
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
# Dựng lại dự án này từ đầu
|
||||
|
||||
Tài liệu này ghi **toàn bộ quá trình**: từ một file PDF tới một chatbot RAG chạy
|
||||
trên k3s. Viết cho người chưa từng đụng repo — đọc xong dựng lại được.
|
||||
|
||||
Mọi con số ở đây đều **đo được**, không phải ước lượng. Chỗ nào chưa kiểm chứng
|
||||
thì ghi rõ là chưa.
|
||||
|
||||
---
|
||||
|
||||
## 0. Bức tranh tổng thể
|
||||
|
||||
```
|
||||
PDF (1.668 trang)
|
||||
│ ingestion/ — bóc chữ, phân đoạn, chunk
|
||||
▼
|
||||
684 chuyên luận → 15.100 chunk
|
||||
│ embed cohere-v4 (Bedrock), 1024 chiều
|
||||
▼
|
||||
Qdrant collection duocthu_v1
|
||||
│
|
||||
▼
|
||||
apps/ai-service ── hiểu câu hỏi → truy hồi → sinh → đối chiếu → trích dẫn
|
||||
│
|
||||
▼
|
||||
apps/web (Next.js BFF) → realvuxbaro.me
|
||||
```
|
||||
|
||||
Hạ tầng: k3s + ArgoCD trên một EC2. CI tự build image, đẩy GHCR, trỏ ArgoCD sang
|
||||
tag mới. Quan sát: OpenTelemetry → Tempo + Langfuse, Prometheus + Grafana.
|
||||
|
||||
---
|
||||
|
||||
## 1. Nguồn dữ liệu
|
||||
|
||||
**Dược thư Quốc gia Việt Nam 2018**, `ingestion/data/raw/*.pdf` (38 MB, 1.668 trang).
|
||||
|
||||
Chỉ **Phần 2 (chuyên luận thuốc), trang 99–1496** được đưa vào corpus. Phần 1
|
||||
(hướng dẫn chung, ngộ độc, tương tác) và Phần 3 (phụ lục BSA, ATC) **cố ý loại
|
||||
ra** — mọi câu hỏi rơi vào hai phần đó sẽ bị từ chối, và đó là đúng thiết kế.
|
||||
|
||||
Mỗi chuyên luận có tối đa **18 mục** (`ten_chung_quoc_te`, `chi_dinh`,
|
||||
`chong_chi_dinh`, `lieu_luong_va_cach_dung`, `tuong_tac_thuoc`, …). Danh sách
|
||||
chuẩn nằm ở `apps/ai-service/rag/sections.py` (`SECTION_ORDER`).
|
||||
|
||||
> **Bẫy đã gặp:** `pdfplumber` bóc chữ **hỏng** trên tài liệu này. Một phần chữ
|
||||
> chỉ tồn tại dưới dạng **vector path**, không extractor nào trả về. Đã xử lý
|
||||
> bằng cách chuyển 51 đoạn vector-outlined (1.116 ký tự) trở lại luồng chữ.
|
||||
> Xem `docs-legacy/pdf-parsing-outlier-catalog.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ingestion — PDF thành chunk
|
||||
|
||||
Chạy bằng `python -m ingestion.cli` (trong thư mục `ingestion/`).
|
||||
|
||||
### 2.1 Dò bảng trước (chậm, có cache)
|
||||
|
||||
```bash
|
||||
python -m ingestion.cli detect-tables --pdf data/raw/duoc-thu-quoc-gia-viet-nam-2018.pdf
|
||||
# → data/processed/table_regions.json (183 vùng bảng trên 135 trang)
|
||||
```
|
||||
|
||||
Chạy một lần rồi dùng lại. **Đừng xoá file này** — dựng lại rất lâu.
|
||||
|
||||
### 2.2 Bóc chữ + phân đoạn thành chuyên luận
|
||||
|
||||
```bash
|
||||
python -m ingestion.cli run \
|
||||
--pdf data/raw/duoc-thu-quoc-gia-viet-nam-2018.pdf \
|
||||
--tables data/processed/table_regions.json \
|
||||
--out data/processed/monographs.jsonl
|
||||
```
|
||||
|
||||
Kết quả đo được (2026-08-24): 253.518 span → **684 chuyên luận**, 151 khối bảng
|
||||
được **tách khỏi văn xuôi và cách ly** (quarantine).
|
||||
|
||||
> **Hợp đồng quarantine — bắt buộc phải hiểu:** bảng và công thức bị lấy ra khỏi
|
||||
> prose. Tầng trả lời **được phép trưng ảnh crop nguồn**, nhưng **tuyệt đối không
|
||||
> được tự phát biểu một liều lấy từ đó**. Đây là quyết định an toàn, không phải
|
||||
> giới hạn kỹ thuật.
|
||||
|
||||
### 2.3 Các cổng kiểm trước khi chunk
|
||||
|
||||
```bash
|
||||
python -m ingestion.cli residual-ink --pdf <pdf> --pages 202 # mực không span nào giải thích
|
||||
python -m ingestion.cli coverage --pdf <pdf> # span đi đâu về đâu
|
||||
python -m ingestion.cli validate --pdf <pdf> # đối chiếu mục lục cuối sách
|
||||
python -m ingestion.cli chunk-ready --pdf <pdf> # cổng chặn rác đầu vào
|
||||
```
|
||||
|
||||
Nguyên tắc: **cổng có mục tiêu bằng 0**, ví dụ `unclassified = 0`. Nội dung không
|
||||
dựng lại được thì **cách ly**, không bao giờ âm thầm bỏ đi hoặc âm thầm giữ lại.
|
||||
|
||||
### 2.4 Chunk
|
||||
|
||||
```bash
|
||||
python -m ingestion.cli chunk \
|
||||
--monographs data/processed/monographs.jsonl \
|
||||
--tables data/processed/table_regions.json \
|
||||
--pdf data/raw/duoc-thu-quoc-gia-viet-nam-2018.pdf \
|
||||
--out data/processed/chunks.jsonl
|
||||
```
|
||||
|
||||
**15.100 chunk** — 14.949 prose + 151 block_descriptor. 0 chunk vượt trần 800
|
||||
token. Tổng 4.105.382 token (cl100k_base).
|
||||
|
||||
> **Đã kiểm 2026-08-24:** dựng lại từ PDF cho ra **681/684 chuyên luận giống hệt
|
||||
> từng byte**. Pipeline có tính tất định.
|
||||
|
||||
---
|
||||
|
||||
## 3. Embed và nạp vào Qdrant
|
||||
|
||||
```bash
|
||||
python -m ingestion.load.run \
|
||||
--chunks data/processed/chunks.jsonl \
|
||||
--cache data/processed/embeddings \
|
||||
--provider cohere-v4 \
|
||||
--collection duocthu_v1 \
|
||||
--qdrant-url http://localhost:6333
|
||||
```
|
||||
|
||||
- Model: **cohere-v4** qua Bedrock, **1024 chiều, Cosine**
|
||||
- Cache khoá theo `(model_id, input_kind, text_sha256)` — **không** theo
|
||||
`chunk_id`. Nên sửa id không làm mất cache; sửa *chữ* mới làm mất.
|
||||
- Point id sinh từ `chunk_id` → nạp lại cùng corpus thì **hội tụ**, không nhân đôi
|
||||
|
||||
### Hai chốt an toàn phải biết
|
||||
|
||||
1. **Manifest guard.** Nạp corpus có `sha256` khác vào collection đang có dữ liệu
|
||||
sẽ **bị từ chối TRƯỚC khi ghi** (`CorpusMismatch`). Production không thể hỏng
|
||||
vì ai đó chạy nhầm lệnh nạp.
|
||||
2. **Count gate.** Số điểm trong collection phải khớp số chunk, lệch là FAIL.
|
||||
|
||||
> **Muốn đổi corpus trên production: đừng ghi đè.** Nạp vào collection **mới**
|
||||
> (`duocthu_v2`) rồi đổi `aiService.config.qdrantCollection`. Lý do: ArgoCD roll
|
||||
> back được **cấu hình**, **không** roll back được **dữ liệu**. Cách này biến một
|
||||
> cuộc di trú dữ liệu thành một thay đổi cấu hình.
|
||||
|
||||
---
|
||||
|
||||
## 4. ai-service — đường đi của một câu hỏi
|
||||
|
||||
`POST /v1/rag/query` → `apps/ai-service/rag/agent.py`:
|
||||
|
||||
| Chặng | Làm gì | Thời gian thật (production) |
|
||||
|---|---|---|
|
||||
| `understanding` | LLM sinh `QueryFrame` (turn_type, thuốc, mục, bối cảnh bệnh nhân) | ~5–6s |
|
||||
| `routing` | Chọn nhánh theo `turn_type` | ~2–3s |
|
||||
| `retrieval` | Embed câu hỏi → tìm trong Qdrant | ~0,2–0,3s |
|
||||
| `rerank` | Cohere rerank | ~0,14s |
|
||||
| `generation` | Sinh câu trả lời có trích dẫn | ~1,5–3s |
|
||||
| `entailment` | **Đối chiếu từng mệnh đề với nguồn** | ~0,7–1,8s |
|
||||
|
||||
Tổng một lượt bình thường: **13–17 giây**.
|
||||
|
||||
### Điểm quan trọng nhất về kiến trúc
|
||||
|
||||
Sau khi LLM sinh `QueryFrame`, có **một chuỗi hàm vá hậu kỳ** ghi đè kết quả dựa
|
||||
trên **danh sách chuỗi tiếng Việt cứng** (`_apply_condition_candidate_cue`,
|
||||
`_apply_named_drug_cues`, …).
|
||||
|
||||
**Đây là điểm yếu lớn nhất của hệ thống.** Nó khiến hệ trả lời đúng khi người dùng
|
||||
gõ đúng câu trong bộ eval, và khác đi khi gõ cách khác. Đã chứng minh: cùng một ý
|
||||
hỏi 4 cách → 3 quyết định khác nhau.
|
||||
|
||||
> **Luật khi sửa:** thêm cue là **nuôi bệnh**. Chuyển tri thức vào
|
||||
> **prompt + schema + một cổng tất định**, để lớp cue **teo đi**. Ví dụ mẫu:
|
||||
> commit `1a6e6eb` (sửa lỗi phạm vi bằng trường `unsupported_request`).
|
||||
|
||||
### Entailment là lưới an toàn thật sự
|
||||
|
||||
Đã cứu ít nhất một lần thật: câu *"Bệnh nhân sốt cao dùng thuốc gì?"* truy hồi
|
||||
nhầm ra `dantrolen`/`halothan` (thuốc của **sốt cao ác tính**). Generation dựng
|
||||
câu trả lời trên bằng chứng sai, **entailment bác** → abstain.
|
||||
|
||||
Không có nó, hệ thống đã bảo bác sĩ dùng dantrolen để hạ sốt.
|
||||
|
||||
---
|
||||
|
||||
## 5. Chạy local
|
||||
|
||||
```bash
|
||||
# 1. Qdrant
|
||||
docker run -d --name qdrant -p 6333:6333 qdrant/qdrant:v1.19.0
|
||||
python -m ingestion.load.run --chunks ... --collection duocthu_v1 --qdrant-url http://localhost:6333
|
||||
|
||||
# 2. ai-service (mặc định đã trỏ localhost:6333 / duocthu_v1)
|
||||
cd apps/ai-service && python -m uvicorn main:app --host 127.0.0.1 --port 8099
|
||||
```
|
||||
|
||||
Cần AWS credentials có quyền gọi Bedrock (`aws sts get-caller-identity` để kiểm).
|
||||
|
||||
> **Bẫy Windows:** không dùng `--reload` (server không nạp lại đúng, phải
|
||||
> **restart tay**). Git Bash làm hỏng đường dẫn kiểu `origin/master:path` → phải
|
||||
> `export MSYS_NO_PATHCONV=1`. Console tiếng Việt lỗi mã →
|
||||
> `export PYTHONIOENCODING=utf-8`.
|
||||
|
||||
> **Local KHÔNG đo được hiệu năng.** Máy ở Việt Nam gọi Bedrock `us-east-1` nên
|
||||
> trung vị **44s/câu** so với ngưỡng budget **40s** → nhiều case đỏ vì mạng chứ
|
||||
> không vì code. Muốn số thật phải chạy trên production.
|
||||
|
||||
---
|
||||
|
||||
## 6. Triển khai
|
||||
|
||||
```
|
||||
push master (chạm apps/** hoặc packages/**)
|
||||
→ .github/workflows/build-practice-images.yml
|
||||
→ build 4 image, đẩy GHCR theo tag = commit SHA
|
||||
→ sync_practice_argocd.py trỏ Application sang tag mới
|
||||
→ chờ Synced + Healthy + đúng SHA
|
||||
→ xác nhận realvuxbaro.me đang phục vụ bản mới
|
||||
```
|
||||
|
||||
Workflow có **`paths:` filter** — sửa tài liệu **không** kích hoạt build.
|
||||
|
||||
### Roll back
|
||||
|
||||
```bash
|
||||
gh workflow run rollback-k3s.yml -f target_sha=<sha cũ>
|
||||
```
|
||||
|
||||
Không build lại, chỉ trỏ về image tag cũ đã có trên GHCR. Ba chốt: kiểm image tồn
|
||||
tại → chờ Synced/Healthy đúng SHA → **xác nhận trang thật đang phục vụ bản đã lùi**.
|
||||
|
||||
| Roll back được | Không roll back được |
|
||||
|---|---|
|
||||
| Code, prompt, model id, config | **Dữ liệu Qdrant** |
|
||||
| (đều nằm trong image / Helm values) | **Dữ liệu Postgres** |
|
||||
|
||||
---
|
||||
|
||||
## 7. Đánh giá chất lượng
|
||||
|
||||
```bash
|
||||
# 90 case bất biến (quyết định, trích dẫn, đúng thuốc)
|
||||
python scripts/run_all_evals.py --base-url https://realvuxbaro.me --output-dir out/
|
||||
|
||||
# chấm chất lượng câu trả lời (venv RIÊNG — ragas phá vỡ deps của service)
|
||||
<venv_ragas>/python scripts/score_evals_ragas.py ...
|
||||
|
||||
# ổn định theo cách diễn đạt — rẻ, không cần LLM judge
|
||||
python scripts/paraphrase_probe.py --base-url https://realvuxbaro.me
|
||||
```
|
||||
|
||||
**Số chốt 2026-08-24 (production): 84/90**, trung vị 13,8s.
|
||||
|
||||
### Bài học về phương pháp đo — đọc trước khi tin bất kỳ con số nào
|
||||
|
||||
1. **Đừng để judge trùng model với generator** — nó tự thiên vị. Đã đổi sang Mistral Large 3 (một model độc lập, khác hẳn cả generator lẫn model được thử đầu tiên nhưng không dùng được trên account này).
|
||||
2. **Đừng sinh câu hỏi eval từ chính chunk mà nó kiểm** — điểm sẽ đẹp giả.
|
||||
3. **`context_precision` chỉ tái lập được 52% theo từng case** — một judge đơn gần
|
||||
như tung đồng xu. Chỉ tin khi hai judge đồng thuận.
|
||||
4. **Bộ 90 case có thể bị "học thuộc"** vì định tuyến chạy trên danh sách chuỗi
|
||||
cứng. `paraphrase_probe.py` mới là thứ phát hiện được điều đó — và nó đã tìm ra
|
||||
2 lỗi thật mà bộ 90 case không thấy. **Câu diễn đạt lại phải viết tay**: bảo
|
||||
model paraphrase thì nó giữ nguyên từ khoá điều khiển định tuyến, đúng thứ cần
|
||||
phải thay đổi.
|
||||
5. **Nghiệm thu phải chạy end-to-end qua `/api/chat`**, không bao giờ nghiệm thu
|
||||
trên tầng understanding tách rời. PR #54 "verify 3/3" trên bản cô lập rồi vẫn
|
||||
hỏng trên production.
|
||||
|
||||
---
|
||||
|
||||
## 8. Quan sát
|
||||
|
||||
- **Langfuse** (`langfuse.realvuxbaro.me`) — trace từng chặng, kèm điểm Ragas và
|
||||
thumbs của người dùng. **Đây là chỗ mở ra khi không rõ tầng nào hỏng.**
|
||||
- **Tempo / Prometheus / Grafana** (`realvuxbaro.me/grafana/`)
|
||||
|
||||
> **Bài học lặp lại 2 lần trong một ngày:** `reason` code **không** cho biết tầng
|
||||
> nào hỏng. `unsupported_claim` trông như lỗi grounding, mở trace ra mới thấy lỗi
|
||||
> ở **truy hồi**. `evidence_insufficient` trông như không liên quan tới một thay
|
||||
> đổi về phạm vi, hoá ra chính nó gây ra. **Mở trace, đừng suy từ mã lỗi.**
|
||||
|
||||
> **Đừng bao giờ trỏ Pod tới hostname công khai của chính cụm nó** — hairpin
|
||||
> routing làm trace bị nuốt im lặng. Dùng Service nội bộ.
|
||||
|
||||
---
|
||||
|
||||
## 9. Những chỗ vẫn đang hỏng (2026-08-24)
|
||||
|
||||
| Lỗi | Bản chất |
|
||||
|---|---|
|
||||
| 3 chuyên luận `Đ` sai `drug_id` | Slugifier nuốt chữ `Đ`. Code đã sửa, **corpus chưa nạp lại** — đúng 65/15.100 chunk cần đổi |
|
||||
| Định tuyến lệch theo cách diễn đạt | 4 cách hỏi → 3 quyết định. Bệnh gốc: lớp cue cứng |
|
||||
| Truy hồi "sốt cao" | Hiểu thành **sốt cao ác tính** → dantrolen/halothan |
|
||||
| Mục > 2.000 token | 287 mục (2%) làm đổ lượt gọi Bedrock: `read_timeout=20` × 2 lần > budget 40s → `provider_unavailable` |
|
||||
| F3 chưa đạt 100% | Cổng **tất định**, nhưng **trigger là phán đoán LLM** → ~1 trượt/35 lần. Muốn 100% thì trigger cũng phải tất định |
|
||||
|
||||
---
|
||||
|
||||
## 10. Nhật ký phát triển
|
||||
|
||||
`docs-legacy/progress-log.md` (~326 KB) ghi **vì sao** mọi thứ thành ra như hiện
|
||||
tại: các số đo, các ngõ cụt, các quyết định bị lật lại. Đọc code không suy ra được
|
||||
phần đó. Đọc nó trước khi định lật lại một quyết định nào.
|
||||
+1
-2
@@ -10,8 +10,7 @@ phía app không bao giờ đụng vào dữ liệu). Cả hai đặt `syncPolic
|
||||
`selfHeal` + `prune` — **mọi merge vào `master` áp thẳng vào production, không
|
||||
có cổng duyệt thủ công.** EC2 Docker Compose (`52.0.158.61`) đã **stop** từ
|
||||
2026-08-18, không còn nhận deploy tự động và không còn là đường lui sống; xem
|
||||
`coordination/CLAUDE_PLAN_CICD_SAFETY_2026-08-18.md` cho lý do và mục Rollback
|
||||
bên dưới cho cách khởi động lại nếu cần.
|
||||
mục Rollback bên dưới cho cách khởi động lại nếu cần.
|
||||
|
||||
## Deploy
|
||||
|
||||
|
||||
Reference in New Issue
Block a user