惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V2EX - 技术
V2EX - 技术
L
LangChain Blog
IT之家
IT之家
S
SegmentFault 最新的问题
博客园 - 三生石上(FineUI控件)
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
The Blog of Author Tim Ferriss
Blog — PlanetScale
Blog — PlanetScale
N
Netflix TechBlog - Medium
U
Unit 42
B
Blog RSS Feed
GbyAI
GbyAI
Microsoft Security Blog
Microsoft Security Blog
博客园 - 司徒正美
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
The Register - Security
The Register - Security
Vercel News
Vercel News
S
Schneier on Security
Spread Privacy
Spread Privacy
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
博客园 - 叶小钗
雷峰网
雷峰网
博客园_首页
人人都是产品经理
人人都是产品经理
P
Palo Alto Networks Blog
The Hacker News
The Hacker News
T
Tor Project blog
L
Lohrmann on Cybersecurity
Know Your Adversary
Know Your Adversary
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
P
Privacy International News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tenable Blog
V
Vulnerabilities – Threatpost
大猫的无限游戏
大猫的无限游戏
博客园 - 【当耐特】
V
V2EX
Security Latest
Security Latest
A
About on SuperTechFans
Cloudbric
Cloudbric
S
Security Affairs
MongoDB | Blog
MongoDB | Blog
Y
Y Combinator Blog
Martin Fowler
Martin Fowler
TaoSecurity Blog
TaoSecurity Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Я устал от Duolingo и написал себе AI-репетитора. Go, Clean Architecture, 4 LLM-модели — и вот что из этого вышло
vdv007 · 2026-05-04 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение7 мин

Охват и читатели0

Кейс

Зачем вообще писать ещё одно приложение для изучения языка

Мой рабочий день - это код. Вечером я хочу разговаривать с кем-то по-английски, а не нажимать на пингвинчиков.

  • Duolingo учит меня заказывать яблоки в магазине.

  • Memrise превратился в видеоплатформу с озвучкой.

  • ChatGPT-чат отлично объясняет грамматику, но не помнит, что я уже разбирал Present Perfect в среду и опять путаю его с Past Simple в пятницу.

Я хотел простую штуку: написать модели «давай сегодня про багтрекеры», получить чат на 15 минут, а в конце - три новых слова, которые она же мне и подобрала по уровню B1. Чтобы завтра эти слова всплыли в упражнениях. Чтобы статистика показывала, что я реально продвинулся, а не залип на стрике.

Такого продукта в моём публичном поиске не нашлось. Самописные «AI-tutor» в основном - обёртка над OpenAI API без памяти и без структуры. Я разработчик, у меня есть Go, Postgres, Redis и пара выходных. Через месяц получился Lexis - приложение с MIT-лицензией, четырьмя режимами тренировок и pluggable AI-провайдерами, которое теперь живёт у меня локально.

Это не история про «как заработать на edtech». Это инженерная история про то, как написать рабочий продукт с архитектурой, которая не развалится, когда я через год захочу добавить голосовой режим.

Дальше - три технических якоря, которыми я доволен, и честный список того, что ещё не готово.


Архитектура: модульный монолит, четыре модуля, Clean Arch внутри каждого

Версия 0.10.0 на момент записи статьи, репозиторий github.com/VDV001/lexis, MIT-лицензия.

Стек - короткий и без экзотики

Технология

Версия

Зачем

Go

1.26.1

Не 1.21, потому что писал в апреле 2026 и хотелось свежие generics-улучшения

chi

v5.2.5

Минимальный роутинг, прозрачный, без магии

PostgreSQL + pgx

v5.9.1

Основная БД

golang-migrate

v4.19.1

Миграции, эмбеддятся в бинарь через embed.FS

Redis

v9.18.0

Blacklist-токенов и кеширование

sqlc

-

Типобезопасный SQL без ORM-абстракций

JWT

v5.3.1

Симметричный HS256, ниже расскажу про rotation

zerolog + viper

-

Логи и конфиг

testify + gomock

v1.11.1

Юнит-тесты

Структура внутри каждого модуля

Классическая Clean Architecture:

  • domain - интерфейсы и модели

  • usecase - бизнес-логика

  • handler - HTTP-обработчики

  • infra - адаптеры к БД, Redis, внешним API

Между модулями

In-memory EventBus с интерфейсом, чтобы потом подменить на Kafka, когда (и если) понадобится. Сейчас бас отправляет события вроде WordLearned, SessionCompleted, StreakBroken - их слушает модуль progress, чтобы пересчитать аналитику без прямой связности с vocabulary.

Почему именно так

