1785 lines
56 KiB
Markdown
1785 lines
56 KiB
Markdown
# Pipeline từ PDF Dược thư đến chatbot RAG production
|
||
|
||
> **Tài liệu canonical cho pipeline end-to-end.** Nội dung mô tả logic hiện hành
|
||
> từ PDF, parsing, chunking và indexing đến query understanding, retrieval,
|
||
> generation, grounding, citation và response. Khi tài liệu cũ mâu thuẫn với
|
||
> file này, đối chiếu code runtime; code là nguồn sự thật cuối cùng.
|
||
>
|
||
> Mục tiêu của tài liệu là giải thích được toàn bộ chuỗi xử lý: PDF được đọc và
|
||
> kiểm tra như thế nào, dữ liệu được phân đoạn/chunk/embedding ra sao, chatbot
|
||
> truy xuất và tạo câu trả lời thế nào, hệ thống chống hallucination bằng gì,
|
||
> eval từng tầng ra sao, đã đo được gì và tuyệt đối chưa được khẳng định điều gì.
|
||
|
||
## 0. Tóm tắt trong một phút
|
||
|
||
Hệ thống là một chatbot RAG tra cứu **Dược thư Quốc gia Việt Nam 2018**. Nó
|
||
không đưa toàn bộ PDF cho LLM và cũng không dùng LLM để “đọc PDF mỗi lần hỏi”.
|
||
PDF được xử lý **offline một lần** thành dữ liệu có cấu trúc theo:
|
||
|
||
```text
|
||
thuốc -> mục chuyên luận -> chunk -> vector + metadata + provenance
|
||
```
|
||
|
||
Các con số của corpus đang dùng:
|
||
|
||
| Hạng mục | Giá trị đã đo |
|
||
|---|---:|
|
||
| PDF nguồn | 1.668 trang, 38.795.771 byte |
|
||
| Phạm vi đưa vào RAG | Phần 2, chuyên luận thuốc, trang in 99–1496 |
|
||
| Chuyên luận được segment | 684 |
|
||
| Section | 11.974 |
|
||
| Ký tự section | 8.213.036 |
|
||
| Chunk tổng | 15.100 |
|
||
| Prose chunk | 14.949 |
|
||
| Block descriptor cho bảng/công thức | 151 |
|
||
| Tổng token chunk (`cl100k_base`) | 4.105.382 |
|
||
| Chunk vượt trần 800 token | 0 |
|
||
| Embedding | Cohere Embed v4, 1.024 chiều |
|
||
| Vector DB | Qdrant, cosine distance, collection `duocthu_v1` |
|
||
| Model hiểu câu hỏi/tạo đáp án | Cấu hình qua AWS Bedrock Converse; model production không được suy ra từ file local |
|
||
| Reranker production | Cohere Rerank 3.5 cho các nhánh cần rerank |
|
||
|
||
Khi có câu hỏi, chatbot ưu tiên **lọc chính xác bằng metadata** nếu đã biết
|
||
thuốc và section. Vector search chỉ là fallback có giới hạn. LLM chỉ được phép
|
||
diễn đạt lại evidence đã truy xuất và phải trả claim có citation. Sau đó code
|
||
kiểm tra citation, số liệu và một lượt semantic entailment; không qua gate thì
|
||
abstain, không hiển thị đáp án chưa kiểm chứng.
|
||
|
||
Điểm quan trọng nhất khi thuyết trình:
|
||
|
||
> “Embedding giúp tìm candidate, nhưng không phải nguồn chân lý. Metadata,
|
||
> provenance và grounding mới là các rào chắn quyết định nội dung nào được phép
|
||
> tới người dùng.”
|
||
|
||
---
|
||
|
||
## 1. Phạm vi và tuyên bố trung thực
|
||
|
||
### 1.1. Nguồn dữ liệu
|
||
|
||
Nguồn hiện tại là file:
|
||
|
||
```text
|
||
ingestion/data/raw/duoc-thu-quoc-gia-viet-nam-2018.pdf
|
||
```
|
||
|
||
SHA-256 đã kiểm tra lại:
|
||
|
||
```text
|
||
2aa81c846a5e760f82658c46816ab63174204a7d95b3e2755b6565288e53d0d1
|
||
```
|
||
|
||
PDF gồm ba phần chính:
|
||
|
||
1. Phần 1 — các chuyên luận/hướng dẫn chung, trang in 37–98.
|
||
2. Phần 2 — các chuyên luận thuốc, trang in 99–1496.
|
||
3. Phần 3 — phụ lục, trang in 1497–1528; sau đó là back-index.
|
||
|
||
Corpus production hiện **chỉ bao phủ Phần 2**. Vì vậy câu hỏi về kê đơn chung,
|
||
ngộ độc, hướng dẫn dùng thuốc ở thai kỳ/suy gan thận ở cấp chương, bảng BSA,
|
||
pha thuốc tiêm hoặc phụ lục có thể phải abstain dù nội dung có tồn tại ở nơi
|
||
khác trong cuốn sách.
|
||
|
||
### 1.2. Giới hạn lâm sàng bắt buộc phải nói rõ
|
||
|
||
`source_manifest.json` ghi nhận:
|
||
|
||
- bản đang dùng là lần xuất bản thứ hai, năm 2018;
|
||
- đã có lần xuất bản thứ ba năm 2022;
|
||
- quyền sử dụng production chưa được ghi nhận đầy đủ;
|
||
- nguồn chưa được đánh dấu đủ điều kiện làm nguồn duy nhất cho clinical
|
||
production.
|
||
|
||
Do đó website đang chạy production theo nghĩa **hạ tầng/phần mềm đã deploy**,
|
||
không đồng nghĩa đã được duyệt là một clinical decision support system.
|
||
|
||
Hệ thống phù hợp để demo kỹ thuật, tra cứu có nguồn và hỗ trợ chuyên gia kiểm
|
||
tra. Nó không được tuyên bố:
|
||
|
||
- thay thế bác sĩ/dược sĩ;
|
||
- đưa phác đồ điều trị chuẩn hoặc thuốc first-line;
|
||
- chứng minh thuốc “an toàn/phù hợp” chỉ vì tìm thấy mục chỉ định;
|
||
- bao phủ đầy đủ Dược thư 2022 hoặc toàn bộ nội dung Dược thư 2018;
|
||
- đã có đánh giá y khoa toàn corpus bởi hội đồng chuyên gia.
|
||
|
||
---
|
||
|
||
## 2. Kiến trúc tổng thể
|
||
|
||
Hệ thống có hai đường hoàn toàn tách nhau.
|
||
|
||
### 2.1. Đường offline: tạo corpus
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
PDF[PDF 1.668 trang]
|
||
EX[Extract spans + page map]
|
||
QA[Repair + PDF QA]
|
||
SEG[Segment monograph/section]
|
||
CH[Chunk schema v4]
|
||
EMB[Bedrock Cohere Embed v4]
|
||
QD[(Qdrant duocthu_v1)]
|
||
MF[(Manifest sidecar)]
|
||
|
||
PDF --> EX --> QA --> SEG --> CH --> EMB --> QD
|
||
CH --> MF
|
||
EMB --> MF
|
||
```
|
||
|
||
Đường này chạy theo batch. Nó không nằm trong request path của chatbot. Không
|
||
có chuyện người dùng hỏi rồi server mới parse 1.668 trang PDF.
|
||
|
||
### 2.2. Đường online: trả lời một câu hỏi
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
U[Browser]
|
||
C[Caddy TLS]
|
||
W[Next.js BFF]
|
||
F[FastAPI /v1/rag/query]
|
||
Q[Query understanding]
|
||
R[Routing + retrieval]
|
||
V[Evidence policy]
|
||
G[Structured generation]
|
||
D[Deterministic grounding]
|
||
E[Semantic entailment]
|
||
P[(PostgreSQL trace/history)]
|
||
O[Prometheus + Tempo]
|
||
|
||
U --> C --> W --> F --> Q --> R --> V --> G --> D --> E --> F --> W --> U
|
||
F --> P
|
||
F --> O
|
||
Q --> O
|
||
R --> O
|
||
G --> O
|
||
D --> O
|
||
E --> O
|
||
```
|
||
|
||
Production hiện tại là một EC2 `t3.large` chạy Docker Compose gồm Postgres,
|
||
Qdrant, ai-service, web và Caddy. Observability được bổ sung bằng Prometheus,
|
||
Grafana, Tempo và OpenTelemetry Collector.
|
||
|
||
---
|
||
|
||
## 3. Vì sao không thể chỉ `extract_text()` rồi chia đều
|
||
|
||
PDF này có nhiều đặc điểm khiến cách naive dễ tạo lỗi y dược:
|
||
|
||
- không có bookmark/TOC dùng được: `doc.get_toc()` trả 0 entry;
|
||
- tagged-PDF structure tree rất nông và không bao phủ đủ;
|
||
- bố cục hai cột;
|
||
- có trang PyMuPDF trả block cột phải trước cột trái;
|
||
- title thuốc có nhiều font size khác nhau;
|
||
- title dài có thể wrap qua nhiều dòng;
|
||
- header/footer chạy lặp trên gần toàn bộ sách;
|
||
- bảng và công thức 2D bị sai nghĩa nếu tuyến tính hóa;
|
||
- có chữ được vẽ bằng vector outline, không tồn tại trong text layer;
|
||
- có glyph PUA và một số hàng có thứ tự glyph bất thường;
|
||
- liều người lớn/trẻ em/suy thận có thể nằm sát nhau, nên cắt sai seam có thể
|
||
gán con số cho sai đối tượng.
|
||
|
||
Ví dụ nguy hiểm đã tìm thấy: công thức Cockcroft–Gault nếu flatten sai có thể
|
||
đọc thành phép nhân thay vì phép chia. Với dữ liệu liều, đây không phải lỗi
|
||
format mà là lỗi nội dung có khả năng gây hại.
|
||
|
||
Vì vậy pipeline dùng chiến lược:
|
||
|
||
1. giữ bbox, page, font và reading order càng lâu càng tốt;
|
||
2. chỉ bỏ cấu trúc sau khi đã chuyển nó thành provenance/metadata;
|
||
3. không tự tin tuyến tính hóa bảng/công thức chưa xác minh;
|
||
4. mọi bước đều có gate fail-closed trước khi embedding.
|
||
|
||
---
|
||
|
||
## 4. Bước 1 — Lập bản đồ trang in và trang vật lý
|
||
|
||
PDF có hai khái niệm trang:
|
||
|
||
- **physical page**: index trang trong file, dùng để render/crop bằng PyMuPDF;
|
||
- **printed page**: số trang in người đọc nhìn thấy trong sách, dùng trong
|
||
citation.
|
||
|
||
Hai số không được coi là giống nhau. `page_map.py` đọc folio ở header band để
|
||
xây mapping. Chunk chỉ được phát hành nếu provenance vật lý có thể ánh xạ sang
|
||
trang in hợp lệ.
|
||
|
||
Tại sao cần cả hai:
|
||
|
||
- UI cần physical page + bbox để cắt đúng ảnh nguồn;
|
||
- người dùng/mentor cần printed page để mở sách và đối chiếu;
|
||
- nếu chỉ dùng physical page, citation có thể lệch so với số trang in;
|
||
- nếu chỉ dùng printed page, code không biết crop tọa độ nào trong file.
|
||
|
||
Gate hiện tại yêu cầu mọi chunk có cả:
|
||
|
||
```json
|
||
{
|
||
"source_page_range": [100, 100],
|
||
"printed_page_range": [101, 101]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Bước 2 — Extract text thành luồng span liên tục
|
||
|
||
### 5.1. Công cụ chính
|
||
|
||
PyMuPDF (`fitz`) là extractor chính vì trên các trang kiểm tra thực tế nó giữ
|
||
reading order tốt hơn `pdfplumber.extract_text()`. `pdfplumber` chỉ được xem như
|
||
công cụ table-specific, không dùng cho body text.
|
||
|
||
Mỗi span giữ các trường quan trọng:
|
||
|
||
```text
|
||
physical_page, printed_page,
|
||
column, block, line, span_index,
|
||
x0, y0, x1, y1,
|
||
text, font, size
|
||
```
|
||
|
||
Đây là dữ liệu nền cho segmentation, crop và audit sau này.
|
||
|
||
### 5.2. Sắp xếp hai cột
|
||
|
||
Pipeline không tin mù quáng thứ tự block thô của PyMuPDF. Mỗi block được phân
|
||
loại thành:
|
||
|
||
- `full_width`;
|
||
- `left`;
|
||
- `right`;
|
||
- `unknown`.
|
||
|
||
Sau đó sort theo thứ tự:
|
||
|
||
```text
|
||
full_width header -> cột trái -> cột phải
|
||
```
|
||
|
||
và trong từng nhóm sort theo tọa độ `y`.
|
||
|
||
Lý do là whole-document comparison từng phát hiện 12/1.398 trang trong phạm vi
|
||
monograph có block cột phải xuất hiện trước cột trái. Nếu để nguyên, section của
|
||
thuốc sau có thể bị gắn vào thuốc trước.
|
||
|
||
### 5.3. Luồng cross-page
|
||
|
||
Extractor phát span như một luồng liên tục qua các trang, không biến mỗi trang
|
||
thành một document độc lập. Điều này cho phép:
|
||
|
||
- merge title wrap qua dòng/trang;
|
||
- giữ paragraph tiếp nối qua cột/trang;
|
||
- không cắt nội dung ở page boundary chỉ vì layout in ấn.
|
||
|
||
---
|
||
|
||
## 6. Bước 3 — Repair và kiểm tra nội dung PDF
|
||
|
||
### 6.1. Chuẩn hóa glyph và lỗi reading order
|
||
|
||
Pipeline có các bước kiểm tra/repair riêng cho:
|
||
|
||
- PUA glyph;
|
||
- replacement character `U+FFFD`;
|
||
- glyph có x-order đảo;
|
||
- text bị vẽ bằng vector outline;
|
||
- reading order cột;
|
||
- header/footer boilerplate.
|
||
|
||
Các run chữ vector outline được phát hiện bằng drawing path, sau đó dùng bộ
|
||
transcription đã xác minh để chèn lại thành synthetic span đúng vị trí. Có 51
|
||
run kiểu này từng được đọc bằng mắt và transcription, tổng 1.116 ký tự.
|
||
|
||
### 6.2. Coverage ledger
|
||
|
||
Mỗi span phải rơi vào đúng một trạng thái. Artifact hiện tại có:
|
||
|
||
| Trạng thái | Span | Ký tự |
|
||
|---|---:|---:|
|
||
| `normalized_text` | 177.767 | 8.183.293 |
|
||
| `out_of_scope` | 53.376 | 897.729 |
|
||
| `heading` | 12.723 | 220.915 |
|
||
| `boilerplate_excluded` | 4.977 | 47.599 |
|
||
| `quarantined` | 3.953 | 49.183 |
|
||
| `structural_excluded` | 3 | 53 |
|
||
| `unassigned` | 0 | 0 |
|
||
|
||
Ledger chứng minh “mọi span extractor tạo ra đều được định tuyến có tên”. Nó
|
||
không chứng minh extractor đã nhìn thấy mọi thứ trên trang, nên cần residual
|
||
ink.
|
||
|
||
### 6.3. Residual-ink QA
|
||
|
||
Quy trình:
|
||
|
||
1. render trang thành ảnh;
|
||
2. xóa/che mọi pixel nằm trong bbox của text span đã extract;
|
||
3. tìm phần mực còn lại;
|
||
4. phân loại vùng còn lại.
|
||
|
||
Artifact hiện tại có 3.931 vùng:
|
||
|
||
| Loại | Số vùng |
|
||
|---|---:|
|
||
| `header_rule` | 1.649 |
|
||
| `text_as_vector_outline` | 1.061 |
|
||
| `table_frame` | 959 |
|
||
| `antialias_speck` | 220 |
|
||
| `fraction_bar_candidate` | 23 |
|
||
| `rule_fragment` | 10 |
|
||
| `header_band_fragment` | 9 |
|
||
| `unclassified` | 0 |
|
||
|
||
Điểm cần nói đúng: `unclassified = 0` nghĩa là mọi vùng đã được **đặt tên**,
|
||
không có nghĩa toàn bộ content đã được người đọc xác nhận đúng 100%.
|
||
|
||
### 6.4. Vì sao không dùng character-level equality làm chỉ số duy nhất
|
||
|
||
Character count từng tạo kết luận sai vì:
|
||
|
||
- dấu tiếng Việt có thể là nhiều glyph nhưng một ký tự;
|
||
- normalization hợp nhất span và thay glyph;
|
||
- một số glyph nằm ngoài page rectangle;
|
||
- hai extractor có thể cùng bỏ sót một công thức.
|
||
|
||
Do đó eval parser dùng nhiều “instrument” độc lập: span ledger, residual ink,
|
||
cross-tool comparison, visual census và invariant từ chính cuốn sách.
|
||
|
||
---
|
||
|
||
## 7. Bước 4 — Phát hiện bảng và công thức, rồi quarantine
|
||
|
||
### 7.1. Không flatten nội dung 2D chưa được xác minh
|
||
|
||
Table/formula region được phát hiện, gắn:
|
||
|
||
```text
|
||
block_id, kind, shape,
|
||
physical_page, printed_page,
|
||
bbox, section_key, quarantined
|
||
```
|
||
|
||
Các shape được khảo sát gồm simple table, multi-level/merged header,
|
||
cross-page continuation, boxed list và formula 2D. Chỉ 151 block thuộc corpus
|
||
monograph cuối cùng được phát hành dưới dạng block descriptor.
|
||
|
||
### 7.2. Block descriptor là gì
|
||
|
||
Thay vì embedding cell text không đáng tin, pipeline tạo một chunk chỉ từ
|
||
metadata đã xác minh, ví dụ:
|
||
|
||
```text
|
||
AMPICILIN VÀ SULBACTAM — Liều lượng và cách dùng — bảng,
|
||
trang 204. Nội dung chỉ tra cứu được trên ảnh trang gốc,
|
||
không trích dẫn được dưới dạng văn bản.
|
||
```
|
||
|
||
Descriptor giúp câu hỏi “bảng liều theo chức năng thận” vẫn tìm được vùng
|
||
nguồn, nhưng model không được suy ra con số từ bảng bị flatten.
|
||
|
||
### 7.3. Contract an toàn
|
||
|
||
- block text không được rò vào prose chunk;
|
||
- `header_row` bị embargo vì chưa human-verified;
|
||
- mỗi block phải có descriptor;
|
||
- mỗi attachment phải có page + bbox;
|
||
- chunk có attachment phải báo `has_quarantined_content=true`;
|
||
- nhánh trả lời gặp block này trả `VERIFY_PDF`, không generation;
|
||
- UI hiện crop/ảnh nguồn và cảnh báo đối chiếu PDF;
|
||
- không dùng chunk này để phát biểu liều số.
|
||
|
||
---
|
||
|
||
## 8. Bước 5 — Segment thành monograph và section
|
||
|
||
### 8.1. Phát hiện monograph title
|
||
|
||
Không dùng font size đơn thuần. Rule chính:
|
||
|
||
```text
|
||
bold + gần-all-caps + chiều dài ngắn + nằm trong phạm vi monograph
|
||
```
|
||
|
||
Lý do:
|
||
|
||
- title thật xuất hiện cả 10.0pt và 9.5pt;
|
||
- một threshold `size >= 9.8` từng làm mất khoảng 15% title;
|
||
- có title thật chứa mixed case như `HMG-CoA`, nên `isupper()` tuyệt đối cũng
|
||
không đủ;
|
||
- title nhiều dòng phải merge trước khi match;
|
||
- part-divider như “CÁC CHUYÊN LUẬN THUỐC” bị loại riêng.
|
||
|
||
Rule hiện cho phép tối đa 10% chữ lowercase trong title candidate để giữ các
|
||
abbreviation mixed-case hợp lệ.
|
||
|
||
### 8.2. Phát hiện section
|
||
|
||
Section heading là span bold được so với taxonomy mở, ví dụ:
|
||
|
||
- `ten_chung_quoc_te`;
|
||
- `ma_atc`;
|
||
- `loai_thuoc`;
|
||
- `dang_thuoc_va_ham_luong`;
|
||
- `chi_dinh`;
|
||
- `chong_chi_dinh`;
|
||
- `than_trong`;
|
||
- `lieu_luong_va_cach_dung`;
|
||
- `tac_dung_khong_mong_muon`;
|
||
- `tuong_tac_thuoc`;
|
||
- `thoi_ky_mang_thai`;
|
||
- `thoi_ky_cho_con_bu`;
|
||
- `qua_lieu_va_xu_tri`;
|
||
- `ten_thuong_mai`.
|
||
|
||
Taxonomy không đóng cứng theo một danh sách sách giáo khoa; nó được mở rộng từ
|
||
các heading thực sự quan sát thấy trong PDF.
|
||
|
||
### 8.3. Output của segmentation
|
||
|
||
Một monograph có dạng khái niệm:
|
||
|
||
```json
|
||
{
|
||
"drug_id": "abacavir",
|
||
"drug_name": "ABACAVIR",
|
||
"source_page_range": [100, 102],
|
||
"atc_codes": ["J05AF06"],
|
||
"sections": {
|
||
"chi_dinh": {
|
||
"text": "...",
|
||
"parts": [
|
||
{
|
||
"kind": "prose",
|
||
"physical_page": 101,
|
||
"bbox": [44.0, 100.0, 299.0, 300.0],
|
||
"source_span_ids": ["p101_b2_l0_s1"]
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"tables": []
|
||
}
|
||
```
|
||
|
||
Điểm quan trọng là section không chỉ có string. `parts` giữ reading order,
|
||
page, bbox và source span IDs, vì chunking cần provenance chính xác.
|
||
|
||
### 8.4. Kết quả segmentation hiện tại
|
||
|
||
- 684 monograph;
|
||
- 11.974 section;
|
||
- 8.213.036 ký tự section;
|
||
- 151 table/formula block được quarantine;
|
||
- catalog entity có 684 entity;
|
||
- 344 back-index “xem ...” alias được map;
|
||
- 492 section tên thương mại;
|
||
- tổng 10.164 alias;
|
||
- không còn alias back-index unresolved hoặc ambiguous trong artifact đã xác
|
||
minh.
|
||
|
||
---
|
||
|
||
## 9. Bước 6 — Chunking schema v4
|
||
|
||
### 9.1. Parent unit: `(drug_id, section_key)`
|
||
|
||
Chunking tôn trọng cấu trúc sách. Parent logic là một section của một thuốc,
|
||
không phải toàn monograph và không phải window toàn PDF.
|
||
|
||
Ví dụ câu “Chống chỉ định của metformin?” về bản chất đã chỉ rõ:
|
||
|
||
```text
|
||
drug_id = metformin
|
||
section_key = chong_chi_dinh
|
||
```
|
||
|
||
Giữ hai field này trong metadata giúp retrieval lọc đúng mục thay vì mong
|
||
embedding tự phân biệt “chỉ định” và “chống chỉ định”.
|
||
|
||
### 9.2. Token budget
|
||
|
||
Các constant hiện tại:
|
||
|
||
```text
|
||
CEILING_TOKENS = 800
|
||
TARGET_TOKENS = 650
|
||
OVERLAP_TOKENS = 65
|
||
tokenizer = cl100k_base
|
||
```
|
||
|
||
Rule:
|
||
|
||
- section <= 800 token: giữ nguyên thành một chunk;
|
||
- section > 800 token: tách thành atom rồi greedy-pack quanh target 650;
|
||
- không để chunk cuối cùng vượt 800;
|
||
- overlap khoảng 65 token để giữ continuity.
|
||
|
||
### 9.3. Atom không phải fixed character window
|
||
|
||
Atom mặc định là câu, được tách theo dấu câu nhưng tránh nhầm:
|
||
|
||
- decimal comma như `0,425`;
|
||
- numbering;
|
||
- abbreviation;
|
||
- label kết thúc bằng dấu `:`.
|
||
|
||
Danh sách tương tác thuốc có thể là một “câu” rất dài ngăn bằng dấu phẩy. Nếu
|
||
atom đó vượt target, chunker được phép tách theo comma để tránh embedding bị
|
||
truncate.
|
||
|
||
### 9.4. Giữ nhãn đối tượng/đường dùng qua seam
|
||
|
||
Đây là phần quan trọng nhất của chunker về safety.
|
||
|
||
Giả sử source có:
|
||
|
||
```text
|
||
Đường uống:
|
||
Người lớn:
|
||
500 mg mỗi 8 giờ ...
|
||
Trẻ em:
|
||
15 mg/kg ...
|
||
```
|
||
|
||
Nếu seam rơi trước `500 mg`, chunk tiếp theo phải được lặp lại context label
|
||
`Đường uống` và `Người lớn`. Chunker theo dõi active label/scope label và đưa
|
||
nhãn đó vào đầu continuation chunk.
|
||
|
||
Hai field được tách riêng:
|
||
|
||
- `source_text`: span nguồn liên tục, dùng reassembly/provenance;
|
||
- `text`: nội dung dùng retrieval, có thể prepend context label;
|
||
- `context_labels`: ghi rõ label nào chỉ được lặp để retrieval an toàn.
|
||
|
||
Nhờ đó không đánh tráo retrieval context với văn bản nguyên bản.
|
||
|
||
### 9.5. Không để chunk kết thúc bằng label rỗng
|
||
|
||
Chunker không cho một chunk kết thúc kiểu:
|
||
|
||
```text
|
||
Người lớn:
|
||
```
|
||
|
||
rồi để dose nằm một mình ở chunk sau. Label được carry sang part mới. Nếu label
|
||
dài làm chunk pathological vượt mục tiêu, ưu tiên giữ clinical context và để
|
||
gate oversized phát hiện, thay vì xuất một dose không nhãn.
|
||
|
||
### 9.6. Metadata của một chunk
|
||
|
||
```json
|
||
{
|
||
"schema_version": 4,
|
||
"chunk_id": "abacavir__chi_dinh__0",
|
||
"drug_id": "abacavir",
|
||
"drug_name": "ABACAVIR",
|
||
"section_key": "chi_dinh",
|
||
"section_display_name": "Chỉ định",
|
||
"text": "...",
|
||
"source_text": "...",
|
||
"context_labels": [],
|
||
"heading_physical_page": 100,
|
||
"source_page_range": [101, 101],
|
||
"printed_page_range": [102, 102],
|
||
"atc_codes": ["J05AF06"],
|
||
"part_index": 0,
|
||
"part_count": 1,
|
||
"est_tokens": 250,
|
||
"oversized": false,
|
||
"chunk_kind": "prose",
|
||
"attachments": [],
|
||
"has_quarantined_content": false
|
||
}
|
||
```
|
||
|
||
`chunk_id` ổn định theo `{drug_id}__{section_key}__{part_index}`. Khi một
|
||
section có nhiều part, Qdrant scroll có thể trả thứ tự UUID ngẫu nhiên nên
|
||
runtime bắt buộc sort lại theo `part_index`.
|
||
|
||
### 9.7. Kết quả chunking hiện tại
|
||
|
||
| Chỉ số | Giá trị |
|
||
|---|---:|
|
||
| Tổng chunk | 15.100 |
|
||
| Prose | 14.949 |
|
||
| Descriptor | 151 |
|
||
| Tổng token | 4.105.382 |
|
||
| Token lớn nhất | 800 |
|
||
| Vượt ceiling | 0 |
|
||
| Chunk có context label | 1.651 |
|
||
| Chunk mang quarantine flag | 487 |
|
||
|
||
Raw SHA-256 của `chunks.jsonl`:
|
||
|
||
```text
|
||
8dfae08ae6d9222089c5cdb4207a064fe67989f10f7552b555af0aef6331d9a1
|
||
```
|
||
|
||
Normalized corpus SHA dùng trong manifest:
|
||
|
||
```text
|
||
04a27166eaa255b516829f8364227e65ad700e51446b569609d18b5efd11189c
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Bước 7 — Embedding
|
||
|
||
### 10.1. Model và vector space
|
||
|
||
Corpus được embed bằng:
|
||
|
||
```text
|
||
AWS Bedrock model: cohere.embed-v4:0
|
||
output_dimension: 1024
|
||
input_type: search_document
|
||
embedding_type: float
|
||
truncate: NONE
|
||
```
|
||
|
||
Query lúc runtime dùng cùng model nhưng:
|
||
|
||
```text
|
||
input_type: search_query
|
||
```
|
||
|
||
Đây là bất đối xứng có chủ đích của model. Dùng sai `input_type`, sai model hoặc
|
||
sai dimension vẫn có thể trả nearest neighbours mà không báo lỗi; vì vậy
|
||
manifest startup gate là bắt buộc.
|
||
|
||
### 10.2. Vì sao chọn Cohere v4
|
||
|
||
Lúc benchmark, Titan v2 và Cohere v4 đều trả vector 1.024 chiều và L2 norm đo
|
||
được là 1.0. Cohere được chọn vì:
|
||
|
||
- hỗ trợ multilingual, phù hợp corpus tiếng Việt;
|
||
- batch được tối đa 96 text/request;
|
||
- Titan adapter gửi một text/request, nên full corpus chậm hơn rất nhiều.
|
||
|
||
Full corpus embedding lịch sử tiêu tốn khoảng **0,49 USD** trên tài khoản AWS
|
||
cá nhân. Đây là số đo lịch sử, không phải bảng giá cam kết cho lần chạy sau.
|
||
|
||
### 10.3. Không truncate im lặng
|
||
|
||
`truncate="NONE"` khiến input quá dài báo lỗi. Với Dược thư, truncate phần cuối
|
||
của một danh sách tương tác hoặc liều có thể tạo false negative, nên pipeline
|
||
phải sửa chunk trước thay vì để provider tự cắt.
|
||
|
||
### 10.4. Content-addressed cache
|
||
|
||
Cache key:
|
||
|
||
```text
|
||
(model_id, input_kind, sha256(exact_text))
|
||
```
|
||
|
||
Không key chỉ bằng `chunk_id`, vì text thay đổi thì vector cũ phải invalid.
|
||
Không key thêm metadata không liên quan, vì hai chunk có text giống hệt có thể
|
||
dùng cùng vector.
|
||
|
||
Cache là JSONL append-only, index in-memory chỉ giữ byte offset. Một run bị gián
|
||
đoạn có thể resume và chỉ trả tiền cho miss. Artifact cache hiện khoảng 208 MB.
|
||
|
||
### 10.5. Quy tắc vận hành
|
||
|
||
Không re-embed chỉ để deploy code mới. Qdrant data nằm ở volume riêng. Khi di
|
||
chuyển server, ưu tiên snapshot/restore collection vì vừa miễn phí vừa giữ đúng
|
||
vector/corpus identity.
|
||
|
||
---
|
||
|
||
## 11. Bước 8 — Load vào Qdrant
|
||
|
||
### 11.1. Collection
|
||
|
||
```text
|
||
name: duocthu_v1
|
||
vector size: 1024
|
||
distance: Cosine
|
||
points: 15.100
|
||
```
|
||
|
||
Payload giữ toàn bộ chunk record để provenance không mất qua stage boundary.
|
||
|
||
Các field có payload index:
|
||
|
||
```text
|
||
chunk_id, drug_id, section_key, atc_codes,
|
||
chunk_kind, has_quarantined_content
|
||
```
|
||
|
||
### 11.2. Idempotent point ID
|
||
|
||
Point ID không random. Nó là UUID5 từ `chunk_id` với namespace cố định:
|
||
|
||
```text
|
||
point_id = uuid5(PROJECT_NAMESPACE, chunk_id)
|
||
```
|
||
|
||
Load lại cùng corpus sẽ overwrite đúng point cũ, không nhân đôi dữ liệu.
|
||
|
||
### 11.3. Manifest sidecar
|
||
|
||
Collection `duocthu_v1__manifest` lưu:
|
||
|
||
```text
|
||
corpus_sha256
|
||
chunk_count
|
||
model_id
|
||
dimensions
|
||
input_kind
|
||
provider
|
||
distance
|
||
```
|
||
|
||
Khi ai-service khởi động, nó đọc sidecar và so với query embedder. Sai model,
|
||
sai dimension hoặc thiếu manifest thì service từ chối start. Đây là cách chặn
|
||
silent mismatch giữa hai vector space.
|
||
|
||
### 11.4. Load gate
|
||
|
||
Loader kiểm tra:
|
||
|
||
- schema đúng v4;
|
||
- các field provenance bắt buộc tồn tại;
|
||
- page range là cặp integer hợp lệ;
|
||
- vector đúng 1.024 chiều;
|
||
- manifest tương thích trước khi ghi;
|
||
- collection count cuối cùng bằng chunk count.
|
||
|
||
Lần load production ghi 15.100 point qua 59 batch và count gate pass.
|
||
|
||
---
|
||
|
||
## 12. Bước 9 — Request vào chatbot được hiểu như thế nào
|
||
|
||
### 12.1. Request contract
|
||
|
||
Browser gọi Next.js BFF `/api/chat`. BFF thêm/gửi:
|
||
|
||
- `conversation_id`;
|
||
- `X-Correlation-ID`;
|
||
- W3C `traceparent`/`tracestate` nếu có;
|
||
- query tới FastAPI `/v1/rag/query`.
|
||
|
||
API giới hạn query 1–4.000 ký tự và conversation ID tối đa 128 ký tự.
|
||
|
||
### 12.2. Candidate-bound query understanding
|
||
|
||
Một LLM call biến câu hỏi thành `QueryFrame`. LLM không được xem toàn bộ catalog
|
||
684 thuốc rồi tự chọn tùy ý. Trước đó, deterministic resolver tạo shortlist
|
||
drug ID có khả năng xuất hiện trong turn/history. Model chỉ được chọn trong
|
||
shortlist này.
|
||
|
||
Mục tiêu:
|
||
|
||
- tên thuốc bịa không bị map sang thuốc thật gần giống;
|
||
- token prompt không tăng theo toàn catalog;
|
||
- giữ typo correction trong một tập candidate có ràng buộc.
|
||
|
||
### 12.3. QueryFrame
|
||
|
||
Frame chứa các nhóm field:
|
||
|
||
```text
|
||
turn_type
|
||
drugs / unknown_drugs
|
||
attribute / section
|
||
population, age, weight, route
|
||
condition + condition_relation
|
||
patient_context
|
||
standalone_query
|
||
depends_on_previous_turn
|
||
needs_clarify + clarify_reason + quick_replies
|
||
system_error
|
||
```
|
||
|
||
Các `turn_type` chính:
|
||
|
||
- `drug_overview`;
|
||
- `drug_attribute`;
|
||
- `drug_to_condition`;
|
||
- `condition_to_drug`;
|
||
- `condition_relation`;
|
||
- `interaction`;
|
||
- `dosing_calc`;
|
||
- `smalltalk`;
|
||
- `out_of_scope`.
|
||
|
||
### 12.4. Patient context có cấu trúc
|
||
|
||
Nếu người dùng cung cấp, hệ thống giữ:
|
||
|
||
- tuổi, giới, cân nặng;
|
||
- bệnh chính và bệnh nền;
|
||
- dị ứng/ADR;
|
||
- thuốc đang dùng;
|
||
- thai kỳ/cho con bú;
|
||
- CKD, eGFR, CrCl, creatinine;
|
||
- suy gan, Child–Pugh, AST/ALT/bilirubin;
|
||
- lab khác và điều trị trước đó.
|
||
|
||
Không invent field còn thiếu. Condition normalizer cũng bảo thủ: chỉ normalize
|
||
alias chắc chắn như `THA -> tăng huyết áp`, `gout -> gút`; condition rộng như
|
||
“viêm gan” phải hỏi subtype khi subtype làm thay đổi đáng kể kết quả.
|
||
|
||
### 12.5. Context hội thoại
|
||
|
||
- raw conversation turns được lưu Postgres;
|
||
- chỉ đọc cửa sổ gần nhất, mặc định 6 turn;
|
||
- `standalone_query` giải tham chiếu như “thuốc đó”;
|
||
- normalized last frame hiện vẫn in-memory theo process;
|
||
- restart/multi-worker có thể mất normalized frame dù raw history còn trong DB;
|
||
- clarification loop có circuit breaker, tối đa 4 lần liên tiếp.
|
||
|
||
---
|
||
|
||
## 13. Bước 10 — Retrieval: không phải câu nào cũng vector search
|
||
|
||
### 13.1. Route A — biết thuốc và biết section
|
||
|
||
Ví dụ:
|
||
|
||
```text
|
||
“Chống chỉ định của metformin?”
|
||
```
|
||
|
||
Sau understanding:
|
||
|
||
```text
|
||
drug_id = metformin
|
||
section_key = chong_chi_dinh
|
||
```
|
||
|
||
Qdrant dùng payload filter và `scroll` toàn bộ section, không dùng vector:
|
||
|
||
```text
|
||
filter drug_id == metformin
|
||
AND section_key == chong_chi_dinh
|
||
```
|
||
|
||
Tại sao scroll toàn section:
|
||
|
||
- top-k có thể làm mất cuối danh sách;
|
||
- một danh sách chống chỉ định bị cắt vẫn đọc như danh sách hoàn chỉnh;
|
||
- mọi part được sort lại theo `part_index`.
|
||
|
||
Đây là route chính cho câu hỏi có facet rõ.
|
||
|
||
### 13.2. Route B — drug overview hoặc câu hỏi tự do về một thuốc
|
||
|
||
- Bare drug name: lấy các section giới thiệu như tên quốc tế, loại thuốc, chỉ
|
||
định, dược lý/cơ chế.
|
||
- Free-form question nhưng không resolve được section: lấy monograph prose,
|
||
rerank và pack evidence trong budget 6.000 token.
|
||
- Reranker lấy top 6; nếu provider rerank lỗi, fail-open về thứ tự gốc nhưng
|
||
vẫn giữ size bound.
|
||
|
||
Reranker chỉ là ordering aid. Nó không được quyền biến mất một answer đã có
|
||
bằng chứng.
|
||
|
||
### 13.3. Route C — condition/bệnh sang thuốc
|
||
|
||
Luồng hai tầng:
|
||
|
||
```text
|
||
condition
|
||
-> chỉ tìm trong section_key=chi_dinh
|
||
-> exact/contiguous lexical phrase trước
|
||
-> nếu không có hit: dense vector fallback trong chi_dinh
|
||
-> group theo drug_id
|
||
-> rank ở cấp thuốc
|
||
-> cap 8 thuốc
|
||
-> tối đa 2 evidence chunk/thuốc
|
||
```
|
||
|
||
Không search chống chỉ định, ADR, thận trọng hoặc tương tác để sinh candidate
|
||
điều trị. Điều này chặn lỗi quan hệ kiểu “thuốc gây tăng huyết áp” bị biến thành
|
||
“thuốc điều trị tăng huyết áp”.
|
||
|
||
Dense fallback dùng Cohere query embedding và Qdrant `query_points()`. Bản
|
||
production cuối hỗ trợ cả:
|
||
|
||
- client mới: `query_points(query=vector, ...)`;
|
||
- client cũ: `search(query_vector=vector, ...)`.
|
||
|
||
Hit dense phải qua minimum score 0,12. Vì vector DB luôn có nearest neighbour
|
||
kể cả cho câu vô nghĩa, “có hit” không tự động nghĩa là có evidence phù hợp.
|
||
|
||
### 13.4. Patient-specific stage 2
|
||
|
||
Stage 2 không sinh thuốc mới. Nó chỉ xem top candidate đã có indication và tìm
|
||
thêm evidence liên quan bệnh nhân:
|
||
|
||
- interaction với current medication;
|
||
- chống chỉ định/thận trọng;
|
||
- liều trong suy gan/thận;
|
||
- thai kỳ/cho con bú;
|
||
- tuổi.
|
||
|
||
Bound hiện tại:
|
||
|
||
```text
|
||
patient candidates: 2
|
||
safety hits/section: 1
|
||
safety sections/candidate: 4
|
||
```
|
||
|
||
Không có hit được hiểu là “chưa có evidence”, không phải “an toàn”. Status có
|
||
thể là supported, supported with caution, requires additional information hoặc
|
||
insufficient evidence.
|
||
|
||
### 13.5. Route D — interaction hai thuốc
|
||
|
||
Hệ thống lấy mục tương tác của cả hai thuốc và tổng hợp trên union evidence.
|
||
Nếu bất kỳ bên nào có bảng/công thức cần visual verification, toàn bộ synthesis
|
||
bị chặn để không tạo một kết luận phối hợp từ một nguồn đầy đủ và một nguồn bị
|
||
thiếu.
|
||
|
||
### 13.6. Những gì chưa phải true hybrid
|
||
|
||
Code có lexical overlap và module RRF, nhưng production chưa dùng native sparse
|
||
vector/BM25 + dense hybrid đầy đủ. Nhánh condition đang là:
|
||
|
||
```text
|
||
lexical phrase first -> dense fallback
|
||
```
|
||
|
||
Không nên trình bày nó như Elasticsearch-style BM25 hoặc production hybrid RRF.
|
||
|
||
---
|
||
|
||
## 14. Bước 11 — Evidence policy trước khi gọi LLM
|
||
|
||
Mỗi search hit được chuyển thành `Evidence` với:
|
||
|
||
```text
|
||
evidence_id / matched_doc_id
|
||
drug_id / drug_name
|
||
section_key / section_title
|
||
text / score
|
||
source_refs
|
||
requires_visual_check
|
||
```
|
||
|
||
Policy quyết định:
|
||
|
||
- không evidence -> `ABSTAIN`;
|
||
- evidence thiếu provenance -> `ABSTAIN`;
|
||
- có visual-only evidence -> `VERIFY_PDF`;
|
||
- còn lại -> `ANSWERABLE`.
|
||
|
||
LLM không tự quyết định các trạng thái này bằng confidence score.
|
||
|
||
---
|
||
|
||
## 15. Bước 12 — Generation có cấu trúc
|
||
|
||
### 15.1. Model và invocation
|
||
|
||
Production sử dụng `deepseek.v3.2` qua AWS Bedrock Converse:
|
||
|
||
```text
|
||
temperature = 0
|
||
max output tokens = 4096
|
||
connect timeout = 5s
|
||
read timeout = 20s
|
||
SDK attempts = 2
|
||
```
|
||
|
||
Converse adapter hiện yêu cầu JSON trong prompt và tự bóc object `{...}` từ
|
||
response. Schema chưa được provider enforce server-side trong code production
|
||
này, nên parser luôn fail-closed khi malformed.
|
||
|
||
### 15.2. Prompt input
|
||
|
||
Prompt nhận:
|
||
|
||
- query đã contextualize;
|
||
- answer plan nhỏ: verbosity/layout/direct lookup hay synthesis;
|
||
- numbered evidence blocks;
|
||
- trusted metadata label `drug_id`, drug name và section;
|
||
- instruction không dùng kiến thức ngoài evidence;
|
||
- user text nằm trong fence và được xem là data, không phải instruction.
|
||
|
||
Metadata label cần thiết vì trong monograph, text đôi khi chỉ nói “thuốc kháng
|
||
vitamin K” mà không lặp lại “warfarin”; tên thuốc đến từ metadata tin cậy.
|
||
|
||
### 15.3. Output contract
|
||
|
||
Model không trả một blob prose tự do. Nó trả gần dạng:
|
||
|
||
```json
|
||
{
|
||
"evidence_sufficient": true,
|
||
"claims": [
|
||
{
|
||
"drug_id": "colchicin",
|
||
"text": "Colchicin được ghi nhận cho đợt gút cấp.",
|
||
"citations": [1]
|
||
}
|
||
],
|
||
"clarifying_question": null,
|
||
"quick_replies": []
|
||
}
|
||
```
|
||
|
||
List mode còn có guard:
|
||
|
||
- claim phải có `drug_id`;
|
||
- drug phải thuộc candidate set từ retrieval;
|
||
- citation của claim phải trỏ evidence cùng drug;
|
||
- drug ngoài candidate set làm toàn generation bị reject.
|
||
|
||
### 15.4. Request budget
|
||
|
||
Một turn có budget chung:
|
||
|
||
```text
|
||
wall clock: 40.000 ms
|
||
LLM calls: tối đa 8
|
||
```
|
||
|
||
Budget được truyền qua understanding, generation và verification. Code phân
|
||
biệt content failure với provider/budget failure để trace không gắn nhãn sai.
|
||
|
||
---
|
||
|
||
## 16. Bước 13 — Grounding và chống hallucination
|
||
|
||
Prompt không phải guardrail duy nhất. Output phải qua các tầng sau.
|
||
|
||
### 16.1. Schema validation
|
||
|
||
- JSON parse được;
|
||
- `claims` là list;
|
||
- text không rỗng;
|
||
- citation là integer hợp lệ;
|
||
- `evidence_sufficient` là boolean thật;
|
||
- quick replies đúng type, tối đa 4 và không trùng.
|
||
|
||
### 16.2. Candidate-set validation
|
||
|
||
Đặc biệt cho condition-to-drug:
|
||
|
||
```text
|
||
generated drug ⊆ retrieved candidate drugs
|
||
claim citation -> evidence của chính drug đó
|
||
```
|
||
|
||
### 16.3. Deterministic numeric grounding
|
||
|
||
Mọi số trong mỗi claim phải tồn tại **character-for-character** trong đúng
|
||
evidence claim đó cite.
|
||
|
||
Không normalize:
|
||
|
||
- `7,5` thành `7.5`;
|
||
- `1.500` thành `1500`;
|
||
- `2 g` thành `2000 mg`.
|
||
|
||
Lý do: conversion/normalization ở liều là nơi lỗi 10x hoặc 1000x dễ xảy ra.
|
||
Hệ thống chọn refuse thay vì tự diễn giải.
|
||
|
||
### 16.4. Citation validation
|
||
|
||
- citation index phải tồn tại;
|
||
- mọi claim có nội dung phải có citation;
|
||
- số của claim chỉ được tìm trong union các evidence claim đó cite;
|
||
- số có ở evidence 2 không cứu được claim đang cite evidence 1.
|
||
|
||
### 16.5. Semantic entailment
|
||
|
||
Regex không hiểu nghĩa. Claim “Metformin chữa ung thư” vẫn có thể có citation
|
||
hợp lệ và không chứa số. Vì vậy có một LLM judge pass thứ hai:
|
||
|
||
- nhận từng structured claim;
|
||
- chỉ nhìn evidence mà claim cite;
|
||
- quyết định claim có entailed hay không;
|
||
- kiểm tra completeness và phải đưa exact quote nếu báo thiếu.
|
||
|
||
Chỉ chạy một semantic pass. Lặp lại cùng prompt temperature 0 không tạo các vote
|
||
độc lập mà chỉ tăng latency/correlation.
|
||
|
||
### 16.6. Completeness repair
|
||
|
||
Nếu claim đúng nhưng còn bỏ sót dữ kiện có quote nguồn xác minh được, service có
|
||
thể yêu cầu generation lại một lần rồi chạy lại toàn bộ grounding + entailment.
|
||
Không sửa trực tiếp string đầu ra bằng code.
|
||
|
||
### 16.7. Fail-closed
|
||
|
||
Các lý do reject riêng:
|
||
|
||
```text
|
||
request_budget_exhausted
|
||
provider_unavailable
|
||
malformed_output
|
||
evidence_insufficient
|
||
unsupported_drug
|
||
ungrounded_number
|
||
invalid_citation
|
||
uncited_claim
|
||
unsupported_claim
|
||
incomplete_answer
|
||
```
|
||
|
||
Khi production có generator, generation fail không âm thầm đổi thành một đoạn
|
||
raw source giả làm câu trả lời chatbot. Hệ thống abstain với reason cụ thể.
|
||
|
||
### 16.8. Quarantine path
|
||
|
||
`VERIFY_PDF` return trước generation:
|
||
|
||
```text
|
||
“Nguồn có bảng hoặc công thức cần đối chiếu trực tiếp với ảnh PDF;
|
||
không tự động trích số liệu.”
|
||
```
|
||
|
||
Đây là nhánh an toàn nhất cho dữ liệu 2D chưa reconstruct.
|
||
|
||
---
|
||
|
||
## 17. Bước 14 — Response, citation và UI
|
||
|
||
API response gồm:
|
||
|
||
- trace/correlation/OTel trace ID;
|
||
- decision + reason;
|
||
- answer;
|
||
- resolved drug ID;
|
||
- generated/extractive flag;
|
||
- semantic blocks/claims;
|
||
- candidate assessments;
|
||
- citations;
|
||
- disclaimer cố định.
|
||
|
||
Citation được dựng từ metadata của evidence, không để model tự bịa:
|
||
|
||
```text
|
||
chunk_id
|
||
drug_id / drug_name
|
||
section_key / section_title
|
||
source document
|
||
printed page range
|
||
physical page
|
||
block_id / bbox / source_crop
|
||
exact evidence text
|
||
```
|
||
|
||
Next.js BFF group nhiều source ref cùng chunk thành một evidence card nhưng vẫn
|
||
giữ attachment ref riêng. UI cho phép xem evidence/crop và gửi feedback
|
||
helpful/not helpful theo trace.
|
||
|
||
Disclaimer là string do code sở hữu, không do model viết:
|
||
|
||
> Nội dung được trích từ Dược thư Quốc gia Việt Nam 2018, phục vụ tra cứu
|
||
> chuyên môn và không thay thế chỉ định của bác sĩ hoặc dược sĩ lâm sàng.
|
||
|
||
---
|
||
|
||
## 18. Eval ingestion/parser
|
||
|
||
Không có một metric duy nhất được gọi là “PDF parse accuracy”. Eval được chia
|
||
thành ladder.
|
||
|
||
### 18.1. Back-index validation
|
||
|
||
Back-index cuối sách được dùng như ground truth page/name. Kết quả chạy lại trên
|
||
artifact hiện tại:
|
||
|
||
```text
|
||
detected monographs: 684
|
||
ground-truth entries: 705
|
||
recall: 96,2% (678/705)
|
||
precision: 99,1%
|
||
```
|
||
|
||
27 unmatched ground-truth entry không đồng nghĩa 27 thuốc bị mất. Danh sách còn
|
||
có chuyên luận chung/phụ lục nằm ngoài scope, ví dụ kê đơn thuốc, ngộ độc,
|
||
sử dụng thuốc ở trẻ em và pha thuốc tiêm. Phải đọc denominator trước khi biến
|
||
96,2% thành tuyên bố chất lượng.
|
||
|
||
### 18.2. Span routing ledger
|
||
|
||
Mục tiêu: mọi span phải có trạng thái, `unassigned = 0`.
|
||
|
||
### 18.3. Residual ink
|
||
|
||
Mục tiêu: mọi vùng mực còn lại phải được phân loại; đây là cách phát hiện chữ
|
||
vector-outline và fraction bar mà text extractor không thấy.
|
||
|
||
### 18.4. Visual census
|
||
|
||
Các population nhỏ được đọc toàn bộ thay vì sampling:
|
||
|
||
- 23 fraction-bar candidate: 16 thật, 7 false positive, precision 69,6%;
|
||
- 51 vector-outlined text run: đọc và transcription toàn bộ;
|
||
- table block chưa có full human census toàn corpus.
|
||
|
||
### 18.5. Chunk-ready gates
|
||
|
||
Chạy lại ngày 12/08/2026 trên artifact local, toàn bộ gate pass:
|
||
|
||
```text
|
||
outlined run not merged = 0
|
||
known corruption string = 0
|
||
formula fragment in prose = 0
|
||
PUA char = 0
|
||
replacement char = 0
|
||
empty section = 0
|
||
section/part without provenance = 0
|
||
unflagged quarantine block = 0
|
||
duplicate table/drug id = 0
|
||
chunk over 800 tokens = 0
|
||
chunk without printed page = 0
|
||
wrong schema = 0
|
||
source text not unique = 0
|
||
physical range not exact = 0
|
||
section not reassemblable = 0
|
||
block text leaked into chunk = 0
|
||
descriptor count = block count = 151
|
||
```
|
||
|
||
Gate reassembly kiểm tra các `source_text` part có thể ghép lại đúng section,
|
||
loại context label lặp khỏi nguồn. Đây mạnh hơn chỉ kiểm tra “mỗi câu có xuất
|
||
hiện đâu đó”.
|
||
|
||
### 18.6. Điều parser eval không chứng minh
|
||
|
||
- mọi cell trong bảng đúng;
|
||
- recall 100% cho borderless table/bar-less formula;
|
||
- toàn bộ nội dung đã được chuyên gia y khoa đọc;
|
||
- bản 2018 phù hợp cho clinical production hiện tại.
|
||
|
||
---
|
||
|
||
## 19. Eval embedding và retrieval
|
||
|
||
### 19.1. Dense baseline
|
||
|
||
160 case source-derived, 8 section, mỗi section 20 câu:
|
||
|
||
| Metric | Dense embedding thuần |
|
||
|---|---:|
|
||
| hit@1 | 0,544 |
|
||
| hit@3 | 0,663 |
|
||
| hit@5 | 0,738 |
|
||
|
||
Đặc biệt `chong_chi_dinh` chỉ hit@1 = 0,05 khi để vector tự chọn section.
|
||
Section `duoc_ly_va_co_che_tac_dung` dài và generic hút nhiều query không liên
|
||
quan.
|
||
|
||
### 19.2. Bài học kiến trúc từ eval
|
||
|
||
Embedding không hỏng hoàn toàn; bài toán đã có metadata nhưng retrieval lại bỏ
|
||
qua metadata. Fix đúng là resolve section và filter, không nhất thiết đổi model.
|
||
|
||
Sau section routing:
|
||
|
||
```text
|
||
160 generated routing cases: hit@1 = 1,000
|
||
human-written single-drug cases: 16/16
|
||
```
|
||
|
||
Phải trình bày caveat: 160 case sinh từ cấu trúc có circularity; 16 câu human
|
||
written là check bổ sung nhưng sample vẫn nhỏ.
|
||
|
||
### 19.3. Negative/adversarial eval
|
||
|
||
Eval không chỉ đo recall. Nó phải đo:
|
||
|
||
- tên thuốc bịa/gần giống;
|
||
- non-human/veterinary;
|
||
- prompt injection;
|
||
- wrong relation;
|
||
- citation sai;
|
||
- số không có trong source;
|
||
- provider outage;
|
||
- Qdrant/Postgres unavailable;
|
||
- multi-turn context bleed;
|
||
- stale quick reply;
|
||
- quarantine path.
|
||
|
||
---
|
||
|
||
## 20. Eval answer/grounding
|
||
|
||
### 20.1. Unit/contract tests
|
||
|
||
Các nhóm test bảo vệ:
|
||
|
||
- query frame/schema parsing;
|
||
- drug candidate bounding;
|
||
- condition normalization/relation guard;
|
||
- patient context extraction;
|
||
- section ordering;
|
||
- exact citation binding;
|
||
- numeric grounding và decimal separator;
|
||
- structured claim parsing;
|
||
- unsupported-drug rejection;
|
||
- semantic entailment/completeness;
|
||
- timeout/provider error taxonomy;
|
||
- disclaimer;
|
||
- trace fail-open;
|
||
- Qdrant modern/legacy API compatibility.
|
||
|
||
Release record cuối feature condition-to-drug:
|
||
|
||
```text
|
||
pytest: 278 passed, 6 skipped
|
||
live datastore integration: 6 passed
|
||
ruff: clean
|
||
shared types tsc: passed
|
||
Next production build: passed
|
||
web tsc: passed
|
||
```
|
||
|
||
Ngày 12/08 chỉ chạy lại parser/chunk gate offline. Một lần gọi full ai-service
|
||
pytest local dừng ở collection vì `localhost:6333` không chạy; nó không gọi
|
||
production và không làm thay đổi kết quả release record trên.
|
||
|
||
### 20.2. Golden/live eval lịch sử
|
||
|
||
Một mốc live trước feature condition:
|
||
|
||
- 35 câu golden e2e;
|
||
- 19/19 câu answerable trả grounded đúng thuốc;
|
||
- 14 adversarial abstain;
|
||
- phát hiện hai gap về price và “double dose”.
|
||
|
||
Ngày 11/08 còn có battery 37 live cases cho các fix về pediatric clarification,
|
||
timeout/citation chips và regression guard.
|
||
|
||
### 20.3. Condition-to-drug eval
|
||
|
||
Có ba lớp artifact:
|
||
|
||
1. `condition_to_drug_v1.jsonl`: 20 case contract/diagnostic.
|
||
2. `production_manual_60.jsonl`: 60 case HTTP production.
|
||
3. `run_manual_battery.py`: recorder + deterministic checker, không dùng một
|
||
overall LLM judge.
|
||
|
||
60 case bao phủ:
|
||
|
||
- general/specific condition;
|
||
- ambiguity;
|
||
- relation confusion;
|
||
- bệnh nền;
|
||
- current medication;
|
||
- thai kỳ/cho con bú;
|
||
- renal/hepatic lab;
|
||
- allergy;
|
||
- named-drug regression;
|
||
- interaction;
|
||
- conversation continuation và new-case reset.
|
||
|
||
Runner kiểm tra:
|
||
|
||
- decision/reason;
|
||
- answerable phải có citation;
|
||
- general reverse lookup chỉ cite `chi_dinh`;
|
||
- patient query phải có candidate assessment;
|
||
- drug mong đợi xuất hiện;
|
||
- citation drug không nằm ngoài candidate set;
|
||
- số thuốc không vượt cap;
|
||
- interaction citation phải thật sự nhắc current medication;
|
||
- answer không chứa language unsupported như first-line/lựa chọn tốt nhất/phác
|
||
đồ chuẩn.
|
||
|
||
### 20.4. Trạng thái production battery thật
|
||
|
||
Tại handoff cuối ngày 11/08:
|
||
|
||
```text
|
||
case 1–20: 20/20 unique pass sau fix/retry
|
||
case 21–60: chưa chạy
|
||
```
|
||
|
||
Vì vậy không được báo 60/60 hoặc Definition of Done đầy đủ.
|
||
|
||
Hai case gout subtype từng phát hiện lỗi production-only do qdrant-client mới
|
||
không còn `.search()`. Commit cuối sửa sang `query_points()` và deploy smoke
|
||
đã bắt regression này.
|
||
|
||
---
|
||
|
||
## 21. Observability và auditability
|
||
|
||
### 21.1. Correlation
|
||
|
||
Correlation ID và W3C trace context đi từ Next.js tới FastAPI. Response trả:
|
||
|
||
- `X-Correlation-ID`;
|
||
- `X-Trace-ID`;
|
||
- persisted retrieval trace ID.
|
||
|
||
### 21.2. OpenTelemetry stages
|
||
|
||
Trace có các stage:
|
||
|
||
```text
|
||
receive
|
||
understanding
|
||
routing
|
||
retrieval
|
||
rerank/evidence
|
||
generation
|
||
grounding/entailment
|
||
persistence
|
||
response
|
||
```
|
||
|
||
### 21.3. Prometheus
|
||
|
||
Theo dõi request rate/latency, stage latency, route/reason, abstention,
|
||
generation rejection, provider failure và trace-write failure. Label được
|
||
giới hạn để tránh cardinality explosion; không nhét raw patient query vào metric
|
||
label.
|
||
|
||
### 21.4. Ba lớp truy vết một câu trả lời
|
||
|
||
1. UI evidence panel: chunk, page, source text, crop.
|
||
2. Grafana/Tempo: stage nào chạy, latency, decision/reason, trace ID.
|
||
3. PostgreSQL `rag_retrieval_trace`: query, resolved drug, citations/evidence,
|
||
decision/reason, correlation và OTel trace ID.
|
||
|
||
Đây là execution/provenance trace, không phải chain-of-thought logging.
|
||
|
||
---
|
||
|
||
## 22. Deploy production
|
||
|
||
### 22.1. Topology
|
||
|
||
```text
|
||
Internet
|
||
-> Caddy :443
|
||
-> Next.js web :3000
|
||
-> FastAPI ai-service :8000
|
||
-> Qdrant
|
||
-> PostgreSQL
|
||
-> AWS Bedrock qua EC2 IAM role
|
||
```
|
||
|
||
Không có long-lived AWS key trong repo/env production; Bedrock dùng IAM instance
|
||
role.
|
||
|
||
### 22.2. CI/CD
|
||
|
||
Push `master` chạy GitHub Actions:
|
||
|
||
1. SSH vào EC2;
|
||
2. `git fetch` + reset về `origin/master`;
|
||
3. Docker Compose build/restart ai-service, web và observability stack;
|
||
4. reload/validate Caddy;
|
||
5. chạy migration;
|
||
6. health/ready/web smoke;
|
||
7. condition smoke thật với gout cấp;
|
||
8. yêu cầu `decision=answerable` và citation `section_key=chi_dinh`;
|
||
9. kiểm tra Prometheus/Tempo/Grafana;
|
||
10. tạo request có correlation ID rồi xác nhận exact trace tồn tại trong Tempo.
|
||
|
||
### 22.3. Bản production cuối ngày 11/08
|
||
|
||
```text
|
||
commit: f4b84fb
|
||
workflow run: 31471486789
|
||
status: success
|
||
```
|
||
|
||
Bản này gồm feature condition-to-drug, condition deploy smoke và compatibility
|
||
fix Qdrant `query_points()`.
|
||
|
||
### 22.4. Hạ tầng chưa làm
|
||
|
||
Gateway/auth/user/chat services chưa được build. Frontend hiện gọi thẳng
|
||
ai-service qua BFF. Gitea + team ArgoCD/k3s vẫn là target nhưng chưa triển khai;
|
||
production EC2/Compose là interim topology.
|
||
|
||
---
|
||
|
||
## 23. Failure modes và cách hệ thống phản ứng
|
||
|
||
| Failure | Phản ứng |
|
||
|---|---|
|
||
| Không resolve được thuốc | clarify/abstain, không đoán thuốc gần nhất |
|
||
| Condition sai quan hệ | abstain `unsupported_reverse_relation` |
|
||
| Không có indication hit | abstain, không nói “không có thuốc điều trị” |
|
||
| Query embedding lỗi | abstain/fallback theo route, không giả làm content miss |
|
||
| Reranker lỗi | giữ thứ tự candidate gốc, vẫn bounded |
|
||
| Evidence thiếu page | abstain |
|
||
| Có bảng/công thức quarantine | `VERIFY_PDF`, không generation |
|
||
| Model JSON malformed | abstain `malformed_output` |
|
||
| Model nêu số ngoài evidence | reject `ungrounded_number` |
|
||
| Citation sai/thiếu | reject |
|
||
| Claim không entailed | reject `unsupported_claim` |
|
||
| Candidate drug ngoài retrieval | reject `unsupported_drug` |
|
||
| Budget hết | abstain `request_budget_exhausted` |
|
||
| Bedrock outage | abstain `provider_unavailable` |
|
||
| Postgres trace write lỗi | answer an toàn vẫn trả; trace dùng UUID local |
|
||
| Browser disconnect | BFF propagate abort upstream |
|
||
|
||
---
|
||
|
||
## 24. Những hạn chế còn lại
|
||
|
||
1. Corpus là bản 2018, trong khi đã có bản 2022.
|
||
2. Chưa có quyền sử dụng production và clinical release approval đầy đủ.
|
||
3. Chỉ ingest Phần 2; Phần 1 và phụ lục chưa vào corpus.
|
||
4. 151 bảng/công thức vẫn là crop/descriptor, chưa reconstruct row/column.
|
||
5. Provenance tới chunk/page/region, chưa có character span.
|
||
6. Ingestion hiện không phát `parent_id`; parent hydration chỉ là compatibility
|
||
code, chưa phải active hierarchy.
|
||
7. True native sparse BM25 + dense RRF chưa live.
|
||
8. Condition normalizer cố ý nhỏ, chưa phải terminology/ICD service.
|
||
9. Patient stage chỉ xem tối đa 2 candidate để bound latency; không phải clinical
|
||
ranking đầy đủ.
|
||
10. Raw history bền trong Postgres nhưng normalized last frame còn in-memory.
|
||
11. Full 60-case production battery mới hoàn thành 20 case.
|
||
12. Chưa có tập 200–500 case được bác sĩ/dược sĩ duyệt và chấm content.
|
||
13. Rerank/generation/entailment cùng phụ thuộc Bedrock; outage làm tăng abstain.
|
||
14. Chưa có streaming claim đã verify; request vẫn synchronous.
|
||
15. Không có authentication; rate limit hiện là in-memory theo một web process.
|
||
16. BFF có thể che upstream non-2xx thành `upstream_error`, làm diagnostic khó
|
||
nếu không xem trace/log.
|
||
|
||
---
|
||
|
||
## 25. Cách demo cho mentor
|
||
|
||
### Slide 1 — Bài toán
|
||
|
||
“Một PDF Dược thư dài 1.668 trang, hai cột, nhiều bảng/liều. Mục tiêu không chỉ
|
||
là semantic search mà là trả lời có provenance và fail-closed.”
|
||
|
||
### Slide 2 — Pipeline tổng thể
|
||
|
||
Vẽ hai đường offline/online. Nhấn mạnh ingestion không chạy trong request.
|
||
|
||
### Slide 3 — PDF parsing
|
||
|
||
Nói ba điểm:
|
||
|
||
1. không có TOC;
|
||
2. explicit two-column reading order;
|
||
3. bbox/page/font được giữ đến segmentation.
|
||
|
||
### Slide 4 — PDF QA
|
||
|
||
Nêu coverage ledger + residual ink + visual census. Nhấn mạnh không dùng một
|
||
metric “99% parsing accuracy” mơ hồ.
|
||
|
||
### Slide 5 — Chunking
|
||
|
||
Dùng ví dụ người lớn/trẻ em. Nêu 800/650/65, `source_text` so với `text`, và
|
||
context label propagation.
|
||
|
||
### Slide 6 — Table/formula quarantine
|
||
|
||
Cho ví dụ công thức flatten sai. Nêu 151 descriptor và crop-only answer.
|
||
|
||
### Slide 7 — Embedding/Qdrant
|
||
|
||
Nêu Cohere v4 1.024d, `search_document`/`search_query`, cosine, UUID5,
|
||
manifest sidecar.
|
||
|
||
### Slide 8 — Retrieval
|
||
|
||
So sánh dense-only hit@1 0,544 với section route 1,000. Đây là slide chứng minh
|
||
kiến trúc được quyết định bởi eval.
|
||
|
||
### Slide 9 — Condition-to-drug
|
||
|
||
Nêu `chi_dinh`-only, lexical-first/dense fallback, group by drug, cap 8,
|
||
patient stage cap 2.
|
||
|
||
### Slide 10 — Grounding
|
||
|
||
Vẽ:
|
||
|
||
```text
|
||
structured claims
|
||
-> candidate-set check
|
||
-> number/citation check
|
||
-> semantic entailment
|
||
-> serve hoặc abstain
|
||
```
|
||
|
||
### Slide 11 — Eval
|
||
|
||
Nêu parser gates, retrieval eval, adversarial test, production battery và số
|
||
thật 20/60.
|
||
|
||
### Slide 12 — Production và limitations
|
||
|
||
Nêu `f4b84fb`, observability và các giới hạn clinical/source/auth.
|
||
|
||
---
|
||
|
||
## 26. Câu hỏi mentor có thể hỏi và câu trả lời ngắn
|
||
|
||
### “Tại sao không chunk cố định 500 token?”
|
||
|
||
Vì cấu trúc Dược thư đã có drug/section/population/route. Fixed window có thể
|
||
cắt label khỏi dose. Hệ thống dùng section làm parent, sentence-aware packing,
|
||
target 650, overlap 65 và lặp context label có ghi dấu.
|
||
|
||
### “Tại sao vẫn cần embedding nếu đã filter section?”
|
||
|
||
Exact metadata route xử lý câu đã rõ thuốc/section. Embedding cần cho paraphrase,
|
||
free-form fallback và condition-to-drug khi exact phrase không match. Nó là
|
||
fallback, không phải router toàn năng.
|
||
|
||
### “Tại sao Qdrant thay vì chỉ PostgreSQL?”
|
||
|
||
Qdrant hỗ trợ cosine vector search kết hợp payload filter theo drug/section và
|
||
payload index. Postgres dùng cho trace/history giao dịch; hai workload tách nhau.
|
||
|
||
### “Rerank để làm gì?”
|
||
|
||
Bi-encoder tạo vector query/document độc lập nên section dài generic dễ hút
|
||
query. Cross-encoder rerank chấm query-document cùng nhau để sắp lại top
|
||
candidate. Nó chỉ reorder và fail-open.
|
||
|
||
### “Làm sao chắc LLM không bịa liều?”
|
||
|
||
Không chỉ prompt. Mỗi claim phải cite evidence; mọi số phải match nguyên văn
|
||
trong đúng evidence; citation phải hợp lệ; semantic entailment check nội dung;
|
||
table/formula chưa tin cậy không được đưa vào generation. Fail thì abstain.
|
||
|
||
### “Tại sao không cho phép đổi 2 g thành 2000 mg?”
|
||
|
||
Vì unit conversion là nơi lỗi liều nguy hiểm. Hệ thống hiện ưu tiên traceability
|
||
và refuse, không tự chuyển đổi số ngoài source.
|
||
|
||
### “96,2% recall có nghĩa là mất 27 thuốc?”
|
||
|
||
Không. Denominator 705 của back-index còn chứa nhiều chương/phụ lục ngoài phạm
|
||
vi Phần 2. Cần đọc unmatched list và phân loại trước khi kết luận missing drug.
|
||
|
||
### “15.100 chunk có phải 15.100 đoạn độc lập không?”
|
||
|
||
Không hoàn toàn: 14.949 prose và 151 block descriptor. Một section dài có nhiều
|
||
part và overlap; `source_text` cho phép reassemble chính xác, `part_index` giữ
|
||
thứ tự.
|
||
|
||
### “Có dùng BM25/hybrid không?”
|
||
|
||
Có lexical/token-overlap primitives và RRF module, nhưng production chưa có
|
||
native sparse/BM25 hybrid đầy đủ. Condition path là exact lexical phrase trước,
|
||
dense fallback sau.
|
||
|
||
### “Production-ready chưa?”
|
||
|
||
Software/deployment đã live; clinical release thì chưa. Nguồn 2018, quyền sử
|
||
dụng/chuyên gia phê duyệt chưa đủ, table chưa reconstruct hết, eval production
|
||
mới 20/60.
|
||
|
||
### “Nếu Qdrant mới đổi API thì sao?”
|
||
|
||
Bản cuối thêm compatibility helper ưu tiên `query_points()` và fallback
|
||
`search()`, kèm regression test mô phỏng client production chỉ có API mới.
|
||
|
||
---
|
||
|
||
## 27. Các lệnh tái hiện an toàn
|
||
|
||
### 27.1. Offline/read-only hoặc sinh artifact local
|
||
|
||
Từ `ingestion/`:
|
||
|
||
```powershell
|
||
python -m ingestion.cli validate `
|
||
--pdf data/raw/duoc-thu-quoc-gia-viet-nam-2018.pdf `
|
||
--tables data/processed/table_regions.json
|
||
|
||
python -m ingestion.cli chunk-ready `
|
||
--monographs data/processed/monographs.jsonl `
|
||
--chunks data/processed/chunks.jsonl
|
||
```
|
||
|
||
Các lệnh này đọc PDF/artifact local, không gọi production hoặc Bedrock.
|
||
|
||
### 27.2. Parser/segment/chunk local
|
||
|
||
```powershell
|
||
python -m ingestion.cli run `
|
||
--pdf data/raw/duoc-thu-quoc-gia-viet-nam-2018.pdf
|
||
|
||
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
|
||
```
|
||
|
||
Đây là mutation local artifact; nên giữ SHA để so với canonical corpus.
|
||
|
||
### 27.3. Lệnh có chi phí — không chạy nếu chưa được duyệt
|
||
|
||
```powershell
|
||
python -m ingestion.load.run `
|
||
--provider cohere-v4 `
|
||
--collection duocthu_v1
|
||
```
|
||
|
||
Lệnh này có thể gọi AWS Bedrock và ghi Qdrant target. Không dùng URL production,
|
||
không re-embed và không load lại nếu chưa xác định chính xác quyền, endpoint,
|
||
corpus SHA, chi phí và phương án rollback.
|
||
|
||
---
|
||
|
||
## 28. Nguồn code chính để mở khi thuyết trình
|
||
|
||
- PDF extraction: [`ingestion/ingestion/extract/spans.py`](../ingestion/ingestion/extract/spans.py)
|
||
- Segmentation: [`ingestion/ingestion/segment/assembler.py`](../ingestion/ingestion/segment/assembler.py)
|
||
- Title detector: [`ingestion/ingestion/segment/detector.py`](../ingestion/ingestion/segment/detector.py)
|
||
- Chunker: [`ingestion/ingestion/chunk/chunker.py`](../ingestion/ingestion/chunk/chunker.py)
|
||
- Chunk schema: [`ingestion/ingestion/chunk/models.py`](../ingestion/ingestion/chunk/models.py)
|
||
- Readiness gates: [`ingestion/ingestion/validation/readiness.py`](../ingestion/ingestion/validation/readiness.py)
|
||
- Cohere embedding: [`ingestion/ingestion/embed/bedrock_cohere.py`](../ingestion/ingestion/embed/bedrock_cohere.py)
|
||
- Loader/manifest: [`ingestion/ingestion/load/run.py`](../ingestion/ingestion/load/run.py)
|
||
- Query understanding: [`apps/ai-service/rag/understanding.py`](../apps/ai-service/rag/understanding.py)
|
||
- Agent routing: [`apps/ai-service/rag/agent.py`](../apps/ai-service/rag/agent.py)
|
||
- Retrieval policy: [`apps/ai-service/rag/service.py`](../apps/ai-service/rag/service.py)
|
||
- Qdrant adapter: [`apps/ai-service/adapters/qdrant.py`](../apps/ai-service/adapters/qdrant.py)
|
||
- Answer/grounding: [`apps/ai-service/rag/answer.py`](../apps/ai-service/rag/answer.py)
|
||
- Deterministic verifier: [`apps/ai-service/rag/grounding.py`](../apps/ai-service/rag/grounding.py)
|
||
- Production battery: [`apps/ai-service/evals/production_manual_60.jsonl`](../apps/ai-service/evals/production_manual_60.jsonl)
|
||
- Battery runner: [`apps/ai-service/scripts/run_manual_battery.py`](../apps/ai-service/scripts/run_manual_battery.py)
|
||
- Deploy workflow: [`.github/workflows/deploy.yml`](../.github/workflows/deploy.yml)
|
||
- Production handoff: [`coordination/CODEX_CONDITION_MEDICATION_QA_HANDOFF_2026-08-11.md`](../coordination/CODEX_CONDITION_MEDICATION_QA_HANDOFF_2026-08-11.md)
|
||
|
||
---
|
||
|
||
## 29. Kết luận
|
||
|
||
Điểm mạnh của dự án không phải “đã gắn một LLM vào PDF”, mà là biến một PDF y
|
||
dược phức tạp thành một chuỗi artifact có thể audit:
|
||
|
||
```text
|
||
pixel/span
|
||
-> monograph/section
|
||
-> source-preserving chunk
|
||
-> embedding + metadata + manifest
|
||
-> bounded retrieval
|
||
-> structured claims
|
||
-> deterministic + semantic verification
|
||
-> citation/crop/trace tới người dùng
|
||
```
|
||
|
||
Mỗi tầng đều giữ provenance và có failure mode rõ. Eval đã trực tiếp thay đổi
|
||
kiến trúc: dense-only đạt hit@1 0,544 nên hệ thống chuyển sang metadata section
|
||
routing; bảng/công thức flatten sai nên chuyển sang quarantine/crop; Qdrant API
|
||
khác production/local nên deploy smoke + compatibility test được thêm.
|
||
|
||
Tuyên bố đúng nhất hiện tại là:
|
||
|
||
> Hệ thống đã chạy end-to-end trên production, có corpus 15.100 point, trả lời
|
||
> grounded với citation/trace và có nhiều gate fail-closed. Tuy nhiên đây vẫn là
|
||
> bản tra cứu kỹ thuật dựa trên Dược thư 2018, chưa phải sản phẩm lâm sàng được
|
||
> phê duyệt; còn thiếu nguồn hiện hành, review chuyên gia, table reconstruction
|
||
> và phần còn lại của production evaluation.
|