Blue Flower

Шаблоны мультиагентных систем

Наш предыдущий агент стал большим шагом вперед. Он может использовать инструменты для получения данных в режиме реального времени, что очень важно. Проблема в том, что это, по сути, одноразовая операция: он решает, что ему нужен инструмент, вызывает его один раз, а затем пытается ответить.

 

Но что происходит, когда проблема становится более сложной и требует для решения множества взаимозависимых шагов?

 

ReAct (Reason + Act) — это создание цикла. Он позволяет агенту динамически обдумывать дальнейшие действия, например, выполнять операцию (например, вызывать инструмент), наблюдать за результатом, а затем использовать эту новую информацию для повторного анализа . Это переход от статического механизма вызова инструментов к адаптивному механизму решения проблем.

 

В любой системе искусственного интеллекта ReAct — это основной шаблон для решения задач, требующих многошагового логического вывода.

 

Давайте разберемся, как протекает этот процесс.

 

 

Рабочий процесс ReAct (создан пользователем)

Фарид Хан

)

Цель получения: Агенту поручают сложную задачу, которую нельзя решить за один шаг.

Мышление (Размышление): Агент генерирует мысль, например: «Чтобы ответить на этот вопрос, мне сначала нужно найти информацию X».

Действие: Исходя из этой мысли, оно выполняет действие, например, вызывает инструмент поиска по запросу «X».

Обратите внимание: агент получает результат для параметра 'X' обратно от инструмента.

Повторение: система берет новую информацию и возвращается к шагу 2, думая: «Хорошо, теперь, когда у меня есть X, мне нужно использовать его, чтобы найти Y». Этот цикл продолжается до тех пор, пока не будет достигнута конечная цель.

Хорошая новость в том, что мы уже создали большую часть компонентов. Мы будем повторно использовать AgentState, web_search, и tool_node. Единственное изменение в нашей логике графа: после tool_nodeвыполнения мы отправляем результат обратно в agent_nodeвместо завершения. Это создает цикл рассуждений, позволяя агенту просматривать результаты и выбирать следующий шаг.

 

def react_agent_node ( state: AgentState ): 

    """Узел агента, который думает и принимает решение о следующем шаге."""

     console. print ( "--- REACT AGENT: Thinking... ---" ) 

    response = llm_with_tools.invoke(state[ "messages" ]) 

    return { "messages" : [response]} 

 

# Граф ReAct с характерным циклом

 react_graph_builder = StateGraph(AgentState) 

react_graph_builder.add_node( "agent" , react_agent_node) 

react_graph_builder.add_node( "tools" , tool_node) # Мы повторно используем tool_node из предыдущего примера

 

 react_graph_builder.set_entry_point( "agent" ) 

react_graph_builder.add_conditional_edges( 

    "agent" , 

    # Мы можем повторно использовать ту же функцию маршрутизатора

     router_function, 

    # Теперь карта определяет логику маршрутизации

     { "call_tool" : "tools" , "__end__" : "__end__" } 

 

# Это ключевое отличие: ребро идет от узла инструмента ОБРАТНО к агенту

 react_graph_builder.add_edge( "tools" , "agent" ) 

 

react_agent_app = react_graph_builder. compile ()

 

 

 

 

Архитектура ReAct (создано компанией...)

Фарид Хан

)

Вот и всё. Единственное реальное изменение — это react_graph_builder.add_edge("tools", "agent"). Эта единственная строка создаёт цикл и превращает нашего простого пользователя инструмента в динамического агента ReAct.

 

Чтобы понять, почему этот цикл так эффективен, давайте дадим ему задачу, которую невозможно решить за один раз — классический многошаговый вопрос. Простой агент, использующий инструмент, не справится с этим, потому что он не может связать шаги воедино.

 

multi_step_query = "Кто является нынешним генеральным директором компании, создавшей научно-фантастический фильм «Дюна», и каков был бюджет последнего фильма этой компании?" console.print

 

 ( f "[жирный желтый]Тестирование агента ReAct на многошаговом запросе:[/жирный желтый] ' {multi_step_query} '\n" ) final_react_output = None # Потоковая передача вывода для просмотра пошагового обоснования для chunk в react_agent_app.stream({ "messages" : [( "user" , multi_step_query)]}, stream_mode= "values" ): final_react_output = chunk console.print ( f"--- [жирный фиолетовый] Обновление текущего состояния[/жирный фиолетовый] ---" ) chunk[ 'messages' ][- 1 ].pretty_print() console.print ( " \n" ) console. print ( "\n--- [жирный зеленый]Итоговый вывод от агента ReAct[/жирный зеленый] ---" ) console.print ( Markdown (final_react_output[ 'messages' ][- 1 ].content))

 

 

