เนื้อหาวันนี้

  1. Detect — LLM Agent เดี่ยว
  2. Investigate — + Tools
  3. Triage Context — + Session/State
  4. Sanitize — + Callbacks
  1. Triage Pipeline — Sequential Workflow
  2. Detection Rule — Custom Agent
  3. Contain or Escalate — Graph Routing
  4. Open Lab — Generate Report

ADK คืออะไร

Google เปิดตัวที่ Cloud Next 2025 (เม.ย. 2025) — open source, code-first framework สำหรับสร้าง stateful AI agent

4 ภาษา: Python (ตัวแรก) → Java → Go (พ.ย. 2025) → TypeScript (ธ.ค. 2025)

v2.1: native OpenAI support (model/openaimodel) — เชื่อม OpenAI-compatible endpoint ตรงได้เลย ไม่ต้องเขียน adapter เอง

ทำไม ADK

  • Multi-agent จริง — Sequential/Parallel/Loop + Graph routing (v2.0) ไม่ใช่แค่ 1 agent คุยกับ tool
  • MCP native ลึกสุดในกลุ่ม framework — เป็นได้ทั้ง MCP client และ MCP server ในตัวเดียว
  • เลือกโมเดลได้อิสระ — เหมาะกับ Gemini สุด แต่ต่อ 100+ provider ผ่าน LiteLLM ได้ (Go v2.1 ต่อตรงแบบวันนี้เลยก็ได้ ไม่ง้อ adapter)
  • Eval + Web UI ในตัว — เช็ค trajectory/response, debug เห็น event ทีละสเต็ป ไม่ต้องหา tool แยก
  • Deploy ที่ไหนก็ได้ — laptop → Cloud Run/GKE → Vertex AI Agent Engine

ADK Architecture

4 Core Concepts

ADK Core Concepts

วันนี้แค่ยอดภูเขาน้ำแข็ง

หมวด Package ทำอะไร
Tools tool/mcptoolset ต่อ MCP server ได้ทันที — ใช้ tool ของระบบอื่นโดยไม่ต้องเขียนเอง
Tools tool/agenttool ใช้ agent อื่นเป็น "tool" — agent เรียก agent ได้
Tools tool/toolconfirmation Human-in-the-loop — หยุดรอคนกดยืนยันก่อนรัน tool อันตราย
Tools tool/skilltoolset โหลด "skill" (tool+instruction สำเร็จรูปเป็นชุด) แล้วให้ agent เลือกใช้เองแบบ dynamic
Memory memory, memory/vertexai ความจำระยะยาว ข้ามหลาย session (ต่างจาก state วันนี้ที่อยู่แค่ใน session เดียว)
Artifact artifact, artifact/gcsartifact จัดการไฟล์แนบ/ผลลัพธ์ที่ agent สร้าง (รูป, PDF, รายงาน)
Production plugin/retryandreflect retry + self-heal อัตโนมัติเมื่อ tool call fail
Expose server/adka2a เปิด agent ให้ agent ทีมอื่น/org อื่นเรียกผ่าน A2A protocol

pkg.go.dev/google.golang.org/adk/v2 — ลองไล่ดูเองได้

Setup

export OPENAI_MODEL=qwen3.7-flash
export OPENAI_BASE_URL=https://inf-bifrost.sandbox.vayu-krungthai.com/v1
export OPENAI_API_KEY=sk-bf-xxxxxxx   # ค่าจริงจากบริษัท

cd ../workshop/adk-go-v2/step1-detect
go build ./...

Clean build = พร้อมเข้า Block 1

โจทย์วันนี้

ทีม SecOps ได้ auth log ดิบทุกวัน — ต้องไล่อ่านเองว่าเหตุการณ์ไหน "พิมพ์รหัส ผิดธรรมดา" กับ "credential-stuffing จริง"

ภารกิจ: สร้าง agent ที่อ่าน log ดิบ แล้วตัดสินใจเอง — auto_close หรือ escalate ให้คนตรวจ

2026-08-10T09:14:02Z login_failed user=alice ip=203.0.113.7
2026-08-10T09:14:05Z login_failed user=alice ip=203.0.113.7
2026-08-10T09:14:09Z login_failed user=alice ip=203.0.113.7
2026-08-10T09:14:13Z login_success user=alice ip=203.0.113.7

Log ปกติ vs ผิดปกติ

Normal vs Suspicious Log

Block 1

Detect — LLM Agent

LLM Agent (สมอง)

LLM Agent

Takeaway: agent เปล่าก็สืบสัญญาณได้ — แต่จำ context ข้ามรอบไม่ได้, เรียก tool จริงไม่ได้

Step 1 — IncidentAnalystAgent

