Агуулгыг алгасах

AI туслах

Gemini дээр суурилсан SDK-гүй REST pipeline: function-calling чат, дуут мессеж, яриа→текст (STT), текст→яриа (TTS), шууд орчуулга, ба pgvector дээрх мэдлэгийн сан.

UI: /me/ai, /me/translate, нүүр хуудасны нээлттэй виджет.

Ерөнхий зураг

Браузер (/me/ai, /me/translate)
   │  ижил-origin fetch (CSRF header)
Next.js BFF  /api/ai/{chat,stt,tts,translate}   ← хэлбэр шалгаж JWT хавсаргана
   │  server→server
Go API  /api/v1/ai/*   (JWT + rate limit ~20/мин)
usecases/ai ──────────► pkg/gemini ──────► Gemini REST API
   │   ▲                 (429/5xx/сүлжээнд 3× retry + backoff)
   │   └─ functionResponse
ToolDef.Execute()  ← хүсэлтийн context-оор СЕРВЕР дээр ажиллана
   ├─ search_knowledge → ai_knowledge (pgvector)
   └─ get_server_time  → жишээ tool

Гол зарчим

Аль tool-ийг дуудахаа model шийднэ, backend гүйцэтгэнэ. Model хэзээ ч код ажиллуулахгүй; tool-ууд хүсэлтийн context-оор ажилладаг тул DB хандалтад RLS болон timeout үйлчилнэ.

Чатын урсгал

  1. History (≤ 20 ээлж) + шинэ prompt-оос contents угсарна. Дуут мессеж inline base64 audio хэсгээр ирдэг — чат model мультимодаль тул STT алхам хэрэггүй.
  2. Давхаргат system instruction + tool зарлалуудтайгаар Gemini-г дуудна.
  3. Хариу function дуудлага агуулж байвал: tool бүрийг гүйцэтгээд, functionResponse ээлжийг нэмж давтана (дээд тал нь MaxSteps, default 4). Гүйцэтгэсэн дуудлага бүр Step{Tool, Args, Result} болж клиентэд буцна — UI «AI юу хийснийг» харуулна.
  4. Хариу текст бол буцаана.

Алдааны семантик:

Нөхцөл Хариу
Gemini түр зуур унасан (3× retry-ийн дараа ч) Хэрэглэгчийн хэл дээрх fallback мессеж + degraded: true
GEMINI_API_KEY байхгүй Жинхэнэ 500 (шалтгаан логдоно)
Танигдаагүй / унасан tool Model руу {"error": …} — model эелдгээр тайлбарлана

Degraded-ыг 5xx болгож болохгүй

Түр зуурын Gemini алдаа нь хэрэглэгчид зөөлөн уналт байх ёстой. Үүнийг 500 болговол чат бүхэлдээ эвдэрсэн мэт харагдана.

Prompt-ийн гурван давхарга

Давхарга Эх сурвалж Засварлагдах уу Зориулалт
1. Суурь дүрэм Кодод хатуу const хэзээ ч үгүй Хариултын хэл, хүрээний сахилт, prompt-injection эсэргүүцэл
2. Хамрах хүрээ ai_promptsAI_SCOPE_PROMPT → built-in админ, ажиллаж байхад Туслах юугаар туслах вэ
3. Нэмэлт заавар ai_prompts (сонголттой) админ, ажиллаж байхад Өнгө аяс, нэмэлт дүрэм

Guardrail давхаргыг тохируулгатай болгож БОЛОХГҮЙ

1-р давхарга нь prompt-injection эсэргүүцэл, хүрээний сахилтыг агуулна. Түүнийг DB-ээс тохируулдаг болговол админ бүр (эсвэл админы данс алдагдвал халдагч) хамгаалалтыг унтраах боломжтой болно.

Хариултын хэл: хүсэлтийн lang талбар (frontend UI-ийн хэлээ илгээнэ: mn/en/zh/ru; танихгүй ⇒ mn) нь эцсийн шийдэгч. Хэрэглэгч өөр хэлээр бичсэн ч, мэдлэгийн сан өөр хэлтэй ч хариулт UI-ийн хэлээр ирнэ — өөр хэлтэй эх сурвалжийг орчуулж өгнө. Заавар нь prompt-ын эхэнд ба төгсгөлд давхар орно (primacy + recency).

Кэш: prompt 60 секунд кэшлэгдэнэ; SetPrompt кэшийг хүчингүй болгоно. SetPrompt нь seed хийгдсэн key-үүд (scope, instructions) дээр зөвхөн UPDATE хийдэг — API-аар prompt-ийн гадаргуу өргөжихгүй.

Админ UI: Админ → Тохиргоо. API: GET/PUT /v1/admin/ai/prompts/{key} (settings.manage).

Tools

ai.ToolDef{
    Declaration: gemini.FunctionDeclaration{
        Name:        "my_tool",
        Description: "Model хэзээ дуудахыг эндээс ойлгоно…",
        Parameters:  map[string]any{ /* JSON Schema */ },
    },
    Execute: func(ctx context.Context, args map[string]any) (map[string]any, error) {
        // backend дээр, хүсэлтийн identity-тэй ctx-ээр (RLS үйлчилнэ)
        return map[string]any{"result": "…"}, nil
    },
}

