เลือกระดับความลึก:
บล็อกที่มีป้าย เจาะลึก จะซ่อน/แสดงตามโหมด

01ระบบนี้ทำอะไร — ในประโยคเดียว

เข้าใจ "เป้าหมาย" ให้ชัดก่อน แล้วทุกอย่างที่เหลือจะมีเหตุผล

เมื่อผู้ใช้ถามคำถามเชิงเหตุการณ์ เช่น "ณ วันที่ 1 ม.ค. 2566 หุ้นตัวใดมี exposure ต่อ Digital Asset / Crypto" ระบบจะตอบกลับเป็น ตาราง 4 คอลัมน์ ได้แก่ SymbolImpactReasoningSource คำตอบทุกข้ออ้างอิงจากเอกสารทางการจริงเท่านั้น ไม่ว่าจะเป็นงบการเงิน 56-1 One Report หรือข่าวแจ้งตลาด ระบบห้ามเดาหรือแต่งเติมข้อมูล (no hallucination) และรายงานเวลาที่ใช้กับต้นทุน token ไว้ทุกครั้ง

หัวใจสำคัญ 3 อย่าง

1. Grounding (อ้างอิงได้จริง) — ทุกประโยคในคอลัมน์ Reasoning ต้องชี้กลับไปยังข้อความจริงในเอกสารได้เสมอ ถ้าไม่มีหลักฐานรองรับ ระบบจะตอบว่า "ไม่มีผลกระทบ / ไม่พบข้อมูล" เท่านั้น ห้ามเดา

2. Temporal (ตามกาลเวลา) — คำว่า "ณ วันที่ 1 ม.ค. 66" หมายความว่าข้อมูลที่เกิดขึ้นหลังวันนั้นต้องถูกตัดออก ระบบจะตอบเฉพาะสิ่งที่ทราบ ณ เวลานั้นเท่านั้น

3. Completeness (ครบทุกตัว) — เพราะคำถามถามว่า "หุ้นใดบ้าง" ระบบจึงต้องไม่พลาดบริษัทที่เกี่ยวข้องแม้แต่ตัวเดียว

เกณฑ์ให้คะแนน POC (ต้องจำ)

น้ำหนักเกณฑ์
70%ความถูกต้อง/ครบถ้วน + citation จริง
10%ทำตามคำสั่ง (ครบทุกคอลัมน์/รูปแบบ)
10%Stability — ถามซ้ำได้คำตอบเดิม
10%ประสิทธิภาพ — เวลา + ต้นทุน token

ทุกการตัดสินใจออกแบบในระบบ ล้วนมาจาก 4 เกณฑ์นี้

ทำไมมันถึงยาก?
ข้อมูลในระบบนี้คือเอกสารทางการ 693 ไฟล์ มีหลายรูปแบบ ทั้ง PDF, Word, Excel และ ZIP ครอบคลุมหลายปีตั้งแต่ พ.ศ. 2564 ถึง 2568 บางไฟล์เป็นสแกนต้องผ่าน OCR ก่อน บางไฟล์เป็นฟอร์แมตเก่าอย่าง .DOC หรือ .XLS AI ทั่วไปอาจ "เดา" คำตอบได้ แต่ระบบนี้ทุกคำตอบต้องพิสูจน์ได้จากเอกสารจริง

02ภาพรวมกระบวนการ — 2 ครึ่ง

ระบบแบ่งออกเป็น 2 ส่วนหลัก ส่วนแรกคือ "เตรียมข้อมูล" ซึ่งทำล่วงหน้าเป็นรอบ ส่วนที่สองคือ "ตอบคำถาม" ซึ่งทำงานทุกครั้งที่มีคำถามเข้ามา

  ฝั่ง A — INGESTION (เตรียมข้อมูล, ทำล่วงหน้า)
  ┌────────────────────────────────────────────────────────────────┐
  693 ไฟล์ดิบ ─► แตกไฟล์ ─► อ่านข้อความ ─► ทำความสะอาด ─► ติด metadata
       (PDF/Word/Excel/ZIP)   (+OCR)        (ภาษาไทย)      (วันที่/บริษัท)
                                                  │
                          ┌───────────────────────┴───────────────────┐
                          ▼                                            ▼
                   หั่นเป็นชิ้น (chunk)                         ดึง entity/ความสัมพันธ์
                   ─► ทำ embedding                              ─► สร้าง Knowledge Graph
                   ─► เก็บใน Vector Store                       (บริษัท→ผลิตภัณฑ์→ธีม)
  └────────────────────────────────────────────────────────────────┘
                                  ▼  เก็บไว้ใน Cloud Spanner (ฐานเดียว)

  ฝั่ง B — SERVING (ตอบคำถาม, ทำทุกครั้ง)
  ┌────────────────────────────────────────────────────────────────┐
  คำถาม + as_of_date ─► [1 เข้าใจเหตุการณ์] ─► [2 แตกเป็น criteria]
       ─► [3 ค้นหาบริษัท: graph ∪ vector + กรองเวลา]
       ─► [4 ประเมิน impact รายบริษัท + ตรวจหลักฐาน]
       ─► [5 จัดตาราง 4 คอลัมน์ + เวลา + ต้นทุน]  ─► ผู้ใช้
  └────────────────────────────────────────────────────────────────┘
เปรียบเทียบให้เห็นภาพ
ลองนึกภาพบรรณารักษ์ ฝั่ง A คือการจัดหนังสือเข้าชั้น บรรณารักษ์อ่านทุกเล่ม ทำดัชนี ติดป้ายหมวดหมู่ และเตรียมไว้ล่วงหน้า ส่วนฝั่ง B คือตอนที่มีคนมาถามคำถาม บรรณารักษ์จะเข้าใจคำถาม เดินไปหยิบเฉพาะหนังสือที่เกี่ยวข้อง เปิดหน้าที่ตรงประเด็น แล้วสรุปคำตอบพร้อมบอกว่า "อยู่หน้าไหน เล่มไหน"

03สถาปัตยกรรม — รันบน Google Cloud

เทคโนโลยีถูกเลือกแล้ว เพราะลูกค้า (ตลาดหลักทรัพย์ฯ) กำหนดให้ใช้ GCP + Gemini

ส่วนประกอบหลัก

☁️ GCP โปรเจกต์ set-chatbot region asia-southeast1 (สิงคโปร์ ใกล้ไทย)

🧠 Gemini โมเดล gemini-3.5-flash ใช้ทั้งงานเข้าใจคำถามและประเมินผล

🔢 Embeddings text-multilingual-embedding-002 (รองรับไทย)

🗄️ Cloud Spanner ทำหน้าที่ทั้ง Vector DB และ Knowledge Graph ในฐานเดียว

💬 Cloud SQL เก็บประวัติแชต

ทำไม Spanner ทำ 2 หน้าที่?

Spanner สามารถค้นแบบ vector (ความหมายใกล้เคียง) และเดินกราฟความสัมพันธ์ (property graph) ได้ในฐานข้อมูลเดียวกัน ข้อดีคือไม่ต้องย้ายข้อมูลไปมาระหว่างระบบ ได้ความสอดคล้องเชิง transaction และสามารถquery ผสม vector+graph+เงื่อนไขเวลาได้ในคำสั่งเดียว

