Engineering

DevPulse: Kiến Trúc Của Một AI Workspace Real-Time

By Ginbok13 min read
DevPulse bắt đầu từ một tiền đề đơn giản: dữ liệu project không nên bị đóng băng. Trong hầu hết developer tools, các AI feature hoạt động trên một snapshot — dữ liệu được index đêm qua, hoặc khi một background job chạy. Với một workspace mà developer đang chủ động viết task, cập nhật documentation, và query AI liên tục, mô hình đó tạo ra một mismatch liên tục giữa những gì tồn tại và những gì AI biết.

Kiến trúc của DevPulse là một nỗ lực để thu hẹp khoảng cách đó. Bài này mô tả hệ thống tổng thể — các component kết nối như thế nào, các ranh giới chính nằm ở đâu, và những tradeoff nào được thực hiện ở mỗi tầng.

Tổng quan hệ thống

Ở mức cao nhất, DevPulse là một hệ thống ba plane: client plane xử lý user interaction và rendering, compute plane xử lý AI orchestration và business logic, và data plane xử lý persistence và vector indexing. Cả ba chạy trên GCP/Firebase, điều này giới hạn các lựa chọn infrastructure nhưng loại bỏ overhead vận hành cho một team nhỏ.

DEVPULSE · TỔNG QUAN HỆ THỐNG · KIẾN TRÚC BA PLANE CLIENT PLANE React + Vite UI / state Firebase SDK realtime listeners Callable Client gọi Cloud Functions Mermaid Renderer lazy diagram engine Tailwind styling HTTPS callable / SDK COMPUTE PLANE · Cloud Functions Gen 2 (Cloud Run) saveAppEntity write + vectorize đồng bộ chatQueryProject embed + retrieve + generate importProject parse unstructured → structured entities AI Hooks middleware pre / post LLM khác slides timesheet Firestore R/W · Vector index Gemini API DATA PLANE · Firestore Structured Store projects / tasks / docs Vector Store projectVectors/chunks Vector Index findNearest · COSINE EXTERNAL AI · Google Gemini Flash 1.5 generation Embed-004 768-dim vectors

Client plane là stateless với AI: nó gửi request và phản ứng với kết quả. AI orchestration hoàn toàn nằm trong compute plane, nghĩa là client không bao giờ giữ model context hay maintain conversation state trực tiếp. State được reconstruct trên mỗi Cloud Function invocation từ Firestore.

Write pipeline và synchronous indexing

Quyết định kiến trúc trung tâm trong DevPulse là vectorization xảy ra ở đâu so với xác nhận write mà user thấy. Có hai lựa chọn: trigger-based async indexing, nơi Firestore fires một event sau write và một function riêng xử lý vectorization ở background; và synchronous callable indexing, nơi một Cloud Function duy nhất xử lý cả structured write lẫn vector write trước khi trả response.

Cách async đơn giản và rẻ hơn để xây. Nó cũng tạo ra một consistency window — một khoảng thời gian mà structured data tồn tại nhưng vector thì chưa. Với một workspace mà developer thêm task và hỏi AI ngay lập tức, khoảng cửa sổ này có thể quan sát được và phá vỡ mental model của user về hệ thống.

Cách synchronous loại bỏ consistency window nhưng đánh đổi bằng việc coupling write latency với response time của embedding API. Tail latency của embedding call (p99 ~1.8s trong điều kiện tải thông thường) trở thành tail latency của thao tác save. Đây là tradeoff chấp nhận được cho một workspace có write frequency thấp. Nó sẽ không chấp nhận được với hệ thống xử lý hàng trăm write mỗi giây.

WRITE PIPELINE · ĐÁNH ĐỔI CONSISTENCY t=0 time → Async (trigger) user write data saved ✓ confirmed toast hiện consistency window · AI chưa biết · 5–15s trigger async embed + index vector ready Sync (callable) user write Cloud Function · saveAppEntity ① setDoc → ② embed → ③ vectorDoc ✓ confirmed data + vector cả hai sẵn sàng zero consistency gap Tradeoff: sync thêm embedding latency vào write path (p50 ~400ms, p99 ~1.8s). Chấp nhận được khi write frequency thấp và query freshness là ưu tiên. Không phù hợp với hệ thống write tần suất cao. Chọn dựa trên SLO, không phải dựa trên cái gì dễ xây hơn.

RAG engine: kiến trúc query

Query path cho AI chat là một pipeline bốn bước: query embedding, vector retrieval, context assembly, và generation. Mỗi bước có implication kiến trúc compound với nhau. Một quyết định yếu ở bước retrieval không thể sửa bằng một prompt tốt hơn ở bước generation.

RAG QUERY PIPELINE · chatQueryProject ① Query Embed tin nhắn user → text-embedding-004 → float32[768] cùng model với lúc index ② Vector Retrieval findNearest() COSINE · topK=6 scope theo projectId Firestore infra không in-memory scan ③ Context Assembly top-6 chunks → grounded system prompt chỉ trả lời từ context · không fill ④ Generation gemini-1.5-flash startChat() với history + system conversation state duy trì per session