--- Сообщение от человека ---   

Кто является генеральным директором компании, снявшей «Дюну»...   

 

--- Агент REACT: Размышляет... ---   

--- Маршрутизатор: Вызов инструмента...   

 

--- Сообщение от ИИ ---   

Вызовы инструмента: web_search...   

Аргументы: query: текущий генеральный директор компании, снявшей «Дюну»...   

 

--- Сообщение от инструмента ---   

[{"title": "Дюна: Часть третья - Википедия", "content": "Легендарный генеральный директор Джошуа Гроде..."}]   

 

--- Агент REACT: Размышляет... ---   

--- Маршрутизатор: Завершение...   

 

--- Итоговый результат ---   

Генеральный директор компании, снявшей «Дюну», - Джошуа Гроде...   

Бюджет последнего фильма не найден...

Когда мы запускаем это, трассировка выполнения агента показывает гораздо более интеллектуальный процесс. Он не просто выполняет один поиск. Вместо этого он логически просчитывает решение проблемы:

 

Мысль 1: «Сначала мне нужно выяснить, какая компания сняла фильм «Дюна»».

Действие 1: Оно вызывает web_search('production company for Dune movie').

Наблюдение 1: Оно возвращает "Легендарное развлечение".

Мысль 2: «Ладно, теперь мне нужен генеральный директор Legendary Entertainment».

Действие 2: Оно звонит web_search('CEO of Legendary Entertainment')и так далее, пока не соберет все части.

Для формализации улучшений мы можем снова использовать нашего магистра права в качестве судьи, на этот раз сосредоточившись на выполнении задач.

 

class TaskEvaluation ( BaseModel ): 

    """Схема для оценки способности агента выполнить задачу."""

     task_completion_score: int = Field(description= "Оценка от 1 до 10, показывает, успешно ли агент выполнил все части запроса пользователя." ) 

    reasoning_quality_score: int = Field(description= "Оценка от 1 до 10, показывает логический поток и процесс рассуждения, продемонстрированные агентом." ) 

    justification: str = Field(description= "Краткое обоснование оценок." ) 

 

def evaluate_agent_output ( query: str , agent_output: dict ): 

    """Запускает LLM-as-a-Judge для оценки итоговой производительности агента."""

     trace = "\n" .join([ f" {m. type } : {m.content} " for m in agent_output[ 'messages' ]]) 

    

    prompt = f"""Вы эксперт" Оцените работу агентов ИИ. Оцените производительность следующего агента в заданной задаче по шкале от 1 до 10. Оценка 10 означает, что задача выполнена идеально. Оценка 1 означает полный провал. 

    

    **Задача пользователя:** 

    {query}

    

     **Полная трассировка разговора агента:** 

    ``` 

    {trace}

     ``` {data-source-line= "108" }

     """

    

     judge_llm = llm.with_structured_output(TaskEvaluation) 

    return judge_llm.invoke(prompt)

Давайте посмотрим на результаты. Обычный агент, пытающийся выполнить это задание, получил бы очень низкий балл, поскольку ему не удалось бы собрать всю необходимую информацию. Однако наш агент ReAct показывает гораздо лучшие результаты.

 

--- Оценка результатов работы агента ReAct --- 

    'task_completion_score': 8, 

    'reasoning_quality_score': 9, 

    'justification': "Агент правильно разбил проблему на несколько этапов... Он успешно определил компанию, а затем и генерального директора. Хотя ему было сложно найти бюджет для последнего фильма, его логическое мышление было обоснованным, и он выполнил большую часть задачи." 

}

Мы видим, что это reasoning_quality_scoreподтверждает логичность и обоснованность пошагового процесса, подтвержденную нашим судьей (магистром права).

 

Паттерн ReAct позволяет агенту решать подобные сложные, многоэтапные задачи, требующие динамического мышления.

 

Использование инструмента

Разработанный нами шаблон рефлексии отлично подходит для оттачивания внутреннего логического мышления агента.

Но что происходит, когда агенту нужна информация, которой он еще не располагает?