เหมาะกับโจทย์ "current vs historical exposure" เพราะเก็บช่วงเวลา valid_from/valid_to บนเส้นความสัมพันธ์ (edge) ได้โดยตรง

เจาะลึก Pipeline แบบ event-driven (production)
# เมื่อมีไฟล์ใหม่เข้า bucket → trigger อัตโนมัติ
Cloud Storage ─► Pub/Sub (คิวไฟล์) ─► Cloud Run / Dataflow (file processor)
   └─ parse → clean → chunk → Embeddings API → upsert ลง Spanner

# ฝั่ง serving
Agent ─► Agent Engine (orchestrate 5 step) ─► vector search + graph + Ranking API ─► Gemini summarize

ในโค้ด external service ทุกตัวจะมี interface + adapter สองชุด คือ gcp สำหรับโปรดักชัน และ local สำหรับ dev/test ที่รันได้โดยไม่ต้องต่อ GCP สลับระหว่างสองชุดได้ด้วย ENV — ดูที่ src/common/interfaces.py: LLMClient, EmbeddingClient, VectorStore, GraphStore, Reranker

04Ingestion — เปลี่ยน 693 ไฟล์ให้ค้นได้

เป้าหมายของขั้นตอนนี้คือแปลงไฟล์ดิบทั้งหมดให้กลายเป็น "ชิ้นข้อความที่ค้นได้" และ "กราฟความสัมพันธ์" ที่สำคัญต้องเก็บร่องรอยที่มา ทั้งชื่อไฟล์ เลขหน้า และวันที่ ไว้ครบทุกชิ้น เพื่อนำไปทำ citation ในภายหลัง

ข้อมูลจริงมีอะไรบ้าง (15 บริษัท)

ประเภทรูปแบบจริงจำนวนมีอะไรสำคัญ
56-1 One ReportZIP → PDF47โครงสร้างธุรกิจ, รายได้ตาม segment, ใบอนุญาต, บริษัทย่อย
งบการเงินZIP → Excel + Word206ตัวเลขงบ + auditor report + notes (เช่น การด้อยค่า)
MD&A + ข่าวแจ้งตลาดPDF (ปนกัน)439ต้องแยกประเภทด้วยเนื้อหา; ข่าวบอก "เมื่อไหร่"
SET Company ProfileCSV1master list symbol/ชื่อ/sector

ช่วงข้อมูล พ.ศ. 2564–2568 • สั่ง python -m scripts.profile_dataset เพื่อทำ inventory ก่อนเริ่มเสมอ

สถิติจริงหลัง ingest ครบ (ดูสดได้บนหน้า dashboard)
แตกไฟล์ได้ทั้งสิ้น 1,138 ไฟล์ หั่นเป็น 84,370 chunks สร้างกราฟความรู้ได้ 47,193 จุด เชื่อมกัน 125,844 เส้น ในจำนวนนี้เป็นเส้นเชื่อม exposure ระหว่างบริษัทกับประเด็นที่เกี่ยวข้อง (Exposure links) ถึง 10,313 เส้น

ลำดับ 7 ขั้นของการป้อนข้อมูล

RAW ─► EXTRACT ─► CLEAN ─► METADATA ─► CHUNK ─► EMBED ─► UPSERT
                                              └─► ดึง ENTITY ─► สร้าง GRAPH

1. Extract — อ่านข้อความออกจากไฟล์ต้นฉบับ: PDF จะดึงทีละหน้าพร้อมเก็บเลขหน้าไว้สำหรับ citation, Excel ดึงทีละ sheet และ cell, Word ดึงย่อหน้าและตาราง, ส่วน ZIP จะแตกไฟล์ออกก่อนแล้วส่งต่อตามชนิดไฟล์

2. Clean — ทำความสะอาดข้อความด้วยการตัด header, footer และเลขหน้าที่ซ้ำออก รวมคำที่ถูกตัดบรรทัดกลับคืน ปรับ normalize ภาษาไทยทั้งสระและวรรณยุกต์ รวมถึงแปลงปี พ.ศ.↔ค.ศ. ให้ตรงกัน แต่เก็บข้อความดิบต้นฉบับไว้เสมอเพื่อใช้แสดง snippet

3. Metadata — ติดป้ายทุกชิ้น: doc_id, doc_type, symbol, company_name, fiscal_year, doc_date, page/sheet, source_path, content_hash

4. Chunk — หั่นข้อความออกเป็นชิ้นๆ ชิ้นละประมาณ 800 tokens โดยให้แต่ละชิ้นเหลื่อมกัน 120 tokens และตัดที่ขอบเขตย่อหน้าเสมอ ไม่ตัดกลางประโยค

5. Embed — แปลงแต่ละชิ้นเป็นเวกเตอร์ตัวเลข (ความหมาย) ด้วยโมเดล embedding

6. Upsert — บันทึกลง Vector Store

7. Entity → Graph — ใช้ Gemini ดึงข้อมูลเชิงโครงสร้างออกมา ได้แก่ บริษัท บริษัทย่อย ผลิตภัณฑ์/แบรนด์ ใบอนุญาต วัตถุดิบ ลูกค้า ที่ตั้ง segment และเหตุการณ์ต่างๆ พร้อมวันที่ จากนั้นสร้างเป็นจุด (node) และเส้นเชื่อม (edge) ในกราฟความสัมพันธ์

🪤 กับดักจากข้อมูลจริง (สำคัญมาก)

ฟอร์แมตเก่า: ไฟล์ .DOC/.XLS รุ่นเก่า (OLE2, code page ไทย 874) นั้น python-docx และ openpyxl อ่านไม่ได้โดยตรง ต้องใช้ LibreOffice headless หรือ antiword/xlrd แปลงก่อน โดยระบบจะ detect ชนิดไฟล์จาก magic bytes แล้วเลือก parser ที่เหมาะสมให้อัตโนมัติ

ไฟล์สแกน: ~67 PDF เป็นภาพ ไม่มี text layer ต้อง OCR (ไทย+อังกฤษ) ก่อน

วันที่: ไฟล์ที่ตั้งชื่อแบบ coded มีวันที่ในชื่อไฟล์ แต่ไฟล์ numeric ต้องดึงวันที่จากเนื้อหา

ทำไม "doc_date" สำคัญที่สุด?
doc_date คือหัวใจของ temporal reasoning ในระบบนี้ ถ้าวันที่ผิด ระบบจะตอบคำถาม "ณ 1 ม.ค. 66" โดยใช้ข้อมูลที่ยังไม่เกิดขึ้น ณ เวลานั้น ระบบดึงวันที่จากหลายแหล่งตามลำดับ ได้แก่ ชื่อไฟล์, หัวเอกสาร และวันที่ข่าวหรือวันลงนามงบ ถ้าหาไม่ได้จริงๆ จึง fallback เป็นปีงบ แล้วติดธง date_inferred=true ไว้
เจาะลึก Idempotency — ทำไมป้อนซ้ำไม่พัง