Hai lựa chọn kiến trúc ở đây rất quan trọng. Thứ nhất, embedding model dùng lúc query phải giống hệt model dùng lúc index. Dùng một model khác — dù là phiên bản mới hơn của cùng model — tạo ra embedding space không thể so sánh, và retrieval trở nên vô nghĩa. Thứ hai, bước context assembly dùng instruction "ground-only": model được nói rõ không được tự điền gap bằng parametric knowledge. Điều này tạo ra refusals cho câu hỏi ngoài phạm vi, đây là hành vi đúng cho một grounded workspace tool. Thay thế sẽ tạo ra hallucination tự tin về dữ liệu cụ thể của project.

Kiến trúc vector store

Lưu vector trong Firestore không có native index là bài toán O(n) — mỗi query phải load toàn bộ collection vào memory của function, tính similarity, và trả top kết quả. Ở 1.000 chunks đó là khoảng 6MB mỗi query. Ở 10.000 chunks vượt Cloud Function memory limit. Chi phí mỗi query scale tuyến tính với số lượng vector được lưu.

Firestore Vector Search, GA từ cuối 2024, giải quyết bài toán này ở tầng infrastructure. Một vector index trên chunks sub-collection cho phép Firestore thực hiện approximate nearest-neighbor search nội bộ và chỉ trả top-k kết quả về function. Chi phí và latency của query gần như không đổi bất kể collection size, bị ràng buộc bởi result count thay vì tổng số vector.

VECTOR STORE · HÀNH VI SCALING Không có vector index · O(n) scan Cloud Fn load ALL tất cả N chunks → RAM ~6MB mỗi 1k chunks JS cosine loop O(n) compute top-6 về vỡ ở 10k Với Firestore Vector Search · ANN index Cloud Fn query vec Firestore · findNearest() ANN search chạy bên trong Firestore infra · chỉ trả top-k · chi phí = O(1) của result count top-6 về scale tới 1M+ So sánh Collection size Naive: ~6MB RAM / 1k chunks Vector Search: hằng số, độc lập với N Index overhead Không có (scan lúc query) Tạo index một lần mỗi collection

Data model trong Firestore dùng hierarchy hai cấp: root collection projectVectors, scope theo projectId, với sub-collection chunks bên trong mỗi project. Cách scope này nghĩa là vector search tự động bị giới hạn trong project liên quan — không cần filter predicate để tách dữ liệu của một tenant khỏi tenant khác. Field vector phải được lưu dưới dạng Firestore VectorValue type (không phải plain array) để index có thể áp dụng.

Chunking và chất lượng retrieval

Chất lượng retrieval là hàm của cách text được split trước khi embedding, không chỉ là thuật toán retrieval nào được dùng. Hard character cap — split mỗi 4.000 ký tự — là lựa chọn mặc định vì đơn giản. Nó tạo ra hai class retrieval error compound theo scale.

Thứ nhất là boundary degradation. Khi split xảy ra giữa câu, chunk kết quả bắt đầu hoặc kết thúc với sentence fragment. Embedding model gán confidence vector thấp hơn cho linguistic unit chưa hoàn chỉnh so với những unit hoàn chỉnh về mặt ngữ nghĩa. Vector cho nửa câu là nhiễu. Lúc retrieval, một query đáng lẽ match chunk có thể fail vì phần relevant nhất của text gốc bị split qua hai chunk kề nhau, không cái nào chứa đủ semantic signal để score cao.

Thứ hai là metadata dilution. Trong một workspace đa entity — projects, tasks, documents, configs — các chunk giữa document mất entity context. Một chunk trích từ giữa task description không mang thông tin rằng nó thuộc một task cụ thể với title cụ thể. Query tham chiếu task đó bằng tên sẽ không retrieve được chunk vì embedding distance cao.

CHUNKING STRATEGY · RANH GIỚI + METADATA Hard char split · boundary degradation ...luồng xác thực được xử lý CUT ✗ bởi middleware layer mà không có entity context · câu bị cắt Paragraph split + title prefix · ranh giới ngữ nghĩa [TASK] Implement OAuth middleware Luồng xác thực được xử lý bởi middleware layer, validate token trước khi forward request downstream. Query: "trạng thái auth middleware" → score thấp · split phá tín hiệu Query: "trạng thái auth middleware" → score cao · entity title align với query intent

Cả hai vấn đề được giải quyết bằng paragraph-aware split với character budget 3.800 ký tự mỗi chunk, kết hợp với title prefix được inject vào đầu mỗi chunk — kể cả continuation chunk từ entity nhiều phần. Title prefix đảm bảo rằng ngay cả chunk giữa document cũng mang đủ entity context để score tốt đối với query tham chiếu entity bằng tên. Đây là cải thiện retrieval precision không cần thay đổi model, reindex, hay bất kỳ thay đổi nào trong query pipeline.

AI middleware: kiến trúc hook

Mỗi AI feature trong DevPulse — chat, project import, timesheet parsing, slide generation — thực hiện một hoặc nhiều lần gọi Gemini API. Không có shared middleware layer, mỗi feature tự xử lý độc lập các concerns như usage tracking, safety filtering, và quality gating. Logic bị duplicate, monitoring coverage không đồng nhất, và thêm một policy mới (ví dụ PII scrubbing) đòi hỏi phải chỉnh sửa mọi call site.