Бүртгэх нь server.go-д:

aiTools := append(ai.DefaultTools(), ai.KnowledgeSearchTool(aiRepo), myTool)
Tool Юу
search_knowledge ai_knowledge дээрх семантик хайлт
get_server_time Хамгийн энгийн жишээ (УБ цаг)

Мэдлэгийн сан (RAG)

Корпус нь ai_knowledge-д — код болон баримтаас бичсэн ~58 бүлэг (migration 48). Мөр бүр тогтвортой slug, source, lang, ба vector(768) төрлийн embedding-тэй (migration 47, pgvector + HNSW индекс).

Хайлтын логик:

  1. Асуултыг embed хийнэ (RETRIEVAL_QUERY).
  2. Cosine зайгаар (embedding <=> $1) top-8 нэр дэвшигч гаргана.
  3. Шилдэг таарцтай харьцуулж шүүнэ — relativeScoreMargin (0.03)-оос хол зөрсөнг хаяад 2–4 бичлэг үлдээнэ.
  4. Embedder тохируулаагүй / embedding унасан / юу ч үлдээгүй бол ILIKE түлхүүр үгийн хайлт руу уналт хийнэ.

Tool-ийн хариунд аль горим ажилласныг ("mode": "vector" | "keyword") заана. Лог руу горим, олдсон тоо, шилдэг оноо, slug бичигдэнэ — хэрэглэгчийн асуултын текст хэзээ ч биш.

Яагаад харьцангуй босго вэ?

Энэ корпус дээр хэмжихэд хамааралгүй хоёр бүлэг хүртэл 0.64+ cosine ижилсэлтэй байсан тул тогтмол босго (хуучин 0.55) юуг ч шүүхгүй байв. minVectorScore (0.35) нь одоо зөвхөн хог хаях шал.

Backfill: ачаалалтын дараа API нь embedding NULL, эсвэл content_hash таарахгүй мөрүүдийг 20-оор багцалж embed хийнэ. Арын дэвсгэрт ажиллана — boot хүлээхгүй.

Корпус засах: migration дотор мөрийг нэм / өөрчил (slug-ийг хэвээр үлдээ), дараа нь дахин ачаал эсвэл POST /v1/admin/ai/knowledge/reindex (settings.manage) дуудна.

Model: GEMINI_EMBED_MODEL хоосон үед client өөрөө сонгоно — gemini-embedding-001text-embedding-004embedding-001. Хүсэлт бүрд outputDimensionality: 768 захиалдаг тул вектор нь баганад үргэлж таарна.

Нээлттэй (нэвтрэлтгүй) чат

Нүүр хуудсанд бүртгэлгүй ажиллах хөвөгч виджет: POST /v1/public/ai/chat. Ижил pipeline ч тусдаа usecase instance — зөвхөн мэдлэгийн хайлтын tool-той.

Энэ тусгаарлалт бол аюулгүй байдлын хил

Хэрэглэгчийн өгөгдөл уншдаг tool-ыг нэвтэрсэн туслахад нэмсэн ч нэргүй зочинд хүрэхгүй.

Гурван нэмэлт хязгаар:

Хязгаар Утга
Rate limit IP тус бүрт ~6/мин, burst 3
Payload Мессеж ≤ 1000 тэмдэгт, түүх ≤ 6 ээлж
Prompt Зочинтой ярьж байгааг заасан нэмэлт hardcoded давхарга

Push-to-talk + stream: том дугуй товчийг дарж барих хугацаанд бичээд тавихад ~250 KB base64 (≈15 сек) бичлэгийг POST /v1/public/ai/chat/stream руу явуулж, Server-Sent Events-ээр хариу авна. Чат model мультимодаль тул нэг дуудалтаар хуулбар ба хариулт хоёуланг өгнө: эхний мөр нь хуулбар (сервер transcript event болгоно), үлдсэн нь delta event-үүд.

Дуут хариу: клиент урсгалт текстийг өгүүлбэрийн зааг дээр таслаад, дууссан өгүүлбэр бүрийг POST /v1/public/ai/tts руу явуулж ээлжлэн тоглуулна — бүтэн хариулт биш, эхний өгүүлбэр бэлэн болмогц дуугарч эхэлнэ.

Дуу хоолой

Чадвар Endpoint Хэрхэн
Дуут чат мессеж POST /v1/ai/chat + audio Audio нь user ээлжид inline орно
Яриа→текст POST /v1/ai/stt «яг сонссоноо буцаа» гэсэн чанд заавартай нэг дуудлага
Текст→яриа POST /v1/ai/tts Тусдаа TTS model; PCM (L16/24 kHz)-ийг WAV толгойгоор ороож буцаана
Шууд орчуулга POST /v1/ai/translate Текст → орчуулга; audio → хоёр алхамт STT→орчуулга

TTS-ийн 503

Model хааяа 200 буцаагаад дотор нь аудиогүй байдаг (ижил текст дараагийн дуудалтад бүтэн ирдэг). Тиймээс Speak нь 3 хүртэл удаа дахин оролдоод сая 503 буцаана — 500 биш, учир нь энэ бол түр зуурын саатал.

Live орчуулгын UX: микрофон ~7 секундын сегментээр бичнэ — сегмент бүрд шинэ MediaRecorder (timeslice chunk-ууд зөвхөн эхнийдээ container header-тэй байдаг). Чимээгүй сегмент хоосон талбар буцаана — алдаа биш.

Audio хязгаар: mime whitelist (webm/ogg/wav/mpeg/mp3/mp4/m4a/aac/flac) + ~700 KB base64 (~30 сек opus) — BFF (lib/aiBff.ts) болон backend DTO хоёуланд.

Хариултын олон янз байдал

Ижил асуултад үсэг үсгээрээ ижил хариулт өгөхгүй:

  • System prompt-д [НАЙРУУЛГА] хэсэг — давтагдлын эсрэг дүрэм + хүсэлт бүрд санамсаргүй сонгосон хэв маяг.
  • Sampling: temperature 1.0, topP 0.95.

Зөвхөн найруулга хэлбэлзэнэ — баримт, тоо, алхам, эх сурвалж өөрчлөгдөхгүй.

Тохиргоо

GEMINI_API_KEY=                                # заавал; хоосон бол /ai/* 500
GEMINI_MODEL=gemini-2.5-flash                  # чат / STT / орчуулга
GEMINI_TTS_MODEL=gemini-2.5-flash-preview-tts  # TTS
GEMINI_EMBED_MODEL=                            # хоосон = автоматаар сонгоно
GEMINI_VOICE=Kore                              # prebuilt дуу хоолой
GEMINI_API_BASE=                               # proxy / тестэд
AI_SCOPE_PROMPT=                               # DB давхарга хоосон үеийн fallback

Rate limit: /v1/ai/* ~20 хүсэлт/мин (burst 5) — live орчуулгын ~8 chunk/мин урсгал багтана. Timeout нь энэ замд 50 секунд (бусад дээр 30).

Тест

Бүгд Gemini-гүйгээр ажиллана:

  • gemini.Generator нь interface — usecase тестүүд fakeGenerator ашиглана.
  • repointerface.AIRepository-г prompt / tool тестэд fake-ээр солино.
  • HTTP client өөрөө httptest серверийн эсрэг тестлэгдэнэ (retry/backoff, 4xx no-retry, function-call parsing).