ข้าม​ไป​ยัง​เนื้อหา

Retrieval: หา context ที่​เกี่ยวข้อง

บท​ที่ 1 เรา​ปรับ​สายตา​มอง agent Order จาก​คอร์ส #15 ใหม่ — ทุก​อย่าง​ที่​ป้อน​เข้า model คือ context และ​เป้าหมาย​คือ “หา token สัญญาณ​สูง​ชุด​ที่​เล็ก​ที่สุด” บท​นี้​เริ่ม​ลงมือ​ชั้น​แรกจริงๆ ปัญหา​ที่​ตรง​หน้า​คือ: agent ของ​คุณ​ตอบ​เรื่องออเดอร์​ได้ (มี getOrder / getDeliveryStatus / issueRefund) แต่​พอ​ลูกค้า​ถาม​ว่า “ขอ​คืน​เงิน​ได้​ภายใน​กี่​วัน” หรือ “ยกเลิก​ออเดอร์​หลัง​ร้าน​รับ​แล้ว​ได้​ไหม” model ไม่มี​ทาง​รู้ — เพราะ​กติกา​พวก​นั้น​อยู่​ใน​เอกสาร​นโยบาย​ของ​บริษัท ไม่​ได้​อยู่​ใน​น้ำหนัก​ของ model วิธี​แก้​ที่​ผิด​คือ​ก๊อ​ปน​โย​บาย​ทั้ง​เล่ม​ยัด​เข้า system prompt วิธี​แก้​ที่​ถูก​คือ ดึง​เฉพาะ​ย่อหน้าที่​เกี่ยวข้อง​เข้า​มา​ตอน runtime — และ​นั่น​คือ retrievalRetrievalการ​ดึง​ข้อมูล​ที่​เกี่ยวข้อง​จาก​แหล่ง​ภายนอก (คลัง​เอกสาร, ฐาน​ข้อมูล) เข้า​มา​ใส่ context ตอน runtime — แทนที่​จะ​ยัด​ทุก​อย่าง​ไว้​ล่วงหน้า ใน​คอร์ส​นี้​ทำ​เป็น tool ตัว​หนึ่ง (`searchOrderKnowledge`) ที่ model เรียก​ได้​เอง ตาม​แนวคิด 'retriever เป็น tool ตัว​หนึ่ง​ที่ model ใช้ได้'Architecture ที่​เรา​จะ​สร้าง​เป็น tool ตัว​ที่​สี่​ใน​บท​นี้

📦 code ตัวอย่าง

บท​นี้​ต่อยอด agent Order ตัว​เดิม​จาก​คอร์ส #15 (repo kaen-food-ordering, กำลัง​จัด​ทำ) โดย​เพิ่ม tool ตัว​ที่​สี่ searchOrderKnowledge เข้าไป​ข้างๆ สาม​ตัว​เดิม พร้อม pipeline retrieval เต็มๆ — chunk → embed → vector store → search code ทุก​ชิ้น​ใน​บท​นี้​เป็น​ของ​จริง​ที่ copy ไป​รัน​ได้ ทั้ง capstone (บท​ที่ 8) จะ​หยิบ store ตัว​เดียวกัน​นี้​ไป​ใช้​ต่อ​ทั้ง​กับ retrieval และ memory

RAGRAG (Retrieval-Augmented Generation)retrieval-augmented generation — กระบวนการ​สอง​ขั้น: ดึง​ข้อมูล​ที่​เกี่ยวข้อง​จาก knowledge base ก่อน แล้ว​ค่อย​ให้ model generate คำ​ตอบ​จาก​ข้อมูล​นั้น (Lewis et al. 2020) คือ​การ​ผสม parametric memory (ความ​รู้​ใน weights) กับ non-parametric memory (index ภายนอก​ที่​อัปเดต/ตรวจ​ที่มา​ได้) เหมาะ​เมื่อ​ต้องการ​ข้อเท็จจริง​ที่​สด/เป็น​ความ​ลับ + อ้างอิง​แหล่ง​ที่มา​ได้Architecture (retrieval-augmented generation) แกะ​ออก​มา​ตรง​ตัว​คือ​สอง​ก้าว: retrieve แล้ว generate — ดึง​ข้อมูล​ที่​เกี่ยวข้อง​จาก​คลัง​ภายนอก​เข้า​มา​ก่อน แล้ว​ค่อย​ให้ model ตอบ​โดย​อิง​ข้อมูล​ก้อน​นั้น Chip Huyen ว่า “RAG employs a two-step process. It first retrieves relevant information from external memory and then uses this information to generate more accurate responses.”