Без доступа к внешним инструментам LLM ограничена предварительно обученными параметрами, она может генерировать данные и рассуждать, но не может запрашивать новые данные или взаимодействовать с внешним миром. Вот тут-то и вступает в действие наша вторая архитектура, Tool Use.

В любой крупномасштабной системе искусственного интеллекта использование инструментов не является необязательным, а важным и обязательным компонентом. Они выступают в качестве моста между рассуждениями агента и данными из реального мира. Будь то бот службы поддержки, проверяющий статус заказа, или финансовый агент, получающий котировки акций в режиме реального времени.

Давайте разберемся, как протекает этот процесс.

-

 Инструмент, использующий рабочий процесс (создан пользователем)

Фарид Хан

)

Получение запроса: Агент получает запрос от пользователя.

Решение: Агент анализирует запрос и рассматривает доступные инструменты. Затем он решает, необходим ли тот или иной инструмент для точного ответа на вопрос.

Действие: Если требуется инструмент, агент формирует вызов этого инструмента, например, конкретной функции с правильными аргументами.

Наблюдение: Система выполняет вызов инструмента, и результат («наблюдение») отправляется обратно агенту.

Синтез: Агент берет выходные данные инструмента, объединяет их со своими собственными рассуждениями и генерирует окончательный, обоснованный ответ для пользователя.

Для этого нам нужно предоставить нашему агенту инструмент. Для этого мы будем использовать TavilySearchResultsинструмент, который дает нашему агенту доступ к поиску в интернете. Самая важная часть здесь — описание . LLM читает это описание на естественном языке, чтобы понять, что делает инструмент и когда его следует использовать, поэтому сделать его ясным и точным — ключ к успеху.

 

# Инициализируем инструмент. Мы можем установить максимальное количество результатов, чтобы контекст был кратким.

 search_tool = TavilySearchResults(max_results= 2 ) 

 

# Крайне важно дать инструменту четкое имя и описание для агента.

 search_tool.name = "web_search"

 search_tool.description = "Инструмент, который можно использовать для поиска в интернете актуальной информации по любой теме, включая новости, события и текущие события."

 

 tools = [search_tool]

Теперь, когда у нас есть функциональный инструмент, мы можем создать агента, который научится им пользоваться. Состояние агента, использующего инструмент, довольно простое: это всего лишь список сообщений, отслеживающий всю историю разговора.

 

class AgentState ( TypedDict ): 

    messages: Annotated[ list [AnyMessage], add_messages]

Далее нам необходимо сообщить LLM о предоставленных нам инструментах. Это критически важный шаг. Мы используем .bind_tools()метод, который, по сути, внедряет название и описание инструмента в системную подсказку LLM, позволяя ему самому решать, когда вызывать этот инструмент.

 

llm = ChatNebius(model= "meta-llama/Meta-Llama-3.1-8B-Instruct" , temperature= 0 ) 

 

# Привязываем инструменты к LLM, делая его совместимым с инструментами

 llm_with_tools = llm.bind_tools(tools)

Теперь мы можем определить рабочий процесс нашего агента с помощью LangGraph. Нам нужны два основных узла: agent_node«мозг», который вызывает LLM для принятия решения о дальнейших действиях, и tool_node«руки», которые фактически выполняют инструмент.

 

def agent_node ( state: AgentState ): 

    """Первичный узел, который вызывает LLM для принятия решения о следующем действии."""

     console. print ( "--- АГЕНТ: Размышляет... ---" ) 

    response = llm_with_tools.invoke(state[ "messages" ]) 

    return { "messages" : [response]} 

 

# ToolNode — это предварительно созданный узел из LangGraph, который выполняет инструменты

 tool_node = ToolNode(tools)

После agent_nodeвыполнения заданий нам нужен маршрутизатор, чтобы определить, куда двигаться дальше. Если последнее сообщение агента содержит tool_callsатрибут, это означает, что он хочет использовать инструмент, поэтому мы направляем его к этому инструменту tool_node. В противном случае это означает, что у агента есть окончательный ответ, и мы можем завершить рабочий процесс.

 