mdl, err := openaimodel.NewModel(ctx, os.Getenv("OPENAI_MODEL"),
    &openaimodel.ClientConfig{
        APIKey:  os.Getenv("OPENAI_API_KEY"),
        BaseURL: os.Getenv("OPENAI_BASE_URL"),
    })

incidentAnalyst, err := llmagent.New(llmagent.Config{
    Name:        "IncidentAnalystAgent",
    Description: "Reviews raw auth log lines and points out suspicious patterns",
    Model:       mdl,
    Instruction: `You are a security on-call analyst...`,
})

../workshop/adk-go-v2/step1-detect/

Workshop 1

Incident มาตรฐานที่ใช้ทั้งวัน:

2026-08-10T09:14:02Z login_failed user=alice ip=203.0.113.7
2026-08-10T09:14:05Z login_failed user=alice ip=203.0.113.7
2026-08-10T09:14:09Z login_failed user=alice ip=203.0.113.7
2026-08-10T09:14:13Z login_success user=alice ip=203.0.113.7

🎯 แก้ 1 จุด: ปรับ Instruction ให้ตอบเป็นภาษาอังกฤษเสมอ ไม่ว่า user จะพิมพ์ภาษาไหน

Success: ถามเป็นไทย → คำตอบยังเป็นอังกฤษล้วน

Block 2

Investigate — Tools

Tools

LLM ทำได้แค่ "คิด" แล้วบอกว่าอยากเรียก tool อะไร — คนที่ลงมือทำจริงคือโค้ดฝั่งเรา

parse_log_line ใช้ strings/time.Parse ล้วนๆ — deterministic parse ของ <timestamp> <event> user=<name> ip=<addr> ไม่มี exec.Command เลย

Takeaway: tool description คือสิ่งเดียวที่ LLM เห็น — เขียนให้ชัด

Tools

Tools — Call Flow

Tool Call Flow

Step 2 — parse_log_line tool

func parseLogLine(_ agent.Context, args ParseLogLineArgs) (ParseLogLineResult, error) {
    fmt.Println(">>> parse_log_line called, input:") // ① เห็น input จริงที่ LLM ส่งมา
    fmt.Println(args.Line)
    entry, err := ParseLine(args.Line) // ② Go รันจริง (deterministic)
    ...
    return ParseLogLineResult{Event: entry.Event, User: entry.User, IP: entry.IP}, nil // ③ ผลจริงกลับไปให้ LLM
}

① เห็น argument จริงที่ LLM ส่งเข้า tool (ไม่เชื่อคำเล่า) ② parse จริง ไม่ใช่เดา ③ ผลลัพธ์ไม่ใช่ LLM เดา

../workshop/adk-go-v2/step2-investigate/

Workshop 2

🎯 แก้ 1 จุด: ทำให้ agent เรียก parse_log_line ทุกครั้งก่อนตอบ (ตอนนี้เรียกไม่สม่ำเสมอ) — หาจุดที่ควบคุมพฤติกรรมนี้ใน main.go เอง

Success (เช็คจาก log ตรงๆ ไม่ต้องเชื่อคำตอบ LLM):

  1. >>> parse_log_line called, input: โผล่ทุกครั้งที่รัน (paste incident เดิม ซ้ำ 3 รอบ)
  2. บรรทัดถัดจากนั้นตรงกับ line ที่ paste จริง (ไม่ถูก truncate/hallucinate)
  3. >>> parsed: event=... user=... ip=... โชว์ค่าที่ parse ถูกต้อง

Block 3

Triage Context — Session & State

Sessions & State

Sessions

Takeaway: state ข้าม session ใหม่ได้ — chat history ข้ามไม่ได้เลย

Sessions & State — เลือกกลไกยังไง

Requirement and Options

Takeaway: เลือกกลไกตามที่มาของข้อมูล ไม่ใช่ตามความเคยชิน

State Flow

State Flow Animation

Step 3 — จำ incident context

func setIncidentContext(
    ctx agent.Context, args SetIncidentContextArgs,
) (SetIncidentContextResult, error) {
    if err := ctx.State().Set(incidentContextKey, args); err != nil { // ① เขียนออกนอก agent
        return SetIncidentContextResult{}, err
    }
    return SetIncidentContextResult{Message: "saved: " + args.TicketID}, nil
}

① เก็บใน state ไม่ใช่ในตัว agent — turn ถัดไปอ่านผ่าน InstructionProvider ได้

../workshop/adk-go-v2/step3-triage-context/

Workshop 3

🎯 แก้ 1 จุด: เพิ่ม field Reporter ใน SetIncidentContextArgs แล้วโชว์ใน instructionWithContext

Success: รัน ./demo-workshop.sh — เช็คstate ดิบผ่าน GET session ว่ามี key "reporter" จริงไหม ไม่เชื่อคำตอบ agent (สคริปต์ตอบ PASS/FAIL)

