ИИ-ассистент сайта: границы возможностей и техническое решение
ИИ-ассистент сайта сейчас — rule-based Q&A (заранее заданная база знаний, без доступа к медкартам и без внешних LLM); ниже — планируемое инженерное решение, иллюстрирующее подход и референсную реализацию.
Общая архитектура системы
Задачи четырёх ключевых модулей
Диалоговый интерфейс (фронтенд)
React/Vue-компоненты: список сообщений, ввод, индикатор набора, быстрые чипы, потоковый рендеринг, переключение языков и доступность (aria / клавиатура).
Сервис обработки сообщений (бэкенд)
Состояние сессии, маршрутизация интентов, комплаенс-ограждения, сборка контекста, вызов LLM, эскалация и журналирование.
Интеграция диалогового LLM-интерфейса (в планах)
API LLM-сервиса: комплаенс System Prompt для медицины, RAG-поиск, Function Calling к KB/CRM, потоковые ответы SSE.
R&D-ассистент · генерация / исправление кода
Планируемый R&D-ассистент: генерирует код новых интентов и FAQ, чинит дефекты, создаёт PR; merge после ревью человеком.
Цепочка обработки одного диалогового запроса
- 1Пользователь отправляет
Фронтенд собирает сообщение и sessionId и отправляет на /api/chat по шифрованному WSS/HTTPS.
- 2Маршрутизация интентов
Бэкенд распознаёт интент (цены / офтальмология / поездка / членство / ЧС) по ключевым словам и классификатору и собирает контекст с фрагментами базы знаний.
- 3генерирует диалоговый LLM-интерфейс
Бэкенд вызывает LLM (с ограждениями и RAG) и возвращает ответ потоком SSE; при срабатывании условий эскалации — перевод на живого оператора.
- 4Фронтенд-рендеринг
Фронтенд дорендеривает ответ по частям, пишет лог диалога и оценки удовлетворённости — для будущих итераций и самоисправления.
Линейка R&D-поддержки работает независимо: разработчик заводит тикет (новый интент / баг) → агент пишет или чинит код → создаёт Pull Request → ревью и merge в прод.
Ключевые интерфейсы / контракты данных
POST /api/chat (SSE) request/response contract:
// Request
{
"sessionId": "sess_8f3a", // session id
"message": "How much is a dental implant?", // user text
"lang": "en" // language
}
// Response (text/event-stream, chunked)
data: {"delta": "The indicative USD range for a single implant is ..."}
data: {"delta": "Final pricing follows the hospital."}
data: [DONE]
Контракт вызова LLM (бэкенд → модельный сервис):
POST https://api.openai.com/v1/chat/completions
{
"model": "gpt-4o-mini",
"stream": true,
"messages": [
{"role":"system","content":"compliance guardrails + KB snippets"},
{"role":"user","content":"[en] How much is a dental implant?"}
]
}
Контракт задач R&D-ассистента (бэкенд → агент):
{
"task": "Add an FAQ follow-up to the price intent: does it include bone graft costs",
"file": "src/intents/price.js",
"failing_test": null,
"review": "required" // must be human-reviewed before merging
}
Ключевые функции (готовый к запуску код)
Ниже — типовой пример, иллюстрирующий лишь подход к интеграции,не является производственным кодом этого сайта, и не развёрнуто в проде.
Сервис обработки сообщений + интеграция LLM (Node 18+, требуется express и openai):
// server.js
import express from 'express';
import OpenAI from 'openai';
const app = express();
app.use(express.json());
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Medical compliance guardrails: no diagnosis / prescriptions / efficacy promises
const GUARDRAILS = [
'You are the AI assistant of a medical travel concierge site; provide objective process and credential information only.',
'Never diagnose, prescribe, promise outcomes, or rank doctors.',
'Actual treatment requires an in-person visit with a licensed physician; costs follow hospital pricing.',
'For emergencies, guide users to local emergency numbers (120 in China / 911 in the US).',
'When unsure, escalate to a human agent.'
].join(String.fromCharCode(10));
// Intent routing: keyword hit decides the class, otherwise general
const INTENTS = {
price: /价格|比价|费用|报价|usd|\$/i,
eye: /眼科|近视|激光|smile|lasik|icl|白内障/i,
travel: /行程|签证|机票|酒店|陪护/i,
member: /会员|登录|账号|保险/i,
emergency: /紧急|急救|意外|危险/i
};
function route(text){ for(const k in INTENTS) if(INTENTS[k].test(text)) return k; return 'general'; }
app.post('/api/chat', async (req, res) => {
const { sessionId, message, lang = 'zh-CN' } = req.body;
const intent = route(message);
const kb = await retrieveKB(intent, message); // vector retrieval placeholder
const stream = await openai.chat.completions.create({
model: 'gpt-4o-mini', stream: true,
messages: [
{ role:'system', content: GUARDRAILS + '\nKB snippets:\n' + kb },
{ role:'user', content: '[' + lang + '] ' + message }
]
});
res.setHeader('Content-Type','text/event-stream');
for await (const chunk of stream) {
const t = (chunk.choices[0] && chunk.choices[0].delta && chunk.choices[0].delta.content) || '';
if (t) res.write('data: ' + JSON.stringify({ delta: t }) + String.fromCharCode(10) + String.fromCharCode(10));
}
res.write('data: [DONE]' + String.fromCharCode(10) + String.fromCharCode(10));
res.end();
});
async function retrieveKB(intent, q){ return '(Example) public credential and price-range notes for this intent.'; }
app.listen(3000, () => console.log('AI chat service on :3000'));
Потоковый вызов с фронтенда (браузер):
// chat-client.js
export async function sendChat(sessionId, message, lang, onDelta) {
const res = await fetch('/api/chat', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ sessionId, message, lang })
});
const reader = res.body.getReader();
const dec = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of dec.decode(value).split(String.fromCharCode(10))) {
if (line.startsWith('data: ')) {
const payload = line.slice(6);
if (payload === '[DONE]') return;
onDelta(JSON.parse(payload).delta);
}
}
}
}
Модуль генерации / исправления кода (R&D-ассистент, в планах) (Python, требуется openai, замените model при подключении R&D-ассистента):
# codex_agent.py
import os, openai, sys
client = openai.OpenAI(api_key=os.environ['OPENAI_API_KEY'])
def generate_or_fix(task, file_path=None, failing_test=None):
ctx = ''
if file_path and os.path.exists(file_path):
ctx += '\n# Existing file ' + file_path + '\n' + open(file_path, encoding='utf-8').read()
if failing_test:
ctx += '\n# Failing test output\n' + failing_test
prompt = ('You are the code engineer for the customer-service system. Task: ' + task +
'\nOutput only a mergeable code diff or the complete file, with a brief explanation.\n' + ctx)
r = client.chat.completions.create(
model='gpt-4o', # replace model when wiring in the actual code model
messages=[{'role':'system','content':'You are a senior frontend / Node engineer familiar with this customer-service system.'},
{'role':'user','content':prompt}])
return r.choices[0].message.content
if __name__ == '__main__':
print(generate_or_fix(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None))