chunk_id = hash(content + doc_id + page) ซึ่งหมายความว่าถ้าป้อนไฟล์เดิมซ้ำจะได้ id เดิม จึงไม่มีข้อมูลซ้ำ (สำคัญต่อ stability) นอกจากนี้มี manifest.jsonl ที่บันทึกสถานะของแต่ละไฟล์ว่า parsed/embedded/graph_done เสร็จแล้วหรือยัง ทำให้ resume จากจุดที่ค้างได้ โค้ดทั้งหมดอยู่ที่ src/ingestion/

05Knowledge Base — 2 สมองในฐานเดียว

Knowledge Base แบ่งเป็นสองส่วน คือ (A) Vector Store สำหรับค้นหาด้วยความหมาย และ (B) Knowledge Graph สำหรับหาว่า "มีบริษัทใดเกี่ยวข้องบ้าง" แบบมีโครงสร้างชัดเจน

A. Vector Store — ค้นด้วย "ความหมาย"

Vector Store เก็บแต่ละ chunk พร้อมเวกเตอร์ความหมายไว้ เมื่อค้นหา ระบบจะแปลงคำถามเป็นเวกเตอร์แล้วหา chunk ที่มีความหมายใกล้เคียงที่สุด (KNN) พร้อมกรองว่า doc_date <= as_of เพื่อตัดข้อมูลในอนาคตออก

จุดแข็งคือจับ "ความหมาย" ได้แม้ใช้คำต่างกัน แต่อาจพลาดบริษัทที่พูดถึง exposure แบบอ้อมๆ

B. Knowledge Graph — หา "ความเชื่อมโยง"

Knowledge Graph เก็บข้อมูลเป็นจุด (Company, Product, License, Segment, Theme ฯลฯ) เชื่อมกันด้วยเส้น เช่น Company —EXPOSED_TO→ Theme

ข้อดีคือให้ recall ที่ครบถ้วน ดึงทุกบริษัทที่มีเส้นเชื่อมไปยังธีม ผลิตภัณฑ์ หรือใบอนุญาตที่ตรงกับเงื่อนไข ช่วยแก้จุดอ่อนของ vector search ได้

ทำไมต้องมีทั้งคู่? (Hybrid)
เพราะคำถามถามว่า "หุ้นใดบ้าง" ซึ่งต้องการความครบถ้วน (เกณฑ์ 70%) Graph ช่วยให้ครบเชิงโครงสร้าง Vector ช่วยดึง snippet หลักฐาน แล้วทั้งสองยืนยันกันอีกครั้ง

Temporal model — หัวใจของ "ณ วันที่..."

ทุกเส้นเชื่อม (edge) ในกราฟจะเก็บช่วงเวลาไว้ว่า [valid_from, valid_to) ถ้าความสัมพันธ์ยัง active อยู่ จะตั้งค่า valid_to = NULL

Current ณ as_of valid_from <= as_of AND (valid_to IS NULL OR valid_to > as_of)

Historical valid_to IS NOT NULL AND valid_to <= as_of — เคยมีแต่หยุดแล้ว

อนาคต ข้อมูลที่เกิดหลัง as_of → ตัดทิ้ง (กันตอบด้วยสิ่งที่ ณ เวลานั้นยังไม่เกิดขึ้น)

เจาะลึก ตัวอย่าง Spanner Graph query (GoogleSQL)
GRAPH corp_radar
MATCH (c:Company)-[e:EXPOSED_TO]->(t:Theme {key: @theme})
WHERE e.valid_from <= @as_of
  AND (e.valid_to IS NULL OR e.valid_to > @as_of)   -- current
RETURN c.symbol, e.source_chunk_id, e.confidence;

แต่ละ edge เก็บ source_chunk_id และ confidence ไว้ด้วย ทำให้ทุกความสัมพันธ์ในกราฟ trace กลับไปยังข้อความต้นทางได้เสมอ การสร้างกราฟอยู่ที่ src/kb/graph_builder.py ซึ่งจะ resolve entity ซ้ำด้วยชื่อที่ normalize แล้วจาก Company Profile และกำหนดช่วงเวลาจาก doc_date รวมกับคำว่า "เริ่ม/ยกเลิก/ขายเมื่อ" ในเอกสาร

06Agent — 5 ขั้นตอนตอบคำถาม

คลิกแต่ละขั้นเพื่อดูรายละเอียด นี่คือหัวใจฝั่ง serving (โค้ด: src/agent/pipeline.py) — เมื่อผู้ใช้กดถาม หน้าเว็บจะแสดงขั้นตอนการทำงานแบบสดทีละขั้น โดยชื่อของแต่ละขั้นตอนตรงกับระเบียบวิธี 5 ขั้นในเอกสารชี้แจงของ SET แบบหนึ่งต่อหนึ่ง กรรมการจึงเห็นได้ด้วยตาตนเองว่าระบบเดินตามขั้นตอนที่ SET กำหนดโดยไม่มีการข้ามขั้น

ก่อนเริ่ม 5 ขั้น มี "Router" คั่นก่อน
classify_question() จะตรวจก่อนว่าคำถามอยู่ในประเภทไหน ได้แก่ impact (วิเคราะห์ผลกระทบ — โจทย์หลักของ POC), profile (โปรไฟล์ธุรกิจและความพึ่งพา) และ peer (เปรียบเทียบระหว่างบริษัท) หากเป็นประเภท impact จะเดินครบ 5 ขั้นตอนด้านล่าง

ขั้นนี้ใช้ LLM (temperature 0) แปลงคำถามให้เป็น "โครงเหตุการณ์" หรือที่เรียกว่า EventFrame:

อะไร — สินค้า บริการ อุตสาหกรรม ใบอนุญาต หรือวัตถุดิบที่เหตุการณ์นั้นเกี่ยวข้อง
ใคร — บทบาท เช่น ผู้ผลิต ผู้ขาย ผู้ถือหุ้น ผู้รับจ้างผลิต
อย่างไร — กลไกที่กระทบ เช่น รายได้ลด ต้นทุนเพิ่ม หรือต้องขอใบอนุญาตใหม่
เมื่อไหร่ — เทียบกับ as_of_date

ตัวอย่าง: คำถาม Digital Asset → อะไร: crypto, สินทรัพย์ดิจิทัล, exchange • ใคร: ผู้ได้ใบอนุญาต ก.ล.ต., ผู้ถือหุ้นใน exchange • อย่างไร: รายได้จาก trading, การด้อยค่าเงินลงทุน

เจาะลึก understand_event() จะโหลด prompt จาก prompts/understand_event.md แล้วเรียก llm.generate(..., temperature=0.0, json_schema=FRAME_SCHEMA) เพื่อบังคับให้ผลลัพธ์ตรงกับ schema ของ pydantic EventFrame


📝 Prompt ที่ใช้จริง (จาก prompts/understand_event.md — Gemini Flash, temperature 0)

ขั้นนี้ป้อนคำถามพร้อมวันที่อ้างอิงให้ Gemini Flash แล้วรับ JSON กลับมา ด้านล่างคือ prompt จริงที่ระบบใช้ทุกครั้ง:

