Технический дизайн AI-чата для обращений на сайте Astro
Содержание
- Общая структура
- Держать контракт endpoint небольшим
- Управлять валидацией и вызовом модели на сервере
- Сделать контекст сайта явным
- Писать правила в prompt
- Разделить каналы обращения
- Сохранять URL по locale
- Origin check и rate limit
- Рендерить Markdown-ссылки через allowlist
- Проверять local, preview и production
- Операционные метрики
- Отдельный скоуп
- Итог
Добавить AI-чат на сайт несложно. Сложнее сделать это пригодным для эксплуатации: определить, что AI может отвечать, куда направлять посетителя, какие URL показывать и как контролировать стоимость API.
На сайте Acecore AI-чат для обращений был добавлен в статическую архитектуру Astro + Cloudflare Pages. Основная реализация находится в PR с AI для контактов и CMS-ограниченным потоком переводов. Затем безопасный рендеринг Markdown-ссылок был доработан в отдельном PR. Подробности вынесены в статью Безопасный рендеринг Markdown-ссылок в ответах AI-чата.
Эта статья описывает не только конкретную работу, а повторно используемый технический дизайн для других статических сайтов. Даже вне Astro принцип тот же: разделить клиентский widget, API-границу, prompt и renderer.
Общая структура
| Слой | Ответственность |
|---|---|
| Chat widget | UI, ввод, текущий locale, минимальная история, Markdown rendering |
/api/ai-contact | Валидация, Origin check, rate limit, prompt, вызов OpenAI |
| OpenAI Responses API | Генерация ответа из публичного контекста и состояния диалога |
Браузер не должен вызывать OpenAI напрямую. Серверный endpoint скрывает ключ, позволяет менять prompt и контекст на сервере, а также централизует лимиты и ошибки.
В Astro + Cloudflare Pages это можно реализовать как Pages Function /api/ai-contact. В Next.js это был бы Route Handler, в Hono или Express обычный API route.
Держать контракт endpoint небольшим
type ContactAiRequest = {
message: string
locale: 'ja' | 'en' | 'zh-cn' | 'es' | 'pt' | 'fr' | 'ko' | 'de' | 'ru'
history?: Array<{
role: 'user' | 'assistant'
content: string
}>
}
type ContactAiResponse = {
answer: string
}
Имя, email, телефон, компания и подробные поля формы не должны проходить через чат. Его задача — помочь выбрать услугу и канал обращения, а не собирать персональные данные.
История тоже ограничивается несколькими последними сообщениями и максимальной длиной. Это уменьшает prompt и стоимость.
Управлять валидацией и вызовом модели на сервере
export async function onRequestPost({ request, env }: PagesFunction<Env>) {
assertSameOrigin(request)
assertRateLimit(request)
const body = await request.json()
const message = validateMessage(body.message)
const locale = validateLocale(body.locale)
const history = trimHistory(body.history)
const prompt = buildContactPrompt({
locale,
message,
history,
siteContext: buildPublicSiteContext(locale),
})
const answer = await callOpenAIResponsesApi({
apiKey: env.OPENAI_API_KEY,
model: env.OPENAI_MODEL,
prompt,
})
return Response.json({ answer })
}
Главное — уменьшить и проверить ввод до вызова AI API. Длинные сообщения, бесконечная история и внешние повторные запросы быстро делают эксплуатацию нестабильной.
OPENAI_MODEL лучше хранить в переменной окружения, а OPENAI_API_KEY только на сервере. Про доставку и CSP см. статью о безопасности Cloudflare Pages.
Сделать контекст сайта явным
Для сайта такого размера не обязательно начинать с векторной базы. Часто достаточно структурированного контекста из публичных страниц.
Контекст может включать описание компании и услуг, целевые аудитории, примеры обращений, URL, FAQ, правила для формы/LINE/email/телефона, области без утверждений вроде цены и договоров, а также внутренние URL по locale.
function buildPublicSiteContext(locale: Locale) {
return {
services: [
{
name: 'Web production',
summary: 'Corporate sites, recruiting sites, and landing pages',
url: localizePath('/services/web-production/', locale),
},
{
name: 'Business systems',
summary: 'Reservation, inventory, and customer management systems',
url: localizePath('/services/business-system/', locale),
},
],
contact: {
form: localizePath('/contact/', locale),
line: 'https://lin.ee/...',
emailPolicy:
'Show email only when the form cannot be used or follow-up is needed',
phonePolicy: 'Show phone only for urgent confirmation',
},
}
}
Цель не в том, чтобы модель отвечала из общих знаний, а в том, чтобы она знала, что этот сайт имеет право сказать. При росте сайта слой можно развить в Pagefind, CMS JSON, D1, Vectorize или другой retrieval.
Писать правила в prompt
You are the contact guidance AI for this website.
Answer only from public site information.
Rules:
- Do not make firm statements about pricing, contracts, schedules, or guarantees
- Send formal consultations and estimates to the contact form
- Also suggest LINE for short questions and school-related inquiries
- Show email and phone only when the user asks for direct contact
- Use URLs that match the current locale
- If unsure, do not guess; guide the user to the form
Типичная ошибка — слишком полезная AI, которая обещает слишком много. Вопросы о цене, сроках и гарантиях должны получать общую ориентацию и переход к форме.
Разделить каналы обращения
| Канал | Роль |
|---|---|
| FAQ | Ответить на частые вопросы прямо на странице |
| AI-чат | Помочь выбрать услуги, каналы и связанные страницы |
| LINE | Короткие вопросы, темы школы и легкие уточнения |
| Форма | Оценки, производство, партнерства и найм |
| Прямой контакт | Дополнение после формы или срочное подтверждение |
AI соединяет общий контент вроде обзора услуг с конкретными маршрутами на странице контактов. Это подходит для B2B, агентств, школ и SaaS support.
Сохранять URL по locale
На многоязычном сайте важен не только язык ответа, но и URL.
function localizePath(path: string, locale: Locale) {
if (locale === 'ja') return path
return `/${locale}${path}`
}
Генерация на сервере надежнее, чем только инструкция в prompt. Основа переводов описана в статье Как вести многоязычный блог с Sveltia CMS.
Origin check и rate limit
function assertSameOrigin(request: Request) {
const origin = request.headers.get('Origin')
if (!origin) return
const requestUrl = new URL(request.url)
const originUrl = new URL(origin)
if (originUrl.host !== requestUrl.host) {
throw new Response('Forbidden', { status: 403 })
}
}
IP-based rate limit — первая защита. В Cloudflare можно использовать CF-Connecting-IP, X-Forwarded-For или CF-Ray. При большем трафике лучше Cloudflare WAF, Turnstile, KV, D1 или Durable Objects. CMS-эксплуатация для обновления контента описана в руководстве по внедрению Sveltia CMS; защита форм и комментариев от ботов относится к отдельному уровню.
Рендерить Markdown-ссылки через allowlist
Поддерживайте только абзацы, списки, жирный текст, inline code и Markdown-ссылки. Цели ссылок ограничиваются внутренними путями, текущим origin, https://acecore.net, официальным LINE и нужными mailto: или tel:.
function sanitizeHref(rawHref: string, currentOrigin: string) {
const href = rawHref.trim()
if (href.startsWith('/')) return href
if (href.startsWith(`${currentOrigin}/`)) return href
if (href.startsWith('https://acecore.net/')) return href
if (href.startsWith('https://lin.ee/')) return href
if (href === 'mailto:[email protected]') return href
if (href === 'tel:05088902788') return href
return null
}
trim() важен, потому что AI может вернуть [Services]( /services/ ). Небольшой строгий renderer проще поддерживать, чем полный Markdown.
Проверять local, preview и production
Astro dev или preview не полностью совпадает с Cloudflare Pages Functions. Без OPENAI_API_KEY локально проверяются fallback и ошибки UI.
В preview или production проверьте POST на /api/ai-contact, переменные OPENAI_API_KEY и OPENAI_MODEL, отказ для другого Origin, лимиты ввода, ответы в нужном locale, локализованные URL, отсутствие утверждений о смете или договоре, отсутствие email и телефона по умолчанию, а также Markdown-ссылки только при разрешенном URL.
Отдельно тестируйте длинный ввод, неожиданные вопросы, английские страницы, запрос прямого контакта и вопросы о цене.
Операционные метрики
После релиза смотрите error rate API, срабатывания rate limit, среднее число сообщений на обращение, переходы к форме и LINE, случаи перевода на форму и использование по locale.
Если сохранять тексты диалогов, сначала определите правила приватности. Безопаснее начать с событий и ошибок без текста сообщений.
Отдельный скоуп
Эта статья только о техническом дизайне AI-чата. Навигация, передающая предмет консультации со страницы услуги в форму, также реализована и описана в Технический дизайн передачи контекста от CTA услуги в форму обратной связи.
- AI-чат: через диалог убрать неопределенность и безопасно направить
- Service CTA: передать в форму контекст услуги, которую читал посетитель
Разделение делает статьи понятнее и облегчает внутренние ссылки.
Итог
Для AI-чата на статическом сайте сначала проектируйте API-границу и контроль ответов.
Ключевые решения: вызывать OpenAI из Cloudflare Pages Function, держать ввод и историю маленькими, собирать контекст и locale URL на сервере, писать ограничения в prompt, разделить форму/LINE/прямой контакт, добавить Origin check и rate limit, а Markdown-ссылки рендерить после trim() через allowlist.
Статические сайты могут иметь полезный AI-чат для обращений. Цель не в том, чтобы выделить AI, а в том, чтобы посетитель безопасно выбрал следующий шаг.
Референсная архитектура
Widget
UI чата в Astro отправляет только вопрос, текущий locale и минимальную историю.
Function
Cloudflare Pages Function проверяет ввод, Origin, rate limit и собирает prompt.
Model
OpenAI Responses API получает публичный контекст сайта и состояние диалога.
Renderer
Клиент рендерит только разрешенный Markdown и ведет к внутренним ссылкам или одобренным контактам.
Если все смешано
- AI API вызывается прямо из браузера
- Контекст сайта, API key, UI и рендеринг ссылок смешаны
- AI может слишком уверенно утверждать цены, договоры или сроки
- Markdown и URL могут попасть в HTML без контроля
Если ответственности разделены
- API key и вызов модели остаются на сервере
- Публичная информация сайта управляется как явный контекст
- Prompt контролирует область ответа и маршруты контакта
- Markdown и URL проходят через allowlist
- Выполнено: Определить чат как навигационную помощь, а не замену формы
- Выполнено: Создать серверную границу API и не раскрывать API key в браузере
- Выполнено: Ограничить ответы публичной информацией сайта
- Выполнено: Определить, что AI не должен утверждать, например цены, договоры, сроки и гарантии
- Выполнено: Разделить роли формы, LINE, email и телефона
- Выполнено: Генерировать URL по locale, чтобы не ломать многоязычную навигацию
- Выполнено: Добавить Origin check, лимиты длины, лимиты истории и rate limiting
- Выполнено: Делать trim URL в Markdown-ссылках перед проверкой allowlist
Нужны ли RAG или векторная база для такого AI-чата?
Видит ли браузер OpenAI API key?
Может ли AI выводить любые ссылки?
Комментарии
Gui
Генеральный директор Acecore. Руководит бизнес-системами, вебом, базами данных и инфраструктурой, качеством и внедрением ИИ от формулирования бизнес-задач до проектирования, запуска и дальнейшего улучшения. Опирается на практическую экспертизу C#/.NET и также учитывает PHP/JavaScript, SQL Server/PostgreSQL/MySQL и Linux/Windows Server, проектируя требования, технологический выбор, стандарты качества и GitHub-ориентированные процессы разработки как единую систему. Встраивает генеративный ИИ в процессы разработки, проверки и организации информации как практическую основу, помогающую небольшим командам быстрее и надежнее достигать результата.
Хотите узнать больше о наших услугах?
Мы обеспечиваем комплексную поддержку: разработка систем, веб-дизайн, графический дизайн и IT-образование.
Похожие статьи
Как развивать сайт на Astro + Cloudflare по функциям 7 июня 2026 г. в 19:00
Как добавить комментарии в Astro-блог только на Cloudflare 7 июня 2026 г. в 18:00