Запуск стартапа — это не только идея, но и понимание, как она станет бизнесом. Lean Canvas, предложенный Эшем Маурья, помогает на одной странице структурировать ключевые аспекты: проблемы клиентов, решения, каналы продаж и издержки.
Но Lean Canvas за пять минут не заполнить: нужны гипотезы, исследования, слаженная работа команды. А что если большую часть рутины возьмёт на себя AI-агент? Мы в GigaChain решили попробовать. Рассказываем, что из этого получилось.
В Сбере мы активно внедряем искусственный интеллект для решения сложных бизнес-задач. Одно из перспективных направлений — AI-агенты: автономные системы, умеющие рассуждать, планировать и использовать инструменты для достижения цели. Мы подробно разбираем подходы к их разработке в руководстве «Разработка и применение мультиагентных систем в корпоративной среде». А в этой статье мы покажем, как создать такой агент на примере автоматического генерирования Lean Canvas.
Это не просто техническая демонстрация, а практический пример того, как с помощью GigaChat 2 Max и LangGraph можно собрать рабочее решение на современных агентных подходах, которое:
генерирует все 9 блоков Lean Canvas по краткому описанию идеи;
анализирует конкурентов через веб-поиск;
учитывает вашу обратную связь, реализуя принцип Human-in-the-Loop (HITL);
демонстрирует возможности LangGraph (ReAct-агенты, Structured Output, управление состоянием, условная маршрутизация, прерывание и возобновление графа).
Мы разберём реализацию такого агента в Jupyter-ноутбуке (ссылки на GitHub и GitVerse) и покажем, как подобные решения ускоряют бизнес-процессы и помогают принимать обоснованные решения. Погружаемся в создание AI-ассистента для Lean Canvas!
Часть 1. Подготовка к созданию AI-агента
Прежде чем перейти к работе агента, настроим окружение, подключим GigaChat и познакомимся с ключевыми концепциями: ReAct-архитектурой и структурированным выводом.
Установка и настройка
Нам понадобятся библиотеки: pip install langchain_gigachat langgraph ddgs langchain_tavily rich python-dotenv -q
langchain_gigachat
иlanggraph
— основа агента, интеграция GigaChat API и фреймворка, а также сам фреймворк для создания агентов;ddgs
,langchain_tavily
— инструменты поиска;rich
— красивый вывод результатов;python-dotenv
— для хранения ключей в .env.
Пример содержимого .env:
GIGACHAT_CREDENTIALS=ВАШ_КЛЮЧ
TAVILY_API_KEY=КЛЮЧ_ОТ_TAVILY
GIGACHAT_VERIFY_SSL_CERTS=false
GIGACHAT_SCOPE=GIGACHAT_API_CORP
Если вы ранее не работали с GigaChat API, то вам потребуется получить ключ доступа. Для этого перейдите по ссылке и следуйте инструкциям.
Подключение к GigaChat
Создаём экземпляр модели:
from langchain_gigachat.chat_models import GigaChat
llm = GigaChat(model="GigaChat-2-Max", top_p=0, timeout=120)
Проверяем подключение:
response = llm.invoke("Кто тебя создал?")
print(response.content)
Меня создала компания Sber (Сбер) в 2023 году. Разработка выполнена в России.
Первые шаги с ReAct
Простейший способ создать агента — это воспользоваться ReAct-архитектурой (читайте нашу статью на эту тему). Она позволяет модели выбирать способ получения ответа на запрос пользователя: использовать обращения к инструментам из числа доступных агенту, и/или генерировать текст ответа пользователю.
Для простоты создадим агента с одним инструментом — поисковиком:
from langgraph.prebuilt import create_react_agent
from ddgs import DDGS
from langchain.tools import tool
@tool("search_tool", description="Ищет в поисковике (RU, неделя, 5 ссылок)")
def search_tool(query: str, max_results: int = 5) -> str:
with DDGS() as ddgs:
hits = ddgs.text(query, region="ru-ru", time="w", max_results=max_results)
return "\n".join(f"{hit['title']}: {hit['body']} -- {hit['href']}" for hit in hits[:max_results])
react_agent = create_react_agent(llm, tools=[search_tool], prompt="Ты полезный ассистент")
response = react_agent.invoke({"messages": [("user", "Какая самая дорогая публичная компания в мире?")]})
print(response["messages"][-1].content)
Такой агент обладает высокой автономностью, что позволяет решать широкий круг задач с его помощью. Однако, если наша задача чётко структурирована, имеет много конкретных подзадач, то автономность ReAct-агента будет избыточной и даже затруднит контроль над логикой выполнения. Поэтому познакомимся с другими подходами.
Structured Output: сразу в нужный формат
Одной из самых полезных способностей языковых моделей является возможность получать строго структурированный ответ. Таким образом мы можем получать структурированные данные из текстов на естественном языке, чтобы передавать дальше для машинной обработки.
Рассмотрим это на примере девяти полей Lean Canvas. Используем Pydantic для определения нужной структуры:
from pydantic import BaseModel
class LeanCanvasResult(BaseModel):
"""
Представляет результат генерирования Lean Canvas.
Каждое поле соответствует разделу Lean Canvas.
"""
problem: str # Проблема, которую пытается решить продукт или услуга.
solution: str # Краткое описание предлагаемого решения.
key_metrics: str # Ключевые показатели, которые необходимо измерять для отслеживания прогресса.
unique_value_proposition: str # Единое, ясное и убедительное сообщение, объясняющее, почему вы отличаетесь от других и почему стоит покупать именно у вас.
unfair_advantage: str # То, что конкуренты не могут легко скопировать или купить.
channels: str # Пути охвата ваших клиентских сегментов.
customer_segments: str # Целевая аудитория или группы людей, которых вы пытаетесь охватить.
cost_structure: str # Основные затраты, связанные с ведением бизнеса.
revenue_streams: str # Как бизнес будет зарабатывать деньги.
Запрос к модели с этой структурой:
structured_llm = llm.with_structured_output(LeanCanvasResult)
prompt = "Создайте Lean Canvas для онлайн-платформы изучения языков, которая связывает изучающих язык с носителями языка."
result = structured_llm.invoke(prompt)
print(result)
На выходе получим вот такой результат:
LeanCanvasResult(
problem='Трудности в изучении языка, нехватка практики общения с носителями языка, отсутствие мотивации и
дисциплины, высокие цены на языковые курсы, нехватка времени на посещение традиционных языковых школ. ",\n ',
solution='Онлайн-платформа, связывающая изучающих язык с носителями языка для практики общения, доступ к
учебным материалам и курсам, возможность выбора удобного времени и формата занятий, мотивационные программы и
достижения. ",\n ',
key_metrics='Количество активных пользователей, количество проведенных уроков, коэффициент удержания
пользователей, средняя продолжительность урока, количество рефералов, доход на одного пользователя. ",\n ',
unique_value_proposition='Изучение языка с носителями в удобное время и по доступной цене, возможность выбора
формата занятий и учебных материалов, мотивационные программы и достижения, персонализированные рекомендации по
обучению. "\n}',
unfair_advantage='Уникальная система подбора преподавателей и учеников на основе интересов и целей,
персонализированные рекомендации по обучению, возможность выбора удобного времени и формата занятий, доступ к
эксклюзивным учебным материалам и курсам. ",\n ',
channels='Платформы социальных сетей, реферальные программы, контент-маркетинг, SEO, партнерские программы с
языковыми школами и университетами. ",\n ',
customer_segments='Изучающие иностранные языки, студенты, путешественники, эмигранты, корпоративные клиенты,
преподаватели и репетиторы. ",\n ',
cost_structure='Разработка и поддержка платформы, маркетинг и привлечение пользователей, оплата труда
модераторов и администраторов, юридические и бухгалтерские расходы. ",\n ',
revenue_streams='Подписка на платформу, комиссия с каждого урока, продажа дополнительных материалов и курсов,
реклама и спонсорство, корпоративные контракты. ",\n '
)
Этот подход уже даёт результат, но у него есть недостатки:
Модель генерирует всё сразу, без возможности уточнений.
Нет взаимодействия с пользователем.
Нет логики для обработки каждого блока по отдельности.
Поэтому в следующей части мы перейдём к полноценному агенту на основе LangGraph с пошаговым генерированием, возможностью обратной связи и итерированием, а также более гибким управлением логикой процесса.
Часть 2. Создание AI-агента с LangGraph
Выше мы разобрались, что генерирование Lean Canvas «одним махом» работает, но не даёт гибкости. Теперь создадим полноценого AI-агента с LangGraph — фреймворком для построения состояний и управляющих графов.
Что такое StateGraph?
В отличие от монолитного генерирования, LangGraph предоставляет возможность явно задать последовательность действий в виде графа состояний (StateGraph). Он состоит из узлов (node), каждый из которых реализует отдельный шаг или функцию агента, а также из рёбер (edge), определяющих возможные переходы между этими шагами. Вся информация о текущем прогрессе работы агента хранится в объекте состояния. Мы определим объект состояния как структуру класса LeanGraphState
, в котором будут:
основная задача пользователя (краткое описание бизнес-идеи или запроса, с которого начинается работа агента);
девять блоков Lean Canvas;
анализ конкурентов и обратная связь.
from typing_extensions import TypedDict, Annotated
class LeanGraphState(TypedDict):
main_task: Annotated[str, "Основная задача от пользователя"]
competitors_analysis: Optional[Annotated[str, "Анализ конкурентов"]]
feedback: Optional[
Annotated[
str, "Фидбэк от пользователя. Обязательно учитывай его в своих ответах!"
]
]
# Lean Canvas
problem: Optional[
Annotated[str, "Проблема, которую пытается решить продукт или услуга."]
]
solution: Optional[Annotated[str, "Краткое описание предлагаемого решения."]]
key_metrics: Optional[
Annotated[
str,
"Ключевые показатели, которые необходимо измерять для отслеживания прогресса.",
]
]
unique_value_proposition: Optional[
Annotated[
str,
"Единое, ясное и убедительное сообщение, объясняющее, почему вы отличаетесь от других и почему стоит покупать именно у вас.",
]
]
unfair_advantage: Optional[
Annotated[str, "То, что конкуренты не могут легко скопировать или купить."]
]
channels: Optional[Annotated[str, "Пути охвата ваших клиентских сегментов."]]
customer_segments: Optional[
Annotated[
str, "Целевая аудитория или группы людей, которых вы пытаетесь охватить."
]
]
cost_structure: Optional[
Annotated[str, "Основные затраты, связанные с ведением бизнеса."]
]
revenue_streams: Optional[Annotated[str, "Как бизнес будет зарабатывать деньги."]]
StateGraph делает архитектуру агента более прозрачной, управляемой и расширяемой: вы всегда видите, на каком шаге находится агент, какие данные уже собраны, а какие ещё предстоит сгенерировать или уточнить.
Генерирование блоков Lean Canvas
Каждый блок Lean Canvas генерируется через отдельную функцию-узел. Для этого мы внутри функции-узла используем общую функцию ask_llm
, которая обращается к GigaChat с текущим состоянием и конкретным вопросом, соответствующим генерируемому блоку Lean Canvas. Любой узел в LangGraph принимает на вход текущее состояние графа, а на выходе возвращает обновлённое состояние.
Пример генерирования сегмента клиентов:
def customer_segments(state, config):
return {"customer_segments": ask_llm(state, "Кто ваши целевые клиенты?", config)}
Здесь функция для сегмента клиентов принимает состояние и формирует новый ответ, обращаясь к LLM с запросом: «Кто ваши целевые клиенты?» Полученный ответ записывается в поле customer_segments
, а остальные части состояния остаются без изменений. Такой подход делает процесс генерирования гибким и управляемым — каждую часть Lean Canvas можно дополнять, уточнять или перегенерировать независимо от других.
Так мы определим узлы и для остальных блоков (problem, UVP, solution и т. д.), а LangGraph сам обновит состояние с помощью обращений к LLM.
Проверка уникальности UVP
После генерирования уникального ценностного предложения (UVP) мы проверим его уникальность через веб-поиск (Tavily). Агент:
ищет по UVP;
передаёт результаты в GigaChat;
получает анализ и флаг
is_unique
;либо переходит к следующему шагу, либо возвращается к перегенерированию UVP.
if res.is_unique:
# Если предложение уникально, переходим к следующему шагу
return Command(
update={"competitors_analysis": competitors_analysis.strip()},
goto="4_solution",
)
else:
# Если предложение не уникально, возвращаемся к шагу "3_unique_value_proposition"
return Command(
update={"competitors_analysis": competitors_analysis.strip()},
goto="3_unique_value_proposition",
)
В этом фрагменте кода использована специальная структура Command
, которую можно подставлять в return
функции-узла, тем самым описывая обновление состояния и к выполнению каких узлов должен перейти агент далее. Для этого у Command
есть два аргумента: update
— словарь с изменениями состояния, и goto
— идентификатор следующего шага графа. Такой способ условного соединения узлов между собой позволяет гибко задавать логику переходов между ними. Более привычный способ сборки узлов воедино мы рассмотрим в главе «Сборка графа».
Можно проверить, как работает модуль проверки уникальности отдельно от остального графа. Для этого запустим следующий код:
state = {"unique_value_proposition": "Бесконтактная оплата айфоном в России"}
config = {"configurable": {"skip_search": False}}
print(check_unique(state, config))
В результате выполнения я получил следующий результат:
Command(update={'competitors_analysis': 'Бесконтактная оплата айфоном в России - Конкурентами являются решения
банков, такие как приложение "Активы Онлайн" от Сбера и сервис "Вжух". Оба сервиса предлагают бесконтактную оплату
через iPhone в России, поддерживая разные типы банковских карт и работая даже без постоянного соединения с
интернетом. Основное отличие нашего предложения должно заключаться либо в технологии реализации платежа, либо в
дополнительном функционале, который сделает процесс удобнее или безопаснее для пользователей.'},
goto='3_unique_value_proposition')
Что здесь произошло? Идея "бесконтактной оплаты в России айфоном" была бы уникальной на момент, когда была обучена модель (например, апрель 2025), но в августе 2025 Сбер запустил бесконтактную оплату айфонами "Вжух" и идея перестала быть уникальной. Агент нашел информацию об этом в интернете и принял решение вернуться к шагу 3_unique_value_proposition
, чтобы придумать новые уникальные идеи.
Обратная связь от пользователя
Когда все блоки заполнены, агент делает паузу (через interrupt
) и просит пользователя оценить результат. Далее GigaChat анализирует обратную связь и решает завершать работу (goto=END
) или вернуться к нужному блоку для доработки (goto="7_cost_structure"
и т. п.).
Это позволяет реализовать принцип «human-in-the-loop», когда разработчик или пользователь может вмешаться в ход работы агента задним числом и инициировать новую ветку выполнения. Для того, чтобы реализовать вызов функции interrupt()
внутри узла, необходимо сохранение состояния графа через чекпоинтер, что подробнее рассмотрим в следующей главе. Возобновление выполнения графа происходит через Command(resume=…)
. Значение, переданное в resume
, станет результатом функции interrupt
, а узел выполнится заново с учётом этого ответа.
Сборка графа
Финальный шаг — связать все узлы в единую структуру. Для этого нужно объявить объект класса StateGraph
, передав в качестве аргумента объект-состояние (в нашем случае это LeanGraphState
). Затем нужно добавить все функции-узлы с помощью метода add_node()
. Далее заданные узлы необходимо связать друг с другом. Чтобы реализовать безусловные переходы между ними, нужно использовать метод add_edge
.
graph = StateGraph(LeanGraphState)
graph.add_node("1_customer_segments", customer_segments)
graph.add_node("2_problem", problem)
graph.add_node("3_unique_value_proposition", unique_value_proposition)
graph.add_node("3.1_check_unique", check_unique)
...
graph.add_edge(START, "1_customer_segments")
graph.add_edge("1_customer_segments", "2_problem")
...
graph.add_edge("9_unfair_advantage", "get_feedback")
Узлы с ветвлением (check_unique
, get_feedback
) возвращают Command
, поэтому их переходы задаются динамически. Однако в LangGraph можно реализовать логику условных переходов и посредством метода add_conditional_edges
. Он позволяет задать для любого узла функцию маршрутизации (routing function), которая после выполнения узла анализирует текущее состояние и определяет, к какому следующему узлу (или узлам) должен перейти агент.
Готовый граф компилируется в приложение:
app = graph.compile(checkpointer=MemorySaver())
Класс MemorySaver
— это объект для хранения состояний графа. Каждый шаг, сопровождающийся переходом от узла к узлу, сопровождается изменением состояния графа, которое сохраняется в виде контрольной точки класса StateSnapshot
. Все контрольные точки в рамках одной сессии работы агента объединяются в один тред (thread) с общим идентификатором.
Использование StateSnapshot
позволяет не только посмотреть, каким было состояние агента на каждом этапе работы, но и, при необходимости, вручную откорректировать это состояние или перезапустить выполнение графа с любой выбранной контрольной точки. Такой механизм облегчает отладку, анализ траекторий и реализацию сценариев «human-on-the-loop».
Скомпилированный граф можно визуализировать средствами Mermaid. Получилась вот такая сложная структура с циклами через проверку уникальности предложения в Интернете и обратную связь с пользователем.

Часть 3. Как работает агент: от запуска до финального Lean Canvas
После сборки графа пора его «оживить»: запустить и посмотреть, как агент проходит по узлам, запрашивает обратную связь и дорабатывает результат.
Запуск и взаимодействие
Для запуска графа мы используем вспомогательную функцию execute_graph
. Она:
запускает граф с помощью app.stream();
выводит на экран шаги выполнения;
обрабатывает прерывания (
__interrupt__
), например, когда агент ждёт от нас обратную связь;принимает наш ответ и возобновляет выполнение.
inputs = {
"main_task": "Онлайн-платформа для изучения английского с AI-агентами, дающими персонализированную обратную связь."
}
conf = {
"configurable": {
"thread_id": str(uuid.uuid4()),
"model": "GigaChat-2-Max",
"need_interrupt": True,
"skip_search": False
}
}
execute_graph(inputs, conf)
В процессе работы агента:
генерирует все девять блоков Lean Canvas;
проверяет уникальность идеи через веб-поиск;
приостанавливается и просит нас дать комментарии к результату;
при необходимости возвращается к нужному узлу и перегенерирует ответ с учётом обратной связи.
Это реализует принцип human-in-the-loop: мы остаёмся в процессе и можем корректировать работу агента.
Получение результата
Когда работа графа завершена, можно получить итоговое состояние:
final_state = LeanGraphState(**app.get_state(config=conf).values)
Для визуализации можно вывести нужные поля:
for key, value in final_state.items():
if key not in ["main_task", "feedback", "competitors_analysis"] and value:
print(f"{key}: {value}")

Часть 4. Как оценить работу AI-агента: журналирование и анализ
Создание агента — это только первый шаг. Чтобы понять, насколько он хорош, и где его нужно доработать, важно ввести систему оценки. Мы покажем, как использовать Arize Phoenix — open-source платформу для анализа качества работы LLM-приложений.
Запуск Phoenix и загрузка данных
Установим и запустим интерфейс Phoenix:
!pip install arize-phoenix opentelemetry-exporter-otlp openinference-instrumentation-langchain -U -q
import phoenix as px
session = px.launch_app(use_temp_dir=False)
Phoenix будет доступен по адресу http://localhost:6006/
.
Загрузим датасет с бизнес-идеями из файла (пример есть у нас в репозитории, но можно и создать свой):
with open('validation_data.txt', 'r') as f:
questions = [line.strip() for line in f if line.strip()]
import pandas as pd
dataset_df = pd.DataFrame({"question": questions})
client = px.Client()
dataset = client.upload_dataset(
dataframe=dataset_df,
dataset_name="lean_canvas_questions",
input_keys=["question"]
)
Запуск агента применительно к датасету
Для оценки мы создаём эксперимент, в рамках которого агент обрабатывает каждую строку из датасета. Вот упрощённая функция:
from phoenix.experiments import run_experiment
from phoenix.experiments.types import Example
import uuid
def create_experiment(name, model):
def run(example: Example):
inputs = {"main_task": example.input["question"]}
conf = {
"configurable": {
"thread_id": str(uuid.uuid4()),
"model": model,
"need_interrupt": False,
"skip_search": True
}
}
result = app.invoke(inputs, config=conf)
result.pop("main_task", None)
return result or {}
return run_experiment(
dataset=client.get_dataset(name="lean_canvas_questions"),
run_fn=run,
evaluators=[check_structure],
experiment_name=name
)
Оценка структуры
Простая функция оценки: проверяем, что результат содержит все блоки Lean Canvas, а его длина находится в допустимых границах. Это самая простая и грубая оценка того, что результат был сформирован.
import json
def check_structure(_, output) -> bool:
try:
result = json.dumps(output, ensure_ascii=False)
return 400 < len(result) < 3000
except:
return False
Если такой проект нужно будет вывести в прод, то, конечно, здесь потребуется сделать полноценную проверку структуры: проверку того, что каждое поле заполнено и имеет нужный формат и размер.
Оценка качества через LLM (LLM as Judge)
Если формальные правила не подходят, то можно попросить другую LLM оценить результат по шкале от 1 до 5:
from langchain_core.prompts.chat import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
def check_output_quality(output) -> float:
text = "\n".join(f"{k}: {v}" for k, v in output.items() if v)
prompt = ChatPromptTemplate.from_messages([
("system", "Оцени качество заполнения Lean Canvas от 1 до 5. Верни только число."),
("human", "{output}")
])
chain = prompt | llm | StrOutputParser()
try:
return float(chain.invoke({"output": text}).strip())
except:
return 1.0
Здесь стоит сделать оговорку. Такой способ оценки подходит для учебного примера, но на практике не рекомендован, потому что здесь нет информации о том, как именно модель должна проставлять оценки. В каких случаях ставить 5, а в каких 3? Известный специалист по LLM Evaluation Hamel Husain рекомендует вообще не использовать скалярные оценки, а вместо этого применить несколько бинарных.
Сравнение моделей
Теперь можно запустить сравнение моделей:
experiment1 = create_experiment("Lean Canvas GigaChat-2", "GigaChat-2")
evaluate_experiment(experiment1, evaluators=[check_output_quality])
experiment2 = create_experiment("Lean Canvas GigaChat-2-Max", "GigaChat-2-Max")
evaluate_experiment(experiment2, evaluators=[check_output_quality])
После этого в интерфейсе Phoenix можно:
посмотреть баллы по каждому примеру;
сравнить среднее качество генерирования между моделями;
увидеть, где происходят ошибки или слабые ответы.

На что стоит обратить внимание
LLM как судья: лучше использовать внешнюю, более мощную модель, а не ту же, что генерирует, к примеру Claude Sonnet 4. При этом важно чётко формулировать критерии и промпт, иначе оценки могут быть случайными и вводить в заблуждение.
Градиентные оценки не всегда стабильны. В реальных проектах лучше использовать бинарную оценку (хорошо или плохо) или выбор из двух вариантов.
Качество данных для проверки влияет на результат больше, чем модель.
Проверка — это процесс, а не одноразовое действие. Внесли изменения — проверили снова.
Заключение
Мы показали, как на основе GigaChat и LangGraph можно создать AI-агент, который сам заполняет Lean Canvas, анализирует конкурентов, учитывает обратную связь и работает по принципу human-in-the-loop. Это не просто автоматизация формы, а пример того, как современные агентные подходы могут усиливать аналитические и творческие процессы.
Что важно:
LangGraph даёт контроль над логикой, состояниями и взаимодействиями внутри агента.
GigaChat справляется не только с генерированием текста, но и с рассуждениями, структурой и адаптированием под задачи.
Structured Output и Pydantic делают интеграцию с LLM удобной и надёжной.
Валидация через Phoenix помогает объективно оценивать работу агента и сравнивать модели.
Мы верим, что такие решения — это пример правильных практик построения интеллектуальных помощников. Их можно адаптировать под отрасль, встроить в бизнес-процессы и развивать в сторону мультиагентных систем, комбинируя разные агенты для решения сложных задач.
Заглядывайте в репозитории команды GigaChain на GitHub и GitVerse. Там собраны разнообразные инструменты для создания LLM-приложений и мультиагентных систем:
библиотеки для интеграции GigaChat API с LangChain и LangGraph;
утилиты и MCP-серверы, расширяющие возможности агентов;
руководства с примерами разработки агентов на Python, TypeScript/JavaScript и Java.
Спасибо за внимание — и успешных AI-экспериментов! За подготовку данной статьи отдельное спасибо @trashchenkov
? Полезные ссылки
Мой TG канал, где делюсь мыслями про AI, роботов и наступающую сингулярность