Với một dự án LangGraph kiểu RAG production như bạn mô tả:
1 layout_analysis
2 chunk
3 embedding
4 qdrant
5 llm
thì cách tổ chức folder rất quan trọng.
Vì LangGraph project KHÔNG nên tổ chức kiểu:
script-based
notebook-style
"mọi thứ trong 1 file"
Mà nên tổ chức theo:
workflow orchestration + domain module separation
1. Kiến trúc folder production-grade tôi khuyên dùng
Tôi sẽ đề xuất structure kiểu này:
1 project_root/
2 │
3 ├── README.md
4 ├── pyproject.toml
5 ├── langgraph.json
6 ├── .env
7 │
8 ├── src/
9 │ └── rag_system/
10 │
11 │ ├── graph/
12 │ │ ├── builder.py
13 │ │ ├── nodes/
14 │ │ │ ├── ingest.py
15 │ │ │ ├── retrieve.py
16 │ │ │ ├── rerank.py
17 │ │ │ ├── generate.py
18 │ │ │ └── summarize.py
19 │ │ │
20 │ │ ├── edges/
21 │ │ │ └── routing.py
22 │ │ │
23 │ │ └── state.py
24 │ │
25 │ ├── services/
26 │ │ ├── layout_analysis/
27 │ │ │ ├── parser.py
28 │ │ │ ├── ocr.py
29 │ │ │ └── models.py
30 │ │ │
31 │ │ ├── chunking/
32 │ │ │ ├── semantic_chunk.py
33 │ │ │ ├── recursive_chunk.py
34 │ │ │ └── splitter.py
35 │ │ │
36 │ │ ├── embedding/
37 │ │ │ ├── embedder.py
38 │ │ │ ├── bge.py
39 │ │ │ └── e5.py
40 │ │ │
41 │ │ ├── vectordb/
42 │ │ │ ├── qdrant_client.py
43 │ │ │ ├── schema.py
44 │ │ │ └── retrieval.py
45 │ │ │
46 │ │ └── llm/
47 │ │ ├── openai_client.py
48 │ │ ├── prompts.py
49 │ │ └── generation.py
50 │ │
51 │ ├── pipelines/
52 │ │ ├── ingestion_pipeline.py
53 │ │ ├── indexing_pipeline.py
54 │ │ └── query_pipeline.py
55 │ │
56 │ ├── configs/
57 │ │ ├── settings.py
58 │ │ └── models.yaml
59 │ │
60 │ ├── schemas/
61 │ │ ├── document.py
62 │ │ ├── chunk.py
63 │ │ └── retrieval.py
64 │ │
65 │ ├── utils/
66 │ │ ├── logging.py
67 │ │ ├── tokenizer.py
68 │ │ └── metrics.py
69 │ │
70 │ └── api/
71 │ ├── main.py
72 │ └── routes/
73 │
74 ├── tests/
75 │
76 └── scripts/
77 ├── ingest.py
78 └── evaluate.py
2. Tư duy QUAN TRỌNG nhất
Bạn phải tách:
Layer Vai trò graph orchestration services business logic pipelines end-to-end flows schemas data contract configs configuration api serving
3. LangGraph chỉ nên làm orchestration
Đây là mistake phổ biến.
Nhiều người viết:
1 node ( ) :
2 parse pdf
3 chunk
4 embed
5 store qdrant
6 retrieve
=> sai kiến trúc.
LangGraph node nên:
1 def retrieve_node ( state ) :
2 docs = retrieval_service . search ( . . . )
3 return { . . . }
Tức là:
node = orchestration
service = implementation
4. Graph folder
Đây là layer workflow.
Ví dụ:
chứa:
state.py
Shared state:
1 class RAGState ( TypedDict ) :
2 query : str
3 retrieved_docs : list
4 reranked_docs : list
5 answer : str
builder.py
1 builder = StateGraph ( RAGState )
2
3 builder . add_node ( . . . )
4 builder . add_edge ( . . . )
nodes/
Mỗi file:
Ví dụ:
1 retrieve.py
2 generate.py
3 rerank.py
5. services/ mới là trái tim hệ thống
Đây là nơi chứa actual implementation.
A. layout_analysis/
Ví dụ:
OCR
PDF parsing
table extraction
layout detection
Bạn từng làm CV nên chắc quen kiểu:
1 PDF
2 → detect block
3 → title
4 → paragraph
5 → table
B. chunking/
Tách riêng chunking rất quan trọng.
Vì production thường có:
semantic chunk
recursive chunk
markdown chunk
layout-aware chunk
C. embedding/
Abstract embedding provider.
Ví dụ:
1 class Embedder :
2 embed ( texts )
Sau này swap:
rất dễ.
D. vectordb/
Tách Qdrant riêng.
Không được để Qdrant code khắp project.
Ví dụ:
1 class QdrantRepository :
2 upsert ( )
3 search ( )
Đây là repository pattern.
E. llm/
LLM wrapper layer.
Ví dụ:
prompt
generation
structured output
retry
streaming
6. pipelines/
Đây là abstraction cao hơn service.
Ví dụ:
ingestion_pipeline.py
1 pdf
2 → layout analysis
3 → chunk
4 → embed
5 → qdrant
query_pipeline.py
1 query
2 → retrieve
3 → rerank
4 → generate
7. Tại sao phải tách pipeline và graph
Vì:
Pipeline Graph reusable orchestration deterministic agentic callable directly workflow runtime
8. Qdrant nên nằm ở đâu?
NÊN:
KHÔNG nên:
vì sau này:
Milvus
Weaviate
Elasticsearch
có thể swap.
Tên folder nên abstract:
9. Kiến trúc ingestion flow thực tế
1 PDF
2 ↓
3 layout analysis
4 ↓
5 document structure
6 ↓
7 chunking
8 ↓
9 embedding
10 ↓
11 qdrant upsert
10. Query flow thực tế
1 User Query
2 ↓
3 query rewrite
4 ↓
5 retrieve
6 ↓
7 rerank
8 ↓
9 context compression
10 ↓
11 LLM generation
12 ↓
13 citation formatting
11. Nếu dùng LangGraph
Graph nên chỉ orchestration query-time.
KHÔNG nên orchestration ingestion-time trừ khi:
ingestion async lớn
distributed workflow
retry phức tạp
12. Ví dụ LangGraph cho query
1 START
2 ↓
3 rewrite_query
4 ↓
5 retrieve_docs
6 ↓
7 rerank_docs
8 ↓
9 generate_answer
10 ↓
11 END
13. Nếu muốn scale production lớn
Bạn sẽ cần thêm:
1 infra/
2 deployment/
3 observability/
4 evaluation/
Ví dụ:
tracing
LangSmith
Prometheus
benchmark
offline eval
14. Với background của bạn
Tôi nghĩ bạn nên follow architecture này:
1 services/
2 layout_analysis/
3 chunking/
4 embedding/
5 retrieval/
6 llm/
vì bạn đang thiên:
computer vision
multimodal
document AI
nên layout-aware RAG sẽ rất hợp.
15. Một điều QUAN TRỌNG
Rất nhiều người build LangGraph sai kiểu:
LangGraph = whole backend
Thực ra đúng hơn là:
LangGraph = orchestration runtime
Nó không nên chứa:
embedding logic
qdrant logic
OCR logic
business logic
Mà chỉ:
coordinate
route
retry
manage state
parallelize tasks
Đây là mindset production-grade AI system.
trong module src/graph, tôi thấy bạn chia ra builder.py, nodes/, edges/ state.py. thì tôi hiểu state là để chứa các state trao đổi giữa các node (hoặc các agent), nhưng tại sao lại có cải file edges. tôi thường thấy người ta chỉ định nghĩa các hàm node, rồi add node (tức là add tên hàm đó) vào graph, rồi gắn edge giữa các node, tại sao bạn lại cần cả 1 file edge riêng?
Bạn đang chạm đúng vào một điểm kiến trúc rất hay của LangGraph.
Thực tế:
phần lớn project LangGraph KHÔNG có folder edges/.
Và đúng như bạn nói, người ta thường viết:
builder.add_edge("retrieve", "generate")
hoặc:
builder.add_conditional_edges(...)
ngay trong builder.py.
Điều này hoàn toàn bình thường.
1. Vậy tại sao tôi vẫn tách edges/?
Vì tôi đang nghĩ theo hướng:
production orchestration logic
chứ không phải:
demo workflow
linear agent.
2. Edge trong LangGraph thực ra không chỉ là “đường nối”
Đây là insight QUAN TRỌNG nhất.
Trong workflow đơn giản:
edge chỉ là:
Nhưng trong production agent:
edge thường chứa:
1 routing logic
2 branching logic
3 retry policy
4 conditional flow
5 dynamic dispatch
6 tool selection
7 multi-agent coordination
Lúc này edge bắt đầu trở thành:
3. Ví dụ đơn giản
Static edge
builder.add_edge("retrieve", "generate")
Không cần file riêng.
Conditional edge
Ví dụ:
1 if confidence < threshold :
2 return "web_search"
3
4 if no_docs :
5 return "rewrite_query"
6
7 return "generate"
lúc này:
routing logic bắt đầu phức tạp.
4. Một production routing function thường như này
Ví dụ:
1 def retrieval_router ( state ) :
2 if not state [ "docs" ] :
3 return "rewrite_query"
4
5 if state [ "confidence" ] < 0.7 :
6 return "web_search"
7
8 if state [ "requires_multihop" ] :
9 return "decompose_query"
10
11 return "generate"
Lúc này:
edge không còn “simple edge” nữa,
nó là policy engine rồi.
5. Tại sao tôi thích tách edges/
Vì routing logic thường:
grow rất nhanh,
khó maintain,
dễ spaghetti hơn node logic.
6. Node và Edge có role khác nhau
Thành phần Vai trò node làm việc edge quyết định đi đâu tiếp state shared memory
Ví dụ:
Node
→ thực hiện retrieval.
Edge
→ quyết định workflow.
7. Trong AI Agent system hiện đại
Routing là phần KHÓ nhất.
Không phải prompt.
Ví dụ:
query decomposition
fallback model
retry
hallucination detection
tool selection
đều là edge logic.
8. Khi nào KHÔNG cần edges/
Nếu workflow:
1 START
2 → retrieve
3 → generate
4 → END
thì:
Để trong builder.py là đẹp nhất.
9. Khi nào NÊN có edges/
Khi có:
A. Conditional routing
1 if confidence low
2 if docs empty
3 if timeout
B. Multi-agent orchestration
1 planner
2 → researcher
3 → coder
4 → verifier
C. Dynamic dispatch
Ví dụ:
1 legal question → legal agent
2 finance question → finance agent
D. Retry policy
1 retry retrieval
2 retry tool
3 fallback model
10. Open Deep Research có cần edges/ không?
Thực ra:
hiện tại chưa cần nhiều,
vì workflow của họ khá linear.
Nên họ để:
routing logic inline luôn.
Điều đó hợp lý.
11. Nhưng nếu bạn build RAG thật
Bạn sẽ sớm gặp:
1 empty retrieval
2 low score
3 hybrid search fallback
4 web fallback
5 multimodal branch
6 OCR branch
Lúc này routing complexity sẽ tăng mạnh.
12. Ví dụ cực thực tế với hệ của bạn
Bạn có:
Bạn có thể có edge logic kiểu:
1 def ingestion_router ( state ) :
2
3 if state [ "doc_type" ] == "pdf" :
4 return "layout_analysis"
5
6 if state [ "doc_type" ] == "image" :
7 return "ocr"
8
9 if state [ "contains_table" ] :
10 return "table_parser"
11
12 return "chunking"
Đây là orchestration logic.
13. Đây là lý do nhiều người gọi LangGraph là:
AI workflow state machine
vì:
node = state transition handler
edge = transition rule
14. Kiến trúc tôi khuyên cho bạn
Nếu project nhỏ
1 graph/
2 builder.py
3 nodes.py
4 state.py
là đủ.
Nếu project vừa/lớn
1 graph/
2 builder.py
3 nodes/
4 edges/
5 state.py
sẽ dễ maintain hơn.
15. Insight QUAN TRỌNG nhất
Bạn không cần nghĩ:
Mà nên nghĩ:
edge = workflow decision logic
Đó mới là mindset đúng khi build complex LangGraph systems.
à file edge này thực chất là chứa các hàm để conditional edge đúng không? chứ các hàm truyền thẳng thì không cần ghi vào đây?
Đúng rồi. Bạn hiểu chính xác rồi.
Trong đa số trường hợp:
thực chất là nơi chứa:
routing functions cho add_conditional_edges().
1. Static edge KHÔNG cần file riêng
Ví dụ:
builder.add_edge("retrieve", "generate")
Đây chỉ là:
transition cố định,
không có logic.
Thường viết trực tiếp trong builder.py.
2. Cái cần tách ra là:
1 builder . add_conditional_edges (
2 "retrieve" ,
3 retrieval_router
4 )
vì:
retrieval_router() chứa business workflow logic.
3. Tư duy đúng là:
Loại edge Có cần file riêng? static edge thường không conditional edge thường có dynamic routing nên có multi-agent dispatch nên có
4. Ví dụ thực tế
Static edge
1 builder . add_edge ( "chunk" , "embedding" )
2 builder . add_edge ( "embedding" , "store_qdrant" )
Không cần:
chunk_to_embedding_edge.py
vì vô nghĩa.
Conditional edge
1 builder . add_conditional_edges (
2 "retrieve" ,
3 retrieval_router
4 )
THÌ nên tách.
5. Vì conditional edge thực ra là policy logic
Ví dụ:
1 if score low :
2 retry
3
4 if image detected :
5 multimodal branch
6
7 if table :
8 table parser
Đây không còn là:
graph structure thuần nữa.
Nó là:
6. Một insight QUAN TRỌNG
Trong LangGraph:
1 node = execution
2 edge = control flow
Conditional edge chính là:
7. Thực tế nhiều project không có folder edges/
Họ viết luôn:
1 def route ( state ) :
2 . . .
3
4 builder . add_conditional_edges (
5 "retrieve" ,
6 route
7 )
ngay trong builder.py.
Điều này hoàn toàn OK nếu:
8. Tôi thường chỉ tách edges/ khi có:
A. nhiều routing function
Ví dụ:
1 retrieval_router
2 agent_router
3 tool_router
4 retry_router
5 fallback_router
B. routing logic dài
Ví dụ:
1 if . . .
2 elif . . .
3 elif . . .
4 elif . . .
C. cần unit test routing riêng
D. multi-agent orchestration
Ví dụ:
supervisor chọn subagent,
planner chọn workflow.
9. Với project RAG của bạn
Tôi nghĩ ban đầu bạn có thể:
1 graph/
2 builder.py
3 nodes/
4 state.py
là đủ.
10. Sau này khi complexity tăng
Ví dụ:
OCR branch
image branch
table branch
fallback retrieval
web search fallback
hallucination retry
thì mới thêm:
11. Một pattern production rất phổ biến
builder.py:
1 builder . add_conditional_edges (
2 "retrieve" ,
3 retrieval_router
4 )
edges/retrieval_router.py
1 def retrieval_router ( state ) :
2
3 if state [ "score" ] < 0.5 :
4 return "rewrite"
5
6 return "generate"
Đây là clean nhất.
12. Điều QUAN TRỌNG nhất
Đừng over-architect sớm.
LangGraph repo thường intentionally:
flat,
simple,
procedural.
Và điều đó đúng.
Bạn chỉ nên tách:
khi orchestration logic thật sự bắt đầu phức tạp.
services/
└── layout_analysis/
│
├── ocr/
│ ├── base.py
│ ├── paddle_ocr.py
│ ├── vietocr.py
│ ├── predictor.py
│ ├── preprocess.py
│ ├── postprocess.py
│ └── schemas.py
│
├── parser/
│ ├── pdf_parser.py
│ └── image_parser.py
│
└── layout/
├── layout_detector.py
└── layout_models.py
giả dụ service/ocr/paddleocr.py và service/layout-analysis/paddlelayout.py của tôi đều là model convert sang tensorrt, đều chung format load engine, allocate bufffer, nhưng nó lại thuộc 2 service khác nhau, thì tôi vẫn nên để mỗi service 1 base.py riêng, hay nên có 1 base chung? vì bạn biết mà, model tensorrt infrene thì na ná giống nhau, chứ khác với model pytorch thông thường
Trong case này, bạn đang gặp một tình huống rất điển hình của production AI/CV systems:
NHƯNG:
inference runtime lại giống nhau:
TensorRT engine
CUDA buffer
stream
enqueueV3
bindings
memory management
Và đây chính là lúc:
shared infrastructure abstraction
bắt đầu có ý nghĩa.
1. Câu trả lời ngắn gọn
Tôi khuyên:
✅ VẪN giữ:
ocr/base.py
layout/base.py
cho domain contract.
NHƯNG:
✅ thêm một:
shared TensorRT infrastructure base
ở tầng thấp hơn.
2. Đây là điều QUAN TRỌNG nhất
Bạn đang có HAI loại abstraction khác nhau:
Abstraction Vai trò domain abstraction OCR/Layout runtime abstraction TensorRT
3. Domain abstraction
Ví dụ:
OCR
1 class BaseOCR :
2
3 def predict (
4 self ,
5 input : OCRInput
6 ) - > OCROutput :
7 pass
Layout
1 class BaseLayoutDetector :
2
3 def predict (
4 self ,
5 input : LayoutInput
6 ) - > LayoutOutput :
7 pass
Đây là:
business/domain contract.
4. Runtime abstraction
Đây là thứ shared.
Ví dụ:
1 TensorRT engine loading
2 buffer allocation
3 cuda stream
4 bindings
5 context . execute_async_v3 ( )
5. Kiến trúc clean nhất cho bạn
Tôi cực kỳ khuyên:
1 services/
2 │
3 ├── inference/
4 │ └── tensorrt/
5 │ ├── base.py
6 │ ├── engine.py
7 │ ├── buffers.py
8 │ └── utils.py
9 │
10 ├── layout_analysis/
11 │ ├── ocr/
12 │ │ ├── base.py
13 │ │ ├── paddle_ocr_trt.py
14 │ │ └── schemas.py
15 │ │
16 │ └── layout/
17 │ ├── base.py
18 │ ├── paddle_layout_trt.py
19 │ └── schemas.py
6. Shared TensorRT base
Ví dụ:
1 # services/inference/tensorrt/base.py
2
3 class TensorRTModel :
4
5 def __init__ ( self , engine_path ) :
6
7 self . engine = self . load_engine ( engine_path )
8
9 self . context = self . engine . create_execution_context ( )
10
11 self . allocate_buffers ( )
12
13 def load_engine ( self , path ) :
14 . . .
15
16 def allocate_buffers ( self ) :
17 . . .
18
19 def infer ( self , inputs ) :
20 . . .
7. OCR kế thừa runtime base
Ví dụ:
1 # paddle_ocr_trt.py
2
3 from services . inference . tensorrt . base import TensorRTModel
4 from . base import BaseOCR
5
6
7 class PaddleOCRTensorRT (
8 TensorRTModel ,
9 BaseOCR
10 ) :
11
12 def predict ( self , input ) :
13
14 tensor = self . preprocess ( input )
15
16 outputs = self . infer ( tensor )
17
18 return self . postprocess ( outputs )
8. Layout detector cũng reuse
1 class PaddleLayoutTensorRT (
2 TensorRTModel ,
3 BaseLayoutDetector
4 ) :
9. Đây là production-grade architecture thật sự
Vì bạn đang tách:
tầng infrastructure
1 TensorRT runtime
2 CUDA
3 memory
khỏi tầng business/domain
10. Đây là mindset rất QUAN TRỌNG
Đừng nghĩ:
mà hãy nghĩ:
1 Domain Layer
2 ↓
3 Inference Runtime Layer
4 ↓
5 CUDA/TensorRT Layer
11. TensorRT project production thường rất giống nhau ở tầng thấp
Ví dụ:
YOLO TRT
OCR TRT
SAM TRT
CLIP TRT
đều có:
deserialize engine
allocate buffer
memcpy
stream synchronize
Nên shared layer rất hợp lý.
12. Nhưng KHÔNG nên làm thế này
Sai:
rồi:
OCR kế thừa
Embedding kế thừa
Retrieval kế thừa
=> abstraction quá generic.
13. Shared base chỉ nên dùng khi có:
✅ runtime behavior giống nhau thật.
TensorRT đúng là trường hợp như vậy.
14. Tôi còn khuyên thêm 1 tầng nữa cho bạn
Ví dụ:
1 services/
2 └── inference/
3 ├── tensorrt/
4 ├── pytorch/
5 └── onnxruntime/
Sau này:
TRT
ONNXRuntime
TorchScript
có thể coexist.
15. Một kiến trúc cực đẹp cho CV systems
1 domain
2 ↓
3 runtime
4 ↓
5 hardware
Ví dụ:
1 OCR
2 ↓
3 TensorRT
4 ↓
5 CUDA
16. Điều QUAN TRỌNG nhất
Bạn nên tách:
Layer Ownership OCRInput/OCROutput domain TensorRT engine infrastructure CUDA buffer infrastructure OCR postprocess domain TRT inference runtime
17. Với background Jetson/TensorRT của bạn
Tôi nghĩ architecture đúng nhất sẽ là:
1 services/
2 inference/
3 tensorrt/
4
5 layout_analysis/
6 ocr/
7
8 embedding/
9
10 retrieval/
vì TensorRT runtime chắc chắn sẽ được reuse rất nhiều trong tương lai.
ô hóa ra có kiểu kế thừa từ 2 base thế này à
Đúng rồi 😄
Đây gọi là:
multiple inheritance (đa kế thừa)
trong Python.
Và trong các AI/CV production systems, pattern này dùng khá nhiều khi bạn muốn combine:
domain behavior
infrastructure/runtime behavior
1. Trong ví dụ của bạn
1 class PaddleOCRTensorRT (
2 TensorRTModel ,
3 BaseOCR
4 ) :
thực ra class này đang combine:
Parent class Vai trò TensorRTModel runtime/inference engine BaseOCR OCR contract/domain interface
2. Tư duy rất QUAN TRỌNG
Bạn không còn nghĩ:
mà nên nghĩ:
class = composition of capabilities
3. TensorRTModel
Cho capability:
1 load engine
2 allocate buffers
3 infer()
4 cuda stream
4. BaseOCR
Cho capability:
predict(input: OCRInput) -> OCROutput
5. Class cuối cùng combine cả hai
1 class PaddleOCRTensorRT (
2 TensorRTModel ,
3 BaseOCR
4 ) :
=> cực hợp lý.
6. Đây là pattern rất phổ biến trong AI systems
Ví dụ:
1 class YOLOTensorRT (
2 TensorRTModel ,
3 BaseDetector
4 )
hoặc:
1 class CLIPONNX (
2 ONNXRuntimeModel ,
3 BaseEmbedder
4 )
7. Multiple inheritance KHÔNG xấu
Nhiều người backend truyền thống sợ multiple inheritance.
Vì trong OOP classic:
dễ diamond problem,
hard debugging.
NHƯNG:
Trong AI engineering:
mix capability rất common,
đặc biệt inference/runtime layer.
8. Điều QUAN TRỌNG là:
Mỗi base phải:
có responsibility rõ ràng,
ít overlap.
9. Trong ví dụ của bạn
Đây là GOOD multiple inheritance.
Vì:
TensorRTModel
phụ trách:
1 runtime
2 hardware
3 inference engine
BaseOCR
phụ trách:
business/domain interface
Không overlap.
=> clean.
10. Đây là thứ rất giống mixin pattern
Bạn có thể nghĩ:
TensorRTModel = inference mixin
11. Production AI repo dùng pattern này rất nhiều
Đặc biệt:
TensorRT
ONNXRuntime
Accelerate
distributed inference
async serving
12. Ví dụ thực tế rất đẹp
Shared runtime
1 class TensorRTModel :
2
3 def infer ( self , tensor ) :
4 . . .
Domain contract
1 class BaseOCR :
2
3 def predict ( self , input ) :
4 . . .
Final implementation
1 class PaddleOCRTRT (
2 TensorRTModel ,
3 BaseOCR
4 ) :
13. Một insight cực kỳ production-grade
Bạn đang tách:
khỏi:
Ví dụ:
Layer Meaning BaseOCR WHAT TensorRTModel HOW PaddleOCRTRT concrete implementation
14. Đây là abstraction rất mạnh
Vì sau này bạn có thể:
cùng OCR
nhưng:
1 PyTorch
2 TensorRT
3 ONNXRuntime
Ví dụ:
1 class PaddleOCRTorch (
2 TorchModel ,
3 BaseOCR
4 )
15. Đây chính là "orthogonal abstraction"
Tức là:
là hai chiều độc lập.
16. Một lưu ý QUAN TRỌNG
Đừng abuse multiple inheritance.
GOOD
BAD
1 BaseAI
2 BaseVision
3 BaseInference
4 BasePredictor
5 BaseLogger
6 BaseManager
=> spaghetti inheritance.
17. Rule rất hay
Nếu:
hai base giải quyết hai concern độc lập,
không share state lộn xộn,
THÌ multiple inheritance rất ổn.
18. Trong AI/CV systems
Tôi thấy pattern này đặc biệt hợp với:
Shared capability Domain TensorRT OCR ONNXRuntime Detection batching Embedding async inference VLM streaming LLM
19. Với background TensorRT/Jetson của bạn
Tôi nghĩ bạn sẽ rất hợp với architecture kiểu:
1 domain abstraction
2 +
3 runtime abstraction
vì TensorRT runtime thực sự reusable cực mạnh giữa:
OCR
detection
segmentation
embedding
multimodal encoder.
Tức là:
services/
trả lời:
HOW to do something
workflows/
trả lời:
WHEN and IN WHAT ORDER
Với project của bạn
Tôi nghĩ structure hiện tại rất hợp lý:
src/
│
├── services/
│
├── graph/
│
├── workflows/
│
└── bootstrap/
8. Ý nghĩa từng layer
services/
business/infrastructure capabilities
Ví dụ:
qdrant search,
embedding,
parser,
llm generation.
workflows/
business flows
Ví dụ:
rag,
ingestion,
log analysis.
graph/
LangGraph orchestration infra
bootstrap/
application lifecycle
Tức là:
Workflow nên đọc như:
business story
Ví dụ:
embed query
↓
retrieve chunks
↓
expand chunks
↓
build prompt
↓
generate answer
KHÔNG nên đọc như:
build MatchAny filter
↓
build SearchParams
↓
query_points
Điều tôi chỉ muốn tweak nhẹ
Tôi sẽ recommend:
workflows/
nên chứa:
graph-level orchestration.
Ví dụ:
workflows/
│
├── rag/
│ ├── graph.py
│ ├── nodes.py
│ ├── state.py
│ └── prompts.py
THAY VÌ:
generic graph/ folder riêng.
17. Vì sao?
Hiện tại bạn có:
graph/
Nhưng graph nào?
rag graph?
ingestion graph?
log graph?
=> eventually:
graph/
sẽ become ambiguous.
Tôi recommend future structure
BETTER
workflows/
│
├── rag/
│ ├── graph/
│ │ ├── builder.py
│ │ ├── state.py
│ │ ├── nodes/
│ │ └── edges/
│ │
│ ├── prompts/
│ └── service.py
│
├── ingestion/
│ ├── graph/
│ └── service.py
│
└── log_analysis/
├── graph/
└── service.py
src/
├── bootstrap/
├── configs/
├── services/ # Các service dùng chung toàn hệ thống (DB, LLM Client, VectorDB Client)
├── utils/
└── workflows/ # Mỗi workflow là một thực thể độc lập, đóng gói hoàn chỉnh
├── rag/
│ ├── graph/
│ │ ├── builder.py # Nơi compile graph, add_node, add_edge
│ │ ├── state.py # Định nghĩa State (TypedDict/Pydantic) của riêng RAG
│ │ ├── nodes/ # Thư mục chứa logic các bước xử lý
│ │ └── edges/ # Logic điều hướng (Conditional edges)
│ ├── prompts/ # System prompts, few-shot examples cho RAG
│ └── service.py # Giao diện gọi ngoài: def run_rag_flow(query): ...
│
└── ingestion/
├── graph/
└── service.py
Nhưng RAG của tôi có nhiều kiểu RAG, RAG lên DB question-answer sẽ khác, RAG lên DB production info sẽ khác, đây là code hiện tại của tôi. Nếu tôi thêm 1 thư mục graph, rồi có builder rồi state vào đây, có làm độn lên không?
Không 😄
THỰC RA lúc này bạn đang bắt đầu chạm tới:
Và đây là lúc:
workflow,
graph,
retrieval strategy,
bắt đầu cần được tách đúng.
1. Điều QUAN TRỌNG nhất
Bạn đang có:
RAG workflow có thể:
giống nhau về orchestration,
nhưng khác retrieval strategy.
Ví dụ bạn nói:
Type Khác nhau QA RAG search QA db Production-info RAG search docs Log RAG search logs Multi-doc RAG doc_id filter Agentic RAG iterative
=> cái khác thực ra là:
KHÔNG nhất thiết:
2. Vì vậy hiện tại
Tôi KHÔNG recommend:
1 workflows/rag/
2 graph/
3 builder.py
4 state.py
vì với scale hiện tại:
hơi overkill,
đúng như bạn nói:
3. Tôi nghĩ bạn đang ở sweet spot này:
KEEP SIMPLE
1 workflows/
2 └── rag/
3 ├── retrieval.py
4 ├── filtering.py
5 ├── expansion.py
6 ├── qa_curation.py
=> rất hợp lý.
4. Thứ bạn THỰC SỰ cần thêm là:
KHÔNG phải:
5. Tôi recommend structure này
CLEAN VERSION
1 workflows/
2 └── rag/
3 │
4 ├── retrieval.py
5 ├── filtering.py
6 ├── expansion.py
7 ├── qa_curation.py
8 │
9 ├── graphs/
10 │ ├── simple_rag.py
11 │ ├── qa_rag.py
12 │ ├── production_rag.py
13 │ └── agentic_rag.py
14 │
15 ├── state.py
16 └── prompts.py
6. Đây là VERY scalable
Vì:
retrieval.py
chứa:
Ví dụ:
1 search_documents ( )
2
3 search_qa ( )
4
5 expand_chunks ( )
6
7 filter_by_doc ( )
7. Còn:
graphs/simple_rag.py
orchestrate:
1 embed
2 ↓
3 retrieve
4 ↓
5 generate
graphs/qa_rag.py
orchestrate:
1 classify
2 ↓
3 retrieve QA db
4 ↓
5 generate
graphs/agentic_rag.py
orchestrate:
1 rewrite
2 ↓
3 retrieve
4 ↓
5 judge
6 ↓
7 retry retrieve
8 ↓
9 generate
8. Đây là separation đúng
Layer Responsibility retrieval.py HOW retrieve graph/*.py WHEN retrieve
9. Điều bạn KHÔNG nên làm
BAD
1 rag_qa/
2 rag_docs/
3 rag_logs/
4 rag_agentic/
=> duplicated retrieval code.
10. Điều đẹp nhất của architecture hiện tại
Là bạn đã có:
atomic retrieval primitives
Ví dụ:
filtering,
expansion,
qa_curation.
=> graph chỉ compose chúng 😄
11. Vậy state.py thì sao?
Tôi nghĩ hiện tại:
ONE SHARED RAG STATE
là đủ.
Ví dụ:
1 class RagState ( TypedDict ) :
2
3 user_query : str
4
5 query_vector : list
6
7 retrieved_chunks : list
8
9 reranked_chunks : list
10
11 answer : str
12
13 retrieval_mode : str
12. Vì semantic vẫn là:
KHÔNG phải:
ingestion state,
log analysis state.
13. Tức là:
SAME DOMAIN
=> cùng state vẫn OK.
14. Chỉ khi nào:
completely different semantics
mới nên:
subgraph riêng,
state riêng.
15. Ví dụ:
ingestion
log analysis
=> hoàn toàn khác.
16. Nhưng:
Workflow Same RAG semantic? QA RAG ✅ production docs RAG ✅ multi-doc RAG ✅ hybrid retrieval RAG ✅
=> cùng:
RagState,
retrieval primitives.
17. Tôi nghĩ architecture đẹp nhất cho bạn hiện tại là:
1 workflows/
2 └── rag/
3 │
4 ├── retrieval.py
5 ├── filtering.py
6 ├── expansion.py
7 ├── qa_curation.py
8 │
9 ├── graphs/
10 │ ├── simple_rag.py
11 │ ├── qa_rag.py
12 │ ├── hybrid_rag.py
13 │ └── agentic_rag.py
14 │
15 ├── state.py
16 └── prompts.py
18. Đây là scalable nhưng chưa over-engineering 😄
Bạn đang dùng graph đúng chỗ 😄
Workflow Graph?
conversational rag ✅
log analysis ✅
document ingestion ❌
qa insertion ❌
delete document ❌
Tôi nghĩ đây là chỗ cực kỳ quan trọng để phân biệt:
1 workflow orchestration
2 vs
3 backend business pipeline
Câu trả lời ngắn gọn 😄
Query / Retrieval
→ rất hợp với LangGraph.
Insert / Ingestion
→ chưa chắc cần graph 😄
1. Vì sao retrieval rất hợp graph 😄
Vì retrieval có:
branching,
routing,
retry,
conditional flow,
memory shortcut,
prompt orchestration,
multi-step reasoning.
Nó là:
2. Nhưng ingestion thường 😄
lại là:
Ví dụ:
1 upload file
2 → parse
3 → chunk
4 → embed
5 → insert
Đây thực ra giống:
KHÔNG phải:
3. Tôi nghĩ hiện tại 😄
Bạn KHÔNG nên cố graph everything.
Đây là lỗi rất nhiều người dùng LangGraph mắc phải 😄
4. Tôi recommend 😄
KEEP SIMPLE
1 src/workflows/
2 ├── rag/
3 ├── log_analysis/
4 └── ingestion/
nhưng:
conversational_rag
→ graph.
log_analysis
→ graph (vì có map-reduce reasoning).
ingestion
→ normal service pipeline.
5. Ví dụ ingestion của bạn 😄
document ingestion
1 parse
2 → chunk
3 → embed
4 → upsert
=> linear deterministic.
qa ingestion
1 question
2 → embed
3 → insert
=> càng không cần graph 😄
6. Khi nào ingestion nên dùng graph 😄
Chỉ khi có:
conditional parsing,
OCR routing,
async retries,
fallback embedding models,
human approval,
dynamic validation,
distributed ingestion.
Ví dụ:
1 if pdf scanned
2 → OCR
3
4 if table-heavy
5 → table parser
6
7 if image-heavy
8 → vision parser
THÌ graph mới đáng 😄
7. Nhưng hiện tại 😄
Backend sẽ call:
1 POST /insert-document
2 POST /insert-qa
=> simple service là đẹp nhất.
8. Tôi recommend architecture này 😄
GOOD
1 src/workflows/
2 ├── rag/
3 ├── log_analysis/
4 └── ingestion/
5 ├── document_ingestion.py
6 ├── qa_ingestion.py
7 └── service.py
service.py
1 insert_document ( )
2
3 insert_question_answer ( )
4
5 delete_document ( )
6
7 delete_temporary_document ( )
9. Và backend layer 😄
sẽ gọi:
1 FastAPI
2 → ingestion service
KHÔNG cần:
10. Một insight QUAN TRỌNG 😄
LangGraph mạnh nhất khi:
execution path is uncertain
KHÔNG phải:
11. Bạn đang dùng graph đúng chỗ 😄
Workflow Graph? conversational rag ✅ log analysis ✅ document ingestion ❌ qa insertion ❌ delete document ❌
Đây là architecture rất hợp lý 😄
12. Một recommendation nữa 😄
Tôi nghĩ ingestion nên tách:
ra khỏi:
Ví dụ:
1 src/api/
2 src/workflows/
3 src/services/
để sau này:
Celery,
Kafka,
async queue,
rất dễ scale 😄
13. Điều QUAN TRỌNG 😄
Đừng biến LangGraph thành:
general backend framework
Nó không optimize cho:
CRUD,
deterministic ETL,
synchronous APIs.
Nó optimize cho:
stateful AI orchestration
Và hiện tại bạn đang dùng đúng hướng 😄
Bạn có cần docker cho integration test không 😄
Có. Rất nên.
Ví dụ:
docker-compose.test.yml
services:
qdrant:
image: qdrant/qdrant
ports:
- "6333:6333"
Rồi pytest chạy:
pytest tests/integration
9. KHÔNG nên 😄
Không nên integration test bằng:
production qdrant
vì:
flaky
polluted data
race conditions
slow ==>
đoạn nội dung này nghĩa là thế nào? Tức là tôi nên tạo 1 qdrant mới để test intergraion ư?
Đúng rồi 😄
Ý là:
Integration test nên có môi trường riêng
đặc biệt với:
Qdrant
Redis
Postgres
Kafka
MinIO
Vì sao?
Integration test là test:
service thật + networking thật + database thật
Ví dụ:
Python app thật
Qdrant thật
insert vector thật
retrieval thật
Nhưng KHÔNG nên test trên production Qdrant
Ví dụ:
đang chứa:
dữ liệu dev thật
collection thật
documents thật
Vì sẽ xảy ra rất nhiều vấn đề
1. Polluted data
Test insert:
xong không cleanup.
Sau vài tuần:
production collection đầy rác test
2. Race conditions
Ví dụ:
app dev đang insert thật
test cũng insert
cùng collection:
=> nondeterministic.
3. Flaky test
Hôm nay pass:
mai fail vì:
dữ liệu production thay đổi.
4. Slow
Production DB:
nhiều data
nhiều index
nhiều segment
=> test chậm.
5. Test có thể phá production
Ví dụ test:
💀
Vì vậy best practice là:
tạo environment riêng cho test
Với Docker thì cực dễ
Ví dụ:
1 # docker-compose.test.yml
2
3 services :
4
5 qdrant-test :
6
7 image : qdrant/qdrant
8
9 ports :
10 - "6339:6333"
Test connect tới:
KHÔNG phải:
production qdrant
dev qdrant.
Flow chuẩn
Khi test bắt đầu
docker compose -f docker-compose.test.yml up -d
Pytest chạy
Sau test
docker compose -f docker-compose.test.yml down
Đây gọi là:
ephemeral test environment
Production engineering rất thích cách này
Vì:
reproducible
isolated
deterministic
CI-friendly
Với project RAG của bạn
Mình cực kỳ khuyên:
Unit test
Mock:
Integration test
Dùng:
Qdrant Docker thật
collection thật
insert/query thật
NHƯNG:
Ví dụ integration test rất đẹp
1 def test_qdrant_retrieval ( ) :
2
3 client . create_collection ( . . . )
4
5 client . upsert ( . . . )
6
7 results = retriever . search ( . . . )
8
9 assert len ( results ) > 0
Thường người ta dùng collection test riêng
Ví dụ:
hoặc:
CI/CD thực tế
GitLab pipeline:
1 services :
2 - qdrant/qdrant
rồi:
spin up container
chạy pytest
destroy container
mọi thứ isolated hoàn toàn.
Một insight rất quan trọng
AI/RAG integration test KHÔNG chỉ là:
mà còn:
retrieval consistency
metadata filtering
chunk expansion
vector dimension compatibility
ANN behavior
Tóm tắt
Đúng 😄
Bạn nên có:
Qdrant riêng cho integration test
thường bằng:
Docker container riêng
port riêng
collection riêng
dữ liệu fixture riêng
Đây là best practice production/backend engineering thật sự.
CI/CD 😄
Đây là phần cực quan trọng.
Flow thực tế 😄
AI Team 😄
push code lên:
GitLab
GitLab CI 😄
trigger:
.gitlab-ci.yml
Pipeline thường 😄
stages:
lint
unit-test
workflow-test
integration-test
build
deploy
Ví dụ 😄
unit-test:
stage: unit-test
script:
- pytest tests/unit
Integration 😄
integration-test:
stage: integration-test
services:
- qdrant/qdrant
script:
- pytest -m integration
Docker build 😄
docker-build:
stage: build
script:
- docker build -t rag-service . ==>
ủa chưa build docker thì làm sao chạy test unit và integration được, phải có môi trường env mới chạy được chứ.
Đây chính là chỗ nhiều người mới làm DevOps/CI hiểu nhầm 😄
Bạn đang nghĩ:
Muốn chạy test thì phải build Docker app trước
Nhưng thực tế CI có nhiều strategy khác nhau.
Flow bạn đang thấy là kiểu:
1 test source code trước
2 → build artifact sau
Đây là workflow rất phổ biến.
Hãy hiểu CI runner là gì trước
GitLab CI khi chạy pipeline sẽ tạo:
1 môi trường Linux sạch tạm thời
Ví dụ:
Ubuntu container
Docker runner
Kubernetes pod
Trong đó:
clone source code của bạn
setup environment
chạy commands.
Nghĩa là test KHÔNG nhất thiết phải chạy trong app Docker
Ví dụ CI:
1 unit-test :
2
3 image : python : 3.11
4
5 script :
6 - pip install - r requirements.txt
7 - pytest tests/unit
Flow thật bên dưới
1 GitLab Runner
2 ↓
3 start python:3.11 container
4 ↓
5 git clone repo
6 ↓
7 pip install dependencies
8 ↓
9 pytest
Đây đã là environment rồi 😄
Chỉ là:
ephemeral CI environment
không phải app Docker image của bạn.
Tại sao test trước build?
Vì:
Nếu test fail thì khỏi build image
tiết kiệm:
CI time
Docker build time
registry storage.
Flow chuẩn production thường là
1 lint
2 ↓
3 unit test
4 ↓
5 integration test
6 ↓
7 build docker image
8 ↓
9 deploy
Vì sao integration test vẫn chạy được?
Do:
1 services :
2 - qdrant/qdrant
Ý nghĩa thật
GitLab sẽ spin up thêm:
cho test.
Kiến trúc thật sự
1 CI Runner Container
2 |
3 |--- pytest
4 |
5 |--- qdrant service container
Tức là
Python test chạy ở:
Qdrant chạy ở:
sidecar service container
Giống docker-compose mini
Ví dụ thực tế
1 integration-test :
2
3 image : python : 3.11
4
5 services :
6 - qdrant/qdrant
7
8 script :
9 - pip install - r requirements.txt
10 - pytest - m integration
GitLab tự tạo network nội bộ
Test của bạn có thể connect:
Đây là cực kỳ phổ biến
Không cần build app Docker trước.
Vậy khi nào mới test Docker image?
Đó là:
container/system/e2e test
Ví dụ
1 build image
2 → run container
3 → test API
Ví dụ CI thật
Stage 1 — unit
Stage 2 — integration
Stage 3 — build image
Stage 4 — container smoke test
1 docker run ...
2 curl localhost : 8000/health
Một insight rất quan trọng
Có 2 loại environment:
1. Runtime environment
Docker image production của app.
2. CI execution environment
Temporary environment để:
Chúng không bắt buộc giống hệt nhau.
Tuy nhiên production team mạnh sẽ cố sync chúng
Ví dụ:
để:
CI env gần production hơn.
Với AI project của bạn
Mình khuyên:
Unit test
KHÔNG cần Docker app.
Chỉ cần:
1 python : 3.11
2 pip install
3 pytest
Integration test
Cần:
qdrant service
maybe redis/postgres
E2E/container test
Lúc này mới:
build Docker app
run app container
test API thật.
Production-grade flow cho bạn sau này
1 lint
2 ↓
3 unit tests
4 ↓
5 integration tests
6 ↓
7 graph workflow tests
8 ↓
9 build docker image
10 ↓
11 container smoke test
12 ↓
13 deploy staging
14 ↓
15 eval tests
16 ↓
17 deploy prod
Điều cực hay ở CI hiện đại
CI không chỉ test code.
Nó test:
infra
orchestration
networking
containers
workflows
deployment contracts
luôn 😄
ừ tôi biết rồi, nhưng AI của tôi có nhiều requirement lắm, giả sử OCR thì có ultralytics (detect text), rồi paddlex[ocr], rồi torch, vậy thì tôi chỉ cần ghi hết các dependencies này ở file requirements trong repo của tôi là được đúng không? Bên CI env sẽ tự tạo env chứa các dependencies này?
Đúng rồi 😄
Đó chính là ý tưởng của CI/CD hiện đại:
repo = source of truth cho environment
Nghĩa là
Bạn commit:
hoặc:
pyproject.toml
poetry.lock
environment.yml
Rồi CI runner sẽ:
1 tự tạo environment sạch
2 → cài dependencies
3 → chạy test
Flow thật
Ví dụ:
1 unit-test :
2
3 image : python : 3.11
4
5 script :
6 - pip install - r requirements.txt
7 - pytest tests/unit
Bên dưới GitLab sẽ làm
1 create fresh container
2 ↓
3 clone repo
4 ↓
5 pip install -r requirements.txt
6 ↓
7 pytest
Nghĩa là environment được recreate mỗi pipeline 😄
Đây là điểm cực mạnh:
Với AI project của bạn
Đúng là dependencies sẽ rất nhiều 😄
Ví dụ:
1 torch
2 transformers
3 ultralytics
4 paddlex[ocr]
5 onnxruntime-gpu
6 qdrant-client
7 langgraph
8 opencv-python
Và đúng 😄
Bạn chỉ cần đảm bảo:
Ví dụ thực tế
1 torch==2.7.1
2 transformers==4.52.0
3 ultralytics==8.3.0
4 paddlex[ocr]
5 qdrant-client==1.14.0
6 langgraph
7 opencv-python
Nhưng AI project có vài vấn đề đặc biệt 😄
1. CI rất chậm nếu install full AI stack mỗi lần
Ví dụ:
có thể:
vài GB
install 10-20 phút.
Production team giải quyết thế nào?
Cách 1 — cache pip
Ví dụ GitLab cache:
1 cache :
2 paths :
3 - .cache/pip
Cách 2 — base Docker image (RẤT PHỔ BIẾN)
Ví dụ build sẵn:
đã chứa:
torch
cuda
ultralytics
OCR libs
Sau đó CI dùng:
Flow sẽ nhanh hơn cực nhiều
Vì:
không phải reinstall torch mỗi lần.
2. GPU dependency rất tricky
Ví dụ:
1 torch==cu126
2 onnxruntime-gpu
3 tensorrt
CI runner thường KHÔNG có GPU
Nên production AI teams thường:
CPU-only test trong CI
Ví dụ:
GPU tests riêng
Trên:
self-hosted GPU runner
nightly pipeline
3. OCR model download
Ví dụ:
PaddleOCR
HuggingFace
Ultralytics
thường auto download model.
CI có thể fail vì:
internet
timeout
nondeterminism
Production best practice
preload model
Hoặc:
cache model
artifact model
bake vào Docker image.
4. AI dependency conflict rất phổ biến 😄
Ví dụ:
torch
torchao
xformers
cuda
onnxruntime
conflict nhau.
Như bạn vừa gặp:
torchao incompatible torch version
Vì vậy AI project nên pin version
KHÔNG nên:
Mà nên:
Với project của bạn
Mình khuyên:
requirements/base.txt
1 fastapi
2 pydantic
3 qdrant-client
4 langgraph
requirements/ai.txt
1 torch==2.7.1
2 transformers
3 ultralytics
4 paddlex[ocr]
requirements/dev.txt
1 pytest
2 pytest-cov
3 ruff
4 mypy
CI có thể chọn install phù hợp
Unit test nhẹ
1 pip install -r requirements/base.txt
2 pip install -r requirements/dev.txt
Full AI integration
pip install -r requirements/ai.txt
Đây là practice rất production-oriented
Vì:
CI nhanh hơn
dependency rõ hơn
ít conflict hơn
Một insight cực kỳ quan trọng
Trong AI backend:
environment reproducibility
khó gần ngang:
Vì:
AI stack phụ thuộc:
CUDA
GPU driver
torch
onnxruntime
model weights
native libs
rất nhiều.
Tóm lại
Đúng 😄
Bạn chỉ cần:
commit dependency files vào repo
CI sẽ tự tạo env sạch
tự install dependencies
tự chạy test
Đó chính là:
reproducible CI
infrastructure-as-code mindset.