# MCP Optimization: ลด round-trip ของ resolver ด้วย Jev (ตอนที่ 3)

## ปัญหาที่ MCP เจอทุกวัน: การปิงปอง

MarketDX เปิดให้ผู้ช่วย AI (เช่น Claude) เรียกข้อมูลตลาดผ่าน **MCP** — ชุด "เครื่องมือ" ที่ LLM เรียกใช้ได้เอง ค้นหุ้น ดึงงบ สกรีน ฯลฯ

ฟังดูง่าย แต่มีขั้นตอนหนึ่งที่แพงเงียบ ๆ คือ **การแปลงคำพูดคนให้เป็นค่าที่ระบบเข้าใจ**

ลองดูคำถามจริง:

> *"หุ้นในกลุ่มค้าปลีกที่มีสินทรัพย์หมุนเวียนเพิ่มขึ้นก้าวกระโดด"*

ก่อนจะสกรีนได้ ระบบต้องแปลง 3 ชิ้นนี้เป็น "โทเคน" ที่ฐานข้อมูลรู้จัก:

- "สินทรัพย์หมุนเวียน" → `CurrentAssets`
- "เพิ่มขึ้นก้าวกระโดด" → `accelerating`
- "กลุ่มค้าปลีก" → `2550` (รหัส GICS)

งานนี้เราทำด้วยเครื่องมือตระกูล `find_*` (find_fsconcept, find_fspattern, find_gics ...) ปัญหาคือมันทำงานแบบนี้:

1. LLM เรียก `find_fsconcept("สินทรัพย์หมุนเวียน")` → ระบบคืน **รายการผู้สมัคร (top-k)** กลับมาสัก 10 ตัว
2. LLM **อ่านแล้วเลือก** อันที่ใช่
3. ทำซ้ำกับ `find_fspattern` → คืน top-k → เลือก
4. ทำซ้ำกับ `find_gics` → คืน top-k → เลือก
5. **แล้วค่อย** เรียกเครื่องมือสกรีนจริงด้วยค่าที่เลือกได้

นับ round-trip ดู: **สี่รอบไปกลับ** กว่าจะได้เริ่มงานจริง ทุกรอบมี latency ของเน็ต บวกกับที่หนักกว่านั้น — **LLM ต้องเผา reasoning token อ่าน top-k แล้วตัดสินใจเลือกทุกครั้ง**

นี่คือ "การปิงปอง" ที่ MCP เจอแทบทุกคำถาม ยิ่งคำถามซับซ้อน (หลาย concept) ยิ่งปิงปองหลายรอบ ผู้ใช้ก็รอนานขึ้น

### `find_*` ทำงานยังไงข้างใน (vector search 101)

ก่อนไปต่อ ขออธิบายว่า `find_*` แปลง "สินทรัพย์หมุนเวียน" เป็น `CurrentAssets` ได้ยังไง เพราะมันคือหัวใจของเรื่องนี้

หัวใจคือเทคนิคชื่อ **vector embedding** — แปลง "ความหมาย" ของข้อความให้เป็นชุดตัวเลขยาว ๆ (เวกเตอร์หลายพันมิติ) ข้อความที่ความหมายใกล้กันจะได้เวกเตอร์ที่อยู่ใกล้กันในปริภูมิ

ระบบเราทำสองอย่าง:

1. **เตรียมล่วงหน้า (ครั้งเดียว):** ทุกแนวคิดใน catalog (เช่น "Total Revenue", "Current Assets", "Gross Margin") ถูกแปลงเป็นเวกเตอร์ด้วยโมเดล embedding (เราใช้ของ Gemini) แล้วเก็บไว้ในฐานข้อมูล
2. **ตอนมีคำถาม:** เอาคำที่ผู้ใช้ถาม ("สินทรัพย์หมุนเวียน") มาแปลงเป็นเวกเตอร์แบบเดียวกัน แล้ว **หาเพื่อนบ้านที่ใกล้ที่สุด** (nearest-neighbor ด้วย cosine similarity) — เวกเตอร์ยิ่งใกล้ = ความหมายยิ่งคล้าย

