| # Tổng kết project ChatVNS | |
| ## 1. Dataset, tech stack và bộ tiêu chí đánh giá | |
| ### Dataset | |
| Dataset của project là dữ liệu tài chính/chứng khoán Việt Nam được crawl tự động theo từng mã cổ phiếu, hiện tập trung vào các mã như `HPG`, `FPT`, `VCB`. | |
| Nguồn dữ liệu chính: | |
| - Trang tổng quan cổ phiếu từ 24HMoney. | |
| - Tin tức và sự kiện doanh nghiệp từ 24HMoney/Vietstock. | |
| - Báo cáo phân tích dạng HTML/PDF. | |
| - Báo cáo tài chính dạng PDF hoặc tài liệu liên quan. | |
| - Ảnh chart TradingView. | |
| - CSV market snapshot/timeseries. | |
| Dữ liệu được lưu theo dạng raw artifacts trong `data/raw`, gồm: | |
| - `html` | |
| - `text` | |
| - `csv` | |
| - `pdf` | |
| - `images` | |
| - `metadata` | |
| Sau đó pipeline xử lý thành: | |
| - `data/processed/text`: text đã đọc/làm sạch. | |
| - `data/processed/chunks`: chunks dùng cho retrieval. | |
| - `data/processed/metadata`: metadata tổng hợp. | |
| - Qdrant collection: vector index cho dense retrieval. | |
| ### Tech stack | |
| Backend/RAG: | |
| - Python 3.11. | |
| - Streamlit cho UI chatbot và dashboard. | |
| - Qdrant làm vector database. | |
| - `sentence-transformers` cho embedding local. | |
| - Hugging Face Inference API nếu bật embedding qua API. | |
| - `rank-bm25` cho keyword/BM25 retrieval. | |
| - Gemini làm LLM trả lời. | |
| - DeepEval cho LLM-as-a-Judge evaluation. | |
| Data processing: | |
| - PyMuPDF để extract text từ PDF có text layer. | |
| - Custom structure parser/chunker trong `app/processing`. | |
| Retrieval hiện tại: | |
| - Dense retrieval bằng BGE-M3 embedding. | |
| - Keyword retrieval bằng BM25. | |
| - Hybrid score để merge candidates. | |
| - BGE-Reranker để rerank candidates trước khi đưa vào answer. | |
| - Extractive context compression để rút gọn context trước khi gọi LLM. | |
| Model chính: | |
| - Embedding model mặc định: `BAAI/bge-m3`. | |
| - Embedding dimension: `1024`. | |
| - Reranker mặc định: `BAAI/bge-reranker-v2-m3`. | |
| - LLM mặc định: Gemini, đọc từ `GEMINI_MODEL`. | |
| Embedding có 2 chế độ: | |
| - Local: tải/chạy `BAAI/bge-m3` qua `sentence-transformers`. | |
| - API: đặt `EMBEDDING_PROVIDER=hf_api` và `HF_API_KEY` để gọi Hugging Face API, tránh tải model local lần đầu. | |
| ### Bộ tiêu chí đánh giá | |
| Project hiện dùng một flow evaluation duy nhất: | |
| ```powershell | |
| .\.venv\Scripts\python.exe -m app.evaluate | |
| ``` | |
| Bộ metrics đã rút gọn còn 8 metric chính: | |
| | Nhóm | Metric | Ý nghĩa | | |
| | --- | --- | --- | | |
| | Retrieval | Precision@5 | Tỷ lệ chunk liên quan trong top-5 kết quả | | |
| | Retrieval | Hit Rate@5 | Tỷ lệ câu hỏi có ít nhất một kết quả liên quan trong top-5 | | |
| | Retrieval | Recall@5 | Tỷ lệ expected evidence xuất hiện trong top-5 chunks | | |
| | Retrieval | MRR | Thứ hạng của chunk liên quan đầu tiên | | |
| | Generation | Faithfulness | Câu trả lời có được hỗ trợ bởi context hay không | | |
| | Generation | Answer Relevancy | Câu trả lời có đúng trọng tâm câu hỏi hay không | | |
| | Finance | Numerical Accuracy | Số liệu trong answer có khớp reference không | | |
| | Finance | Citation Accuracy | Source/citation có khớp reference không | | |
| Kết quả chạy gần nhất trong `data/evaluation/reports/evaluation_report_20260601T101548Z.json`: | |
| | Metric | Kết quả | | |
| | --- | --- | | |
| | Recall@5 | 1.0 | | |
| | MRR | 1.0 | | |
| | Faithfulness | 0.893 | | |
| | Answer Relevancy | 0.883 | | |
| | Numerical Accuracy | 1.0 | | |
| | Citation Accuracy | 1.0 | | |
| Lưu ý: run này có fallback vì Gemini/DeepEval bị quota `429 RESOURCE_EXHAUSTED`, và eval cases hiện tại là auto-generated nên chưa có đủ `expected_numbers`, `expected_source_keywords`, `expected_chunks`. Vì vậy kết quả nên được xem là smoke test cho pipeline, chưa phải benchmark chất lượng cuối cùng. | |
| ## 2. Pipeline và code | |
| ### Pipeline tổng thể | |
| Luồng xử lý chính: | |
| ```text | |
| Collect raw data | |
| -> Read/parse raw artifacts | |
| -> Clean text | |
| -> Parse structure blocks | |
| -> Chunk theo token | |
| -> Embed chunks bằng BGE-M3 | |
| -> Upsert vào Qdrant | |
| -> Hybrid retrieval: dense + BM25 | |
| -> Rerank bằng BGE-Reranker | |
| -> Extractive context compression | |
| -> Prompt Gemini | |
| -> Trả lời kèm sources | |
| -> Log interaction + dashboard/evaluation | |
| ``` | |
| ### Các module chính | |
| Collector: | |
| - `bot-collect-data/crawl_raw_data.py`: crawl HTML/text/CSV/PDF/image/metadata. | |
| Pipeline: | |
| - `app/pipeline.py`: điều phối collect, process, index, launch Streamlit. | |
| Processing: | |
| - `app/processing/documents.py`: load raw artifacts thành `RawDocument`. | |
| - `app/processing/readers.py`: đọc CSV/PDF/text/image. PDF dùng PyMuPDF để extract text. | |
| - `app/processing/structures.py`: suy đoán block/table/widget structure. | |
| - `app/processing/chunking.py`: chunk theo token, giữ metadata. | |
| - `app/processing/outputs.py`: ghi processed text/chunks/metadata. | |
| Embedding/index: | |
| - `app/embeddings.py`: embedding bằng local BGE-M3 hoặc Hugging Face API. | |
| - `app/vector_store.py`: tạo/rebuild Qdrant collection, upsert vectors, dense retrieve. | |
| Retrieval: | |
| - `app/keyword_search.py`: BM25 keyword retrieval. | |
| - `app/retriever.py`: hybrid dense + BM25, merge candidates. | |
| - `app/reranker.py`: BGE-Reranker rerank candidates. | |
| Answer generation: | |
| - `app/context_compression.py`: extractive context compression. | |
| - `app/rag.py`: build prompt, gọi Gemini, trả answer + sources. | |
| - `app/prompts/rag_system.md`: system prompt yêu cầu chỉ trả lời dựa trên context. | |
| - `app/market_snapshot.py`: ưu tiên market snapshot cho câu hỏi giá hiện tại/khối lượng. | |
| - `app/technical_analysis.py`: bổ sung technical context nếu câu hỏi liên quan kỹ thuật. | |
| UI/observability: | |
| - `app/streamlit_app.py`: giao diện chatbot. | |
| - `app/pages/1_Dashboard.py`: dashboard inventory, logs, evaluation. | |
| - `app/observability.py`: log interactions. | |
| Evaluation: | |
| - `app/evaluate.py`: chạy retrieval/generation/finance/performance evaluation và xuất JSON/Markdown report. | |
| ### Các kỹ thuật xử lý code đặc biệt | |
| 1. Đọc PDF bằng PyMuPDF thay cho `pypdf`. | |
| File: `app/processing/readers.py` | |
| ```python | |
| def read_pdf_text(path: Path) -> str: | |
| try: | |
| import fitz | |
| document = fitz.open(str(path)) | |
| try: | |
| text = "\n".join(page.get_text("text") or "" for page in document) | |
| finally: | |
| document.close() | |
| except Exception as exc: | |
| return f"[PDF artifact without extracted text] {path.name} | pymupdf_error={exc}" | |
| if text.strip(): | |
| return text | |
| return f"[PDF artifact without extracted text] {path.name}" | |
| ``` | |
| Ý nghĩa: PDF có text layer được extract trực tiếp bằng PyMuPDF. Project hiện không dùng OCR, nên PDF scan/ảnh không có text layer sẽ không trích được chữ. | |
| 2. Structure-aware token chunking. | |
| File: `app/processing/chunking.py` | |
| ```python | |
| def split_block_by_tokens(block: StructureBlock, max_tokens: int, overlap: int) -> list[StructureBlock]: | |
| tokens = tokenize(block.text) | |
| if len(tokens) <= max_tokens: | |
| return [block] | |
| blocks = [] | |
| start = 0 | |
| part_index = 0 | |
| while start < len(tokens): | |
| end = min(start + max_tokens, len(tokens)) | |
| text = detokenize(tokens[start:end]) | |
| blocks.append( | |
| StructureBlock( | |
| text=text, | |
| structure_type=block.structure_type, | |
| heading_path=block.heading_path, | |
| metadata={**block.metadata, "split_part": part_index}, | |
| ) | |
| ) | |
| if end >= len(tokens): | |
| break | |
| start = max(end - overlap, start + 1) | |
| part_index += 1 | |
| return blocks | |
| ``` | |
| Ý nghĩa: block dài được cắt theo token budget nhưng vẫn giữ `structure_type`, `heading_path` và metadata. Điều này tốt hơn cắt phẳng toàn bộ text. | |
| 3. Gom nhiều block nhỏ thành một chunk có metadata. | |
| File: `app/processing/chunking.py` | |
| ```python | |
| def flush() -> None: | |
| text = "\n\n".join(block.text for block in current_blocks) | |
| structure_types = [block.structure_type for block in current_blocks] | |
| heading_path = current_blocks[-1].heading_path | |
| metadata = { | |
| "structure_types": structure_types, | |
| "primary_structure_type": structure_types[0], | |
| "block_count": len(current_blocks), | |
| "block_metadata": [block.metadata for block in current_blocks], | |
| } | |
| chunks.append((text, structure_types[0], heading_path, token_count(text), metadata)) | |
| ``` | |
| Ý nghĩa: chunk không chỉ có text mà còn biết nó đến từ heading/table/widget/paragraph nào. Metadata này đi tiếp vào Qdrant payload. | |
| 4. BGE-M3 embedding local hoặc Hugging Face API. | |
| File: `app/embeddings.py` | |
| ```python | |
| def encode(self, texts: list[str]) -> list[list[float]]: | |
| if self.provider == "hf_api": | |
| return self._api_embedding(texts) | |
| if self._model is not None: | |
| vectors = self._model.encode(texts, normalize_embeddings=True) | |
| return [vector.tolist() for vector in vectors] | |
| return [self._hash_embedding(text) for text in texts] | |
| ``` | |
| Ý nghĩa: mặc định có thể chạy local bằng `sentence-transformers`; nếu bật `EMBEDDING_PROVIDER=hf_api` thì gọi API để tránh tải BGE-M3 local lần đầu. | |
| 5. Validate và normalize embedding từ API. | |
| File: `app/embeddings.py` | |
| ```python | |
| vectors = self._coerce_api_vectors(payload, expected_count=len(texts)) | |
| return [self._normalize_vector(vector) for vector in vectors] | |
| ``` | |
| Ý nghĩa: API embedding có thể trả về vector hoặc token vectors. Code ép shape về đúng số lượng embeddings, fit dimension 1024 và normalize trước khi upsert Qdrant. | |
| 6. Hybrid retrieval dense + BM25 bằng RRF. | |
| File: `app/retriever.py` | |
| ```python | |
| dense_hits = dense_retrieve(query, top_k=dense_limit, ticker=ticker) | |
| keyword_hits = keyword_search(query, top_k=keyword_limit, ticker=ticker) | |
| for rank, hit in enumerate(dense_hits, start=1): | |
| merged[hit.id] = hit | |
| scores[hit.id] = scores.get(hit.id, 0.0) + dense_weight / (rrf_k + rank) | |
| for rank, hit in enumerate(keyword_hits, start=1): | |
| merged.setdefault(hit.id, hit) | |
| scores[hit.id] = scores.get(hit.id, 0.0) + keyword_weight / (rrf_k + rank) | |
| ``` | |
| Ý nghĩa: dense retrieval bắt ngữ nghĩa, BM25 giữ keyword/số liệu/ticker. Hai nguồn được merge bằng Reciprocal Rank Fusion, tức ưu tiên tài liệu có rank cao ở từng nguồn thay vì phụ thuộc trực tiếp vào raw score khác thang đo. | |
| 7. BGE-Reranker sau hybrid retrieval. | |
| File: `app/reranker.py` | |
| ```python | |
| pairs = [(query, chunk.text) for chunk in chunks] | |
| raw_scores = self._model.predict(pairs, batch_size=RERANK_BATCH_SIZE) | |
| ranked = sorted( | |
| zip(chunks, [float(score) for score in raw_scores]), | |
| key=lambda item: item[1], | |
| reverse=True, | |
| ) | |
| ``` | |
| Ý nghĩa: hybrid retrieval lấy candidates rộng, còn BGE-Reranker sắp xếp lại candidates theo mức liên quan query-document chính xác hơn. | |
| 8. Extractive context compression. | |
| File: `app/context_compression.py` | |
| ```python | |
| ranked = sorted( | |
| enumerate(sentences), | |
| key=lambda item: sentence_score(item[1], query_tokens, chunk.ticker), | |
| reverse=True, | |
| ) | |
| selected_indexes = sorted( | |
| index | |
| for index, sentence in ranked[:CONTEXT_MAX_SENTENCES_PER_CHUNK] | |
| if sentence_score(sentence, query_tokens, chunk.ticker) > 0 | |
| ) | |
| ``` | |
| Ý nghĩa: trước khi gọi Gemini, mỗi chunk được rút còn các câu liên quan nhất với query/ticker/từ khóa tài chính/số liệu. Đây là compression kiểu extractive, an toàn hơn LLM summarization vì không tự viết lại số. | |
| 9. Build prompt chỉ từ context đã nén và metadata nguồn. | |
| File: `app/rag.py` | |
| ```python | |
| blocks.append( | |
| "\n".join( | |
| [ | |
| f"[{index}] ticker={chunk.ticker} modality={chunk.modality} structure={chunk.structure_type} score={chunk.score:.4f}", | |
| f"source={chunk.source_path}", | |
| f"heading={' > '.join(chunk.heading_path)}", | |
| compress_chunk_text(question, chunk), | |
| ] | |
| ) | |
| ) | |
| ``` | |
| Ý nghĩa: prompt vẫn có citation metadata (`source`, `heading`, `ticker`) nhưng phần text được nén để giảm nhiễu và giảm token. | |
| 10. Evaluation fallback khi DeepEval/quota/reference thiếu. | |
| File: `app/evaluate.py` | |
| ```python | |
| fallback_scores = { | |
| "faithfulness": round(context_grounding_score(str(result.get("answer", "")), retrieval_chunks), 3), | |
| "answer_relevancy": round( | |
| lexical_answer_relevancy_score(case["question"], str(result.get("answer", ""))), | |
| 3, | |
| ), | |
| } | |
| ``` | |
| Ý nghĩa: nếu DeepEval lỗi quota/API, dashboard vẫn có số theo heuristic fallback và ghi lý do trong `fallback_metrics`, tránh report bị trống. | |
| ### Các điểm đã cải thiện | |
| - Đổi embedding sang BGE-M3. | |
| - Có thể gọi embedding qua Hugging Face API để tránh tải model local. | |
| - Thêm BGE-Reranker. | |
| - Chuyển đọc PDF sang PyMuPDF. | |
| - Thêm extractive context compression cho answer chính. | |
| - Rút gọn evaluation thành một lệnh duy nhất. | |
| - Dashboard hiển thị một bộ metrics thống nhất. | |
| ## 3. Prompt cho ChatGPT tạo hình kiến trúc tổng thể | |
| Dùng prompt sau để tạo hình kiến trúc hệ thống: | |
| ```text | |
| Create a clean architecture diagram for a Vietnamese stock RAG chatbot named ChatVNS. | |
| Style: | |
| - Professional software architecture diagram. | |
| - White or light background. | |
| - Clear boxes and arrows. | |
| - Use Vietnamese labels. | |
| - Avoid decorative illustrations. | |
| Main flow: | |
| 1. Data Sources: | |
| - 24HMoney stock overview | |
| - Vietstock news/events | |
| - Analyst reports HTML/PDF | |
| - Financial statements PDF | |
| - TradingView chart screenshots | |
| - CSV market snapshots | |
| 2. Raw Data Storage: | |
| - data/raw/html | |
| - data/raw/text | |
| - data/raw/csv | |
| - data/raw/pdf | |
| - data/raw/images | |
| - data/raw/metadata | |
| 3. Processing Pipeline: | |
| - Read raw artifacts | |
| - PDF text extraction with PyMuPDF | |
| - Clean text | |
| - Structure parsing | |
| - Token-based chunking | |
| - Write processed chunks and metadata | |
| 4. Indexing: | |
| - BGE-M3 embedding, dimension 1024 | |
| - Qdrant vector database | |
| - BM25 keyword index from processed chunks | |
| 5. Retrieval: | |
| - Dense retrieval from Qdrant | |
| - BM25 keyword retrieval | |
| - Hybrid candidate merge | |
| - BGE-Reranker reranking | |
| - Extractive context compression | |
| 6. Answer Generation: | |
| - Gemini LLM | |
| - RAG system prompt | |
| - Market snapshot guard for current price questions | |
| - Answer with citations/sources | |
| 7. UI and Monitoring: | |
| - Streamlit Chatbot | |
| - Dashboard | |
| - Interaction logs | |
| - Evaluation reports | |
| 8. Evaluation: | |
| - Recall@5 | |
| - Precision@5 | |
| - Hit Rate@5 | |
| - MRR | |
| - Faithfulness | |
| - Answer Relevancy | |
| - Numerical Accuracy | |
| - Citation Accuracy | |
| Show arrows from data sources to raw storage, processing, indexing, retrieval, generation, UI, and evaluation. | |
| Emphasize that the system is a finance-focused RAG pipeline with source-grounded answers. | |
| ``` | |
| ## 4. Khó khăn | |
| ### 4.1. Dataset có vấn đề gì không? Đã xử lý như thế nào? | |
| Có. Dataset có một số vấn đề đặc thù: | |
| 1. Dữ liệu đến từ nhiều nguồn và nhiều format. | |
| Cùng một mã cổ phiếu có HTML, PDF, CSV, ảnh chart, metadata. Cách xử lý là chuẩn hóa thành `RawDocument`, sau đó parse/chunk theo cùng một schema. | |
| 2. PDF có thể có text layer hoặc là scan/ảnh. | |
| Project hiện dùng PyMuPDF để extract text từ PDF có text layer. PDF scan/ảnh không có text layer sẽ không trích được chữ vì project đã bỏ OCR. | |
| 3. Ảnh chart chưa có text thật. | |
| Ảnh TradingView hiện vẫn được lưu để hiển thị/truy vết, nhưng chưa image embedding và không OCR ảnh rời. Với ảnh, pipeline hiện tạo placeholder text. | |
| 4. HTML web có nhiều nhiễu. | |
| Trang crawl có script, menu, layout, quảng cáo hoặc text không liên quan. Pipeline xử lý bằng clean text, structure parser, chunking theo block và metadata. | |
| 5. Dữ liệu tài chính có nhiều số liệu dễ nhầm. | |
| Project thêm market snapshot guard để câu hỏi giá hiện tại ưu tiên snapshot, tránh nhầm với giá mục tiêu trong báo cáo phân tích. | |
| 6. Eval cases hiện còn yếu. | |
| `eval_cases.json` hiện là auto-generated starter cases, thiếu `expected_numbers`, `expected_source_keywords`, `expected_chunks`. Vì vậy evaluation hiện là smoke test, chưa phải benchmark cuối cùng. | |
| ### 4.2. Còn những hạn chế nào? | |
| Các hạn chế hiện tại: | |
| 1. PDF scan không có text layer chưa được đọc nội dung chữ. | |
| Vì project đã bỏ OCR, PyMuPDF chỉ extract được text layer có sẵn trong PDF. | |
| 2. Ảnh rời chưa image embedding. | |
| Chart screenshot vẫn chủ yếu để UI hiển thị, chưa đưa tín hiệu hình ảnh thật vào retrieval. | |
| 3. BGE-M3 và BGE-Reranker có thể cần tải model local. | |
| Có thể dùng `EMBEDDING_PROVIDER=hf_api` cho embedding, nhưng reranker hiện vẫn chạy local qua `CrossEncoder`. | |
| 4. Context compression là extractive, chưa semantic compression. | |
| Cách hiện tại an toàn vì không tự viết lại số liệu, nhưng có thể bỏ sót câu quan trọng nếu câu không overlap nhiều với query. | |
| 5. Hallucination protection chưa hoàn chỉnh. | |
| Project đã có prompt guard, source grounding, market snapshot guard, rerank, context compression và evaluation faithfulness. Tuy nhiên chưa có runtime validator chặt để kiểm tra từng số liệu/claim trong answer trước khi trả về. | |
| 6. Evaluation còn phụ thuộc quota DeepEval/Gemini. | |
| Khi quota lỗi, report dùng fallback cục bộ. Điều này giúp dashboard không trống, nhưng điểm fallback không thay thế hoàn toàn LLM-as-a-Judge. | |
| 7. Cần rebuild index khi đổi embedding model/dimension. | |
| Vì BGE-M3 dùng vector dimension 1024, Qdrant collection cũ 384 chiều phải được rebuild bằng: | |
| ```powershell | |
| .\.venv\Scripts\python.exe -m app.pipeline index | |
| ``` | |
| 8. Dataset hiện chưa đủ reference ground truth. | |
| Để đánh giá nghiêm túc hơn, cần bổ sung eval cases có: | |
| - `expected_chunks` | |
| - `expected_numbers` | |
| - `expected_source_keywords` | |
| - `expected_output` | |
| ## Kết luận ngắn | |
| ChatVNS hiện là một hệ thống RAG cho chứng khoán Việt Nam với pipeline khá đầy đủ: crawl dữ liệu tài chính, xử lý đa định dạng, đọc PDF bằng PyMuPDF, chunking, BGE-M3 embedding, Qdrant, hybrid retrieval, BGE-Reranker, context compression, Gemini answer và dashboard/evaluation. | |
| Điểm mạnh hiện tại là kiến trúc có đủ các lớp chính của một RAG thực tế. Điểm cần cải thiện tiếp theo là ground-truth eval cases, image understanding cho ảnh chart/PDF scan, và hallucination validator ở runtime. | |