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 เอง


| หมวด | 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 — ลองไล่ดูเองได้
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

Takeaway: agent เปล่าก็สืบสัญญาณได้ — แต่จำ context ข้ามรอบไม่ได้, เรียก tool จริงไม่ได้
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/
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: ถามเป็นไทย → คำตอบยังเป็นอังกฤษล้วน
LLM ทำได้แค่ "คิด" แล้วบอกว่าอยากเรียก tool อะไร — คนที่ลงมือทำจริงคือโค้ดฝั่งเรา
parse_log_line ใช้ strings/time.Parse ล้วนๆ — deterministic parse ของ
<timestamp> <event> user=<name> ip=<addr> ไม่มี exec.Command เลย
Takeaway: tool description คือสิ่งเดียวที่ LLM เห็น — เขียนให้ชัด


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/
🎯 แก้ 1 จุด: ทำให้ agent เรียก parse_log_line ทุกครั้งก่อนตอบ
(ตอนนี้เรียกไม่สม่ำเสมอ) — หาจุดที่ควบคุมพฤติกรรมนี้ใน main.go เอง
✅ Success (เช็คจาก log ตรงๆ ไม่ต้องเชื่อคำตอบ LLM):
>>> parse_log_line called, input: โผล่ทุกครั้งที่รัน (paste incident เดิม
ซ้ำ 3 รอบ)>>> parsed: event=... user=... ip=... โชว์ค่าที่ parse ถูกต้อง
Takeaway: state ข้าม session ใหม่ได้ — chat history ข้ามไม่ได้เลย
Takeaway: เลือกกลไกตามที่มาของข้อมูล ไม่ใช่ตามความเคยชิน
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/
🎯 แก้ 1 จุด: เพิ่ม field Reporter ใน SetIncidentContextArgs แล้วโชว์ใน
instructionWithContext
✅ Success: รัน ./demo-workshop.sh — เช็คstate ดิบผ่าน GET session
ว่ามี key "reporter" จริงไหม ไม่เชื่อคำตอบ agent (สคริปต์ตอบ PASS/FAIL)
BeforeModelCallbacks รันก่อนทุก model call — return response ที่ไม่ nil
= ข้ามโมเดล call ไปเลย ไม่มี API round-trip
ใช้ regex สแกน log line หา pattern ของ credential/PII ก่อนส่งออกไปนอกเครื่อง
Takeaway: อย่าไว้ใจ LLM ให้กรอง credential — ใช้โค้ดกรองก่อนยิง API เสมอ


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/
🎯 แก้ 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 ผ่าน

Takeaway: deterministic pipeline — ไม่ให้ LLM เลือกลำดับรันเอง
Takeaway: ผู้จัดการไม่ตัดสินใจอะไรเลย แค่บังคับให้ทีมเฉพาะทางรันตามลำดับตายตัว
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/
💡 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

Takeaway: เช็คที่ deterministic ได้ ให้เขียนเป็นโค้ดล้วนๆ ประหยัด cost/latency
Takeaway: ยามไม่ต้องฉลาด แค่ต้องไว — วางเช็คที่ตอบได้ทันทีไว้ก่อนงานที่ต้องคิด

Takeaway: Event คือหน่วยเดียวที่ทุก agent (LLM/Workflow/Custom) ใช้คุยกัน — ไม่ว่าจะพิมพ์เองหรือให้ LLM สร้าง
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/
Takeaway: หยุดกลางทางได้จริงเสมอ — "เห็นผลทันทีระหว่างทาง" ขึ้นกับ transport ที่ใช้
iter.Seq2[]*Event — ต้องรันจบก่อน return ได้ ขัด streamingchan *Event — spawn goroutine ทุกชั้น agent tree เสี่ยง leakiter.Seq2 — pull-based stack เดียวกัน ไม่ต้อง goroutineiter.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 เดียวกัน
💡 deterministic check (เร็ว/ฟรี) ควรอยู่ก่อน LLM check (ช้า/แพง) เสมอ
🎯 แก้ 1 จุด: ย้าย rateCheckAgent ใน SubAgents ไปรันเป็นตัวสุดท้าย
(หลัง RecommendAgent) แทนที่จะเป็นตัวที่สอง
✅ Success: ./demo.sh ต้องได้ author เรียงแบบนี้ (ไม่ใช่ตำแหน่งเดิม)
ExtractAgent → ClassifyAgent → RecommendAgent → RateCheckAgent


Takeaway: routing condition ไม่จำเป็นต้องมาจาก LLM เสมอไป
Takeaway: เคสเบาปิดเอง เคสเสี่ยงจริงถึงจะปลุกคน — ป้ายบอกทางตัดสินเอง ไม่ใช้ LLM
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/
Takeaway: Routes กับ Output คือคนละเรื่องกัน แม้ตั้งค่าบรรทัดเดียวกันในโค้ด
💡 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.
เริ่มจาก step7-contain-escalate เพิ่ม node ใหม่ 1 อัน:
generate_incident_report — เขียน incident summary เป็น markdown
(fan-in จากทั้ง auto_close/escalate)ไม่มี deploy — โฟกัสที่กราฟทำงานถูกใน go run . web webui api
github.com/pallat/adk — workshop/adk-go-v2/
ทวนเส้นทางวันนี้: IncidentAnalystAgent → +Tools → +Session → +Callbacks → Sequential → +Custom Agent → Graph Routing → Open Lab
Q&A 🐹