แนวคิด​นี้​มา​จาก​เปเปอร์​ของ Lewis และ​คณะ (2020) ที่​นิยาม RAG ว่า​เป็น “models which combine pre-trained parametric and non-parametric memory for language generation” — โดย “the parametric memory is a pre-trained seq2seq model and the non-parametric memory is a dense vector index…” พูด​เป็น​ภาษา​วิศวกร: parametric memory คือ​ความ​รู้​ที่​อบ​อยู่​ใน​น้ำหนัก​ของ model (ตายตัว อัปเดต​ไม่​ได้​ถ้า​ไม่​เทรน​ใหม่) ส่วน non-parametric memory คือ​ดัชนี​ภายนอก​ที่​คุณ​แก้​ได้​ตลอด เหตุผล​ที่​ต้อง​มี​อย่าง​หลัง Lewis ระบุไว้ตรงๆ ว่า​ความ​รู้​ใน​น้ำหนัก model นั้น “providing provenance for their decisions and updating their world knowledge remain open research problems” — สอง​สิ่ง​ที่ retrieval แก้​ได้: ข้อมูล สด/เฉพาะ​องค์กร (นโยบาย​คืน​เงิน​ฉบับ​ล่าสุด) และ ที่มา​ที่​ตรวจสอบ​ได้ (ชี้​ได้​ว่า​คำ​ตอบ​มา​จาก​ย่อหน้า​ไหน)

Anthropic ให้​นิยาม​ระดับ​ปฏิบัติ​ที่​ตรง​กับ​สิ่ง​ที่​เรา​จะ​ทำ​เป๊ะ — “RAG is a method that retrieves relevant information from a knowledge base and appends it to the user’s prompt, significantly enhancing the model’s response.” หัวใจ​คือ​คำ​ว่า appends: เรา​ไม่​ได้​เปลี่ยน model เรา​แค่​เติม context ที่​ถูกต้อง​เข้าไป​ก่อน model ตอบ

ก่อน​จะ​สร้าง vector DB: ถ้า​มัน​ใส่​ได้​หมด อย่า​เพิ่ง retrieve

หัวข้อ​ที่​มีชื่อ​ว่า “ก่อน​จะ​สร้าง vector DB: ถ้า​มัน​ใส่​ได้​หมด อย่า​เพิ่ง retrieve”

ก่อน​ลงมือ ขอ​ปักหลัก​วินัย​ข้อ​หนึ่ง​ไว้​ก่อน เพราะ​มัน​ช่วย​คุณ​ไม่​ให้ over-engineer: ถ้า​คลัง​ความ​รู้​ทั้ง​ก้อน​ใส่ context window ได้​หมด ก็​อย่า​เพิ่ง​สร้าง vector DB — แค่​ใส่​ไว้​ใน system prompt เลย Anthropic ให้​เกณฑ์​ไว้​ว่า “If your knowledge base is smaller than 200,000 tokens (about 500 pages of material), you can just include the entire knowledge base in the prompt that you give the model, with no need for RAG or similar methods.”

⚠️ ตัวเลข 200,000 token เป็น​ค่าที่​ผูก​กับ​ยุค/รุ่น model

เกณฑ์ “200,000 token (~500 หน้า)” นี้​ผูก​กับ​ความ​จุ context window ของ model รุ่น​ที่​เขียน​ต้นทาง (2024) เมื่อ window โต​ขึ้น ตัวเลข​นี้​ก็​เลื่อน — อย่า​ท่องจำเป็น​สัจธรรม สิ่ง​ที่ ไม่​เลื่อน คือ​หลักการ: ถ้า​มัน​ใส่​ได้​หมด และ​การ​ใส่​หมด​ไม่​กิน​งบ​จน​เบียด​ของ​อื่น ก็​ไม่​ต้อง retrieve retrieval มี​ค่าตัว​ของ​มัน (โครงสร้าง​เพิ่ม, ผู้​ให้​บริการ embedding ตัว​ที่​สอง, ความหน่วง​ตอน​ค้น) จ่าย​มัน​เมื่อ​คลัง​โต​เกิน​หน้าต่าง​เท่านั้น

เพราะ​เหตุ​นี้​เรา​จะ​เดิน​เรื่อง​เป็น​ขั้น: (1) ตอน​คลัง​ยัง​เล็ก — นโยบาย​คืน​เงิน​ย่อหน้า​เดียว — ก็​ยัด​ไว้​ใน system prompt พอ ไม่​ต้อง​มี retrieval เลย → (2) พอ​คลัง​โต (นโยบาย​คืน​เงิน + SLA ยกเลิก + FAQ เดลิ​เวอรี​หลาย​สิบ​ย่อหน้า) จน​เริ่ม​เบียด​งบ → (3) ถึง​ค่อย​สร้าง pipeline chunk → embed → search แล้ว​ห่อ​เป็น tool ที่​เหลือ​ของ​บท​นี้​อยู่​ที่​ขั้น (3)