def router_function ( state: AgentState ) -> str : 

    """Проверяет последнее сообщение агента, чтобы решить, какой будет следующий шаг."""

     last_message = state[ "messages" ][- 1 ] 

    if last_message.tool_calls: 

        # Агент запросил вызов инструмента

         console. print ( "--- МАРШРУТИЗАТОР: Решение — вызвать инструмент. ---" ) 

        return "call_tool" 

    else : 

        # Агент предоставил окончательный ответ

         console. print ( "--- МАРШРУТИЗАТОР: Решение — завершить. ---" ) 

        return "__end__"

Хорошо, у нас есть все необходимые элементы. Давайте соединим их в граф. Ключевым моментом здесь является условное ребро, которое использует наш механизм router_functionдля создания основного цикла рассуждений агента: agent-> router-> tool-> agent.

 

graph_builder = StateGraph(AgentState) 

 

# Добавить узлы

 graph_builder.add_node( "agent" , agent_node) 

graph_builder.add_node( "call_tool" , tool_node) 

 

# Установить точку входа

 graph_builder.set_entry_point( "agent" ) 

 

# Добавить условный маршрутизатор

 graph_builder.add_conditional_edges( 

    "agent" , 

    router_function, 

 

# Добавить ребро от узла инструмента обратно к агенту, чтобы завершить цикл

 graph_builder.add_edge( "call_tool" , "agent" ) 

 

# Скомпилировать граф

 tool_agent_app = graph_builder. compile ()

 

 

 

 

 

 

 

Архитектура вызовов инструментов (создана)

 

Теперь давайте протестируем это. Мы зададим ему вопрос, на который он никак не сможет ответить, используя свои обучающие данные, заставив его использовать инструмент веб-поиска для поиска ответа в реальном времени.

 

user_query = "Какие основные анонсы были сделаны на последнем мероприятии Apple WWDC?"

 initial_input = { "messages" : [( "user" , user_query)]} 

 

console. print ( f"[bold cyan]🚀 Запуск рабочего процесса использования инструментов для запроса:[/bold cyan] ' {user_query} '\n" ) 

 

for chunk in tool_agent_app.stream(initial_input, stream_mode= "values" ): 

    chunk[ "messages" ][- 1 ].pretty_print() 

    console. print ( "\n---\n" ) 

 

console. print ( "\n[bold green]✅ Рабочий процесс использования инструментов завершен![/bold green]" )

Давайте посмотрим на результат, чтобы увидеть ход мыслей агента.

 

================================== Сообщение от человека ================================== 

Какие основные анонсы были сделаны на последнем мероприятии Apple WWDC? 

--- 

--- АГЕНТ: Размышляю... --- 

--- МАРШРУТИЗАТОР: Решение — вызвать инструмент. --- 

=================================== Сообщение ИИ =================================== 

Вызовы инструмента: 

  web_search (call_abc123) 

  Аргументы: 

    query: Последние анонсы Apple на WWDC 

--- 

================================== Сообщение инструмента ================================== 

Имя: web_search 

[{"title": "WWDC 2025: Всё, что мы знаем...", "url": "...", "content": "Мероприятие Apple длилось час... мы подвели итоги всех анонсов... iOS 26, iPadOS 26, macOS Tahoe..."}] 

--- 

--- АГЕНТ: Размышляет... --- 

--- МАРШРУТИЗАТОР: Решение — завершить. --- 

================================== Сообщение ИИ =================================== 

Основные анонсы с последнего мероприятия Apple WWDC включают новый дизайн, который определит разработку iOS, iPadOS и macOS в следующем десятилетии, новые функции для iPhone... и обновления для всех платформ, включая iOS 26, iPadOS 26, CarPlay, macOS Tahoe...

Траектория движения наглядно демонстрирует логику агента:

 

Сначала система agent_nodeобдумывает ситуацию и решает, что ей необходимо выполнить поиск в интернете, после чего отправляет tool_callsзапрос.

Далее, программа tool_nodeвыполняет поиск и возвращает результат ToolMessageв исходном виде.

Наконец, алгоритм agent_nodeзапускается снова, на этот раз используя результаты поиска в качестве контекста для формирования окончательного, полезного ответа для пользователя.

Для формализации этого вопроса мы снова привлечем нашего магистра права в качестве судьи, но с критериями, специально разработанными для оценки использования инструментов.

 

class ToolUseEvaluation ( BaseModel ): 

    """Схема для оценки использования инструментов агентом и его окончательного ответа."""

     tool_selection_score: int = Field(description= "Оценка от 1 до 5, насколько агент выбрал правильный инструмент для задачи." ) 

    tool_input_score: int = Field(description= "Оценка от 1 до 5, насколько хорошо были сформированы и релевантны входные данные для инструмента." ) 

    synthesis_quality_score: int = Field(description= "Оценка от 1 до 5, насколько хорошо агент интегрировал выходные данные инструмента в свой окончательный ответ." ) 

    justification: str = Field(description= "Краткое обоснование оценок." )

Когда мы запускаем проверку на основе полной трассировки разговора, мы получаем структурированную оценку.

 

--- Оценка эффективности использования инструмента ---

 { 

    'tool_selection_score' : 5 , 

    'tool_input_score' : 5 , 

    'synthesis_quality_score' : 4 , 

    'justification' : "Агент ИИ правильно использовал инструмент веб-поиска для поиска релевантной информации... Вывод инструмента был корректным и релевантным... агент ИИ мог бы лучше обработать информацию..."

 }

Высокие баллы свидетельствуют о том, что наш агент не просто вызывает инструмент, а действительно эффективно его использует.

 

Она правильно определяла, когда следует искать, что искать и как использовать результаты. Эта архитектура является фундаментальным строительным блоком практически для любого практического ИИ-помощника.

 

 

 

 

Создание 17 шаблонов агентного ИИ и их роль в крупномасштабных системах искусственного интеллекта.

Ансамблирование, мета-управление, ToT, рефлексивность, PEV и многое другое.

При создании крупномасштабной системы искусственного интеллекта вы, по сути, объединяете различные шаблоны проектирования агентов . Каждый из них имеет свой этап, метод построения, результат и оценку. Если мы рассмотрим эти шаблоны в целом, их можно разбить на 17 высокоуровневых архитектур, которые отражают основные формы, которые могут принимать агентные системы…
 

Вот некоторые из этих узоров:

  • Многоагентная система , в которой несколько инструментов и агентов работают вместе для решения проблемы.
  • Ансамблевая система принятия решений , в которой несколько агентов предлагают по одному ответу, а затем голосуют за лучший из них.
  • Древо мыслей , в котором агент исследует множество различных путей рассуждений, прежде чем выбрать наиболее перспективное направление.
  • Рефлексивный подход , при котором субъект способен распознать и признать то, чего он не знает.
  • Цикл ReAct , в котором агент попеременно думает, предпринимает действия, а затем снова думает, чтобы усовершенствовать свой процесс.

И их гораздо больше…

В этом блоге мы подробно рассмотрим различные типы агентных архитектур и покажем, какую уникальную роль каждая из них играет в полноценной системе искусственного интеллекта.

Мы визуально оценим важность каждой архитектуры, закодируем соответствующий рабочий процесс и проведем оценку, чтобы определить, действительно ли он улучшает производительность по сравнению с базовым вариантом.

Весь код доступен в моём репозитории на GitHub:

GitHub - FareedKhan-dev/all-agentic-architectures: Реализация более 17 агентных архитектур…

Реализация более 17 агентных архитектур, разработанных для практического применения на различных этапах развития систем искусственного интеллекта…

github.com

 

Кодовая база организована следующим образом:

all-agentic-architectures/ 
├── 01_reflection.ipynb
├── 02_tool_use.ipynb
├── 03_ReAct.ipynb
...
├── 06_PEV.ipynb
├── 07_blackboard.ipynb
├── 08_episodic_with_semantic.ipynb
├── 09_tree_of_thoughts.ipynb
...
├── 14_dry_run.ipynb
├── 15_RLHF.ipynb
├── 16_cellular_automata.ipynb
└── 17_reflexive_metacognitive.ipynb

Оглавление

  1. Настройка среды
  2. Отражение
  3. Использование инструмента
  4. Реагировать (Разум + Действие)
  5. Планирование
  6. PEV (Планировщик-Исполнитель-Верификатор)
  7. Древо мыслей (ToT)
  8. Многоагентные системы
  9. Мета-контроллер
  10. Доска
  11. Принятие решений коллективом
  12. Эпизодическая + Семантическая память
  13. Память графа (мировой модели)
  14. Цикл самосовершенствования (аналогия RLHF)
  15. Тренировочная страховочная система
  16. Симулятор (ментальная модель в контуре управления)
  17. Рефлексивная метакогнитивная
  18. Клеточные автоматы
  19. Объединение архитектурных решений
 

Настройка среды

Прежде чем приступить к построению каждой архитектуры, нам необходимо определить основные параметры и четко понимать, что мы используем, почему нужны те или иные модули, модели и как все это взаимодействует друг с другом.

Нам известно, что LangChain , LangGraph и LangSmith — это практически стандартные модули для создания любой серьезной системы RAG или агентной системы. Они предоставляют все необходимое для построения, оркестрации и, что наиболее важно, для понимания того, что происходит внутри наших агентов, когда ситуация усложняется, поэтому мы и остановились на этих трех.

Первым делом нужно импортировать основные библиотеки. Таким образом, мы избежим повторения этих действий в дальнейшем и сохраним систему в чистоте и порядке. Давайте это сделаем.

import os 
from typing import List , Dict , Any , Optional , Annotated, TypedDict
from dotenv import load_dotenv # Загрузка переменных окружения из файла .env

# Pydantic для моделирования/проверки данных
from pydantic import BaseModel, Field

# Компоненты LangChain и LangGraph
from langchain_nebius import ChatNebius # Оболочка Nebius LLM
from langchain_tavily import TavilySearch # Интеграция инструмента поиска Tavily
from langchain_core.prompts import ChatPromptTemplate # Для структурирования подсказок
from langgraph.graph import StateGraph, END # Создание графа конечного автомата
from langgraph.prebuilt import ToolNode, tools_condition # Предварительно созданные узлы и условия

# Для красивого вывода в консоль
from rich.console import Console # Стилизация консоли
from rich.markdown import Markdown # Отображение Markdown в терминале

Итак, давайте быстро разберемся, почему мы используем именно эти три варианта.

  • LangChain — это наш набор инструментов, предоставляющий нам основные строительные блоки, такие как подсказки, определения инструментов и оболочки LLM.
  • LangGraph — это наш механизм оркестровки, объединяющий все элементы в сложные рабочие процессы с помощью циклов и ветвлений.
  • LangSmith — это наш отладчик, отображающий визуальную трассировку каждого шага, который предпринимает агент, что позволяет нам быстро выявлять и исправлять проблемы.

Мы будем работать с модулями LLM с открытым исходным кодом от таких поставщиков, как Nebius AI или Together AI . Преимущество в том, что они работают точно так же, как стандартный модуль OpenAI, поэтому нам не нужно многое менять, чтобы начать работу. А если мы захотим запускать всё локально, мы просто заменим его чем-нибудь вроде Ollama .

Чтобы наши агенты не были привязаны к статическим данным, мы предоставляем им доступ к API Tavily для поиска в реальном времени (1000 кредитов в месяц, думаю, достаточно для тестирования). Таким образом, они смогут…

Они активно ищут информацию самостоятельно, что позволяет сосредоточиться на реальном логическом мышлении и использовании инструментов.

Далее нам нужно настроить переменные окружения. Здесь мы будем хранить конфиденциальную информацию, например, ключи API. Для этого создайте файл с именем .envв той же директории и поместите в него ваши ключи, например:

# Ключ API для Nebius LLM (используется с ChatNebius)
NEBIUS_API_KEY= "ваш_ключ_API_nebius_здесь"

# Ключ API для LangSmith (платформа мониторинга/телеметрии LangChain)
LANGCHAIN_API_KEY= "ваш_ключ_API_langsmith_здесь"

# Ключ API для инструмента поиска Tavily (используется с интеграцией TavilySearch)
TAVILY_API_KEY= "ваш_ключ_API_tavily_здесь"

После того как .envфайл будет настроен, мы сможем легко интегрировать эти ключи в наш код, используя dotenvмодуль, который мы импортировали ранее.

load_dotenv()   # Загрузка переменных окружения из файла .env 

# Включение трассировки LangSmith для мониторинга/отладки
os.environ[ "LANGCHAIN_TRACING_V2" ] = "true"
os.environ[ "LANGCHAIN_PROJECT" ] = "Реализация 17 агентных архитектур" # Название проекта для группировки трассировок

# Проверка наличия всех необходимых ключей API
for key in [ "NEBIUS_API_KEY" , "LANGCHAIN_API_KEY" , "TAVILY_API_KEY" ]:
if not os.environ.get(key): # Если ключ не найден в переменных окружения
print ( f" {ключ} не найден. Пожалуйста, создайте файл .env и установите его." )

 

Теперь, когда наша базовая конфигурация готова, мы можем начать создавать каждую архитектуру по отдельности, чтобы посмотреть, как они себя показывают и где они наиболее целесообразны в крупномасштабной системе искусственного интеллекта.

Отражение

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

Речь идёт о том, чтобы дать агенту возможность отступить на шаг назад, оценить свою собственную работу и улучшить её.

В крупномасштабной системе искусственного интеллекта этот подход идеально подходит для любого этапа, где качество генерируемого результата имеет решающее значение. Подумайте о таких задачах, как генерация сложного кода, написание подробных технических отчетов — везде, где простой, черновой вариант ответа недостаточно хорош и может привести к реальным проблемам.

Давайте разберемся, как протекает этот процесс.

 

  1. Создание: Агент принимает запрос пользователя и создает первоначальный черновик или решение. Это его первая, неотфильтрованная попытка.
  2. Критика: Затем агент меняет роли и становится собственным критиком. Он анализирует черновик на предмет недостатков, задавая такие вопросы, как «Это правильно?», «Это эффективно?» или «Что я упускаю?».
  3. Уточнение: В заключение, проведя собственную критику, агент генерирует окончательную, улучшенную версию результата, напрямую устраняя обнаруженные недостатки.

Прежде чем создавать логику агента, необходимо определить структуры данных, с которыми он будет работать. Использование моделей Pydantic — отличный способ заставить LLM выдавать чистые, структурированные JSON-данные, что крайне важно для многоэтапного процесса, где выходные данные одного этапа становятся входными данными для следующего.

 
 
    prompt = f"""Вы — опытный программист на Python, которому поручено доработать фрагмент кода на основе критики. 

Ваша цель — переписать исходный код, внедрив все предложенные улучшения из критики.

**Исходный код:**
```python
{draft_code}
``` {data-source-line= "115" }

**Критика и предложения:**
{critique_suggestions}

Пожалуйста, предоставьте окончательный, доработанный код и краткое описание внесенных вами изменений.
"""


refined_code = refiner_llm.invoke(prompt)
return { "refined_code" : refined_code.model_dump()}

Итак, у нас есть три логических элемента. Теперь нам нужно объединить их в рабочий процесс. Вот тут-то и пригодится LangGraph. Мы определим параметры, stateкоторые будут передаваться между нашими узлами, а затем построим сам граф.

class  ReflectionState ( TypedDict ): 
"""Представляет состояние нашего графа отражений."""
user_request: str
draft: Optional [ dict ]
critique: Optional [ dict ]
refined_code: Optional [ dict ]

# Инициализация нового графа состояний
graph_builder = StateGraph(ReflectionState)

# Добавление узлов в граф
graph_builder.add_node( "generator" , generator_node)
graph_builder.add_node( "critic" , critic_node)
graph_builder.add_node( "refiner" , refiner_node)

# Определение ребер рабочего процесса как простой линейной последовательности
graph_builder.set_entry_point( "generator" )
graph_builder.add_edge( "generator" , "critic" )
graph_builder.add_edge( "critic" , "refiner" )
graph_builder.add_edge( "refiner" , END)

# Компиляция графа в исполняемое приложение
reflection_app = graph_builder.compile ( )

Поток представляет собой простую прямую линию: generator-> critic-> refiner. Это классический шаблон отражения, и теперь мы готовы его протестировать.

 

 

Агент отражения (создан) )

