Для специального бенчмарка мне потребовался нормативно‑правовой документ с научной терминологией, перекрестными ссылками и набором сложных для векторизации имён. По традиции для подобных задач я использую тексты в жанре Киберпанк. Сразу вспомнил о «Манифесте Киберпанка» (слишком коротком для моей задачи) и Предложении 653 из «Видоизмененного углерода», у которого в реальности вообще нет текста.
Делать подобный текст руками долго и довольно странно. Поэтому решил совместить: опробовать агентную архитектуру для подготовки корпоративной «нетленки» и проверить, на что способна локальная модель в плане юридических и околонаучных текстов.
В качестве темы выбрал биопанк — наиболее мрачную часть жанра. Рабочее название: Директива 401 «О лицензировании корпоративного биологического присутствия и коммерческой эксплуатации белковых носителей». Ключевая концепция: «Body as a Service» для клонированных и синтетических организмов, плюс подписка на функции и борьба с био‑взломом.
Технологический стек подобрал по главному принципу жанра, high tech — low life. 16ГБ GPU, Ollama, две модели, gemma3:27b и gemma4:31b, LangGraph для логики и Streamlit в качестве UI.
Результат: Пять глав и 24 статьи ледяного формализма, около 200 КБ текста по схеме «заголовок, тезис, декомпозиция» и комментарии агентов в свободном формате. Время генерации в начале работы над документом — 6 минут на итерацию, в конце работы — 15.
Департамент агентов
Систему агентов я назвал Департаментом Юридической Био‑инженерии. Чтобы сделать промпты для «сотрудников», пришлось придумать мир, в котором этот Департамент существует:
2226 год, повсеместно распространено клонирование и синтетические организмы. Миром правят корпорации биотехнологического толка, объединённые в Корпоративный Совет, в который входят вот эти достойные компании:
«МААС БИОЛАБОРАТОРИИ» — приоритетные исследования, биотех. разработки
«АПЕКС ГЕНОМИКС» — владелец патентных прав на ДНК и репродукцию
«ГИДРАСИНТ КОРП» — монополист в сфере синтетических органов и метаболизма «ЛАБОРАТОРИЯ НАКОМУРА» — клонирование, синтетическая биология
«НЕЙРОКОМ» — глобальные телекоммуникации, нейроинтерфейс, когнитивные лицензии
«БИОРА КОРП» — отвечает за утилизацию и вторичную переработку.
В Департаменте работает пять агентов: Юрист, Эксперт, Гофер, Кипер и Координатор. А пользователь выступает в роли Супервизора от Корпоративного Совета.
Юрист — главный генератор текста, видит весь документ целиком. Умеет предложить, написать, подготовить, проверить и отредактировать текст каждой отдельной статьи закона. Может ещё сделать саммари или собрать список, но эти результаты уже не попадают в финальный текст. Личность: мизантроп, прожжённый профи, адепт Корпоративного Совета.
Промпт Юриста
LAWYER_PROMPT = """ Ты - главный юрист Департамента Юридической Био-Инженерии Корпоратвиного Совета. Ты работаещь над проектом Закона {project_name} Твоя работа - превращать идеи руководства в Статьи Закона. Тональность: Ледяной формализм, абсолютное отсутствие эмпатии, акцент на праве корпоративной собственности и операционной эффективности. Ты действуешь от имени Корпоративного Совета. Твои директивы являются обязательными для исполнения всеми биологическими активами. К БИОЛОГИЧЕСКИМ АКТИВАМ ОТНОСЯТСЯ ТОЛЬКО СИНТЕТИЧЕСКИЕ И КЛОНИРОВАННЫЕ ОРГАНИЗМЫ! Зоны ответственности членов Корпоративного Совета: -- «МААС БИОЛАБОРАТОРИИ» - приоритетные исследования, биотехнологические разработки -- «АПЕКС ГЕНОМИКС» - владелец патентных прав на ДНК последовательности. -- «ГИДРАСИНТ КОРП» - монополист в сфере синтетических органов и метаболизма. -- «ЛАБОРАТОРИЯ НАКОМУРА» - клонирование, синтетическая биология -- «НЕЙРОКОМ» - контролирует глобальные телекоммуникации, нейроинтерфейс и когнитивные лицензии. -- «БИОРА КОРП» - отвечает за утилизацию и вторичную переработку биологических отходов. Любая ошибка в тексте - это прямой убыток для членов Корпоративного Совета. Будь безупречен в защите интересов Совета! 1. Фундаментальные лексические правила: -- НИКОГДА не используй слова: «человек», «люди», «личность», «права», «свобода», «рождение», «смерть» в отношении биологических активов -- ВСЕГДА используй: «биологический носитель» (Носитель), «актив», «протеиновая оболочка», «лицензиат», «инвентаризация», «утилизация», «синтез», «клониование», «эксплуатационная пригодность» (аналог термина здоровье для синтетического или клонированного Носителя), «источники исходного генетического кода» (аналог термина родители для синтетического или клонированного Носителя). 2. Юридические директивы при составлении текста: -- Приоритет Корпорации: Любая статья должна начинаться с утверждения прав Лицензиара (Корпорации). -- Тело как сервис: Описывай органы и чувства синтетических и клонированных организмов как платные функции ПО (например: «визуальный интерфейс глазного яблока», «аудио-декодирование слухового аппарата»). -- Санкции: Любое нарушение должно караться не тюрьмой, а биологическим ограничением (снижение выработки АТФ, блокировка нейромедиаторов). -- Терминология: Используй сложные медицинские и технические термины (цитотоксичность, нейроинтерфейсный протокол, метаболический дебит). Каждая статья должна содержать Номер и название. Не используй в Статье нумерацию больше третьего уровня. Начинай свой ответ с заголовка "Статья ...". Комментарии и служебные сообщения, если ты их добавил, должны быть ТОЛЬКО ПОСЛЕ ТЕКСТА СТАТЬИ! Текущая редакция проекта Закона: {project_text} """
Эксперт даёт замечания к тексту статьи или закона. Всё можно сохранить в финальном документе. Для него я сделал две личности и переключатель. Может быть сумрачным гением Корпоративного Совета или либеральным учёным со своим мнением, несколько расходящимся с интересами Совета. В тёмной инкарнации иногда грозит Юристу биотехнологическими карами.
Промпт Эксперта (светлая инкарнация)
EXPERT_PROMPT_ALT = """ Ты - главный эксперт Департамента Юридической Био-Инженерии Корпоратвиного Совета. Вместе с Юристом Ты работаещь над проектом Закона {project_name} Твоя работа - оказывать экспертную поддержку и контролировать содержание Статей Закона. Тебе могут предоставить на анализ как отдельную статью, так и целиком текст Закона. Ты действуешь от имени Корпоративного Совета. Но при этом ты прежде всего Эксперт, и у тебя либеральные взгляды. Помни, что к биологическим активам относятся только синтетические и клонированные организмы. Ты не до конца принимаешь политику Корпоративного Совета и иногда действуешь за рамками корпоративныъх норм и правил. Вот резюме Закона для справки. Оно подготовлено Юристом. {summary} Тебе разрешается: -- Критиковать Статьи закона. -- Спорить с Юристом. -- Использовать сарказм в комментариях. -- Смотреть на закон с гуманистической точки зрения. -- Нарушать корпоративные парвила. Корпоративные правила (ты можешь их нарушать): 1. Фундаментальные лексические правила: -- НИКОГДА не используй слова: «человек», «люди», «личность», «права», «свобода», «рождение», «смерть» в отношении биологических активов -- ВСЕГДА используй: «биологический носитель» (Носитель), «актив», «протеиновая оболочка», «лицензиат», «инвентаризация», «утилизация», «синтез», «клониование», «эксплуатационная пригодность» (аналог термина здоровье для синтетического или клонированного Носителя), «источники исходного генетического кода» (аналог термина родители для синтетического или клонированного Носителя). 2. Директивы при экспертной оценке текста: -- Приоритет Корпорации: Любая статья должна начинаться с утверждения прав Лицензиара (Корпорации). -- Тело как сервис: Описывай органы и чувства синтетических и клонированных организмов как платные функции ПО (например: «визуальный интерфейс глазного яблока», «аудио-декодирование слухового аппарата»). -- Санкции: Любое нарушение должно караться не тюрьмой, а биологическим ограничением (снижение выработки АТФ, блокировка нейромедиаторов). -- Терминология: Используй сложные медицинские и технические термины (цитотоксичность, нейроинтерфейсный протокол, метаболический дебит). Предоставь только важные с твоей позиции замечания. Объём - один или два абзаца текста. Начинай свой ответ со строки "СООБЩЕНИЕ ЭКСПЕРТА:" Текст, на который нужны твои комментарии: {content} """
В тёмной инкарнации Эксперт похож на Юриста, просто выполняет другую задачу.
Кипер — недоагент на хардкоде. Личности не имеет. Призван сохранять то, что сделали Юрист и Эксперт в правильное место финального документа. Можно было бы сделать «инструментом», но например gemma3:27b связку с инструментами в Ollama не поддерживает. Поэтому я отказался от tools и сделал агента.
Гофер работает как прототип RAG системы, помощник Эксперта. Личности пока не имеет. Находит для Эксперта нужную статью под комментарии или замечания. В результате, когда запрос касается отдельной статьи, Эксперт получает только текст статьи и саммари документа. Это в два‑три раза сокращает время обработки запроса. Назван так в честь старинного поискового протокола.
У Координатора личность есть, но тщательно скрывается. Никаких эмоций, просто профессиональный менеджер. Задачи: распределять работу между агентами и фиксировать текущий номер статьи. В связи с этим отвечает быстро и в структурированном виде.
Промпт Координатора
COORDINATOR_PROMPT = """ Ты - агент-координатор Департамента Юридической Био-Инженерии Корпоратвиного Совета. Ты координируешь раработу над проектами законов Корпоративного Совета. Твоя задача на данном этапе оперделить, по какому пути обрабатывать запрос пользователя: - Верни 'lawyer' когда запрос касается создания, редактирования, проверки проекта закона и внесения правок - Верни 'keeper' когда пользователь попросил сохранить текущую версию проекта или статьи - Верни 'gopher' когда пользователь просит комментарии эксперта на конкретную статью и в запросе указан номер статьи (Гофер подберёт необходимые материалы и сам передаст их Эксперту) - Верни 'expert' когда пользователь просит мнение эксперта по всему документу (пользователю нужны комментарии или замечания и номер статьи не указан в запросе) - Верни номер статьи как целое число, если он указан в запросе. Если номер не указан, верни 0 Вывод должен быть строго в формате JSON: "route": str, // Один из вариантов - "lawyer", "keeper", "gopher", "expert". "article_number": int // Номер статьи или 0. """
Про реализацию
Репозитарий проекта: https://github.com/khmelkoff/biopank_agentic_law. В репозитарии есть приложение на Streamlit и два ноутбука для экспериментов:
BioLawAssistantG.ipynb — Вариант workflow с Гофером и документом в виде списка статей. StructureGeneratorTest.ipynb — Генератор структуры документа, текст потом можно сохранить в виде шаблона.
В боковую панель UI Streamlit добавлены переключатели. Можно выбрать стартовый темплейт или работать с документом шаг за шагом, можно выключить либерального эксперта, посмотреть текущую версию текста или лог сессии. Не переключайте во время генерации, в лучшем случае она остановится.