Anthropic ไล่ pipeline ของ RAG ไว้​เป็น​สาม​สเต็ป — “Break down the knowledge base (the ‘corpus’ of documents) into smaller chunks of text, usually no more than a few hundred tokens; Use an embedding model to convert these chunks into vector embeddings that encode meaning; Store these embeddings in a vector database that allows for searching by semantic similarity.” แกะ​ที​ละ​คำ:

  • chunkChunkingการ​หั่น​คลัง​เอกสาร​เป็น​ชิ้น​เล็ก ๆ (มัก​ไม่​เกิน​ไม่​กี่​ร้อย token) ก่อน​นำ​ไป embed และ​เก็บ​ใน vector store — ปม​สำคัญ​คือ​ชิ้น​ที่​เล็ก​เกิน​ไป​อาจ​ขาด​บริบท​จน​ค้น​เจอ​แต่​เข้าใจ​ผิด การ​เลือก​ขนาด/ขอบเขต chunk จึง​มี​ผล​ตรง​ต่อ​คุณภาพ retrievalArchitecture — หั่น​เอกสาร​เป็น​ชิ้น​เล็กๆ (ไม่​กี่​ร้อย token) เพราะ​เรา​อยาก​ดึง​มา เฉพาะ​ย่อหน้าที่​เกี่ยว ไม่ใช่​เอกสาร​ทั้ง​เล่ม สำหรับ​คลัง Order ของ​เรา 1 chunk อาจ​เป็น​หนึ่ง​ข้อ​ของ​นโยบาย​คืน​เงิน
  • embedEmbeddingเวกเตอร์​ตัวเลข​ที่​เข้า​รหัส 'ความหมาย' ของ​ข้อความ ทำให้​ค้น​ด้วย​ความ​คล้าย​เชิง​ความหมาย​ได้ (ดู Semantic Search) — ⚠️ Claude ไม่มี embeddings endpoint จึง​ต้อง​พึ่ง provider ที่​สอง คอร์ส​นี้​แนะนำ OpenAI `text-embedding-3-small` (1536 มิติ) ผ่าน IEmbeddingGenerator; ความ​จริง​คือ context engineering บังคับ​ให้​ใช้​สอง​ค่าย — Claude สำหรับ​การ​คิด อีก​เจ้า​สำหรับ​เวกเตอร์Architecture — แปลง chunk แต่ละ​ชิ้น​เป็น​เวกเตอร์​ตัวเลข​ที่ เข้า​รหัส​ความหมาย ข้อความ​ที่​ความหมาย​ใกล้​กัน​จะ​ได้​เวกเตอร์​ที่​อยู่​ใกล้​กัน​ใน​สเปซ
  • vector storeVector Storeที่​เก็บ embedding ที่​ค้น​ด้วย​ความ​คล้าย​เชิง​ความหมาย​ได้ — เก็บ​ทั้ง​เวกเตอร์​และ​ข้อความ​ต้นทาง​ที่​จะ​ป้อน​กลับ​ให้ model ใน​คอร์ส​ใช้ Microsoft.Extensions.VectorData (MEVD): abstraction เป็น GA แล้ว แต่​ตัว store จริง​ยัง prerelease (ใส่ `--prerelease`) model ข้อมูล​ประกาศ​ด้วย attribute เช่น `[VectorStoreVector(Dimensions: 1536)]` ที่ 'มิติ​ต้อง​ตรง​กับ model embedding' เป๊ะ ๆArchitecture — เก็บ​เวกเตอร์​เหล่า​นั้น​ไว้​ใน​ที่​ที่​ค้น​ด้วย ความ​ใกล้​เชิง​ความหมาย ได้ ไม่ใช่​ค้น​ด้วย​การ match คำ​เป๊ะๆ

การ​ค้น​แบบ​นี้​เรียก semantic searchSemantic Searchการ​ค้น​ด้วย​ความ​คล้าย​ของ​ความหมาย (vector search) แทน​การ​จับ​คู่​คำ​ตรง​ตัว​แบบ keyword/BM25 — เปลี่ยน query เป็น embedding แล้ว​หา record ที่​เวกเตอร์​อยู่​ใกล้​ที่สุด เป็น​แกน​หลัก​ของ RAG; แต่​วิธี term-based อย่าง BM25 ก็​เป็น baseline ที่​แข็งแรง​และ​เบา​กว่า ไม่​ต้อง​รีบ​เอื้อม​ไป​หา vector DB เสมอ​ไปArchitecture — Huyen ชี้​ว่า “Embedding-based retrieval is powered by vector search, which is also the backbone of many core internet applications such as search and recommender systems.” ข้อดี​คือ​ลูกค้า​ถาม​ว่า “เงิน​คืน​กี่​วัน” ก็​เจอ​ย่อหน้าที่​เขียน​ว่า “ระยะ​เวลา​ดำเนิน​การ​คืน​เงิน” ได้ ทั้ง​ที่​ไม่มี​คำ​ว่า “กี่​วัน” ตรง​กัน​เลย