บทบาท: นักวิเคราะห์ผลกระทบบริษัทจดทะเบียน SET
งาน: แตกคำถาม/เหตุการณ์ออกเป็น frame เพื่อใช้ค้นหาบริษัท ห้ามเดาข้อเท็จจริงเกี่ยวกับบริษัทใดๆ

อินพุต: {{question}} | as_of_date: {{as_of_date}}
ให้ตอบเป็น JSON ตาม schema: {what:[], who:[], how:[], when:""}
- what: สินค้า/บริการ/อุตสาหกรรม/ใบอนุญาต/วัตถุดิบที่เหตุการณ์แตะ
- who: บทบาท (ผู้ผลิต/ผู้ขาย/ผู้นำเข้า/ผู้ถือหุ้น/ผู้รับจ้างผลิต)
- how: กลไกผลกระทบ (รายได้ลด/ต้นทุนเพิ่ม/หยุดจำหน่าย/ต้องขอใบอนุญาต)
- when: timing เทียบ as_of_date

INPUT   question = "ณ 1 ม.ค. 2566 หุ้นใดมี exposure ต่อ Digital Asset/Crypto"   as_of_date = 2023-01-01

OUTPUT

{
  "what": ["สินทรัพย์ดิจิทัล", "คริปโทเคอร์เรนซี", "ศูนย์ซื้อขายสินทรัพย์ดิจิทัล (exchange)", "โทเคนดิจิทัล"],
  "who":  ["ผู้ได้รับใบอนุญาต ก.ล.ต.", "ผู้ถือหุ้นใน exchange", "ผู้ลงทุน/ผู้ขุดเหรียญ"],
  "how":  ["รายได้จากค่าธรรมเนียมซื้อขาย", "การด้อยค่าเงินลงทุนในสินทรัพย์ดิจิทัล"],
  "when": "ณ 2023-01-01 — ข้อมูลที่เกิดหลังวันนี้ถูกตัดทิ้ง"
}

ขั้นนี้แปลง EventFrame ให้เป็น ExposureCriteria โดยวิเคราะห์ตาม taxonomy 7 มิติ ได้แก่ ผลิตภัณฑ์/บริการ, วัตถุดิบ, ลูกค้า/ตลาด, ช่องทางจำหน่าย, ใบอนุญาต, ที่ตั้ง และ business segment

Query expansion — เพิ่มคำพ้องความ ชื่อแบรนด์ และชื่อใบอนุญาต ทั้งภาษาไทยและอังกฤษ เพื่อให้ค้นพบเอกสารที่เกี่ยวข้องได้ครบมากขึ้น

ขยายคำ: "digital asset" → crypto, คริปโต, สินทรัพย์ดิจิทัล, bitcoin, โทเคน, ICO, ใบอนุญาตศูนย์ซื้อขายสินทรัพย์ดิจิทัล, ก.ล.ต., exchange...

เจาะลึก ผลลัพธ์ที่ได้นำไปใช้สองทาง คือเป็นเงื่อนไข graph match และเป็น query text สำหรับ vector search — ดูที่ criteria_query_text()


📝 Prompt ที่ใช้จริง (จาก prompts/map_exposure.md — Gemini Flash, temperature 0)

ขั้นนี้ป้อน EventFrame และ taxonomy ให้ Gemini Flash แล้วรับ ExposureCriteria พร้อมชุดคำค้นขยายกลับมา:

อินพุต: EventFrame {{frame}} + taxonomy {{taxonomy}}
งาน: แปลง "ประเด็น/เหตุการณ์" เป็น ExposureCriteria 7 มิติ + query_expansion เพื่อเพิ่ม recall

กฎการทำ query_expansion (สำคัญต่อความแม่นยำ — กัน false positive ข้ามอุตสาหกรรม):
- ใส่ได้: synonym ไทย/อังกฤษ, ชื่อแบรนด์/ผลิตภัณฑ์, ชื่อทางวิทยาศาสตร์, ชื่อใบอนุญาต/มาตรฐาน,
  คำที่เฉพาะเจาะจงกว่า (hyponym), และ แนวคิด/เทคโนโลยีที่เกี่ยวข้องใกล้ชิดในโดเมนเดียวกัน
- ห้าม: หมวดหมู่กว้าง (hypernym) ที่ครอบ "อุตสาหกรรมอื่น" ที่ไม่เกี่ยวกับแก่นคำถาม
  เช่น "รถยนต์ไฟฟ้า" → EV, แบตเตอรี่ EV, สถานีชาร์จ — แต่ห้าม "ยานยนต์"/"พลังงาน" ลอย ๆ
- เกณฑ์ตัดสิน: คำขยายต้องทำให้ "เจอบริษัทที่ทำสิ่งที่ถามจริง ๆ" ไม่ใช่ "บริษัทใดก็ได้ในหมวดพ่อแม่"

มิติ exposure 7 มิติมาตรฐาน = product_service / raw_material / customer_market /
distribution_channel / license / location / business_segment — ถ้าเหตุการณ์กระทบผ่านมิติอื่น
(แหล่งเงินทุน, เทคโนโลยี/IP, พันธมิตร/JV, กฎระเบียบ, FX, ราคาโภคภัณฑ์, ESG) ให้ใส่ใน other_dimensions

ตอบเป็น JSON ตาม schema ExposureCriteria

INPUT   EventFrame จากขั้น 1 (ด้านบน) + taxonomy 7 มิติ

OUTPUT (JSON — ExposureCriteria)

{
  "theme_key": "digital_asset_crypto",
  "theme_label_th": "สินทรัพย์ดิจิทัล/คริปโท",
  "product_service": ["ศูนย์ซื้อขายสินทรัพย์ดิจิทัล", "นายหน้าสินทรัพย์ดิจิทัล", "การขุดเหรียญ"],
  "license": ["ใบอนุญาตประกอบธุรกิจสินทรัพย์ดิจิทัล (ก.ล.ต.)"],
  "business_segment": ["ธุรกิจสินทรัพย์ดิจิทัล"],
  "other_dimensions": ["เงินลงทุนในโทเคน/เหรียญ", "การร่วมทุน (JV) กับผู้ประกอบการ DA"],
  "query_expansion": ["crypto", "คริปโท", "สินทรัพย์ดิจิทัล", "bitcoin", "โทเคน", "ICO",
                      "exchange", "ก.ล.ต.", "Validator Node", "Bitkub", "KUB", "..."]
}
(มิติที่ไม่เกี่ยว เช่น raw_material / customer_market เว้นว่างได้)

ขั้นนี้ค้นหา candidate แบบ recall สูง คือยังไม่ตัดบริษัทใดออก ปล่อยให้ขั้น 4 เป็นผู้ตัดสิน:

① Graph match — ดึงทุกบริษัทที่มีเส้นเชื่อม EXPOSED_TO / PRODUCES / HOLDS_LICENSE / HAS_SEGMENT ตรงกับ criteria โดยแยกว่าเป็น current หรือ historical ด้วย valid_from/valid_to เทียบกับ as_of

② Vector search — ดึง top_k chunks ที่ match query ที่สุด (กรอง doc_date <= as_of) แล้วเก็บ symbol ที่พบเพิ่มเติม

