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 ตัวที่สี่ในบทนี้
บทนี้ต่อยอด agent Order ตัวเดิมจากคอร์ส #15 (repo kaen-food-ordering, กำลังจัดทำ) โดยเพิ่ม tool ตัวที่สี่ searchOrderKnowledge เข้าไปข้างๆ สามตัวเดิม พร้อม pipeline retrieval เต็มๆ — chunk → embed → vector store → search code ทุกชิ้นในบทนี้เป็นของจริงที่ copy ไปรันได้ ทั้ง capstone (บทที่ 8) จะหยิบ store ตัวเดียวกันนี้ไปใช้ต่อทั้งกับ retrieval และ memory
RAG คือ ดึงก่อน แล้วค่อยให้ model ตอบ
หัวข้อที่มีชื่อว่า “RAG คือ ดึงก่อน แล้วค่อยให้ model ตอบ”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 (~500 หน้า)” นี้ผูกกับความจุ context window ของ model รุ่นที่เขียนต้นทาง (2024) เมื่อ window โตขึ้น ตัวเลขนี้ก็เลื่อน — อย่าท่องจำเป็นสัจธรรม สิ่งที่ ไม่เลื่อน คือหลักการ: ถ้ามันใส่ได้หมด และการใส่หมดไม่กินงบจนเบียดของอื่น ก็ไม่ต้อง retrieve retrieval มีค่าตัวของมัน (โครงสร้างเพิ่ม, ผู้ให้บริการ embedding ตัวที่สอง, ความหน่วงตอนค้น) จ่ายมันเมื่อคลังโตเกินหน้าต่างเท่านั้น
เพราะเหตุนี้เราจะเดินเรื่องเป็นขั้น: (1) ตอนคลังยังเล็ก — นโยบายคืนเงินย่อหน้าเดียว — ก็ยัดไว้ใน system prompt พอ ไม่ต้องมี retrieval เลย → (2) พอคลังโต (นโยบายคืนเงิน + SLA ยกเลิก + FAQ เดลิเวอรีหลายสิบย่อหน้า) จนเริ่มเบียดงบ → (3) ถึงค่อยสร้าง pipeline chunk → embed → search แล้วห่อเป็น tool ที่เหลือของบทนี้อยู่ที่ขั้น (3)
pipeline: chunk → embed → vector store → search
หัวข้อที่มีชื่อว่า “pipeline: chunk → embed → vector store → search”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.” ข้อดีคือลูกค้าถามว่า “เงินคืนกี่วัน” ก็เจอย่อหน้าที่เขียนว่า “ระยะเวลาดำเนินการคืนเงิน” ได้ ทั้งที่ไม่มีคำว่า “กี่วัน” ตรงกันเลย
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 ก่อนเสมอ
ความจริงที่ต้องพูดออกมา: Claude ไม่มี embeddings endpoint
หัวข้อที่มีชื่อว่า “ความจริงที่ต้องพูดออกมา: Claude ไม่มี embeddings endpoint”ก่อนเขียน 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)
พูดให้ขึ้นใจ: 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หรือ Ollamanomic-embed-textผ่านOllamaSharp) — ไม่ต้องมี cloud key ตัวที่สอง แต่ความสมบูรณ์ของ connector แปรผัน
จุดสำคัญคือหน้า IEmbeddingGenerator กลาง คือสิ่งที่ทำให้สลับสามตัวนี้ได้โดยไม่รื้อ logic retrieval
สถานะ package: abstraction เป็น GA แต่ store ตัวจริงยัง prerelease
หัวข้อที่มีชื่อว่า “สถานะ package: abstraction เป็น GA แต่ store ตัวจริงยัง prerelease”ก่อนติดตั้ง ขอพูดเรื่องสถานะ package ให้จบในที่เดียว (ที่นี่ ที่เดียวของทั้งคอร์ส) เพราะมันมีจุดที่คนสะดุดบ่อย:
- ตัว abstraction เป็น GA แล้ว —
Microsoft.Extensions.AI10.8.0 และMicrosoft.Extensions.VectorData.Abstractions10.8.0 (เผยแพร่ 2026-07-14) นี่คือหน้า interface กลางทั้งIEmbeddingGeneratorและ model ข้อมูล vector store - แต่ store ตัวจริงทุกตัวยังเป็น prerelease —
Microsoft.SemanticKernel.Connectors.InMemory1.74.0-preview,SqliteVec(preview),Qdrant(preview) - ชื่อ
Microsoft.SemanticKernel.*ทำให้เข้าใจผิดได้ — connector พวกนี้ ไม่ได้ผูกกับ Semantic Kernel เป็นชื่อเดิมที่ตกทอดมา ใช้ที่ไหนใน .NET ก็ได้ ไม่ต้องลาก SK ทั้งก้อนมา
ฉะนั้นตอนติดตั้ง store ตัวจริงต้องใส่ธง --prerelease:
dotnet add package Microsoft.Extensions.AIdotnet add package Microsoft.Extensions.AI.OpenAIdotnet add package Microsoft.Extensions.VectorData.Abstractionsdotnet add package Microsoft.SemanticKernel.Connectors.InMemory --prereleaseสำหรับคอร์สนี้ prerelease ไม่มีปัญหา แต่ในโปรดักชัน ให้ตั้งใจ pin version และรู้ตัวว่ากำลังพึ่ง preview อยู่
สร้าง embedder และ model ข้อมูล
หัวข้อที่มีชื่อว่า “สร้าง embedder และ model ข้อมูล”ตัว 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; }}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 แล้วค้น
หัวข้อที่มีชื่อว่า “เก็บลง store แล้วค้น”สร้าง 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)) { /* ... */ }MEVD ทำ embedding ให้อัตโนมัติได้: ตั้ง new InMemoryVectorStore(new InMemoryVectorStoreOptions { EmbeddingGenerator = embedder }) แล้วประกาศ property เวกเตอร์เป็น string ส่ง text ดิบเข้า SearchAsync ได้เลย ประหยัดการ embed ด้วยมือ แต่มีกับดัก: property เวกเตอร์แบบ auto-embed เป็น write-only — อ่านเวกเตอร์หรือ text ต้นทางกลับมาไม่ได้ ฉะนั้น เก็บ [VectorStoreData] Text แยกไว้เสมอ สำหรับสิ่งที่จะป้อนกลับให้ Claude
หัวใจของบท: retrieval คือ tool อีกตัวหนึ่ง
หัวข้อที่มีชื่อว่า “หัวใจของบท: retrieval คือ tool อีกตัวหนึ่ง”ทั้งหมดข้างบนยังไม่ใช่ 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 เดิม
precision กับ recall: ทำไม retrieval ที่ห่วยลาก context rot มาให้
หัวข้อที่มีชื่อว่า “precision กับ recall: ทำไม retrieval ที่ห่วยลาก context rot มาให้”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 จะไขว้เขว
chunking ทำให้เสีย context — และสูตรแก้ที่มีชื่อ
หัวข้อที่มีชื่อว่า “chunking ทำให้เสีย context — และสูตรแก้ที่มีชื่อ”การหั่น 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 วันทำการ”)
ต้นทางรายงานว่า 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
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- Anthropic, “Effective context engineering for AI agents” (2025-09-29) — retrieval เป็นส่วนหนึ่งของการคัดสรร context และกลยุทธ์ just-in-time/hybrid
- Anthropic, “Contextual Retrieval in AI Systems” (2024-09-19) — นิยาม RAG เชิงปฏิบัติ, pipeline chunk→embed→vector DB, เกณฑ์ 200k token, chunk ที่ขาดบริบท และตัวเลข eval 35%/49%/67% (ลงวันที่ 2024, ไม่ใช่กฎ)
- Anthropic, “Embeddings” (เข้าถึง 2026-07-19) — Anthropic ไม่มี embeddings endpoint; แนะ Voyage เป็นพาร์ตเนอร์
- Lewis et al., “Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks” (2020-05-22) — ต้นกำเนิดและนิยาม RAG: parametric + non-parametric memory
- Chip Huyen, “AI Engineering” — chapter summaries (2025) — RAG สองก้าว, embedding = vector search, term-based vs embedding trade-off, retriever คือ tool (สรุปความ precision/recall)
- Microsoft Learn, “Microsoft.Extensions.AI libraries” (upd. 2026-06-18) —
IEmbeddingGenerator,GenerateAsync,Embedding<float>.Vector - Microsoft Learn, “Use vector stores in .NET AI apps” (ms.date 2026-02-28) — collection,
UpsertAsync,SearchAsync,VectorSearchResult<T> - Microsoft Learn, “Define your vector store data model” (ms.date 2026-03-24) — attribute
[VectorStoreKey]/[VectorStoreData]/[VectorStoreVector(Dimensions=)] - Microsoft DevBlogs, “AI + Vector Data Extensions GA” (2026) — abstraction เป็น GA
- Microsoft Learn API, “OpenAIClientExtensions.AsIEmbeddingGenerator” (เข้าถึง 2026-07-19) —
.AsIEmbeddingGenerator() - NuGet, “Microsoft.Extensions.VectorData.Abstractions 10.8.0” (เผยแพร่ 2026-07-14) — abstraction GA; store ตัวจริงยัง preview
เช็กความเข้าใจ — บทที่ 2
ข้อ 1 / 3ทำไม code retrievalถึงต้องมีผู้ให้บริการตัวที่สอง (OpenAI) นอกเหนือจาก Claude?