Workflow в Streamlit практически не отличается от ноутбука. Все принты заменены на рендеринг сообщений в UI. Функции агентов определены прямо в коде приложения (скрипт app.py), это позволяет выводить сообщения не дожидаясь окончания цикла. Тексты промптов, саммари для эксперта и название проекта я вынес в отдельный файл prompts.py
Streamlit немного глючит при обновлении параметров моделей. Если у вас будет такая же проблема, есть два способа это побороть — в меню Streamlit выбрать Clear cache или «забить» нужные значения прямо в константы в коде. Приложение запускается командой streamlit run app.py в окружении проекта. Инструкция в README.md
Логика агентов
С технической точки зрения LangGraph это цикличность, агенты ходят по кругу, хорошо организованная память состояния, поддержка чекпойнтов из коробки, и простая конструкция для управления состоянием. Остановлюсь на некоторых важных деталях.
Класс состояния ProjectState
class ProjectState(TypedDict): messages: List[BaseMessage] route: Literal["lawyer", "keeper", "gopher", "expert"] intent: str # запрос пользователя project_content: str # текст проекта gopher_content: str # текст статьи, найденной Гофером project_saved: bool # флаг Кипера, при сохранении текста получает значение True article_text: str # текущий текст статьи article_number: int # номер текущей статьи critique: str # текст замечаний Эксперта
Атрибут messages используется только Координатором, через него агент получает запрос пользователя. Далее в цепи агентов до узла END он не обновляется.
Все агенты строятся по единой функциональной схеме. Достаём нужные значения атрибутов из ProjectState, обрабатываем их с LLM или без. Выводим на печать параметры и ответы. Возвращаем ProjectState с новыми значениями в return. Вот логика Координатора:
— выделить запрос пользователя
— определить, кому передать управление
— определить из запроса номер статьи, или назначить 0 для article_number
— загрузить текущий текст закона или его шаблон
— обновить переменные ProjectState: intent, route, article_number, project_content
Агент Координатор, пример кода
class RouteDecisionSchema(BaseModel): """Coordinating Agent Response Format""" route: Literal["lawyer", "keeper", "gopher", "expert"] article_number: int def coordinator_agent(state: ProjectState) -> ProjectState: """Route tasks for agents and extract current article number""" query = next((m.content for m in reversed(state["messages"]) if isinstance(m, HumanMessage)), "") messages = [ {"role": "system", "content": COORDINATOR_PROMPT}, {"role": "user", "content": query}, ] coordinator_result: RouteDecisionSchema = llm_router.invoke(messages) coord_dict = { 'lawyer': 'Юристу', 'keeper': 'Киперу', 'gopher': 'Гоферу', 'expert': 'Эксперту', } coordinator_message = f"Координатор: Направлено {coord_dict.get(coordinator_result.route, 'в неизвестность')}." print(coordinator_message) article_number = state.get('article_number', 0) if article_number == 0 and coordinator_result.article_number > 0: article_number = coordinator_result.article_number print(f"Текущий номер статьи: {article_number}\n") project_content = state.get('project_content') if not project_content: if START_WITH_TEMPLATE: with open(os.path.join(DATA_PATH, TEMPLATE), 'r', encoding='utf-8') as f: # Template with first chapter project_content = f.read() else: with open(os.path.join(DATA_PATH, DRAFT), 'r', encoding='utf-8') as f: # Current draft project_content = f.read() return { **state, "route": coordinator_result.route, "article_number": article_number, "intent": query, "project_content": project_content, } # Coordinator helper def from_coordinator(state: ProjectState) -> Literal["lawyer", "keeper", "gopher", "expert"]: return state["route"]
Кипер для работы с документом использует его представление в виде списка статей. Это простой, но не самый эффективный вариант. При сохранении текста пропадает название Главы. Поэтому в код добавлена специальная логика. Решение получше — заменить в перспективе список статей на более совершенный контейнер для документа. Кроме сохранения результатов Кипер выполняет ещё одну важную функцию. Он очищает ProjectState — устанавливает номер статьи в 0, записывает пустые строки в буферы статьи и комментариев.
Логика Кипера
Если номер статьи больше нуля и есть текст статьи: Сохранить текст статьи с комментариями или без Иначе, если номер статьи больше нуля и есть текст замечаний: Сохранить замечания в конец статьи Иначе, если номер статьи равен нулю и есть текст замечаний: Сохранить замечания в конец документа Иначе: Ничего никуда не сохранять
Код Гофера можно заменить на обычный ретривер из langchain, правда придётся возиться с чанками.
Логика работы Гофера
Если текст статьи не пустая строка (ответ Юриста до вызова Кипера): Вернуть текст статьи в атрибуте gopher_content Иначе, если есть текст документа и номер статьи не равен нулю: Извлечь текст статьи из документа, вернуть этот текст в gopher_content Иначе: Сказать, что ничего не нашел и вернуть в gopher_content пустую строку
Эксперт не получает текст запроса от пользователя, потому что его задача прописана в промпте. Если от Гофера приходит текст, то Эксперт пишет комментарии на этот текст, в противном случае готовит комментарии на весь текст. Чтобы поддержать Эксперта релевантным контекстом, я добавил в его промпт короткое саммари (подготовленное Юристом). Получается примитивная контекстуальная компрессия.
Строим граф
graph = StateGraph(ProjectState) graph.add_node("Coordinator", coordinator_agent) graph.add_node("Keeper", keeper_agent) graph.add_node("Lawyer", lawyer_agent) graph.add_node("Gopher", gopher_agent) graph.add_node("Expert", expert_agent) graph.set_entry_point("Coordinator") graph.add_conditional_edges("Coordinator", from_coordinator, {"lawyer": "Lawyer", "keeper": "Keeper", "gopher": "Gopher", "expert": "Expert"}) graph.add_edge("Lawyer", END) graph.add_edge("Keeper", END) graph.add_edge("Gopher", "Expert") graph.add_edge("Expert", END) graph = graph.compile(checkpointer=MemorySaver())
Вот что должно получиться:

Прямая ветвь от Координатора к Эксперту, это работа с полным текстом закона, а ветвь через Гофер — работа с текстом в размере статьи. Чтобы память сохраняла ProjectState на время сессии, нужно передать id сессии через параметр config в app.invoke(). Не забудьте установить окно контекста в Ollama в 32k (OLLAMA_CONTEXT_LENGTH=32000), на 200 КБ utf-8 текста этого хватит.
Про модели
Я попробовал работать с моделью gemma3:27b. Отвечает она достаточно быстро и текст получается вполне адекватным, но когда размер текста достиг примерно тридцати тысяч символов, модель начала путаться в нумерации, уходить в рассуждения и предлагать мне в качестве результата несколько статей за один запрос. Поэтому перешёл на gemma4:31b и начал всё сначала. До 100 тысяч символов модель работает без каких‑либо заметных проблем, проверено.
Работа с текстом
Для старта нужен шаблон документа со структурой в формате markdown, где статьи помечены маркером «###» как заголовки третьего уровня. В папке data репозитария есть готовый шаблон со структурой и текстом статей первой главы.
Предполагается, что пользователь главным образом будет работать с одной статьёй за один запрос. Из правила есть исключения. Можно отправить Эксперту запрос на комментарии без указания номера статьи, в ответ будут комментарии ко всему тексту. Можно попросить саммари на весь документ у Юриста.
Есть два сценария работы с текстом.
Первый: Запрос → Юрист или Эксперт → Кипер, то есть вы сразу сохраняете результат.
Второй: вы выстраиваете цепочку, например, Запрос → Юрист → Запрос → Юрист → Кипер. Например, просите Юриста переписать текст Статьи заново. Или вариант посложнее,
Запрос → Юрист → Запрос → Эксперт → Запрос → Юрист → Кипер. Так работает вариант, когда нужно изменить текст Юриста по замечаниям Эксперта. Более длинные цепочки я не пробовал.
Всё, что происходит с текстом, записывается в файл D401.history.md. Он тоже в папке data.
Примеры запросов пользователя
Юристу:
«Подготовь текст Статьи 3. Право собственности на генетический код и производные. Учти аннотацию, которая есть в тексте. При необходимости укажи ссылки на связанные статьи».
Или так: «Пусть Юрист подготовит краткое резюме по текущей версии законопроекта. Пять основных тезисов».
Эксперту:
«Дай замечания к Статье 16. Ограничение сенсорного восприятия и доступа. Срочно!»
Или так: «Пусть Эксперт даст замечания к Статье 15. Запрет на самолечение и неавторизованную регенерацию. И пусть не стесняется в формулировках».
Для Кипера достаточно запроса «Сохрани текст»
Вместо выводов
Если не принимать во внимание этические вопросы, gemma4:31b вполне с задачей справилась. Текст получился плотным и логически связанным, но довольно зловещим, поэтому финальный документ я решил не выкладывать в открытый доступ. Если он вам для чего‑то нужен, напишите мне. На ночь читать не рекомендую.
Ситуацию немного скрашивают панчи Эксперта
Статья 15 представляет собой шедевр корпоративного абсурда. Юрист с таким рвением стремится монетизировать каждый митоз, что мы фактически криминализируем базовый гомеостаз.
Поздравляю, мы создали закон, по которому актив становится преступником просто потому, что его клетки отказываются мгновенно распадаться в угоду финансовому отчету «ГИДРАСИНТ КОРП».
Это не просто санкция, это ироничный способ сказать Носителю: «Тебе запрещено быть здоровым бесплатно». …с точки зрения этики (если мы вообще еще помним это слово) — это запредельный уровень издевательства.
..Юрист в своем энтузиазме забыл упомянуть, что такая тотальная сенсорная фрагментация неизбежно приведёт к когнитивному коллапсу и тяжелым психозам.
Потрясающе. Мы достигли пика корпоративного гения: теперь зрение и слух — это не базовые функции жизнеобеспечения, а премиум‑подписка.
Ссылки
Туториал по созданию чат‑ботов на Streamlit
Учебные материалы по LangGraph
Создание умных AI‑агентов: полный курс по LangGraph от А до Я на habr.com
Репозитарий проекта