05 / 06 · Best Practices

🔍 Simon Willison

Создатель Django, исследователь LLM и автор блога simonwillison.net. Подход Willison строится на одном принципе: CC — мощный инструмент который врёт с уверенным видом. Верификация каждого вывода — не паранойя, а профессионализм.

🔍
Simon Willison
Создатель Django, LLM-researcher, автор simonwillison.net
⚠️
Частая ошибка новичков: доверяют выводам CC о данных без проверки источников. CC может уверенно написать «в таблице 1,247 записей» или «топ-3 категории это X, Y, Z» — и ошибаться. Simon Willison называет это «уверенной галлюцинацией»: модель не знает разницы между тем что она вычислила и тем что она выдумала. Правило: любой числовой или фактический вывод CC требует независимой верификации SQL-запросом или кодом который вы запускаете сами.
🔍 Ключевой принцип

"Never Trust, Always Verify" — CC может написать отличный код и при этом незаметно изменить логику, сослаться на несуществующий метод или "вспомнить" факт которого нет в данных. Каждая строка кода, каждый вывод из данных, каждое утверждение — требует проверки человеком прежде чем попасть в production.

💡
Тонкость для опытных: паттерн «LLM-assisted journalism» Simon Willison переносится на любой анализ кодовой базы. Когда CC говорит «в этой кодовой базе нет прямых SQL-запросов» или «все контроллеры используют единый паттерн» — это стоит верифицировать grep-ом независимо. CC синтезирует по выборке кода которую видел в контексте, а не сканирует все файлы. Его утверждения о кодовой базе — это оценки, не аудит.
1
"LLM-assisted journalism" паттерн — данные с источниками

Willison активно использует LLM для анализа данных и документов в своих исследовательских проектах. Но он установил жёсткое правило: каждый факт из вывода CC должен сопровождаться конкретной ссылкой на источник в исходных данных.

Это не недоверие к LLM — это понимание архитектуры: модель интерполирует и обобщает. При анализе CSV или JSON файлов она может "сгладить" выброс, округлить цифру или создать правдоподобный но несуществующий паттерн.

"Если LLM говорит тебе что-то интересное о твоих данных — попроси его показать конкретные строки которые это доказывают. Если не может — это галлюцинация, а не анализ."

Промпт-шаблон для безопасного анализа данных:

import anthropic

client = anthropic.Anthropic()

def analyze_with_citations(data: str, question: str) -> str:
    """
    Анализ данных с обязательными цитатами источников.
    Willison-паттерн: каждое утверждение = ссылка на строку данных.
    """
    prompt = f"""Вот данные для анализа:

{data}

Вопрос: {question}

ВАЖНЫЕ ПРАВИЛА:
1. Для каждого факта или вывода — укажи точную строку/запись из данных
2. Используй формат: "Факт X (строка N: [цитата из данных])"
3. Если данных недостаточно для вывода — прямо скажи об этом
4. НЕ ДОДУМЫВАЙ факты которых нет в предоставленных данных
5. Разделяй: "данные показывают" vs "можно предположить"

Ответ:"""

    message = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=2048,
        messages=[{"role": "user", "content": prompt}]
    )
    return message.content[0].text


# Пример использования
with open("sales_data.csv", "r") as f:
    data = f.read()

result = analyze_with_citations(
    data,
    "Какой месяц был самым прибыльным и почему?"
)
print(result)
# Вывод будет содержать: "Март (строка 47: 'март,145000,+23%')"
# а не просто "Март был лучшим месяцем"
Почему это критически важно

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

2
"Datasette approach" — данные как API

Simon Willison создал Datasette — инструмент который превращает любую SQLite базу данных в REST API и веб-интерфейс. CC идеально вписывается в этот workflow: пишет SQL-запросы, Python скрипты для ETL, но каждый шаг верифицируется через реальный результат запроса.

Паттерн работы с данными через CC:

1
Описать структуру данных
CC получает схему БД и примеры данных — не весь датасет
2
CC пишет SQL запрос
Конкретный SQL запрос для ответа на вопрос исследования
3
Python запускает запрос
Скрипт выполняет запрос и возвращает РЕАЛЬНЫЕ результаты — не интерпретацию
4
Человек верифицирует
Проверяешь результаты вручную на 5-10 строках прежде чем доверять выводам
5
CC объясняет (не придумывает)
CC получает верифицированные результаты и интерпретирует их — уже с фактической опорой
import sqlite3
import anthropic

def cc_sql_pipeline(db_path: str, research_question: str) -> dict:
    """
    Willison-паттерн: CC пишет SQL, Python верифицирует,
    только потом CC интерпретирует.
    """
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    # Шаг 1: получить схему для CC
    cursor.execute("SELECT sql FROM sqlite_master WHERE type='table'")
    schema = "\n".join(row[0] for row in cursor.fetchall() if row[0])

    client = anthropic.Anthropic()

    # Шаг 2: CC пишет SQL запрос (только запрос, без интерпретации)
    sql_response = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=512,
        messages=[{
            "role": "user",
            "content": f"""Схема БД:
{schema}

Напиши ТОЛЬКО SQL запрос (без объяснений) для ответа на:
"{research_question}"

Требования: только SELECT, без subqueries сложнее 2 уровней."""
        }]
    )

    sql_query = sql_response.content[0].text.strip()
    # Убрать markdown если CC обернул в ```sql
    if sql_query.startswith("```"):
        sql_query = sql_query.split("\n", 1)[1].rsplit("```", 1)[0].strip()

    # Шаг 3: выполнить запрос и получить РЕАЛЬНЫЕ данные
    try:
        cursor.execute(sql_query)
        results = cursor.fetchmany(100)  # Лимит для безопасности
        columns = [d[0] for d in cursor.description]
    except Exception as e:
        return {"error": str(e), "sql": sql_query}

    # Шаг 4: CC интерпретирует верифицированные данные
    results_text = "\n".join(str(dict(zip(columns, row))) for row in results[:20])
    interpretation = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"""Реальные результаты SQL запроса:
{results_text}

Вопрос исследования: {research_question}

Интерпретируй ТОЛЬКО то что видно в данных выше.
Каждый вывод — с цитатой из результатов."""
        }]
    )

    conn.close()
    return {
        "sql": sql_query,
        "row_count": len(results),
        "sample": results[:5],
        "interpretation": interpretation.content[0].text
    }
3
"Annotated code" практика — человеческие пометки на CC-коде

Willison ввёл в своей практике правило: каждый нетривиальный фрагмент кода написанного CC должен иметь человеческий комментарий, объясняющий почему CC принял именно это решение. Это принципиально отличается от обычных комментариев к коду.

"Обычный комментарий объясняет что делает код. Аннотация Willison объясняет почему LLM выбрал именно такой подход — какой контекст был в промпте, какое ограничение задачи привело к этому решению."

Через 6 месяцев ты не вспомнишь что именно ты просил CC сделать. Аннотация сохраняет эту информацию прямо в коде:

# [CC-DECISION 2026-03-15] Willison-аннотация:
# Задача была: разобрать CSV с непоследовательными датами (3 разных формата).
# CC выбрал dateutil.parser вместо strptime потому что в промпте
# было упомянуто "форматы неизвестны заранее". Если форматы стабилизируются —
# заменить на явный strptime для скорости.
from dateutil import parser as dateutil_parser

def parse_flexible_date(date_str: str):
    """Парсинг даты с неизвестным форматом."""
    return dateutil_parser.parse(date_str)


# [CC-DECISION 2026-03-15] Willison-аннотация:
# Пагинация через cursor вместо offset/limit — CC предложил это
# когда я описал проблему пропуска строк при параллельных вставках.
# offset/limit давал дублирование при изменении данных между запросами.
# Cursor-based pagination решает это. Источник: промпт содержал
# "данные могут изменяться во время экспорта".
def paginate_results(cursor_id: str = None, limit: int = 100):
    query = "SELECT * FROM events WHERE id > ? ORDER BY id LIMIT ?"
    params = (cursor_id or 0, limit)
    return db.execute(query, params).fetchall()
