204 lines
15 KiB
Markdown
204 lines
15 KiB
Markdown
# Audit pipeline RAG hội thoại hiện tại
|
||
|
||
> Phạm vi: worktree `D:\VSF-DUOCTHU` ngày 2026-08-10. Báo cáo phản ánh
|
||
> implementation thật đang có trong worktree, bao gồm các thay đổi chưa commit.
|
||
> `EXISTS` không có nghĩa là đã đạt chất lượng production; nó chỉ nghĩa là đã
|
||
> tìm thấy implementation live tương đương.
|
||
|
||
## 1. Request path đã xác minh
|
||
|
||
```text
|
||
ChatPanel.handleSendMessage
|
||
-> POST /api/chat (Next.js BFF)
|
||
-> POST /v1/rag/query (FastAPI)
|
||
-> RagAgent.handle
|
||
-> history + prior QueryFrame
|
||
-> LlmQueryUnderstander.understand
|
||
-> RagAgent._route
|
||
-> RetrievalService.retrieve_framed / retrieve_by_indication
|
||
-> Qdrant metadata route hoặc bounded fallback
|
||
-> parent hydration + dedupe + evidence policy
|
||
-> GroundedAnswerService.answer_from_result
|
||
-> structured claim generation
|
||
-> deterministic number/citation grounding
|
||
-> semantic support + completeness verifier
|
||
-> RagQueryResponse (answer blocks + claims + source ids)
|
||
-> /api/chat mapping sang ChatMessage
|
||
-> ChatBubble + CitationCard/Evidence panel
|
||
```
|
||
|
||
Đường live không dùng `QueryRoutingService` để fuzzy-resolve thuốc; class này còn
|
||
được giữ cho retrieval-only fallback. Live agent dùng candidate-bounded
|
||
`LlmQueryUnderstander` rồi truyền `drug_id` và `section_key` đã resolve vào
|
||
`RetrievalService.retrieve_framed`.
|
||
|
||
### Trace live đã chạy
|
||
|
||
| Query | Kết quả | Latency quan sát | Đối chiếu raw |
|
||
|---|---|---:|---|
|
||
| `Levetiracetam cần tránh những điều kiện môi trường nào khi cất giữ?` qua browser `localhost:3000` | answerable; block `Bảo quản`; 1 source, tr. 888 | 10,9 s end-to-end | Đủ 20–25 °C, tránh ánh sáng, dung dịch uống giữ trong bao bì ban đầu |
|
||
| `Nêu đầy đủ tác dụng không mong muốn của Medroxyprogesteron acetat...` qua API | answerable; 14 nhóm ADR | 14,6 s | Tên ADR và điều kiện chính đủ; hierarchy tần suất kế thừa vẫn cần regression test chặt hơn |
|
||
| `Bảo quản Levetiracetam thế nào?` | abstain `provider_unavailable` | 17,9 s | Không có answer để chấm; không tính pass |
|
||
| Medroxy completeness repair | abstain `incomplete_answer` | 23,5–26,8 s | Cho thấy repair path có thể chạm budget/provider và làm latency xấu |
|
||
|
||
Số mẫu trên chưa đủ để gọi là p50/p95. TTFB bằng gần toàn bộ latency vì response
|
||
hiện là JSON nguyên khối, không có streaming.
|
||
|
||
## 2. Capability matrix
|
||
|
||
| Capability | Status | Evidence implementation | Quyết định |
|
||
|---|---|---|---|
|
||
| Conversation state | PARTIAL | `rag/agent.py:RagAgent._get_history/_remember`; `adapters/postgres.py:PostgresConversationStore` | EXTEND: raw lines có window 6 turns; `_last_frame` chỉ in-process, không bền qua restart/multi-worker |
|
||
| Context resolution | PARTIAL | `LlmQueryUnderstander.understand`, `_known_facts_block`, `_merge_with_prior_frame` | EXTEND: merge có code backstop chủ yếu cho clarify continuation; topic switching vẫn phụ thuộc model |
|
||
| Standalone query rewrite | PARTIAL | `rag/agent.py:_synthesize_query` | EXTEND: đã fold population/age/weight/route/indication nhưng không lưu `standalone_query` first-class trong frame/trace |
|
||
| Active entity tracking | PARTIAL | `QueryFrame.drugs`; `RagAgent._last_frame` | EXTEND persistence/isolation; active frame hiện mất khi process restart |
|
||
| Intent/facet detection | EXISTS | `QueryFrame.turn_type`, `attribute`, `population`, `route`, `indication`; closed vocab trong `understanding.py` | REUSE; mở rộng multi-facet/reasoning mode, không thêm classifier call riêng |
|
||
| Metadata routing | EXISTS | `RetrievalService.retrieve_framed`; `QdrantRetriever.find_by_section/find_by_drug` | KEEP: known entity + facet đi thẳng đúng section |
|
||
| Dense retrieval | EXISTS | `QdrantRetriever.search/search_indication` | KEEP bounded fallback; không dùng cho mọi query |
|
||
| Sparse/lexical retrieval | PARTIAL | `QdrantRetriever.search_lexical` | EXTEND nếu cần: term-overlap/BM25-style, không phải một sparse vector/BM25 index đầy đủ |
|
||
| Hybrid/RRF | PARTIAL | `rag/fusion.py:reciprocal_rank_fusion` có testable primitive nhưng live `RetrievalService` chưa gọi | Không quảng cáo là live hybrid; chỉ wire sau eval chứng minh lợi ích |
|
||
| Reranker | PARTIAL | `RetrievalService._rerank`; `BedrockCohereReranker` trong bootstrap | KEEP: chỉ overview/similarity fallback; explicit section route cố ý không rerank |
|
||
| Parent/sibling expansion | PARTIAL | `RetrievalService._hydrate` parent hydration; `_pooled_neighbour_hits` bounded cross-section | KEEP bounded; không có generic sibling expansion cho mọi query |
|
||
| Evidence selector | PARTIAL | `_hydrate` dedupe, provenance/quarantine policy, `pack_evidence` token budget | EXTEND: chưa có explicit selected/rejected reason trace theo population/route relevance |
|
||
| Evidence sufficiency | PARTIAL | generation `evidence_sufficient`; `_check_sufficiency`; completeness verifier | EXTEND thành supported/partial/insufficient/conflicting; hiện boolean và fail toàn answer |
|
||
| Multi-section retrieval | PARTIAL | interaction gom evidence nhiều thuốc; `than_trong` opt-in lexical neighbor | EXTEND cho multi-facet có kế hoạch; không mở cross-section pooling toàn cục |
|
||
| Reasoning/multi-step logic | MISSING | Không có premise/conclusion representation hoặc bounded decomposition path | ADD sau P0–P2; không dùng agent loop cho simple lookup |
|
||
| Structured claims | EXISTS | `prompt.py:ANSWER_SCHEMA`; `answer.py:_parse_claims` | KEEP |
|
||
| Claim-to-evidence mapping | EXISTS | claim citation indices được map sang stable `source_ids`; response blocks giữ mapping | KEEP; bổ sung claim id/support status khi cần inference/partial |
|
||
| Grounding validation | EXISTS | `grounding.verify`; `_verify_entailment` | KEEP; completeness judge cần eval để giảm false positive/negative |
|
||
| Abstention | EXISTS | `EvidenceDecision`; granular reject reasons; provider/malformed/grounding guards | KEEP |
|
||
| Answer planning | PARTIAL | generation instruction + `_answer_mode` theo claim count + `_build_blocks` theo section | REPLACE heuristic bằng compact plan trong cùng generation call; không thêm LLM call |
|
||
| Adaptive verbosity | PARTIAL | `_answer_mode` chỉ dựa claim count; prompt phân biệt broad/specific | EXTEND theo query complexity/answer mode, không chỉ số claim |
|
||
| Response composition | PARTIAL | `AnswerBlock/AnswerClaim` và BFF DTO | EXTEND: hiện block granularity còn section-centric; chưa có lead/limitation/group hierarchy |
|
||
| SSE/streaming | MISSING | `ChatPanel` dùng `await res.json()`; FastAPI trả `RagQueryResponse`, không `StreamingResponse` | ADD sau correctness; hiện không được nói là streaming |
|
||
| Source rendering | EXISTS | `CitationCard`, evidence pane, printed/physical page, raw snippet | KEEP provenance; giảm chip lặp dưới từng claim |
|
||
| Semantic response components | PARTIAL | `ChatBubble` render `AnswerBlock.kind`; citation panel | EXTEND nhỏ; không biến mỗi paragraph/section thành card |
|
||
| Follow-up handling | PARTIAL | history, prior frame merge, latest-clarify quick replies, clarify circuit breaker | EXTEND và eval 50–100 turns; history window hiện 6 turns nên long chat chưa được chứng minh |
|
||
| Prometheus/Grafana | PARTIAL | `/metrics`, `PrometheusMetrics`, provisioned Grafana dashboard | KEEP aggregate counters; stack chưa được xác minh running trong audit này |
|
||
| Full request trace | PARTIAL | Postgres `rag_retrieval_trace` chỉ lưu query/decision/reason/resolved drug/citations | EXTEND stage timing/frame/route/evidence/guard verdict; không đưa lên user UI |
|
||
|
||
## 3. Actual pipeline so với target
|
||
|
||
Phần nên giữ:
|
||
|
||
- candidate-bounded entity understanding;
|
||
- structured `QueryFrame` và deterministic metadata route;
|
||
- whole-section retrieval cho explicit facet;
|
||
- parent hydration, dedupe, provenance và quarantine;
|
||
- structured claims, deterministic numeric grounding và semantic verifier;
|
||
- Postgres conversation/trace, Prometheus counter và evidence panel.
|
||
|
||
Khoảng trống có tác động lớn nhất:
|
||
|
||
1. active frame không durable và standalone meaning không phải first-class output;
|
||
2. một `attribute` duy nhất không biểu diễn multi-facet query;
|
||
3. evidence selection/sufficiency chưa biểu diễn partial/conflicting;
|
||
4. chưa có direct/synthesis/inference mode và premise mapping;
|
||
5. answer plan chỉ là heuristic, renderer hiện quá card-heavy/source-heavy;
|
||
6. không streaming; latency 9–27 s và provider availability là lỗi backend thực;
|
||
7. trace chưa đủ stage timing để drill-down từ Grafana.
|
||
|
||
## 4. Failure taxonomy theo layer
|
||
|
||
| Layer | Failure đã thấy hoặc có code path | Không được ngụy trang thành |
|
||
|---|---|---|
|
||
| Understanding/provider | timeout/throttle/malformed frame | user clarification |
|
||
| Context | stale entity, mất constraint, clarify loop | retrieval miss |
|
||
| Routing | sai facet, single-facet collapse | generator hallucination |
|
||
| Retrieval | wrong section, dense weak neighbor, parent missing | answer-style problem |
|
||
| Evidence | duplicate, mất heading/condition, token truncation | citation success |
|
||
| Generation | unsupported/partial/incomplete claim | “đã grounded” |
|
||
| Composition | hierarchy bị làm phẳng, source chip lặp | RAG correctness |
|
||
| Availability | provider unavailable, request budget exhausted | “không có trong Dược thư” |
|
||
| Observability | thiếu stage timing/selected-rejected evidence | user-facing technical trace |
|
||
|
||
## 5. Smallest coherent change-set
|
||
|
||
Không dựng pipeline thứ hai. Mở rộng các abstraction đang có theo thứ tự:
|
||
|
||
1. **P0 evidence/response contract:** giữ structured claims, thêm answer plan nhỏ
|
||
trong cùng generation call; hỗ trợ `lead`, semantic group và limitation;
|
||
verifier trả support/completeness rõ, partial không bị trình bày như full.
|
||
2. **P1 context:** đưa `standalone_query` và `depends_on_previous_turn` vào
|
||
`QueryFrame`; persist active frame cùng conversation store thay vì dict local.
|
||
3. **P2 retrieval planning:** cho frame mang nhiều facets; gọi
|
||
`retrieve_framed` theo từng facet có giới hạn rồi dùng cùng `decide`/provenance
|
||
policy. Không bật generic RRF/cross-section pooling nếu eval chưa chứng minh.
|
||
4. **Composition/UI:** prose/list là mặc định; warning/dosage/table chỉ khi plan
|
||
yêu cầu; một affordance `Xem căn cứ` theo group/message, không chip dưới mọi dòng;
|
||
bỏ dashboard chrome trong mỗi answer.
|
||
5. **Trace/latency:** stage timing và call counts vào internal trace/metrics; sau
|
||
khi correctness ổn mới thiết kế safe streaming commit-by-verified-claim.
|
||
|
||
## 6. Những gì chưa được gọi là pass
|
||
|
||
- Batch 30 thuốc đã chạy xong nhưng **không pass**: chỉ 11/30 trả lời, 11/30
|
||
abstain và 8/30 hỏi lại. Đây là baseline trước bản sửa `section_overview` và
|
||
evidence-quoted completeness bên dưới, không được dùng làm số sau-fix.
|
||
- Hội thoại dài đã chạy qua BFF; xem kết quả và giới hạn encoding ở mục 7.
|
||
- Prometheus/Grafana chưa được mở và xác minh trong phiên audit này.
|
||
- Không có p50/p95/p99 đủ mẫu.
|
||
- Grounded inference chưa được implement.
|
||
- Medroxy đã tốt hơn nhưng hierarchy tần suất cần test machine-checkable và
|
||
browser review sau khi answer-plan contract hoàn thiện.
|
||
|
||
## 7. Kết quả triển khai và kiểm chứng ngày 2026-08-10
|
||
|
||
Thay đổi nhỏ trên đúng pipeline hiện hữu, không tạo pipeline thứ hai:
|
||
|
||
- `QueryFrame` có `standalone_query`, `depends_on_previous_turn` và
|
||
`section_overview`. Tra toàn mục được tách khỏi yêu cầu chọn một liều cho ca
|
||
bệnh; drug + facet rõ không còn bị classifier tự ý biến thành chip thu hẹp.
|
||
- Answer plan compact (`verbosity`, `layout`, `reasoning_mode`, heading/warning)
|
||
được lập trước generation bằng code, không thêm model call.
|
||
- Completeness objection phải kèm `evidence_quote`; code kiểm tra quote tồn tại
|
||
trong raw và thật sự hỗ trợ mô tả “bị thiếu”. Judge không còn có thể loại câu
|
||
bảo quản chỉ vì câu hỏi nhắc “độ ẩm” trong khi raw không nêu độ ẩm.
|
||
- Renderer dùng prose/list mặc định, một `Xem căn cứ` cho group, không `[1] [2]`
|
||
trong câu trả lời và không card cho từng claim.
|
||
|
||
Baseline random 30 trước-fix theo facet:
|
||
|
||
| Facet | Answer | Abstain | Clarify | Nhận xét |
|
||
|---|---:|---:|---:|---|
|
||
| Bảo quản | 4 | 2 | 0 | completeness false-positive |
|
||
| Tương tác | 6 | 0 | 0 | tốt nhất trong mẫu |
|
||
| ADR | 1 | 5 | 0 | incomplete/provider/grounding gây fail |
|
||
| Liều/cách dùng | 0 | 2 | 4 | ép population cho cả truy vấn toàn mục |
|
||
| Thận trọng | 0 | 2 | 4 | classifier hỏi lại dù facet đã rõ |
|
||
|
||
Retest có đối chiếu raw:
|
||
|
||
- Levetiracetam sau-fix: answerable 9,3 giây; đủ `20–25 °C`, tránh ánh sáng,
|
||
dung dịch uống giữ bao bì ban đầu. Browser localhost sau hot path: 6,1 giây.
|
||
- Ergotamin tartrat: answerable 6,8 giây; giữ đúng nhiệt độ riêng theo dạng dùng.
|
||
- Isosorbid dinitrat toàn mục liều: answerable 19,9 giây thay vì chip; giữ nhãn
|
||
chỉ định/đường dùng/liều, nhưng latency chưa đạt.
|
||
- Sildenafil ADR vẫn fail `ungrounded_number`; đây là fail đúng của safety gate,
|
||
không được đổi nhãn thành pass.
|
||
- Ganciclovir, Glipizid, Vancomycin, Isradipin từng gặp
|
||
`provider_unavailable`; availability/provider vẫn là blocker thực.
|
||
|
||
Validation code hiện tại:
|
||
|
||
- Ruff: pass.
|
||
- Pytest: `226 passed, 5 skipped`.
|
||
- TypeScript `--noEmit`: pass.
|
||
- Next.js production build: pass.
|
||
- UI browser: pass về request/render; ảnh review xác nhận hết bullet kép và câu
|
||
trả lời không còn citation marker nội tuyến.
|
||
|
||
Long conversation:
|
||
|
||
- Một conversation ID chạy 50 request liên tiếp qua `localhost:3000/api/chat`,
|
||
không có HTTP error. Runner đầu làm mất dấu tiếng Việt trong user lines khi
|
||
đi qua PowerShell nên không dùng 6 lượt cuối của lần này làm kết luận context.
|
||
- Giữ nguyên conversation đó và chạy sạch lượt 51–56 bằng chuỗi không lỗi
|
||
encoding: Levetiracetam → follow-up chống chỉ định → đổi sang Isradipin →
|
||
follow-up bảo quản → đổi sang Zolpidem → follow-up ADR. Cả ba follow-up đều
|
||
bám đúng thuốc gần nhất; không rò Levetiracetam sang Isradipin/Zolpidem.
|
||
- Isradipin thận trọng ở lượt 53 bị `incomplete_answer`, nhưng lượt 54 vẫn resolve
|
||
“thuốc này” đúng Isradipin và trả bảo quản dưới 30 °C, lọ kín, tránh sáng/ẩm.
|
||
- Latency lượt sạch 51–56: 6,9–18,8 giây; correctness context đạt trong kịch bản
|
||
này nhưng tốc độ và provider/completeness availability chưa đạt.
|