Это сознательный выбор:

  • Микросервисы для пет-проекта на одного юзера - оверинжиниринг.

  • Монолит, который через год нельзя распилить, - тоже путь в никуда.

  • Модульный монолит с границами на уровне пакетов и шиной событий даёт обе опции: сейчас один процесс и один Postgres, потом - выделить любой модуль в отдельный сервис без переписывания.


Якорь №1: pluggable AI-провайдеры через интерфейс

Изначально хотел только Claude. Потом подумал: если я буду тестировать упражнения, мне нужно сравнивать модели. И вообще - привязка к одному вендору в 2026 году выглядит наивно.

Каждый провайдер - отдельный файл в tutor/infra/

Файл

Размер

Статус

claude_provider.go

6.2K

✅ готов, Anthropic Messages API

openai_provider.go

6.6K

✅ готов, Chat Completions + streaming

gemini_provider.go

7.1K

✅ готов, Google Generative AI

qwen_provider.go

104 байта

🚧 заглушка. Честно: не дописал. В roadmap

Юзер в настройках выбирает модель, фронт шлёт model_id в каждом запросе, handler достаёт провайдера из registry и вызывает.

Что я понял на этом якоре

Интерфейс должен покрывать минимум возможностей - три метода, и всё. Если добавлять «специфические» фичи каждого провайдера в интерфейс, он раздуется и сломается на четвёртом провайдере. Гемини и OpenAI поддерживают tool-calling по-разному - я просто не использую tool-calling в чате, и эта боль откладывается до момента, когда она реально понадобится.


Якорь №2: SSE для стриминга AI-ответов вместо WebSocket

Когда модель отвечает в чате, я хочу видеть текст по мере генерации, а не ждать 8 секунд блок целиком.

  • Очевидное решение - WebSocket.

  • Не очевидное, но правильное для моего кейса - Server-Sent Events.

Почему SSE, а не WS

  1. Однонаправленный поток. AI-ответ идёт сервер → клиент. Юзер не пишет в этот канал. WebSocket для одностороннего стрима - оверкилл.

  2. HTTP-инфраструктура. SSE работает поверх обычного HTTP/2, проходит через прокси, легко балансируется. WS требует отдельной обработки в nginx и балансировщиках.

  3. Реконнект из коробки. Браузер сам переподключает SSE при разрыве с заголовком Last-Event-ID. С WS это надо писать руками.

  4. Простота. SSE-обработчик в Go - 30 строк, WS - 100+ с обработкой ping/pong, контролем frame size, закрытием соединения.

Единственный минус

SSE поверх HTTP/1.1 ограничен 6 одновременными соединениями на домен. Для одиночного приложения это не проблема, для прода с тысячами юзеров - перейти на HTTP/2, где лимит 100.


Якорь №3: JWT с rotation и reuse detection

Это часть, на которую ушло больше всего времени и которой я больше всего горжусь. Большинство туториалов по JWT в Go останавливаются на «проверь подпись и таймстемп». Это не работает в проде.

Проблема

Если refresh-токен утёк, злоумышленник может получать новые access-токены вечно. Как понять, что токен утёк? Только если жертва однажды попытается использовать тот же refresh-токен после злоумышленника.

Решение: token rotation + reuse detection

Реализовано в auth/usecase/auth_service.go:138-190. Логика:

1. Login Юзер получает access-токен (15 минут) и refresh-токен (30 дней). Refresh-токен записывается в БД с полем family_id и used = false.

2. Refresh через /auth/refresh Бэк проверяет:

  • Подпись валидна.

  • Токен не в Redis-blacklist.

  • В БД used = false.

3. Если всё ок Помечаем старый refresh used = true, выдаём новую пару с тем же family_id. Старый access добавляется в Redis blacklist до своего истечения.

4. Если refresh уже used = true - REUSE Значит, кто-то его уже использовал. Реакция: вызываем RevokeAllForUser(userID, familyID) - инвалидируем всю семью токенов и все access-токены этого юзера.

Юзер вылетает на логин на всех устройствах. Это плохо для UX, но правильно для безопасности: если токен утёк, лучше пять минут раздражения, чем неделя кражи данных.

Race condition

Между GetRefreshToken и MarkRefreshUsed решается транзакцией с SELECT ... FOR UPDATE. Это важно: без блокировки строки два одновременных refresh-запроса могут оба пройти проверку Used == false, оба получат новые токены, и reuse detection не сработает.

Redis-blacklist

Через infra/redis_blacklist.go хранит JTI инвалидированных access-токенов с TTL равным оставшемуся времени жизни токена. Каждый middleware проверяет blacklist - +1 round-trip к Redis на запрос, но это компромисс между security и latency, который я готов платить.

В сумме файл auth_service.go - 230 строк, и это честный production-ready код. Не «на потом перепишем», а то, что я сам ставлю на свои данные.


