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 үйлчилнэ.
Чатын урсгал¶
- History (≤ 20 ээлж) + шинэ prompt-оос
contentsугсарна. Дуут мессеж inline base64 audio хэсгээр ирдэг — чат model мультимодаль тул STT алхам хэрэггүй. - Давхаргат system instruction + tool зарлалуудтайгаар Gemini-г дуудна.
- Хариу function дуудлага агуулж байвал: tool бүрийг гүйцэтгээд,
functionResponseээлжийг нэмж давтана (дээд тал ньMaxSteps, default 4). Гүйцэтгэсэн дуудлага бүрStep{Tool, Args, Result}болж клиентэд буцна — UI «AI юу хийснийг» харуулна. - Хариу текст бол буцаана.
Алдааны семантик:
| Нөхцөл | Хариу |
|---|---|
| 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_prompts → AI_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-д:
| Tool | Юу |
|---|---|
search_knowledge |
ai_knowledge дээрх семантик хайлт |
get_server_time |
Хамгийн энгийн жишээ (УБ цаг) |
Мэдлэгийн сан (RAG)¶
Корпус нь ai_knowledge-д — код болон баримтаас бичсэн ~58 бүлэг
(migration 48). Мөр бүр тогтвортой slug, source, lang, ба vector(768)
төрлийн embedding-тэй (migration 47, pgvector + HNSW индекс).
Хайлтын логик:
- Асуултыг embed хийнэ (
RETRIEVAL_QUERY). - Cosine зайгаар (
embedding <=> $1) top-8 нэр дэвшигч гаргана. - Шилдэг таарцтай харьцуулж шүүнэ —
relativeScoreMargin(0.03)-оос хол зөрсөнг хаяад 2–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-001 → text-embedding-004 → embedding-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:
temperature1.0,topP0.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).