Skip to content

AI / Language Model

v2 มองภาษาโมเดลเป็น capability หนึ่งของ App เหมือน IStorage หรือ IMailer — ไม่ใช่ framework ใหม่ที่ซ้อนเข้ามา

ชั้นใช้เมื่อหน้า
core.LLM(ctx)อยากได้ ข้อความ — ตอบแชท สรุป เขียนใหม่Text Generation
llm.New[T](ctx)อยากได้ ค่า Go — สกัดข้อมูล จัดหมวด ให้คะแนนTyped Values
.With(core.LLMImage(...))อยากให้ model ดูรูปหรืออ่านเอกสารAttachments
core.LLMToolอยาก ให้ model เรียกฟังก์ชันของเราTool Use
core.Embedder(ctx)อยากได้ เวกเตอร์ — semantic search, RAGEmbeddings

เหตุผลการออกแบบทั้งหมดอยู่ใน design/v2/17-ai.md


core ให้อะไร และไม่ให้อะไร

ให้: ท่อไปหา model + สิ่งที่ทุกทีมต้องทำซ้ำแล้วมักทำไม่เหมือนกัน

เรื่องทำไมต้องอยู่ใน core
นับ token / ค่าใช้จ่ายถ้าไม่มี คำถาม "เดือนนี้จ่ายค่า AI ไปเท่าไหร่ แยกตาม service และ model" จะไม่มีคำตอบ — และกว่าจะมีคนถาม call ก็หายไปแล้ว
แปลง errorถ้าไม่แปลง ทุก failure จะเป็น 500 เหมือนกันหมด rate limit แยกจาก API key ผิดไม่ได้ คนเลย retry อันที่ไม่ควร และเลิก retry อันที่ควร
ปกปิด promptprompt คือสิ่งที่ผู้ใช้พิมพ์ — ห้าม log เนื้อเป็นค่าเริ่มต้น
นโยบาย timeoutgeneration กินเวลาเป็นนาที ถ้าใช้ timeout ทรง HTTP จะตัดคำตอบที่กำลังจะมาถึง (และโดนคิดเงินอยู่ดี)
gate ก่อนรัน tooltool loop คือโค้ดที่ model ตัดสินใจเรียกฟังก์ชันของเราเอง — ต้องมีจุดเดียวที่อนุมัติ บันทึก และเทสต์ได้
memory implementationทุก capability ใน v2 มี ตัวนี้ก็ต้องมี

ไม่ให้: conversation store, RAG pipeline สำเร็จรูป, MCP client, สร้างรูปภาพ — ปล่อยให้ service ประกอบเอง (ดู Providers & Tuning)


ตั้งค่า

sh
APP_AI_PROVIDER=anthropic
APP_AI_MODEL=claude-opus-5
APP_AI_API_KEY=sk-ant-...
keyค่าเริ่มต้นคำอธิบาย
AI_PROVIDERanthropic · openai · google · ollama · compat
AI_MODELmodel id ของ provider นั้น
AI_API_KEYkey
AI_BASE_URLendpoint อื่น (gateway/proxy/local) — จำเป็น เมื่อใช้ compat
AI_MAX_TOKENS4096เพดานคำตอบเมื่อ request ไม่ได้ระบุ
AI_TIMEOUT120วินาที — generation กินเวลาเป็นนาทีได้ อย่าตั้งทรง HTTP
AI_MAX_RETRIES2retry เมื่อเจอ 429 / 5xx / network
AI_EMBED_MODELembedding model — ดู Embeddings
AI_EMBED_PROVIDERตาม AI_PROVIDERเจ้าที่ใช้ embed ถ้าต่างจากเจ้าที่ใช้ generate
AI_EMBED_API_KEYตาม AI_API_KEY
AI_EMBED_BASE_URLตาม AI_BASE_URL
AI_EMBED_DIMENSIONSความยาวเวกเตอร์ที่ขอทุกครั้ง — ต้องตรงกับคอลัมน์ที่ประกาศไว้
AI_LOG_LEVELตาม LOG_LEVELsilent · error · warn · info/debug
AI_LOG_PROMPTfalseเขียนขาเข้า (prompt, argument ของ tool) ลง log — เครื่อง dev เท่านั้น
AI_LOG_COMPLETIONfalseเขียนขาออก (คำตอบ, ผลลัพธ์ tool, source) — เครื่อง dev เท่านั้น
AI_LOG_SLOW30วินาที — เกินแล้วขึ้น warn