SM-2 spaced repetition: словарь, который сам подбирает повторения

В версии 0.10.0 модуль vocabulary хранит слова юзера в Postgres со следующими полями:

  • word, translation

  • easiness_factor (по умолчанию 2.5)

  • interval_days, repetitions

  • last_reviewed_at, next_review_at

Quality

Оценка от 0 до 5, как юзер вспомнил слово.

  • Плюс алгоритма - он реально работает, проверено десятилетиями Anki.

  • ⚠️ Минус - юзеру надо честно отвечать на quality, иначе кривая повторений сломается.

Каждый день фоновая горутина с time.Ticker пересчитывает «сколько слов сегодня к повторению» и кеширует это в Redis. Без кеша на каждый заход в дашборд был бы запрос в Postgres с фильтром next_review_at <= NOW() - не катастрофа, но лишняя нагрузка.

Четыре режима тренировки

Режим

Что делает

Откуда слова

Квиз

Выбор перевода из 4 вариантов

Из «к повторению сегодня»

Перевод

Юзер пишет перевод текстом, AI оценивает

Обновляет SM-2 quality

Заполнение пропусков

AI генерирует предложение с пропуском

Слово из своего словаря

Составление слов

Буквы перемешаны, надо собрать

Простой режим для орфографии


Тесты: testify, gomock, Playwright

Принцип: ATDD-цикл. Acceptance-тест (Playwright e2e) пишется первым, падает. Юнит-тесты внутри слоёв пишутся, чтобы acceptance прошёл.

  • testify v1.11.1 - assertions и suites. assert.Equal, require.NoError, suite.Suite.

  • go.uber.org/mock - мокаем интерфейсы доменного слоя. Например, mocks/mock_ai_provider.go для интерфейса AIProvider - usecase-тесты не вызывают реальный Anthropic API.

  • Playwright e2e на TypeScript - запускают приложение в Docker, открывают браузер, проходят флоу регистрации → создания сессии → ответа в чате.


Что не готово - честный список

  1. Qwen-провайдер - заглушка 104 байта. Дописать - дело двух часов, но не было приоритета.

  2. Голосовой режим - хочу диктовать ответы и слышать произношение. Web Speech API на фронте + ElevenLabs на бэке. В планах.

  3. Импорт из Anki - юзеры с большими колодами не захотят начинать с нуля. Парсер .apkg файлов - в roadmap.

  4. Только 2 миграции - users и vocabulary. Это сразу выдаёт молодой проект. Будут ещё, когда добавлю темы (topics), повторяющиеся сессии (recurring_sessions) и группы слов (word_groups).

  5. Нет мобильного приложения - только веб. PWA достаточно, нативное iOS/Android - не в этом году.

  6. Нет публичного хостинга - локальный запуск через docker compose up. Деплоить мульти-юзер сервис с биллингом за LLM-токены - отдельный проект, и пока не моя цель.


Попробовать

git clone https://github.com/VDV001/lexis
# вписать AnthropicKey / OpenAIKey / GoogleKey хотя бы один
docker compose up -d
# фронт: http://localhost:3000
# бэк:   http://localhost:8080

В .env нужны:

  • ключ хотя бы одного AI-провайдера

  • JWT_SECRET (любой длинный рандомный)

  • DB_DSN (по умолчанию работает с docker-compose)

  • REDIS_ADDR (тоже по умолчанию)

Регистрация - email + пароль. Никаких внешних OAuth, я не хотел зависеть от чужой аутентификации. Bcrypt для хеширования, минимум 8 символов.

После регистрации - выбор языка (English), уровня (A1-C2), темы недели. Создаётся первая сессия, и можно писать модели.


Вместо вывода

Lexis как продукт - он мой личный, я им пользуюсь. Эта статья - про инженерные решения, которые мне нравятся и которые я бы рекомендовал в любом своём следующем проекте:

  • Модульный монолит с готовностью к распилу.

  • Pluggable провайдеры через минимальный интерфейс.

  • SSE вместо WebSocket там, где поток однонаправленный.

  • JWT rotation + reuse detection как стандарт, а не «может потом».

Если у вас есть вопросы по архитектуре или вы видите спорные решения - GitHub Issues открыты, MIT-лицензия позволяет форкать без вопросов. Если вы тоже устали от пингвинов и хотите AI-репетитора, который помнит, что вы вчера разбирали - попробуйте.

Репозиторий: github.com/VDV001/lexis Лицензия: MIT


P.S.

Если статья зашла - поставьте плюс, и я напишу разбор отдельных частей: например, про настройку Playwright для Go-бэкенда или про то, как я писал систему промптов для четырёх режимов упражнений на трёх разных моделях и они отвечают примерно одинаково.

Скриншоты будут в проекте в директории screenshots.