# 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. > > **Cập nhật 2026-08-11 — đây là bản ghi theo ngày 2026-08-10.** Đo lại trên > production ngày 2026-08-11 cho hai kết quả khác: > > - §7 ghi "Sildenafil ADR vẫn fail `ungrounded_number`". Hai lần chạy lại > cho hai kết quả khác nhau (46,8s `abstain/unsupported_claim`; 25,1s > `answerable/grounded`, 2 citation), không lần nào là `ungrounded_number`. > Nhiều khả năng là nhiễu ở tầng entailment/generation hơn là một lỗi xác > định, nên nếu xử lý thì nên tiếp cận theo hướng đó. > - §7 ghi "Pytest: 226 passed, 5 skipped". Số hiện tại là 230 passed (bỏ > `test_api.py`/`test_live_datastores.py` vốn cần datastore sống). > > Một phần §6/§7 đã được xử lý ngày 2026-08-11: `incomplete_answer` ở đường > completeness-repair phần lớn đến từ việc cạn budget, nay tách thành > `request_budget_exhausted`/`provider_unavailable`. Xem mục 2026-08-11 trong > `docs/progress-log.md`. ## 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.