③ Union — รวมเป็น candidate set

ผล: candidates = {MBKET: current, JMT: current, KTC: current...} + chunk หลักฐานของแต่ละตัว

เจาะลึก retrieve() คืน RetrievalResult ซึ่งมี candidates, graph_current, graph_historical, vector_hits และ query_vector สำหรับ theme ที่อยู่ใน taxonomy ระบบจะ "pin" chunk ที่ใช้สร้าง edge เข้าไปในขั้น assess เพื่อให้ผลคงที่ (deterministic) ส่วน surprise theme จะไม่ pin เพื่อป้องกัน evidence แกว่ง


🔎 ตรรกะ/Query ที่ใช้ (ไม่ใช้ LLM)

ขั้นนี้ไม่มี prompt — ระบบค้นฐานข้อมูลโดยตรงด้วย 2 วิธีพร้อมกัน แล้วรวมผลเข้าด้วยกัน:

① Spanner Graph query — ดึงทุกบริษัทที่มีเส้นเชื่อม EXPOSED_TO ยังมีผล ณ as_of:

GRAPH corp_radar
MATCH (c:Company)-[e:EXPOSED_TO]->(t:Theme {key: @theme})
WHERE e.valid_from <= @as_of
  AND (e.valid_to IS NULL OR e.valid_to > @as_of)   -- เฉพาะ current ณ as_of
RETURN c.symbol, e.source_chunk_id, e.confidence;

② Vector search — ค้น chunks ที่ความหมายใกล้เคียง query_expansion โดยกรอง doc_date <= as_of แล้ว union กับผลกราฟ

INPUT   criteria จากขั้น 2 + as_of = 2023-01-01

OUTPUT

candidates       = {BTC: current, COMAN: current, PROEN: current, AS: current, BC: current, ...}
graph_current    : บริษัทที่ยังมี exposure ณ 1 ม.ค. 66
graph_historical : บริษัทที่เคยมีแต่เลิกไปก่อน as_of
vector_hits      : chunk หลักฐานเพิ่มเติมที่ความหมายใกล้เคียง
pinned           : source_chunk_id ที่ใช้สร้าง edge → ส่งต่อให้ขั้น 4 (ให้ผลคงที่)
(ข่าวปี 2566 เป็นต้นไป ถูกกรองทิ้งเพราะ doc_date > as_of)

สำหรับแต่ละบริษัท ระบบจะดึงหลักฐานเฉพาะบริษัทนั้น จากนั้นrerank จัดอันดับให้หลักฐานที่ตรงประเด็นที่สุดอยู่บนสุด ลดทั้ง token และ noise แล้วเลือก top_n เข้าสู่การประเมิน

ขั้นนี้แบ่งหน้าที่ออกจากกันอย่างชัดเจน: LLM ทำหน้าที่เพียง "อ่านหลักฐานแล้วสกัดข้อมูล" เท่านั้น ส่วน "การตัดสินระดับผลกระทบ" เป็นหน้าที่ของโค้ดที่ทำงานตามตาราง Analysis Logic ของ SET แบบ deterministic โดย LLM จะสกัดข้อมูล 4 อย่างจากหลักฐาน ได้แก่ (1) exposure_type ว่าบริษัทเกี่ยวข้องโดยตรง โดยอ้อม เคยเกี่ยวข้องในอดีต หรือไม่มีความเกี่ยวข้อง (2) สัดส่วนรายได้ของธุรกิจที่เกี่ยวข้อง หาก disclosure ระบุตัวเลขไว้ (3) materiality สำรองจากตัวชี้วัดอื่น เช่น เงินลงทุน สินทรัพย์ หรือกำลังการผลิต ซึ่งใช้เมื่อไม่มีตัวเลขรายได้ และ (4) direction ว่าผลกระทบเป็นบวกหรือลบ โดยพิจารณาจากทิศทางของเหตุการณ์ในคำถาม ไม่ใช่จากน้ำเสียงของเอกสาร ทั้งนี้ หากคำถามไม่ระบุทิศทาง ระบบจะตอบว่า "ไม่ระบุ" ไม่เดาเอง จากนั้นโค้ดจะตัดเกณฑ์ Materiality จากสัดส่วนรายได้ (ตั้งแต่ 20% ขึ้นไป = สูง, 10–19% = ปานกลาง, ต่ำกว่า 10% = ต่ำ) แล้วนำไป derive ระดับผลกระทบจากตารางโดยตรง ผลก็คือ LLM ไม่มีสิทธิ์ชี้ขาดระดับผลกระทบเองได้เลย จึงป้องกันทั้งการแต่งเรื่องและความไม่คงเส้นคงวาของคำตอบได้อย่างเป็นระบบ

Materiality \ Exposureไม่มีhistoricalindirectdirect
สูง (รายได้ ≥20%)ไม่มีผลกระทบไม่มีผลกระทบน้อย–มากมาก
ปานกลาง (10–19%)ไม่มีผลกระทบไม่มีผลกระทบน้อย–ปานกลางปานกลาง
ต่ำ (<10%)ไม่มีผลกระทบไม่มีผลกระทบน้อยน้อย

นอกจากนี้ยังระบุ Direction ว่าเป็นบวกหรือลบ และ Impact type ว่าตรงหรืออ้อม ส่วนใดที่กระทบ และเป็นผลระยะสั้น กลาง หรือยาว

🚪 Grounding Gate — ด่านกัน hallucination
LLM ถูกบังคับให้ระบุ citation_ids ที่ต้องมีอยู่จริงในชุดหลักฐาน หากอ้างอิง id ที่ไม่มีอยู่จริง หรือไม่มี citation ใดเลยแต่กลับอ้างว่ามีผลกระทบ ระบบจะลดผลของแถวนั้นเป็น "ไม่มีผลกระทบ" ทันที และบันทึก log ไว้เพื่อตรวจสอบ นั่นหมายความว่าคำตอบใดที่ขาดหลักฐานรองรับจะไม่มีทางผ่านไปถึงผู้ใช้ได้เลย
🔍 ความโปร่งใสบนหน้าเว็บ
ทุกแถวในตารางผลลัพธ์สามารถคลิกที่ป้าย Impact ได้ ระบบจะเปิดหน้าต่าง "ขั้นที่ 4" ซึ่งแสดง (1) ผัง Analysis Logic ตามสไลด์ของ SET โดยไฮไลต์สีทองเฉพาะเส้นทางที่บริษัทนั้นเข้าเกณฑ์จริง (2) ตาราง Impact Assessment Matrix พร้อมช่องที่บริษัทตกอยู่ และ (3) ฐานการคำนวณ Materiality — กรณีที่ disclosure ระบุตัวเลขสัดส่วนรายได้ไว้ บรรทัด "รายได้ที่เกี่ยวข้อง ประมาณ X%" จะปรากฏใต้ป้ายในตารางหลักทันทีโดยไม่ต้องคลิกเพิ่ม