Для проверки этого алгоритма мы рассмотрим классическую задачу программирования, где наивная первая попытка часто оказывается неэффективной: нахождение n-го числа Фибоначчи . Это идеальный тестовый пример, поскольку простое рекурсивное решение легко написать, но оно вычислительно затратно, что оставляет много возможностей для улучшения.

user_request = "Напишите функцию на Python для нахождения n-го числа Фибоначчи."
initial_input = { "user_request" : user_request}

console. print ( f"[bold cyan]🚀 Запуск рабочего процесса рефлексии для запроса:[/bold cyan] ' {user_request} '\n" )

# Передача результатов и сохранение конечного состояния
final_state = None
for state_update in reflection_app.stream(initial_input, stream_mode= "values" ):
final_state = state_update

console. print ( "\n[bold green]✅ Рабочий процесс рефлексии завершен![/bold green]" )

Давайте посмотрим на фотографии «до» и «после» , чтобы увидеть, что сделал наш агент.

--- ### Первоначальный вариант ---
Пояснение: Эта функция использует рекурсивный подход для вычисления n-го числа Фибоначчи... Этот подход неэффективен для больших значений n из-за повторяющихся вычислений...

1 def fibonacci(n):
2 if n <= 0:
3 return 0
4 elif n == 1:
5 return 1
6 else :
7 return fibonacci(n-1) + fibonacci(n-2)