เรียงผลตามความใกล้ เอา k อันบนสุด = **top-k** (จริง ๆ มีอีกชั้นคือจับคำตรงตัว/ชื่อพ้อง — *lexical match* — มาก่อน semantic แต่หลักการรวมคือ "คืนผู้สมัครที่คล้ายที่สุดมาสัก 10 ตัว")

**จุดอ่อนอยู่ตรงนี้:** "ใกล้ในเชิงเวกเตอร์" ไม่ได้แปลว่า "ใช่" เสมอ คำที่ความหมายเฉียด ๆ กัน — "ผลตอบแทนผู้ถือหุ้น" กับ "เงินปันผลต่อหุ้น", "ค่าเผื่อหนี้สงสัยจะสูญ" กับ "ตั้งสำรองหนี้สงสัยจะสูญ" — เวกเตอร์มันใกล้กันมากจนโผล่ปนกันใน top-k และบางทีตัวผิดดันอยู่อันดับสูงกว่าตัวถูกด้วยซ้ำ นั่นคือเหตุผลที่ระบบ *ไม่กล้า* ฟันธงอันเดียว

### ทำไมต้องคืน top-k ตั้งแต่แรก?

คำถามที่ดีคือ — ทำไม `find_*` ไม่คืนคำตอบเดียวไปเลย ให้จบ?

เพราะ **อันดับ 1 ไม่ได้ถูกเสมอ** เราวัดจริงกับ `find_fspattern` (ตัวหา"รูปแบบ" ของงบ): ถ้าเชื่ออันดับ 1 ของ vector search ดื้อ ๆ จะถูกแค่ **71%** แต่ถ้าดู top-10 ทั้งชุด — คำตอบที่ถูก **อยู่ในนั้น 100%**

```
find_fspattern (51 รูปแบบ):
  เชื่ออันดับ 1 ดื้อ ๆ         → ถูก 71%
  คำตอบอยู่ใน top-10          → 100%
```

เห็นช่องว่าง 71% → 100% ไหม? **นั่นแหละคือเหตุผลที่มี top-k** — ระบบไม่มั่นใจพอจะฟันธง เลยโยนตัวเลือกกลับให้ LLM ตัดสิน และนั่นคือต้นตอของการปิงปอง

---

## ไอเดีย: ย้าย "การเลือก" ไปไว้ฝั่งเซิร์ฟเวอร์

ถ้าปัญหาคือ "ระบบไม่กล้าเลือก เลยโยนกลับให้ LLM" — แล้วถ้าเรามีตัวที่ **กล้าเลือกแทน** อยู่ฝั่งเซิร์ฟเวอร์ล่ะ?

นี่คือจุดที่ Jev เข้ามา จากตอน 1-2 เรารู้แล้วว่ามันเป็น "โมเดลตัดสินใจ" ที่เร็วและถูก — ให้ตัวเลือกกับคำถาม มันคืน **คำตอบเดียว + ความมั่นใจ (confidence)** ไม่ต้อง generate ข้อความยาว

โครงใหม่จึงเป็น:

```
เดิม:  user → LLM เลือก route → find_* คืน top-k → LLM เลือก → เรียก tool จริง   (ปิงปอง)
ใหม่:  user → LLM ส่งคำดิบเข้า tool → tool ให้ Jev เลือกเอง → ทำงานต่อได้เลย     (รอบเดียว)
```

LLM ส่ง "สินทรัพย์หมุนเวียน" เข้าเครื่องมือสกรีนตรง ๆ เครื่องมือเรียก Jev แปลงเป็น `CurrentAssets` ข้างในเอง แล้วสกรีนต่อได้เลย — **ตัดขั้นตอนโยน top-k กลับไปให้ LLM เลือกทิ้งทั้งหมด**

คำถามคือ Jev เลือกแม่นพอจะทำแบบนี้ไหม เราลองสองสถานการณ์

---

## กรณี catalog เล็ก: ให้ Jev เลือกจากทั้งชุดทีเดียว

`find_fspattern` มีรูปแบบทั้งหมด 51 ตัว — เล็กพอจะยัดทั้งชุดเป็นตัวเลือกให้ Jev เลือกในครั้งเดียว

**อ่านผล:**

```
find_fspattern (51 รูปแบบ, คำถามไทย/อังกฤษ):
  Jev เลือกทีเดียว           → ถูก 92.9%
  vector อันดับ 1 (แบบเดิม)   → ถูก 71.4%
```