เจาะลึก เนื่องจาก POC มีแค่ 15 บจ. ระบบจึงประเมินทุกบริษัทที่รู้จักในทุกครั้ง แทนการพึ่ง rank cutoff ที่อาจแกว่งตรงขอบ วิธีนี้ทำให้ candidate set คงที่ ได้ stability 100% และไม่พลาดบริษัทใด การประเมินรันแบบขนานด้วย ThreadPoolExecutor โค้ดอยู่ที่ assess_company() และ prompts/assess_impact.md


📝 Prompt ที่ใช้จริง (จาก prompts/assess_impact.md — Gemini Flash, temperature 0, รันทีละบริษัท)

ขั้นนี้ป้อนหลักฐานเฉพาะของแต่ละบริษัทให้ LLM อ่าน แล้วให้ LLM สกัดข้อมูล 4 ค่า เพื่อส่งต่อให้โค้ดคำนวณระดับผลกระทบจากตาราง Impact Matrix:

อินพุต: symbol {{symbol}} + คำถาม/เหตุการณ์ {{scenario}} + evidence chunks {{evidence}} (มี chunk_id)
        + criteria {{criteria}} + as_of {{as_of_date}}

ทำตาม Analysis Logic ของ SET (ระบบจะคำนวณ Impact Level เองจากตาราง — งานของคุณคือประเมิน "อินพุต" ของตาราง):
1. exposure_type — direct (ทำ/ลงทุน/ถือใบอนุญาตโดยตรง) / indirect (ทางอ้อม ต้องมีประโยคในเอกสารเชื่อมชัด)
   / historical (เคยทำแต่เลิกก่อน as_of) / none (ไม่พบ; แผน/MOU/มติที่ยังไม่เริ่ม = none)
2. revenue_proportion_pct — สัดส่วนรายได้ธุรกิจที่เกี่ยวข้อง (%) ถ้า disclosure ระบุ; ไม่ระบุใส่ null
3. materiality (ใช้เมื่อไม่มี revenue %) — ประเมินจากเงินลงทุน/สินทรัพย์/กำลังผลิต: high/medium/low
4. direction บวก/ลบ/ไม่ระบุ — พิจารณาจาก "ทิศทางของเหตุการณ์ในคำถาม" ไม่ใช่ sentiment ในเอกสาร
   ถ้าคำถามไม่บอกทิศทาง (แค่ "มี exposure") → ไม่ระบุ อย่าเดา
5. reasoning 3-5 บรรทัด — ระบุ Materiality + ลักษณะ exposure + ส่วนที่กระทบ + ระยะเวลา + มาตรการผ่อนผัน (ถ้ามี)

กฎเหล็ก (กัน hallucination):
- ใช้เฉพาะข้อมูลใน evidence; ทุกประโยคใน reasoning ต้องมาจาก chunk ที่ให้
- คืน citation_ids ที่ "มีอยู่จริง" ใน evidence เท่านั้น
- หลักฐานไม่พอ/ไม่ตรงประเด็น → exposure_type="none"

ตอบ JSON: {exposure_type, revenue_proportion_pct, materiality, direction, reasoning, citation_ids[]}

INPUT   symbol = "PROEN" + คำถามเดิม + evidence chunks ของ PROEN (แต่ละชิ้นมี chunk_id) + criteria + as_of

OUTPUT (JSON — สิ่งที่ LLM สกัด)

{
  "exposure_type": "direct",
  "revenue_proportion_pct": null,
  "materiality": "low",
  "direction": "ไม่ระบุ",
  "reasoning": "ลงทุนตรงในเหรียญ KUB ทำหน้าที่ Validator Node ร่วมกับ Bitkub มูลค่าเงินลงทุนเล็กเมื่อเทียบสินทรัพย์รวม (materiality ต่ำ) กระทบด้าน 'เงินลงทุน' ระยะกลาง คำถามไม่ระบุทิศทางราคา จึงเป็น 'ไม่ระบุ'",
  "citation_ids": ["PROEN_onereport_2565_p192", "PROEN_news_2565_da"]
}

จากนั้น โค้ด (ไม่ใช่ LLM) นำ exposure_type=direct × materiality=low ไป lookup ตาราง Impact Matrix ได้ผลเป็น "น้อย" — LLM ไม่มีสิทธิ์ชี้ขาดระดับผลกระทบเองได้เลย

เรียง rows ด้วยลำดับคงที่ตาม severity ก่อน แล้วตาม symbol เรียง A→Z จากนั้นประกอบเป็นตาราง Symbol | Impact | Reasoning | Source

แนบ metrics: response_time_sec (จับด้วย time.perf_counter) และ token cost (USD + THB) จาก TokenMeter

เจาะลึก renderer มีให้เลือก 3 แบบ ได้แก่ Markdown table สำหรับ CLI/chat, Excel ที่บันทึกลง outputs/<run_id>.xlsx สำหรับแนบ presentation และ JSON ฉบับเต็มสำหรับ audit — ดูที่ sort_rows() ใน src/agent/format.py


🧾 ตรรกะที่ใช้ (โค้ดล้วน ไม่ใช้ LLM)

ขั้นนี้ไม่มี prompt — เป็นโค้ดล้วนที่เรียงแถวและประกอบตารางจากผลขั้น 4 ทั้งหมด:

① เรียงลำดับแถว — ฟังก์ชัน sort_rows() เรียงด้วยลำดับคงที่: ความรุนแรง (severity) มาก→น้อย ก่อน แล้วตาม symbol เรียง A→Z เพื่อให้ผลคงเส้นคงวาทุกครั้ง

② ประกอบตาราง — สร้างตาราง 4 คอลัมน์ Symbol | Impact | Reasoning | Source จากแถวที่เรียงแล้ว

③ แนบ metrics — ต่อท้ายด้วย response_time_sec (จาก time.perf_counter) และต้นทุน token (USD/THB) จาก TokenMeter

INPUT   rows = ผลประเมินทั้ง 15 บริษัทจากขั้น 4 + metrics

OUTPUT

| Symbol | Impact       | Reasoning (ย่อ)                                    | Source                        |
|--------|--------------|---------------------------------------------------|-------------------------------|
| BTC    | มาก          | นายหน้าซื้อขายสินทรัพย์ดิจิทัลโดยตรง...              | ข่าวแจ้งตลาด 2565, One Report |
| COMAN  | ปานกลาง       | ธุรกิจเกี่ยวกับสินทรัพย์ดิจิทัล...                    | One Report 2565               |
| PROEN  | น้อย          | ลงทุนในเหรียญ KUB เป็น Validator Node...            | One Report 2565 น.192         |
| ...    | ไม่มีผลกระทบ   | (อีก 10 บริษัท พร้อมเหตุผลว่าไม่พบหลักฐาน)          | ...                           |

⏱ response_time ≈ 40 วินาที   💰 cost ≈ 7 บาท/คำถาม

07ตัวอย่างเดินครบลูป — Digital Asset (ผลจริงจากระบบ)