Kiến trúc hook giải quyết vấn đề này bằng một pre/post pipeline đơn giản bọc quanh mọi Gemini call. Pre-hook transform input trước khi đến model. Post-hook inspect và tùy chọn modify output. Pipeline được áp dụng đồng nhất bất kể feature nào triggered call.

AI HOOK MIDDLEWARE PIPELINE Feature chat / import / timesheet Pre-hooks PII scrub input filter usage log token count Gemini API Flash 1.5 generation Post-hooks quality gate yếu → retry hallucination confidence check out về client Hook pipeline cho phép gì · PII scrubbing: áp dụng cho mọi feature input mà không cần chỉnh sửa feature code · Quality gate: response yếu (không đủ ngữ cảnh) tự động trigger expanded RAG retrieval (topK=12) · Usage tracking: token count, latency, và feature attribution được log tập trung để phân bổ chi phí và monitoring

Quality gate post-hook đáng được chú ý riêng như một architectural pattern. Khi model trả về response có dấu hiệu context không đủ — các câu như "Tôi không có đủ thông tin" hoặc confidence score thấp — hook có thể tự động re-run bước retrieval với top-k lớn hơn (6 → 12) và generate lại. Điều này cho hệ thống một cơ chế self-correcting cho retrieval failure mà không cần client implement retry logic hay developer phải tune thủ công retrieval parameter cho từng feature.

Frontend rendering: Mermaid và bài toán bundle

Developer muốn diagram. Mermaid.js là lựa chọn thực tế cho một developer-facing tool vì nó render từ markdown-style syntax và bao gồm các diagram type phổ biến nhất — flowchart, sequence diagram, Gantt chart — xuất hiện trong project documentation.

Mermaid v11 giới thiệu kiến trúc dynamic import: core library lazy-load các diagram subtype theo nhu cầu. Đây là đúng về lý thuyết. Trong thực tế, nó tạo ra production problem khi kết hợp với Vite's content-hash chunk strategy.

Sau một deployment mới, old chunk filename bị xóa. User có cached old app shell trong browser sẽ cố dynamic import một diagram subtype chunk tại URL không còn tồn tại. Import fail, diagram không render, và không có graceful fallback — component đơn giản là bị vỡ. Đây là một loại failure khác với render chậm hay output sai.

MERMAID · STALE CHUNK FAILURE + FIX Vấn đề: stale chunk import sau deploy old app shell trong browser cache import() mermaid-flowchart -abc123.js → 404 diagram vỡ Fix: content-hash stability + service worker force-reload new deploy SW phát hiện update force reload shell hash URL mới đã sẵn sàng import() resolve đúng chunk mọi lúc diagram render Bundle note: lazy import theo diagram type giữ initial bundle nhỏ. manualChunks mega-bundle giải quyết stale import nhưng block first render — đó là tradeoff sai.

Fix gồm hai phần phải cùng tồn tại. Content-hash filenames — hành vi mặc định của Vite — đảm bảo chunk URL ổn định trong một deployment. Service worker phát hiện version thay đổi và force reload app shell đảm bảo rằng stale chunk reference từ deployment cũ không bao giờ được attempt. Kết hợp, chúng ngăn failure case. Cách tiếp cận thay thế — bundle tất cả Mermaid diagram type vào một vendor chunk lớn — loại bỏ stale import problem nhưng tạo ra synchronous load cost block initial render. Cả hai vấn đề tồn tại độc lập. Cả hai cần fix riêng.

Tóm tắt kiến trúc

DevPulse không phải hệ thống phức tạp theo tiêu chuẩn hiện đại. Điều thú vị là một số lượng nhỏ quyết định thiết kế — đặt indexing boundary ở đâu, dùng vector search primitive đúng cách, represent chunk metadata như thế nào, abstract LLM call ra sao — quyết định liệu hệ thống có hoạt động mạch lạc ở tầm user experience hay không.

Tầng Quyết định chính Điều nó cho phép
Write pipeline Sync callable thay vì async trigger Zero consistency window giữa write và AI awareness
Vector retrieval Firestore Vector Search (ANN) Chi phí retrieval không đổi bất kể collection size
Chunking Paragraph split + entity title prefix Retrieval precision cao hơn không cần thay model
Generation Ground-only instruction Loại bỏ hallucination tự tin về project-specific data
AI middleware Pre/post hook pipeline Monitoring, safety, quality gating viết một lần cho mọi feature
Frontend Content hash + SW reload Loại bỏ stale chunk failure sau deploy

Stack bản thân là thông thường. Giá trị nằm ở các lựa chọn cụ thể ở mỗi tầng và sự hiểu biết về chi phí và lợi ích của từng lựa chọn. Async trigger rẻ hơn để xây và phá user trust. Synchronous callable khó xây hơn và duy trì được trust đó. Sự đánh đổi đó chính là công việc.

]]>
#RAG#firebase#firestore#vector-search#gemini#architecture#cloud-functions#real-time
← Back to Articles