--- ### Критика ---
Резюме: Функция содержит потенциальные ошибки и неэффективность. Ее следует переработать для обработки отрицательных входных данных и улучшения временной сложности.
Предлагаемые улучшения:
- Функция некорректно обрабатывает отрицательные числа.
- Функция имеет высокую временную сложность из-за повторяющихся вычислений. Рассмотрите возможность использования динамического программирования или мемоизации.
- Функция не соответствует соглашениям PEP 8...

--- ### Окончательный улучшенный код ---
Краткое описание улучшений: Исходный код был переработан для обработки отрицательных входных данных, улучшения временной сложности и соответствия соглашениям PEP 8.

1 def fibonacci(n):
2 "" " Вычисляет n-е число Фибоначчи." ""
3 if n < 0:
4 raise ValueError( "n должно быть неотрицательным целым числом" )
5 elif n == 0:
6 return 0
7 elif n == 1:
8 return 1
9 else :
10 fib = [0, 1]
11 for i in range(2, n + 1):
12 fib.append(fib[i-1] + fib[i-2])
13 return fib[n]

Первоначальный вариант представляет собой простую рекурсивную функцию, правильную, но ужасно неэффективную. Критик указывает на экспоненциальную сложность и другие проблемы. А доработанный код — это гораздо более умное и надежное итеративное решение. Это прекрасный пример того, как шаблон работает так, как задумано.

