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

Parallelization — หั่น​ขนาน แล้ว​รวม​ผล

บท3 คุณ​ประกอบ workflow สอง​ตัว​ที่​เดิน​เป็น เส้นตรง — prompt chaining (ทำที​ละ​ขั้น​ตาม​ลำดับ) และ routing (จัด​ประเภท​หนึ่ง​ครั้ง​แล้ว​ส่ง​ต่อ​ไป​ทาง​เดียว) ทั้ง​คู่​ยัง​เป็น loop เดี่ยวๆ ที่​เรียก model ที​ละ​ครั้ง บท​นี้​เพิ่ม​มิติ​ใหม่​ที่​คอร์ส​ยัง​ไม่​เคย​แตะ — การ​เรียก model หลาย​ตัว พร้อม​กัน แล้ว​เอา​ผล​มา​รวม นี่​คือ multi-actor pattern ตัว​แรก​ของ​คอร์ส และ​เพราะ​เป็น​ตัว​แรก มัน​จึง​เป็น​บท​ที่​เรา​ต้อง เปิด​ด้วย​กติกา ก่อน​เปิด code

📦 code ตัวอย่าง

บท​นี้​ต่อยอด agent Order ตัว​เดิม​จาก​คอร์ส #15 (พร้อม context layer จาก #16) — repo kaen-food-ordering (กำลัง​จัด​ทำ) code ที่​แสดง​คือ รูปทรง fan-out → aggregate บน IChatClient เดิม ไม่มี tool หรือ store ใหม่ กติกา API ฝั่ง​แชต​ทุก​ข้อ​จาก​บท1 ยัง​ยึด​เหมือน​เดิม: model id เปล่า claude-opus-4-8 (ตัวอย่าง ณ 2026-07), ห้าม​ส่ง temperature, และ​อ่าน​คำ​ตอบ​ผ่าน resp.Messages[].Contents[] / resp.Text (convenience) ไม่ใช่​สมมติ​ว่า​มี .text ระดับ​บน​สุด

ก่อน​พูด​ถึง fan-out แม้แต่​บรรทัด​เดียว ต้อง​ตอก​เสา​ต้น​นี้​ให้​แน่น เพราะ​บท​นี้ (และ​บท5) เสี่ยง​ที่สุด​ที่​จะ​อ่าน​ผิด​เป็น “agent จริงๆ ต้อง​แตก​เป็น​หลาย​ตัว” — ซึ่ง ผิด

คอร์ส​นี้​สอน workflowWorkflowตาม​นิยาม BEA: 'systems where LLMs and tools are orchestrated through predefined code paths' — เส้นทาง​ถูก​กำหนด​ไว้​ใน code จึง​คาด​เดา​ได้​และ test ได้ (prompt chaining, routing, parallelization, orchestrator-workers, evaluator-optimizer) เป็น​ที่มา​ของ​คุณค่า​ส่วน​ใหญ่​ใน​โปร​ดัก​ชัน และ​เป็น​ค่า​เริ่มต้น​ที่​ควร​เอื้อมหา​ก่อน autonomous loop ตรง​ข้าม​กับ Autonomous AgentArchitecture แบบ multi-actor ก็​จริง แต่ loopAgent Loopวงจร runtime ที่ harness วน​ซ้ำ: gather context → take action → verify work → repeat (Claude Agent SDK) — เรียก model, dispatch tool ที่​ถูก​เรียก, ผนวก​ผลกลับ​เข้า context, ตรวจ​เงื่อนไข​หยุด แล้ว​วน​ใหม่ ลึก​กว่า loop ของ #15 บท6 ที่​หยุด​ด้วย predicate เดียว ตรง​ที่​เพิ่ม​เฟส verify — หลัง write action (เช่น issueRefund) อ่าน ground truth ซ้ำ (getOrder) ก่อน​ประกาศ​ว่า​เสร็จArchitecture เดี่ยว​ที่​มี​ขอบเขต​ยัง​เป็น ค่า​เริ่มต้น เสมอ Anthropic วาง​หลัก​ไว้​ชัด​ใน Building Effective Agents — “When building applications with LLMs, we recommend finding the simplest solution possible, and only increasing complexity when needed.” — และ​เจาะจง​กว่า​นั้น​ว่า “For many applications, however, optimizing single LLM calls with retrieval and in-context examples is usually enough.” คือ การ​หั่น​งาน​เป็น​หลาย call ที่​รัน​พร้อม​กัน​ไม่ใช่ อัปเกรด ที่​คุณ​เอื้อมหา แต่​เป็น ข้อ​ยกเว้น ที่​คุณ​ต้อง “ได้​มา” ด้วย​เหตุผล