Block 4

Sanitize — Callbacks

Callbacks

BeforeModelCallbacks รันก่อนทุก model call — return response ที่ไม่ nil = ข้ามโมเดล call ไปเลย ไม่มี API round-trip

ใช้ regex สแกน log line หา pattern ของ credential/PII ก่อนส่งออกไปนอกเครื่อง

Takeaway: อย่าไว้ใจ LLM ให้กรอง credential — ใช้โค้ดกรองก่อนยิง API เสมอ

Callbacks

Callbacks — Cost/Latency

Callback Latency

Step 4 — blockSensitiveData

var sensitivePattern = regexp.MustCompile(
    `(?i)(api[_-]?key|secret|password|token)\s*[:=]\s*['"]?[A-Za-z0-9/+_-]{8,}`)
func containsSensitiveData(text string) bool { return sensitivePattern.MatchString(text) }
func blockSensitiveData(_ agent.Context, req *model.LLMRequest) (*model.LLMResponse, error) {
    var text string // ดึงจาก req.Contents ตัวสุดท้าย
    if !containsSensitiveData(text) { return nil, nil }
    return &model.LLMResponse{
        Content: genai.NewContentFromText("⚠️ Blocked: ...", genai.RoleModel)}, nil
}

../workshop/adk-go-v2/step4-sanitize/

Workshop 4

🎯 แก้ 1 จุด: เพิ่ม pattern จับเลขบัตรเครดิตที่หลุดปนใน log message (เช่น msg="card=4111 1111 1111 1111 declined") ใน sensitivePattern — เขียน test ก่อน (red), แล้วค่อยแก้ให้ผ่าน (green) — regex ให้มาเลย ไม่ต้อง เขียนเอง จุดสอนคือ TDD red→green + guardrail ต้องเป็นโค้ด ไม่ใช่ทักษะ regex

Success: paste log line ที่มีเลขบัตร → ถูก block ทันที + unit test ผ่าน

Block 5

Triage Pipeline — Sequential Workflow

Workflow Agent (ผู้จัดการ)

Workflow Agent

Takeaway: deterministic pipeline — ไม่ให้ LLM เลือกลำดับรันเอง

TriagePipeline — ใครทำหน้าที่อะไร

Sequential pipeline business roles

Takeaway: ผู้จัดการไม่ตัดสินใจอะไรเลย แค่บังคับให้ทีมเฉพาะทางรันตามลำดับตายตัว

Step 5 — TriagePipeline

extractAgent, _ := llmagent.New(llmagent.Config{
    Name: "ExtractAgent", Model: newModel(), OutputKey: "temp:events"})

classifyAgent, _ := llmagent.New(llmagent.Config{
    Name: "ClassifyAgent", OutputSchema: classificationSchema}) // severity: low/high

pipeline, _ := sequentialagent.New(sequentialagent.Config{AgentConfig: agent.Config{
    Name: "TriagePipeline",
    SubAgents: []agent.Agent{extractAgent, classifyAgent, recommendAgent}}})

../workshop/adk-go-v2/step5-triage-pipeline/

Workshop 5

💡 prose อ่านง่าย แต่ paste ต่อ ticket เป็น checklist ทำตามทีละข้อไม่ได้

🎯 แก้ 1 จุด: แก้ instruction ของ RecommendAgent ให้ตอบเป็น fenced code block ชนิด runbook (numbered checklist) แทน prose ธรรมดา

Success: ./demo.sh ต้องได้แบบนี้ (ไม่ใช่ prose)

1. Force password reset for alice
2. Block IP 203.0.113.7

Block 6

Automate Detection Rule — Custom Agent

Custom Agent (ผู้เชี่ยวชาญ)

Custom Agent

Takeaway: เช็คที่ deterministic ได้ ให้เขียนเป็นโค้ดล้วนๆ ประหยัด cost/latency

TriagePipeline — ใครทำหน้าที่อะไร

TriagePipeline business roles

Takeaway: ยามไม่ต้องฉลาด แค่ต้องไว — วางเช็คที่ตอบได้ทันทีไว้ก่อนงานที่ต้องคิด

Event

Event

Takeaway: Event คือหน่วยเดียวที่ทุก agent (LLM/Workflow/Custom) ใช้คุยกัน — ไม่ว่าจะพิมพ์เองหรือให้ LLM สร้าง

Step 6 — RateCheckAgent

func runRateCheck(ic agent.InvocationContext) iter.Seq2[*session.Event, error] {
    return func(yield func(*session.Event, error) bool) {
        var text string // RateCheck(userText(ic)) เช็ค failed-login/IP/window
        event := session.NewEvent(ic, ic.InvocationID())
        event.LLMResponse = model.LLMResponse{
            Content: genai.NewContentFromText(text, genai.RoleModel),
        }
        yield(event, nil)
    }
}