**ตั้งค่าแยกรายเจ้า — คีย์เอามาจากไหน, model id ตัวไหน, อะไรใช้ไม่ได้บ้าง: Provider Setup

ไม่ตั้ง AI_PROVIDER หรือ AI_MODEL → service ยัง boot ได้ปกติ** แต่ทุกการเรียกจะ fail ด้วย LLM_DISABLED (503)

ตั้งใจให้ดัง — cache ที่ miss แล้วคำนวณใหม่ได้จึง degrade เงียบได้ แต่ คำตอบว่างเปล่าที่ caller เชื่อว่าจริง กู้คืนไม่ได้ ที่นี่จึงทำตัวเหมือน storage/mq/mailer ไม่ใช่เหมือน cache


Wiring

go
env, _ := core.NewEnv()

model, err := goai.New(env)          // gitlab.finema.co/finema/idin-core/v2/llm/goai
if err != nil {
    log.Fatal(err)                    // AI_PROVIDER สะกดผิด = error ตั้งแต่ boot ไม่ใช่ตอน request แรก
}

embedder, err := goai.NewEmbedder(env)   // ถ้าใช้ embeddings
if err != nil {
    log.Fatal(err)
}

app, _ := core.NewApp(env,
    core.WithLLM(model),
    core.WithEmbedder(embedder),
)

boot log บอกว่า process นี้ผูกกับ model ไหน — เพราะ llm=true ไม่ใช่คำถามที่ใครมี แต่ "จะโดนคิดเงินค่าอะไร" คือคำถามจริง:

INFO app ready  env=prod service=api llm=anthropic/claude-opus-5 embedder=google/gemini-embedding-001 ...

เช็คว่า model ทำอะไรได้บ้าง

go
caps := core.LLM(ctx).Capabilities()

caps.Streaming         // stream ได้ไหม
caps.StructuredOutput  // บังคับ JSON schema ได้ไหม
caps.Tools             // เรียก tool ได้ไหม
caps.Reasoning         // ตั้งระดับการคิดได้ไหม
caps.PromptCaching     // cache prompt ได้ไหม
caps.TokenCounting     // นับ token ล่วงหน้าได้ไหม
caps.Vision            // ส่งรูป/เอกสารแนบไปได้ไหม

ใช้เมื่ออยากแตกทางก่อนส่ง — เพราะฟีเจอร์ที่ provider ทำไม่ได้จะ คืน error ไม่ใช่เงียบ ๆ ข้าม (ดู Providers & Tuning)


Error

codestatusความหมายretry ช่วยไหม
LLM_DISABLED503ไม่ได้ตั้ง AI_PROVIDER/AI_MODELไม่
LLM_INVALID_CONFIG400provider ไม่รู้จัก / compat ไม่มี base URL — error ตั้งแต่ bootไม่
LLM_INVALID_REQUEST400request ผิดรูป — ไม่ส่งไป providerไม่
LLM_INVALID_SCHEMA400type อธิบายเป็น schema ไม่ได้ (recursive / ชนิดไม่รองรับ)ไม่
LLM_INVALID_TOOL400tool ไม่มีชื่อ/คำอธิบาย หรือ input type อธิบายไม่ได้ไม่
LLM_ATTACHMENT_TOO_LARGE413attachment รวมกันเกิน LLMMaxAttachmentBytesไม่
LLM_UNSUPPORTED400model ทำสิ่งที่ขอไม่ได้ไม่
LLM_UNAUTHORIZED401/403key ผิดหรือหมดสิทธิ์ไม่
LLM_MODEL_NOT_FOUND404model id ผิดไม่
LLM_RATE_LIMITED429โดน rate limitใช่
LLM_PROVIDER_ERROR5xxprovider ล่มใช่
LLM_REQUEST_REJECTED4xxprovider ปฏิเสธไม่
LLM_TOOL_NOT_LOCAL500driver ส่ง tool ที่ provider รันเองกลับเข้าลูปในเครื่องไม่
EMBED_*เหมือนกันฝั่ง embedding ใช้รหัสชุดเดียวกันแต่ขึ้นต้น EMBED_ (EMBED_UNSUPPORTED = ตัวเลือกที่ provider นั้นไม่มี)

