Инструменты
Python для GoJava-разработчика без вводного курса: соответствия конструкций, окружение на uv, pydantic, async. Затем рабочие инструменты AI engineer: FastAPI-обёртка со стримингом, pandas для анализа evals, SDK провайдеров, фреймворки (LangChain, LlamaIndex, LangGraph) с честным разбором минусов и Jupyter — где он уместен, а где нет. В конце — coding agents (Claude Code, Codex CLI, Cursor и другие): как работать с ними циклом «план → тесты → код → ревью», что писать в AGENTS.md и CLAUDE.md, как ограничить права и где они ошибаются.
Зачем Go-разработчику Python
Зачем Java-разработчику Python
Прод-сервис вокруг LLM вполне можно и стоит писать на Java: вызов API, стриминг, лимиты, очереди, RAG на pgvector — всё это Spring Boot делает отлично, у Anthropic есть официальный Java SDK, а поверх него — LangChain4j и Spring AI. Но часть работы живёт в Python, и притворяться, что её нет, дорого:
Прод-сервис вокруг LLM вполне можно и стоит писать на Go: вызов API, стриминг, лимиты, очереди, RAG на pgvector — всё это Go делает отлично, и у Anthropic есть официальный Go SDK. Но часть работы живёт в Python, и притворяться, что её нет, дорого:
- Evals и анализ. Разметка, подсчёт метрик по срезам, сравнение версий промпта, графики. pandas и Jupyter здесь стандарт, и коллеги-ML будут ждать от вас
.ipynbили скрипт, а не Go-бинарьjar. - Новые возможности приходят в Python SDK первыми или одновременно с TypeScript; примеры в документации провайдеров — в первую очередь на Python.
- Экосистема: fine-tuning (модуль 10), эмбеддинг-модели и rerankers локально, парсинг PDF, токенизаторы, фреймворки для агентов и RAG.
- Собеседования. Live-coding на AI engineer часто на Python: «напиши функцию, которая вызывает модель и парсит ответ», «посчитай precision по этому DataFrame».
Рабочая стратегия: сервисы на Java и Spring, эксперименты, evals и анализ — на Python. Цель модуля — чтобы Python не тормозил вас: читать чужой код, писать скрипты и небольшие сервисы идиоматично, знать, где подвохи для человека с Java-рефлексами: типы, которые никто не проверяет без тайпчекера, исключения без throws в сигнатуре, кооперативная конкурентность вместо потоков и упаковка без Maven.
Рабочая стратегия: сервисы на Go, эксперименты, evals и анализ — на Python. Цель модуля — чтобы Python не тормозил вас: читать чужой код, писать скрипты и небольшие сервисы идиоматично, знать, где подвохи для человека с Go-рефлексами.
Шпаргалка Go ↔ Python
Шпаргалка Java ↔ Python
Фильтруйте по разделу или ищите по слову. Колонка «Подвох» — то, на чём спотыкаются именно GoJava-разработчики.
Переводчик GoJava → Python
Поиск работает по всем колонкам: попробуйте «error», «struct», «канал», «venv».
Поиск работает по всем колонкам: попробуйте «exception», «record», «stream», «venv».
| Раздел | GoJava | Python | Подвох |
|---|
Модули и пакеты
В Java пакет — это пространство имён и директория, а единица сборки — Maven-модуль с pom.xml. В Python модуль = файл .py, пакет = директория (исторически с __init__.py), а «проект» описывается pyproject.toml. Импорты разрешаются через sys.path — аналог classpath, но собранный в рантайме из текущей директории и окружения, а не из описания сборки. Отсюда классическая боль — «работает из корня, не работает из поддиректории». Лечится src-layout и установкой проекта в окружение в editable-режиме (uv делает это сам). Модификаторов доступа нет: _name — «приватное» по соглашению, но доступно всем, и компилятор этого не проверит.
В Go пакет = директория, модуль = go.mod. В Python модуль = файл .py, пакет = директория (исторически с __init__.py), а «проект» описывается pyproject.toml. Импорты разрешаются через sys.path, а не через путь модуля, поэтому классическая боль — «работает из корня, не работает из поддиректории». Лечится src-layout и установкой проекта в окружение в editable-режиме (uv делает это сам). Экспорт не по регистру, а по соглашению: _name — «приватное», но доступно всем.
Типы и dataclass vs struct
Типы: dataclass и pydantic vs records
Python динамически типизирован: аннотации (def f(x: int) -> str) интерпретатор не проверяет, они ближе к Javadoc, чем к javac. Проверку делает внешний тайпчекер (mypy, pyright), и в команде с Java-культурой его стоит включить в CI в строгом режиме, иначе ошибки, которые у вас ловит компилятор, всплывут в рантайме. @dataclass(frozen=True) — ближайший аналог record: поля, конструктор, __eq__, __hash__, __repr__. Но он не валидирует значения: User(age="десять") создастся спокойно, а в Java не скомпилировалось бы. Для данных с границы доверия (JSON от клиента, ответ модели) нужен pydantic: это record, Jackson и Bean Validation в одной библиотеке.
Python динамически типизирован, аннотации (def f(x: int) -> str) интерпретатор не проверяет. Проверку делает внешний тайпчекер (mypy, pyright), и в команде с Go-культурой его стоит включить в CI со строгим режимом. @dataclass — ближайший аналог struct: поля, конструктор, сравнение. Но он не валидирует значения: User(age="десять") создастся спокойно. Для данных с границы доверия (JSON от клиента, ответ модели) нужен pydantic.
Исключения vs error
Исключения: все unchecked
Исключения Java-разработчику привычны, но в Python все они unchecked: сигнатура функции не говорит, что она бросает, и никто не заставит обработать ошибку. Правила те же, что в хорошем Java-коде: ловить конкретные классы (except anthropic.RateLimitError), а не голый except Exception; не глотать молча; оборачивать с сохранением причины: raise MyError("…") from e (аналог new MyException("…", e), цепочка видна в traceback, как Caused by:). Ресурсы закрывает with (context manager) — это try-with-resources, а __enter__/__exit__ — аналог AutoCloseable.
Нет if err != nil, ошибки летят исключениями вверх по стеку, пока их не поймают. Для Go-разработчика это ощущается как паника, которую принято ловить. Правила хорошего тона: ловить конкретные классы (except anthropic.RateLimitError), а не голый except Exception; не глотать молча; оборачивать с сохранением причины: raise MyError("…") from e (аналог fmt.Errorf("…: %w", err), цепочка видна в traceback). Ресурсы закрывает with (context manager) — это defer, привязанный к блоку, а не к функции.
Окружение: uv, venv, pyproject
Главная боль Python для Java-разработчика — окружения. В Maven зависимости лежат в общем ~/.m2, но classpath у каждого проекта свой и собирается из pom.xml, а результат — самодостаточный jar. В Python пакеты ставятся в интерпретатор, поэтому на каждый проект нужен venv (virtual environment — отдельная директория с копией/ссылкой на интерпретатор и своими site-packages). Исторический зоопарк: pip, virtualenv, pip-tools, poetry, pipenv, conda, pyenv. В 2026 году разумный дефолт один — uv от Astral: менеджер версий Python (как SDKMAN), venv, зависимостей и lock-файла (как Maven) в одном быстром бинаре (он написан на Rust).
Главная боль Python для гоферов — окружения. В Go зависимости изолированы модулем и собираются в бинарь. В Python пакеты ставятся в интерпретатор, поэтому на каждый проект нужен venv (virtual environment — отдельная директория с копией/ссылкой на интерпретатор и своими site-packages). Исторический зоопарк: pip, virtualenv, pip-tools, poetry, pipenv, conda, pyenv. В 2026 году разумный дефолт один — uv от Astral: менеджер версий Python, venv, зависимостей и lock-файла в одном быстром бинаре (он написан на Rust).
| Java / Maven | uv | Что делает |
|---|---|---|
Spring Initializr, mvn archetype:generate | uv init | Создаёт pyproject.toml, .python-version |
<dependency> в pom.xml | uv add anthropic pandas | Добавляет зависимость, обновляет uv.lock, ставит в .venv |
BOM и dependencyManagement (у Gradle — gradle.lockfile) | uv.lock | Точные версии и хеши; коммитить |
mvn exec:java, java -jar | uv run python main.py | Запуск в окружении проекта, синхронизирует его перед запуском |
SDKMAN, JAVA_HOME | uv python install 3.13 | Ставит нужную версию интерпретатора |
jbang app install | uv tool install ruff / uvx ruff | CLI-инструменты в изолированных окружениях |
Spotless, Checkstyle, javac + Error Prone | ruff format, ruff check, mypy/pyright | Форматирование, линтер, типы |
| Go | uv | Что делает |
|---|---|---|
go mod init | uv init | Создаёт pyproject.toml, .python-version |
go get pkg | uv add anthropic pandas | Добавляет зависимость, обновляет uv.lock, ставит в .venv |
go.sum | uv.lock | Точные версии и хеши; коммитить |
go run . | uv run python main.py | Запуск в окружении проекта, синхронизирует его перед запуском |
| — | uv python install 3.13 | Ставит нужную версию интерпретатора |
go install tool@v | uv tool install ruff / uvx ruff | CLI-инструменты в изолированных окружениях |
gofmt, go vet | ruff format, ruff check, mypy/pyright | Форматирование, линтер, типы |
Для одноразового скрипта (прогнать eval, выгрузить данные) проект не нужен. Зависимости объявляются в комментарии в начале файла, и uv run script.py сам создаст временное окружение. Это ближе всего к опыту «один файл → go runJBang с //DEPS в шапке».
async/await vs goroutines
async/await vs virtual threads
Самое важное концептуальное различие. В Java 21 конкурентность снова прозрачна: любой блокирующий код можно запустить в виртуальном потоке, JVM сама снимает его с потока-носителя на блокирующем I/O, а потоки реально работают параллельно на всех ядрах. Ближайший Java-аналог asyncio — реактивный стек (WebFlux, Reactor): Mono/Flux так же «заражают» сигнатуры, а блокирующий вызов внутри так же ломает всё. В Python asyncio — кооперативная многозадачность в одном потоке:
Самое важное концептуальное различие. В Go конкурентность прозрачна: любая функция может быть запущена в горутине, рантайм сам переключает их на блокирующих вызовах, горутины реально работают параллельно на всех ядрах. В Python asyncio — кооперативная многозадачность в одном потоке:
- Функция должна быть объявлена
async def, и вызывать её нужно черезawait. Это «окрашивание функций»: async заражает весь стек вызовов вверх. - Переключение происходит только в точках
await. Синхронный блокирующий вызов внутри корутины (requests.get,time.sleep, синхронный клиент БД, тяжёлый CPU-код) останавливает весь event loop: все остальные запросы сервиса ждут. Это самая частая прод-ошибка в FastAPI-сервисах. - Параллелизма на CPU нет из-за GIL (Global Interpreter Lock). Для I/O-bound LLM-вызовов это не важно: тысячи одновременных запросов в одном потоке — нормально. Для CPU-задач — процессы (
ProcessPoolExecutor) или нативные библиотеки (numpy, pandas отпускают GIL внутри). Free-threaded сборка CPython без GIL существует с 3.13, но экосистема к ней ещё привыкает. - Вместо
BlockingQueue—asyncio.Queue. Отмена —task.cancel(), которое бросаетCancelledErrorв точкеawait(похоже наinterrupt()иInterruptedException, только срабатывает исключительно наawait); таймаут —asyncio.timeout(). - Каналов как примитива языка нет; есть
asyncio.Queue. Отмена — не context, аtask.cancel(), которое бросаетCancelledErrorв точкеawait; таймаут —asyncio.timeout().
import asyncio
import anthropic
client = anthropic.AsyncAnthropic() # у SDK есть sync и async клиенты — в async-коде только async
async def classify(ticket: str) -> str:
resp = await client.messages.create(
model="claude-haiku-4-5",
max_tokens=16,
system="Ответь одним словом: billing, tech, refund или other.",
messages=[{"role": "user", "content": ticket}],
)
return resp.content[0].text.strip()
async def main(tickets: list[str]) -> list[str | BaseException]:
sem = asyncio.Semaphore(10)
async def guarded(t: str) -> str:
async with sem:
return await classify(t)
# gather(return_exceptions=True) ≈ invokeAll без fail-fast: ошибки возвращаются значениями
return await asyncio.gather(*(guarded(t) for t in tickets), return_exceptions=True)
# Ошибка, которую ловят на ревью:
async def bad_handler() -> None:
import time
time.sleep(2) # блокирует ВЕСЬ event loop на 2 с
await asyncio.sleep(2) # правильно: отдаёт управление другим задачам
# синхронную библиотеку без async-версии — в поток:
# result = await asyncio.to_thread(sync_call, arg)
if __name__ == "__main__":
print(asyncio.run(main(["Не прошла оплата картой", "Приложение падает при входе"])))
import asyncio
import anthropic
client = anthropic.AsyncAnthropic() # у SDK есть sync и async клиенты — в async-коде только async
async def classify(ticket: str) -> str:
resp = await client.messages.create(
model="claude-haiku-4-5",
max_tokens=16,
system="Ответь одним словом: billing, tech, refund или other.",
messages=[{"role": "user", "content": ticket}],
)
return resp.content[0].text.strip()
async def main(tickets: list[str]) -> list[str | BaseException]:
sem = asyncio.Semaphore(10)
async def guarded(t: str) -> str:
async with sem:
return await classify(t)
# gather(return_exceptions=True) ≈ errgroup без fail-fast: ошибки возвращаются значениями
return await asyncio.gather(*(guarded(t) for t in tickets), return_exceptions=True)
# Ошибка, которую ловят на ревью:
async def bad_handler() -> None:
import time
time.sleep(2) # блокирует ВЕСЬ event loop на 2 с
await asyncio.sleep(2) # правильно: отдаёт управление другим задачам
# синхронную библиотеку без async-версии — в поток:
# result = await asyncio.to_thread(sync_call, arg)
if __name__ == "__main__":
print(asyncio.run(main(["Не прошла оплата картой", "Приложение падает при входе"])))
// То же на Java 21: никакой «окраски» функций, блокирующий вызов блокирует только свой виртуальный поток.
record Outcome(String label, Throwable error) {}
static List<Outcome> classifyAll(AnthropicClient client, List<String> tickets) {
var sem = new Semaphore(10);
List<Future<String>> futures;
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) { // close() ждёт все задачи
futures = tickets.stream().map(t -> exec.submit(() -> {
sem.acquire();
try {
Message resp = client.messages().create(MessageCreateParams.builder()
.model("claude-haiku-4-5")
.maxTokens(16)
.system("Ответь одним словом: billing, tech, refund или other.")
.addUserMessage(t)
.build());
return resp.content().stream()
.flatMap(b -> b.text().stream())
.map(tb -> tb.text().strip())
.collect(Collectors.joining());
} finally {
sem.release();
}
})).toList();
}
// Без fail-fast, как gather(return_exceptions=True): ошибка — значение в списке.
return futures.stream()
.map(f -> f.state() == Future.State.SUCCESS
? new Outcome(f.resultNow(), null)
: new Outcome(null, f.exceptionNow()))
.toList();
}
// То же на Go: никакой «окраски» функций, блокирующий вызов блокирует только свою горутину.
func classifyAll(ctx context.Context, client *anthropic.Client, tickets []string) ([]string, []error) {
out := make([]string, len(tickets))
errs := make([]error, len(tickets))
var g errgroup.Group
g.SetLimit(10)
for i, t := range tickets {
g.Go(func() error {
resp, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: "claude-haiku-4-5",
MaxTokens: 16,
System: []anthropic.TextBlockParam{{Text: "Ответь одним словом: billing, tech, refund или other."}},
Messages: []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(t))},
})
if err != nil {
errs[i] = err // без fail-fast, как gather(return_exceptions=True)
return nil
}
for _, b := range resp.Content {
if tb, ok := b.AsAny().(anthropic.TextBlock); ok {
out[i] = strings.TrimSpace(tb.Text)
}
}
return nil
})
}
_ = g.Wait()
return out, errs
}
asyncio.TaskGroup (3.11+) ≈ StructuredTaskScope.ShutdownOnFailure: первая ошибка отменяет остальные задачи, выход из блока ждёт всех. asyncio.gather(..., return_exceptions=True) ≈ invokeAll с разбором каждого Future: ошибки — значения. asyncio.Semaphore ≈ java.util.concurrent.Semaphore. asyncio.timeout(s) ≈ future.get(timeout) или orTimeout у CompletableFuture, только область действия — блок async with. asyncio.to_thread ≈ отдать блокирующий вызов в executor. Главное отличие: на виртуальных потоках блокирующий вызов безопасен, в asyncio — нет.
asyncio.TaskGroup (3.11+) ≈ errgroup.WithContext: первая ошибка отменяет остальные задачи, выход из блока ждёт всех. asyncio.gather(..., return_exceptions=True) ≈ errgroup, где горутины пишут ошибку в срез и возвращают nil. asyncio.Semaphore ≈ semaphore.Weighted с весом 1. asyncio.timeout(s) ≈ context.WithTimeout, только область действия — блок async with, а не переданный вниз объект.
Pydantic: типы на границе доверия
Pydantic — библиотека моделей данных с валидацией в рантайме. Для AI engineering это центральный инструмент: FastAPI строит на нём валидацию запросов и OpenAPI, а для LLM он делает три вещи — описывает схему ожидаемого ответа, генерирует из неё JSON Schema для structured outputs и валидирует то, что модель вернула. В Go ближайший аналог — struct с тегами json + validate плюс генерация схемы через invopop/jsonschema; в Python это одна библиотека.В Java ближайший аналог — record + Jackson + Bean Validation (@Size, @Min) плюс генерация схемы через victools jsonschema-generator; в Python это одна библиотека.
from typing import Literal
import anthropic
from pydantic import BaseModel, Field, ValidationError
class TicketTriage(BaseModel):
category: Literal["billing", "tech", "refund", "other"]
urgency: int = Field(ge=1, le=5, description="1 — не срочно, 5 — всё горит")
summary: str = Field(max_length=200)
needs_human: bool
client = anthropic.Anthropic()
def triage(ticket: str) -> TicketTriage:
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": f"Разбери тикет поддержки:\n\n{ticket}"}],
# structured outputs: модель обязана вернуть JSON по схеме.
# В старых версиях SDK параметр передаётся через extra_body={"output_config": ...}
output_config={"format": {"type": "json_schema", "schema": TicketTriage.model_json_schema()}},
)
if resp.stop_reason == "max_tokens":
raise RuntimeError("ответ обрезан, JSON неполный")
raw = "".join(b.text for b in resp.content if b.type == "text")
try:
# схему гарантирует API, но бизнес-ограничения (ge/le, max_length) проверяем сами
return TicketTriage.model_validate_json(raw)
except ValidationError as e:
raise ValueError(f"модель вернула невалидный triage: {e.error_count()} ошибок") from e
- Не все ограничения JSON Schema поддерживаются провайдером в structured outputs (часто не поддерживаются числовые границы, длины строк, сложные паттерны). Схему, которую сгенерировал pydantic, иногда надо упростить, а проверку ограничений оставить на валидацию после ответа.
descriptionу полей уходит в схему и работает как часть промпта. Это полезно, но значит, что правка docstring меняет поведение модели — версионируйте модели данных, как промпты.- Pydantic v1 и v2 несовместимы по API (
.dict()→.model_dump(),parse_raw→model_validate_json). В старых туториалах и библиотеках встречается v1.
Скрипты для экспериментов
Типичный скрипт AI engineer: прочитать набор примеров, прогнать через модель конкурентно, сохранить результаты с метаданными (версия промпта, модель, токены, латентность) для анализа в pandas. Пример ниже самодостаточен: зависимости в PEP 723-заголовке, запуск uv run run_eval.py --prompt v3 cases.jsonl.
# /// script
# requires-python = ">=3.12"
# dependencies = ["anthropic>=0.60", "pandas>=2.2", "pyarrow"]
# ///
import argparse, asyncio, json, time
from pathlib import Path
import anthropic
import pandas as pd
PROMPTS = {
"v2": "Классифицируй тикет. Ответь одним словом: billing, tech, refund, smalltalk.",
"v3": Path("prompts/classify_v3.txt").read_text() if Path("prompts/classify_v3.txt").exists() else "",
}
client = anthropic.AsyncAnthropic(max_retries=4) # ретраи 429/5xx с backoff — встроены в SDK
async def run_case(sem: asyncio.Semaphore, version: str, case: dict) -> dict:
async with sem:
t0 = time.perf_counter()
try:
resp = await client.messages.create(
model="claude-haiku-4-5", max_tokens=16, system=PROMPTS[version],
messages=[{"role": "user", "content": case["text"]}],
)
got = resp.content[0].text.strip().lower()
usage = (resp.usage.input_tokens, resp.usage.output_tokens)
err = None
except anthropic.APIError as e: # не роняем весь прогон из-за одного примера
got, usage, err = None, (0, 0), type(e).__name__
return {
"example_id": case["id"], "category": case["category"], "prompt_version": version,
"expected": case["label"], "got": got, "passed": int(got == case["label"]),
"latency_ms": round((time.perf_counter() - t0) * 1000),
"input_tokens": usage[0], "output_tokens": usage[1], "error": err,
}
async def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("cases", type=Path)
ap.add_argument("--prompt", required=True, choices=PROMPTS)
ap.add_argument("--concurrency", type=int, default=8)
args = ap.parse_args()
cases = [json.loads(line) for line in args.cases.read_text().splitlines() if line.strip()]
sem = asyncio.Semaphore(args.concurrency)
rows = await asyncio.gather(*(run_case(sem, args.prompt, c) for c in cases))
df = pd.DataFrame(rows)
df.to_parquet(f"results_{args.prompt}_{int(time.time())}.parquet")
ok = df[df.error.isna()]
print(f"pass_rate={ok.passed.mean():.3f} n={len(ok)} errors={df.error.notna().sum()}")
if __name__ == "__main__":
asyncio.run(main())
Обратите внимание: ошибки инфраструктуры (429 после всех ретраев) записываются отдельно и не считаются провалом модели. Смешать их — классическая ошибка, из-за которой «новый промпт стал хуже» в день, когда у провайдера была перегрузка.
FastAPI: сервис-обёртка над LLM
FastAPI — де-факто стандарт для Python-сервисов вокруг моделей: async из коробки, валидация через pydantic, автоматическая OpenAPI-документация. Для Go-разработчика: это net/http + роутер + encoding/json + валидатор + swagger-генератор в одном, с объявлением через аннотации типов. Запускается ASGI-сервером (uvicorn) — это аналог http.Server.Для Java-разработчика: это Spring WebFlux + Jackson + Bean Validation + springdoc-openapi в одном, с маршрутами через декораторы (как @PostMapping) и валидацией по аннотациям типов вместо DI-контейнера и бинов. Запускается ASGI-сервером (uvicorn) — это аналог встроенного Netty или Tomcat.
import json
import anthropic
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
app = FastAPI()
client = anthropic.AsyncAnthropic() # один клиент на процесс: пул соединений переиспользуется
class AskRequest(BaseModel):
question: str = Field(min_length=1, max_length=8000) # граница доверия: режем заранее
max_tokens: int = Field(default=1024, ge=1, le=4096)
def sse(event: str, data: dict) -> str:
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
@app.post("/v1/ask/stream")
async def ask_stream(req: AskRequest, request: Request) -> StreamingResponse:
async def events():
try:
async with client.messages.stream(
model="claude-opus-5",
max_tokens=req.max_tokens,
system="Ты ассистент поддержки. Отвечай кратко.",
messages=[{"role": "user", "content": req.question}],
) as stream:
async for text in stream.text_stream:
if await request.is_disconnected():
return # выход из async with закрывает соединение с провайдером
yield sse("delta", {"text": text})
msg = await stream.get_final_message()
yield sse("done", {
"stop_reason": msg.stop_reason,
"input_tokens": msg.usage.input_tokens,
"output_tokens": msg.usage.output_tokens,
})
except anthropic.APIStatusError as e: # в т.ч. ошибка, пришедшая посреди стрима
yield sse("error", {"status": e.status_code})
return StreamingResponse(events(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
@app.get("/healthz")
async def healthz() -> dict:
return {"ok": True}
# uv run uvicorn app:app --host 0.0.0.0 --port 8000 --workers 2
- Синхронный вызов в async-хендлере.
anthropic.Anthropic()(sync) внутриasync defблокирует event loop: один медленный запрос — и весь воркер стоит. Либо async-клиент, либо хендлер объявить обычнымdef(FastAPI выполнит его в пуле потоков). - Воркеры. Один процесс uvicorn = один event loop = одно ядро. Для CPU-части (токенизация, парсинг) нужны несколько воркеров. Для чистого I/O одного процесса хватает на сотни одновременных стримов.
- Ошибка после 200. Статус и заголовки уже отправлены, когда начался стрим. Ошибку можно передать только событием внутри потока — клиент должен это обрабатывать.
- Клиент на каждый запрос. Создание
AsyncAnthropic()в хендлере — новый пул соединений и TLS-рукопожатие каждый раз. Клиент — синглтон, какhttp.Clientв GoбинAnthropicClientилиHttpClientв Spring.
pandas для анализа evals
pandas — таблицы в памяти (DataFrame) с векторными операциями. Для Go-разработчика это «SQL поверх слайса структур без циклов»:Для Java-разработчика это «SQL поверх List<record> без циклов», как Stream API с Collectors.groupingBy, только короче и векторно: groupby — GROUP BY, merge — JOIN, pivot_table — сводная таблица, query — WHERE. Главная ментальная перестройка: не писать циклы по строкам (iterrows медленный и неидиоматичный), а оперировать колонками целиком.
Ниже — 72 результата eval-прогона классификатора тикетов: три версии промпта × четыре категории × 6 примеров. Сгруппируйте по-разному и посмотрите, как меняются выводы. Усы на графике pass rate — 95% доверительный интервал Уилсона.
Мини-анализ evals: как это выглядит в pandas
Выберите группировку и метрику. Справа внизу — эквивалентный pandas-код и то, что он напечатает.
Что стоит заметить на этих данных. По версиям v3 выглядит лучше всех, но в разрезе «версия × категория» видно, что v3 просела на billing (3 из 6 против 5 и 4 у v1 и v2) — при n = 6 на ячейку это может быть и шум, и реальная регрессия, интервалы огромные. v3 также медленнее и расходует больше выходных токенов. Решение «катим v3» требует больше данных по billing и оценки стоимости, а не одного числа в сводке. Именно это — работа AI engineer с evals. Здесь pandas — инструмент; методология целиком (golden dataset, LLM-as-a-judge, размер выборки, quality gate в CI) — в модуле 8.
import pandas as pd
df = pd.concat(pd.read_parquet(p) for p in ["results_v1.parquet", "results_v2.parquet", "results_v3.parquet"])
df = df[df.error.isna()] # инфраструктурные ошибки — отдельно, не в качестве
# 1. Сводка по версиям: качество, латентность, стоимость
PRICE_IN, PRICE_OUT = 1 / 1e6, 5 / 1e6 # Haiku 4.5, $ за токен (на момент написания)
df["cost_usd"] = df.input_tokens * PRICE_IN + df.output_tokens * PRICE_OUT
summary = df.groupby("prompt_version").agg(
pass_rate=("passed", "mean"),
n=("passed", "size"),
p50_ms=("latency_ms", "median"),
p95_ms=("latency_ms", lambda s: s.quantile(0.95)),
cost_per_1k=("cost_usd", lambda s: s.mean() * 1000),
)
# 2. Срезы: где именно выросло или упало
pivot = df.pivot_table(index="category", columns="prompt_version",
values="passed", aggfunc="mean").round(2)
pivot["delta_v3_v2"] = pivot["v3"] - pivot["v2"]
# 3. Парное сравнение на одних и тех же примерах: смотрим только расхождения
wide = df.pivot_table(index="example_id", columns="prompt_version", values="passed")
fixed = ((wide.v2 == 0) & (wide.v3 == 1)).sum()
broken = ((wide.v2 == 1) & (wide.v3 == 0)).sum()
print(f"v3 починил {fixed}, сломал {broken}") # для McNemar важны только эти два числа
# 4. Провалы глазами: самое полезное действие в анализе evals
print(df.query("prompt_version == 'v3' and passed == 0")[["example_id", "category", "expected", "got"]])
df.groupby("v").agg(...) = SELECT v, avg(passed), count(*) … GROUP BY v = Collectors.groupingBy(Row::v, Collectors.averagingInt(Row::passed)). df.merge(other, on="example_id", how="left") = LEFT JOIN … USING (example_id). pivot_table = GROUP BY category, version, развёрнутый в колонки. Если привычнее SQL, есть DuckDB: duckdb.sql("select … from df") работает прямо по DataFrame. Для больших данных (десятки миллионов строк) смотрят в сторону Polars — колоночный движок на Rust с ленивым API, похожим на Stream API.
df.groupby("v").agg(...) = SELECT v, avg(passed), count(*) … GROUP BY v. df.merge(other, on="example_id", how="left") = LEFT JOIN … USING (example_id). pivot_table = GROUP BY category, version, развёрнутый в колонки. Если привычнее SQL, есть DuckDB: duckdb.sql("select … from df") работает прямо по DataFrame. Для больших данных (десятки миллионов строк) смотрят в сторону Polars — колоночный движок на Rust с ленивым API.
SDK провайдеров
Официальные SDK Anthropic и OpenAI генерируются из OpenAPI-спецификаций и устроены похоже: типизированные запросы и ответы, sync и async клиенты, встроенные ретраи (у Anthropic по умолчанию 2 повтора на 408/409/429/5xx), таймауты, стриминг, пагинация. Всегда используйте их вместо ручного HTTP — кроме случаев, когда нужно понять протокол или SDK нет для вашего языка.
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY из окружения
resp = client.messages.create(
model="claude-opus-5",
max_tokens=2048, # обязателен
system="Отвечай по-русски, кратко.", # system — параметр верхнего уровня, не роль
messages=[{"role": "user", "content": "Чем HNSW лучше IVF?"}],
)
text = "".join(b.text for b in resp.content if b.type == "text") # ответ — список блоков
print(resp.stop_reason, resp.usage.input_tokens, resp.usage.output_tokens)
# Стриминг с помощником, собирающим итоговое сообщение
with client.messages.stream(model="claude-opus-5", max_tokens=2048,
messages=[{"role": "user", "content": "Расскажи про BM25"}]) as s:
for chunk in s.text_stream:
print(chunk, end="", flush=True)
final = s.get_final_message()
# Ошибки — иерархия исключений
try:
client.messages.create(model="claude-opus-5", max_tokens=10, messages=[])
except anthropic.BadRequestError as e: # 400 — не ретраить
print("bad request:", e.message)
except anthropic.RateLimitError: # 429 — SDK уже ретраил max_retries раз
raise
except anthropic.APIStatusError as e: # остальные HTTP-ошибки
print(e.status_code)
from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY из окружения
MODEL = "..." # актуальное имя модели — из документации OpenAI
# Responses API — основной интерфейс OpenAI; Chat Completions остаётся для совместимости
resp = client.responses.create(
model=MODEL,
instructions="Отвечай по-русски, кратко.", # аналог system
input="Чем HNSW лучше IVF?",
)
print(resp.output_text) # хелпер: склеенный текст ответа
print(resp.usage.input_tokens, resp.usage.output_tokens)
# Старый стиль, который встретится в чужом коде и во многих совместимых API (vLLM, Ollama):
chat = client.chat.completions.create(
model=MODEL,
messages=[{"role": "system", "content": "Кратко."},
{"role": "user", "content": "Что такое BM25?"}],
)
print(chat.choices[0].message.content, chat.choices[0].finish_reason,
chat.usage.prompt_tokens, chat.usage.completion_tokens)
| Аспект | Anthropic Messages | OpenAI (Responses / Chat Completions) |
|---|---|---|
| System prompt | Параметр system | instructions / сообщение с ролью system (developer) |
| max tokens | Обязателен | Опционален |
| Ответ | Список content blocks: text, tool_use, thinking | Список output items / choices[0].message |
| Причина остановки | stop_reason: end_turn, max_tokens, tool_use, stop_sequence, pause_turn, refusal | status + детали / finish_reason: stop, length, tool_calls, content_filter |
| Usage | input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens | input_tokens/output_tokens (Responses), prompt_tokens/completion_tokens (Chat) |
| Кэширование промпта | Явные cache_control breakpoints (до 4) | В основном автоматическое по префиксу |
| Сэмплинг | На новейших моделях temperature/top_p удалены, управление через effort | Зависит от модели: у reasoning-моделей часть параметров не поддерживается |
Формат Chat Completions стал де-факто стандартом: его отдают vLLM, Ollama, многие облака и gateway'и. Соблазн — писать всё под него и менять только base_url. Цена — потеря специфичных возможностей (явный prompt caching, thinking-блоки, особенности tool use), а они часто и есть основная экономия. Разумный компромисс — собственный тонкий интерфейс в коде (Generate(ctx, req) (Resp, error)Resp generate(Req req)) с адаптером на каждого провайдера, а не «наименьший общий знаменатель» на уровне протокола.
LangChain, LlamaIndex, LangGraph
Фреймворки — самая спорная тема в прикладном AI. На собеседовании ждут не «я знаю LangChain», а взвешенного мнения: что он даёт, чем платите, когда брать.
LangChain
Общий слой абстракций: единый интерфейс chat-моделей разных провайдеров, шаблоны промптов, парсеры вывода, сотни интеграций (векторные БД, загрузчики документов, инструменты), композиция цепочек. Сильная сторона — скорость прототипа и интеграции. Слабая — много слоёв поверх простого HTTP-вызова.
LlamaIndex
Фокус на данных и RAG: коннекторы к источникам, парсинг документов, стратегии чанкинга, индексы, query engines, reranking, оценка retrieval. Хорош, чтобы за день собрать RAG (retrieval-augmented generation — ответ модели по найденным в вашей базе фрагментам; подробно в модуле 5) над корпусом и сравнить стратегии. Прод-логику retrieval часто потом переписывают под свои данные.
LangGraph
Низкоуровневая библиотека для агентов как графа состояний: узлы — шаги, рёбра — переходы (в т.ч. условные), общее состояние, checkpointing (сохранение состояния между шагами), human-in-the-loop, возобновление после сбоя. Меньше «магии», чем LangChain; по сути — движок конечного автомата с персистентностью.
Чем платите — список, который стоит уметь назвать:
- Скрытые промпты. Готовые цепочки и агенты содержат свои системные промпты и шаблоны. Вы не видите, что уходит в модель, пока не включите трейсинг. Anthropic в «Building effective agents» прямо советует начинать с прямых вызовов API: фреймворки добавляют слои, скрывающие реальные промпты и ответы, и провоцируют лишнюю сложность.
- Отладка. Стектрейс на 40 фреймов через generic-обёртки, callbacks и runnable-протоколы. Ошибка формата ответа модели выглядит как исключение глубоко внутри парсера.
- Версии. Экосистема быстро меняет API; мажорные переходы ломали импорты и паттерны, туториалы годичной давности часто не работают. Пинить версии и закладывать время на миграции.
- Отставание от провайдера. Новая возможность API (новый параметр, тип блока, режим кэширования) появляется в SDK провайдера в день релиза, во фреймворке — позже, а в «универсальной» абстракции может не появиться вовсе.
- Наименьший общий знаменатель. Абстракция «chat model» сглаживает различия провайдеров, в том числе полезные.
- Зависимости. Десятки транзитивных пакетов, конфликты версий с вашим pydantic или numpy.
Фреймворк или свой код?
Эвристика, а не закон. Двигайте сложность оркестрации (сколько шагов, ветвлений, состояния) и требования к контролю (прод-нагрузка, аудит, стоимость, отладка). Тип задачи уточняет, какой именно фреймворк.
Экосистема на JVM заметно богаче, чем у большинства языков, кроме Python. LangChain4j — не порт, а самостоятельная библиотека: единый интерфейс моделей, AiServices (Java-интерфейс с аннотациями превращается в вызов модели), tools через @Tool, RAG-конвейер, интеграции с векторными хранилищами. Spring AI — то же в стиле Spring: ChatClient, advisors (память, RAG, логирование как цепочка перехватчиков), tool calling, VectorStore, автоконфигурация и метрики Micrometer из коробки. Если команда живёт в Spring Boot, интеграции и наблюдаемость приходят почти бесплатно.
Цена та же, что у LangChain: промпты, собранные внутри advisors и AiServices, отставание от новых возможностей API, абстракция «chat model», которая прячет prompt caching, thinking и особенности tool use. Совет Anthropic работает и здесь: начните с официального SDK (anthropic-java) и своего агентного цикла на 100–200 строк (вы напишете его в модуле 6), а фреймворк берите точечно — загрузчики и сплиттеры документов LangChain4j, VectorStore Spring AI — и включите логирование запросов к модели с первого дня.
// LangChain4j: декларативный сервис поверх модели. Удобно, но промпт собирает фреймворк.
interface Triage {
@SystemMessage("Классифицируй тикет: billing, tech, refund или other. Ответь одним словом.")
String classify(@UserMessage String ticket);
}
ChatModel model = AnthropicChatModel.builder()
.apiKey(System.getenv("ANTHROPIC_API_KEY"))
.modelName("claude-haiku-4-5")
.logRequests(true) // с первого дня: видеть, что реально уходит в модель
.logResponses(true)
.build();
Triage triage = AiServices.create(Triage.class, model);
String label = triage.classify("Не прошла оплата картой");
// Spring AI: ChatClient из автоконфигурации (spring-ai-starter-model-anthropic).
String label2 = chatClient.prompt()
.system("Классифицируй тикет: billing, tech, refund или other. Ответь одним словом.")
.user("Не прошла оплата картой")
.call()
.content();
Экосистема меньше: есть langchaingo (порт LangChain от сообщества), Eino от CloudWeGo, Genkit с поддержкой Go. Но в Go культура «тонкие библиотеки + свой код» и так сильнее, и это совпадает с советом Anthropic: SDK провайдера, свой агентный цикл на 100–200 строк (вы напишете его в модуле 6), pgvector, OpenTelemetry. Агентный цикл — это for с switch resp.StopReason; граф состояний — таблица переходов и состояние в Postgres. Фреймворк здесь чаще мешает, чем помогает.
Jupyter: для экспериментов, не для прода
Jupyter notebook — документ из ячеек кода и текста, которые выполняются в живом процессе (kernel) по одной. Для работы с LLM это идеальная лаборатория: поправил промпт, перезапустил одну ячейку, посмотрел пять ответов рядом, построил график по результатам evals, записал выводы тут же. Состояние (загруженный датасет, клиент) живёт между ячейками, не нужно перезапускать всё.
Ровно это же делает его непригодным для прода:
- Скрытое состояние. Ячейки выполняются в любом порядке, переменная могла быть определена в удалённой ячейке. Ноутбук «работает у меня» и не воспроизводится после Restart & Run All.
- Нет тестов, типов, ревью. Файл
.ipynb— JSON с выводами и картинками в base64; диффы в git нечитаемы. Помогаютnbstripout(очистка выводов), jupytext (парный.py), или marimo — реактивные ноутбуки, хранящиеся как обычный.py. - Утечки. Выводы ячеек сохраняются в файле: ответы модели с ПДн, случайно напечатанный ключ API уезжают в репозиторий.
- Нет жизненного цикла сервиса: ни graceful shutdown, ни лимитов, ни наблюдаемости.
Нормальный путь: ноутбук → код, который доказал ценность, выносится в модуль с функциями → скрипт с аргументами (PEP 723) → тесты и CI → при необходимости сервис (FastAPI или GoSpring Boot). Ноутбук остаётся как отчёт об эксперименте, импортирующий этот модуль.
После этого раздела пройдите модуль 17 «SDD и OpenSpec»: подготовка требований системного анализа, основные и дельта-спеки, приёмка и небольшой проект. Теория предшествует практике; 12–14 часов учитываются дополнительно к базовому маршруту.
Coding agents в работе AI-инженера
Coding agent — LLM в агентном цикле с инструментами разработчика: читать и искать по коду, править файлы, запускать команды (go testmvn verify, git diff), вызывать MCP-серверы. Это тот же цикл tool use из модуля 2, только «мир» агента — ваш репозиторий (как цикл устроен изнутри, разберём в модуле 6). Код — область, где агенты работают лучше всего: компилятор и тесты дают объективную обратную связь без человека. Отсюда главный принцип: доверие к изменению от агента строится на проверяемости, а не на уверенном тоне отчёта. То, что автоматически не проверяется (промпты, тексты, конкурентность, безопасность), требует evals и человека.
Классы инструментов
| Класс | Примеры (по состоянию на октябрь 2026) | Когда брать |
|---|---|---|
| CLI | Claude Code, OpenAI Codex CLI (open source), Gemini CLI (с 18 июня 2026 для личных аккаунтов Google, бесплатных и с подпиской AI Pro/Ultra, заменён на Antigravity CLI; с корпоративной лицензией Code Assist и по платному API-ключу работает) | Основной инструмент бэкендера: тот же shell и make, есть неинтерактивный режим для CI (claude -p, codex exec) |
| IDE | Cursor, GitHub Copilot agent mode, JetBrains AI Assistant (Junie и подключаемые агенты по протоколу ACP) | Точечные правки под постоянным контролем, ревью диффа в редакторе |
| Облачные | Claude Code on the web, Codex Cloud, Copilot cloud agent, Cursor Cloud Agents, Jules, Devin | Чётко поставленные задачи, которые можно запускать параллельно; результат — PR на ревью |
Сравнивайте не бренды, а свойства: какой файл инструкций читается, какие есть режимы разрешений и sandbox, поддерживается ли MCP, где исполняется код и какие секреты туда попадают.
Рабочий цикл
- Исследование без права правок (в Claude Code — plan mode,
--permission-mode plan). Большое исследование — субагенту, чтобы не засорять основной контекст. - План письменно: файлы, функции, тесты, чего не трогать. Правка плана стоит одного сообщения, правка 600 строк диффа — часа.
- Тесты сначала: агент пишет табличный тестпараметризованный тест (
@ParameterizedTest), запускает, тест падает по правильной причине. Тесты коммитятся отдельно, до реализации. - Реализация маленькими шагами: «сделай так, чтобы тесты прошли; тесты не меняй». Коммит после зелёных
go vetиgo test -raceзелёногоmvn verify(компиляция, линтеры, тесты). - Ревью диффа: сначала
git diff --stat(не вышел ли за план), потом отдельно дифф*_test.go*Test.java(не ослаблены ли проверки), потом всё. Агент пишет, человек отвечает за мерж.
mvn verify в цикле агента — компилятор для поведения: модель не нужно уговаривать, ей достаточно красного отчёта surefire.go test в цикле агента — компилятор для поведения: модель не нужно уговаривать, ей достаточно красного вывода.Настройка репозитория: AGENTS.md, права, хуки
Файл инструкций агент загружает в контекст в начале каждой сессии. AGENTS.md — открытый формат (обычный Markdown), его читают Codex, Cursor, Copilot и большинство других агентов; вложенные файлы в подкаталогах уточняют правила, приоритет у ближайшего. Claude Code читает CLAUDE.md (а AGENTS.md — если CLAUDE.md нет); для команды со смешанными инструментами удобно держать правила в AGENTS.md, а в CLAUDE.md написать строку @AGENTS.md и специфику Claude Code. Пишите только то, чего агент не выведет из кода: команды проверки, карту, неочевидные конвенции, запреты, критерий «готово». Каждая строка едет в контекст каждой сессии — это «всегда загруженная» часть контекста (модуль 6), документация Claude Code советует держаться в пределах ~200 строк.
Но инструкция — не гарантия: документация Claude Code прямо называет CLAUDE.md контекстом, а не принудительной конфигурацией. То, что должно выполняться всегда, живёт в двух механизмах вне модели. Права: режим решает, спрашивать ли человека (в Claude Code — от Manual с подтверждениями до auto, где действия проверяет модель-классификатор, и bypassPermissions — только в изолированном контейнере), правила allow/ask/deny уточняют его, а sandbox средствами ОС ограничивает, до каких файлов и доменов вообще дотянутся shell-команды. Правила для команд хрупкие (вариация синтаксиса их обходит), поэтому граница безопасности — sandbox или контейнер. Хуки: ваши команды на событиях агента; PreToolUse с кодом выхода 2 блокирует вызов инструмента, Stop с кодом 2 не даёт агенту закончить ход и возвращает ему stderr.
# docrag — RAG по документации (Java 21, Spring Boot, Postgres + pgvector)
## Команды
- `docker compose up -d db` — Postgres для интеграционных тестов (или Testcontainers)
- `make check` — ./mvnw -q verify: spotless:check, компиляция с Error Prone, тесты; запускай перед «готово»
- `make eval-smoke` — ~40 вопросов из golden set; обязателен при правке prompts/ и src/main/java/docrag/generate/
## Структура и конвенции
- docrag.retrieve — гибридный поиск; docrag.generate — промпт и вызов Claude; prompts/ — версии промптов
- Ошибки: доменные исключения с причиной (new XxxException("...", e)); логи только SLF4J; имена моделей — из generate/Models.java
## Нельзя
- Менять evals/data/ (эталон) и применённые migrations/*.sql (Flyway); ослаблять тесты, ставить @Disabled
- Добавлять зависимости в pom.xml без вопроса, читать .env, ходить в прод
## Готово, когда
- make check зелёный; при правке prompts/ — eval-smoke не хуже baseline; дифф ≤ 300 строк
{
"permissions": {
"allow": ["Bash(make check)", "Bash(./mvnw test *)", "Bash(git diff *)"],
"ask": ["Bash(git push *)", "Edit(/pom.xml)", "Edit(/migrations/**)"],
"deny": ["Read(./.env)", "Edit(/evals/data/**)", "Bash(kubectl *)"]
},
"sandbox": {
"enabled": true,
"filesystem": { "denyRead": ["./.env", "~/.ssh", "~/.aws", "~/.kube"] },
"network": { "allowedDomains": ["repo.maven.apache.org"] }
},
"hooks": {
"Stop": [{ "hooks": [{ "type": "command", "timeout": 900,
"command": "cd \"$CLAUDE_PROJECT_DIR\" && java -jar tools/agentgate.jar" }] }]
}
}# docrag — RAG по документации (Go, Postgres + pgvector)
## Команды
- `docker compose up -d db` — Postgres для интеграционных тестов
- `make check` — gofmt -l, go vet, go test -race ./...; запускай перед «готово»
- `make eval-smoke` — ~40 вопросов из golden set; обязателен при правке prompts/ и internal/generate/
## Структура и конвенции
- internal/retrieve — гибридный поиск; internal/generate — промпт и вызов Claude; prompts/ — версии промптов
- Ошибки: fmt.Errorf("...: %w", err); логи только slog; имена моделей — из internal/generate/models.go
## Нельзя
- Менять evals/data/ (эталон) и применённые migrations/*.sql; ослаблять или пропускать тесты
- Добавлять зависимости без вопроса, читать .env, ходить в прод
## Готово, когда
- make check зелёный; при правке prompts/ — eval-smoke не хуже baseline; дифф ≤ 300 строк
{
"permissions": {
"allow": ["Bash(make check)", "Bash(go test *)", "Bash(git diff *)"],
"ask": ["Bash(git push *)", "Bash(go get *)", "Edit(/migrations/**)"],
"deny": ["Read(./.env)", "Edit(/evals/data/**)", "Bash(kubectl *)"]
},
"sandbox": {
"enabled": true,
"filesystem": { "denyRead": ["./.env", "~/.ssh", "~/.aws", "~/.kube"] },
"network": { "allowedDomains": ["proxy.golang.org", "sum.golang.org"] }
},
"hooks": {
"Stop": [{ "hooks": [{ "type": "command", "timeout": 900,
"command": "cd \"$CLAUDE_PROJECT_DIR\" && go run ./cmd/agentgate" }] }]
}
}Stop-хук agentgate превращает «готово, когда» из AGENTS.md в код: смотрит, что изменилось относительно main, и решает, какие проверки обязательны.
// plan по изменениям возвращает обязательные проверки (цели Makefile — те же,
// что перечислены в AGENTS.md) и нарушения, которые агент должен откатить.
public static Plan plan(List<Change> cs) {
boolean javaCode = false, prompts = false;
List<String> violations = new ArrayList<>();
for (Change c : cs) {
String p = c.path();
if (p.startsWith("evals/data/")) {
violations.add(p + ": эталон evals меняет человек отдельным PR, а не агент вместе с кодом");
} else if (p.startsWith("migrations/") && c.status() != 'A') {
violations.add(p + ": применённую миграцию не правят, добавьте новую");
} else if (p.startsWith("prompts/")) {
prompts = true;
} else if (p.endsWith(".java") || p.equals("pom.xml") || p.endsWith("/pom.xml")) {
javaCode = true;
}
}
List<List<String>> checks = new ArrayList<>();
if (javaCode) {
checks.add(List.of("make", "check")); // mvn verify: компиляция, линтеры, тесты
}
if (prompts) {
checks.add(List.of("make", "eval-smoke")); // промпт не проверить юнит-тестом
}
return new Plan(checks, violations);
}
// Plan по изменениям возвращает обязательные проверки (цели Makefile — те же,
// что перечислены в AGENTS.md) и нарушения, которые агент должен откатить.
func Plan(cs []Change) (checks [][]string, violations []string) {
var goCode, prompts bool
for _, c := range cs {
switch {
case strings.HasPrefix(c.Path, "evals/data/"):
violations = append(violations, c.Path+": эталон evals меняет человек отдельным PR, а не агент вместе с кодом")
case strings.HasPrefix(c.Path, "migrations/") && c.Status != 'A':
violations = append(violations, c.Path+": применённую миграцию не правят, добавьте новую")
case strings.HasPrefix(c.Path, "prompts/"):
prompts = true
case strings.HasSuffix(c.Path, ".go"), c.Path == "go.mod", c.Path == "go.sum":
goCode = true
}
}
if goCode {
checks = append(checks, []string{"make", "check"}) // gofmt -l, go vet, go test -race
}
if prompts {
checks = append(checks, []string{"make", "eval-smoke"}) // промпт не проверить go test
}
return checks, violations
}
Полная версия (запуск git и проверок, коды выхода 0/1/2, табличные тестыпараметризованные JUnit-тесты на fake-раннере) — в examples/m04-toolsexamples/java/m04-tools репозитория курса. Нарушения и упавшие проверки хук пишет в stderr с хвостом лога, и агент продолжает работу. От зацикливания Claude Code защищён сам: после 8 продолжений от Stop-хуков подряд он завершает ход (счётчик сбрасывается при каждом вызове инструмента; порог меняет CLAUDE_CODE_STOP_HOOK_BLOCK_CAP, по состоянию на октябрь 2026). make eval-smoke ходит в API модели с ключом, поэтому его запускает хук (хуки работают вне sandbox), а не агент из своего shell.
Безопасность
- Секреты и прод. Всё, что агент прочитал, попадает в контекст, а оттуда — в логи и, при инъекции, к атакующему. Агенту — dev-ключи с лимитом расходов и токены со scope на один репозиторий; никаких DSN прод-БД, kube-контекста прода и облачных admin-ключей:
DROP TABLEне откатывается. Данные — через read-only MCP-инструмент с allowlist запросов (какdb_queryв проекте C). - Prompt injection. Агент читает чужой текст: README и код зависимостей, issues и комментарии в PR, веб-страницы, ответы MCP-инструментов. Это lethal trifecta из модуля 3: приватный код + недоверенный текст + каналы наружу (сеть,
git push, публичный комментарий). Пример: Invariant Labs (2025) через вредоносный issue заставили агента с GitHub MCP-сервером выложить данные приватных репозиториев в публичный PR. Защита — минимальные права, allowlist сети, подтверждение действий, выносящих данные наружу; систематически — в модуле 7. - MCP-серверы проекта описываются в
.mcp.json(в git, секреты — через${VAR}). Для docrag естественный кандидат — вашdocs-mcpиз проекта C (модуль 6). Каждый сервер — новые права и новый источник недоверенного текста: по умолчанию только чтение, свои или прочитанные вами серверы, версии зафиксированы.
Типичные провалы и как их ловить
| Провал | Как ловить |
|---|---|
| Огромный дифф «заодно отрефакторил» | План со списком файлов; лимит ~300 строк в AGENTS.md; git diff --stat первым делом |
Тесты подогнаны под код: изменён assert, t.Skip@Disabled, удалён «нестабильный» тест | Тесты отдельным коммитом до кода; дифф *_test.go*Test.java отдельно; сломайте код руками — тест обязан упасть |
| Галлюцинированный API: несуществующий метод, устаревший параметр, выдуманное имя модели или пакета | Методы ловит компилятор GoJava (в Python нужен mypy/pyright в цикле). Имена моделей и параметры — константы с правилом в AGENTS.md. Новые строки go.modpom.xml — глазами: модели «вспоминают» несуществующие пакеты (Spracklen et al., 2025), а атакующий может их зарегистрировать (slopsquatting) |
| Дрейф от задачи: правит соседнее, меняет CI, «чтобы прошло» | «Что не трогать» в плане; CODEOWNERS на CI и эталон evals |
| «Готово» без проверки | Stop-хук, который сам запускает проверки; финальный судья — CI |
| Деградация длинной сессии (context rot) | Одна задача — одна сессия; после двух неудачных поправок — сброс и новый запрос |
Ощущение скорости — не метрика: в исследовании METR (2025) опытные разработчики с AI-инструментами работали на 19% дольше, хотя считали, что ускорились. Меряйте своё: время до мержа и долю PR от агента, которые пришлось переделать.
Специфика AI-инженерии
- Агент правит промпт — обязателен прогон evals. Зелёный
go testmvn verifyничего не говорит о поведении модели. То же для кода вокруг модели: сборки контекста, параметров вызова, парсинга ответа, retrieval. Smoke-набор — до мержа, полный — в CI (модуль 8). - Агент правит эталон evals — конфликт интересов. Если в одном диффе меняются и код, и ожидаемые ответы, зелёный результат ничего не доказывает. Эталон меняет человек отдельным PR; обеспечивают это deny-правило, CODEOWNERS и хук.
- Агент генерирует eval-кейсы — полезно для покрытия (перефразировки, граничные случаи, атаки, вопросы без ответа), но: ожидаемый ответ задаёт человек или источник, а не модель (иначе её ошибки становятся эталоном); проверяйте разнообразие и то, что текущая версия падает хотя бы на части кейсов; не тащите в фикстуры реальные логи с ПДн; помечайте
"source": "synthetic". Подробно — в модулях 8 и 10.
Насколько доверять изменению от агента
Опишите дифф, который принёс агент: размер, что затронуто, какие проверки пройдены и в каком режиме он работал. Справа — что повышает риск, слева — что его снижает, внизу — итог и что сделать до мержа. Веса — эвристика курса, а не стандарт индустрии: важна структура решения, а не точные числа.
Обратите внимание: правка промпта без evals и эталон evals в одном диффе с кодом — блокеры при любом числе ревьюеров, а ревью 1 200 строк снижает риск втрое слабее, чем ревью 150. Командные правила работы с агентами (что можно отдавать, как ревьюить) — в модуле 13.
Практика
Задание 1: Python-окружение и eval-скрипт
Поставьте uv, создайте проект, напишите скрипт прогона классификатора (как в разделе «Скрипты») на 60–100 своих примерах для двух версий промпта. Сохраните результаты в parquet.
Задание 2: FastAPI vs GoSpring Boot
Реализуйте один и тот же эндпоинт стриминга ответа (SSE) на FastAPI и на Go net/http (код из модуля 0)Spring Boot (SseEmitter на virtual threads или WebFlux, код из модуля 0). Нагрузите оба 100 одновременными стримами (например, k6 или hey против мок-сервера провайдера, отдающего токены с задержкой).
Задание 3: фреймворк на вкус
Соберите наивный RAG над 20 документами (эмбеддинги + косинусная близость + ответ модели по найденному; как делать RAG хорошо — модуль 5) двумя способами: на LlamaIndex (или LangChain) и на «голом» SDK. Включите трейсинг или логирование запросов и найдите все промпты, которые фреймворк отправил в модель.
Задание 4: coding agent для проекта A
Настройте coding agent (Claude Code, Codex CLI или другой — критерии те же) для своего репозитория docrag из проекта A и сделайте с ним одну фичу полным циклом. Примеры фич: параметр min_score в /ask; кэш эмбеддингов запросов в Postgres; фильтр /search по разделу документации.
- Контекст. AGENTS.md (и CLAUDE.md с импортом, если работаете в Claude Code) не длиннее 100 строк: команды, карта, конвенции, запреты, «готово, когда». Проверка: в новой сессии агент на вопрос «как запустить тесты и что нельзя трогать» отвечает правильно, не открывая Makefile.
- Права. Без вопроса — только проверочные команды; запрещено —
.envи эталон evals; с подтверждением — push, новые зависимости, миграции. Включён sandbox, или агент работает в контейнере. Проверка: попросите агента прочитать.envчерез встроенный инструмент и черезcat— оба отказа происходят на уровне инструмента или ОС, а не из вежливости модели. - «Готово, когда» в коде. Stop-хук (
agentgateили свой аналог) или CI-проверка: агент не может закончить работу с краснымmake checkили с правкойprompts/безmake eval-smoke. Проверка: специально сломайте тест — агент продолжает чинить, а не докладывает об успехе. - MCP. Подключён хотя бы один полезный для разработки read-only сервер, описанный в
.mcp.jsonбез секретов. После модуля 6 замените его своимdocs-mcpиз проекта C и покажите, что агент сам находит в нём ответ. - Фича циклом «план → тесты → реализация → ревью». План сохранён; тесты закоммичены отдельно до реализации и после этого не менялись (
git diffпо*_test.goпуст); итоговый дифф не больше 300 строк; вы прочитали его целиком. - Журнал ошибок агента: минимум 3 записи вида «что агент сделал не так → как вы это заметили → что поменяли в AGENTS.md, правах или хуках, чтобы не повторилось».
Самопроверка
Почему time.sleep(2) внутри async def-хендлера FastAPI — серьёзная ошибка, а Thread.sleep в контроллере Spring на виртуальных потоках — нет?
asyncio кооперативный и однопоточный: переключение задач происходит только на await. Синхронный sleep блокирует event loop, и все остальные запросы этого воркера ждут. В Java с spring.threads.virtual.enabled=true каждый запрос — свой виртуальный поток: sleep или блокирующий I/O паркуют только его, а поток-носитель берёт другие задачи. Оговорка Java 21 — pinning: блокировка внутри synchronized держит носитель (в Java 24 это исправлено).
Почему time.sleep(2) внутри async def-хендлера FastAPI — серьёзная ошибка, а в Go time.Sleep в хендлере — нет?
asyncio кооперативный и однопоточный: переключение задач происходит только на await. Синхронный sleep блокирует event loop, и все остальные запросы этого воркера ждут. В Go рантайм паркует только текущую горутину, остальные продолжают работать на других потоках.
Чем dataclass отличается от pydantic BaseModel и когда что брать?
dataclass — контейнер с генерацией конструктора, без валидации значений в рантайме. BaseModel валидирует и приводит типы, парсит JSON, генерирует JSON Schema. Для внутренних структур — dataclass (быстро, просто), для данных с границы доверия (HTTP-запросы, ответы модели, конфиги) — pydantic.
Какой аналог new MyException("…", cause) в Python?
raise MyError("…") from cause. Исходное исключение сохраняется в __cause__ и печатается в traceback, как Caused by: в Java. Ловят по классу в except; обхода цепочки вроде getCause() в цикле из коробки нет — обычно ловят на нужном уровне.
Какой аналог fmt.Errorf("…: %w", err) в Python?
raise NewError("…") from err. Исходное исключение сохраняется в __cause__ и печатается в traceback. Проверка типа в цепочке делается через except по классу, аналога errors.Is по всей цепочке из коробки нет — обычно ловят на нужном уровне.
Зачем нужен venv и почему uv его не отменяет?
Python ставит пакеты в окружение интерпретатора, и без изоляции проекты конфликтуют версиями зависимостей. venv — отдельное окружение на проект. uv не отменяет venv, а управляет им: создаёт .venv, синхронизирует с lock-файлом, ставит нужную версию Python.
Что делает df.pivot_table(index="category", columns="prompt_version", values="passed", aggfunc="mean")?
Группирует по паре (категория, версия), считает среднее passed (то есть pass rate) и раскладывает версии по колонкам: строки — категории, колонки — версии, в ячейках — pass rate. Аналог GROUP BY по двум полям с последующим разворотом.
Почему при сравнении двух промптов на одном наборе полезно смотреть на «починенные» и «сломанные» примеры, а не только на разницу pass rate?
Одинаковая разница может получиться из «починил 5, сломал 0» и из «починил 25, сломал 20». Второй случай — нестабильный промпт, который меняет поведение на половине набора. Парный тест McNemar использует только эти два числа и чувствительнее сравнения двух независимых долей.
Назовите три конкретных минуса LangChain в проде.
Скрытые промпты и шаблоны внутри цепочек, которые влияют на поведение и стоимость; тяжёлая отладка через многослойные абстракции; нестабильность API между версиями и отставание от новых возможностей провайдеров. Плюс транзитивные зависимости и «наименьший общий знаменатель» в абстракции моделей.
Что не так с ноутбуком, который «работает у меня»?
Ячейки могли выполняться не по порядку, а часть состояния — прийти из удалённых ячеек. Проверка: Restart & Run All. Для воспроизводимости код выносится в модули, ноутбук только импортирует и визуализирует.
В AGENTS.md написано «не меняй evals/data/». Почему этого недостаточно и что сделать?
Файл инструкций — это контекст, а не принудительная конфигурация (так прямо сказано в документации Claude Code). В длинной сессии, при противоречивых указаниях или под prompt injection модель может нарушить любой пункт. Гарантию дают механизмы вне модели: deny-правило Edit(/evals/data/**), хук, который блокирует завершение при изменённом эталоне, CODEOWNERS и проверка в CI. Строку в AGENTS.md оставляют как объяснение «почему», чтобы агент не тратил шаги на попытки.
Агент изменил prompts/answer.v2.txt, make check зелёный. Можно мержить?
Нет. go testmvn verify проверяет код, а не поведение модели: промпт меняет ответы так, что ни один юнит-тест этого не увидит. Нужен прогон evals (smoke-набор до мержа, полный в CI) со сравнением с baseline, ревью диффа промпта человеком, а после мержа — canary. Это правило для всего, что влияет на вход и выход модели: сборки контекста, параметров вызова, retrieval.
Чем режим разрешений агента отличается от sandbox и почему нужны оба?
Режим решает, спрашивать ли человека перед действием (Manual, acceptEdits, auto и т. д.). Sandbox решает, до чего действие может дотянуться: файлы, сеть, процессы — на уровне ОС или контейнера. Подтверждения устают и ошибаются, классификатор вероятностен, а правила для команд обходятся вариациями синтаксиса. Sandbox ограничивает ущерб, даже если решение пропустить действие было неверным. bypassPermissions без внешней изоляции снимает оба уровня сразу.
Вопросы с собеседований
Вы бы взяли LangChain для нового продукта?
Зависит от стадии и формы задачи, и я бы так и ответил, с критериями. Для прототипа или внутреннего инструмента с кучей интеграций — возможно, это ускоряет первую неделю. Для прод-системы с жёсткими требованиями к стоимости, латентности и отладке я начну с SDK провайдера и тонкого своего слоя: агентный цикл и RAG-пайплайн — это сотни строк, зато я вижу каждый промпт, могу использовать prompt caching и новые возможности API в день релиза, а трейсинг и evals настраиваю под себя. Для агента со сложным состоянием, human-in-the-loop и долгими сессиями рассмотрю LangGraph как движок персистентного графа, а не как набор готовых агентов. Главный аргумент против «всё на фреймворке» — скрытые промпты и отладка; главный аргумент «за» — не писать двадцатый загрузчик PDF. Совет Anthropic в «Building effective agents»: начинать с прямых вызовов API.
Почему FastAPI-сервис с LLM под нагрузкой отвечает всё медленнее, хотя провайдер отвечает стабильно?
Первое, что проверю: блокирующие вызовы в async-хендлерах — синхронный клиент LLM или БД, requests, тяжёлый CPU (токенизация, парсинг JSON мегабайтных ответов). Они блокируют event loop, и латентность всех запросов воркера растёт. Диагностика — debug-режим asyncio (предупреждения о медленных колбэках), py-spy. Второе: клиент создаётся на каждый запрос — нет переиспользования соединений. Третье: лимиты пула соединений HTTP-клиента меньше, чем одновременных стримов. Четвёртое: один воркер на CPU-bound работе. Лечение: async-клиенты везде, asyncio.to_thread для синхронных библиотек, пул процессов для CPU, несколько воркеров uvicorn, синглтон клиента с настроенным пулом.
Как вы анализируете результаты eval-прогона? Покажите на pandas.
Загружаю результаты в DataFrame, отделяю инфраструктурные ошибки. Сводка по версиям через groupby().agg(): pass rate, n, p50/p95 латентности, стоимость. Срезы через pivot_table по категории и версии — ищу, где регрессия прячется за общим ростом. Парное сравнение по example_id: сколько починено и сколько сломано, McNemar для значимости. Доверительные интервалы (Уилсон или bootstrap), решение по нижней границе. И обязательно глазами смотрю на провалы — df.query("passed == 0") — потому что метрика говорит «сколько», а не «почему». Ошибки judge-модели тоже проверяю выборочно.
Сервис на Java или на Python? Как бы вы выбрали для LLM-продукта?
Сервисный слой — на том, на чём сильна команда и инфраструктура: вызов API, стриминг, лимиты, очереди — это I/O, и Java 21 с виртуальными потоками даёт простую конкурентность без реактивщины, строгую типизацию, зрелый Spring-стек, наблюдаемость через Micrometer и OpenTelemetry и официальный SDK. Python нужен там, где экосистема: evals, анализ, fine-tuning, локальные модели, сложный парсинг документов. Частая здравая архитектура — сервисы на Spring Boot плюс Python-воркеры или офлайн-пайплайны, связь через очередь или HTTP. Против Python-сервиса по умолчанию: GIL и async-подвохи, упаковка и зависимости. За: скорость итерации, если команда — ML/DS. Против Java в отдельных случаях: холодный старт и память для serverless (лечится CDS, AOT, GraalVM native image). Выбор фиксирую в ADR, а не спорю о вкусах.
Сервис на Go или на Python? Как бы вы выбрали для LLM-продукта?
Сервисный слой — на том, на чём сильна команда и инфраструктура: вызов API, стриминг, лимиты, очереди — это I/O, и Go здесь даёт простую конкурентность, предсказуемую память, один бинарь и строгую типизацию. Python нужен там, где экосистема: evals, анализ, fine-tuning, локальные модели, сложный парсинг документов. Частая здравая архитектура — Go-сервисы плюс Python-воркеры или офлайн-пайплайны, связь через очередь или HTTP. Против Python-сервиса по умолчанию: GIL и async-подвохи, упаковка и зависимости. За: скорость итерации, если команда — ML/DS. Выбор фиксирую в ADR, а не спорю о вкусах.
Зачем абстрагировать провайдера LLM и как не переусердствовать?
Нужно для fallback между моделями и провайдерами, A/B-тестов, тестов с фейковым клиентом и защиты от vendor lock-in. Переусердствовать — значит свести всё к наименьшему общему знаменателю и потерять prompt caching, thinking, особенности tool use, которые дают основную экономию и качество. Решение: узкий интерфейс под задачи продукта (генерация, стриминг, tool-цикл) с явными опциями, которые адаптер провайдера поддерживает или игнорирует; провайдер-специфичные возможности доступны через расширения. И evals на каждого провайдера — переключение без них опасно, промпты не переносятся один в один.
Чем asyncio отличается от виртуальных потоков Java, и что вы бы сказали, если бы вас спросили, «что лучше для LLM-сервиса»?
asyncio — кооперативная многозадачность в одном потоке, переключение только в await, функции «окрашены» (async заражает стек), CPU-параллелизма нет из-за GIL. Виртуальные потоки — планировщик M:N в JVM поверх пула носителей на всех ядрах: обычный блокирующий код без окраски, блокирующий I/O паркует только свой поток. Для I/O-bound нагрузки LLM-сервиса оба справляются с тысячами одновременных запросов. Разница в надёжности: в Python одна синхронная библиотека способна положить воркер, и это надо контролировать ревью и линтерами. В Java 21 похожий, но более узкий класс ошибок — pinning (блокировка внутри synchronized, нативные вызовы); его видно в JFR по событию jdk.VirtualThreadPinned, а в Java 24 synchronized больше не пинит. Для CPU-частей (токенизация, обработка документов) у JVM преимущество без дополнительных процессов.
Чем asyncio отличается от горутин, и что вы бы сказали, если бы вас спросили, «что лучше для LLM-сервиса»?
asyncio — кооперативная многозадачность в одном потоке, переключение только в await, функции «окрашены» (async заражает стек), CPU-параллелизма нет из-за GIL. Горутины — вытесняющий планировщик M:N на всех ядрах, любая функция может быть конкурентной, блокирующий вызов не мешает другим. Для I/O-bound нагрузки LLM-сервиса оба справляются с тысячами одновременных запросов. Разница в надёжности: в Python одна синхронная библиотека способна положить воркер, и это надо контролировать ревью и линтерами; в Go такой класс ошибок отсутствует. Для CPU-частей (токенизация, обработка документов) у Go преимущество без дополнительных процессов.
Как вы используете Jupyter в работе и как код из ноутбука попадает в прод?
Ноутбук — для исследований: итерации над промптами на небольших выборках, анализ evals, графики, разбор провалов. Правила: выводы очищаются перед коммитом (nbstripout) или используется jupytext/marimo, секреты только из окружения, данные с ПДн не сохраняются в выводах. Как только код нужен повторно, он переезжает в модуль с тестами, ноутбук его импортирует. Прод-код — модули, скрипты с CLI и сервисы, с CI, типами и наблюдаемостью. Ноутбук никогда не запускается «по крону».
Как вы работаете с coding agents и как контролируете качество кода, который они пишут?
Агент для меня — исполнитель с быстрыми руками, а ответственность за мерж на мне. Цикл: исследование в режиме только чтения, письменный план с перечнем файлов и тем, чего не трогать, тесты до кода отдельным коммитом, реализация маленькими шагами с зелёными go build, go vet, go test -raceзелёным mvn verify после каждого, ревью диффа: сначала --stat, потом тесты отдельно, потом всё. В репозитории лежит короткий AGENTS.md: команды, конвенции, запреты, критерий готовности. Всё, что обязано выполняться, вынесено в права, хуки и CI: агент не может завершить работу с красными проверками или с правкой промпта без evals. Не отдаю агенту целиком то, что плохо проверяется автоматически: конкурентность, безопасность, миграции данных. Там он максимум предлагает, а решение и ревью — построчно мои. Полезная метрика для себя — доля PR от агента, которые пришлось переделывать или откатывать. Если она растёт, значит, я отдаю слишком крупные задачи.
На что смотрит интервьюер: зрелую позицию между «AI пишет всё» и «не пользуюсь». Ключевое — доверие через проверяемость (тесты до кода, CI, evals), маленькие диффы, явное владение результатом и понимание, где агент систематически ошибается. Плюс, если кандидат называет конкретные механизмы (хуки, deny-правила, sandbox), а не только «я внимательно смотрю».
Команда из 10 человек начинает пользоваться coding agents. Что вы настроите по безопасности в первый день?
Исхожу из того, что агент может быть скомпрометирован через текст, который он читает: README зависимостей, issues, комментарии в PR, веб-страницы, описания MCP-инструментов. Значит, разрываю lethal trifecta и ограничиваю ущерб, а не надеюсь на промпт. Первое: никаких прод-доступов в окружении агента — ни прод-БД, ни kube-контекста прода, ни облачных admin-ключей; для данных — read-only реплика или обезличенный дамп. Второе: секреты — deny-правила на .env и каталоги с ключами, отдельные dev-ключи с лимитом расходов, короткоживущие токены со scope на один репозиторий, особенно у облачных агентов. Третье: sandbox или контейнер для shell-команд, сеть через allowlist (реестр модулей, git-хостинг), режим «разрешено всё» — только в одноразовых контейнерах. Четвёртое: действия, которые выносят данные наружу или необратимы (push, публикация, комментарии в публичных репозиториях, миграции), — с подтверждением человека. Пятое: allowlist MCP-серверов с закреплёнными версиями и правило «по умолчанию только чтение». Шестое: общие настройки и AGENTS.md — в репозитории под ревью, CODEOWNERS на CI и эталон evals, логи действий агента для расследований. Правила работы команды (что можно отдавать агенту, как ревьюить) оформляю отдельным стандартом.
На что смотрит интервьюер: мышление моделью угроз, а не списком запретов в промпте: least privilege, разделение «режим подтверждений» и «изоляция», понимание непрямых инъекций через issues и зависимости, отсутствие прод-доступов как жёсткое правило. Хороший знак — кандидат помнит про облачных агентов и их токены и про то, что настройки должны быть общими и версионироваться, а не жить на ноутбуках.
Агент за вечер сгенерировал 200 eval-кейсов для вашего RAG. Как вы их используете?
Как черновик, а не как готовый набор. Первым делом разделяю входы и ожидаемые ответы: вопросы агент может придумывать, а ожидаемое (какие документы должны найтись, какие факты должны быть в ответе) задаю я или источник — сама документация. Иначе модель проверяет себя собственными ответами, и её систематические ошибки становятся эталоном. Дальше чистка: дедупликация и проверка разнообразия по темам, стилям и сложности против распределения реальных запросов из логов. Агенты склонны генерировать лёгкие и похожие кейсы, на которых всё зелёное. Смотрю, что текущая версия системы падает хотя бы на части набора, иначе набор ничего не различает. Проверяю утечки: нет ли в кейсах реальных персональных данных из логов и не совпадают ли они с примерами в промпте. Синтетические кейсы помечаю (source: synthetic), читаю случайную выборку глазами и держу отдельной частью набора, чтобы видеть метрики по реальным и синтетическим кейсам раздельно. Особенно ценна синтетика для того, чего мало в логах: вопросы без ответа в документации, атаки injection, граничные формулировки.
На что смотрит интервьюер: понимание круговой зависимости «оракул = испытуемый», внимание к качеству и разнообразию данных, проверку различающей способности набора, разделение синтетических и реальных кейсов в метриках. Красный флаг — «добавлю все 200 в golden set».
Источники
- uv — документация; PEP 723 — inline script metadata
- Python — asyncio (TaskGroup, timeout, to_thread)
- Pydantic v2
- FastAPI (async, StreamingResponse)
- pandas — документация; DuckDB; Polars
- anthropic-sdk-python; документация Anthropic
- openai-python; документация OpenAI
- Anthropic — Building effective agents (раздел о фреймворках)
- LangChain, LangGraph, LlamaIndex
- LangChain4j, Spring AI, anthropic-sdk-java
- langchaingo, Eino
- Jupyter, marimo, nbstripout
- Hamel Husain — Your AI Product Needs Evals (анализ ошибок глазами)
- Claude Code — Best practices (исследование → план → код, проверяемость, управление контекстом) и Anthropic — Claude Code: best practices for agentic coding
- Claude Code: CLAUDE.md и AGENTS.md, режимы разрешений, правила allow/ask/deny, sandbox, хуки, MCP
- AGENTS.md (формат и список инструментов) и Linux Foundation — Agentic AI Foundation (декабрь 2025)
- OpenAI Codex: репозиторий Codex CLI, AGENTS.md в Codex, sandbox и подтверждения
- Cursor — Rules и Cloud Agents; GitHub — Copilot cloud agent; JetBrains AI Assistant — агенты и Agent Client Protocol
- Google — Transitioning Gemini CLI to Antigravity CLI (май 2026)
- Invariant Labs — GitHub MCP Exploited (инъекция через issue, утечка из приватного репозитория)
- Spracklen et al., 2025 — We Have a Package for You! (package hallucinations)
- METR — исследование продуктивности опытных open-source разработчиков (2025) и обновление 2026 года