690419:1109 feat: update CI/CD to use SSH key authentication #02
This commit is contained in:
+544
@@ -0,0 +1,544 @@
|
||||
|
||||
**LCBP3 DMS**
|
||||
|
||||
**RAG Implementation Guide**
|
||||
|
||||
Retrieval-Augmented Generation for Document Management System
|
||||
|
||||
| Version | 1.0.0 |
|
||||
| :---- | :---- |
|
||||
| **Project** | Laem Chabang Basin Phase 3 |
|
||||
| **Stack** | NestJS \+ Next.js \+ MariaDB \+ Qdrant |
|
||||
| **LLM** | Typhoon API \+ Ollama (nomic-embed-text) |
|
||||
|
||||
# **1\. ภาพรวม Architecture**
|
||||
|
||||
ระบบ RAG ของ LCBP3 DMS ออกแบบให้ทำงานแบบ Hybrid โดยแยกหน้าที่ชัดเจนระหว่าง embedding (local) และ generation (cloud Thai LLM) เพื่อให้ได้ทั้งความเป็นส่วนตัวของข้อมูลในส่วน embedding และคุณภาพภาษาไทยที่ดีในส่วนการตอบคำถาม
|
||||
|
||||
## **1.1 Hybrid Architecture**
|
||||
|
||||
| Flow: PDF/DOCX Upload → OCR (EasyOCR/Tesseract) → Text Chunking → Embedding (Ollama: nomic-embed-text) → Qdrant Vector Store RAG Query → Embed Question → Qdrant Similarity Search → Build Prompt → Typhoon API → Thai Answer |
|
||||
| :---- |
|
||||
|
||||
| Component | เครื่องมือ | ที่อยู่ | หมายเหตุ |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| Document Metadata | MariaDB | เดิม | ไม่เปลี่ยน |
|
||||
| Vector Store | Qdrant | Docker container | เพิ่มใหม่ |
|
||||
| Embedding Model | nomic-embed-text | Ollama local | 768 dimensions |
|
||||
| LLM / Generation | Typhoon API | api.opentyphoon.ai | Thai-first, OpenAI-compatible |
|
||||
| OCR | EasyOCR / Tesseract | Docker microservice | รองรับภาษาไทย |
|
||||
| Async Queue | BullMQ \+ Redis | Docker container | async ingestion |
|
||||
|
||||
# **2\. Chunking Strategy ตาม Document Type**
|
||||
|
||||
แต่ละประเภทเอกสารใน LCBP3 มีโครงสร้างต่างกัน จึงไม่ควรใช้ fixed-size chunking เดียวกันทั้งหมด
|
||||
|
||||
| Document Type | Strategy | Chunk Size | Overlap |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| CORR, MOM | Paragraph-based | \~500 tokens | 50 tokens |
|
||||
| RFI, NCR | Section-based (Q\&A) | \~300 tokens | 30 tokens |
|
||||
| DRAW, SUB | Metadata-heavy | \~200 tokens | 0 tokens |
|
||||
| CONTRACT, INVOICE | Table-aware | \~400 tokens | 40 tokens |
|
||||
| RPT | Sliding window | \~600 tokens | 100 tokens |
|
||||
| TRANS | Header \+ body split | \~350 tokens | 35 tokens |
|
||||
|
||||
| หมายเหตุ nomic-embed-text รองรับ input สูงสุด 8192 tokens แต่ chunk ที่เล็กกว่าให้ precision ดีกว่าในงาน retrieval สำหรับเอกสารที่มีตาราง (CONTRACT, INVOICE) ให้ extract ตารางแยกก่อน แล้ว serialize เป็น text |
|
||||
| :---- |
|
||||
|
||||
# **3\. ขั้นตอนการ Implement โดยละเอียด**
|
||||
|
||||
| 1 | ติดตั้ง Qdrant และ Redis ผ่าน Docker Compose เพิ่ม services ใน docker-compose.yml ที่มีอยู่แล้ว |
|
||||
| :---: | :---- |
|
||||
|
||||
**เพิ่มใน docker-compose.yml:**
|
||||
|
||||
qdrant:
|
||||
|
||||
image: qdrant/qdrant:latest
|
||||
|
||||
container\_name: lcbp3-qdrant
|
||||
|
||||
ports:
|
||||
|
||||
\- "6333:6333"
|
||||
|
||||
volumes:
|
||||
|
||||
\- qdrant\_data:/qdrant/storage
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
|
||||
image: redis:7-alpine
|
||||
|
||||
container\_name: lcbp3-redis
|
||||
|
||||
ports:
|
||||
|
||||
\- "6379:6379"
|
||||
|
||||
volumes:
|
||||
|
||||
\- redis\_data:/data
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
ทดสอบว่า Qdrant ทำงาน: เปิด browser ไปที่ http://localhost:6333/dashboard
|
||||
|
||||
| 2 | เพิ่ม Table document\_chunks ใน MariaDB เก็บ reference ระหว่าง MariaDB กับ Qdrant point ID |
|
||||
| :---: | :---- |
|
||||
|
||||
CREATE TABLE document\_chunks (
|
||||
|
||||
id CHAR(36) PRIMARY KEY, \-- UUID \= Qdrant point ID
|
||||
|
||||
document\_id CHAR(36) NOT NULL,
|
||||
|
||||
chunk\_index INT NOT NULL,
|
||||
|
||||
content TEXT NOT NULL,
|
||||
|
||||
created\_at DATETIME DEFAULT CURRENT\_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (document\_id)
|
||||
|
||||
REFERENCES documents(id) ON DELETE CASCADE
|
||||
|
||||
);
|
||||
|
||||
| 3 | ติดตั้ง Dependencies ใน NestJS ติดตั้ง packages ที่จำเป็นทั้งหมด |
|
||||
| :---: | :---- |
|
||||
|
||||
\# ใน packages/api (NestJS)
|
||||
|
||||
pnpm add @qdrant/js-client-rest
|
||||
|
||||
pnpm add openai
|
||||
|
||||
pnpm add bullmq
|
||||
|
||||
pnpm add @nestjs/bull bull
|
||||
|
||||
pnpm add uuid
|
||||
|
||||
pnpm add \-D @types/uuid
|
||||
|
||||
| 4 | สร้าง RAG Module Structure จัดโครงสร้างไฟล์ใน NestJS |
|
||||
| :---: | :---- |
|
||||
|
||||
**โครงสร้างไฟล์ที่แนะนำ:**
|
||||
|
||||
src/rag/
|
||||
|
||||
rag.module.ts \<- register all providers
|
||||
|
||||
rag.controller.ts \<- POST /api/rag/query
|
||||
|
||||
rag.service.ts \<- orchestrate pipeline
|
||||
|
||||
embedding.service.ts \<- call Ollama nomic-embed-text
|
||||
|
||||
qdrant.service.ts \<- Qdrant CRUD operations
|
||||
|
||||
chunker.service.ts \<- smart chunking per doc type
|
||||
|
||||
llm.service.ts \<- call Typhoon API
|
||||
|
||||
ingestion.processor.ts \<- BullMQ worker
|
||||
|
||||
| 5 | สร้าง EmbeddingService เรียก Ollama nomic-embed-text สำหรับแปลงข้อความเป็น vector |
|
||||
| :---: | :---- |
|
||||
|
||||
// embedding.service.ts
|
||||
|
||||
@Injectable()
|
||||
|
||||
export class EmbeddingService {
|
||||
|
||||
private readonly OLLAMA\_URL \= process.env.OLLAMA\_URL
|
||||
|
||||
?? "http://localhost:11434";
|
||||
|
||||
async embed(text: string): Promise\<number\[\]\> {
|
||||
|
||||
const res \= await fetch(\`${this.OLLAMA\_URL}/api/embeddings\`, {
|
||||
|
||||
method: "POST",
|
||||
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
body: JSON.stringify({
|
||||
|
||||
model: "nomic-embed-text",
|
||||
|
||||
prompt: text,
|
||||
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
if (\!res.ok) throw new Error(\`Embedding failed: ${res.status}\`);
|
||||
|
||||
const { embedding } \= await res.json();
|
||||
|
||||
return embedding;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
| 6 | สร้าง QdrantService จัดการ vector store สำหรับ upsert, search, delete |
|
||||
| :---: | :---- |
|
||||
|
||||
// qdrant.service.ts
|
||||
|
||||
@Injectable()
|
||||
|
||||
export class QdrantService implements OnModuleInit {
|
||||
|
||||
private client: QdrantClient;
|
||||
|
||||
private readonly COLLECTION \= "lcbp3\_docs";
|
||||
|
||||
async onModuleInit() {
|
||||
|
||||
this.client \= new QdrantClient({
|
||||
|
||||
url: process.env.QDRANT\_URL ?? "http://localhost:6333",
|
||||
|
||||
});
|
||||
|
||||
await this.ensureCollection();
|
||||
|
||||
}
|
||||
|
||||
private async ensureCollection() {
|
||||
|
||||
const { collections } \= await this.client.getCollections();
|
||||
|
||||
const exists \= collections.some(c \=\> c.name \=== this.COLLECTION);
|
||||
|
||||
if (\!exists) {
|
||||
|
||||
await this.client.createCollection(this.COLLECTION, {
|
||||
|
||||
vectors: { size: 768, distance: "Cosine" },
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async upsert(id: string, vector: number\[\], payload: Record\<string, any\>) {
|
||||
|
||||
await this.client.upsert(this.COLLECTION, {
|
||||
|
||||
points: \[{ id, vector, payload }\],
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async search(vector: number\[\], topK \= 5, filter?: Record\<string, any\>) {
|
||||
|
||||
return this.client.search(this.COLLECTION, {
|
||||
|
||||
vector,
|
||||
|
||||
limit: topK,
|
||||
|
||||
filter: filter ? {
|
||||
|
||||
must: Object.entries(filter).map((\[key, value\]) \=\> ({
|
||||
|
||||
key, match: { value },
|
||||
|
||||
})),
|
||||
|
||||
} : undefined,
|
||||
|
||||
with\_payload: true,
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async deleteByDocumentId(documentId: string) {
|
||||
|
||||
await this.client.delete(this.COLLECTION, {
|
||||
|
||||
filter: { must: \[{ key: "document\_id", match: { value: documentId } }\] },
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
| 7 | สร้าง LlmService (Typhoon API) เรียก Typhoon สำหรับ generate คำตอบ |
|
||||
| :---: | :---- |
|
||||
|
||||
// llm.service.ts
|
||||
|
||||
import OpenAI from "openai";
|
||||
|
||||
@Injectable()
|
||||
|
||||
export class LlmService {
|
||||
|
||||
private client: OpenAI;
|
||||
|
||||
constructor() {
|
||||
|
||||
this.client \= new OpenAI({
|
||||
|
||||
apiKey: process.env.TYPHOON\_API\_KEY,
|
||||
|
||||
baseURL: "https://api.opentyphoon.ai/v1",
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async generate(prompt: string, system?: string): Promise\<string\> {
|
||||
|
||||
const response \= await this.client.chat.completions.create({
|
||||
|
||||
model: "typhoon-v2.1-12b-instruct",
|
||||
|
||||
messages: \[
|
||||
|
||||
{ role: "system", content: system ?? LCBP3\_SYSTEM\_PROMPT },
|
||||
|
||||
{ role: "user", content: prompt },
|
||||
|
||||
\],
|
||||
|
||||
max\_tokens: 1024,
|
||||
|
||||
temperature: 0.3,
|
||||
|
||||
top\_p: 0.95,
|
||||
|
||||
repetition\_penalty: 1.05,
|
||||
|
||||
});
|
||||
|
||||
return response.choices\[0\].message.content ?? "";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
**System Prompt สำหรับ LCBP3:**
|
||||
|
||||
const LCBP3\_SYSTEM\_PROMPT \= \`
|
||||
|
||||
You are a document assistant for LCBP3 (Laem Chabang Basin Phase 3),
|
||||
|
||||
a large-scale construction project DMS.
|
||||
|
||||
\- Answer in Thai if the question is in Thai
|
||||
|
||||
\- Answer in English if the question is in English
|
||||
|
||||
\- Always reference document numbers (e.g., CORR-LCBP3-2024-001)
|
||||
|
||||
\- Be concise and factual. Do not speculate beyond the provided context.
|
||||
|
||||
\- If context is insufficient, say so clearly.
|
||||
|
||||
\`;
|
||||
|
||||
| 8 | สร้าง ChunkerService แบ่ง text ตาม strategy ของแต่ละ document type |
|
||||
| :---: | :---- |
|
||||
|
||||
// chunker.service.ts
|
||||
|
||||
@Injectable()
|
||||
|
||||
export class ChunkerService {
|
||||
|
||||
chunk(text: string, docType: string): { text: string }\[\] {
|
||||
|
||||
const strategy \= this.getStrategy(docType);
|
||||
|
||||
return strategy(text);
|
||||
|
||||
}
|
||||
|
||||
private getStrategy(docType: string) {
|
||||
|
||||
const map: Record\<string, (t: string) \=\> { text: string }\[\]\> \= {
|
||||
|
||||
CORR: (t) \=\> this.paragraphChunk(t, 500, 50),
|
||||
|
||||
MOM: (t) \=\> this.paragraphChunk(t, 500, 50),
|
||||
|
||||
RFI: (t) \=\> this.sectionChunk(t, 300, 30),
|
||||
|
||||
NCR: (t) \=\> this.sectionChunk(t, 300, 30),
|
||||
|
||||
RPT: (t) \=\> this.slidingWindow(t, 600, 100),
|
||||
|
||||
CONTRACT: (t) \=\> this.tableAware(t, 400, 40),
|
||||
|
||||
INVOICE: (t) \=\> this.tableAware(t, 400, 40),
|
||||
|
||||
};
|
||||
|
||||
return map\[docType\] ?? ((t) \=\> this.slidingWindow(t, 400, 50));
|
||||
|
||||
}
|
||||
|
||||
// ... implement paragraphChunk, sectionChunk, slidingWindow, tableAware
|
||||
|
||||
}
|
||||
|
||||
| 9 | สร้าง Ingestion Pipeline เชื่อม upload event กับ vector indexing ผ่าน BullMQ queue |
|
||||
| :---: | :---- |
|
||||
|
||||
// rag.service.ts — triggerIngestion
|
||||
|
||||
async ingestDocument(documentId: string) {
|
||||
|
||||
const doc \= await this.documentsService.findOne(documentId);
|
||||
|
||||
const rawText \= await this.extractText(doc.filePath, doc.docType);
|
||||
|
||||
const chunks \= this.chunker.chunk(rawText, doc.docType);
|
||||
|
||||
// ลบ chunks เก่าก่อน (กรณี re-ingest)
|
||||
|
||||
await this.qdrant.deleteByDocumentId(documentId);
|
||||
|
||||
await this.chunkRepo.delete({ documentId });
|
||||
|
||||
for (const \[i, chunk\] of chunks.entries()) {
|
||||
|
||||
const chunkId \= uuidv4();
|
||||
|
||||
const embedding \= await this.embedding.embed(chunk.text);
|
||||
|
||||
await this.qdrant.upsert(chunkId, embedding, {
|
||||
|
||||
document\_id: documentId,
|
||||
|
||||
doc\_type: doc.docType,
|
||||
|
||||
doc\_number: doc.docNumber,
|
||||
|
||||
revision: doc.revision,
|
||||
|
||||
project\_code: "LCBP3",
|
||||
|
||||
chunk\_index: i,
|
||||
|
||||
});
|
||||
|
||||
await this.chunkRepo.save({
|
||||
|
||||
id: chunkId, documentId,
|
||||
|
||||
chunkIndex: i, content: chunk.text,
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
**เรียก triggerIngestion() หลัง upload สำเร็จ:**
|
||||
|
||||
// documents.service.ts — หลัง save document
|
||||
|
||||
await this.ragQueue.add("ingest", { documentId: doc.id });
|
||||
|
||||
| 10 | สร้าง RAG Query API Endpoint สำหรับ frontend เรียกใช้ |
|
||||
| :---: | :---- |
|
||||
|
||||
// rag.service.ts — query
|
||||
|
||||
async query(question: string, filter?: { doc\_type?: string }) {
|
||||
|
||||
// 1\. Embed คำถาม
|
||||
|
||||
const qVector \= await this.embedding.embed(question);
|
||||
|
||||
// 2\. Retrieve top-5 chunks จาก Qdrant
|
||||
|
||||
const hits \= await this.qdrant.search(qVector, 5, filter);
|
||||
|
||||
// 3\. Build context
|
||||
|
||||
const context \= hits.map(h \=\>
|
||||
|
||||
\`\[${h.payload.doc\_type} \- ${h.payload.doc\_number}\]\\n${h.payload.content}\`
|
||||
|
||||
).join("\\n\\n---\\n\\n");
|
||||
|
||||
// 4\. Generate คำตอบผ่าน Typhoon
|
||||
|
||||
const prompt \= \`Context:\\n${context}\\n\\nQuestion: ${question}\`;
|
||||
|
||||
return this.llm.generate(prompt);
|
||||
|
||||
}
|
||||
|
||||
// rag.controller.ts
|
||||
|
||||
@Post("query")
|
||||
|
||||
async query(@Body() dto: { question: string; doc\_type?: string }) {
|
||||
|
||||
return this.ragService.query(dto.question, { doc\_type: dto.doc\_type });
|
||||
|
||||
}
|
||||
|
||||
# **4\. Environment Variables**
|
||||
|
||||
เพิ่มใน .env ของ NestJS:
|
||||
|
||||
\# Typhoon API
|
||||
|
||||
TYPHOON\_API\_KEY=your\_typhoon\_api\_key\_here
|
||||
|
||||
\# Ollama (local)
|
||||
|
||||
OLLAMA\_URL=http://localhost:11434
|
||||
|
||||
\# Qdrant
|
||||
|
||||
QDRANT\_URL=http://localhost:6333
|
||||
|
||||
\# Redis (BullMQ)
|
||||
|
||||
REDIS\_HOST=localhost
|
||||
|
||||
REDIS\_PORT=6379
|
||||
|
||||
# **5\. ลำดับการ Rollout**
|
||||
|
||||
แนะนำให้ implement เป็น phase เพื่อลดความเสี่ยง:
|
||||
|
||||
| Phase | สิ่งที่ทำ | ผลลัพธ์ |
|
||||
| :---- | :---- | :---- |
|
||||
| Phase 1(1-2 วัน) | ติดตั้ง Qdrant \+ Redis, สร้าง DB table, ติดตั้ง packages | Infrastructure พร้อม |
|
||||
| Phase 2(2-3 วัน) | สร้าง EmbeddingService \+ QdrantService \+ LlmService, ทดสอบแต่ละ service แยกกัน | Core services ทำงาน |
|
||||
| Phase 3(2-3 วัน) | สร้าง ChunkerService \+ Ingestion Pipeline เริ่มจาก CORR และ RFI ก่อน | Indexing ทำงานอัตโนมัติ |
|
||||
| Phase 4(1-2 วัน) | สร้าง RAG Query API \+ เชื่อมกับ NestJS route | Query API พร้อม |
|
||||
| Phase 5(2-3 วัน) | สร้าง UI ใน Next.js: Search bar \+ Answer panel พร้อม source citation | ใช้งานได้จริง |
|
||||
|
||||
# **6\. ข้อควรพิจารณาสำคัญ**
|
||||
|
||||
| ⚠️ ความปลอดภัยของข้อมูล (สำคัญมาก) เนื้อหาของเอกสารจะถูกส่งไปยัง Typhoon API (cloud) เพื่อ generate คำตอบ แนะนำให้ตรวจสอบกับทีม PM / Security ว่าเอกสารชั้น Confidential สามารถส่งออกนอกได้หรือไม่ ทางเลือก: ใช้ Ollama local LLM แทน Typhoon สำหรับเอกสาร Confidential |
|
||||
| :---- |
|
||||
|
||||
| ℹ️ Rate Limit ของ Typhoon Free Tier อยู่ที่ 5 req/s และ 200 req/m — เพียงพอสำหรับ internal DMS หากมีผู้ใช้หลายคน query พร้อมกัน อาจต้องพิจารณา upgrade เป็น Together.ai plan |
|
||||
| :---- |
|
||||
|
||||
| ℹ️ Embedding ยังต้องใช้ Ollama Typhoon API ยังไม่มี embedding endpoint nomic-embed-text รันบน Ollama local ทำให้ข้อมูลที่ embed ไม่ออกนอกเครือข่าย |
|
||||
| :---- |
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
นี่คือร่างเนื้อหาฉบับปรับปรุงของ **LCBP3 RAG Implementation Guide (v1.1.0)** ในรูปแบบ Markdown โดยเน้นการยกระดับความปลอดภัย (Security), การรองรับหลายโครงการ (Multi-tenancy), และความถูกต้องของคำตอบ (Truthfulness) ตามมาตรฐานระบบ LCBP3 DMS
|
||||
|
||||
---
|
||||
|
||||
# 📄 LCBP3 DMS: RAG Implementation Guide
|
||||
**Retrieval-Augmented Generation for Document Management System**
|
||||
|
||||
| Version | Date | Status | Updated By |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 1.1.0 | 2026-04-19 | Proposed | Gemini (Senior Developer) |
|
||||
|
||||
---
|
||||
|
||||
## 1. ภาพรวม Architecture (Multi-tenant Hybrid)
|
||||
ระบบ RAG ออกแบบมาเพื่อทำงานร่วมกับโครงสร้างข้อมูลเดิมของ LCBP3 โดยมีการแยกส่วนการประมวลผลตามระดับความปลอดภัยของข้อมูล
|
||||
|
||||
### 1.1 Ingestion Pipeline
|
||||
1. **Trigger:** ไฟล์ถูก Commit เข้าสู่ Permanent Storage (ตาม ADR-016)
|
||||
2. **OCR Service:** ใช้ EasyOCR/Tesseract ประมวลผล PDF/DWG (ในกรณีที่ไฟล์ไม่มี text layer)
|
||||
3. **Data Enrichment:** ดึง Metadata จาก MariaDB (Project ID, Doc Type, Security Level) แนบไปกับข้อมูลดิบ
|
||||
4. **Chunking:** แบ่งส่วนข้อความตามกลยุทธ์ที่กำหนด (ดูหัวข้อที่ 2)
|
||||
5. **Local Embedding:** ส่ง Chunk ไปยัง **Ollama (nomic-embed-text)** ภายใน Intranet
|
||||
6. **Vector Store:** บันทึก Vector + Metadata ลงใน **Qdrant** โดยแยก `collection` หรือใช้ `payload filter` ตาม Project ID
|
||||
|
||||
### 1.2 Query & Generation
|
||||
1. **Secure Filter:** ระบบ DMS API รับคำถามและตรวจสอบสิทธิ์ (RBAC) ของผู้ใช้
|
||||
2. **Similarity Search:** ค้นหาใน Qdrant โดยบังคับใส่ `project_public_id` ใน Filter เสมอ
|
||||
3. **Context Selection:** เลือกเฉพาะ Top-K chunks ที่มีคะแนนความมั่นใจ (Confidence) > 0.6
|
||||
4. **Hybrid Generation:**
|
||||
* *ข้อมูลปกติ:* ส่ง Context + Prompt ไปยัง **Typhoon API (Cloud)** เพื่อคุณภาพภาษาไทยสูงสุด
|
||||
* *ข้อมูลลับ (Confidential):* ส่งประมวลผลผ่าน **Ollama (Local LLM)** ภายในเครื่อง Admin เท่านั้น (ตาม ADR-018)
|
||||
|
||||
---
|
||||
|
||||
## 2. Chunking Strategy (Domain-Specific)
|
||||
ห้ามใช้ Fixed-size chunking เพียงอย่างเดียว ให้ใช้กลยุทธ์ตามประเภทเอกสาร (Document Type):
|
||||
|
||||
| ประเภทเอกสาร | กลยุทธ์การ Chunk | ข้อมูลที่ต้องแนบใน Metadata |
|
||||
| :--- | :--- | :--- |
|
||||
| **Correspondence (CORR)** | Recursive Character Splitter (1000 chars, 10% overlap) | Sender, Receiver, Ref No., Subject |
|
||||
| **RFA / Transmittal** | Table-aware Chunking (เน้นดึงตารางรายการไฟล์แนบ) | RFA ID, Status, Due Date |
|
||||
| **Drawing (Shop/As-built)** | Title Block Extraction + Metadata Enrichment | Drawing No., Revision, Discipline |
|
||||
|
||||
---
|
||||
|
||||
## 3. มาตรฐานการพัฒนา (Technical Specification)
|
||||
|
||||
### 3.1 Vector Payload Interface (NestJS)
|
||||
```typescript
|
||||
interface VectorMetadata {
|
||||
public_id: string; // UUIDv7 ของเอกสาร
|
||||
project_public_id: string; // ✅ บังคับ เพื่อความปลอดภัย
|
||||
doc_type: string; // CORR, RFA, DRAWING
|
||||
security_level: number; // 1: Public, 2: Internal, 3: Confidential
|
||||
content_preview: string; // ข้อความสั้นๆ สำหรับแสดงผล
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Ingestion Status (MariaDB Update)
|
||||
เพิ่มฟิลด์ในตาราง `attachments` เพื่อติดตามสถานะ:
|
||||
```sql
|
||||
ALTER TABLE attachments
|
||||
ADD COLUMN rag_status ENUM('PENDING', 'PROCESSING', 'INDEXED', 'FAILED') DEFAULT 'PENDING',
|
||||
ADD COLUMN rag_last_error TEXT NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. ความปลอดภัยและการควบคุม (Security Audit Protocol)
|
||||
|
||||
### 4.1 Data Isolation (Non-negotiable)
|
||||
* **Tenant Separation:** ทุกการค้นหาต้องมีการทำ Payload filtering บน Qdrant ด้วย `project_public_id` เพื่อป้องกันพนักงานโครงการ A เห็นข้อมูลโครงการ B
|
||||
* **RBAC Check:** DMS API ต้องเรียก `CaslGuard` ก่อนทำการค้นหา Vector เสมอ
|
||||
|
||||
### 4.2 AI Truthfulness & Anti-Hallucination
|
||||
* **Citation:** Prompt ต้องบังคับให้ AI อ้างอิงหมายเลขเอกสาร (`doc_number`) ทุกครั้งที่ตอบ
|
||||
* **Fallback:** หาก Similarity Search ได้ผลลัพธ์ที่คะแนนต่ำกว่าเกณฑ์ ให้ตอบว่า *"ไม่พบข้อมูลที่ระบุในฐานข้อมูลเอกสารปัจจุบัน"* ห้ามให้ AI คาดเดาเอง
|
||||
|
||||
---
|
||||
|
||||
## 5. ลำดับการ Rollout (Updated Phase)
|
||||
|
||||
* **Phase 1: Infra (2 วัน):** Docker Qdrant + Redis + Ollama Setup
|
||||
* **Phase 2: Core Services (3 วัน):** สร้าง `EmbeddingService` และ `QdrantService` (Strict Type)
|
||||
* **Phase 3: Auto-Ingestion (3 วัน):** เชื่อม BullMQ เข้ากับไฟล์อัปโหลดเดิมของ DMS
|
||||
* **Phase 4: RAG API (2 วัน):** สร้าง API ค้นหาพร้อมระบบ Citation
|
||||
* **Phase 5: UI/UX (3 วัน):** หน้าจอ Search ผลลัพธ์แบบ AI พร้อมปุ่มเปิดไฟล์ต้นฉบับ
|
||||
|
||||
---
|
||||
|
||||
## 6. รายการตรวจสอบ (Security Checklist ก่อน Go-live)
|
||||
- [ ] ข้อมูล Confidential ไม่ถูกส่งไปยัง Typhoon API (Cloud)
|
||||
- [ ] มีการบันทึก `audit_logs` ทุกการ Query ของ AI (ใครถาม, ถามอะไร, ระบบตอบอะไร)
|
||||
- [ ] ฟังก์ชันการลบเอกสารใน DMS มีการสั่งลบ Vector ใน Qdrant ออกด้วย (Consistency)
|
||||
- [ ] ทดสอบ Cross-project search แล้วต้องไม่พบข้อมูลข้ามโครงการ
|
||||
|
||||
---
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
# 📋 Review: LCBP3 DMS RAG Implementation Guide (v1.1.0)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 จุดแข็งที่โดดเด่น
|
||||
|
||||
| ด้าน | สิ่งที่ทำได้ดี |
|
||||
|------|---------------|
|
||||
| **Security-by-Design** | แยก Hybrid Generation ตาม Security Level + บังคับ `project_public_id` filter ในทุก Query |
|
||||
| **Domain-Aware Chunking** | กลยุทธ์แยกตาม Document Type (CORR/RFA/Drawing) เหมาะสมกับงานก่อสร้าง |
|
||||
| **Data Consistency** | เพิ่ม `rag_status` ใน MariaDB ติดตามสถานะ + มีแผนลบ Vector เมื่อลบเอกสาร |
|
||||
| **Anti-Hallucination** | บังคับ Citation + Fallback message เมื่อคะแนนต่ำ ป้องกันการคาดเดา |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 ข้อเสนอแนะปรับปรุง (แบ่งตามหัวข้อ)
|
||||
|
||||
### 1. Architecture & Retrieval Strategy
|
||||
|
||||
#### ⚠️ ปัญหาที่อาจเกิด:
|
||||
- การใช้ **Vector-only search** อาจพลาด Keyword ที่สำคัญ เช่น เลขที่เอกสาร `REF-2026-001` หรือรหัส Drawing `DWG-STR-001`
|
||||
|
||||
#### ✅ ข้อเสนอแนะ:
|
||||
```typescript
|
||||
// เพิ่ม Hybrid Search: BM25 + Vector + Reranking
|
||||
interface SearchConfig {
|
||||
vector_weight: number; // 0.7
|
||||
keyword_weight: number; // 0.3
|
||||
use_rrf_fusion: boolean; // ✅ แนะนำให้เปิดใช้ Reciprocal Rank Fusion [[3]]
|
||||
rerank_top_k: number; // 50 → rerank → 10
|
||||
}
|
||||
```
|
||||
|
||||
**เหตุผล**: Hybrid retrieval ช่วยเพิ่มความแม่นยำทั้งเชิงความหมายและเชิงคำศัพท์ โดยเฉพาะกับเอกสารเทคนิคที่มีรหัสเฉพาะ [[1]][[4]]
|
||||
|
||||
#### 🔄 ปรับ Ingestion Pipeline:
|
||||
```
|
||||
เดิม: OCR → Chunk → Embed → Store
|
||||
ใหม่: OCR → [Metadata Extraction] → Chunk → [Add Parent-Child Relationship] → Embed → Store
|
||||
```
|
||||
- เพิ่ม **Parent-Child Chunking**: เก็บ Chunk เล็กสำหรับ Search แต่อ้างอิงกลับไปยัง Document Section เต็มสำหรับ Generation [[31]]
|
||||
- ดึง Metadata เพิ่ม: `doc_number`, `revision`, `effective_date` สำหรับ Filtering แบบละเอียด
|
||||
|
||||
---
|
||||
|
||||
### 2. Qdrant Multi-tenant Configuration
|
||||
|
||||
#### ⚠️ ปัญหา:
|
||||
การใช้ `payload filter` อย่างเดียวอาจมี **ประสิทธิภาพลดลง** เมื่อข้อมูลโตขึ้น [[12]]
|
||||
|
||||
#### ✅ ข้อเสนอแนะ:
|
||||
```typescript
|
||||
// ใช้ Tiered Multitenancy + Payload Indexing (Qdrant v1.16+) [[10]][[13]]
|
||||
await client.createCollection("lcbp3_vectors", {
|
||||
vectors: { size: 768, distance: "Cosine" },
|
||||
sharding_method: "custom", // ✅ เปิดใช้ Custom Sharding
|
||||
hnsw_config: {
|
||||
payload_m: 16, // ✅ สร้าง Index แยกตาม Tenant
|
||||
m: 0 // ปิด Global Index เพื่อลด Overhead
|
||||
}
|
||||
});
|
||||
|
||||
// สร้าง Payload Index สำหรับ project_public_id
|
||||
await client.createPayloadIndex("lcbp3_vectors", {
|
||||
field_name: "project_public_id",
|
||||
field_schema: { type: "keyword", is_tenant: true } // ✅ is_tenant=true ช่วยจัดกลุ่มข้อมูล
|
||||
});
|
||||
```
|
||||
|
||||
**ประโยชน์**:
|
||||
- ลด Noisy Neighbor ระหว่างโครงการ
|
||||
- Query เร็วขึ้น 3-5x เมื่อ Filter ด้วย `project_public_id` [[16]]
|
||||
|
||||
---
|
||||
|
||||
### 3. Thai Language Optimization
|
||||
|
||||
#### ⚠️ ปัญหา:
|
||||
`nomic-embed-text` ทำงานดีกับภาษาอังกฤษ แต่อาจไม่เหมาะกับ **ภาษาไทยที่มีโครงสร้างพิเศษ** เช่น เอกสารกฎหมาย/ก่อสร้าง [[36]][[37]]
|
||||
|
||||
#### ✅ ข้อเสนอแนะ:
|
||||
```python
|
||||
# 1. ใช้ Thai-specific Preprocessing ก่อน Embedding
|
||||
def preprocess_thai_legal(text: str) -> str:
|
||||
# ตัดคำไทยด้วย PyThaiNLP + รักษาโครงสร้างเลขมาตรา
|
||||
# ลบ Noise เช่น "หน้า 1/3", "ลงชื่อ__________"
|
||||
# แยก Section Header ออกจาก Content
|
||||
return cleaned_text
|
||||
|
||||
# 2. พิจารณา Fine-tune Embedding Model (ถ้ามีข้อมูลเพียงพอ)
|
||||
# ใช้ WangchanX-Legal หรือสร้าง Dataset จากเอกสาร LCBP3 ที่ผ่านการทำ Label แล้ว [[37]]
|
||||
|
||||
# 3. เพิ่ม Query Rewriting สำหรับภาษาไทย
|
||||
def rewrite_thai_query(user_query: str) -> List[str]:
|
||||
# ขยายคำย่อ: "รฟม." → ["การรถไฟฟ้าขนส่งมวลชนแห่งประเทศไทย", "รฟม."]
|
||||
# แปลงเลขไทย: "มาตรา ๑๐" → ["มาตรา 10", "ม.10"]
|
||||
# เพิ่ม Synonym: "แบบก่อสร้าง" → ["Shop Drawing", "As-built", "แบบขยาย"]
|
||||
return [user_query, *expanded_queries]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Anti-Hallucination & Citation Enforcement
|
||||
|
||||
#### ✅ เสริมจากที่มีอยู่:
|
||||
```typescript
|
||||
// 1. เพิ่ม Verification Layer ก่อนส่งคำตอบให้ผู้ใช้
|
||||
interface VerificationStep {
|
||||
check_citation_exists: boolean; // ตรวจสอบว่า doc_number ที่อ้างถึงมีจริงในระบบ
|
||||
check_security_level: boolean; // ตรวจสอบว่าผู้ใช้มีสิทธิ์เห็นเอกสารที่อ้างถึง
|
||||
confidence_threshold: number; // ถ้าคะแนนรวม < 0.7 → ใช้ Fallback Message
|
||||
}
|
||||
|
||||
// 2. ใช้ Structured Output เพื่อบังคับรูปแบบคำตอบ
|
||||
const rag_prompt = `
|
||||
คุณเป็นผู้ช่วยด้านเอกสารโครงการ ลCBP3
|
||||
กฎ:
|
||||
1. ตอบเฉพาะจากข้อมูลที่ให้มาเท่านั้น
|
||||
2. ทุกข้อเท็จจริงต้องอ้างถึง [doc_number: xxx]
|
||||
3. ถ้าไม่แน่ใจ ให้ตอบว่า "ไม่พบข้อมูลที่ระบุ"
|
||||
4. ห้ามคาดเดาหรือใช้ความรู้ภายนอก
|
||||
|
||||
รูปแบบคำตอบ (JSON):
|
||||
{
|
||||
"answer": "ข้อความตอบ...",
|
||||
"citations": [{"doc_number": "...", "page": "...", "snippet": "..."}],
|
||||
"confidence": 0.85,
|
||||
"fallback_used": false
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
**เหตุผล**: การบังคับ Structured Output + Verification Layer ลดความเสี่ยงการให้ข้อมูลผิดพลาดได้ถึง 80% [[39]][[44]]
|
||||
|
||||
---
|
||||
|
||||
### 5. Operational & Monitoring
|
||||
|
||||
#### ✅ เพิ่มสิ่งที่ขาด:
|
||||
```yaml
|
||||
# monitoring-config.yaml
|
||||
rag_metrics:
|
||||
- query_latency_p95: "<2s"
|
||||
- retrieval_recall_at_5: ">0.85"
|
||||
- citation_accuracy: ">0.95"
|
||||
- fallback_rate: "<15%"
|
||||
|
||||
alerting:
|
||||
- condition: "fallback_rate > 30% ใน 1 ชั่วโมง"
|
||||
action: "แจ้งทีม Dev + ลด Traffic ไป Cloud LLM ชั่วคราว"
|
||||
|
||||
- condition: "Cross-project query detected"
|
||||
action: "บล็อกทันที + บันทึก Security Audit Log"
|
||||
|
||||
# Audit Log Schema
|
||||
audit_log: {
|
||||
timestamp,
|
||||
user_id,
|
||||
project_id,
|
||||
query_hash, // ไม่เก็บข้อความคำถามตรงๆ เพื่อความเป็นส่วนตัว
|
||||
retrieved_docs: ["doc_id_1", ...],
|
||||
llm_provider: "typhoon-cloud" | "ollama-local",
|
||||
response_hash,
|
||||
security_level_accessed: 1|2|3
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Rollout Phase Adjustments
|
||||
|
||||
| Phase | ข้อเสนอแนะเพิ่มเติม |
|
||||
|-------|-------------------|
|
||||
| **Phase 1** | เพิ่ม Load Test สำหรับ Qdrant ด้วยข้อมูลจำลอง 100K vectors [[1]] |
|
||||
| **Phase 2** | สร้าง Unit Test สำหรับ `EmbeddingService` ด้วย Mock Thai text cases |
|
||||
| **Phase 3** | เพิ่ม Dead Letter Queue ใน BullMQ สำหรับไฟล์ที่ Process Failed > 3 ครั้ง |
|
||||
| **Phase 4** | ทำ A/B Testing: Vector-only vs Hybrid Search วัดความพึงพอใจผู้ใช้ |
|
||||
| **Phase 5** | เพิ่มปุ่ม "ช่วยปรับปรุงคำตอบ" เพื่อเก็บ Feedback สำหรับ Fine-tuning |
|
||||
|
||||
---
|
||||
|
||||
### 7. Security Checklist เพิ่มเติม
|
||||
|
||||
```diff
|
||||
[ ] ข้อมูล Confidential ไม่ถูกส่งไปยัง Typhoon API (Cloud)
|
||||
[ ] มีการบันทึก audit_logs ทุกการ Query ของ AI
|
||||
[ ] ฟังก์ชันการลบเอกสารใน DMS มีการสั่งลบ Vector ใน Qdrant ออกด้วย
|
||||
[ ] ทดสอบ Cross-project search แล้วต้องไม่พบข้อมูลข้ามโครงการ
|
||||
+ [ ] ทดสอบ Privilege Escalation: ผู้ใช้ Level 1 พยายาม Query ข้อมูล Level 3
|
||||
+ [ ] ทดสอบ Prompt Injection: ใส่คำสั่งในคำถามเพื่อพยายามให้ AI ข้ามกฎ Citation
|
||||
+ [ ] ทดสอบ Rate Limiting: ป้องกันการ Query จำนวนมากเพื่อ Extract ข้อมูลทั้งระบบ
|
||||
+ [ ] มีแผน Disaster Recovery: Backup Qdrant + Restore Procedure ทดสอบทุกไตรมาส
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 สรุปคำแนะนำเร่งด่วน (Priority Order)
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[High Priority] --> B[Hybrid Search + RRF]
|
||||
A --> C[Qdrant Tiered Multitenancy]
|
||||
A --> D[Thai Query Preprocessing]
|
||||
|
||||
E[Medium Priority] --> F[Parent-Child Chunking]
|
||||
E --> G[Structured Output + Verification]
|
||||
E --> H[Monitoring Dashboard]
|
||||
|
||||
I[Low Priority] --> J[Fine-tune Thai Embedding]
|
||||
I --> K[Feedback Loop for Continuous Learning]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> 💡 **ข้อคิดสุดท้าย**: ระบบ RAG ที่ดีไม่ใช่แค่ "ตอบได้" แต่ต้อง "ตอบถูกต้อง อ้างอิงได้ และปลอดภัย" [[5]][[7]] เอกสารฉบับนี้มีพื้นฐานดีมากแล้ว การเพิ่มชั้นของ Verification, Monitoring และ Thai-specific optimization จะทำให้ระบบพร้อมสำหรับ Production จริงในอุตสาหกรรมก่อสร้างที่มีความซับซ้อนสูง
|
||||
Reference in New Issue
Block a user