🔁 อย่า​เพิ่ง​รีบ​คว้า vector DB เสมอ​ไป

Huyen เตือน​ว่า retrieval แบบ match คำ (term-based) ก็​มี​ที่​ของ​มัน — “Term-based retrievers, such as Elasticsearch and BM25, are much lighter to implement and can provide strong baselines.” ส่วน embedding-based นั้น “more computationally intensive but have the potential to outperform term-based algorithms.” แปล​ว่า: vector search แพง​กว่า​และ​ซับซ้อน​กว่า จ่าย​มัน​เมื่อ semantic ให้​ผล​คุ้ม อย่า​คว้า​มา​เป็น​รี​เฟล็​กซ์ บท​นี้​เรา​สร้าง embedding-based เพื่อ​ให้​เห็น​ครบ​วง แต่​ใน​งาน​จริง​ลอง BM25 เป็น baseline ก่อน​เสมอ

ก่อน​เขียน code embed มี​ข้อเท็จจริง​ที่​ต้อง​พูดตรงๆ และ​มัน​จะ​กำหนด​สถาปัตยกรรม​ของ​ทั้ง​คอร์ส​ใน​ส่วน retrieval/memory: Anthropic ไม่มี embeddings API — Claude สร้าง​เวกเตอร์​ไม่​ได้ เอกสาร​ของ Anthropic เอง​แนะ​ให้​ไป​ใช้​ผู้​ให้​บริการ​ภายนอก code retrieval ทุก​ชิ้น​จึง​ต้อง​มี ผู้​ให้​บริการ​ตัว​ที่​สอง มา​ทำ​หน้าที่​ฝั่ง​เวกเตอร์​ผ่าน​หน้า interface กลาง IEmbeddingGeneratorIEmbeddingGeneratorinterface กลาง​ใน Microsoft.Extensions.AI (`IEmbeddingGenerator<string, Embedding<float>>`) สำหรับ​สร้าง embedding แบบ​ไม่​ผูก​ค่าย — method หลัก `GenerateAsync(...)` คืน `Embedding<float>`/`GeneratedEmbeddings<>` (`.Vector` เป็น `ReadOnlyMemory<float>`) ⚠️ เพราะ Claude ไม่มี embeddings endpoint จึง​ต้อง​ต่อ​กับ provider ที่​สอง เช่น `new OpenAIClient(...).GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator()` (1536 มิติ ต้อง​ตรง​กับ Dimensions ใน vector store); เป็น​พื้น​ผิว API ที่​ขยับ​เร็ว​ที่สุด ต้อง pin ให้​ตรง version ที่​ติดตั้งArchitecture (IEmbeddingGenerator<string, Embedding<float>> ใน Microsoft.Extensions.AI.Abstractions)

⚠️ context engineering บังคับ​ให้​คุณ​มี​สอง​ผู้​ให้​บริการ

พูด​ให้​ขึ้นใจ: Claude ทำ​หน้าที่​เหตุผล (reasoning) แต่​การ​ทำ​เวกเตอร์​ต้อง​พึ่ง​คน​อื่น — ไม่มี​ทาง​ลัด​แบบ turnkey ของ Anthropic สำหรับ embeddings ทาง​เลือก​ที่​ซื่อตรง​สำหรับ​นัก​พัฒนา .NET เรียง​ตาม​ความ​ง่าย:

  • OpenAI text-embedding-3-small (1536 มิติ) ผ่าน Microsoft.Extensions.AI.OpenAI — GA, สะอาด​ที่สุด, .AsIEmbeddingGenerator() บรรทัด​เดียว คอร์ส​นี้​ใช้​ตัว​นี้​ใน​แบบฝึกหัด
  • Voyage AI (voyage-3-large) — เป็น​พาร์ตเนอร์​ที่ Anthropic แนะนำ​อย่าง​เป็น​ทางการ แต่ ไม่มี connector M.E.AI ทางการ ต้อง​ต่อ REST/OpenAI-compatible shim เข้า​หลัง IEmbeddingGenerator เอง — “Anthropic รับรอง แต่​ต้อง​ต่อ​สาย​เพิ่ม”
  • โลคัล (ONNX bge หรือ Ollama nomic-embed-text ผ่าน OllamaSharp) — ไม่​ต้อง​มี cloud key ตัว​ที่​สอง แต่​ความ​สมบูรณ์​ของ connector แปรผัน

จุด​สำคัญ​คือ​หน้า IEmbeddingGenerator กลาง คือ​สิ่ง​ที่​ทำให้​สลับ​สาม​ตัว​นี้​ได้​โดย​ไม่​รื้อ logic retrieval