นี่​คือ​กฎ​เดียว​กับ​ที่​คอร์ส #12 (agent เป็น bounded context) ตั้ง​เป็น default “1 agent 1 loop” และ​ที่ #16 เรียก sub-agent ว่า ทาง​เลือก​สุดท้าย — บท​นี้​แค่​พูด​มัน​ใหม่​ใน​ภาษา runtime: structure เป็น​สิ่ง​ที่​ต้อง​ได้​มา ไม่ใช่​รี​เฟล็​กซ์ เก็บ​ประโยค​นี้​ไว้ แล้ว​เรา​จะ​กลับ​มา​ปิด​ด้วย​มัน​ตอน​ท้าย​บท

ParallelizationParallelizationworkflow ตาม BEA: LLM หลาย​ตัว​ทำงาน​พร้อม​กัน​แล้ว​รวม​ผล​ด้วย code — สอง​แบบ: sectioning (หั่น​เป็น subtask อิสระ รัน​ขนาน​ด้วย `Task.WhenAll`) และ voting (รัน​งาน​เดิม N ครั้ง​แล้ว​เอา​เสียง​ข้าง​มาก) ⚠️ เป็น​ข้อ​ยกเว้น​ที่​ต้อง 'ได้​มา' ไม่ใช่​ค่า​เริ่มต้น: ใช้ได้​ต่อ​เมื่อ subtask เป็น​อิสระ​ต่อ​กัน​จริง ๆ ถ้า share context หรือ​พึ่งพา​กัน​ให้​ยุบ​กลับ​เป็น loop เดียว (#12) และ​พึง​จำ​ว่า​มัน​คูณ token ตาม​จำนวน callProcess คือ workflow ที่ Anthropic นิยาม​ไว้​ว่า “LLMs can sometimes work simultaneously on a task and have their outputs aggregated programmatically.” — model หลาย​ตัว​ทำงาน พร้อม​กัน แล้ว​เอา​ผล​มา รวม​ด้วย code (ไม่ใช่​ให้ model อีก​ตัว​มา​ตัดสิน​ใจ​ว่า​จะ​รวม​ยังไง — การ​รวม​เป็น code ของ​คุณ) มัน​มี​สอง​รูปแบบ​ที่​ต่าง​กัน​ชัด:

“Sectioning: Breaking a task into independent subtasks run in parallel. Voting: Running the same task multiple times to get diverse outputs.” — Anthropic (BEA)

  • sectioning — หั่น​งาน​หนึ่ง​เป็น subtask อิสระ หลาย​ชิ้น​ที่​ไม่​พึ่งพา​กัน แล้ว​รัน​ขนาน บน Order agent: ตรวจ “สิทธิ์​คืน​เงิน” + “SLA เดลิ​เวอรี​ถูก​ละเมิด​ไหม” + “ระดับ​สมาชิก​ของ​ลูกค้า” — สาม​คำถาม​ที่​ตอบ​ได้​แยก​กัน​โดย​ไม่​ต้อง​รอ​ผล​ของ​กัน​และ​กัน
  • voting — รัน งาน​เดิม ซ้ำ N ครั้ง​แล้ว​รวม​ด้วย​เสียง​ข้าง​มาก บน Order agent: เมื่อ​คำ​ตัดสิน “ออเดอร์​นี้​เข้า​เงื่อนไข​คืน​เงิน​ไหม” จาก call เดียว​ไม่​น่า​เชื่อถือ​พอ ให้​ถาม 5 ครั้ง​แล้ว​เอา​เสียง​ข้าง​มาก

Anthropic สรุป​เงื่อนไข​ที่​มัน​เหมาะ​ไว้​ว่า “Parallelization is effective when the divided subtasks can be parallelized for speed, or when multiple perspectives are needed.” — sectioning ซื้อ ความเร็ว (subtask อิสระ​รัน​พร้อม​กัน) ส่วน voting ซื้อ หลาย​มุมมอง (ลด​ความ​แปรปรวน​ของ call เดียว)

flowchart TD
  IN["งานเข้า<br/>เช่น ตรวจ 3 มุมของออเดอร์"]
  IN --> S{"หั่นเป็น subtask อิสระได้ไหม"}
  S -->|"ไม่ — share context หรือพึ่งพากัน"| ONE["ยุบเป็น loop เดียว<br/>ค่าเริ่มต้น #12"]
  S -->|"ใช่ — อิสระจริง"| FAN["fan-out ขนาน<br/>Task.WhenAll"]
  FAN --> A["call 1<br/>สิทธิ์คืนเงิน"]
  FAN --> B["call 2<br/>SLA เดลิเวอรี"]
  FAN --> C["call 3<br/>ระดับสมาชิก"]
  A --> AGG["รวมผลด้วย code<br/>aggregate"]
  B --> AGG
  C --> AGG
  AGG --> OUT["คำตอบเดียว"]
  classDef fan fill:#b91c1c,stroke:#7f1d1d,color:#f8fafc;
  class FAN,A,B,C fan;

คำ​บรรยาย​ภาพ: ทาง​แยก​อยู่​ที่​คำถาม​เดียว — “หั่น​เป็น subtask อิสระ​ได้​ไหม” ถ้า​ไม่ (share context หรือ​พึ่งพา​กัน) ให้​ยุบ​กลับ​เป็น loop เดียว​ตาม​ค่า​เริ่มต้น ถ้า​ใช่ จึง fan-out ขนาน​ด้วย Task.WhenAll (สาม​ช่อง​สี​แดง​คือ call ที่​รัน​พร้อม​กัน) แล้ว​มา​บรรจบ​ที่​ขั้น aggregate ที่ code ของ​คุณ​คุม​เส้นทาง​ไว้​ตายตัว — voting รวม​ด้วย code ล้วน ส่วน sectioning อาจ​เรียก model ช่วย​รวม​ก็​จริง แต่​ลำดับ​ทั้งหมด​เป็น path ที่ code กำหนด ต่าง​จาก orchestrator-workers (บท5) ที่ model เป็น​ผู้​กำหนดการ​แตก​งาน​และ​สังเคราะห์​เอง

IChatClient แต่ละ​ครั้ง​เรียก​เป็น state อิสระ (ไม่ share mutable state ระหว่าง call) จึง​ยิงขนานได้ตรงๆ ด้วย Task.WhenAll แล้ว​รวม​ผล​ด้วย code:

using Anthropic; // AnthropicClient, .AsIChatClient
using Microsoft.Extensions.AI; // IChatClient, ChatOptions, ChatResponse
AnthropicClient anthropic = new(); // อ่าน ANTHROPIC_API_KEY จาก env
IChatClient client = anthropic.AsIChatClient("claude-opus-4-8"); // bare id, ตัวอย่าง ณ 2026-07 — ไม่ต่อ suffix วันที่
// 3 subtask ที่ไม่พึ่งพากัน — เริ่มทั้งสามพร้อมกัน (ยังไม่ await ทีละตัว)
Task<ChatResponse> refund = client.GetResponseAsync($"ตรวจสิทธิ์คืนเงินของ {orderId}", options, ct);
Task<ChatResponse> delivery = client.GetResponseAsync($"ตรวจว่า {orderId} เกิน SLA เดลิเวอรีไหม", options, ct);
Task<ChatResponse> loyalty = client.GetResponseAsync($"ระดับสมาชิกของลูกค้าออเดอร์ {orderId} คืออะไร", options, ct);
ChatResponse[] sections = await Task.WhenAll(refund, delivery, loyalty); // เวลารวม ≈ call ที่ช้าสุด ไม่ใช่ผลบวกของทั้งสาม
// ขั้น aggregate เป็น code ของคุณ — ที่นี่ให้ model รวมสามผลเป็นคำตัดสินเดียว
ChatResponse merged = await client.GetResponseAsync(
"รวมสามผลนี้เป็นคำตัดสินเดียว:\n" + string.Join("\n---\n", sections.Select(s => s.Text)),
options, ct);

กำไร​ของ sectioning คือ latency: ถ้า​แต่ละ call ใช้​เวลา ~2 วินาที การ​รัน​ขนาน​ทำให้​เวลา​รวม​อยู่​ที่ ~2 วินาที (call ที่​ช้า​สุด) ไม่ใช่ ~6 วินาที (ผล​บวก) — แต่ จำนวน token ที่​จ่าย​คือ​ผล​บวก​เต็มๆ ทั้ง3 call ความเร็ว​ถูก​ลง แต่​ต้นทุน​ไม่​ลง จำ​แยก​สอง​อย่าง​นี้​ไว้

`.Text` ตรง​นี้​ใช้ได้ — แต่​รู้​ว่า​ทำไม

code ข้าง​บน​อ่าน s.Text เพราะ subtask พวก​นี้​คืน ข้อความ​ล้วน (สรุป/ตัดสิน ไม่​เรียก tool) ChatResponse.Text คือ convenience concatenation ของ text ทุก​ชิ้น​ใน​คำ​ตอบ ใช้​กับ​ผล​ข้อความ​ล้วน​ได้​สบาย แต่​ถ้า subtask ตัว​ไหน เรียก tool เอง คุณ​ต้อง​กลับ​ไป​ไล่ resp.Messages[].Contents[] แล้ว match FunctionCallContent / FunctionResultContent เหมือน​บท1 อย่า​เผลอ​ใช้ .Text แทน​การ​อ่าน​รูป​ดิบ​เมื่อ​มี tool call อยู่​ใน​คำ​ตอบ

voting ใช้ Task.WhenAll แบบ​เดียวกัน แต่​ยิง prompt เดิม N ครั้ง​แล้ว​รวม​ด้วย​เสียง​ข้าง​มาก เหมาะ​เมื่อ call เดียว​มี​ความ​แปรปรวน​สูง​เกิน​ไป​สำหรับ​การ​ตัดสิน​ที่​มี​ผล​ข้าง​เคียง (เช่น​คืน​เงิน):

// รันคำตัดสินเดิม 5 ครั้ง แล้วเอาเสียงข้างมาก — ซื้อ "หลายมุมมอง" เพื่อลดความแปรปรวน
RefundVerdict[] votes = await Task.WhenAll(
Enumerable.Range(0, 5).Select(async _ =>
(await judge.GetResponseAsync<RefundVerdict>(
$"ออเดอร์นี้เข้าเงื่อนไขคืนเงินไหม:\n{orderJson}", cancellationToken: ct)).Result));
RefundVerdict decision = MajorityVote(votes); // รวมด้วย code — คูณ token ไป 5 เท่า

GetResponseAsync<T> คือ overload structured-output ที่​คุณ​เห็น​ใน​บท3 (routing) — คืน​ผล​เป็น type RefundVerdict ที่ parse แล้ว การ​รวม (MajorityVote) เป็น code ของ​คุณ ไม่ใช่ model และ​สังเกต​ราคา: voting 5 เสียง = จ่าย token ห้า​เท่า​ของ call เดียว บิล​นี้​จะ​ไป​โผล่​ที่ cost meter ในบท8 — voting ไม่ใช่​ของ​ฟรี มัน​ซื้อ​ความ​เชื่อถือ​ด้วย​เงิน

เมื่อไร​ที่ ไม่ ควร fan out — เส้น​แบ่ง​อยู่​ที่​คำ​ว่า “อิสระ”

หัวข้อ​ที่​มีชื่อ​ว่า “เมื่อไร​ที่ ไม่ ควร fan out — เส้น​แบ่ง​อยู่​ที่​คำ​ว่า “อิสระ””

นี่​คือ​หัวใจ​ของ​บท และ​เป็น​จุด​ที่​ต้อง​พูดตรงๆ Parallelization ใช้ได้​ต่อ​เมื่อ subtask เป็น​อิสระ​ต่อ​กัน จริงๆ เท่านั้น — ถ้า​พวก​มัน share context หรือ​ผล​ของ​ตัว​หนึ่ง​เป็น input ของ​อีก​ตัว มัน​ไม่​ขนาน มัน​เป็น chain และ​คุณ​ต้อง​ยุบ​กลับ​เป็น loop เดียว

ค่า​เริ่มต้น​จาก #12 ยัง​ยืน: 1 agent 1 loop Parallelization ต้องการ subtask ที่ อิสระ ถ้า subtask share context หรือ​พึ่งพา​กัน คุณ​กำลัง​อยู่​ใน​เคส​ที่ Anthropic ชี้ตรงๆ ว่า​เป็น “[domains that] require all agents to share the same context or involve many dependencies between agents are not a good fit for multi-agent systems today.” (MARS, 2025-06-13) — ยุบ​กลับ​เป็น loop เดียว นี่​คือ​กฎ​ของ #16 พูด​ใหม่: structure เป็น​สิ่ง​ที่​ต้อง​ได้​มา ไม่ใช่​รี​เฟล็​กซ์

แล้ว​ราคา​ล่ะ? multi-actor ไม่ใช่​ของ​ฟรี Anthropic วัด​จาก​งาน​วิจัย​ของ​ตัวเอง​ว่า “agents typically use about 4× more tokens than chat interactions, and multi-agent systems use about 15× more tokens than chats.” — ⚠️ ตัวเลข 15× นี้​เป็น​ข้อมูล​จาก research workload ของ Anthropic เอง ลง​วัน​ที่ 2025-06-13 ไม่ใช่​ค่า​คงที่​ที่​การันตี​ทุก domain แต่​ทิศทาง​ชัด: fan-out คูณ​ต้นทุน token ขึ้น​เป็น​เท่าตัว การ​ตัดสิน​ใจ fan-out จึง​ต้อง​มี​คุณค่า​ที่ คุ้ม กับ token ที่​จ่าย​เพิ่ม ไม่ใช่​ทำ​เพราะ “ดู​เป็น agent มากกว่า”

map กลับ​ลง​บน Order agent ให้​เห็น​ชัด: core loop ของ​มัน — get order → ตัดสิน​สิทธิ์ → คืน​เงิน — เป็น dependency chain ที่ share context (จะ​ตัดสิน​สิทธิ์​ได้​ต้อง​มี order ก่อน จะ​คืน​เงิน​ได้​ต้อง​ตัดสิน​ก่อน) นั่น​คือ​เคส​ที่​ห้าม fan-out ตรงๆ มัน​ต้อง​เป็น loop เดียว แต่ ขั้น รวบรวม​ข้อมูล​ประกอบ​การ​ตัดสิน (สิทธิ์​คืน​เงิน / SLA / ระดับ​สมาชิก) เป็น subtask อิสระ​จริง — ตรง​นั้น sectioning ถึง​จะ​มี​ที่​ยืน เส้น​แบ่ง​ไม่ใช่ “agent นี้​ควร​ขนาน​ไหม” แต่​เป็น “ขั้นตอน​ไหน ของ​มัน​ที่​อิสระ​จริง”

อย่า​สับสน​สอง​ความหมาย​ของ 'ขนาน'

“parallelization workflow” (LLM call หลาย​ตัว รัน​ขนาน​ด้วย Task.WhenAll — ที่​บท​นี้​สอน) เป็น​คนละ​เรื่อง​กับ “การ​รัน tool หลาย​ตัว ขนาน​ภายใน​เทิร์น​เดียว” ของ middleware loop อัน​หลัง​คือ knob FunctionInvokingChatClient.AllowConcurrentInvocation (ค่า​เริ่มต้น false) ที่​คุม​ว่า​เมื่อ model ขอ tool call หลาย​ตัว​ใน​เทิร์น​เดียว จะ dispatch พร้อม​กัน​หรือ​ที​ละ​ตัว — มัน​คือ concurrency ภายใน 1 loop ไม่ใช่ workflow ที่​มี​หลาย LLM call อย่า​เอา​สอง​อัน​นี้​มา​ปน​กัน

Parallelization คือ workflow ที่​เอา LLM หลาย​ตัว​มา​รัน​พร้อม​กัน​แล้ว​รวม​ผล​ด้วย code — sectioning หั่น subtask อิสระ​เพื่อ​ซื้อ​ความเร็ว, voting รัน​งาน​เดิม N ครั้ง​เพื่อ​ซื้อ​ความ​เชื่อถือ ทั้ง​คู่​เขียน​ด้วย Task.WhenAll เหนือ IChatClient เดิม แต่​ประตู​เข้า​ของ​มัน​คือ​คำถาม​เดียว — subtask อิสระ จริง​ไหม — และ​ราคา​ของ​มัน​คือ token ที่​คูณ​ขึ้น (MARS วัด ~15× สำหรับ multi-agent) ค่า​เริ่มต้น​ยัง​เป็น loop เดียว fan-out เป็น​ข้อ​ยกเว้น​ที่​ต้อง​ได้​มา

บท​หน้า​เรา​ขยับ​ไป​อีก​ขั้น​ที่ ยาก กว่า — orchestrator-workers ที่ subtask ไม่​ถูก​กำหนด​ไว้​ล่วงหน้า แต่​ให้ model กลาง​เป็น​คน​หั่น​เอง นั่น​คือ​จุด​ที่​การ reconcile กับ #12 เข้มข้น​ที่สุด และ​ที่​ป้าย​ราคา 15× ต้อง​แขวน​คู่​ทุก​ครั้ง​ที่ pattern โผล่ 🔁 orchestrator-workers (บท5)


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

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

  • Anthropic, “Building Effective Agents” (2024-12-19) — นิยาม parallelization (“work simultaneously… aggregated programmatically”), สอง​รูปแบบ sectioning/voting, เงื่อนไข​ที่​มัน​เหมาะ, และ​หลัก “finding the simplest solution possible” ที่​เป็น​สมอ​ของ​การ reconcile
  • Anthropic, “How we built our multi-agent research system” (2025-06-13) — ป้าย​ราคา “multi-agent systems use about 15× more tokens than chats” และ​เคส​ที่ multi-agent ไม่ เหมาะ (“share the same context… many dependencies between agents”) ⚠️ ตัวเลข​จาก research workload ของ Anthropic เอง เป็น​สัญญาณ​ลง​วัน​ที่ ไม่ใช่​กฎ
  • Microsoft Learn, “Use the IChatClient interface”GetResponseAsync, ChatResponse.Text (convenience concat) และ GetResponseAsync<T> overload structured-output ที่ voting ใช้; แต่ละ call เป็น state อิสระ​จึง​ยิง​ขนาน​ด้วย Task.WhenAll ได้
  • Microsoft Learn, “FunctionInvokingChatClient” (pkg v10.7.0, upd. 2026-06-12) — knob AllowConcurrentInvocation (ค่า​เริ่มต้น false) ที่​คุม tool-call ขนาน​ภายใน​เทิร์น​เดียว — คนละ​เรื่อง​กับ parallelization workflow

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

ข้อ 1 / 3

Parallelization ตาม BEA มีสองรูปแบบอะไร?