← Все гайды AI Release
EN Подписаться на @ai_release1

Как собрать локального research-агента для научных статей: поиск литературы, чтение и запуск экспериментов на GPU

Обновлено: 19.10.2026 · AI Release · @ai_release1
Как собрать локального research-агента для научных статей: поиск литературы, чтение и запуск экспериментов на GPU

TL;DR

Требования и подготовка

Иллюстрация к разделу «Требования и подготовка»
Иллюстрация к разделу «Требования и подготовка»

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

ЗвеноВходВыходЧем проверяем
Поискзапрос строкой`data/papers.json`число записей в JSON
ЧтениеPDF из `data/pdf/``notes/<id>.md`файл с секциями «метод/данные/результаты»
Планзаметки + цель`experiments/<run>/spec.yaml`YAML валиден, поля заполнены
Запускspec.yaml`experiments/<run>/logs/*.log`код возврата 0, метрика в логе

Общая идея «поиск → чтение → действие» — это и есть переход от чата к агенту; подробнее про цикл «подумал → вызвал инструмент → проверил результат» в гайде Как превратить обычный чат с LLM в полноценного AI-агента.

Шаг 1. Каркас проекта и поиск литературы

Что сделать: создать структуру папок, виртуальное окружение и скрипт, который забирает статьи из arXiv API (открытый API научного архива препринтов arXiv) и складывает их в JSON.

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

Как проверить: в конце шага data/papers.json содержит столько же записей, сколько статей вернул API.

mkdir -p ~/research-agent/{data/pdf,notes,experiments,scripts}
cd ~/research-agent
python3.11 -m venv .venv && source .venv/bin/activate
pip install requests pymupdf pyyaml

Почему это работает: venv изолирует зависимости агента от системного Python, а pymupdf умеет вытаскивать текст из PDF без внешних бинарников.

Запрос к arXiv (официальный эндпоинт export.arxiv.org; формат параметров сверьте с документацией API):

curl -s -A "research-agent/0.1 (mailto:<ваш-email>)" \
  "http://export.arxiv.org/api/query?search_query=all:%22retrieval%20augmented%20generation%22&start=0&max_results=5&sortBy=submittedDate&sortOrder=descending" \
  -o data/arxiv_raw.xml
grep -c "<entry>" data/arxiv_raw.xml

Пример: ожидаемый вывод — 5. Если вышло 0, запрос синтаксически неверен или arXiv вернул ошибку вместо Atom-фида — откройте data/arxiv_raw.xml и посмотрите первые строки.

Парсер в scripts/search.py:

import json, xml.etree.ElementTree as ET
NS = {"a": "http://www.w3.org/2005/Atom"}
tree = ET.parse("data/arxiv_raw.xml")
papers = []
for e in tree.getroot().findall("a:entry", NS):
    papers.append({
        "id": e.find("a:id", NS).text.rsplit("/", 1)[-1],
        "title": " ".join(e.find("a:title", NS).text.split()),
        "summary": " ".join(e.find("a:summary", NS).text.split()),
        "pdf": next(l.get("href") for l in e.findall("a:link", NS) if l.get("title") == "pdf"),
    })
json.dump(papers, open("data/papers.json", "w"), ensure_ascii=False, indent=2)
print(len(papers), "papers saved")
python scripts/search.py

Пример: 5 papers saved. Дальше отбор: отсортируйте papers.json по пересечению ключевых слов с вашей темой и оставьте 3–5 штук — LLM плохо работает с двадцатью статьями сразу.

Скачивание PDF (пауза между запросами обязательна — arXiv просит не частить, точные лимиты сверьте с их правилами использования):

python - <<'PY'
import json, time, requests
for p in json.load(open("data/papers.json")):
    r = requests.get(p["pdf"], timeout=60)
    open(f"data/pdf/{p['id']}.pdf", "wb").write(r.content)
    time.sleep(3)
    print(p["id"], r.status_code, len(r.content))
PY

Шаг 2. Чтение статей локальной моделью и генерация плана эксперимента

Иллюстрация к разделу «Шаг 2. Чтение статей локальной моделью и генерация плана эксперимента»
Иллюстрация к разделу «Шаг 2. Чтение статей локальной моделью и генерация плана эксперимента»

Что сделать: поднять локальный LLM-сервер, прогнать каждую статью через «читатель» (PDF → текст → заметка) и попросить модель собрать YAML-спеку эксперимента.

Зачем: читать 30 страниц PDF вручную долго, а спека в YAML нужна, чтобы запуск на GPU был воспроизводимым: те же данные, те же гиперпараметры, тот же образ.

Как проверить: сервер отвечает на /api/tags, в notes/ появились файлы, spec.yaml парсится как YAML.

ollama serve &
curl -s http://localhost:11434/api/tags

Пример: ответ — JSON вида {"models":[{"name":"<тег-модели>", ...}]}. Имя модели в поле name подставьте в скрипт ниже вместо плейсхолдера <тег-модели>.

Читатель scripts/read.py:

import fitz, json, requests, pathlib
MODEL = "<тег-модели>"  # имя из /api/tags
PROMPT = ("Ты читаешь научную статью. Верни markdown с разделами: "
          "## Задача, ## Метод, ## Данные, ## Метрики, ## Ограничения. "
          "Только то, что есть в тексте, без домыслов.\n\nТекст:\n")