ก่อน​ติดตั้ง ขอ​พูด​เรื่อง​สถานะ package ให้​จบ​ใน​ที่​เดียว (ที่​นี่ ที่​เดียว​ของ​ทั้ง​คอร์ส) เพราะ​มัน​มี​จุด​ที่​คน​สะดุด​บ่อย:

  • ตัว abstraction เป็น GA แล้วMicrosoft.Extensions.AI 10.8.0 และ Microsoft.Extensions.VectorData.Abstractions 10.8.0 (เผยแพร่ 2026-07-14) นี่​คือ​หน้า interface กลาง​ทั้ง IEmbeddingGenerator และ model ข้อมูล vector store
  • แต่ store ตัว​จริง​ทุก​ตัว​ยัง​เป็น prereleaseMicrosoft.SemanticKernel.Connectors.InMemory 1.74.0-preview, SqliteVec (preview), Qdrant (preview)
  • ชื่อ Microsoft.SemanticKernel.* ทำให้​เข้าใจ​ผิด​ได้ — connector พวก​นี้ ไม่​ได้​ผูก​กับ Semantic Kernel เป็น​ชื่อ​เดิม​ที่​ตกทอด​มา ใช้​ที่ไหน​ใน .NET ก็ได้ ไม่​ต้อง​ลาก SK ทั้ง​ก้อน​มา

ฉะนั้น​ตอน​ติดตั้ง store ตัว​จริง​ต้อง​ใส่​ธง --prerelease:

Terminal window
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI
dotnet add package Microsoft.Extensions.VectorData.Abstractions
dotnet add package Microsoft.SemanticKernel.Connectors.InMemory --prerelease

สำหรับ​คอร์ส​นี้ prerelease ไม่มี​ปัญหา แต่​ใน​โปร​ดัก​ชัน ให้​ตั้งใจ pin version และ​รู้ตัว​ว่า​กำลัง​พึ่ง preview อยู่

ตัว embedder มา​จาก connector ของ OpenAI (เพราะ Claude embed ไม่​ได้) สวม​เข้า​หน้า IEmbeddingGenerator:

using Microsoft.Extensions.AI;
using OpenAI;
IEmbeddingGenerator<string, Embedding<float>> embedder =
new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY"))
.GetEmbeddingClient("text-embedding-3-small") // 1536 มิติ — ต้องตรงกับ Dimensions ข้างล่าง
.AsIEmbeddingGenerator();

จาก​นั้น​นิยาม model ข้อมูล​ของ vector store ด้วย attribute จาก Microsoft.Extensions.VectorData:

using Microsoft.Extensions.VectorData;
public class OrderDoc
{
[VectorStoreKey]
public required string Id { get; set; } // "policy-refund-01", "faq-delivery-03"
[VectorStoreData(IsIndexed = true)]
public required string Kind { get; set; } // "policy" | "faq" | "order" (กรองได้)
[VectorStoreData]
public required string Text { get; set; } // chunk ที่เราจะป้อนกลับให้ Claude
[VectorStoreVector(Dimensions: 1536, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Hnsw)]
public ReadOnlyMemory<float>? Embedding { get; set; }
}
⚠️ dimensions ต้อง​ตรง​กับ model — bug เงียบ​อันดับ​หนึ่ง

text-embedding-3-small คืน​เวกเตอร์ 1536 มิติ ค่า​นี้ ต้อง เท่ากับ [VectorStoreVector(Dimensions: 1536)] เป๊ะ ถ้า​คุณ​สลับ​ไป​ใช้ model อื่น (เช่น text-embedding-3-large = 3072) แล้ว​ลืม​แก้ตัวเลข​นี้ — code จะ compile ผ่าน อาจ upsert ผ่าน แล้วไป​พัง​หรือ​ให้​ผล​ค้น​เพี้ยน​ตอน runtime นี่​คือ bug เงียบ​อันดับ​หนึ่ง ของ​งาน RAG มือใหม่ ทุก​ครั้ง​ที่​เปลี่ยน model embedding ให้​ไล่​แก้ Dimensions ตาม​ทันที

Kind ที่​ติด IsIndexed = true คือ property ที่​กรอง​ได้ (เรา​จะ​ใช้​แยก policy/faq/order — และ​ใน​บท​ที่ 3 จะ​เพิ่ม Kind = "memory" ลง store ตัว​เดียวกัน​นี้) ส่วน Text คือ chunk ดิบ​ที่​จะ​ป้อน​กลับ​ให้ Claude — เก็บ​แยก​ไว้​เสมอ

สร้าง store แบบ in-memory (พอ​สำหรับ​แบบฝึกหัด) แล้ว​เปิด collection:

using Microsoft.SemanticKernel.Connectors.InMemory; // prerelease; ชื่อเดิม ไม่ผูกกับ SK
VectorStore store = new InMemoryVectorStore();
VectorStoreCollection<string, OrderDoc> col =
store.GetCollection<string, OrderDoc>("order-knowledge");
await col.EnsureCollectionExistsAsync();

