1.3 · Python: uv, async, Pydantic
Неделя 1 · Подготовка · ⏱ ~4 ч · Практика
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»Любой LLM-проект — это окружение с зависимостями (uv), много сетевых вызовов, которые надо делать параллельно и с ограничением частоты (asyncio + httpx), и данные, которые нельзя принимать на веру (Pydantic). Ограничение частоты нужно потому, что у каждого провайдера есть rate limit — предел числа запросов в единицу времени или одновременных запросов. Pydantic особенно важен: из его моделей строится JSON Schema (стандартное описание формы JSON-документа) для structured output — ответа модели строго по заданной схеме — и для tool calling, где модель просит вызвать нашу функцию с аргументами по схеме (обе темы — неделя 2); им же валидируется ответ модели.
В главе 1.1 мы писали скрипты на системном python3; теперь заводим настоящий проект и проходим по порядку: окружение и зависимости, линтер, асинхронные запросы, валидация данных, тесты и отладка. Тесты (pytest) и линтер (Ruff) — то, по чему на ревью отличают инженера от «скрипта из чата».
1. uv — менеджер проекта и версий Python в одном
Заголовок раздела «1. uv — менеджер проекта и версий Python в одном»В Python пакеты ставятся не в папку проекта, а в виртуальное окружение — изолированную копию интерпретатора с собственной site-packages. Без него все проекты делили бы одни версии библиотек. uv создаёт и обслуживает это окружение сам.
Для ориентира — соответствия с npm (в Maven, Cargo или Go modules роли те же):
| npm | uv | Что делает |
|---|---|---|
npm init |
uv init name |
создаёт pyproject.toml, .python-version, main.py, git |
package.json |
pyproject.toml |
метаданные и зависимости |
package-lock.json |
uv.lock |
точные версии, коммитится |
node_modules/ |
.venv/ |
установленные пакеты, в .gitignore |
npm install x |
uv add x |
добавить зависимость |
npm install -D x |
uv add --dev x |
dev-зависимость (pytest, ruff) |
npm ci |
uv sync |
привести .venv в соответствие с lock |
npx x / npm run |
uv run x |
запустить внутри окружения |
| nvm | uv python install 3.12 |
установить нужную версию Python |
Сам uv ставится одной командой по официальной инструкции для своей ОС; проверка — uv --version.
uv run перед запуском сам проверяет, что окружение синхронизировано с lock-файлом, — поэтому «активировать» .venv вручную не нужно. Глобальный pip install не используем: он ставит пакет в системный Python, и через месяц непонятно, что откуда.
# pyproject.toml after `uv add httpx pydantic` and `uv add --dev pytest ruff`[project]name = "learn-python"version = "0.1.0"requires-python = ">=3.12"dependencies = ["httpx>=0.28", "pydantic>=2.10"] # versions illustrative
[dependency-groups]dev = ["pytest>=8", "ruff>=0.8"]Точный вид секций и версий у нас получится свой — смотрим, что сгенерировал uv.
2. Ruff — линтер и форматтер
Заголовок раздела «2. Ruff — линтер и форматтер»Первые dev-зависимости проекта — инструменты контроля качества. Ruff заменяет сразу линтер и форматтер (в мире JavaScript — ESLint и Prettier). uv run ruff check . — линтер (неиспользуемые импорты, подозрительные конструкции), uv run ruff check --fix . — автоисправление, uv run ruff format . — форматтер. Настройки — в pyproject.toml в секции [tool.ruff]. По умолчанию включён умеренный набор правил; полезно добавить импорт-сортировку и «bugbear»:
[tool.ruff.lint]select = ["E", "F", "I", "B", "UP"] # pycodestyle, pyflakes, isort, bugbear, pyupgradeПравило B006 из bugbear ловит изменяемый default из гл. 1.1 — хороший пример, зачем нужен линтер для кода от модели.
3. asyncio — event loop, который надо включить самому
Заголовок раздела «3. asyncio — event loop, который надо включить самому»LLM-приложение большую часть времени ждёт сеть: один ответ модели — это секунды. Если отправлять 100 запросов по очереди, ожидания складываются; асинхронный код позволяет ждать их одновременно. В некоторых средах (JavaScript в браузере и Node) event loop есть всегда. В Python код синхронный, пока мы явно не запустим цикл:
import asyncio
async def fetch_one(i: int) -> int: # coroutine function await asyncio.sleep(1) # non-blocking wait return i
async def main() -> None: results = await asyncio.gather(*(fetch_one(i) for i in range(5))) print(results) # [0, 1, 2, 3, 4] after ~1 s, not 5
asyncio.run(main()) # start the loop, run main, close the loopСоответствия с JavaScript, где async/await знаком большинству:
| JavaScript | Python |
|---|---|
async function |
async def |
Promise |
coroutine / Task |
await p |
await coro |
Promise.all([...]) |
asyncio.gather(*coros) |
Promise.allSettled |
asyncio.gather(*coros, return_exceptions=True) |
setTimeout (ожидание) |
await asyncio.sleep(s) |
AbortSignal.timeout |
async with asyncio.timeout(s): (3.11+) |
| p-limit | asyncio.Semaphore(n) |
Ключевое отличие: вызов fetch_one(1) ничего не запускает — он возвращает объект-корутину. Промис в JS начинает выполняться сразу, корутина — только когда её await-ят или оборачивают в задачу (asyncio.create_task, gather). Забытый await даёт предупреждение coroutine ... was never awaited и тихо пропущенную работу.
Блокирующие вызовы убивают параллельность. Внутри async def нельзя time.sleep(), requests.get(), тяжёлый синхронный расчёт: они останавливают весь цикл, как бесконечный while в однопоточном event loop. Для блокирующего кода — await asyncio.to_thread(func, *args).
Семафор ограничивает число одновременных запросов — без него gather на 1000 вызовов LLM мгновенно упрётся в rate limit провайдера. В примере client — HTTP-клиент из следующего раздела, а async with — асинхронный вариант with из гл. 1.1: вход в блок может ждать, пока освободится место:
sem = asyncio.Semaphore(5)
async def limited(client, url): async with sem: # at most 5 inside at once r = await client.get(url) return r.status_codeasyncio.TaskGroup (3.11+) — более строгая альтернатива gather: если одна задача падает, остальные отменяются. В новом коде и в официальных примерах встречаются оба варианта.
4. httpx — HTTP-клиент с async
Заголовок раздела «4. httpx — HTTP-клиент с async»Asyncio сам по себе в сеть не ходит: нужен HTTP-клиент, который умеет await. Популярная библиотека requests синхронная, поэтому в async-коде берём httpx:
import httpx
async def main() -> None: async with httpx.AsyncClient(timeout=10.0) as client: # reuse one client: connection pool r = await client.get("https://httpbin.org/get", params={"q": "mug"}) r.raise_for_status() # 4xx/5xx do NOT raise by default, only after this call data = r.json()httpx.Client — синхронная версия с тем же API. Официальные SDK Anthropic и OpenAI построены на httpx, поэтому таймауты, ретраи и ошибки там выглядят похоже. async with — асинхронный контекстный менеджер: клиент гарантированно закроет соединения.
5. Pydantic — валидация данных в рантайме
Заголовок раздела «5. Pydantic — валидация данных в рантайме»Запрос отправлен, JSON получен — теперь его надо проверить, прежде чем доверять. Pydantic v2 описывает данные классом с type hints и проверяет их в рантайме — то, чего обычные аннотации не делают (гл. 1.1). Ближайшие аналоги в других экосистемах — zod, Jackson с Bean Validation, struct tags с валидатором в Go.
from datetime import datetimefrom decimal import Decimalfrom typing import Literalfrom pydantic import BaseModel, Field, ValidationError, field_validator
class OrderItem(BaseModel): sku: str = Field(min_length=3, description="Product SKU, e.g. MUG-NORTH") unit: Literal["pcs", "g"] quantity: Decimal = Field(gt=0) unit_price: Decimal = Field(gt=0) created_at: datetime
@field_validator("sku") @classmethod def upper_sku(cls, v: str) -> str: return v.upper()
raw = '{"sku": "mug-north", "unit": "pcs", "quantity": "2", "unit_price": "890.00", "created_at": "2026-11-16T10:00:00Z"}'item = OrderItem.model_validate_json(raw) # parse + validate JSON stringprint(item.sku, item.quantity) # MUG-NORTH 2
try: OrderItem.model_validate({"sku": "x", "unit": "box", "quantity": -1, "unit_price": 1, "created_at": "nope"})except ValidationError as e: print(e.error_count(), e.errors()[0]["loc"]) # all errors at once, with field pathsКак читать пример. Класс наследует BaseModel, поля описаны аннотациями, Field(...) добавляет ограничения (gt=0 — строго больше нуля) и описание. @field_validator("sku") подключает к полю свою функцию: Pydantic вызывает её после проверки типа и подставляет вернувшееся значение (здесь — SKU в верхнем регистре); @classmethod под ним — форма из документации Pydantic: валидатор вызывается у класса, а не у готового объекта. model_validate_json принимает строку JSON, model_validate — уже разобранный словарь.
Что важно:
- Приведение типов. По умолчанию Pydantic работает в режиме «lax»: строку
"2"превратит вDecimal, ISO-строку — вdatetime. Это удобно для JSON. Если нужна строгость —model_config = ConfigDict(strict=True)илиField(strict=True). - Все ошибки сразу, с путём до поля (
loc) — это то, что потом можно отправить модели обратно: «исправь полеunit». - Сериализация:
item.model_dump()→ dict,item.model_dump_json()→ строка JSON. - Схема:
OrderItem.model_json_schema()→ dict в формате JSON Schema.
{ "properties": { "sku": {"description": "Product SKU, e.g. MUG-NORTH", "minLength": 3, "title": "Sku", "type": "string"}, "unit": {"enum": ["pcs", "g"], "title": "Unit", "type": "string"}, "quantity": {"anyOf": [{"exclusiveMinimum": 0, "type": "number"}, {"type": "string"}], "title": "Quantity"}, ... }, "required": ["sku", "unit", "quantity", "unit_price", "created_at"], "title": "OrderItem", "type": "object"}(Фрагмент примерный — точный вывод зависит от версии; стоит обратить внимание, как Decimal превращается в «число или строка».) Именно такую схему мы будем передавать LLM в structured output и как input_schema инструмента. description у полей попадает в схему — это фактически часть промпта.
@dataclass против BaseModel: dataclass — для внутренних структур, которым мы доверяем; Pydantic — на границе системы, где данные приходят извне (файл, API, ответ LLM).
6. pytest — тесты без церемоний
Заголовок раздела «6. pytest — тесты без церемоний»Модель описана — закрепляем её поведение тестами, чтобы правка валидатора не сломала то, что уже работало. Тесты лежат в tests/ рядом с кодом:
import pytestfrom pydantic import ValidationErrorfrom models import OrderItem # models.py in the project root, see pythonpath below
def test_sku_is_uppercased(): item = OrderItem.model_validate({"sku": "tea-sencha", "unit": "g", "quantity": "250", "unit_price": "420", "created_at": "2026-11-16T10:00:00Z"}) assert item.sku == "TEA-SENCHA"
@pytest.mark.parametrize("bad_unit", ["kg", "", "PCS "])def test_rejects_bad_unit(bad_unit): with pytest.raises(ValidationError): OrderItem.model_validate({"sku": "MUG-NORTH", "unit": bad_unit, "quantity": "1", "unit_price": "1", "created_at": "2026-11-16T10:00:00Z"})Файлы test_*.py, функции test_*, обычный assert (pytest сам покажет, что с чем сравнивалось). pytest.raises — ожидание исключения, parametrize — табличные тесты, fixtures — переиспользуемая подготовка (аргумент функции с именем фикстуры). Запуск: uv run pytest -q. Тесты не должны ходить в сеть — это правило для любого проекта.
uv init создаёт плоский проект: models.py лежит в корне, а не в пакете. Чтобы тесты из tests/ его импортировали, добавляем в pyproject.toml:
[tool.pytest.ini_options]pythonpath = ["."]7. Отладка в VS Code
Заголовок раздела «7. Отладка в VS Code»Когда тест падает и причина не видна из сообщения, удобнее остановить код и посмотреть на переменные. Нужно расширение Python (оно ставит Python Debugger на базе debugpy). Command Palette → «Python: Select Interpreter» → .venv проекта. После этого F5 на открытом файле запускает отладчик; breakpoint — клик слева от номера строки; панель Variables, Watch, Debug Console (можно выполнять выражения в текущем кадре). Тесты отлаживаются из панели Testing (иконка колбы) — «Debug Test». В коде можно поставить breakpoint() — встроенная функция, аналог debugger; в JS.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Основные модели курса — DeepSeek (облако, дешёвый, OpenAI-совместимый API, серверы в Китае) и Ollama (локальные модели на своём компьютере); Claude и OpenAI — для сравнения. Ключ DeepSeek и установка Ollama — в главе 1.5 · Видео Карпатого + ключи API; здесь — как инструменты этой главы ложатся на них. Факты сверены с документацией 13.11.2026.
httpx и SDK. Официальный пакет openai (тоже на httpx) работает и с DeepSeek, и с Ollama — меняется только base_url. У него есть асинхронный клиент AsyncOpenAI, у библиотеки ollama — AsyncClient. Правило «один клиент на всю работу» действует так же.
Семафор: у облака и у локальной модели разные причины.
- DeepSeek ограничивает число одновременных запросов на аккаунт (на ноябрь 2026 в документации: 2 500 для
deepseek-flash, 500 дляdeepseek-v4-pro), сверх — HTTP 429. Пока запрос ждёт, сервер шлёт пустые строки (или: keep-aliveв стриме — режиме, где ответ приходит по кусочкам, глава 2.7) — SDK их переваривает, самописный парсер должен уметь. - Ollama ограничена нашим железом: модель считается на GPU (или CPU) нашего компьютера, и десять параллельных запросов не ускорят работу в десять раз — они встанут в очередь или будут делить память. Для локальной модели семафор 1–2 — нормальная отправная точка; подбираем замером.
# illustrative: the same async pattern for both providersimport asyncio, osfrom openai import AsyncOpenAI
deepseek = AsyncOpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com")local = AsyncOpenAI(api_key="ollama", base_url="http://localhost:11434/v1") # key is ignored by Ollama
async def ask(client: AsyncOpenAI, model: str, sem: asyncio.Semaphore, q: str) -> str: async with sem: r = await client.chat.completions.create(model=model, messages=[{"role": "user", "content": q}]) return r.choices[0].message.content or ""
async def main() -> None: sem = asyncio.Semaphore(2) # local model: keep it small qs = [f"One word for the number {i}" for i in range(6)] answers = await asyncio.gather(*(ask(local, os.environ["OLLAMA_MODEL"], sem, q) for q in qs)) print(answers)Pydantic как схема ответа. Здесь провайдеры расходятся сильнее всего (подробно — глава 2.2 · Structured output: Pydantic + Instructor):
- Ollama принимает JSON Schema в параметре
formatи ограничивает вывод под неё:chat(model=..., messages=..., format=OrderItem.model_json_schema()), потомOrderItem.model_validate_json(reply.message.content). Через OpenAI-совместимый эндпоинт —response_format. Документация советуетtemperature=0для предсказуемости. - DeepSeek даёт JSON mode:
response_format={"type": "json_object"}гарантирует валидный JSON, но не нашу схему (о схеме документация JSON mode не говорит). Требования: слово «json» в промпте и пример нужного формата;contentможет прийти из одних пробелов — такой ответ обрабатываем и повторяем запрос. Значит, валидация Pydantic после ответа обязательна. - Claude / OpenAI умеют строгие структурированные ответы по схеме — сравним в неделе 2.
| Что нужно в этой главе | Claude | OpenAI | DeepSeek | Ollama |
|---|---|---|---|---|
| Async-клиент | AsyncAnthropic |
AsyncOpenAI |
AsyncOpenAI + base_url |
AsyncOpenAI + base_url или ollama.AsyncClient |
| Причина ставить семафор | rate limits | rate limits | лимит одновременных запросов на аккаунт, 429 | производительность компьютера |
| Ответ по Pydantic-схеме | structured outputs | structured outputs | только JSON mode без схемы → валидируем сами | format=<JSON Schema> |
| Данные клиентов, рабочий код | нет | нет | нет | да |
Что использовать здесь: все упражнения главы — без моделей. Необязательный бонус ниже — на локальной Ollama (если глава 1.5 уже пройдена): бесплатно и можно гонять сколько угодно.
Маршрут изучения
Заголовок раздела «Маршрут изучения»- uv → Getting started, затем Working on projects (≈25 мин). Обращаем внимание: структура проекта,
uv add/uv remove,uv run,uv sync, что такоеuv.lockи.python-version. - Ruff → разделы Linter и Formatter, Configuring Ruff (≈15 мин). Берём оттуда, как включить наборы правил через
select. - asyncio → страница Coroutines and Tasks: Coroutines, Awaitables, Creating Tasks, Task Groups, Sleeping, Running Tasks Concurrently (
gather), Timeouts (≈35 мин). Затем Synchronization Primitives → Semaphore (≈5 мин). - httpx → QuickStart и раздел Async Support (≈20 мин). Обращаем внимание на Client/AsyncClient, таймауты,
raise_for_status. - Pydantic → Concepts: Models, Fields, Validators, JSON Schema (≈45 мин). Главное:
model_validateпротивmodel_validate_json, lax/strict,Fieldс ограничениями,field_validator,model_json_schema. - pytest → Get Started и How-to про fixtures и parametrize (≈25 мин).
- VS Code: Python debugging → «Basic debugging» (≈10 мин).
- (≈15 мин) Как это выглядит у провайдеров: DeepSeek Rate Limit — лимит одновременных запросов и 429; DeepSeek JSON Output — что JSON mode гарантирует и чего нет; Ollama Structured Outputs — пример с Pydantic и
format. Выносим оттуда: семафор нужен и там и там, но по разным причинам; схему соблюдает Ollama, DeepSeek — только «валидный JSON».
На что обратить внимание
Заголовок раздела «На что обратить внимание»- Забытый
await. Сгенерированный код сclient.get(url)безawaitвнутриasync defне упадёт сразу — вместо ответа вернётся корутина, а ошибка вылезет где-то дальше. requestsиtime.sleepв async-коде. Модель любитrequestsпо привычке; внутриasync defэто сериализует все запросы. Ищем их при ревью.asyncio.runвнутри уже работающего цикла (в Jupyter, в FastAPI) падает сRuntimeError. В ноутбуке пишемawait main()прямо в ячейке (гл. 1.4).- Новый
AsyncClientна каждый запрос — теряем пул соединений, а на сотнях запросов упираемся в лимиты сокетов. Один клиент на всю работу. - Неограниченный
gatherпо сотням LLM-вызовов = мгновенный 429 от провайдера. Всегда семафор. gatherбезreturn_exceptions=Trueпробрасывает первую ошибку, но остальные задачи продолжают работать в фоне — их результаты потеряются.- Lax-режим Pydantic может «спасти» то, что должно было упасть:
"1"станет1,1в полеstrв v2 уже не пройдёт. Важно знать, где нужна строгость. - float для цены в модели — те же проблемы точности, что в гл. 1.1; для денег
Decimal. - Pydantic v1 против v2. Старый код и старые ответы моделей используют
.dict(),.json(),parse_obj,@validator. В v2 этоmodel_dump,model_dump_json,model_validate,@field_validator. Если встречается v1-стиль — проверяем, какая версия стоит.
Практика
Заголовок раздела «Практика»Проект — в exercises/learn-python/ учебного репозитория (гл. 1.2). Команды ниже запускаются из корня репозитория; uv должен быть установлен (раздел 1). Внутри существующего git-репозитория uv init не создаёт вложенный.
- Проект.
Смотрим
Окно терминала cd exercises && uv init learn-python && cd learn-pythonuv add httpx pydanticuv add --dev pytest ruffuv run python -c "import httpx, pydantic; print(pydantic.VERSION)"pyproject.tomlиuv.lock, убеждаемся, что.venv/в игноре (git status). - Async против последовательного.
fetch_compare.py: список из 5 публичных URL, каждый отвечает с задержкой 1 с (напримерhttps://httpbin.org/delay/1— публичный тестовый сервис). Две функции:sequential()с цикломawait client.get(...)иparallel()сasyncio.gather. Замер —time.perf_counter(). Ожидаемо: ~5 с против ~1 с. ДобавляемSemaphore(2)в параллельную версию и до запуска предсказываем время (≈3 с). Если хочется взять другие публичные API — только открытые эндпоинты, без ключей. - Модель «Позиция заказа».
models.pyсOrderItem(sku, unit, quantity, unit_price, created_at) как в теории, плюс одно своё правило (например, дляunit="pcs"количество должно быть целым — черезmodel_validator(mode="after")). - 10 JSON.
order_items.jsonl— 10 вымышленных строк, из них 4 невалидных разными способами (отрицательное количество,unit: "kg", битая дата, нет поля). Скриптvalidate.pyчитает построчно, печатает валидные и для невалидных — номер строки иe.errors()кратко (loc+msg). Ожидаемо: 6 OK, 4 с понятными причинами. - Схема.
print(json.dumps(OrderItem.model_json_schema(), indent=2)). Отвечаем вnotes/week-01.md: где в схеме описания полей, как выглядятLiteralиDecimal, что попало вrequired. Добавляемdescriptionк каждому полю и смотрим, как изменилась схема. - Тесты. Добавляем
pythonpath = ["."]вpyproject.toml(раздел 6).tests/test_models.py: минимум 3 теста — нормализация SKU,parametrizeпо невалидным значениям, своё правило изmodel_validator.uv run pytest -q— зелёные. - Ruff. Включаем
select = ["E", "F", "I", "B", "UP"], прогоняемuv run ruff check --fix .иuv run ruff format .. Специально пишем функцию сdef f(x=[])и убеждаемся, что Ruff её ловит. - Отладка. Выбираем интерпретатор
.venv, ставим breakpoint вvalidate.pyвнутриexcept, запускаем F5, смотримe.errors()в Variables и выполняемe.error_count()в Debug Console. Потом отлаживаем один тест из панели Testing. - (Бонус, после главы 1.5, когда Ollama установлена) Схема на локальной модели.
uv add "ollama==<version>"(версию берём с PyPI на сегодня — фиксируем). Скриптextract.py: даём локальной модели одно выдуманное предложение («Вчера в 14:00 заказали 250 г чая TEA-SENCHA по 420 за 100 г») и просимOrderItemчерезformat=OrderItem.model_json_schema()иtemperature=0(в библиотекеollama— черезoptions={"temperature": 0}); результат — черезOrderItem.model_validate_json. Затем оборачиваем 5 таких вызовов вgatherсSemaphore(1)иSemaphore(3)и сравниваем время: на локальной модели выигрыш от параллельности заметно меньше, чем с httpbin. Только выдуманные строки. - (Для сравнения, по желанию) Тот же
extract.pyна DeepSeek сresponse_format={"type": "json_object"}: пишем в промпте слово «json» и пример объекта, проверяем, что Pydantic ловит ответ, если модель выдала не тот формат. Только выдуманные данные — данные клиентов и рабочий код в облачные модели не отправляем.
Готово, когда
Заголовок раздела «Готово, когда»- Проект запускается через
uv run,uv.lockзакоммичен,.venvнет в git. - Замер показывает выигрыш параллельной версии, и мы можем объяснить, откуда берутся цифры с семафором.
-
validate.pyнаходит все 4 невалидные строки,uv run pytestзелёный,uv run ruff check .без ошибок. - Мы видели
model_json_schema()своей модели и понимаем, как поля превращаются в схему.
Тест для самопроверки
Заголовок раздела «Тест для самопроверки»Вопрос 1
Что делает uv run pytest, если зависимости в pyproject.toml только что поменялись?
Перед запуском uv сверяет окружение с pyproject.toml/lock, при необходимости обновляет lock и .venv, затем запускает pytest внутри окружения. Активировать .venv вручную не нужно.
Вопрос 2
Что выведет этот код и почему?
async def get_price(): return 100
async def main(): p = get_price() print(p + 1)asyncio.run(main())TypeError: get_price() без await возвращает объект-корутину, а не 100; к ней нельзя прибавить 1. Плюс предупреждение «coroutine was never awaited».
Вопрос 3
Чем asyncio.gather отличается от Promise.all в JavaScript в момент вызова корутин-функций?
В JS промис выполняется сразу после вызова функции; в Python вызов возвращает «спящую» корутину, и выполнение начинается, только когда gather оборачивает её в задачу и цикл её запускает.
Вопрос 4
Что не так в этом коде?
async def fetch_all(urls): async with httpx.AsyncClient() as client: for url in urls: r = requests.get(url) time.sleep(0.5)Внутри async-функции синхронные requests.get и time.sleep блокируют event loop, всё идёт последовательно, а созданный AsyncClient не используется. Нужно await client.get(url) и await asyncio.sleep(0.5), а для параллельности — gather.
Вопрос 5
Зачем нужен asyncio.Semaphore при массовых вызовах LLM API?
Чтобы ограничить число одновременных запросов и не получить 429/бан за превышение rate limit (и не сжечь бюджет всплеском).
Вопрос 6
Какой метод Pydantic v2 парсит JSON-строку и валидирует её за один шаг?
b. parse_raw — API Pydantic v1; model_dump_json — сериализация в обратную сторону.
Вопрос 7
Поле объявлено quantity: Decimal = Field(gt=0). Пройдёт ли валидацию {"quantity": "0.5"} в режиме по умолчанию? А {"quantity": "0"}?
"0.5" пройдёт: lax-режим приведёт строку к Decimal. "0" не пройдёт: ограничение gt=0 требует строго больше нуля.
Вопрос 8
Для чего AI-инженеру model_json_schema()?
Это JSON Schema, которую передают модели для structured output и как описание параметров инструмента в tool calling; модель видит типы, ограничения и description полей.
Вопрос 9
Когда выбрать @dataclass, а когда BaseModel?
dataclass — для внутренних данных, которым доверяем (без рантайм-валидации). BaseModel — на границе, где данные приходят извне: файлы, HTTP, ответы LLM.
Вопрос 10
Как в pytest проверить, что функция бросает исключение?
with pytest.raises(ValidationError): ... — тест упадёт, если исключение нужного типа не брошено.
Вопрос 11
Почему asyncio.run(main()) в ячейке Jupyter падает?
В Jupyter event loop уже запущен, а asyncio.run пытается запустить новый — RuntimeError. В ноутбуке пишут await main() прямо в ячейке.
Вопрос 12
Что такое uv.lock и чем он отличается от зависимостей в pyproject.toml?
pyproject.toml задаёт диапазоны (httpx>=0.28), uv.lock фиксирует точные разрешённые версии всех пакетов, включая транзитивные, — это делает установку воспроизводимой. Коммитятся оба.
Вопрос 13
Зачем семафор при массовых запросах к DeepSeek и зачем — к локальной Ollama? Причина одна и та же?
Нет. У DeepSeek — лимит одновременных запросов на аккаунт (сверх — 429) и бюджет. У Ollama лимит — железо компьютера: параллельные запросы делят GPU и память, и большой параллелизм лишь замедляет каждый ответ.
Вопрос 14
DeepSeek вызван с response_format={"type": "json_object"}, и сразу следом написано OrderItem(**json.loads(content)) без try. Что может пойти не так?
JSON mode гарантирует валидный JSON, но не нужную схему: поля могут называться иначе или отсутствовать, а content по документации может прийти из одних пробелов — тогда упадёт уже json.loads. Нужно OrderItem.model_validate_json(content) в try с обработкой ValidationError и повтором.
Вопрос 15
Какой провайдер из четырёх ограничит вывод модели JSON Schema от OrderItem.model_json_schema() через параметр format?
b. Ollama принимает JSON Schema в format. В документации DeepSeek описан только json_object без схемы; у Claude и OpenAI свои механизмы structured output, не параметр format.
Что дальше
Заголовок раздела «Что дальше»Глава 1.4 · Jupyter-ноутбуки в VS Code: подключим .venv этого проекта как ядро ноутбука и повторим упражнение интерактивно. Потом глава 1.5 · Видео Карпатого + ключи API — интуиция об LLM, ключ DeepSeek и установка Ollama, после чего первый настоящий вызов модели.