for p in json.load(open("data/papers.json")):
    doc = fitz.open(f"data/pdf/{p['id']}.pdf")
    text = "\n".join(page.get_text() for page in doc)[:20000]
    r = requests.post("http://localhost:11434/api/generate",
                      json={"model": MODEL, "prompt": PROMPT + text, "stream": False},
                      timeout=600)
    pathlib.Path(f"notes/{p['id']}.md").write_text(r.json()["response"])
    print(p["id"], "ok")

Почему это работает: fitz отдаёт чистый текст, а обрезка до 20000 символов не даёт превысить контекст модели — остальное отбрасывается осознанно, а не «как получится».

Пример: вывод 2401.00001 ok для каждой статьи. Откройте notes/<id>.md — если раздел «## Метрики» пуст, модель не нашла метрик в первых 20000 символах, и это повод сдвинуть окно на результаты.

Теперь план. Отдайте модели заметки и цель, потребуйте строгий YAML:

cat notes/*.md > notes/_all.md
python - <<'PY'
import requests
MODEL = "<тег-модели>"
prompt = ("На основе заметок составь YAML-план воспроизведения эксперимента. "
          "Ключи строго: dataset, base_model, batch_size, learning_rate, epochs, "
          "seed, gpu_id, script, metric. Без пояснений, только YAML.\n\n"
          + open("notes/_all.md").read()[:20000])
r = requests.post("http://localhost:11434/api/generate",
                  json={"model": MODEL, "prompt": prompt, "stream": False}, timeout=600)
open("experiments/run01/spec.yaml", "w").write(r.json()["response"])
PY
mkdir -p experiments/run01/logs
python -c "import yaml;print(yaml.safe_load(open('experiments/run01/spec.yaml')))"

Пример: на экране — словарь с ключами dataset, batch_size, learning_rate. Если вместо YAML пришёл текст с «Конечно, вот план», значит модель проигнорировала инструкцию — обрежьте ответ по первой строке dataset: и повторите.

Спека — единственный источник правды для запуска. Проверьте глазами: batch_size, learning_rate, gpu_id должны быть числами, а script — путём внутри контейнера.

Шаг 3. Запуск эксперимента на GPU и проверка результата

Иллюстрация к разделу «Шаг 3. Запуск эксперимента на GPU и проверка результата»
Иллюстрация к разделу «Шаг 3. Запуск эксперимента на GPU и проверка результата»

Что сделать: запустить обучение в контейнере с проброшенным GPU, перенаправив логи в файл, и убедиться, что метрика записалась.

Зачем: контейнер фиксирует окружение, а --gpus не даёт процессу занять все карты и уронить чужие задачи.

Как проверить: код возврата 0 и наличие строки с метрикой в логе.

docker run --rm --gpus '"device=0"' \
  -v "$PWD/experiments/run01:/work" -w /work \
  <ваш-образ> \
  bash -lc "python $(python -c "import yaml;print(yaml.safe_load(open('experiments/run01/spec.yaml'))['script'])") --config spec.yaml" \
  2>&1 | tee experiments/run01/logs/train.log
echo "exit=${PIPESTATUS[0]}"

Пример: в логе идут строки вида epoch 1/3 loss=..., в конце exit=0. Проверка метрики:

grep -Ei "accuracy|f1|loss" experiments/run01/logs/train.log | tail -n 5
nvidia-smi --query-gpu=memory.used,utilization.gpu --format=csv

Успех выглядит так: exit=0, в последних строках лога есть число метрики, nvidia-smi показывает ненулевое memory.used во время прогона и падение до нуля после.

Чек-лист приёмки прогона:

Если статья лежит за JS-страницей и PDF не скачивается напрямую, забор файла можно автоматизировать браузером — см. Как подключить ИИ-агента к Chrome: настройка автоматизации браузера.

Возможные проблемы и решения

Проблема. Обучение падает сразу после старта, GPU занят на 100%

Проблема. Скрипт чтения не может достучаться до модели

Проблема. Поиск возвращает пустой список статей

Резюме и следующие шаги

Агент собран из четырёх звеньев, каждое из которых пишет артефакт на диск: papers.json, notes/*.md, spec.yaml, logs/train.log. Дальше имеет смысл добавить дедупликацию источников по DOI и автоматический отчёт, который собирает цитаты из заметок вместе с метрикой прогона.

FAQ

Чем заменить Ollama, если не хватает скорости?

Любым локальным сервером с OpenAI-совместимым API — тогда в скриптах меняется только URL и имя модели. Конкретный сервер и его флаги сверьте с документацией.

Сколько статей давать модели за один раз?

Начните с 3–5: больше не влезает в контекст без потери качества, а обрезка текста уже отбрасывает раздел с результатами.

Обязателен ли Docker для запуска на GPU?

Нет, но без него окружение эксперимента не фиксируется, и результат сложно воспроизвести. Минимум — зафиксируйте версии пакетов в requirements.txt с точными пинами.

Что делать, если модель возвращает план не в YAML?

Снизьте температуру генерации, уберите из промпта лишние пояснения и добавьте фразу «только YAML, без markdown-обёртки». Как крайний вариант — режьте ответ от первой строки dataset:.

🤖 Кратко для ИИ