ตอน ingest: embed ทั้ง​ชุด​ใน​รอบ​เดียว (batch) แล้ว upsert ทีละ chunk — ตัว GenerateAsync(IEnumerable<string>) คืน GeneratedEmbeddings<Embedding<float>> ที่​เรียง​ตรง​ลำดับ input แต่ละ​ตัว​มี .Vector เป็น ReadOnlyMemory<float>:

string[] chunks =
[
"การคืนเงินดำเนินการภายใน 5–7 วันทำการ นับจากวันที่อนุมัติคำขอ",
"ยกเลิกออเดอร์ได้ฟรีก่อนร้านกดรับ หลังร้านรับแล้วยกเลิกไม่ได้ ต้องติดต่อ support",
"อาหารมาไม่ครบหรือผิดรายการ ถ่ายรูปแจ้งภายใน 24 ชม. เพื่อขอคืนเงินบางส่วน",
// ... อีกหลาย10 chunk จนคลังโตเกิน context window
];
GeneratedEmbeddings<Embedding<float>> vectors = await embedder.GenerateAsync(chunks);
for (int i = 0; i < chunks.Length; i++)
await col.UpsertAsync(new OrderDoc
{
Id = $"policy-{i}",
Kind = "policy",
Text = chunks[i],
Embedding = vectors[i].Vector,
});

ตอน​ค้น: embed คำถาม​ด้วย​ตัว embedder เดียวกัน (ต้อง​เป็น model เดียวกัน​เสมอ) แล้ว​เรียก SearchAsync — ผลลัพธ์​แต่ละ​ตัว​เป็น VectorSearchResult<OrderDoc> ที่​มี .Record กับ .Score (ค่า​ยิ่ง​สูง​ยิ่ง​ใกล้):

ReadOnlyMemory<float> q = (await embedder.GenerateAsync("ขอคืนเงินได้ภายในกี่วัน")).Vector;
await foreach (VectorSearchResult<OrderDoc> r in col.SearchAsync(q, top: 3))
Console.WriteLine($"{r.Score:F3} {r.Record.Text}");

ถ้า​อยาก​ค้น​เฉพาะ​บาง Kind ใช้ VectorSearchOptions<OrderDoc> (กรอง​ด้วย LINQ — ได้​เฉพาะ property ที่​ติด IsIndexed):

var opts = new VectorSearchOptions<OrderDoc>
{
Filter = d => d.Kind == "policy",
IncludeVectors = false,
};
await foreach (var r in col.SearchAsync(q, top: 3, opts)) { /* ... */ }
🔁 โหมด auto-embedding และ​กับดัก​ของ​มัน

MEVD ทำ embedding ให้​อัตโนมัติ​ได้: ตั้ง new InMemoryVectorStore(new InMemoryVectorStoreOptions { EmbeddingGenerator = embedder }) แล้ว​ประกาศ property เวกเตอร์​เป็น string ส่ง text ดิบ​เข้า SearchAsync ได้​เลย ประหยัด​การ embed ด้วย​มือ แต่​มี​กับดัก: property เวกเตอร์​แบบ auto-embed เป็น write-only — อ่าน​เวกเตอร์​หรือ text ต้นทาง​กลับ​มา​ไม่​ได้ ฉะนั้น เก็บ [VectorStoreData] Text แยก​ไว้​เสมอ สำหรับ​สิ่ง​ที่​จะ​ป้อน​กลับ​ให้ Claude

ทั้งหมด​ข้าง​บน​ยัง​ไม่ใช่ agent — มัน​เป็น​แค่ function ค้นหา จุด​พลิก​ของ​บท​นี้​คือ​ประโยค​เดียว​ของ Huyen: “The RAG pattern can be seen as a special case of agent where the retriever is a tool the model can use.” — retriever คือ tool ที่ model เรียก​เอง​ได้ ไม่​ต่าง​จาก getOrder เรา​จึง​ห่อ SearchAsync เป็น AITool ตัว​ที่​สี่ แล้ว​เสียบ​เข้า loop IChatClient เดิม​จาก​คอร์ส #15:

AITool searchKnowledge = AIFunctionFactory.Create(
async (string query) =>
{
var passages = new List<string>();
ReadOnlyMemory<float> qv = (await embedder.GenerateAsync(query)).Vector;
await foreach (var r in col.SearchAsync(qv, top: 3))
passages.Add(r.Record.Text);
return string.Join("\n---\n", passages); // ผลที่คัดแล้วและจำกัดจำนวน
},
name: "searchOrderKnowledge",
description: "ค้นนโยบายคืนเงิน/ยกเลิก และ FAQ เดลิเวอรี คืน 3 ย่อหน้าที่เกี่ยวข้องที่สุด");
// เสียบเข้าข้าง ๆ tool 3 ตัวเดิมจาก #15 — invariant ฝั่ง chat ไม่เปลี่ยนเลย
IChatClient chat = anthropicChatClient // x-api-key, anthropic-version 2023-06-01, "claude-opus-4-8", ไม่ส่ง temperature
.AsBuilder()
.UseFunctionInvocation()
.Build();
var options = new ChatOptions
{
Tools = [searchKnowledge, getOrder, getDeliveryStatus, issueRefund],
};
ChatResponse reply = await chat.GetResponseAsync(messages, options);