status ไม่ได้มีไว้สวย ๆ — มันคือคำตอบว่า retry แล้วมีโอกาสสำเร็จไหม

go
resp, err := core.LLM(ctx).Generate(req)
switch {
case err == nil:
case err.GetStatus() == 429, err.GetStatus() >= 500:
    return s.enqueueRetry(ctx, req)          // ลองใหม่ได้
default:
    return ctx.NewError(err, errmsgs.InternalServerError)
}

sentinel สำหรับ errors.Is:

go
core.ErrLLMDisabled       // ไม่ได้ตั้งค่า
core.ErrLLMUnsupported    // model ทำไม่ได้ — ไม่ใช่ provider ล่ม
core.ErrEmbedderDisabled
core.ErrEmbedUnsupported  // embedder ไม่รองรับตัวเลือกที่ขอ (Dimensions / Task)

Log

หนึ่งบรรทัดต่อหนึ่ง generation — ระดับตามผลลัพธ์ ไม่ใช่ debug ตายตัว

DEBUG generate google/gemini-2.5-flash stop 3.341s
      provider=google model=gemini-2.5-flash operation=generate took_ms=3340
      prompt_chars=89 input_tokens=373 output_tokens=53 cached_input_tokens=0
      finish_reason=stop steps=2 tool_calls=1

หัวข้อของบรรทัดบอกเรื่องได้ด้วยตัวเอง (generate google/gemini-2.5-flash stop 3.3s) เพราะ JSON pipeline กับ Sentry Logs แสดงแค่ msg จนกว่าจะกดเปิด — "llm call" ไม่ตอบคำถามที่คนกำลังกวาดสายตาหาสักข้อ

สถานการณ์ระดับ
provider ล่ม (5xx)ERROR + error.code
ถูกปฏิเสธ (4xx)WARN + error.code
ช้าเกิน AI_LOG_SLOWWARN + threshold_ms
สำเร็จDEBUG

จุดสำคัญ: generation ที่ล้มต้องไม่จมอยู่ที่ debug — บน production ที่ LOG_LEVEL=info แปลว่าไม่มีอะไรออกมาเลย

ระดับแยกจากทั้งระบบ

sh
APP_LOG_LEVEL=info
APP_AI_LOG_LEVEL=debug     # เห็นทุก generation โดยไม่ต้องเปิด debug ทั้งระบบ

เข้าชุดกับ DB_LOG_LEVEL / HTTP_LOG_LEVEL / DB_MONGO_LOG_LEVEL

Tool call มี audit trail เสมอ

DEBUG llm tool ran get_order_status     tool=get_order_status outcome=ran ...
WARN  llm tool denied refund_order      tool=refund_order outcome=denied ...

outcome เป็น ran · denied · failed · unknown_tool

denied / failed ขึ้นที่ WARN จึงเห็นได้โดยไม่ต้องเปิด debug — การปฏิเสธคือ security control ที่กำลังทำงาน และ tool ที่พังคือของที่ model จะลองใหม่

เขียนให้จาก core ไม่ใช่ปล่อยให้ service ทำเอง เพราะ service ที่ไม่ได้ใส่ Approve คือ service ที่ audit trail สำคัญที่สุด

resp.ToolCalls เก็บครบทั้ง Input (model ขออะไร) Outcome (เกิดอะไรขึ้น) และ Output (model ได้อะไรกลับไป) — สามอย่างนี้ตอบคนละคำถาม และ trail ที่มีแค่สองอย่างแรกอธิบายคำตอบสุดท้ายไม่ได้

tool ที่ provider รันเอง ไม่มีบรรทัดนี้

goai.GoogleSearch() / URLContext() / CodeExecution() ไม่ผ่าน RunTool จึงไม่มี llm tool ... และ resp.ToolCalls ว่าง สิ่งที่บอกได้คือ sources=N ในบรรทัด generation ซึ่งเขียนให้เสมอ — generation ที่ไป ground มาจะแยกจาก generation ธรรมดาได้ตรงนี้ (แยกไม่ได้อย่างเดียวคือ "เสนอ tool ไปแล้ว model ไม่ใช้" กับ "ไม่ได้เสนอ" — provider ไม่ได้บอก)