ติดตามคำถามจริงไหลผ่านทั้ง 5 ขั้นจนได้คำตอบ — ระบบประเมินครบทั้ง 15 บริษัท พบ 5 บริษัทที่มี exposure แบบ Direct ได้แก่ BTC (มาก), COMAN (ปานกลาง), AS, BC และ PROEN (น้อย) ส่วนอีก 10 บริษัทที่เหลือตอบว่า "ไม่มีผลกระทบ" พร้อมเหตุผลที่อ้างอิงจากเอกสารจริงทุกแถว ใช้เวลาประมาณ 40 วินาที ต้นทุนประมาณ 7 บาทต่อคำถาม (คิดด้วยราคาจริงของ Vertex AI ณ กรกฎาคม 2569)

INPUT   "ณ 1 ม.ค. 2566 หุ้นใดมี exposure ต่อ Digital Asset/Crypto"   as_of = 2023-01-01

[1] เข้าใจ   → อะไร: crypto/exchange/ใบอนุญาต DA • ใคร: ผู้ถือหุ้น/ผู้ได้ใบอนุญาต
[2] criteria → theme=digital_asset_crypto • ขยายคำ 20+ terms (ไทย/อังกฤษ)
[3] ค้นหา    → graph: บริษัทที่ EXPOSED_TO theme (current/historical) ∪ vector hits
              กรอง doc_date <= 2023-01-01  (ข่าวปี 2566+ ถูกตัดทิ้ง)
[4] ประเมิน  → ทีละบริษัท: หลักฐาน → rerank → Impact Matrix → grounding gate
[5] จัดตาราง → เรียง severity→A-Z + เวลา + ต้นทุน

OUTPUT (รูปแบบ — ค่าเป็นตัวอย่างเพื่อแสดง format เท่านั้น)

SymbolImpactReasoning (ย่อ)Source
BTCมากประกอบธุรกิจนายหน้าซื้อขายสินทรัพย์ดิจิทัลโดยตรง (Direct × Materiality สูง)...ข่าวแจ้งตลาด 2565, One Report
COMANปานกลางมีธุรกิจที่เกี่ยวข้องกับสินทรัพย์ดิจิทัลตามที่เปิดเผยในรายงาน (Direct × ปานกลาง)...One Report 2565
PROENน้อยลงทุนตรงในเหรียญ KUB เป็น Validator Node ร่วมกับ Bitkub มูลค่าเล็กเทียบสินทรัพย์รวม...ข่าวแจ้งตลาด 2565, One Report น.192
ICHIไม่มีผลกระทบพบเพียงกรรมการเข้าอบรมหลักสูตร Digital Assets ไม่มีการลงทุนหรือรายได้เกี่ยวข้อง...One Report 2565 น.116

นี่คือผลจริงจากระบบ (AS และ BC ติดระดับ "น้อย" ด้วย รวมมีผลกระทบ 5 บริษัท อีก 10 บริษัทตอบ "ไม่มีผลกระทบ" พร้อมเหตุผลทุกแถว) — สังเกตว่า ICHI ตอบ "ไม่มีผลกระทบ" แต่ยังถูกรายงานไว้พร้อมหลักฐานว่า "มีเพียงการอบรม ไม่มีการลงทุนจริง" แสดงว่า grounding ทำงานถูกต้อง ไม่ใช่การละเว้นบริษัท (ตารางตัวอย่าง MBKET/JMT/KTC ในสไลด์ชี้แจงของ SET เป็นข้อมูลสมมติเพื่อแสดง format เท่านั้น)

08รูปแบบผลลัพธ์ที่ต้องส่ง

ตามสไลด์ POC แต่ละคำถามต้องแสดงผลลัพธ์ 2 ชุด ต่อหนึ่งคำถาม โดยแต่ละชุดประกอบด้วย (1) ตาราง (2) response time และ (3) cost

4 คอลัมน์บังคับ

Symbol — ตรงกับ Company Profile เรียงในลำดับคงที่เสมอ

Impact — แสดงเป็น ไม่มีผลกระทบ / น้อย / ปานกลาง / มาก พร้อมทิศทาง บวก หรือ ลบ

Reasoning — อธิบาย 3–5 บรรทัด ต้องมีตัวเลขและชื่อบริษัทกำกับ และทุกข้อความต้องอ้างอิงได้จริง

Source — ระบุชื่อเอกสาร งวด/วันที่ และเลขหน้า โดย map กับ chunk จริงในฐานข้อมูล

ทำไมต้อง 2 ชุด?

การรัน pipeline สองครั้งด้วย input เดียวกันช่วยตรวจสอบ stability ได้ โดย Symbol set ต้องออกมาเหมือนกันเป๊ะ และ impact ต้องไปในทิศทางเดียวกัน

บนหน้าเว็บมีปุ่ม "รัน 2 รอบ (Stability)" เมื่อกด ระบบจะรันคำถามเดิม 2 ครั้งติดต่อกัน แล้วแสดง step trace ของทั้งสองรอบซ้อนกัน เพื่อพิสูจน์ว่ารอบที่สองประมวลผลใหม่ครบทุกขั้นตอนจริง ไม่ได้เรียกคำตอบเดิมซ้ำ พร้อมทั้งแสดงตารางผลลัพธ์ทั้งสองชุด และตารางเปรียบเทียบ Symbol กับ Impact โดยอัตโนมัติว่าตรงกันหรือไม่ ซึ่งตรงกับเกณฑ์ Stability 10% ของ POC พอดี

มีเครื่องมือ diff ใน src/eval/stability.py สำหรับรายงานความแตกต่างถ้าพบ

เจาะลึก JSON schema ภายใน (src/common/models.py)
{
  "question": str, "as_of_date": "YYYY-MM-DD", "run_id": str,
  "rows": [{
    "symbol", "impact", "direction",
    "exposure_type": "direct|indirect|historical|none",
    "materiality": "high|medium|low",
    "reasoning", "sources": [{doc_id,title,date,page,snippet,uri}],
    "citation_ids": ["chunk_abc", ...]   // ต้องมีจริงในชุดหลักฐาน
  }],
  "metrics": { response_time_sec, tokens{input,output,by_model},
              total_token_cost_usd, total_token_cost_thb }
}

09การวัดคะแนน 70/10/10/10

ระบบมี harness อัตโนมัติที่ map ตรงกับเกณฑ์ทั้งสี่ข้อ อยู่ที่ src/eval/ สั่งรันด้วย python -m scripts.evaluate --suite poc

เกณฑ์วัดอะไรวิธี
ความถูกต้อง 70%Symbol ครบ/ถูก (P/R/F1), impact ตรง, ไม่ hallucinate, citation จริงเทียบ gold set + LLM-as-judge + ตรวจ citation id จริง 100% อัตโนมัติ
Follow 10%ครบทุกคอลัมน์, 2 ชุด, severity ตรง enum, มี time+costschema validation (instruction.py)
Stability 10%รันซ้ำได้ Symbol เดิมjaccard(run1, run2) = 1.0 + reasoning similarity
Efficiency 10%เวลา (p50/p95), ต้นทุน token/queryเทียบ budget ใน config (efficiency.py)
Hallucination check (อัตโนมัติ)
ทุก citation_id จะถูกตรวจว่าอยู่ในชุดหลักฐานจริงหรือไม่ และ snippet ต้องตรงกับข้อความในเอกสาร (fuzzy match) หากพบการอ้างอิงที่ปลอม ได้ 0 คะแนนทันที