../workshop/adk-go-v2/step6-detection-rule/

iter.Seq2 อยู่ตรงไหนใน flow

iter.Seq2 flow explanation

Takeaway: หยุดกลางทางได้จริงเสมอ — "เห็นผลทันทีระหว่างทาง" ขึ้นกับ transport ที่ใช้

ทำไม Run ต้องคืน iter.Seq2

ปัญหาที่ต้องแก้

  1. 1 agent ส่งได้หลาย event
  2. lazy/streaming — ห้ามรอครบก่อนส่ง
  3. หยุดกลางทางได้ทันที ไม่ leak goroutine

ทำไมไม่ใช้ slice/channel

iter.Seq2 ทำงานยังไง (compiler desugar)

type Seq2[K, V any] func(yield func(K, V) bool)

// สิ่งที่ compiler แปลงให้จริงๆ — แค่ฟังก์ชันเรียกฟังก์ชัน ไม่มี magic:
runRateCheck(ic)(func(event *session.Event, err error) bool {
    if err != nil { return false } // break → return false แทน
    return true                     // จบ body ปกติ → return true (ทำต่อ)
})

range บน func value ไม่ได้รันขนาน — เป็นแค่ nested function call บน stack เดียวกัน

Workshop 6

💡 deterministic check (เร็ว/ฟรี) ควรอยู่ก่อน LLM check (ช้า/แพง) เสมอ

🎯 แก้ 1 จุด: ย้าย rateCheckAgent ใน SubAgents ไปรันเป็นตัวสุดท้าย (หลัง RecommendAgent) แทนที่จะเป็นตัวที่สอง

Success: ./demo.sh ต้องได้ author เรียงแบบนี้ (ไม่ใช่ตำแหน่งเดิม)

ExtractAgent → ClassifyAgent → RecommendAgent → RateCheckAgent

Block 7

Contain or Escalate — Graph-based Workflow

จาก Fix Pattern สู่ Graph

  • Node = agent
  • Edge = เส้นทางไปต่อ พร้อม condition
  • Routing เขียนเองได้ ไม่ผูก fix pattern (v1.x: Sequential/Parallel/Loop ตายตัว)

Pattern to Graph

Graph — Incident Response Pipeline

Graph Workflow

Takeaway: routing condition ไม่จำเป็นต้องมาจาก LLM เสมอไป

IncidentGraph — ใครทำหน้าที่อะไร

Graph routing business roles

Takeaway: เคสเบาปิดเอง เคสเสี่ยงจริงถึงจะปลุกคน — ป้ายบอกทางตัดสินเอง ไม่ใช้ LLM

Step 7 — classify + route

func classifyAndRoute(
    ctx agent.Context, classificationJSON string, emit func(*session.Event) error,
) (any, error) {
    ev := session.NewEvent(ctx, ctx.InvocationID())
    ev.Routes, ev.Output = []string{parseSeverity(classificationJSON)}, classificationJSON // ① route ② payload
    return nil, emit(ev)
}

① เลือก edge ที่จะไปต่อ ② ส่งข้อมูลต่อให้ node ถัดไปใช้ — สองหน้าที่แยกกัน

../workshop/adk-go-v2/step7-contain-escalate/

classifyAndRoute ทำงานยังไง

classifyAndRoute mechanism

Takeaway: Routes กับ Output คือคนละเรื่องกัน แม้ตั้งค่าบรรทัดเดียวกันในโค้ด

Workshop 7

💡 failure 1 ครั้งแล้วพิมพ์ถูกก็โดน escalate ไป SecOps ทั้งที่ไม่ใช่เหตุจริง

🎯 แก้ 1 จุด: แก้ ClassifyAgent's instruction ให้ผ่อนเกณฑ์ "high" — ต้องมี failed login อย่างน้อย 5 ครั้งก่อน success ถึงจะนับเป็น high (ของเดิม นับแค่มี success ต่อจาก failure ใดๆ ก็พอ)

Success: ./demo.sh แล้ว incident มาตรฐาน (มีแค่ 3 failure) route เปลี่ยนจาก

🚨 Escalated to SecOps on-call.

เป็น

No action needed.

Break

15 นาที

Open Lab

60 นาที — ต่อ node ของตัวเองเข้ากราฟ

เริ่มจาก step7-contain-escalate เพิ่ม node ใหม่ 1 อัน:

ไม่มี deploy — โฟกัสที่กราฟทำงานถูกใน go run . web webui api

ลองเล่นเองได้

github.com/pallat/adkworkshop/adk-go-v2/

ทวนเส้นทางวันนี้: IncidentAnalystAgent → +Tools → +Session → +Callbacks → Sequential → +Custom Agent → Graph Routing → Open Lab

Q&A 🐹