Почему аннотации важнее комментариев

Обычный комментарий # cursor-based pagination для стабильности не объясняет: почему не offset? Какое требование это вызвало? Безопасно ли изменить? Аннотация Willison сохраняет контекст промпта прямо рядом с кодом — это документация решения, а не документация реализации.

4
"Five minute rule" — простота как требование

Если тебе требуется больше 5 минут чтобы понять что именно сделал CC и почему — это не значит что задача сложная. Это значит что промпт был неточным или контекст был недостаточным.

Willison использует это как диагностический инструмент: непонятный код от CC — сигнал переформулировать задачу, не сигнал принять код на веру.

# Ситуация: CC написал что-то и ты не понимаешь зачем
# ПЛОХО: принять код потому что "наверное оно умнее знает"

# ХОРОШО: применить Five Minute Rule

# Промпт 1: объяснение для junior
"Объясни этот код как будто я Junior developer который
 никогда не видел этот паттерн. Почему именно такой подход?
 Какие альтернативы существуют и почему ты выбрал этот?"

# Промпт 2: упрощение
"Этот код решает задачу но я не понимаю зачем нужны
 эти 3 уровня абстракции. Можешь переписать проще,
 даже если будет чуть менее 'правильно'?"

# Промпт 3: явные ограничения
"Перепиши это решение с явными комментариями для каждого
 нетривиального шага. Если используешь паттерн — назови его."

# Промпт 4: trade-offs
"Ты использовал [паттерн X]. Объясни:
 1. Что это даёт в этом контексте
 2. Что это усложняет
 3. При каких условиях стоило бы сделать иначе"
Когда Five Minute Rule не работает

Если ты попросил объяснить и после объяснения всё равно не понял — возможно задача действительно требует изучения домена. Но если объяснение понятно а код непонятен — попроси переписать так чтобы код сам объяснял себя через именование и структуру.

5
Верификация через тесты написанные человеком

Willison придерживается строгого правила: тесты для кода написанного CC должен писать человек. Не CC. Это противоречит популярному паттерну "попроси CC написать и код и тесты" — и вот почему.

"Когда CC пишет тесты для своего же кода — он тестирует своё понимание задачи, а не твоё. Тест может быть зелёным и при этом проверять совсем не то что ты хотел. Ты думаешь что задача выполнена, а баг уже в production."

Рекомендуемый стек для верификации:

import pytest
from hypothesis import given, strategies as st

# Код написан CC: функция нормализации email
# Тест написан ЧЕЛОВЕКОМ на основе бизнес-требований

from app.utils import normalize_email  # CC написал эту функцию

class TestNormalizeEmail:
    """
    Тесты написаны разработчиком — не CC.
    Каждый тест отражает БИЗНЕС-требование, а не реализацию.
    """

    # Property-based test: hypothesis генерирует сотни случаев
    @given(st.emails())
    def test_result_is_always_lowercase(self, email):
        """Email всегда должен быть в нижнем регистре."""
        result = normalize_email(email)
        assert result == result.lower()

    @given(st.emails())
    def test_result_has_no_leading_trailing_spaces(self, email):
        """Email никогда не должен иметь пробелы по краям."""
        result = normalize_email(f"  {email}  ")
        assert result == result.strip()

    def test_gmail_dots_are_removed(self):
        """Gmail игнорирует точки в local-части — наш нормализатор тоже."""
        assert normalize_email("si.mon.w@gmail.com") == "simonw@gmail.com"

    def test_gmail_plus_addressing_stripped(self):
        """Gmail plus-адреса нормализуются к базовому."""
        assert normalize_email("simon+test@gmail.com") == "simon@gmail.com"

    def test_preserves_non_gmail_plus(self):
        """Для не-Gmail доменов plus оставляем как есть."""
        result = normalize_email("user+tag@company.com")
        assert "+" in result  # company.com может использовать plus

    # Намеренно СЛОМАТЬ реализацию и убедиться что тест падает:
    # normalize_email = lambda x: x  # раскомментировать для проверки
    # pytest должен выдать FAILED — иначе тест бесполезен

