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

推荐订阅源

小众软件
小众软件
B
Blog RSS Feed
美团技术团队
博客园 - 【当耐特】
C
Check Point Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
M
MIT News - Artificial intelligence
aimingoo的专栏
aimingoo的专栏
J
Java Code Geeks
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 司徒正美
T
Tailwind CSS Blog
Last Week in AI
Last Week in AI
Google DeepMind News
Google DeepMind News
D
DataBreaches.Net
人人都是产品经理
人人都是产品经理
N
Netflix TechBlog - Medium
Vercel News
Vercel News
P
Proofpoint News Feed
IT之家
IT之家
I
InfoQ
腾讯CDC
H
Hackread – Cybersecurity News, Data Breaches, AI and More

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 выдумывать цифры в данных. Допрос Clau...
Сергей Франгулов · 2026-06-24 · via Все публикации подряд на Хабре

Простой

7 мин

218

Никто не верил, что модель можно подпустить к таблицам и заставить не галлюцинировать. Цифры из воздуха, выдуманные колонки, суммы, которые не сходятся с источником, думаю на этом обжигались все, кто пробовал. Мы заставили, проблемы все еще есть, но выглядят решаемыми.

Сначала про то, почему любая LLM врёт на данных. Она достраивает правдоподобное продолжение токен за токеном. Таблица, которую вы положили ей в контекст, для модели такой же текст, как и всё остальное. Не хватило данных, неудобно читать, вопрос чуть в стороне — она не скажет «не знаю», а допишет то, что выглядит как правда. Прямой путь, отдать ей CSV и попросить посчитать, ровно поэтому и не работает.

Сработал другой подход. Мы взяли Claude Desktop и оказалось, что он прекрасно пережевывает большие датасеты: не захлёбывается и не врёт цифрой там, где обычная связка «модель плюс CSV» уже ломается. Стало интересно, за счёт чего. Мы начали его опрашивать и разбирать, как он это делает. Вот что выяснили — и что из этого перенесли в свой MCP-сервер.

Первый подход: Claude Desktop умеет, но непредсказуемо

Сначала честно: анализ данных Claude делает и сам, без всякой обвязки. Дай ему датасет и вопрос — думаю он посчитает. Он умеет, проблема в том, что делает это каждый раз по-разному. Тот же вопрос и он то возьмёт один разрез, то на полпути сменит подход, то выберет другую логику расчёта. Для разовой задачи сносно, но на должность дата аналитика не тянет.

Раз он справляется с данными, рецепт у него есть. Мы решили его просто спросить.

Что рассказал Claude Desktop

Мы стали опрашивать напрямую: как ты работаешь с большими датасетами, как устроена твоя песочница, как устроены навыки для работы с данными и тд. Устройство его песочницы я оставлю за скобками — это кухня Anthropic, и реконструировать её догадками в статье про то, как не выдумывать, было бы смешно. Если хотите спросите сами, он кое-что расскажет, а песочница claude code вообще с открытыми исходниками. Свою мы потом собрали сами, о ней дальше. Нас интересовало переносимое.

Переносимое свелось к трём вещам. Считать данные движком вместо счёта в уме — тут он сразу назвал DuckDB. Аналитику, которую нельзя выразить в SQL запрос, выносить в Python в песочницу. И описывать данные и схему так, чтобы модель не гадала, что внутри. Эти три вещи мы и зашили в свой MCP-сервер: фронт на Next.js (ai-sdk), бэкенд на FastMCP, под капотом DuckDB, модель ходит в данные только через инструменты. Смысл обвязки — забрать у модели свободу импровизировать и пользоваться инструментами. Дальше по порядку.

Движок: считать, а не прикидывать

Самая частая ложь модели при работе с данными — это арифметика и агрегации в уме. Спросите модель «какая средняя выручка по регионам», положив ей таблицу на 50 тысяч строк, и она с удовольствием выдаст число. Иногда даже близкое к правде.

Поэтому считать ей мы не даём. Она пишет SQL, а выполняет его DuckDB — тот самый движок, который Claude назвал первым. Среднее, сумма, группировка, джойн — всё уходит в движок, который не ошибается в арифметике по определению.

DuckDB выбран не случайно. Это встраиваемый OLAP-движок, который читает CSV, Parquet и JSON напрямую через read_csv_auto() и read_parquet(), а к внешним базам подключается через federation — ATTACH к PostgreSQL, MySQL, SQLite без копирования данных:

ATTACH 'host=... dbname=...' AS pg (TYPE postgres, READ_ONLY)

Подключение всегда READ_ONLY и всегда in-memory: данные живут на диске или во внешней базе, а DuckDB поднимает их в память только на время запроса.

Но дать модели писать произвольный SQL — это путь к биде. Поэтому каждый запрос проходит через гейт, еще один слой защиты. Функция validate_read_only()mcp_server/duckdb_utils.py) режет всё, что меняет данные или лезет мимо данных:

def validate_read_only(sql: str) -> None:
    """Бросает ValueError, если в SQL есть мутации.
    Блокирует INSERT / UPDATE / DELETE / DROP / CREATE / ALTER / ATTACH,
    а ещё функции glob(), sqlite_scan(), postgres_query() — они
    обходят изоляцию файловой системы."""

Проверка идёт по очищенному SQL — без комментариев и строковых литералов, чтобы фильтр нельзя было обмануть, спрятав что-то в комментарий. Рядом prepare_user_sql() разворачивает относительные пути файлов в полные и проверяет, что путь не вышел за пределы разрешённой папки.

Сам движок тоже закрыт на замок. При создании соединения мы запрещаем автозагрузку расширений и в конце замораживаем конфигурацию:

con.execute("SET autoinstall_known_extensions=false")
con.execute("SET autoload_known_extensions=false")
con.execute("SET lock_configuration=true")  # после этого настройки не поменять

И последнее в этой части — то, без чего модель снова начинает врать даже на честно посчитанных данных. Результат запроса мы усекаем: если строк больше двух сотен, показываем первые и последние, а к ним прикладываем предупреждение прямо в выводе тула:

WARNING: показаны 200 из 5000 строк. Агрегации по этому выводу будут неверны — используйте SQL-агрегацию.

Без этой строчки модель честно получает 200 строк, видит, что это «таблица», и спокойно «усредняет» показанное, забыв про оставшиеся 4800. Предупреждение возвращает её к реальности.

Но костяк — это SQL. Не всё на него ложится: сложная статистика, прогнозы, нестандартные преобразования. Для этого есть вторая рука.

Вторая рука: песочница с Python

Второе, что назвал Claude, — Python для аналитики, которую неудобно гнать через SQL. У нас это отдельный сервис-песочница: изолированный контейнер на своём порту, код исполняется под непривилегированным пользователем. Модель пишет код, песочница его запускает и возвращает результат — тот же принцип, что и с SQL: считает среда, модель только формулирует.

Песочница приходит с предустановленным набором библиотек, чтобы модель не тянула зависимости на ходу:

  • данные: pandas, numpy, scipy, polars, pyarrow, duckdb;

  • статистика и прогнозы: statsmodels, scikit-learn, statsforecast, mlforecast, плюс бустинг lightgbm и catboost с интерпретацией через shap;

  • графика: matplotlib, seaborn, plotly;

  • выгрузки и документы: openpyxl, xlsxwriter, python-docx, python-pptx;

  • русский текст: natasha, razdel, pymorphy3.

Список залочен и заранее закеширован в образе: модель работает тем, что есть, и не тянет что попало из сети.

Песочница изолирована на сколько можно: изолированный контейнер, непривилегированный пользователь, фиксированный список либ. Свобода у модели есть, но внутри этих границ.

Описание данных: чтобы модель не гадала о схеме данных

Третий совет Claude был про описание данных, и он закрывает вторую частую ложь — выдуманные колонки и таблицы. Модель пишет SELECT revenue FROM sales, а колонка называется total_amount, и таблицы sales нет вовсе. Корень один: модель не знает структуру и достраивает «похоже на правду».

Лечится это тем, что структуру ей дают раньше, чем она успеет придумать. У нас за это отвечают два тула.

data_list показывает каталог: какие датасеты вообще есть, сколько в каждом строк и колонок, краткое описание. Модель сначала видит, с чем она работает, а не угадывает имена файлов.

data_schema идёт глубже — это профиль каждой колонки. Тип, доля пустых значений, число уникальных, минимум, максимум и среднее для чисел. Плюс то, что мы добавили специально под аналитику: детект валют и единиц измерения. Тул смотрит на сами значения и подсказывает модели, что profit лежит в долларах, а quantity — в тоннах:

Table: sales.csv
Rows: 250 000
Columns: 8

