การเรียก model ครั้งแรก
บทที่แล้วเราวางกรอบว่า LLM คือ function text → token → text ที่คุณเรียกผ่าน HTTPS บทนี้เราลงมือ เรียกจริง — และเราจะเรียกด้วยมือเปล่า ไม่ใช้ SDK ไม่ใช้ library บัง มีแค่ HttpClient กับ System.Text.Json ที่คุณใช้อยู่ทุกวัน เหตุผลที่เริ่มจากดิบๆ แบบนี้เพราะทั้งคอร์สสร้างอยู่บน primitive ตัวเดียว: การส่ง JSON ก้อนหนึ่งไปที่ endpoint หนึ่ง แล้วอ่าน JSON ที่ตอบกลับมา ทุก SDK ทุก abstraction ในบทหลังๆ ก็แค่ห่อสิ่งนี้ไว้ ถ้าคุณเห็นทุก byte ตรงนี้ชัด บทที่เหลือจะไม่มีอะไรเป็นกล่องดำอีกเลย
คอร์สนี้ค่อยๆ ประกอบ agent สำหรับ support ลูกค้าบน domain Order ของบริการฟู้ดเดลิเวอรี จาก repo kaen-food-ordering (กำลังจัดทำ) เป็นตัวอย่างเดินเรื่องตลอดคอร์ส — บทนี้คือการเรียก API ครั้งแรกด้วยมือ สิ่งที่คุณเห็นในบทนี้คือรากที่ทุกบทหลังจากนี้ยืนอยู่
endpoint เดียว กับ3 header ที่ต้องส่งเป๊ะ
หัวข้อที่มีชื่อว่า “endpoint เดียว กับ3 header ที่ต้องส่งเป๊ะ”ปลายทางมีตัวเดียว: POST https://api.anthropic.com/v1/messages คุณส่ง JSON เข้าไปในเนื้อ request แล้วรับ JSON กลับมา จบ ส่วนที่พลาดกันบ่อยที่สุดไม่ใช่ body แต่เป็น header — ต้องส่งให้ครบสามตัวและต้อง ตรงตัว:
x-api-key— key ของคุณ (อ่านจาก environment variableANTHROPIC_API_KEY)anthropic-version— ต้องเป็น2023-06-01content-type—application/json
หนึ่ง — header key คือ x-api-key ไม่ใช่ Authorization: Bearer สาย Bearer ใช้กับ OAuth token เท่านั้น การเรียกด้วย API key ตรงๆ ใช้ x-api-key: $ANTHROPIC_API_KEY ถ้าคุณเผลอเอา pattern Authorization: Bearer ที่คุ้นจาก API อื่นมาใช้ จะได้ 401 กลับมา
สอง — anthropic-version: 2023-06-01 คือค่าปัจจุบันและเป็นค่าเดียวที่ยังไม่ถูก deprecate ไม่มี version ที่ใหม่กว่านี้ และต้องส่งไปกับ ทุก request ไม่ใช่ตั้งครั้งเดียว วันที่นี้ไม่ใช่ “version เก่า” ที่ต้องอัปเกรด — มันคือค่าที่ถูกต้อง ณ ตอนนี้
body: model, max_tokens, system และ messages[]
หัวข้อที่มีชื่อว่า “body: model, max_tokens, system และ messages[]”เนื้อ request เป็น JSON ก้อนเล็กๆ ที่มี4 field หลัก:
{ "model": "claude-opus-4-8", "max_tokens": 1024, "system": "You are a helpful assistant.", "messages": [ { "role": "user", "content": "Hello, Claude!" } ]}แกะทีละ field:
model— id ของ model ใช้ alias เปล่าๆ อย่างclaude-opus-4-8(ตัวอย่าง ณ 2026-07 — id เลื่อนไหวตามเวลา) อย่าเติมวันที่ต่อท้าย alias พวกนี้max_tokens— เพดานจำนวน TokenTokenหน่วยย่อยที่สุดที่ model ประมวลผล อาจเทียบเท่าคำ ส่วนของคำ ตัวอักษร หรือ byte ก็ได้ — ข้อความทุกชิ้นถูกแปลงเป็นชุด token ก่อนเข้า model สำหรับ Claude 1 token ราว 3.5 ตัวอักษรอังกฤษ (แปรตามภาษาที่ใช้) จำนวน token คือทั้งหน่วยวัดขนาดและหน่วยคิดเงิน (ดู Token Cost) — อย่าเดา token ของ Claude ด้วย tiktoken ของ OpenAI เพราะนับต่ำกว่าจริงArchitecture ที่ model สร้างได้ในคำตอบนี้ (จำนวนหน่วยข้อความ ไม่ใช่จำนวนตัวอักษร) เป็น field บังคับ ถ้าคำตอบยาวกว่านี้มันจะถูกตัดsystem— คำสั่งกำกับบทบาท/กติกาของ model จุดที่ต้องจำให้ขึ้นใจ:systemเป็น field ระดับบนสุดของ request ไม่ใช่ข้อความที่มีrole: "system"ใน arraymessages(นี่ต่างจาก API ของเจ้าอื่นที่คุณอาจเคยเห็น) เรื่องเนื้อหาในsystemจะเจาะในบทที่ 3messages— array ของ MessageMessageหน่วยหนึ่งเทิร์นของบทสนทนา ประกอบด้วย role (`user` หรือ `assistant`) กับ content — Messages API รับ `messages[]` ที่เรียงสลับกันไป และตัว API เป็นแบบ stateless คือต้องส่งประวัติทั้งชุดกลับไปใหม่ทุกครั้ง หมายเหตุ: ใน API ดิบไม่มี role ชื่อ `system` (มันเป็น field ระดับบนสุดแยกต่างหาก) และไม่มี role `tool` — ผลของ tool ถูกส่งกลับในข้อความ role `user`Architecture คือบทสนทนาทั้งหมด แต่ละชิ้นมีrole("user"หรือ"assistant") กับcontentประวัติสนทนาทั้งหมดอยู่ใน array นี้
จุดที่วิศวกร .NET ต้องปรับมุมมองคือข้อสุดท้าย: Messages API ไม่มี state ฝั่ง server model ไม่ได้ “จำ” รอบก่อนไว้ให้ ทุกครั้งที่เรียก คุณต้องส่ง array messages ทั้งชุดไปใหม่หมด บทสนทนาต่อเนื่องเกิดจากการที่ คุณ สะสม message แล้วส่งทั้งกองไปทุกครั้ง — role user คือสิ่งที่ผู้ใช้พูด role assistant คือคำตอบของ model ที่คุณเก็บไว้ต่อท้าย เรื่องนี้จะกลายเป็นหัวใจของ loop ในบทที่ 6
response: คำตอบอยู่ใน content[] ไม่ใช่ .text
หัวข้อที่มีชื่อว่า “response: คำตอบอยู่ใน content[] ไม่ใช่ .text”นี่คือจุดที่คนเขียนใหม่สะดุดแรงที่สุด response ที่ตอบกลับมาหน้าตาแบบนี้:
{ "id": "msg_013Zva2CMHLNnXjNJJKqJ2EF", "type": "message", "role": "assistant", "model": "claude-opus-4-8", "content": [ { "type": "text", "text": "Hello! How can I help you today?" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 14, "output_tokens": 11 }}สังเกตว่า ไม่มี field .text ที่ระดับบนสุด แม้จะเป็นคำตอบข้อความธรรมดาที่สุด มันก็มาในรูป content ที่เป็น array ของ block แต่ละ block มี type ของตัวเอง (ตรงนี้เป็น "text") คุณต้องวน array content แล้วเช็ค type ของแต่ละ block เพื่อดึงข้อความออกมา จะไปหยิบ .text ตรงๆ จาก root ไม่ได้
ทำไมต้องยุ่งขนาดนี้ในเมื่อคำตอบมี block เดียว? เพราะโครงสร้าง array นี้คือรากที่รองรับของที่ใหญ่กว่า text — ในบทที่ 4 คำตอบจะมีทั้ง block text และ block tool_use ปนกันใน array เดียว ถ้าคุณคุ้นกับการวน content ตั้งแต่ตอนนี้ tool use จะไม่ใช่เรื่องใหม่เลย มันแค่มี type เพิ่มขึ้นอีกแบบ
stop_reason: ทำไม model ถึงหยุด
หัวข้อที่มีชื่อว่า “stop_reason: ทำไม model ถึงหยุด”ทุก response มี field Stop ReasonStop Reasonfield `stop_reason` ในทุกคำตอบที่บอกว่า model หยุดเพราะอะไร — `end_turn` = ตอบจบเองตามธรรมชาติ, `tool_use` = model ขอเรียก tool (สัญญาณให้แตกกิ่งไปรัน tool), `max_tokens` = ชนเพดานที่ตั้งไว้ (คำตอบอาจถูกตัด) — เป็นสัญญาณควบคุมหลักของ Agent Loop ว่าจะวนต่อหรือหยุดArchitecture — stop_reason — บอกว่า model หยุดสร้างข้อความเพราะอะไร ในบทนี้คุณจะเจอค่า "end_turn" ซึ่งแปลว่า “Claude finished its response naturally” — model พูดจบตามธรรมชาติของมันเอง
ค่านี้ดูเหมือนข้อมูลประกอบเฉยๆ ในตอนนี้ แต่มันคือ สัญญาณควบคุม loop ในอนาคต ค่าอื่นที่จะสำคัญคือ "tool_use" (model ขอเรียก tool — บทที่ 4) และ "max_tokens" (คำตอบชนเพดานจนถูกตัด — บทที่ 7) การอ่าน stop_reason ทุกครั้งแล้วแตกกิ่งตามค่าของมัน คือกลไกที่ทำให้การเรียกครั้งเดียวกลายเป็น agent ที่วนได้ในบทที่ 6 บทนี้แค่ปักหมุดว่า field นี้มีอยู่และต้องอ่านมันเสมอ
usage: ทุกการเรียกบอกว่ากิน token ไปเท่าไร
หัวข้อที่มีชื่อว่า “usage: ทุกการเรียกบอกว่ากิน token ไปเท่าไร”field usage รายงานว่าการเรียกนี้ใช้ token ไปเท่าไร — input_tokens คือ token ของสิ่งที่คุณส่งเข้าไป output_tokens คือ token ของคำตอบ นี่คือหน่วยที่ผูกกับทั้งขนาดและค่าใช้จ่ายโดยตรง (ทวนจากบทที่ 1: token คือทั้งไม้บรรทัดวัดขนาดและมิเตอร์คิดเงิน)
มีรายละเอียดที่ต้องระวังตอนคำนวณต้นทุนจริง: ขนาด prompt ทั้งหมดไม่ได้เท่ากับ input_tokens เพียงตัวเดียว แต่เท่ากับ input_tokens + cache_creation_input_tokens + cache_read_input_tokens — input_tokens เดี่ยวๆ นับเฉพาะส่วนที่ไม่ได้มาจาก cache เท่านั้น ตอนนี้จำแค่ว่า usage คือแหล่งความจริงเรื่องต้นทุน และในบทที่ 6 จะเห็นว่าทุกรอบของ loop ส่งประวัติทั้งกองไปใหม่ ทำให้ token โตขึ้นเรื่อยๆ ทุกรอบ — งบ token จึงกลายเป็นหนึ่งในเงื่อนไขหยุดของ loop เอง
ประกอบทั้งหมดใน C#
หัวข้อที่มีชื่อว่า “ประกอบทั้งหมดใน C#”รวมทุกอย่างข้างบนเป็น code ที่รันได้จริง สังเกตว่าไม่มี temperature ในนี้ — และนั่นตั้งใจ (ทวนจากบทที่ 1: ส่ง temperature/top_p/top_k ไปกับ model รุ่นล่าสุดจะได้ HTTP 400)
using System.Net.Http.Json;using System.Text.Json;
var http = new HttpClient { BaseAddress = new Uri("https://api.anthropic.com/") };http.DefaultRequestHeaders.Add("x-api-key", Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"));http.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
var body = new { model = "claude-opus-4-8", // ตัวอย่าง ณ 2026-07 — อย่าเติมวันที่ต่อท้าย max_tokens = 1024, system = "You are a helpful assistant.", messages = new[] { new { role = "user", content = "Hello, Claude!" } }}; // ไม่มี temperature — 400 บน model รุ่นล่าสุด
using var resp = await http.PostAsJsonAsync("v1/messages", body); // ตั้ง content-type: application/json ให้เองresp.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());var root = doc.RootElement;
string? stopReason = root.GetProperty("stop_reason").GetString();foreach (var block in root.GetProperty("content").EnumerateArray()) if (block.GetProperty("type").GetString() == "text") Console.WriteLine(block.GetProperty("text").GetString()); // อ่านจาก content[] — ไม่มี .text ระดับบนสุด
var usage = root.GetProperty("usage");Console.WriteLine($"หยุดเพราะ: {stopReason} | " + $"in={usage.GetProperty("input_tokens").GetInt32()} " + $"out={usage.GetProperty("output_tokens").GetInt32()} token");ทุกบรรทัดที่ดูจุกจิกในนี้ล้วนมีเหตุผลตามที่อธิบายมา: x-api-key ไม่ใช่ Bearer, anthropic-version ส่งทุกครั้ง, PostAsJsonAsync ตั้ง content-type ให้เอง, วน content แทนการหยิบ .text, และไม่มี temperature เลย นี่คือ primitive ทั้งหมดของคอร์สนี้ใน code หน้าเดียว
flowchart TB
subgraph REQ["request ที่คุณส่ง"]
H["header: x-api-key<br/>anthropic-version 2023-06-01<br/>content-type json"]
B["body: model, max_tokens<br/>system, messages[]<br/>role user/assistant"]
end
REQ -->|POST /v1/messages| API["Anthropic<br/>Messages API"]
API -->|JSON ตอบกลับ| RES["response"]
subgraph RES["response ที่ได้กลับ"]
C["content[] — array ของ block<br/>วนเช็ค type แล้วอ่าน text"]
S["stop_reason<br/>เช่น end_turn"]
U["usage<br/>input/output token"]
end
classDef hot fill:#ea580c,stroke:#7c2d12,color:#f8fafc;
class C hot;
คำบรรยายภาพ: ซ้าย/บน — request ที่คุณประกอบเอง มี2 header ที่พลาดบ่อย (x-api-key ไม่ใช่ Bearer และ anthropic-version: 2023-06-01) กับ body ที่มี messages[] แยก role user/assistant ยิงไปที่ POST /v1/messages ขวา/ล่าง — response ที่ตอบกลับมามีสามส่วนที่ต้องอ่าน: content[] (จุดสีส้ม — เป็น array ของ block คุณต้องวนเช็ค type แล้วดึง text ไม่มี .text ที่ระดับบนสุด), stop_reason ที่บอกว่า model หยุดเพราะอะไร และ usage ที่นับ token เข้า-ออก
จากบทนี้ไปบทหน้า
หัวข้อที่มีชื่อว่า “จากบทนี้ไปบทหน้า”คุณเรียก Messages API ด้วยมือเปล่าเป็นแล้ว — ประกอบ3 header ส่ง messages[] ที่แยก role และอ่านคำตอบจาก content[] พร้อม stop_reason และ usage ทุกอย่างหลังจากนี้คือการต่อยอดจาก primitive ก้อนนี้ บทที่ 3 จะเจาะ field system ที่เราปล่อยผ่านไปในบทนี้ — วิธีตั้งบทบาทและกติกาให้ model ด้วยโครงสร้าง แล้วบทที่ 4 จะเพิ่ม block tool_use เข้าไปใน array content เดิมที่คุณคุ้นแล้ว เพื่อให้ model เรียก code C# ของคุณได้จริง
บทนี้อ้างอิงจากเอกสารทางการของ Anthropic สามหน้า อ่านต่อได้ที่ต้นทางโดยตรง:
- Anthropic, Messages API reference — endpoint
POST /v1/messages, header ที่ต้องส่ง, รูปร่าง body (model/max_tokens/system/messages[]) และ response (content[]เป็น array ไม่มี.textระดับบนสุด,usage) เข้าถึง 2026-07-19 - Anthropic, Handling stop reasons — ความหมายของ
stop_reasonเช่นend_turn(“Claude finished its response naturally”) ในฐานะสัญญาณควบคุม loop เข้าถึง 2026-07-19 - Anthropic, API versioning —
anthropic-version: 2023-06-01เป็นค่าปัจจุบันและค่าเดียวที่ยังไม่ถูก deprecate ต้องส่งทุก request เข้าถึง 2026-07-19
เช็กความเข้าใจ — บทที่ 2
ข้อ 1 / 3เมื่อเรียก Messages API ด้วยมือด้วย API key ควรตั้ง header key อย่างไร?