Принцип "намеренно сломай": после написания теста временно сломай реализацию и проверь что тест падает. Зелёный тест при сломанной реализации = тест проверяет не то что надо.

Hypothesis и property-based testing

Библиотека hypothesis генерирует сотни случайных входных данных и находит граничные случаи которые ты не придумал бы сам. Именно для верификации CC-кода это особенно ценно: CC мог пропустить edge case который hypothesis обнаружит за секунды.

6
Публичный аудит: blogging как верификация

Willison ведёт simonwillison.net с 2002 года и сделал его инструментом верификации: когда он публично описывает что сделал LLM в его проектах — сообщество находит ошибки. Публичность создаёт давление точности.

Для командной разработки этот принцип трансформируется в внутренние decision logs:

# docs/cc-decisions/2026-05-10-payment-refactor.md

## Задача
Рефакторинг PaymentService для поддержки Stripe + YooKassa

## Что попросили CC сделать
"Рефакторни PaymentService чтобы добавить второй провайдер платежей.
 Используй Strategy pattern. Интерфейс должен не меняться для клиентов."

## Что CC предложил
Strategy pattern с PaymentProviderInterface.
Конкретные реализации: StripeProvider, YooKassaProvider.
PaymentService принимает провайдер через DI.

## Что мы изменили после ревью
- CC использовал abstract class вместо interface — изменили на interface
  потому что у нас есть mock для тестов и abstract class с ним плохо дружит
- CC не добавил retry logic — добавили вручную
- Имена методов изменили с execute() на charge() — более явная семантика

## Что НЕ проверяли (риск)
- Обработка webhooks от YooKassa не тестировалась на реальном sandbox
- Refund flow написан CC без покрытия тестами — TODO

## Кто делал ревью
@senior-dev — архитектура
@backend-dev — тесты
CC не участвовал в ревью своего же кода

Ресурсы Simon Willison

Ресурс Тема Почему читать
simonwillison.net/tags/llm LLM в реальных проектах Честные разборы с ошибками и выводами, не хайп
datasette.io SQLite → API Инструмент от автора, воплощение data-pipeline паттерна
PyCon US talks (YouTube) LLM для разработчиков Практические демонстрации без маркетинга
llm.datasette.io CLI для LLM Его собственный инструмент для работы с LLM из терминала

#Code with Claude 2026 — live blog

6 мая 2026 — Code with Claude Conference
Simon Willison вёл подробный live blog конференции. Источник: simonwillison.net/2026/May/6/code-w-claude-2026/

Ключевые анонсы

Наблюдения Саймона

«Лучшие результаты у команд, которые строят на automated evals + simple scaffolding. Акцент на асинхронную разработку: несколько параллельных сессий, разработчик ревьюит готовые к merge PR.»
«Design for the next model — строй фичи с расчётом на то, что следующая модель снимет текущие ограничения. Не оптимизируй под сегодняшние слабости.»

Advisor Strategy

Новый паттерн: топовая модель (Opus 4.8 или Fable 5) в роли «советчика» для более быстрых. Качество frontier-модели при кратном снижении стоимости. Claude Opus планирует, Sonnet исполняет — разработчик наблюдает за результатом.

Антипаттерны из сообщества (реальный опыт)

❌ Что убрали
  • Агрессивные auto-memory writes → 315 дублей файлов за 2 месяца
  • Parallel-agent fanout для простых задач — overhead > выгода
  • Hook-driven auto-commits → коммиты сломанного кода
  • Parallel-agent fanout для простых задач
✅ Что оставили (выжившие практики)
  • 6 плагинов: code-reviewer, brainstorming, debugging, claude-md-improver
  • 3 хука: SessionStart, PreToolUse bash-safety, PostToolUse format
  • Claude Code для 3+ файлов, Copilot для single-file completion
  • CLAUDE.md < 500 токенов — принципиально важно