Jev ทิ้งอันดับ-1 แบบเดิมขาด และเข้าใกล้เพดาน 100% ที่ปกติต้องพึ่ง LLM มาเลือกซ้ำ — **แต่ทำในครั้งเดียว ไม่ต้องปิงปอง**

### เซอร์ไพรส์: เคสที่ "พลาด" ส่วนใหญ่เป็นความผิดของเรา

ตอนแรกเรามีเคสที่ดูเหมือน Jev พลาด: คำถาม *"มูลค่าหุ้นเพิ่มขึ้นเป็นสองเท่า"* Jev ตอบผิดเป็น "สูงกว่าค่าอื่น" แทนที่จะเป็น "เติบโตทบต้น" (compound growth)

เกือบโทษ Jev แล้ว — แต่พอไปดู **สิ่งที่เราป้อนให้มัน** เราส่งแค่ "นิยามสั้น" ของแต่ละรูปแบบ ซึ่งนิยามของ compound growth เขียนเน้นแต่ "อัตราต่อปี (CAGR)" ไม่มีคำว่า "เป็นสองเท่า" เลย ทั้งที่ในฐานข้อมูลจริงมีคำอธิบายอีกฟิลด์ที่เขียนชัดว่า *"มูลค่าเพิ่มเป็นสองเท่า/สามเท่า"*

**วิธีแก้:** ป้อนคำอธิบายให้ครบเหมือนที่ระบบค้นหาใช้จริง — แก้ปุ๊บ Jev ตอบถูกทันที

> บทเรียนนี้ย้ำสิ่งที่เราเจอตั้งแต่ตอน 2: **ก่อนโทษโมเดล ให้ดูสิ่งที่เราป้อนมันก่อน** เกือบทุกเคสที่ "พลาด" กลายเป็น context ที่เราให้ไม่ครบ หรือเฉลยของเราเองที่แคบเกินไป

---

## กรณีหัวใจ: candidate เยอะเกินจะยัดหมด — vector + Jev

นี่คือกรณีที่เกิดบ่อยและสำคัญที่สุด **catalog ส่วนใหญ่ใหญ่เกินกว่าจะยัดทั้งชุดให้ Jev** — ตัวหาแนวคิดงบการเงิน (`find_fsconcept`) มีถึง **428 รายการ** (บรรทัดงบ + อัตราส่วน + ตัวคูณราคา) ตัวหาหุ้นมีเป็นหมื่น ตัวหาข้อมูลเศรษฐกิจมีเป็นหลักหมื่น

โยนทั้ง 428 ตัวให้ Jev เลือกทีเดียวทำไม่ได้ (ทั้งแพงและเกินขีดจำกัด) แต่เราไม่จำเป็นต้องทิ้งของเดิม — **เอา vector search ที่มีอยู่แล้วมาช่วยกรองก่อน**:

```
คำดิบ → [vector search กรองเหลือ top-15] → [Jev เลือก 1 จาก 15] → โทเคน
```

vector search ทำสิ่งที่มันเก่ง (กรองจากพันเหลือสิบ) Jev ทำสิ่งที่มันเก่ง (เลือกตัวที่ใช่จากสิบ) — **และการเลือกยังอยู่ฝั่งเซิร์ฟเวอร์ ไม่ต้องปิงปองกลับหา LLM**

**อ่านผล:**

```
find_fsconcept (428 รายการ, คำถามอังกฤษ):
  vector อันดับ 1 (แบบเดิม)         → ถูก 68%
  vector top-15 → Jev เลือก        → ถูก 100%
```

Jev แก้ **ทุกเคส** ที่ vector จัดอันดับ 1 ผิด และเป็นเคสยากจริง เช่น:

- "ผลตอบแทนผู้ถือหุ้น (ROE)" — vector เดาเป็น "เงินปันผลต่อหุ้น" → Jev เลือก `roe` ถูก
- "หนี้ที่มีภาระดอกเบี้ยรวม" — vector เดาเป็น "ดอกเบี้ยค้างจ่าย" → Jev เลือก `total_debt` ถูก
- "งานระหว่างก่อสร้าง", "เงินลงทุนที่ถือจนครบกำหนด", "กำไรขาดทุนเบ็ดเสร็จอื่นสะสม" — ศัพท์บัญชีลึกที่ vector ลอยหมด แต่ Jev เลือกถูก