Column          Type            Null   Unique
profit          INTEGER         2%     —
quantity        DECIMAL(10,2)   0%     —
region          VARCHAR         0%     7
date            DATE            0%     —

Подсказки по единицам:
- profit   → валюта: USD
- quantity → единица: тонна

Это не косметика. Без подсказки по валютам модель спокойно сложит доллары с евро в одном SUM() и выдаст бессмысленную сумму с видом эксперта.

Чтобы не считать профиль на каждый чих, описания кэшируются. Метаданные датасетов лежат в PostgreSQL (таблица dataset_metadata: число строк, колонок, текст схемы, описание), а структура подключённых внешних баз — в cached_schema. Модель получает готовый контекст за один вызов.

И последнее — дисциплина. Системный промпт требует фазу PLAN: сначала data_list и data_schema, и только потом data_query. Узнай, что есть и как устроено, потом спрашивай данные. Этот порядок снимает большую часть выдуманных колонок ещё до первого запроса.

Когда данных нет: главный соблазн соврать

Самый опасный момент в работе с данными — не сложный запрос, а пустой ответ. Модель спросила, фильтр ничего не нашёл, вернулось ноль строк. Вот тут LLM особенно тянет «спасти» ситуацию: раз данных нет, выдам правдоподобное. Ровно так и берутся цифры из воздуха.

Поэтому просто пустой ответ мы модели не отдаем. На нулевом результате тул сам идёт разбираться, почему фильтр не сработал: прогоняет SELECT DISTINCT по колонкам из условия и возвращает реальные значения:

Запрос вернул 0 строк. Возможная причина в фильтре: колонка year содержит значения: 2020, 2021, 2022. В вашем фильтре было year = 2025.

Теперь у модели есть зацепка, и она чинит фильтр. Промпт это поддерживает: до трёх попыток, прочитай ошибку, поменяй, повтори.

И ещё одна мелочь в ту же сторону. Каждый тул честно сообщает не только данные, но и их состояние: сколько вернулось строк, усечён ли ответ. Модель не гадает, полный ли у неё результат. Без такой честности она додумывает там, где достаточно посмотреть на факт.

Как это выглядит на одном запросе

Соберём вместе. Пользователь спрашивает: «покажи выручку по регионам».

  1. data_list — модель видит, что есть датасет sales на 250 тысяч строк.

  2. data_schema — узнаёт колонки: region, revenue в долларах, date.

  3. data_query — пишет SELECT region, SUM(revenue) ... GROUP BY region, сумму считает DuckDB.

  4. viz_chart — строит график на тех числах, что вернул запрос.

В финале каждое число прослеживается до конкретного выполненного SQL. Нет шага, где модель «прикинула» цифру по памяти. Та самая «выручка +23%» теперь либо берётся из запроса, либо не появляется вовсе.

Где модель всё равно может соврать

Честный взгляд со стороны, без которого разбор был бы не полным.

validate_read_only — это регулярки, а не разбор AST. Гейт ловит мутации и небезопасные функции, но не семантику. Кривой JOIN, который молча размножит строки, пройдёт проверку и даст неверную сумму. Движок посчитает её безупречно — и она будет ошибочной.

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

Предупреждение об усечении спасает не всегда, про «200 из 5000» будет в ответе, но это текст, а текст модель может проигнорировать. Жёсткого запрета агрегировать усечённое нет.

Смешанные валюты — та же история. Детект подсказывает, что складывать доллары с евро нельзя, но это подсказка, а не блок. Дисциплина держится на промпте, а промпт можно не дочитать.

Вывод один. Наша поделка не убирает галлюцинации, она сужает область, где они возможны. Мы перенесли ответственность с памяти модели на инструменты с жёсткими границами. Где граница вшита в код (read-only, скоуп файлов, схема данных), модели не надо выдумывать цифры, они все есть. Ответ воспроизводим: тот же вопрос завтра даёт ту же цифру — ровно то, чего Claude в одиночку не давал. Где осталась промпт-дисциплина, соврать ещё может, и за этими местами приходится следить руками.

И кажется, в этом вся работа. Каждый раз, когда наша модель уверенно выдумывала цифры, разбор упирался не в «модель тупая», а в место, где ей просто негде было взять данные. Вопрос всегда оказывался один: где проходит граница между «пусть посчитает модель» и «пусть посчитает движок».