Чтобы сделать это более наглядным, давайте привлечем еще одного магистра права (LLM), который выступит в роли беспристрастного «судьи» и оценит как первоначальный вариант, так и окончательный код. Это позволит нам получить количественную оценку улучшения.

class  CodeEvaluation ( BaseModel ): 
"""Схема для оценки фрагмента кода."""
correctness_score: int = Field(description= "Оценка от 1 до 10, насколько код логически корректен." )
efficiency_score: int = Field(description= "Оценка от 1 до 10, насколько алгоритмически эффективен код." )
style_score: int = Field(description= "Оценка от 1 до 10, насколько стиль и читаемость кода соответствуют PEP 8." )
justification: str = Field(description= "Краткое обоснование оценок." )

def evaluate_code ( code_to_evaluate: str ):
prompt = f"""Вы — эксперт по оценке кода на Python. Оцените следующую функцию по шкале от 1 до 10 на корректность, эффективность и стиль. Предоставьте краткое обоснование.

Код:
```python
{code_to_evaluate}
```
"""

return judge_llm.invoke(prompt)

if final_state and 'draft' in final_state and 'refined_code' in final_state:
console. print ( "--- Оценка первоначального черновика ---" )
initial_draft_evaluation = evaluate_code(final_state[ 'draft' ][ 'code' ])
console. print (initial_draft_evaluation.model_dump()) # Исправлено: используйте .model_dump()

console. print ( "\n--- Оценка уточненного кода ---" )
refined_code_evaluation = evaluate_code(final_state[ 'refined_code' ][ 'refined_code' ])
console. print (refined_code_evaluation.model_dump()) # Исправлено: используйте .model_dump()
else :
console. print ( "[жирный красный]Ошибка: Невозможно выполнить оценку, поскольку `final_state` неполный.[/жирный красный]" )

Когда мы запускаем проверку на обеих версиях, результаты говорят сами за себя.

--- Оценка первоначального варианта ---
{
'correctness_score' : 2 ,
'efficiency_score' : 4 ,
'style_score' : 2 ,
'justification' : 'Функция имеет временную сложность O(2^n)...'
}

--- Оценка уточненного кода ---
{
'correctness_score' : 8 ,
'efficiency_score' : 6 ,
'style_score' : 9 ,
'justification' : 'Код корректен... он имеет временную сложность O(n)...'
}

Первоначальный вариант получил ужасные оценки, особенно по эффективности. Доработанный код демонстрирует колоссальный прогресс по всем параметрам.

Это дает нам неопровержимое доказательство того, что процесс рефлексии не просто изменил код, а сделал его несколько лучше.