ที่น่าทึ่งกว่าคือมันแยกแยะ "ประเภท" ได้ถูก: *"กำไรขั้นต้น"* → บรรทัดในงบ (`gross_profit`) ส่วน *"อัตรากำไรขั้นต้น"* → อัตราส่วน (`gross_margin`) — คนละตัวกัน Jev ไม่สับสน

---

## แล้วต้นทุนล่ะ? — vector + Jev ยังถูกกว่า

หลายคนคงคิดว่าเพิ่ม Jev เข้าไปก็ต้องแพงขึ้นสิ ตรงกันข้าม

| วิธี | Jev อ่านกี่ตัวเลือก | ต้นทุน / 1,000 ครั้ง |
|---|---:|---:|
| ยัดทั้ง catalog (51 ตัว) | 51 | $0.35 |
| **vector กรองก่อน → Jev (10-15 ตัว)** | ~10 | **$0.08** |

การกรองด้วย vector ก่อน **ถูกลง ~4 เท่า** เพราะต้นทุนหลักของ Jev คือความยาวของตัวเลือกที่ป้อนเข้าไป — อ่าน 10 ตัวย่อมถูกกว่าอ่าน 400 ตัว

แถมค่า embedding ของ vector แทบเป็นศูนย์ เพราะเราเก็บ vector ของคำค้นที่เคยเจอไว้ถาวรอยู่แล้ว (คำค้นซ้ำ = ไม่ต้องคำนวณใหม่) ส่วนตัว Jev เอง — คิดเงินเฉพาะ input, output ฟรี, และถูกกว่า LLM ทั่วไปหลายเท่า

**เทียบเป็น round-trip:** จาก **4 รอบ → 1 รอบ** ผู้ใช้ได้คำตอบไวขึ้นชัดเจน และ LLM ฝั่ง client ไม่ต้องเสียสมาธิมานั่งเลือก top-k ทุกคำถาม — มันเอา token ไปคิดเรื่องที่สำคัญกว่าได้ **นี่คือ UX ของ MCP ที่ดีขึ้นจริง**

---

## บทเรียนที่ต้องจำ (มีจริง)

การจะเอาไปใช้จริงต้องซื่อสัตย์กับข้อจำกัด — เราเจอ 4 เรื่อง

**1. context ที่ป้อนสำคัญกว่าที่คิด**
เกือบทุกเคสที่ Jev "พลาด" คือเราป้อนคำอธิบายไม่ครบ หรือเฉลยของเราเองแคบเกิน แก้คำอธิบายในฐานข้อมูล = ดีขึ้นทั้งระบบ (ทั้ง vector และ Jev กินคำอธิบายชุดเดียวกัน)

**2. "ถามยืนยัน" ต้องฉลาด ไม่งั้นทำลาย UX ที่เพิ่งแก้มา**
บางครั้ง Jev ก็ไม่มั่นใจจริง ๆ — คำถามกำกวมเอง หรือมีตัวเลือกสูสีสองตัว เราออกแบบให้เครื่องมือ **ดู "รูปทรง" ของความมั่นใจ** ไม่ใช่แค่ตัวเลขเดียว:
- มั่นใจชัด → ใช้เลย
- ก้ำกึ่งแต่พอได้ → ตอบไปก่อน พร้อมแนบหมายเหตุว่าตีความยังไง (ไม่บล็อก)
- สองตัวสูสีจริง + ต่างกันจริง → **ค่อย**ถามผู้ใช้ยืนยัน (เกิดน้อยมาก)

หลักคือ: ถามยืนยันเฉพาะตอนที่ "คำตอบที่ถูกมีอยู่จริงแต่ Jev เลือกไม่ขาด" เท่านั้น ไม่ใช่ถามทุกครั้งที่ไม่มั่นใจ — ไม่งั้นก็กลับไปปิงปองเหมือนเดิม