เนื้อหาไม่ถูกเขียน เว้นแต่จะสั่ง — และแยกขาเข้า/ขาออก

sh
APP_AI_LOG_PROMPT=true       # ขาเข้า: system, messages, argument ของ tool
APP_AI_LOG_COMPLETION=true   # ขาออก: คำตอบของ model, ผลลัพธ์ของ tool, URL ของ source
DEBUG generate ...  system="you are a helpful assistant"  msg.0.user="what is 2+2"
DEBUG generate ...  completion="4"  sources=1  source.0="https://go.dev/..."
DEBUG llm tool ran get_order  input={"id":"A1"}  output={"status":"shipped"}

เป็นคนละคีย์กันโดยตั้งใจ เพราะเปิดเผยคนละอย่าง — prompt คือสิ่งที่ผู้ใช้พิมพ์ ส่วน completion คือสิ่งที่ model พูดและสิ่งที่ tool อ่านออกมาจากฐานข้อมูล เพื่อบอก model การเปิด prompt log ไว้จึงต้องไม่ทำให้ผลลัพธ์ query เริ่มไหลลง log ตามไปด้วย

ปิดทั้งคู่เป็นค่าเริ่มต้น ด้วยเหตุผลเดียวกับ HTTP_LOG_BODY — log store คือที่ที่ข้อมูลจะไปโผล่ในที่ที่ไม่มีใครตั้งใจได้ง่ายที่สุด

ต่อให้เปิด: ตัดที่ 2,000 ตัวอักษรต่อฟิลด์ และ ไม่เขียน bytes ของ attachment เลย (ขึ้นเป็น [+1 attachment(s)] แทน) เพราะ base64 จะกลบข้อความที่เปิดมาเพื่อจะอ่าน

ยกเว้นเดียวที่เขียนเต็มคือ URL ของ source — มันคือสิ่งที่คำตอบอ้างว่าอ้างอิงมา และคำตอบที่ไล่กลับไปหาที่มาไม่ได้คือสิ่งที่ grounding มีไว้ป้องกันตั้งแต่แรก

ปิดทั้งหมด

sh
APP_AI_LOG_LEVEL=silent

สำหรับ service ที่บันทึกการเรียก model ไว้ที่อื่นอยู่แล้ว


Metric

ทุก generation ถูกบันทึกอัตโนมัติ แยกตาม provider / model / operation:

metricหน่วย
llm.latencyms — วัดถึง token สุดท้าย ทั้ง Generate และ Stream จึงเทียบกันได้
llm.tokens.inputtoken
llm.tokens.outputtoken
llm.tokens.cached_inputtoken — คิดเงินคนละเรตกับ input ปกติ จึงแยกไว้
llm.tokens.reasoningtoken
llm.errorsนับ พร้อม attribute code

stream ที่ถูกทิ้งกลางคันก็ยังถูกบันทึก — token ที่ใช้ไปแล้วก็คือใช้ไปแล้ว

การนับต้นทุนควรมาจาก metric ไม่ใช่การ grep log


อ่านต่อ

  • Provider Setup — Claude · OpenAI · Gemini · Ollama · Vertex/Bedrock
  • Text GenerationGenerate, บทสนทนาหลายเทิร์น, FinishReason
  • Streaming — iterator, SSE, การปิดกลางคัน
  • Typed Valuesllm.New[T], schema จาก struct, validation
  • Attachments — รูปภาพ, PDF, ต้นทุนของรูป
  • Tool Usellm.Tool[In], ลูป, Approve
  • Agent Loops — ลูปยาว, ต้นทุน, รันเป็น job
  • Embeddings — เวกเตอร์, similarity, RAG
  • Best Practices — เลือกชั้นให้ถูก, คุมต้นทุน, prompt, tool, checklist
  • Providers & Tuning — reasoning, prompt caching, หลาย model, escape hatch, MCP
  • Testing — memory model, conformance suite

Maintained by Passakon Puttasuwan & Dev Core Team.