รองรับ Surprise Test

กรรมการอาจถามคำถามใหม่ที่ไม่เคยเตรียมมาก่อนบนข้อมูลชุดเดิม ระบบไม่ได้ hardcode theme ใดๆ ไว้ ขั้น 1 และ 2 สร้าง criteria จากคำถามใหม่ได้เอง ส่วนขั้น 3 จะจับคู่ criteria กับ product, license หรือ segment ที่มีในกราฟ ระบบจึงตอบได้ทันทีโดยไม่ต้องแก้โค้ดใดเลย (สั่งรันด้วย --suite surprise --question "...")

พิสูจน์ด้วยการรันจริง — ชุดทดสอบ 30 คำถาม

ปุ่ม "ชุดทดสอบ" บนหน้าเว็บรวบรวมคำถาม 30 ข้อใน 6 หมวด ได้แก่ Digital Asset, ต้นทุน-วัตถุดิบ-พลังงาน, โลจิสติกส์-การค้าโลก-ค่าเงิน, การเงินมหภาค, อุตสาหกรรมเฉพาะ และเหตุการณ์-Temporal ทุกคำถามผ่านการรันจริงบนระบบและฝังผลจริงไว้เพื่อให้เปรียบเทียบได้

ผลรวมพิสูจน์ว่าทุกช่องของ Matrix เกิดขึ้นได้ในทางปฏิบัติ ทั้ง Direct (เช่น กลุ่มคริปโต 5 บริษัท), Indirect (เช่น ต้นทุนพลังงาน 6 บริษัท), เคยมีในอดีต 5 กรณี (เช่น SFLEX เคยมีประกันภัยน้ำท่วมแต่ยกเลิกก่อนวันที่ถาม), ทิศทางที่กลับได้ตามเหตุการณ์ (ค่าระวางเรือขึ้นหรือลง RCL ได้ประโยชน์หรือเสียประโยชน์สลับกันได้อย่างถูกต้อง) รวมถึงสัดส่วนรายได้จาก disclosure ที่ปรากฏในมากกว่า 10 จุด เช่น SFLEX ประมาณ 96%, AS ประมาณ 99%, OSP ประมาณ 80%

ต้นทุนเฉลี่ย ประมาณ 6.6 บาทต่อคำถาม (ราคาจริง Vertex AI ณ กรกฎาคม 2569) ตอบได้ภายใน 25–54 วินาที รวมทั้งชุดประมาณ 198 บาท — เท่ากับว่าระบบได้ซ้อม Surprise Test มาแล้ว 30 รูปแบบโดยไม่ต้องแก้โค้ดแม้แต่บรรทัดเดียว

10กลเม็ด Stability & Cost

ทั้งสองเกณฑ์นี้มักพลาดถ้าไม่ออกแบบรองรับตั้งแต่ต้น

⚖️ ทำให้นิ่ง (Stability)

กำหนด temperature = 0.0 และ top_p คงที่ทุก LLM call

Retrieval ไม่มี randomness เรียงด้วย score แล้วใช้ chunk_id เป็น tie-break

ประเมินครบทั้ง 15 บจ. ทุกครั้ง ทำให้ candidate set ไม่แกว่งตรงขอบ rank

สำหรับsurprise theme จะไม่ pin evidence และ anchor query ไว้กับ "คำถามดิบ" เพราะ Gemini ไม่ honor seed ทำให้ query expansion อาจแกว่งได้ — ดูคอมเมนต์ใน criteria_query_text()

💰 ลดต้นทุน (Cost)

Model routing — ใช้ Flash สำหรับงานง่ายอย่าง extraction และ classify ส่วนโมเดลที่แรงกว่าเก็บไว้เฉพาะงาน reasoning ที่ต้องลึก

Rerank ก่อนเข้า LLM — ส่งเฉพาะ top_n หลักฐานที่ตรงประเด็นเข้า LLM ลด input token ได้มาก

Caching — cache ผลลัพธ์ query→result และ embedding

Context budget — จำกัด token ต่อบริษัทไว้ล่วงหน้า และสรุปหลักฐานก่อนป้อนเพื่อประหยัดพื้นที่ context

เจาะลึก การวัด cost จริงTokenMeter จะอ่าน usage_metadata จาก response ของ Gemini ซึ่งระบุทั้ง prompt_token_count และ candidates_token_count สะสมรายครั้งโดยแยกตาม model แล้วคูณด้วยราคาใน config/pricing.yaml เป็น USD และ THB — ราคาใน config เป็นราคาจริงของ Vertex AI (Standard tier, Global endpoint) ดึงเมื่อ 2 กรกฎาคม 2569: Gemini 3.5 Flash input $1.50 / output $9.00 ต่อ 1 ล้าน tokens และ embeddings $0.000025 ต่อ 1,000 ตัวอักษร นอกจากนี้ฝั่ง ingestion ก็มี meter เดียวกันวัดต้นทุนทุกรอบ (สรุปท้ายรัน + แสดงใน /api/status)

11คำศัพท์ที่ควรรู้

คำความหมายแบบเข้าใจง่าย
Chunkชิ้นข้อความขนาดพอดี (~800 tokens) ที่หั่นจากเอกสาร เพื่อให้ค้นและป้อน LLM ได้
Embeddingการแปลงข้อความเป็นเวกเตอร์ตัวเลข เพื่อวัด "ความหมายใกล้เคียง"
Vector searchค้นด้วยความหมาย (ไม่ใช่คำตรงตัว) หาชิ้นที่ใกล้เคียงคำถามที่สุด
Knowledge Graphแผนที่ความสัมพันธ์ จุด=สิ่งของ (บริษัท/ผลิตภัณฑ์) เส้น=ความเชื่อมโยง
RAGRetrieval-Augmented Generation — ค้นหลักฐานก่อน แล้วให้ LLM ตอบจากหลักฐานนั้น (ไม่เดา)
Groundingการยึดคำตอบไว้กับหลักฐานจริง ทุกข้อความ trace กลับ source ได้
Rerankจัดอันดับหลักฐานที่ค้นมาใหม่ให้ตรงประเด็นที่สุด ก่อนส่งเข้า LLM
Temporal / as_ofการตอบ "ณ เวลาหนึ่ง" โดยตัดข้อมูลที่เกิดหลังจากนั้นออก
Materialityระดับนัยสำคัญ มักวัดจากสัดส่วนรายได้ (≥20% สูง / 10–19% กลาง / <10% ต่ำ)
Exposure linksเส้นเชื่อมในกราฟระหว่างบริษัทกับประเด็นความเสี่ยง/โอกาส พร้อมช่วงเวลาที่มีผล (ตัวเลขบนหน้า dashboard)
Analysis Logicผังการตัดสินใจของ SET: มี exposure ไหม → เปิดเผยรายได้ไหม → ใช้สัดส่วนรายได้หรือตัวชี้วัดอื่น → สรุประดับและทิศทาง
Idempotentทำซ้ำกี่ครั้งผลเหมือนเดิม (ป้อนไฟล์ซ้ำไม่เกิดข้อมูลซ้ำ)