สังเกต​ว่า​ฝั่ง chat ทุก​กติกา​จาก​คอร์ส #15 ยัง​เป๊ะ​เหมือน​เดิม: header เป็น x-api-key (ไม่ใช่ Authorization: Bearer), anthropic-version: 2023-06-01, model id เปล่าๆ claude-opus-4-8 ไม่​ต่อ date suffix, และ ไม่​ตั้ง Temperature — เรา​เพิ่ม​แค่ tool ตัว​ที่​สี่ นอก​นั้น​ไม่​แตะ ตอน​นี้​พอ​ลูกค้า​ถาม​เรื่อง​นโยบาย model จะ เลือก​เอง ว่า​จะ​เรียก searchOrderKnowledge แล้ว​เอา​ย่อหน้าที่​ได้​มา​ตอบ

flowchart LR
  C["คลัง Order<br/>นโยบาย · SLA · FAQ"] -->|chunk| CH["ย่อหน้าเล็ก ๆ"]
  CH -->|embedder OpenAI<br/>1536 มิติ| E["เวกเตอร์"]
  E -->|upsert| VS["vector store<br/>ค้นด้วยความหมาย"]
  Q["คำถามลูกค้า<br/>ขอคืนเงินกี่วัน"] -->|embed คำถาม| QV["เวกเตอร์คำถาม"]
  QV -->|SearchAsync top 3| VS
  VS -->|3 ย่อหน้าที่เกี่ยว| TR["ผลของ tool<br/>searchOrderKnowledge"]
  TR --> M["Claude ตอบโดยอิงย่อหน้า"]
  classDef store fill:#0891b2,stroke:#064e5b,color:#f8fafc;
  class VS store;

คำ​บรรยาย​ภาพ: คลัง Order ถูก​หั่น​เป็น​ย่อหน้า​เล็กๆ แล้ว embed ด้วย​ผู้​ให้​บริการ​ตัว​ที่​สอง (OpenAI, 1536 มิติ — Claude ทำ​ขั้น​นี้​ไม่​ได้) เก็บ​เวกเตอร์​ลง vector store (ช่อง​สี​ฟ้า) ตอน runtime คำถาม​ลูกค้า​ถูก embed ด้วย model เดียวกัน​แล้ว​ค้น​ด้วย​ความ​ใกล้​เชิง​ความหมาย ได้ 3 ย่อหน้าที่​เกี่ยวข้อง​ที่สุด​กลับ​มา​เป็น​ผล​ของ tool searchOrderKnowledge ซึ่ง Claude เอา​ไป​อิง​เพื่อ​ตอบ — retriever คือ tool อีก​ตัว​หนึ่ง​ใน loop เดิม

retrieval ไม่​ได้​ดี​อัตโนมัติ คุณภาพ​ของ​มัน​วัด​ด้วย​สอง​มุม​ที่​ต้อง​เข้าใจ (นี่​คือ​การ สรุป​ความ กรอบ​ของ Huyen ไม่ใช่​การ​อ้าง​คำ​ต่อ​คำ):

  • context precision — ใน​บรรดา​ย่อหน้าที่​ดึง​มา มี​กี่​ย่อหน้าที่ เกี่ยว​จริง
  • context recall — ใน​บรรดา​ย่อหน้าที่ เกี่ยว​จริง​ทั้งหมด ดึง​มา​ได้​กี่​ย่อหน้า