**3. หลายภาษา = ให้แปลเป็นอังกฤษก่อน (และนี่คือข้อจำกัดของ vector ไม่ใช่ Jev)**
เราลองศัพท์บัญชีลึกหลายภาษา เจอว่า *"ค่าความนิยม" (goodwill) ภาษาไทย* ตกไปอยู่อันดับ 45 ของ vector — หลุด top-15 ไป Jev เลยไม่เห็นให้เลือก ดูเผิน ๆ เหมือน Jev พัง

แต่เราพิสูจน์แล้วว่า **เป็นความอ่อนของ vector (embedding) ข้ามภาษา ไม่ใช่ Jev** — ถ้าถามคำเดียวกันเป็นภาษาอังกฤษ ("goodwill") มันขึ้นอันดับ 1 ทันที Jev เองก็รู้ว่า のれん (ญี่ปุ่น) = goodwill ด้วยซ้ำ

โชคดีที่เครื่องมือ MCP ของเรากำหนดให้ **client แปลเป็นอังกฤษก่อนส่งอยู่แล้ว** — ปัญหานี้จึงไม่เกิดในทางปฏิบัติ

**4. เส้นแบ่งบาง ๆ ทางบัญชี — Jev ผ่านสวย**
เราจัดชุดทดสอบโหดสุด: คู่ศัพท์ที่ต่างกันนิดเดียวแต่คนละความหมาย

| คู่ที่ต้องแยก | Jev แยกออกไหม |
|---|---|
| หุ้นทุนซื้อคืน (treasury stock) ↔ การซื้อหุ้นคืน (buybacks) | ✅ |
| ตั้งสำรอง (P&L) ↔ ค่าเผื่อ (งบดุล) | ✅ |
| สินทรัพย์ภาษีรอตัดบัญชี ↔ หนี้สินภาษีรอตัดบัญชี | ✅ |
| หมุนเวียน ↔ ไม่หมุนเวียน | ✅ |
| หนี้มีดอกเบี้ย ↔ หนี้สินรวม ↔ เงินกู้ (ไม่รวมลีส) | ✅ |

**25 คู่โหด — Jev ถูก 100% ด้วยความมั่นใจเฉลี่ย 0.998** เทียบกับ vector อันดับ 1 ที่ถูกแค่ 48% นี่คือจุดที่ห่วงที่สุด (ตอบผิดแบบมั่นใจ) แต่กลับผ่านสบายในภาษาอังกฤษ

---

## สรุป: เมื่อไหร่ควรยุบ resolver ด้วย Jev

จากที่ลองมา แนวทางที่ชัดคือ:

- **catalog เล็ก (ไม่กี่สิบ):** ให้ Jev เลือกจากทั้งชุดทีเดียว
- **catalog ใหญ่ (ร้อย/พัน/หมื่น):** ใช้ vector search กรองก่อนเหลือ ~10-15 → ให้ Jev เลือก — **ยังยุบ round-trip และถูกลงพร้อมกัน**
- **มีทางลัด cache:** คำที่เคยแปลแล้ว (เช่น "current assets" → `CurrentAssets`) เก็บผลไว้ ครั้งต่อไปคืนเลย ไม่ต้องเรียก Jev ซ้ำ

ทั้งหมดนี้ทำให้ MCP จาก "ปิงปองสี่รอบต่อคำถาม" เหลือ "รอบเดียว" — เร็วขึ้น ถูกลง และ LLM ฝั่ง client เบาลง

**ข้อควรระวัง:** อย่าเพิ่งสลับทั้งหมดในวันเดียว วิธีที่ปลอดภัยคือรัน Jev **คู่ขนาน**กับของเดิมบน traffic จริงสักพัก เทียบผล ดู distribution ของความมั่นใจ แล้วค่อยเปิดทีละส่วน — และเตรียม fallback ไว้เผื่อ Jev ช้าหรือล่ม (เราวัดได้ว่าปกติ ~1 วินาที แต่มี tail ที่พุ่งได้เป็นสิบวินาที)

แต่ทิศทางชัด: **resolver คือ "ภาษี round-trip" ที่ MCP จ่ายทุกคำถาม — Jev ช่วยจ่ายภาษีนั้นแทน** และแม้ candidate จะเยอะแค่ไหน การจับคู่ vector + Jev ก็ยังยุบมันได้ ทั้งเร็วขึ้นและถูกลง