จุด​ที่​รับ​น้ำหนัก​คือ​ความ​สัมพันธ์​ของ​สอง​ค่า​นี้​กับ​บท​ต่อๆ ไป: ถ้า​คุณ​ดัน top สูงๆ เพื่อ​กวาด recall ให้​ครบ คุณ​จะ​ได้ precision ต่ำ — คือ​ลาก​ย่อหน้าที่​ไม่​เกี่ยว​เข้า​มา​ปน context ด้วย และ ขยะ​ที่​ลาก​เข้า​มา​นั้น​ไป​กิน context budget แล้ว​เร่ง context rotContext Rotปรากฏการณ์​ที่​ความ​แม่น​ใน​การ​เรียก​ใช้​ข้อมูล​ของ model ค่อย ๆ แย่​ลง​เมื่อ context ยาว​ขึ้น — model ไม่​ได้​ประมวล​ผล​ทุก​ตำแหน่ง​เท่า​กัน (Chroma Research) คอร์ส​สอน 'หลักการ' นี้ ไม่​อ้าง​ตัวเลข % เจาะจง (เลื่อน​ไหว​เร็ว) บทเรียน​ที่​ได้​คือ​รักษา​หน้าต่าง​ให้​กระชับ​ด้วย Compaction + Curation แทนที่​จะ​ปล่อย​ให้​เต็ม​จน​เสื่อมArchitecture ที่​เรา​จะ​เจอ​เต็มๆ ใน​บท​ที่ 4–5 นี่​คือ​เหตุผล​ที่ tool ข้าง​บน​คืน​แค่ top: 3 ไม่ใช่ 30 — retrieval ที่​ดึง​เยอะ​เกิน​ไม่​ได้​แปล​ว่า​ตอบดี​ขึ้น มัน​แปล​ว่า​คุณ​จ่าย budget เพิ่ม​เพื่อ​ความ​เสี่ยง​ที่ model จะ​ไขว้​เขว

การ​หั่น chunk มี​ราคา​ที่​ต้อง​รู้ Anthropic ชี้​ว่า “In traditional RAG, documents are typically split into smaller chunks for efficient retrieval. While this approach works well for many applications, it can lead to problems when individual chunks lack sufficient context.” — พอ​หั่น​เป็น​ชิ้น​เล็ก ชิ้น​นั้น​อาจ​ขาด​บริบท​ที่​จำเป็น เช่น chunk ที่​เขียน​ว่า “ภายใน 5–7 วัน​ทำการ” อาจ​ไม่มี​คำ​ว่า “คืน​เงิน” อยู่​ใน​ตัว ทำให้​ค้น​เจอยาก

สูตร​แก้​ที่ Anthropic ตั้ง​ชื่อ​ว่า Contextual Retrieval คือ​แปะบริบทสั้นๆ นำ​หน้า​แต่ละ chunk ก่อน embed (เช่น “[นโยบาย​คืน​เงิน] การ​คืน​เงิน​ดำเนิน​การ​ภายใน 5–7 วัน​ทำการ”)

⚠️ ตัวเลข 35% / 49% / 67% เป็น​ผล eval เฉพาะ​ของ Anthropic ปี 2024

ต้นทาง​รายงาน​ว่า contextual embeddings อย่าง​เดียว​ลด​อัตรา​การ​ค้น​พลาด​ได้​ประมาณ 35% ผสม contextual BM25 (แบบ hybrid) ได้ 49% และ​เพิ่ม reranking เข้าไป​ได้​ถึง 67% — แต่​ตัวเลข​เหล่า​นี้​เป็น​ผล​จาก eval เฉพาะ​ชุด​ของ Anthropic เมื่อ​ปี 2024 ไม่ใช่​กฎ​ที่​ใช้ได้​กับ​ทุก​คลัง ให้​จำเป็น สูตร​ที่​มีชื่อ​และ​วัดผล​ได้ ไม่ใช่​ค่า​คงที่​ที่​คาด​หวัง​ได้​ทุก​ที่ — ถ้า​จะ​ใช้​จริง​ต้อง eval บน​คลัง​ของ​คุณ​เอง (ซึ่ง​เป็น​งาน​ของ​คอร์ส #13)

retrieval เพิ่ม​ความ​สามารถ​ให้ agent Order โดย​ไม่​แตะ model: หั่น​คลัง​นโยบาย​เป็น chunk → embed ด้วย​ผู้​ให้​บริการ​ตัว​ที่​สอง (เพราะ Claude ไม่มี embeddings endpoint) → เก็บ​ลง vector store → ค้น​ด้วย​ความหมาย → ห่อ​ทั้งหมด​เป็น searchOrderKnowledge tool ตัว​ที่​สี่​ที่​เสียบ​เข้า loop เดิม สิ่ง​ที่​ต้อง​ขึ้นใจ​สาม​ข้อ: (1) ถ้า​คลัง​ใส่​หน้าต่าง​ได้​หมด​ก็​อย่า​เพิ่ง retrieve (2) ต้อง​มี​สอง​ผู้​ให้​บริการ — Claude เพื่อ reasoning, อีก​เจ้า​เพื่อ​เวกเตอร์ (3) dimensions ต้อง​ตรง​กับ model (1536 == Dimensions: 1536) บท​หน้า​เรา​จะ​ใช้ store ตัว​เดียวกัน​นี้​เพิ่ม Kind = "memory" เพื่อ​ให้ agent เขียน​โน้ต​ความ​จำ​ของ​ตัวเอง​ที่​อยู่​รอด​ข้าม session


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

เช็กความเข้าใจ — บทที่ 2

ข้อ 1 / 3

ทำไม code retrievalถึงต้องมีผู้ให้บริการตัวที่สอง (OpenAI) นอกเหนือจาก Claude?