Transcription
Добро пожаловать в руководство AI Engineers по LangChain. Это четырёхчасовой курс, который поможет вам пройти путь от полного незнания LangChain до уверенного использования фреймворка, будь то внутри LangChain, LangGraph или где-либо ещё. Основы, которые вы изучите в этом курсе, помогут вам в этом.
Этот курс будет разделён на несколько глав. Мы начнём с обсуждения того, что такое LangChain, когда его следует использовать, а когда нет. Мы поговорим о плюсах и минусах, а также об экосистеме LangChain, а не только о самом фреймворке LangChain. Оттуда мы перейдём к знакомству с LangChain, рассмотрим несколько примеров, прежде чем погрузиться в основы фреймворка.
Хочу заметить, что всё это относится к LangChain 0.3, последней актуальной версии. Тем не менее, мы немного затронем историю LangChain, рассмотрим методы работы с версиями до 0.3, чтобы понять, как делались вещи раньше, и как мы делаем это сейчас, в версии 0.3. А также как глубже погрузиться в эти методы и настроить их.
Далее мы погрузимся в то, что я считаю будущим ИИ — это агенты. Мы уделим много времени агентам. Начнём с простого введения в агенты: как создать простой агент, каковы основные компоненты агентов, как они выглядят. Затем мы углубимся в них и создадим собственный Agent X Computer, который представляет собой своего рода фреймворк вокруг компонентов ИИ агента, который мы строим.
После глубокого погружения в агенты, мы перейдём к LangChain Expression Language (LChain EL), который мы будем использовать на протяжении всего курса. LangChain Expression Language — рекомендуемый способ использования LangChain, а язык выражений EL немного отличается от стандартного синтаксиса Python. Да, мы будем использовать его на протяжении всего курса, но главу по EL мы оставим на потом, потому что к тому моменту мы действительно хотим глубоко погрузиться в основы EL. Идея в том, что к этому моменту у вас уже будет хорошее представление о том, как использовать основы EL, прежде чем мы действительно углубимся.
Затем мы углубимся в потоковую передачу (стриминг), которая является важной функцией UX для приложений ИИ в целом. Потоковая передача может значительно улучшить пользовательский опыт, и это не только о потоковой передаче токенов, то есть о том интерфейсе, где вы видите, как ИИ генерирует текст по словам на экране. Потоковая передача — это больше, чем просто это. Это также возможность (если вы видели интерфейс Perplexity), получать обновления о том, что думает агент, какие инструменты он использует и как он их использует. Это ещё одна важная функция, которую необходимо хорошо понимать для создания. Мы также рассмотрим всё это.
Наконец, мы завершим курс проектным заданием, где мы создадим собственное приложение ИИ-агента, которое будет включать в себя все эти функции. У нас будет агент, который может использовать инструменты, веб-поиск, мы будем использовать потоковую передачу, и мы увидим всё это в удобном интерфейсе, с которым мы можем работать.
Это обзор курса. Конечно, это очень высокоуровневый обзор, я только что прошёл по множеству вещей. И действительно, этот курс может помочь вам, независимо от вашего текущего уровня владения LangChain: новичок вы, немного использовали его или даже имеете средний уровень — вы, вероятно, узнаете много нового.
Без лишних слов, давайте перейдём к первой главе.
Итак, в первой главе курса мы сосредоточимся на том, когда следует использовать LangChain, а когда что-то другое. В этой главе мы не будем слишком сильно фокусироваться на коде. Каждая другая глава будет очень ориентирована на код, но эта глава немного более теоретическая: что такое LangChain, где его место, когда я должен его использовать, когда нет.
Я хочу начать с того, что LangChain — это один из самых популярных, если не самый популярный, фреймворков с открытым исходным кодом в экосистеме Python, по крайней мере, для ИИ. Он довольно хорошо работает для многих вещей, а также ужасно работает для многих других вещей, если быть совсем честным. Есть огромные плюсы и огромные минусы использования LangChain. Здесь мы просто обсудим некоторые из них и посмотрим, как LangChain сравнивается с другими фреймворками.
Самый первый вопрос, который мы должны задать себе: нужен ли нам вообще фреймворк? Действительно ли нужен фреймворк, когда мы можем просто обратиться к API? У вас есть API OpenAI, другие API, и т.д. Мы можем получить ответ от большой языковой модели (LLM) в среднем за пять строк кода. Это невероятно просто. Однако это может быстро измениться, когда мы начнём говорить об агентах, генерации с использованием извлечения (RAG), системах помощи в исследованиях и обо всём подобном. Эти варианты использования, эти методы могут внезапно стать довольно сложными вне фреймворков. И это не обязательно плохо, верно? Это может быть невероятно полезно — просто понимать всё, что происходит, и строить это самим. Но проблема в том, что для этого нужно время. Вам нужно изучить все тонкости построения этих вещей, тонкости этих методов самих по себе. Как они вообще работают? И это идёт вразрез с тем, что мы видим с ИИ в настоящее время: ИИ внедряется в мир невероятно быстро. Из-за этого большинство инженеров, приходящих в эту область, не имеют опыта работы в области машинного обучения или ИИ. У большинства людей нет опыта работы с этими системами. Многие инженеры, которые приходят в эту область, могут быть инженерами DevOps, обычными бэкенд-инженерами Python, даже фронтенд-инженеры, создающие все эти вещи, что замечательно, но у них не обязательно есть опыт. Это может быть и вы, и это не плохо, потому что идея в том, что вы, очевидно, будете учиться и осваивать многие из этих вещей. В этом сценарии есть довольно веский аргумент в пользу использования фреймворка, потому что фреймворк означает, что вы можете начать быстрее. А фреймворк, такой как LangChain, абстрагирует много вещей. Это большая претензия, которую многие люди предъявляют к LangChain, но эта абстракция многих вещей также сделала LangChain популярным, потому что это означает, что вы можете прийти, не зная, что такое RAG, например, и вы можете реализовать конвейер RAG, получить от него преимущества, не нужно действительно понимать его. Да, есть и противоположенный аргумент — реализация чего-либо без реального понимания. Но, как мы увидим на протяжении всего курса, можно работать с LangChain так, как мы будем делать в этом курсе, где вы реализуете эти вещи абстрактным способом, а затем разбираете их на части и начинаете понимать тонкости, хотя бы немного. Это может быть довольно хорошо.
Однако, возвращаясь к тому, что мы сказали в начале, если ваша идея или ваше приложение — это просто что-то очень простое, вам нужно сгенерировать какой-то текст на основе простого ввода, возможно, вам следует просто использовать API. Это тоже совершенно оправдано.
Мы только что сказали, что многие люди, приходящие в LangChain, могут не иметь опыта работы с ИИ. Поэтому ещё один вопрос для многих из этих инженеров может быть таким: если я хочу узнать о RAG, агентах и обо всём таком, должен ли я пропустить LangChain и попытаться построить всё сам с нуля? LangChain может сильно помочь в этом процессе обучения. Вы можете начать с очень абстрактного уровня, и по мере того, как вы постепенно начинаете лучше понимать фреймворк, вы можете снимать всё больше и больше этих абстракций и углубляться в детали. На мой взгляд, этот постепенный переход к более явному коду с меньшим количеством абстракций — это действительно хорошая особенность. И это то, на чём мы фокусируемся на протяжении всего курса. Это то, что мы будем делать: начнём абстрактно, снимая абстракции и становясь более явными в том, что мы строим.
Например, при создании агента в LangChain есть очень простой и невероятно абстрактный метод `create_tools_agent`, который мы можем использовать. Он создаёт для вас агента с инструментами. Он ничего вам не объясняет. Вы можете использовать это, и мы будем использовать это в начале курса, но затем вы можете перейти от этого к определению полной логики выполнения агента, которая в основном представляет собой вызов инструмента OpenAI. Вы будете получать эту информацию об инструменте обратно, но затем вам нужно выяснить, как я собираюсь это выполнить, как я собираюсь решить эту информацию, и как я собираюсь это проитерировать. Мы увидим это снятие абстракций, когда мы будем работать, когда мы будем создавать агентов, как мы это делаем, как мы строим наш вариант использования потоковой передачи среди многих других вещей, даже память чата, мы увидим её там. LangChain может служить трамплином для вашего опыта обучения ИИ.
Затем вы можете обнаружить, и я действительно думаю, что это довольно верно для большинства людей, что если вы серьёзно относитесь к разработке ИИ и это то, чем вы хотите заниматься, то LangChain может быть трамплином, вашей начальной кривой обучения. Но после того, как вы стали компетентны в LangChain, вы можете обнаружить, что хотите перейти к другим фреймворкам. Это не обязательно означает, что вы потратили время на LangChain впустую, потому что, во-первых, LangChain помогает вам учиться, и, во-вторых, один из основных фреймворков, на которые я рекомендую многим людям перейти, — это на самом деле LangGraph, который всё ещё находится в экосистеме LangChain и всё ещё использует множество объектов и методов LangChain, а также концепций. Поэтому, даже если вы перейдёте с LangChain, вы можете перейти на что-то вроде LangGraph, для которого вы всё равно можете использовать LangChain. И давайте скажем, вы перешли на другой фреймворк. В этом случае концепции, которые вы узнали из LangChain, всё ещё довольно важны.
Чтобы завершить эту главу, я просто хочу подвести итог по вопросу о том, следует ли вам использовать LangChain. Важно помнить, что LangChain действительно много абстрагирует. Эта абстракция LangChain является одновременно сильной и слабой стороной. С опытом эти абстракции могут показаться ограничением. Вот почему мы придерживаемся идеи, что LangChain действительно хорош для начала, но по мере роста сложности проекта или по мере накопления опыта инженерами они могут перейти на что-то вроде LangGraph, которое в любом случае будет использовать LangChain в той или иной степени. В любом из этих сценариев LangChain будет основным инструментом в наборе инструментов инженера ИИ. На наш взгляд, его стоит изучить, но, конечно, у него есть свои недостатки, и об этом полезно помнить. Это не идеальный фреймворк, но в большинстве случаев вы многому научитесь и сможете многое создать с его помощью.
Со всем этим мы перейдём к нашей первой практической главе по LangChain, где мы просто познакомимся с LangChain, некоторыми основными концепциями. Я не буду слишком углубляться в синтаксис, но мы просто немного поймём, что мы можем с ним сделать.
Итак, переходим к нашей следующей главе: «Начало работы с LangChain». В этой главе мы познакомимся с LangChain, создав простого помощника на основе большой языковой модели, который будет делать для нас разные вещи. Он будет генерировать текст, изображения, структурированный вывод — он будет делать несколько вещей.
Для начала мы перейдём к репозиторию курса. Весь код, все главы находятся здесь. Есть два способа запуска: локально или в Google Colab. Мы рекомендуем запускать в Google Colab, потому что это намного проще с точки зрения сред, но вы также можете запускать его локально. А для заключительного проекта мы будем запускать его локально, в Colab это сделать невозможно. Если вы хотите запустить всё локально, я быстро покажу вам, как это сделать. Если вы хотите запустить в Colab, что я бы рекомендовал, по крайней мере, для первых глав ноутбуков, просто переходите вперёд. В временной шкале видео будут указаны моменты глав.
Для запуска локально мы просто переходим сюда. Это на самом деле говорит вам всё, что вам нужно. Вам нужно будет установить `uvue`. Это менеджер пакетов, который мы рекомендуем, библиотека управления пакетами Python. Вам не обязательно использовать `uvue`, это на ваше усмотрение. `uvue` очень прост, он отлично работает, поэтому я бы рекомендовал его. Вы установите его с помощью этой команды. Это на Mac, поэтому в других случаях это будет по-другому, если вы используете Windows или что-то другое, вы можете посмотреть руководство по установке, и оно скажет вам, что делать. Поэтому, прежде чем мы это сделаем, я сделаю следующее: я просто клонирую этот репозиторий. Мы перейдём сюда, я создам для себя временную директорию, потому что у меня уже есть курс LangChain, и я просто клонирую курс LangChain. Вам также нужно будет установить Git, если у вас его нет.
Итак, у нас есть это, затем мы сделаем следующее: скопируем это. Это установит для нас Python 3.2.7 с помощью этой команды. Затем это создаст новую виртуальную машину в рамках этой или с использованием Python 3.2.7, который мы установили, и `uvue` фактически будет рассматривать файл `pyproject.toml`, который является установкой пакета для репозитория, и использовать его для установки всего, что нам нужно. Теперь нам следует убедиться, что мы находимся в директории курса LangChain, и да, мы можем запустить эти три команды, и вот так. Всё должно установиться с помощью этого. Теперь, если вы находитесь в VS Code, вы можете просто сделать VS Code или мы можем запустить `code .` Я просто запущу это, и я открыл курс. Внутри этого курса у вас есть ваши ноутбуки, и вы просто запускаете их, убедившись, что вы выбрали ядро pth-среды и что вы используете правильную виртуальную машину отсюда. Так что это должно появиться уже как эта виртуальная машина bin Python, и вы нажмёте это, и затем вы можете запустить. Когда вы запускаете локально, не запускайте эти команды, они вам не нужны, вы уже всё установили, вам это не нужно. Это специально для Colab, это запуск локально. Теперь давайте посмотрим, как запускать всё в Colab.
Для запуска всего в Colab у нас есть наши ноутбуки здесь. Мы нажимаем, и затем у нас есть каждая из глав здесь. Начиная с первой главы, введение, где мы сейчас находимся. Что вы можете сделать, чтобы открыть это в Colab, это либо просто нажать эту кнопку Colab здесь, либо, если вы действительно хотите, например, может быть, это не загружается для вас, вы можете скопировать URL вверху здесь, вы можете перейти в Colab, вы можете перейти к открытию GitHub, а затем просто вставить это сюда и нажать Enter, и вот так. У нас есть наш ноутбук.
Итак, мы сейчас здесь. Что мы сделаем сначала, так это установим предварительные требования. У нас есть LangChain, куча пакетов LangChain здесь: LangChain Core, LangChain OpenAI, потому что мы используем OpenAI, и LangChain Community, который необходим для запуска того, что мы запускаем. Итак, это установило для нас всё, поэтому мы можем перейти к нашему первому шагу, который заключается в инициализации нашей большой языковой модели. Мы будем использовать GPT-4-mini, которая является довольно маленькой, но быстрой и также более дешёвой моделью, которая также очень хороша для OpenAI. Итак, что нам нужно сделать здесь, так это получить API-ключ. Для получения этого API-ключа мы перейдём на сайт OpenAI, и вы можете видеть здесь, что мы открываем platform.openai.com, а затем мы перейдём в настройки, организацию, API-ключи. Вы можете скопировать это, я просто нажму на него отсюда. Итак, я собираюсь создать новый секретный ключ, на всякий случай, если вы ищете, где это находится, это Настройки, Организация, API-ключи снова. Создать новый API-ключ. Я назову его LangChain Course, я просто помещу его на маршрутизатор семантики, это просто моя организация, вы поместите его туда, куда хотите, и затем вы скопируете свой API-ключ. Вы можете видеть мой здесь. Я, очевидно, удалю его, прежде чем вы увидите это, но вы можете попробовать использовать его, если вам действительно нравится. Итак, я скопирую это и помещу это в этот маленький ящик здесь. Вы также можете просто поместить свой полный API-ключ сюда, это на ваше усмотрение, но этот маленький ящик просто упрощает вещи. Теперь то, что мы в основном сделали, это просто передали наш API-ключ, мы устанавливаем нашу модель OpenAI GPT-4-mini, и то, что мы собираемся делать сейчас, это, по сути, просто подключаться и настраивать наши параметры LLM с LangChain. Мы запускаем это, мы говорим: «Хорошо, мы используем GPT-4-mini», и мы также настраиваем себя на использование двух разных LLM здесь или двух одинаковых LLM с немного разными настройками. Первый из них — это LLM с параметром температуры, равным нулю. Параметр температуры в основном контролирует случайность вывода вашей LLM. И способ его работы заключается в том, что когда LLM предсказывает следующий токен или следующее слово в последовательности, он предоставляет вероятность фактически для всех токенов в базе знаний LLM или того, на чём была обучена LLM. Итак, что мы делаем, когда устанавливаем температуру равной нулю, мы говорим: «Вы будете давать нам токен с наивысшей вероятностью, согласно вам». В то время как, когда мы устанавливаем температуру равной 0,9, мы говорим: «На самом деле есть повышенная вероятность того, что вы дадите нам токен, который, согласно вашему сгенерированному выводу, не является токеном с наивысшей вероятностью, согласно LLM, но это, как правило, даёт нам более творческие результаты». Вот что делает температура. Итак, мы создаём обычную LLM, а затем более креативную LLM с помощью этого.
Что мы будем строить? Мы возьмём черновик статьи со страницы обучения Aelio и будем использовать LangChain для генерации различных вещей, которые могут нам помочь, когда у нас есть этот черновик статьи, и мы редактируем его и как бы завершаем. Что это будет? Вы можете видеть их здесь. У нас есть заголовок для статьи, описание и SEO-оптимизированное описание. В третьем случае мы заставим LLM предоставить советы по существующему абзацу и, по сути, написать для нас новый абзац из существующего абзаца. И то, что он будет делать, это часть структурированного вывода, он напишет для нас новую версию этого абзаца, и он даст нам советы о том, где мы можем улучшить своё письмо. Затем мы сгенерируем миниатюрное изображение-заголовок для нашей статьи — красивое изображение, которое вы поместите вверху. Здесь мы просто введём нашу статью. Вы можете ввести что-то другое, если хотите. По сути, это просто большая статья, написанная некоторое время назад об агентах, и теперь мы можем приступить к подготовке наших подсказок, которые по сути являются инструкциями для нашей LLM. LangChain поставляется с множеством различных утилит для подсказок, и мы более подробно рассмотрим их, но я хочу дать вам основы сейчас, чтобы вы хотя бы концептуально понимали, что мы рассматриваем.
Подсказки для агентов чата как минимум разделены на три компонента: это системная подсказка. Это даёт инструкции нашей LLM о том, как она должна вести себя, какова её цель и как она должна добиваться этой цели. Как правило, системные подсказки будут немного длиннее, чем то, что у нас есть здесь, в зависимости от варианта использования. Затем у нас есть подсказки пользователя. Это сообщения, написанные пользователем. Иногда мы можем захотеть предварительно заполнить их, если мы хотим стимулировать определённые модели разговора от нашего агента, но в большинстве случаев да, они будут сгенерированы. Затем у нас есть подсказки ИИ. Они, конечно же, генерируются ИИ. И снова в некоторых случаях мы можем захотеть сгенерировать их сами заранее или в ходе разговора, если у нас есть определённая причина для этого, но в большинстве случаев вы можете предположить, что они фактически генерируются пользователем и ИИ. LangChain предоставляет нам шаблоны для каждого из этих типов подсказок. Давайте посмотрим, как они выглядят в LangChain.
Для начала мы смотрим на это. У нас есть шаблон системного сообщения подсказки и человеческое сообщение, которое мы видели ранее. У нас есть эти два компонента. Системная подсказка довольно проста здесь: вы — система ИИ, которая помогает генерировать заголовки статей. Итак, первый компонент, который мы хотим сгенерировать, — это заголовок статьи. Мы говорим ИИ, что мы хотим, чтобы он это сделал. Затем здесь. Итак, здесь мы фактически предоставляем шаблон для ввода пользователя. Да, как я упоминал, ввод пользователя может быть полностью сгенерирован пользователем, он может быть не сгенерирован пользователем, он может настраивать разговор заранее, который пользователь будет использовать позже, или в этом случае мы фактически создаём шаблон, и то, что пользователь предоставит, фактически будет вставлено сюда, внутри статьи. И поэтому у нас есть эти переменные импорта. Что это будет делать? Хорошо, у нас есть все эти инструкции здесь, они все будут предоставлены OpenAI как если бы это был пользователь, говорящий это, но на самом деле это будет только это, что пользователь предоставит. Хорошо, и мы также можем отформатировать это немного лучше, это зависит от ситуации. Это будет работать так, как есть, но мы также можем добавить что-то вроде этого, чтобы сделать это немного понятнее для LLM. Хорошо, что это за подсказки? У нас есть это, и вы можете видеть, что в этом случае нет большой разницы между тем, что делает системная подсказка и подсказка пользователя. Это конкретный сценарий. Это варьируется, когда вы переходите к более разговорным вещам, как мы будем делать позже, вы увидите, что подсказка пользователя в основном генерируется пользователем или в основном генерируется пользователем, и большую часть этих типов инструкций мы можем фактически поместить в системную подсказку. Это меняется, и мы увидим на протяжении всего курса множество различных способов использования этих различных типов подсказок в разных местах. Затем вы увидите здесь. Я просто хочу показать вам, как это работает. Мы можем использовать этот метод format для нашей подсказки пользователя здесь, чтобы фактически вставить что-то...
Внутри статьи, э-э, входные данные здесь, так что мы собираемся использовать формат запроса, а затем передаем что-то для статьи, окей? И мы также можем, может быть, отформатировать это немного красивее, но я просто покажу вам это сейчас. Итак, у нас есть наше сообщение от человека, а затем внутри содержимого – это текст, который у нас был, верно? Вы можете видеть, что у нас есть все это, верно? И это то, что мы написали раньше. Мы написали все это, за исключением этой части. Мы не писали это. Вместо этого у нас была статья, верно? Так что давайте отформатируем это немного красивее, чтобы мы могли видеть. Окей, так это точно то, что мы написали здесь, вверху, точно то же самое, за исключением того, что теперь у нас есть тестовая строка вместо статьи. Поэтому позже, когда мы вставим нашу статью, она будет помещена туда. Это как, это F-строка в Python, окей? И это опять же, это одна из тех вещей, на которые люди могут жаловаться по поводу LangChain. Вы знаете, этот тип вещи может быть, это кажется избыточным, потому что вы могли бы просто сделать это с помощью nring, но есть, как мы увидим позже, особенно когда вы транслируете, действительно полезные функции, которые появляются при использовании встроенных шаблонов запросов LangChain или, по крайней мере, объектов сообщений, которые мы увидим. Так что нам нужно помнить об этом. Как только вы получите более сложные вещи, LangChain может быть немного полезнее.
Шаблон чат-запроса – это, по сути, просто то, что у нас есть здесь: наш системный запрос, запросы пользователя. Вы также можете включить туда некоторые запросы ИИ, и что он будет делать, так это объединять и то, и другое, а когда мы выполним форматирование, он объединит их в историю чата, окей? Так что давайте посмотрим, как это выглядит сначала, э-э, более беспорядочным способом, окей? Так что вы можете видеть, у нас есть только содержимое, верно? Так что оно не включает все, знаете ли, раньше у нас было сообщение от человека, мы не включаем, мы не видим ничего подобного здесь, вместо этого мы видим только строку. Теперь давайте переключимся обратно на печать, и мы можем видеть, что у нас есть наше системное сообщение здесь, оно просто помечено префиксом "система", а затем у нас есть "человек", и оно помечено префиксом "человек", а затем оно продолжается, верно? Так что это все, что он делает, он просто объединяет их в некий чат-лог. Мы также могли бы включить сообщения ИИ, и они также появятся там, окей? Так что теперь у нас есть это. Это наш шаблон запроса. Давайте объединим его с LLМ, чтобы создать то, что в прошлом в LangChain называлось цепочкой LLМ. Теперь мы не обязательно будем называть это цепочкой LLМ, потому что мы не используем абстракцию цепочки LLМ. Это не очень важно, если это не имеет смысла. Мы подробнее рассмотрим это позже, особенно в главе об LLM. Так что эта цепочка, вы думаете, LangChain – это просто цепочки, где мы связываем вместе эти несколько компонентов, она будет выполнять форматирование запроса STS. Это то, что я только что показал вам. Генерация LLМ, отправка нашего запроса в OpenAI, получение ответа и получение этого вывода. Вы также можете добавить здесь еще один набор, если хотите отформатировать его определенным образом. Мы будем выводить это в определенном формате, чтобы мы могли легче передать его в следующий набор. Но есть также вещи, называемые выходными проходами, которые передают ваш вывод более динамичным или сложным способом в зависимости от того, что вы делаете.
Это наш первый взгляд на LLM. Я не хочу, чтобы мы слишком сильно концентрировались на синтаксисе здесь, потому что мы будем делать это позже, но я хочу, чтобы вы просто поняли, что на самом деле происходит здесь и логически, что мы пишем. Так что все, что нам действительно нужно знать сейчас, это то, что мы определяем наши входные данные с помощью первого сегмента словаря здесь, верно? Это, знаете ли, наши входные данные, которые мы уже определили, окей? Так что если мы вернемся к нашему запросу пользователя здесь, мы сказали, что входная переменная – это наша статья, верно? И мы могли бы также добавить входные переменные к системному запросу здесь, в этом случае, знаете ли, допустим, у нас есть ваш помощник ИИ под названием "имя", который помогает генерировать заголовки статей. В этом сценарии у нас могли бы быть входные переменные "имя" здесь, верно? И тогда нам пришлось бы сделать это здесь, верно? Так что также у нас была бы статья, но у нас также было бы имя. В основном, нам просто нужно убедиться, что здесь мы включаем переменные, которые мы определили как входные переменные для наших первых запросов, окей? Так что мы можем просто пойти вперед и давайте добавим это, э-э, чтобы мы могли видеть, что оно работает, так что мы снова запустим это и просто включим это или переинициализируем как наш первый запрос. Мы видим это, и если мы просто посмотрим, что это значит для этой функции форматирования здесь, это значит, что нам также нужно передать имя, окей? И назвать его Джо, окей? Джо, ИИ, верно? Так что вы – система ИИ по имени Джо, теперь, окей? Так что у нас есть Джо, наш ИИ, который будет подаваться через эти входные переменные, затем у нас есть оператор канала. Оператор канала, по сути, говорит: все, что находится слева от оператора канала, что в данном случае будет это, будет передано во все, что находится справа от оператора канала. Это просто. Опять же, мы будем углубляться в это и разбирать его в главе об LLM, но пока этого достаточно, чтобы знать. Это будет передано в наш первый запрос, который будет форматировать все, он добавит имя и статью, которые мы предоставили, в наш первый запрос, а затем он выведет это, верно? Выведет это. У нас есть наш оператор P здесь, так что вывод этого будет передан на вход нашего следующего шага. Это наш креативный LLМ, затем он будет генерировать некоторые токены, он будет генерировать наш вывод. Этот вывод будет сообщением ИИ, и, как вы видели раньше, если я уберу этот кусочек, внутри этих объектов сообщений у нас есть это поле контента, окей? Так что мы фактически собираемся извлечь поле контента из нашего сообщения ИИ, чтобы получить только контент, и это то, что мы делаем здесь. Мы получаем сообщение ИИ из LLМ, а затем извлекаем контент из этого объекта сообщения ИИ, и мы передаем его в словарь, который просто содержит заголовок статьи, вот так. Нам не нужно делать это, мы можем просто получить сообщение ИИ напрямую. Я просто хочу показать вам, как мы используем этот тип цепочки в LLM. Как только мы настроили нашу цепочку, мы вызываем ее или выполняем ее с помощью метода invoke. В него нам нужно будет передать эти переменные. У нас уже есть наша статья, но мы также дали нашему ИИ имя, теперь давайте добавим это, и мы запустим это. Окей, так Джо сгенерировал для нас заголовок статьи: "Разблокировка будущего: Восхождение нейросимволических агентов ИИ". Круто, гораздо лучшее имя, чем то, что я дал статье, которое было "Агенты ИИ – это нейросимволические системы". Нет, я не думаю, что я сделал слишком плохо, окей? Так что у нас есть это сейчас. Давайте продолжим, и мы будем создавать больше таких цепочек LLМ, где мы подаем некоторые запросы, генерируем что-то, получаем что-то и делаем с этим что-то.
Как упоминалось, у нас есть заголовок, мы теперь переходим к описанию. Чтобы сгенерировать описание, у нас есть шаблон запроса сообщения от человека. Это фактически будет иметь тот же формат, что и раньше. Мы также хотим переопределить это, потому что я думаю, что использую там одно и то же системное сообщение. Так что давайте давайте посмотрим и изменим это, или что мы также могли бы сделать, давайте просто удалим имя сейчас, потому что я показал это. Так что мы могли бы сделать это: вы – система ИИ, которая помогает создавать хорошие статьи, верно? Создавать хорошие статьи, и мы могли бы просто использовать это как наш, знаете ли, общий системный запрос сейчас. Так что давайте скажем, что это наш новый системный запрос. Теперь у нас есть наш запрос пользователя: ваша задача – создать описание для статьи. Статья находится здесь. Внимательно изучите статью. Вот заголовок статьи. Итак, нам теперь также нужен заголовок статьи в наших входных переменных, а затем мы выведем дружественное для ИИ описание статьи, и мы просто говорим "только для уверенности здесь: не выводите ничего, кроме описания". Вы знаете, иногда LLМ может сказать: "Эй, посмотрите, вот что я сгенерировал для вас. Причина, по которой я думаю, что это хорошо, потому что и так далее, и так далее, верно?" Если вы программным способом получаете какой-либо вывод от LLМ, вы не хотите всего этого пуха вокруг того, что сгенерировал LLМ, вы хотите только то, о чем вы его просили, окей? Потому что иначе вам нужно будет разбираться с кодом, и это может стать беспорядочным, а также гораздо менее надежным. Так что мы просто говорим: не выводите ничего другого, чем… Мы объединяем все это: системный запрос и второй запрос пользователя, этот вот, объединяем их в новый шаблон чат-запроса, а затем мы будем подавать все это в другую цепочку LLМ, как у нас здесь, чтобы, ну, чтобы сгенерировать наше описание. Давайте посмотрим, мы вызываем это как раньше, мы просто убедимся, что добавим заголовок статьи, который мы получили раньше, и посмотрим, что мы получим. Окей, так у нас есть это: "Изучите трансформационный потенциал нейросимволических агентов ИИ". Немного длинновато, если честно, но да, вы можете видеть, что он делает здесь, верно? И, конечно же, мы могли бы потом войти… Мы видим, что это слишком длинно, верно? А, дружественное для SEO описание, не совсем. Так что мы можем изменить это. Вывод дружественного для SEO описания… Убедитесь, что мы не превышаем… Давайте напишем это на новой строке. Убедитесь, что мы не превышаем, скажем, 200 символов, или, может быть, даже меньше. Я не, я не знаю, я просто скажу 120 символов. Я не вывожу ничего, кроме описания, верно? Так что мы могли бы просто, знаете ли, вернуться, изменить наш запрос, посмотреть, что он сгенерирует снова. Окей, намного короче, вероятно, слишком короткое сейчас, но это нормально. Круто, так что у нас есть это. У нас есть процесс суммирования, и это теперь, знаете ли, в этом формате словаря, который у нас есть здесь. Круто. Теперь на третьем шаге мы хотим использовать эту первую переменную статьи с нашей полной статьей, и мы собираемся сгенерировать несколько разных выходных полей. Для этого мы будем использовать функцию структурированного вывода. Давайте прокрутим вниз, мы увидим, что это такое, как это выглядит. Структурированный вывод – это по существу, мы заставляем LLМ, как будто он должен выводить словарь с этими, знаете ли, конкретными полями, окей? И мы можем изменить это довольно сильно, но в этом сценарии я хочу, чтобы был исходный абзац, верно? Я просто хочу, чтобы он перегенерировал исходный абзац, потому что я ленивый и не хочу извлекать его, затем я хочу получить новый отредактированный абзац. Это сгенерированный LLМ улучшенный абзац, а затем мы хотим получить некоторую обратную связь, потому что мы не хотим просто автоматизировать себя, мы хотим дополнять себя и улучшаться с помощью ИИ, а не просто быть такими: я, ты, ты делаешь это. Так что это то, что мы делаем здесь, и вы можете видеть, что здесь мы используем этот объект pydantic, и что pydantic позволяет нам делать, это определять эти конкретные поля, и это также позволяет нам назначать эти описания полю, и LangChain фактически собирается пойти вперед и прочитать все это, верно? Даже читает. Например, мы могли бы поместить здесь целое число, и мы могли бы фактически получить числовой балл для нашего абзаца, верно? Мы можем попробовать это, верно? Так что давайте, давайте, давайте просто быстро попробуем это, я покажу вам. Числовой, числовой балл, на самом деле, давайте даже просто проигнорируем… Давайте не будем ничего помещать сюда. Так что я собираюсь поместить конструктивную обратную связь по исходному абзацу, но я просто помещу это сюда. Давайте посмотрим, что произойдет. Окей, у нас есть это, и что я собираюсь сделать, я собираюсь получить наш креативный LLМ, я собираюсь использовать его с методом структурированного вывода, и это фактически изменит этот класс LLМ, создаст новый класс LLМ, который заставляет этот LLМ использовать эту структуру для вывода, верно? Передавая абзац сюда, используя это, мы создаем этот новый структурированный LLМ. Давайте запустим это и посмотрим, что произойдет. Окей, мы собираемся изменить нашу цепочку соответствующим образом. Может быть, что я могу сделать… Давайте также просто удалим этот кусочек сейчас, чтобы мы могли просто увидеть, что выводит структурированный LLМ напрямую, и давайте посмотрим. Окей, теперь вы можете видеть, что у нас фактически есть этот объект абзаца, верно? Тот, который мы определили здесь, что довольно круто, а затем внутри него у нас есть исходный абзац, верно? Так что это отсюда, я определенно помню, что написал что-то очень похожее на это, так что я думаю, что это правильно. У нас есть отредактированный пар… Так что это, окей, что он считает лучше, и затем интересно, что обратная связь – это три, что странно, верно? Потому что здесь мы сказали конструктивная обратная связь по исходному абзацу, но что мы делаем, когда используем это со структурированным выводом, но что делает LangChain, это по существу выполняет вызов инструмента в OpenAI, и что может делать вызов инструмента, это заставлять использовать определенную структуру в выводе LLМ. Когда мы говорим, что обратная связь должна быть целым числом, независимо от того, что мы поместим сюда, она даст нам целое число, потому что как вы можете дать конструктивную обратную связь с целым числом? Это не имеет особого смысла, но поскольку мы установили это ограничение, это ограничение здесь, это то, что он делает, он просто дает нам, э-э, числовое значение. Я собираюсь сменить это на строку, а затем давайте перезапустим это, посмотрим, что мы получим. Окей, теперь мы должны увидеть, что мы действительно получаем конструктивную обратную связь, все правильно. Так что да, вы можете видеть, что это довольно, довольно длинно. Исходный абзац эффективно передает ограничения нейро-ИИ-систем в выполнении определенных тестов, однако он мог бы выиграть от немного улучшенной ясности и краткости. Например, фраза "становилось ясно" может быть сделана более прямой, изменив ее на "стало очевидно". Да, правда, спасибо большое. Так что да, теперь мы действительно получаем эту обратную связь, что довольно приятно. Теперь давайте добавим этот заключительный шаг в нашу цепочку, окей? И он просто извлечет наш объект абзаца здесь и извлечет его в словарь. Нам не обязательно делать это, честно говоря, я на самом деле предпочитаю это в этом объекте абзаца, но просто чтобы мы могли видеть, как мы бы передавали вещи на другую сторону цепочки, окей? Так что теперь мы видим, что мы извлекли это. Круто, так что у нас есть вся эта интересная обратная связь, опять же, но давайте оставим это здесь для текстовой части этого. Теперь давайте посмотрим на мультимодальные функции, с которыми мы можем работать. Это, знаете ли, может быть, одна из тех вещей, которая кажется немного более абстрактной, немного сложной, где ее, возможно, можно улучшить, но, знаете ли, мы не будем слишком сильно концентрироваться на мультимодальных вещах, мы будем концентрироваться на языке, но я хотел просто очень быстро показать вам. Мы хотим, чтобы эта статья выглядела лучше, окей? Мы хотим сгенерировать запрос на основе самой статьи, которую мы затем можем передать в DALL-E, модель генерации изображений от OpenAI, которая затем сгенерирует изображение, как, например, миниатюрное изображение для нас, окей? Первый шаг этого – мы фактически собираемся заставить LLМ сгенерировать это, верно? У нас есть запрос, который мы будем использовать для этого. Я скажу: сгенерируйте запрос менее чем из 500 символов для генерации изображения на основе следующей статьи, окей? Так что это наш запрос, да, супер простой. Используя общий шаблон запроса здесь, вы можете использовать его, вы можете использовать шаблон запроса пользователя, это зависит от вас. Это просто как общий шаблон запроса, а затем мы будем делать это на основе того, что он выведет, мы затем передадим это в эту функцию generate and display image через параметр image prompt, который будет использовать обертку DALL-E API от LangChain, он запустит этот image prompt, и мы получим URL-адрес из этого, по существу, а затем мы прочитаем это с помощью SKimage здесь, верно? Мы просто прочитаем этот URL-адрес изображения, получим данные изображения, а затем просто отобразим его, окей? Довольно просто. Теперь опять же, это вещь LLМ здесь, которую мы делаем. У нас есть эта вещь runable Lambda. Когда мы запускаем функции внутри LLМ, нам нужно обернуть их в эту runable Lambda. Я, знаете ли, я не хочу слишком подробно останавливаться на том, что это делает здесь, потому что мы действительно рассматриваем это в главе об LLM, но это просто, знаете ли, все, что вам действительно нужно знать, это то, что у нас есть пользовательская функция, обернутая в runable Lambda, а затем то, что мы получаем от этого, мы можем использовать здесь, верно? Синтаксис LLM. Что мы делаем здесь? Давайте разберемся. Мы берем наш оригинальный… Этот image prompt, который мы определили прямо здесь, верно? Входная переменная для этого – это статья, окей? У нас есть наша статья d, которая подается сюда, подается в наш запрос, оттуда мы получаем наше сообщение, которое мы затем передаем в наш LLМ. Из LLМ он сгенерирует для нас, как, image prompt, как запрос для генерации нашего изображения для этой статьи. Мы даже можем… Давайте выведем это, чтобы мы могли видеть, что он генерирует, потому что я тоже немного любопытен, окей? Так что мы просто запустим это, а затем посмотрим… Он передаст это содержимое в наш runable, что в основном является этой функцией здесь, и мы увидим, что он сгенерирует. Не ожидайте ничего потрясающего от DALL-E, он не, он не лучший, если честно, но мы, по крайней мере, видим, как его использовать. Окей, так что мы можем видеть запрос, который использовался здесь: "Создайте изображение, которое визуально представляет концепцию нейросимволических агентов. Изобразите футуристический интерфейс, где большой D взаимодействует с традиционным кодом, символизирующим интеграцию… О боже мой… чего-то вычислительного. Включите элементы, такие как мозг, для представления нейронных сетей, шестерни или схемы или символической логики и сеть связей, иллюстрирующих обширные варианты использования агентов ИИ". О боже мой, посмотрите на весь этот большой запрос, а затем мы получаем это. Вы знаете, DALL-E интересный, я бы сказал… Мы даже могли бы взять это… Давайте просто посмотрим, что это придумает в чем-то вроде Midjourney. Вы можете видеть эти намного более крутые изображения, которые мы получаем просто из другой модели генерации изображений, намного лучше, но довольно круто, честно говоря. Таким образом, с точки зрения генерации изображения, формулировка, сам запрос на самом деле довольно хороший, изображение, знаете ли, может быть лучше, но это все, верно? Со всем этим мы увидели небольшое введение в то, что мы могли бы построить с LangChain. Это все для нашей вводной главы. Как я уже упоминал, мы не хотим слишком углубляться в то, что делает каждая из этих вещей, мы действительно хотим сосредоточиться на том, окей, так это как мы строим что-то с LangChain, это общий поток, э-э, но мы действительно не хотим слишком сильно концентрироваться на том, окей, что именно делает LLM или что именно, знаете ли, это запрос, который мы настраиваем. Мы будем гораздо больше концентрироваться на всех этих вещах и гораздо больше в предстоящих главах. Пока что мы просто увидели немного того, что мы можем построить, прежде чем углубляться в детали, окей? Итак, теперь мы посмотрим на наблюдаемость ИИ с помощью LangSmith. Теперь LangSmith – это еще один компонент более широкой экосистемы LangChain. Его цель – позволить нам видеть, что на самом деле делают наши LLМ, агенты и т. д., и это то, что мы определенно рекомендуем использовать, если вы собираетесь использовать LangChain. LangChain graph. Теперь давайте посмотрим, как мы настроим LangSmith, что невероятно просто. Я собираюсь открыть это в Colab, и я просто собираюсь установить предварительные требования здесь. Вы увидите, что все они такие же, как и раньше, но у нас теперь также есть библиотека LangSmith. Теперь мы будем использовать LangSmith на протяжении всего курса, поэтому во всех следующих главах мы будем импортировать LangSmith, и он будет отслеживать все, что мы делаем, но вам не нужен LangSmith, чтобы пройти курс. Это необязательная зависимость, но, как уже упоминалось, я бы рекомендовал его. Мы перейдем сюда, и первое, что нам понадобится, это API-ключ LangChain. Нам нужен API-ключ, но он поставляется с разумным бесплатным уровнем, поэтому мы можем видеть здесь, что у них есть каждый из планов, и это тот, на котором мы по умолчанию находимся, поэтому он бесплатный для одного пользователя до 5000 трассировок в месяц. Если вы создаете приложение, я думаю, довольно легко выйти за эти рамки, но это действительно зависит от того, что вы создаете, поэтому это хорошее место для начала, а затем, конечно, вы можете обновлять по мере необходимости. Итак, мы перейдем на smith.langchain.com, и вы можете видеть здесь, что это автоматически войдет меня. У меня есть все эти проекты трассировки, все они от меня, запускающие различные главы курса. Ваши, если вы будете использовать LangSmith на протяжении всего курса, ваша панель LangSmith в конечном итоге будет выглядеть примерно так. Теперь нам нужен API-ключ, поэтому мы переходим в настройки, у нас есть API-ключи, и мы просто собираемся создать API-ключ, потому что мы сейчас просто проходим некоторое личное обучение, сейчас я бы выбрал персональный токен доступа, мы можем дать имя или описание, если хотите, окей? И мы просто скопируем это, а затем перейдем к нашему блокноту и введем наш API-ключ там, и это все, что нам действительно нужно сделать. Это абсолютно все. Единственное, о чем следует помнить, это то, что вы должны установить свой проект LangChain на тот проект, над которым вы работаете, поэтому, конечно, в рамках курса у нас есть индивидуальные имена проектов для каждой главы, но для ваших собственных проектов, конечно, вы должны убедиться, что это то, что вы узнаете и что будет полезно для вас. LangSmith фактически делает многое без необходимости что-либо делать, поэтому мы можем фактически пройти… Давайте просто инициализируем наш LLМ и начнем вызывать его и видеть, что LangSmith возвращает нам. Нам понадобится наш ключ OpenAI API, введите его сюда, а затем давайте просто вызовем hello. Окей, ничего не изменилось с этой стороны, верно? Мы запускаем код, здесь ничего не отличается, однако теперь, если мы перейдем к LangSmith, я вернусь на свою панель инструментов, окей? И вы можете видеть, что порядок этих проектов немного изменился, и это потому, что последний используемый проект, этот вот, вверху, LangChain Course LangSmith OpenAI, это текущая глава, в которой мы находимся, которая только что была запущена. Я могу войти сюда, и я могу увидеть, о, посмотрите на это, так что у нас фактически есть что-то в интерфейсе LangSmith, и мы не… Все, что мы сделали, это ввели наш LangChain API-ключ. Это все, что мы сделали, и мы установили некоторые переменные среды, и это все. Так что мы можем фактически щелкнуть по этому, и это даст нам больше информации. Вы можете видеть, что было на входе, что было на выходе и некоторые другие метаданные здесь. Вы видите, знаете ли, здесь не так много, однако, когда мы сделаем то же самое для агентов, мы получим гораздо больше информации. Я даже могу показать вам быстрый пример из будущих глав. Если мы перейдем к агентам вводного курса, например, и мы просто посмотрим на один из них, окей? Итак, у нас есть этот вход и выход, но затем слева здесь мы получаем всю эту информацию, и причина, по которой мы получаем всю эту информацию, заключается в том, что агенты, они выполняют несколько вызовов LLМ и т. д. и т. д., поэтому происходит гораздо больше всего. Так что мы можем видеть, окей, что был первый вызов LLМ, а затем мы получаем эти трассировки использования инструментов, мы получаем еще один LLМ, еще один вызов LLМ, еще одно использование инструмента и еще один вызов LLМ. Так что вы можете видеть всю эту информацию, которая невероятно полезна и невероятно проста в использовании, потому что все, что я сделал при настройке этого в той главе о агентах, это просто установил API-ключ и переменные среды, как мы только что сделали, так что вы получаете много от очень небольших усилий с LangSmith, что здорово. Давайте вернемся к нашему проекту LangSmith здесь и вызовем еще несколько. Я уже показал вам…
Знаем, что будем видеть много вещей просто по умолчанию, но мы также можем добавлять другие вещи, которые Lang Smith обычно не отслеживает. Поэтому для этого мы просто импортируем декоратор `traceable` из Lang Smith, и затем сделаем эти случайные функции отслеживаемыми внутри LangSmith. Окей, поэтому мы запустим их, у нас их три, поэтому мы сгенерируем случайное число, изменим время выполнения функции, а также сгенерируем случайное число. И затем в этой функции мы либо вернём `no error`, либо вызовем ошибку. Таким образом, мы увидим, как LangSmith обрабатывает эти разные сценарии. Давайте просто переберём и запустим их несколько раз. Мы просто запустим каждый из них 10 раз. Окей, давайте посмотрим, что произойдёт. Они работают, давайте перейдём к нашему интерфейсу LangSmith и посмотрим, что происходит здесь. Таким образом, мы видим, что всё обновляется, мы добавляем эту информацию, и мы можем видеть, если мы перейдём в пару из них, мы можем увидеть немного больше информации. Вот входные и выходные данные, заняло три секунды, видим случайную ошибку здесь в этом сценарии, случайная ошибка прошла без проблем. Позвольте мне просто быстро обновить страницу. Окей, теперь у нас есть остальная информация, и мы видим, что иногда, если есть ошибка из нашей функции `random error`, она обозначается этим, и мы также видим трассировку, которая была возвращена там, что полезно. Окей, поэтому мы видим, если ошибка была вызвана, мы должны увидеть, что это за ошибка, мы можем видеть различные задержки этих функций, поэтому вы можете видеть, что они меняются здесь. Мы видим все входные данные для каждой из наших функций, а затем, конечно, выходные данные. Таким образом, мы можем увидеть много чего, что довольно хорошо.
Теперь ещё одна вещь, которую мы можем сделать, это можем фактически фильтровать. Если мы перейдём сюда, мы можем добавить фильтр. Давайте отфильтруем ошибки, это будет `ValueError`, и затем мы получим все случаи, когда одна из наших функций вернула или вызвала ошибку, или `ValueError` в частности. Окей, это полезно. И да, есть различные другие фильтры, которые мы можем добавить. Мы могли бы добавить имя, например, если мы хотим искать только функцию `generate_string_delay`, мы могли бы это сделать. Окей, и затем мы можем видеть изменяющиеся задержки этой функции. Отлично, теперь это есть. Одна последняя вещь, которую мы могли бы захотеть сделать, это, может быть, мы хотим сделать имена этих функций немного более описательными или удобными для поиска, например, и мы можем сделать это, указав имя декоратора `traceable` вот так. Давайте запустим это, запустим это несколько раз, а затем перейдём к LangSmith снова, перейдём к проекту LangSmith. Окей, и вы можете видеть, что они тоже появляются. Поэтому мы могли бы также искать их по этому новому имени, так что это было `chitchat_maker`, вот так, и затем мы можем видеть, как вся эта информация передаётся в LangSmith. Таким образом, это наше введение в LangSmith. Здесь действительно не так уж много, что нужно пройти, это очень легко настроить, и, как мы видели, это даёт нам много возможностей наблюдения за тем, что мы строим, и мы будем использовать это на протяжении всего курса. Мы не слишком на нём полагаемся, это полностью необязательная зависимость, поэтому если вы не хотите использовать LangSmith, вам не нужно, но он есть, и я бы порекомендовал сделать это.
Итак, это всё для этой главы, мы перейдём к следующей. Теперь мы перейдём к главе о подсказках в LangChain. Теперь подсказки, они кажутся простой концепцией, и они простая концепция, но на самом деле в них довольно много, когда вы начинаете погружаться в них, и они действительно были очень фундаментальной частью того, что продвинуло нас вперёд от до-LLM времён до нынешних LLM времён. Вы должны понимать, что до того, как LLMs стали широко распространены, способом тонкой настройки модели ИИ или ML модели тогда было получить множество данных для вашего конкретного случая использования, потратить много времени на обучение вашего конкретного трансформатора или части трансформатора, чтобы по существу адаптировать его для этой конкретной задачи. Это могло занять много времени в зависимости от задачи, это могло занять месяцы, а иногда, если это была более простая задача, это, вероятно, заняло бы дни, потенциально недели. Теперь интересная вещь с LLMs заключается в том, что вместо того, чтобы проходить через весь этот процесс тонкой настройки, чтобы модифицировать модель для одной задачи по сравнению с другой задачей, вместо этого мы просто по-разному даём подсказки. Мы буквально говорим модели: "Эй, я хочу, чтобы ты сделал это конкретным образом", и это, знаете ли, это изменение парадигмы в том, что вы делаете. Это настолько быстрее, это займёт у вас пару минут, а не дни, недели или месяцы. И LLMs невероятно мощны, когда дело доходит до обобщения на множество различных задач. Таким образом, подсказки, которые контролируют эти инструкции, являются фундаментальной частью этого. Теперь LangChain естественно имеет много функций вокруг подсказок, и мы можем создавать очень динамичные конвейеры подсказок, которые изменяют структуру и содержание того, что мы фактически передаём в наш LLM, в зависимости от различных переменных, различных входных данных, и мы увидим это в этой главе. Поэтому мы будем работать с подсказками в рамках примера RAG. Давайте начнём с того, что просто разберём различные части подсказки, которые мы могли бы ожидать увидеть для такого случая использования, как RAG.
Наша типичная подсказка для RAG или генерации с расширенным извлечением будет включать правила для LLMs, и это вы увидите в большинстве подсказок, если не во всех. Эта часть подсказки устанавливает поведение LLMs, то есть как он должен реагировать на запросы пользователей, какую личность он должен принимать, на чём он должен сосредотачиваться при ответе, любые конкретные правила или границы, которые мы хотим установить. И действительно, что мы пытаемся сделать здесь, это просто предоставить как можно больше информации LLM о том, что мы делаем. Мы просто хотим дать LLMs контекст относительно места, в котором он находится, потому что LLM понятия не имеет, где он находится, это просто, он принимает некоторую информацию и выдает информацию. Если единственная информация, которую он получает, исходит от пользователя, то есть запроса пользователя, он, знаете ли, не знает контекста, что это за приложение, в котором он находится, какова его цель, какова его задача, каковы границы. Всё это нам нужно просто предположить, что LLM абсолютно ничего не знает об этом, потому что он действительно ничего не знает. Поэтому как можно больше контекста, который мы можем предоставить, но важно, чтобы мы не переусердствовали. Это, мы видим это всё время, люди будут перегружать LLM подсказками. Вы хотите быть кратким, вам не нужен "пух", и в целом, каждая часть вашей подсказки, чем более лаконичной и менее "пушистой" вы можете её сделать, тем лучше. Теперь эти правила или инструкции обычно находятся в системной подсказке вашего LLMs.
Вторая - это контекст, который специфичен для RAG. Контекст относится к некоторой форме внешней информации, которую вы передаёте в ваш LLM. Мы могли получить эту информацию из веб-поиска, запроса к базе данных или довольно часто в этом случае RAG - это векторная база данных. Эта внешняя информация, которую мы предоставляем, по существу, является расширением извлечения RAG. Мы расширяем знания нашего LLMs, знания нашего LLMs содержатся в весах модели LLMs. Мы расширяем эти знания некоторыми внешними знаниями, вот что мы делаем здесь. Теперь для чат-LLMs этот контекст обычно размещается в контексте разговора в сообщениях пользователя или помощника, и с более новыми моделями его также можно размещать в сообщениях инструментов. Затем у нас есть вопрос, это довольно просто, это запрос от пользователя, это или это обычно сообщение пользователя. Конечно, может быть некоторое дополнительное форматирование вокруг этого, вы можете добавить немного дополнительного контекста, или вы можете добавить некоторые дополнительные инструкции. Если вы обнаружите, что ваш LLM иногда отклоняется от правил, которые вы установили в системной подсказке, вы можете, знаете ли, добавить что-то здесь, но по большей части это, вероятно, будет просто ввод пользователя. И наконец, все это входы для нашей подсказки, вот вывод, который мы получим. Ответ от помощника, опять же, это даже не специфично для RAG, это просто то, что вы ожидаете в чат-LLM или любом LLM, и, конечно, это было бы сообщение помощника.
Собирая всё это вместе в фактической подсказке, вы можете увидеть всё, что у нас есть здесь. У нас есть правила для нашей подсказки здесь, инструкции, мы просто говорим: "Окей, ответьте на вопрос на основе контекста ниже. Если вы не можете ответить на вопрос, используя предоставленную информацию, ответьте: "Я не знаю"". Затем у нас есть некоторый контекст здесь. Окей, в этом сценарии этот контекст, который мы передаём сюда, потому что это первое сообщение, мы можем поместить его в системную подсказку, но это также может быть изменено. Окей, если вы, например, имеете агента, вы можете иметь свой вопрос здесь, перед контекстом, и затем это будет поступать из сообщения пользователя, и затем этот контекст будет следовать за вопросом и распознаваться как сообщение инструмента. Он будет передаваться таким образом, это зависит от того, какую структуру вы выбираете, но вы можете сделать и то, и другое. Вы можете передать его в системное сообщение, если оно менее разговорное, тогда как, если оно более разговорное, вы можете передать его как сообщение инструмента. Окей, и затем у нас есть запрос пользователя, который находится здесь, и затем у нас будет ответ ИИ. Окей, и очевидно, что это будет сгенерировано здесь. Окей, давайте переключимся на код. Мы находимся в репозитории LangChain Course, записные книжки 03 подсказки, и я просто открою это в Colab. Окей, прокрутим вниз, и мы начнём с установки предварительных требований. Окей, у нас есть различные библиотеки, как я уже упоминал ранее, LangSmith необязателен, вам не нужно его устанавливать, но если вы хотите видеть ваши трассировщики и всё в LangSmith, тогда я бы порекомендовал сделать это. И если вы используете LangSmith, вам нужно будет ввести свой API ключ здесь. Опять же, если вы не используете LangSmith, вам не нужно ничего вводить, вы просто пропускаете эту ячейку. Окей, отлично, и давайте перейдём к базовым подсказкам.
Таким образом, мы начнём с этой подсказки: "Ответить на запрос пользователя на основе вопроса ниже". Мы просто структурируем то, что мы только что видели в коде, и мы будем использовать шаблон `ChatPromptTemplate`, потому что, вообще говоря, мы используем чат-LLMs в большинстве случаев в наши дни. Поэтому у нас есть наш `ChatPromptTemplate`, и он будет содержать список сообщений, системное сообщение для начала, которое будет просто содержать это, и мы передаём контекст в него, и у нас есть наш запрос пользователя здесь. Окей, мы запустим это, и если мы посмотрим здесь, мы не указали, какие у нас входные переменные, окей, но мы видим, что у нас есть `query` и `context` здесь, верно? Поэтому мы можем видеть, что окей, это входные переменные, мы просто не определили их явно здесь. Поэтому давайте просто подтвердим с помощью этого, что LangChain их воспринял, и мы видим, что он это сделал, у него есть `context` и `query` как наши входные переменные для шаблона подсказки, который мы только что определили. Окей, мы также можем видеть структуру наших шаблонов, давайте посмотрим. Окей, поэтому мы видим, что внутри сообщений здесь у нас есть шаблон системного сообщения подсказки. Способ, которым мы это определяем, вы можете видеть здесь, что у нас есть `from_messages`, и это будет потреблять различные структуры. Поэтому вы можете видеть здесь, что у него есть `from_messages`, это последовательность сообщений, подобных представлению. Поэтому мы могли бы передать объект шаблона системной подсказки, а затем объект шаблона подсказки пользователя, или мы могли бы просто использовать кортеж, как это, и это фактически определяет, окей, эта система, это пользователь, и вы могли бы также сделать сообщения помощника или инструмента и всё такое здесь, используя ту же структуру, и тогда мы можем посмотреть сюда, и, конечно, это переводится в шаблон системного сообщения подсказки и шаблон сообщения человека подсказки. У нас есть наши входные переменные там и там, и у нас есть шаблон тоже. Окей, теперь давайте продолжим. Мы увидим здесь то, что я только что сказал. Поэтому мы импортируем наш шаблон системного сообщения подсказки и шаблон сообщения человека подсказки, и вы можете видеть, что мы используем тот же метод `from_messages` здесь, верно? И вы можете видеть, что это последовательность сообщений, подобных представлению, это просто, знаете ли, что это фактически означает, может варьироваться, верно? Поэтому здесь у нас есть шаблон системного сообщения подсказки из шаблона, из шаблона `query`, знаете ли, есть различные способы, которыми вы можете захотеть сделать это, это просто зависит от того, насколько явным вы хотите быть. Вообще говоря, я думаю, для себя я предпочел бы, чтобы мы придерживались самих объектов и были явными, но это определённо немного сложнее передать, когда вы читаете это, поэтому я понимаю, почему вы также можете предпочесть это, это определённо чище, и это выглядит проще, поэтому это просто зависит, я полагаю, от предпочтений. Окей, поэтому мы снова видим, что это точно то же самое, окей, с `ChatPromptTemplate`, и он содержит это и это. Вы, вероятно, хотите увидеть точный вывод, поэтому это были сообщения, окей, точно так же, как я вывел раньше. Отлично, у нас есть всё это. Давайте посмотрим, как мы бы вызвали наш LLM с этим. Мы будем использовать `gpt-4-mini` снова, нам нужен наш API ключ OpenAI, поэтому введите его, и мы просто инициализируем наш LLM. Мы используем низкую температуру здесь, поэтому меньше случайности или меньше креативности, и во многих случаях это фактически то, что я бы делал. Причина в этом сценарии, что мы используем низкую температуру, заключается в том, что мы делаем RAG, и если вы помните раньше, если мы немного прокрутим вверх, наш шаблон говорит: "Ответьте на запрос пользователя на основе контекста ниже. Если вы не можете ответить на вопрос, используя предоставленную информацию, ответьте: "Я не знаю"", верно? Поэтому, просто прочитав это, мы знаем, что хотим, чтобы наш LLM был как можно более правдивым и точным. Поэтому более креативный LLM будет бороться с этим и с большей вероятностью будет галлюцинировать, тогда как LLM с низкой креативностью или низкой температурой, вероятно, будет лучше придерживаться правил. Поэтому опять же, это зависит от вашего случая использования, знаете ли, если вы занимаетесь креативным письмом, вы можете захотеть использовать более высокую температуру, но для таких вещей, как RAG, где информация, которая выводится, должна быть точной и правдивой, я думаю, важно, чтобы мы поддерживали низкую температуру. Окей, я немного поговорил об этом здесь, поэтому, конечно, более низкая температура 0 делает вывод LLM более детерминированным, что теоретически должно привести к меньшему количеству галлюцинаций. Окей, мы снова будем использовать `LLMChain` здесь, это для тех из вас, кто использует LangChain, это эквивалентно объекту `LLMChain`. Поэтому наш шаблон подсказки передаётся в наш LLM. Окей, и отныне у нас есть этот конвейер. Теперь давайте посмотрим, как мы бы использовали этот конвейер. Таким образом, собираемся получить некоторую, создать некоторый контекст здесь. Таким образом, это просто некоторый контекст вокруг Orelio AI, упомянули, что мы построили семантические маршрутизаторы, SM Junkers, там платформа ИИ и услуги разработки, мы упомянули, я думаю, мы конкретно это описываем позже в примере, поэтому эксперты LangChain, небольшой фрагмент информации. Теперь большинство LLMs не обучались на недавнем интернете, поэтому тот факт, что это произошло в сентябре 2024 года, относительно недавний, поэтому многие LLMs изначально вы не ожидаете, что они это знают, поэтому это хорошая информация, о которой можно спросить. Поэтому мы вызываем, у нас есть наш запрос, так что что мы делаем, и у нас есть этот контекст. Окей, мы передаём это в этот конвейер, который мы определили здесь. Хорошо, когда мы вызываем это, это автоматически возьмёт `query` и `context` и фактически передаст это в наш шаблон подсказки. Окей, если мы хотим, мы также можем быть немного более явными, поэтому вы, вероятно, увидите, что я делаю это на протяжении всего курса, потому что я действительно люблю быть явным во всём, честно говоря, и вы, вероятно, увидите, что я делаю это. Окей, и это делает то же самое, или вы увидите это через мгновение, это делает точно то же самое. Опять же, это просто ещё одна вещь, поэтому всё, что я делаю в этом сценарии, это говорю: "Окей, возьми из словаря `query`, а также возьми из этого входного словаря ключ `context`". Окей, это делает точно то же самое. Причина, по которой мы можем захотеть написать это, в основном для ясности, честно говоря, просто явно сказать: "Окей, это входы", потому что иначе у нас их нет в коде, кроме как в наших исходных подсказках здесь, что не очень понятно. Поэтому я думаю, обычно хорошая идея просто быть более явным с этими вещами, и, конечно, если вы решите немного изменить вещи, скажем, вы измените это на `input` позже, вы всё ещё можете передать тот же ввод здесь, вы просто, знаете ли, сопоставляете его между различными ключами по существу, или если вы хотите просто изменить это, я не знаю, вам нужно преобразовать его в нижний регистр на пути, или что-то вы можете сделать так. Я просто переопределю на самом деле, и мы вызовем снова. Окей, мы видим, что это делает точно то же самое. Окей, наш ИИ, поэтому это сообщение ИИ, только что сгенерированное LLM. Окей, опыт в создании агентов ИИ, несколько фреймворков с открытым исходным кодом, платформа Orelio AI. Окей, верно, у них есть всё, кроме LangChain экспертов, он не упомянул этого, но мы, да, мы проверим это позже. Окей, переходим к подсказкам few-shot. Это специфическая техника подсказок. Теперь многие современные или лучшие LLMs очень хорошо следуют инструкциям, поэтому вы обнаружите, что few-shot подсказки сейчас менее распространены, чем раньше, по крайней мере для больших, более современных моделей, но когда вы начинаете использовать меньшие модели, это не то, что мы можем использовать здесь, но скажем, вы используете модель с открытым исходным кодом, такую как Llama 3 или Llama 2, которая намного меньше, вам, вероятно, придётся учитывать такие вещи, как few-shot подсказки, хотя, если сказать об этом с моделями OpenAI, по крайней мере, с текущими моделями OpenAI это не так важно, тем не менее, это может быть полезно. Идея few-shot подсказок заключается в том, что вы предоставляете несколько примеров вашему LLMs того, как он должен вести себя, прежде чем вы фактически переходите к основной части разговора. Давайте посмотрим, как это будет выглядеть. Таким образом, мы создаём пример подсказки, у нас есть наш человеческий и ИИ, человеческий ввод, ответ ИИ. В основном мы говорим: "Окей, с этим типом ввода вы должны предоставить этот тип вывода", вот что мы делаем здесь, и мы просто предоставим несколько примеров. Окей, у нас есть наш ввод здесь, запрос один, здесь ответ один, верно? Это просто, я просто хочу показать вам, как это работает, это не то, что мы фактически передадим в наш LLM. Затем с обоими этими примерами и нашей примерной подсказкой мы передадим и то, и другое в шаблон few-shot сообщения LangChain. Окей, и вы увидите, что мы получим из этого. Окей, в основном он форматирует всё и структурирует всё для нас. Окей, и используя это, конечно, это зависит от, скажем, вы видите, что ваш пользователь говорит о конкретной теме, и вы хотели бы направлять ваш LLM говорить об этой конкретной теме и конкретным образом, верно? Поэтому вы можете определить, что пользователь говорит об этой теме, либо как совпадение ключевых слов, либо как совпадение семантического сходства, и на основе этого вы можете захотеть изменить эти примеры, которые вы передаёте в ваш шаблон few-shot сообщения, и затем, очевидно, для этого может быть то, что вы делаете для темы A, для темы B вы можете иметь другой набор примеров, которые вы передаёте в это, всё это время ваша примерная подсказка остаётся той же самой, но вы просто изменяете примеры, которые поступают, так что они более актуальны для того, о чём на самом деле говорит ваш пользователь. Поэтому это может быть полезно. Теперь давайте посмотрим пример этого. Когда мы используем крошечный LLM, его возможности будут ограничены, хотя я думаю, что мы, вероятно, в порядке здесь. Мы собираемся сказать: "Ответьте на запрос пользователя на основе контекста ниже, всегда отвечая в формате Markdown. Знаете ли, будьте очень конкретны, системная подсказка". Это хорошо, но что мы здесь сказали, это: "Окей, всегда отвечая в Markdown, сделайте это, но при этом, пожалуйста, предоставьте заголовки, краткое резюме и используйте маркированные списки, затем сделайте вывод". Поэтому вы видите это здесь. Окей, поэтому мы получаем этот обзор уже, у вас есть это и это, это на самом деле довольно хорошо, но если мы спустимся сюда, что я конкретно хочу, это всегда следовать этой структуре, верно? Поэтому у нас есть двойной заголовок для темы, заголовок резюме, несколько маркированных списков, и затем я всегда хочу следовать этому шаблону, где это что-то типа: "В заключение", всегда, это всегда жирным шрифтом. Знаете ли, я хочу быть очень конкретным в том, чего я хочу, и, честно говоря, с gpt-4-mini вы можете фактически просто указать большую часть этого в подсказке, но ради примера мы будем предоставлять few-shot примеры в наших примерах few-shot подсказок вместо этого, чтобы получить это. Поэтому мы будем предоставлять один пример здесь, второй пример здесь, и вы увидите, что мы просто следуем тому же шаблону, мы просто устанавливаем шаблон, который должен использовать LLM. Поэтому мы настроим это здесь, у нас есть наш основной заголовок, небольшое резюме, некоторые подзаголовки, маркированные списки, подзаголовок, маркированные списки, подзаголовок, маркированные списки, в заключение и так далее, то же самое с этим здесь. Окей, и давайте посмотрим, что мы получили. Окей, это структура нашего нового шаблона few-shot подсказки, вы можете видеть, как всё это выглядит. Давайте спустимся, и мы будем делать, мы в основном будем вставлять это непосредственно в наш шаблон подсказки чата, поэтому у нас есть для сообщений системная подсказка, подсказка пользователя, и затем у нас есть в нём эти. Позвольте мне фактически показать вам очень быстро, верно? Поэтому у нас просто есть этот шаблон few-shot подсказки, который будет передаваться в середину здесь, запустите это, а затем передайте всё это обратно в наш конвейер. Окей, и это, знаете ли, изменит структуру так, чтобы у нас был этот жирный "В заключение" в конце здесь. Окей, мы можем хорошо видеть здесь, поэтому мы получаем больше этой точной структуры, которую мы получали. Снова с моделями GPT-4 и многими другими моделями OpenAI вам не нужно этого делать, но вы увидите это в других примерах. У нас есть пример этого, где мы используем Llama и используем, я думаю, Llama 2, если я не ошибаюсь, и вы можете видеть, что добавление этого шаблона few-shot подсказки на самом деле является очень хорошим способом заставить эти меньшие, менее способные модели следовать вашим инструкциям. Поэтому это действительно важно, когда вы работаете с этими меньшими LLMs, это может быть очень полезно, но даже для таких моделей, как GPT-4, если вы обнаружите, что вы боретесь с подсказкой, это просто не совсем следует тому, что вы хотите, чтобы оно делало, это очень хороший метод для того, чтобы заставить его следовать очень прямой структуре или поведению. Окей, переходим к подсказкам Chain of Thought.
Это более распространённая техника подсказок, которая побуждает LLM пошагово продумать свои рассуждения или мысли. Это Chain of Thought. Идея заключается в том, что, окей, в математическом классе, когда вы были ребенком, учителя всегда заставляли вас записывать ваши вычисления, верно? И на это было несколько причин, одна из них - заставить вас думать, потому что они знают, что во многих случаях, на самом деле, знаете ли, вы ребёнок, и вы в стрессе, вы действительно не заботитесь об этом тесте, и, знаете ли, они просто пытаются заставить вас немного замедлиться и действительно записать свои рассуждения, и это заставляет вас думать: "О, на самом деле я пропускаю немного в своей голове, потому что я пытаюсь сделать всё это здесь, если я запишу это, вдруг это как: "О, на самом деле...
Я, да, мне нужно сделать это немного по-другому, вы, вы понимаете, хорошо, вы, вероятно, торопитесь сейчас. Я не говорю, что большая языковая модель торопится, но это похожий эффект, когда большая языковая модель записывает всё, они, как правило, делают всё правильно чаще, и в то же время, это похоже на то, когда вы ребёнок, и учитель проверяет вашу экзаменационную работу. Заставив большую языковая модель записать свои рассуждения, вы, как человек или инженер, можете увидеть, где большая языковая модель ошиблась, если она ошиблась, что может быть очень полезно, когда вы пытаетесь диагностировать проблемы. Таким образом, с «цепочкой рассуждений» мы должны увидеть э-э, меньше галлюцинаций и, как правило, лучшую производительность. Теперь, чтобы реализовать «цепочку рассуждений» в LangChain, нет никаких специфических объектов LangChain, которые это делают, вместо этого это, это просто подсказка. Хорошо, давайте посмотрим и просто посмотрим, как мы могли бы это сделать. Хорошо, будьте полезным помощником и отвечайте на вопросы пользователей. Вы должны ответить на вопрос напрямую, без какого-либо другого текста или объяснения. Хорошо, это наша система без «Цепочки рассуждений». Проблемы. Я просто отмечу здесь, особенно с OpenAI, это одна из тех вещей, где вы увидите её больше с меньшими моделями. Большинство больших языковых моделей фактически обучены использовать подсказки «цепочки рассуждений» по умолчанию. Таким образом, мы фактически специально говорим ей здесь: «Вы должны ответить на вопрос напрямую, без какого-либо другого текста или объяснения». Хорошо, мы фактически как бы даём ей обратную подсказку, чтобы не использовать «цепочку рассуждений», иначе по умолчанию она фактически попытается это сделать, потому что она обучена этому. Вот насколько релевантна «Цепочка рассуждений». Хорошо, я собираюсь сказать, сколько нажатий клавиш мне нужно, чтобы набрать числа от 1 до 500. Хорошо, мы настроили нашу цепочку большой языковой модели, Pipeline, и мы просто вызовем наш запрос, и мы увидим, что получим. Общее количество нажатий клавиш, необходимых для набора чисел от одного до 500, составляет 1511. Э-э, фактическое число, как я написал здесь, составляет 1392. Без «цепочки рассуждений» она галлюцинирует. Хорошо, давайте посмотрим, хорошо, с подсказкой «Цепочка рассуждений», что она делает. Будьте полезным помощником и отвечайте на вопросы пользователей. Чтобы ответить на вопрос, вы должны систематически и точно перечислить все подзадачи, которые необходимо решить, чтобы ответить на вопрос, решить каждую подзадачу индивидуально. Вы должны иногда кричать на большую языковую модель, чтобы заставить её слушать, и последовательно. Наконец, используйте всё, что вы проработали, чтобы дать окончательный ответ. Хорошо, мы заставляем её как бы пройти через всю проблему там. Можно удалить это, не уверен, почему это здесь. Запустим это снова. Я не знаю, почему у нас есть контекст там. Я удаляю это и посмотрим. Вы можете сразу увидеть, что это занимает намного больше времени для генерации вывода. Это потому, что она генерирует гораздо больше токенов. Это всего лишь один недостаток этого, но давайте посмотрим, что у нас есть. Чтобы определить, сколько нажатий клавиш нужно для набора этих чисел, мы разбиваем проблему на несколько подзадач. Итак, посчитайте количество цифр от 1 до 9, от 10 до 99 и так далее, и посчитайте цифры в числе 500. Интересно, так вот как она разбивает это на большее количество цифр, посчитайте на предыдущих шагах. Итак, мы проходим по общему количеству цифр, и мы видим, что это нормально. Девять цифр для тех, для здесь, 180 для здесь, 1200, и, конечно же, три здесь. Итак, она получает все эти суммы, эти цифры и фактически приходит к правильному ответу. Хорошо, это, это разница между «Цепочкой рассуждений» и без неё. Без неё мы просто получаем неправильный ответ, в основном угадывая. С «цепочкой рассуждений» мы получаем правильный ответ просто потому, что большая языковая модель записывает свои рассуждения и разбивает проблему на несколько частей, что я нашёл очень интересным, что она это делает. Это довольно круто. Теперь я просто посмотрю. Как мы упоминали ранее, большинство больших языковых моделей в настоящее время фактически обучаются использовать подсказки «цепочки рассуждений» по умолчанию. Давайте просто посмотрим, если мы ничего не упомянем. Будьте полезным помощником и отвечайте на вопросы пользователей. Итак, мы не говорим ей не обдумывать свои рассуждения, и мы не говорим ей обдумывать свои рассуждения. Давайте просто посмотрим, что она сделает. Хорошо, вы можете снова увидеть, что она фактически делает те же самые рассуждения. Она не даёт нам такие подзадачи в начале, но она проходит через них и разбивает всё на части, что довольно интересно, и мы получаем тот же правильный ответ. Форматирование здесь немного отличается, оно, вероятно, немного чище, хотя я думаю, э-э, я не знаю, здесь мы получаем гораздо больше информации. Оба варианта хороши, и в этом сценарии мы фактически получаем правильный ответ. Таким образом, вы можете видеть, что подсказка «Цепочка рассуждений» фактически была в буквальном смысле обучена в модели, и вы увидите это с большинством, я думаю, всеми передовыми большими языковыми моделями. Хорошо, круто. Это наша глава о подсказках. Ещё раз, мы очень сильно фокусируемся на многих фундаментальных принципах подсказок, и, конечно же, связываем это с фактическими объектами и методами в LangChain. Но на данный момент это всё о подсказках, и мы перейдём к следующей главе. В этой главе мы рассмотрим разговорную память в LangChain. Мы рассмотрим основные компоненты памяти чата, которые действительно были в LangChain с самого начала, но по сути больше не входят в библиотеку, и мы увидим, как мы фактически реализуем эти исторические утилиты разговорной памяти в новых версиях LangChain, то есть 0.3. Сейчас, в качестве предварительного предупреждения, эта глава довольно длинная, но это потому, что разговорная память является настолько важной частью чат-ботов и агентов. Разговорная память позволяет им запоминать предыдущие взаимодействия, и без неё наши чат-боты и агенты просто отвечали бы на последнее сообщение, не понимая предыдущих взаимодействий в разговоре. Таким образом, они просто не были бы разговорными, и в зависимости от типа разговора мы могли бы использовать различные подходы к тому, как мы запоминаем эти взаимодействия в разговоре. Теперь на протяжении всей этой главы мы будем фокусироваться на этих четырёх типах памяти. Мы будем ссылаться на них, и я покажу вам, как каждая из них работает, но мы действительно фокусируемся на переписывании их для последней версии LangChain с использованием так называемого запускаемого объекта с историей сообщений. Таким образом, мы будем по существу рассматривать исходные реализации для каждого из этих четырёх исходных типов памяти, а затем мы будем переписывать их с помощью класса запускаемой памяти истории. Просто взглянем на каждый из этих четырёх типов очень быстро. Буферная память разговора, я думаю, самая простая и интуитивно понятная из этих типов памяти. Это буквально просто: у вас есть ваши сообщения, они поступают в этот объект, они хранятся в этом объекте как по существу список, и когда они вам снова понадобятся, он вернёт их вам. Ничего больше нет, это очень просто. Буферная память окна разговора. Хорошо, новое слово посреди окна. Это работает почти так же, но эти сообщения, которые она хранит, не будут возвращать вам все из них. Вместо этого она будет возвращать только самые последние, скажем, три, например. И это определяется параметром K. Конкатенация памяти резюме. Вместо того, чтобы отслеживать всю память взаимодействия напрямую, она делает так: по мере поступления этих взаимодействий, она фактически будет брать их и сжимать их в меньшее краткое изложение того, что было в этом разговоре. И по мере поступления каждого нового взаимодействия она будет делать это, будет продолжать итерацию по этому резюме, а затем оно будет возвращено нам, когда нам это понадобится. И наконец, у нас есть буферная память разговорного резюме. Итак, это, это берет, так что часть буфера здесь фактически относится к очень похожей вещи, что и буферная память окна, но вместо того, чтобы быть, знаете ли, K последних сообщений, она смотрит на количество токенов в вашей памяти и возвращает последние K токенов. Вот что здесь означает буфер. А затем она также объединяет это с памятью резюме здесь. По существу, вы получаете что-то вроде списка последних сообщений, основанных на длине токенов, а не на количестве взаимодействий, плюс резюме, которое, вы знаете, пришло бы в начало. Таким образом, вы получаете и то, и другое. Идея в том, что, очевидно, это резюме здесь будет хранить все ваши взаимодействия в очень сжатой форме, поэтому вы теряете меньше информации, и вы всё ещё сохраняете, может быть, самое первое взаимодействие. Пользователь мог представиться, сообщив своё имя, надеюсь, это будет сохранено в резюме, и оно не будет потеряно. И у вас есть почти как более высокое разрешение на последние K или k токенов из вашей памяти. Хорошо, давайте перейдём к коду. Мы переходим к блокноту 04 «Память чата», открываем его в Colab. Хорошо, вот мы здесь. Давайте установим предварительные требования. Запустим всё. Мы можем или не можем использовать align Smith, это зависит от вас. Введите это, и давайте начнём. Сначала просто инициализируем нашу большую языковую модель, используя 40 mini в этом примере, опять же низкая температура, и мы начнём с буферной памяти разговора. Итак, это исходная версия этого типа памяти. Итак, где мы находимся? Мы здесь. Память разговора, буфер памяти, и мы возвращаем сообщения, которые должны быть установлены в true. Причина, по которой мы устанавливаем return messages в true, это упоминается здесь, это если вы этого не сделаете, она будет возвращать вашу историю чата как строку большой языковой модели, тогда как современные чат-большие языковые модели ожидают объектов сообщений. Да, вы просто хотите возвращать их как сообщения, а не как строки. В противном случае, да, вы получите какое-то странное поведение от ваших больших языковых моделей, если вернёте их как строки. Таким образом, вы хотите убедиться, что это true. Я думаю, по умолчанию это может быть не true, но это происходит, это устарело. Она говорит вам здесь, как предупреждение об устаревании, это происходит из старой LangChain, но это хорошее место для начала, просто чтобы понять это, а затем мы перепишем это с запускаемыми объектами, что является рекомендуемым способом сделать это в настоящее время. Добавление сообщений в нашу память. Мы напишем это. Итак, это просто, просто разговор пользователь, ИИ, пользователь, ИИ и так далее. Случайный чат. Главное, что нужно отметить здесь, это то, что я указываю своё имя. У нас есть имя модели в начале этих взаимодействий. Таким образом, я просто добавлю все из них, сделаю это так. Тогда мы можем просто увидеть, мы можем загрузить нашу историю так. Давайте просто посмотрим, что у нас есть там. Хорошо, у нас есть сообщение человека, сообщение ИИ, сообщение человека. Это именно то, что я показал вам здесь, оно просто в этом формате сообщения из LangChain. Таким образом, мы можем сделать это альтернативно. Мы фактически можем сделать это. Мы можем получить нашу память. Мы инициализируем буферную память конкатенации, как мы делали раньше, и мы фактически можем добавить сообщение непосредственно в нашу память так. Таким образом, мы можем использовать это add user message, add AI message и так далее. Загрузить снова, и это даст нам точно такую же вещь. Снова есть несколько способов сделать одно и то же. Круто. У нас есть это, чтобы передать всё это в нашу большую языковую модель. Опять же, это всё устарело, поэтому мы узнаем, как правильно сделать это через мгновение, но так LangChain делала это в прошлом. Чтобы передать всё это в нашу большую языковую модель, мы будем использовать эту цепочку разговоров. Опять же, это устарело в настоящее время, мы использовали бы LLM для этого. Я просто хочу показать вам, как всё это будет работать вместе, а затем мы вызовем. Что моё имя? Давайте запустим это, и мы увидим, что получим. Она помнит всё. Помните. Таким образом, эта буферная память разговора не удаляет сообщения, она просто помнит всё. И честно говоря, с такими контекстными окнами многих больших языковых моделей это может быть тем, что вы делаете. Это зависит от того, как долго вы ожидаете, что разговор будет продолжаться, но вы могли бы, вероятно, в большинстве случаев обойтись этим. Хорошо, что, давайте посмотрим, что мы получим. Я говорю: «Что моё имя?» Давайте посмотрим, что она мне даст. Она говорит: «Ваше имя - Джеймс». Отлично, спасибо. Это работает. Теперь, как я уже упоминал, всё это, что я только что показал вам, фактически устарело. Это старый способ делать вещи. Давайте посмотрим, как мы это делаем в современной или актуальной LangChain. Мы будем использовать этот запускаемый объект с историей сообщений для его реализации. Нам нужно будет использовать LLM для этого. Нам нужно будет просто определить шаблоны подсказок, нашу большую языковую модель, как обычно. Итак, мы настроим нашу системную подсказку, которая просто полезный помощник под названием Зета. Мы добавим этот заполнитель сообщений. Итак, это важно. По существу, именно отсюда поступают наши сообщения. Наша буферная память для разговора будет вставлена. Итак, это будет история чата, которая будет вставлена после нашей системной подсказки, но перед нашим последним запросом, который будет вставлен последним здесь. Заполнитель сообщений items, это важно, и мы используем это на протяжении всего курса. Таким образом, мы используем его как для истории чата, так и позже увидим, мы также используем его для промежуточных мыслей, через которые прошёл бы агент. Поэтому важно помнить об этой мелочи. Мы связываем наш шаблон подсказки с нашей большой языковой моделью. Опять же, если бы мы хотели, мы могли бы также добавить, я думаю, у нас здесь только запрос. О, нам, вероятно, также нужна наша история, но я не буду делать это прямо сейчас. Итак, у нас есть наш Pipeline, и мы можем перейти к фактической настройке нашего запускаемого объекта с историей сообщений. Теперь этот класс или объект при инициализации требует нескольких элементов. Мы видим их здесь. Итак, мы видим, что у нас есть наш Pipeline с историей. По сути, это будет, вы можете видеть здесь, правда, у нас есть этот ключ history messages. Это здесь должно совпадать с тем, что мы предоставили в качестве заполнителя сообщений в нашем Pipeline. Итак, у нас есть шаблон подсказки Pipeline здесь и здесь. Итак, именно отсюда он поступает. Он поступает из переменной имени заполнителя messages, история, правда? Это важно, что это связано с этим. Затем для ключа входных сообщений здесь у нас есть запрос, который снова связан с этим. Итак, оба важны, чтобы это было. Другая важная вещь - это, очевидно, мы передаём этот Pipeline из предыдущего примера, но у нас также есть эта функция get session history. По сути, она делает вот что: она говорит: «Хорошо, мне нужно получить список сообщений, которые составляют историю моего чата, которые будут вставлены в эту переменную». Это функция, которую мы определяем. И внутри этой функции мы пытаемся здесь фактически воспроизвести то, что у нас было с предыдущей буферной памятью разговора. Итак, вот что мы здесь делаем. Это очень просто, правда. Итак, у нас есть эта история сообщений чата в памяти. Итак, это просто объект, который мы собираемся вернуть. Это будет устанавливать идентификатор сессии. Идентификатор сессии - это по существу уникальный идентификатор, так что каждое обсуждение или взаимодействие в рамках одного разговора сопоставляется с конкретным разговором, поэтому у вас нет перекрывающихся, скажем, если у вас есть несколько пользователей, использующих одну и ту же систему, вы хотите иметь уникальный идентификатор сессии для каждого из них. И она делает вот что: она говорит: «Хорошо, если идентификатор сессии не в карте чата, которая является этим пустым словарем, который мы определили здесь, мы инициализируем эту сессию историей сообщений чата в памяти». Вот и всё. И мы возвращаем. И всё, что она будет делать, это будет по существу добавлять наши сообщения, они будут добавлены в эту карту чата, идентификатор сессии, и они будут возвращены. Там ничего нет. Там нет ничего ещё. Итак, мы вызываем наш запускаемый объект. Давайте посмотрим, что мы получим. Мне нужно это запустить. Обратите внимание, что у нас есть эта конфигурация. У нас есть идентификатор сессии, который снова, как я уже упоминал, используется для разделения различных разговоров. Итак, мы запустили это. Теперь давайте запустим ещё несколько. Что моё имя? Давайте посмотрим, помнит ли она. Ваше имя - Джеймс. Как я могу вам помочь сегодня, Джеймс? Итак, что мы только что сделали там - это буквально буферная память разговора, но для актуальной LangChain с LLM с RunnerBS. Таким образом, рекомендуемый способ сделать это в настоящее время. Это очень простой пример, правда, и в нём не так уж много. Это становится немного сложнее, когда мы начинаем думать о разных типах памяти, хотя, если сказать честно, это не так уж и сложно. Мы будем редко менять способ получения наших взаимодействий. Давайте, давайте углубимся в это и посмотрим, как мы сделаем что-то подобное с буферной памятью окна конкатенации, но сначала давайте фактически поймём, что такое буферная память окна конкатенации. Как я уже упоминал в начале, она будет отслеживать последние K сообщений. Здесь нужно иметь в виду несколько вещей. Больше сообщений означает больше токенов, отправляемых с каждым запросом, и если у нас больше токенов в каждом запросе, это означает, что мы увеличиваем задержку наших ответов, а также стоимость. С предыдущим типом памяти мы просто отправляем всё, и поскольку мы отправляем всё, это будет увеличивать нашу стоимость, это будет увеличивать нашу задержку для каждого сообщения, особенно по мере того, как разговор становится всё длиннее и длиннее, и нам не обязательно этого захочется. Таким образом, с этой буферной памятью окна разговора мы просто скажем: «Хорошо, просто верни мне самые последние сообщения». Хорошо, давайте, давайте посмотрим, как это будет работать. Мы вернём последние четыре сообщения. Мы снова убедимся, что messages установлено в true. Опять же, это устарело. Это просто старый способ сделать это. Через мгновение мы увидим обновлённый способ сделать это. Мы добавим все наши сообщения. Итак, у нас есть это, и просто посмотрите здесь, правда. Итак, мы добавили все эти сообщения, здесь больше четырёх сообщений, и мы фактически можем видеть это здесь. Итак, у нас есть сообщение человека, ИИ, человек, ИИ, человек, ИИ, человек, ИИ. Итак, у нас есть четыре пары взаимодействий человек-ИИ, но здесь у нас нет, есть больше четырёх пар. Четыре пары приведут нас обратно сюда. Я исследую различные типы разговорной памяти. И если мы посмотрим здесь, первое сообщение, которое у нас есть, это: «Я исследую различные типы разговорной памяти». Итак, она отрезала эти два здесь, что будет немного проблематично, когда мы спросим её о нашем имени. Хорошо, давайте просто посмотрим. Мы собираемся использовать объект цепочки разговоров. Опять же, просто помните, что это устарело. И я хочу сказать: «Что моё имя?» Давайте посмотрим, что она скажет. Она говорит: «Извините, но у меня нет доступа к вашему имени или какой-либо личной информации. Если хотите, вы можете сообщить мне ваше имя». Итак, она фактически не помнит. Итак, это своего рода минус буферной памяти окна разговора. Конечно, чтобы исправить это в этом сценарии, мы могли бы просто увеличить K, может быть, мы скажем: «Запомни предыдущие восемь пар взаимодействий», и она фактически запомнит. Что моё имя? Ваше имя - Джеймс. Итак, теперь она помнит. Мы просто изменили то, сколько она помнит, но, конечно же, у этого есть плюсы и минусы. Это действительно зависит от того, что вы пытаетесь построить. Давайте посмотрим, как мы бы фактически реализовали это с помощью запускаемого объекта с историей сообщений. Хорошо, это становится немного сложнее здесь, хотя это, это не сложно, но мы увидим. Хорошо, у нас есть история сообщений буферного окна. Мы создаём класс здесь. Этот класс будет наследовать от базового объекта истории сообщений чата из LangChain. И все наши другие объекты истории сообщений могут делать то же самое, что и раньше с объектом сообщения в памяти, который по существу воспроизводил буферную память, поэтому нам фактически ничего не нужно было делать. Нам не нужно было определять свой собственный класс здесь. Таким образом, в этом случае мы делаем это. Мы следуем тому же шаблону, что и LangChain, с этой базовой историей сообщений чата, и вы можете видеть несколько функций здесь, которые важны. Добавить сообщения и очистить - это те, на которых мы будем фокусироваться. Нам также нужно иметь сообщения, этот атрибут объекта здесь. Итак, мы просто реализуем синхронные методы здесь. Если мы хотим, чтобы это было асинхронным, если мы хотим поддерживать асинхронность, нам пришлось бы добавить add messages, get messages и clear. Давайте сделаем это. У нас есть сообщения. У нас есть K. Опять же, мы рассматриваем возможность запоминания только самых последних K сообщений. Поэтому важно, чтобы у нас была эта переменная. Мы добавляем сообщения через этот класс. Это будет использоваться LangChain в нашем запускаемом объекте, поэтому нам нужно убедиться, что у нас есть этот метод. И всё, что мы будем делать, это расширять список self messages. А затем мы фактически просто обрежем его, чтобы мы не помнили ничего, кроме тех самых последних K сообщений, которые мы установили отсюда. А затем у нас также есть метод clear. Таким образом, нам нужно включить это. Это просто очистит историю. Итак, это не сложно, правда? Это просто даёт нам этот хороший стандартный интерфейс по умолчанию для истории сообщений, и нам просто нужно убедиться, что мы следуем этому шаблону. Я включил это print здесь, просто чтобы мы могли видеть, что происходит. Итак, у нас есть это, а теперь для той функции get chat history, которую мы определили ранее, вместо использования встроенного метода мы будем использовать свой собственный объект, который является историей сообщений буферного окна, которая будет определена здесь. Итак, если идентификатор сессии не в карте чата, как мы делали раньше, мы будем инициализировать нашу историю сообщений буферного окна. Мы устанавливаем K здесь со значением по умолчанию 4, а затем просто возвращаем его. И это всё. Давайте запустим это. У нас есть запускаемый объект с историей сообщений. У нас есть все эти переменные, которые точно такие же, как и раньше, 4, но у нас также есть эти переменные здесь с конфигурацией истории фабрики, и именно здесь, если у нас есть новые переменные, которые мы добавили в нашу историю сообщений, в этом случае k, который у нас есть здесь, нам нужно предоставить это LangChain и сказать ей, что это новое настраиваемое поле. И мы также добавили его для идентификатора сессии здесь. Таким образом, мы просто явны и имеем всё это. Итак, у нас есть это, и мы запускаем. Теперь давайте перейдём к вызову и посмотрим, что мы получим. Хорошо, важно здесь, эта конфигурация истории фабрики, которая как бы передаётся в наш вызов, чтобы мы фактически могли изменить эти переменные отсюда. Хорошо, у нас есть настраиваемый идентификатор сессии. Мы просто помещаем сюда всё, что хотим, а затем у нас также есть число K. Запомните предыдущие четыре взаимодействия. Я думаю, в этом случае мы делаем что-то немного другое. Я думаю, что мы запоминаем четыре взаимодействия, а не предыдущие четыре пары взаимодействий. Моё имя - Джеймс. Мы пройдёмся. Я просто собираюсь фактически очистить это, а теперь я начну снова, и мы будем использовать точно такие же add user message, add AI message, которые мы использовали раньше. Мы просто вручную вставляем всё это в нашу историю, чтобы мы могли тогда просто увидеть, хорошо, каков результат. И вы можете видеть, что k равно 4 фактически в отличие от того, что было раньше, когда мы сохраняли верхние четыре пары взаимодействий, мы теперь сохраняем последние четыре взаимодействия, а не пары, только взаимодействия. И честно говоря, я просто думаю, что это яснее. Я думаю, что это странно, что число 4 для K фактически сохраняло бы последние восемь сообщений, правда? Я думаю, что это странно. Поэтому я просто не воспроизвожу эту странность. Мы могли бы, если бы захотели. Мне это просто не нравится, поэтому я этого не делаю. В любом случае, мы можем видеть из сообщений, которые мы возвращаем, только последние четыре сообщения. Хорошо, круто. Мы просто используя запускаемый объект воспроизвели старый способ.
Об использовании оконной памяти и порядке действий. Ладно, я снова спрошу, как меня зовут, как и раньше, он не запомнит, поэтому мы можем перейти сюда. Извините, у меня нет доступа к личной информации, и так далее, если вы хотите сообщить мне своё имя, он не знает. Теперь давайте попробуем новый вариант, где мы инициализируем новый сеанс. Хорошо, мы используем ID K4, это создаст новую беседу, и мы скажем, что установим K равным 14. Отлично. Я вручную вставлю другие сообщения, как мы делали раньше. И мы можем увидеть все эти сообщения, и видим вверху, что мы по-прежнему сохраняем сообщение «Привет, меня зовут Джеймс». Теперь давайте посмотрим, помнит ли он моё имя. Ваше имя – Джеймс. Хорошо, вот и всё. Это работает. Мы также видим, что мы только что добавили вопрос «Как меня зовут?» Давайте посмотрим, добавилось ли это в наш список сообщений. «Как меня зовут?» Отлично. И у нас также есть ответ: «Ваше имя – Джеймс». Просто вызывая это, поскольку мы используем runable с историей сообщений, он автоматически добавляет всё это в нашу историю сообщений, что очень удобно. Отлично. Итак, это буферная оконная память. Теперь мы рассмотрим, как можно сделать что-то немного более сложное, а именно – сводки. Итак, когда вы думаете о сводке, что мы делаем? Мы фактически берём сообщения, используем этот вызов LM для их суммирования, сжатия и затем сохраняем их в сообщениях. Давайте посмотрим, как это делается на самом деле. Для начала давайте просто посмотрим, как это было сделано в Old Line chain. У нас есть память сводки разговора. Давайте пройдёмся по этому и посмотрим, что мы получим. Снова те же взаимодействия. Я просто вызываю, вызываю, вызываю. Я не добавляю их напрямую в сообщения, потому что им фактически нужно пройти через процесс суммирования. И если мы посмотрим, мы увидим, как это происходит. Текущий разговор. Извините, текущий разговор: «Привет, меня зовут Джеймс». ИИ генерирует текущий разговор. Человек представляется как Джеймс. ИИ приветствует Джеймса и выражает свою готовность к чату и помощи, спрашивая, как прошёл его день. Итак, он суммирует предыдущие взаимодействия. А затем у нас есть самое последнее сообщение человека, и затем ИИ сгенерирует свой ответ. Хорошо, и это продолжается. И вы видите, что окончательная сводка здесь будет намного длиннее. Она отличается от первой сводки, конечно, спрашивающей о его дне. Человек изучает различные типы памяти разговора. ИИ отвечает с энтузиазмом, объясняя, что память разговора включает кратковременную память, долговременную память, контекстную память, персонализированную память, и затем спрашивает, сосредоточен ли Джеймс на определённом типе памяти. Отлично. Итак, мы получаем, по сути, сводка просто становится всё длиннее и длиннее по мере нашего продвижения, но в какой-то момент идея в том, что она не будет продолжать расти, и она должна быть короче, чем если бы вы сохраняли каждое взаимодействие, сохраняя при этом всю информацию. Но, конечно, вы не будете сохранять всю информацию, которую вы бы сохранили, например, с буферной памятью. Сводкой вы потеряете информацию, но, надеюсь, меньше информации, чем если бы вы просто обрезали взаимодействия. Вы пытаетесь уменьшить количество токенов, сохраняя при этом как можно больше информации. Теперь давайте спросим: «Как меня зовут?» Он должен суметь ответить, потому что мы видим в сводке, что я представился как Джеймс. Ответ: «Ваше имя – Джеймс. Как ваши исследования?» Хорошо. Круто. Давайте посмотрим, как это реализовать. Снова, как и раньше, мы будем использовать эту историю сообщений сводки разговора. Мы будем импортировать это системное сообщение. Мы будем использовать его не для LM, с которым мы общаемся, а для LM, который будет генерировать нашу сводку. На самом деле это не совсем точно. Существует создание сводки, не то чтобы это имело значение, это просто строка документации. У нас есть сообщения, и у нас также есть LM. Другой атрибут здесь, чем у нас был раньше, когда мы инициализировали историю сообщений сводки разговора. Нам нужно передать наш LM. У нас есть те же методы, что и раньше: добавление сообщений и очистка. И мы делаем так: по мере поступления сообщений мы расширяем их нашими текущими сообщениями, но затем мы их изменяем. Итак, мы составляем наши инструкции для создания сводки. Итак, это здесь. У нас есть системный фронт, дающий существующую сводку разговора и новые сообщения, генерирующие новую сводку разговора, гарантируя сохранение как можно больше релевантной информации. Затем у нас есть сообщение человека, через которое мы передаём существующую сводку. А затем мы передаём новые сообщения. Отлично. Итак, мы форматируем их, вызываем llm, и затем мы делаем так: в сообщениях мы фактически заменяем существующую историю, которая у нас была раньше, новой историей, которая представляет собой просто одно системное сообщение сводки. Давайте посмотрим, что мы получим. Как и раньше, у нас есть эта история чата, точно такая же, как и раньше. Единственное реальное различие заключается в том, что мы передаём параметр llm. И, конечно, поскольку мы передаём параметр LM сюда, это также означает, что нам придётся включить его в спецификацию настраиваемого поля, и что нам нужно будет включить его при вызове нашего конвейера. Итак, мы запускаем это, передаём LM. Конечно, одним из побочных эффектов генерации сводок для всего является то, что мы фактически генерируем больше, так что вы фактически используете довольно много токенов. Сохраняете ли вы токены или нет, на самом деле зависит от длины разговора. По мере того как разговор становится длиннее, если вы храните всё, через некоторое время использование токенов будет фактически увеличиваться. Если в вашем случае вы ожидаете более короткие разговоры, вы сэкономите деньги и токены, просто используя эту стандартную буферную память, тогда как если вы ожидаете очень длинные разговоры, вы сэкономите токены и деньги, используя историю сводки. Итак, давайте посмотрим, что мы получили оттуда. У нас есть сводка разговора. Джеймс представился, сказав: «Привет, меня зовут Джеймс». ИИ ответил, спросив: «Привет, Джеймс». Взаимодействие включает подробности об использовании токенов. Итак, мы фактически включили здесь всё, чего, вероятно, не стоило делать. Почему мы это сделали? Итак, здесь мы включаем всё это сюда. Итак, мы используем или включаем всё содержимое из сообщений. Думаю, может быть, мы просто сделаем X content для X в сообщениях. Это должно решить эту проблему. Вот и всё. Мы быстро это исправили. Итак, раньше мы передавали весь объект mage, который, очевидно, включает всю эту информацию, тогда как на самом деле мы хотим передавать только содержимое. Мы изменили это, и теперь мы получаем то, что ожидаем. Отлично. И мы можем продолжать. Итак, по мере того, как мы продолжаем, сводка должна становиться более абстрактной, как мы только что видели, она буквально просто даёт нам сообщения напрямую, почти. Итак, мы получаем немного сводки, и мы можем продолжить. Мы собираемся добавить ещё больше сообщений к этому, мы увидим, что, как только мы отправим их, мы получим ответ, отправим его снова, получим ответ, и мы просто добавляем всё это, вызывая всё это, и это, конечно, добавит всё в нашу историю сообщений. Отлично. Итак, мы запустили это, давайте посмотрим, какая последняя сводка. И у нас есть это. Итак, это сводка, которая у нас есть в нашей истории чата. Отлично. Ну и наконец, давайте посмотрим: «Как меня зовут?» Мы можем просто проверить, есть ли моё имя там, поэтому он должен суметь сказать нам. Отлично. Итак, ваше имя – Джеймс. Довольно интересно. Давайте быстро взглянем на LangSmith. Причина, по которой я хочу сделать это, заключается просто в том, чтобы указать на различные типы использования токенов, которые мы получаем с каждым из них. Итак, мы видим, что у нас есть эта Runner mess history, которая, вероятно, улучшена в наименовании, но мы видим, насколько длинна каждая из них, сколько токенов они также используют. Вернёмся сюда. У нас есть эта история сообщений runable. Мы пройдёмся по нескольким из них, может быть, сюда. Думаю, мы можем видеть здесь первое взаимодействие, где мы используем буферную память, и мы можем видеть, сколько токенов мы использовали здесь. 112 токенов, когда мы спрашиваем: «Как меня зовут?» Затем мы изменили это, включив, по-моему, 14 взаимодействий или что-то в этом роде, очевидно, увеличивая количество используемых нами токенов. Итак, мы могли видеть, что это происходит в LangChain, что очень приятно, и мы можем сравнить, сколько токенов использует каждый из них. Сейчас мы смотрим на окно буфера, а затем, если мы перейдём сюда и посмотрим на это, это использует нашу сводку. Итак, наша сводка с вопросом «Как меня зовут?» на самом деле использовала больше токенов в этом сценарии, что интересно, потому что мы пытаемся сжать информацию. Причина, по которой их больше, заключается в том, что не было так много взаимодействий. По мере увеличения длины разговора со сводкой это общее количество токенов, особенно если мы правильно настроим его, чтобы оно оставалось низким, должно оставаться относительно небольшим, тогда как с буферной памятью оно будет просто увеличиваться и увеличиваться по мере удлинения разговора. Полезный способ использования LangSmith, чтобы просто выяснить, в плане токенов и затрат, что мы рассматриваем для каждого из этих типов памяти. Итак, наш окончательный тип памяти выступает как смесь памяти сводки и буферной памяти. Что он будет делать, так это хранить буфер до N-го количества токенов, и как только сообщение превысит предел N-го количества токенов для буфера, оно будет фактически добавлено в нашу сводку. Эта память имеет преимущество в том, что она подробно запоминает самые последние взаимодействия, а также не имеет ограничения по использованию слишком большого количества токенов по мере удлинения разговора и даже потенциально превышает контекстные окна, если вы очень сильно постараетесь. Это очень интересный подход. Как и раньше, давайте попробуем исходный способ реализации этого, а затем мы перейдём к использованию нашего метода обновления для реализации этого. Мы переходим сюда, и мы будем использовать импорт памяти conversation summary buffer memory. Несколько вещей здесь: LM для сводки, у нас есть N-ное количество токенов, которое мы можем хранить, прежде чем они будут добавлены в сводку, и затем возвращаемые сообщения, конечно. Вы можете видеть снова, что это устарело. Мы используем цепочку разговора, а затем просто передаём нашу память туда, и затем мы можем общаться. Итак, очень просто. Первое сообщение, мы добавим ещё несколько здесь, и нам нужно вызвать, поскольку тип памяти здесь использует NM для создания этих сводок по мере их продвижения, и давайте посмотрим, как они выглядят. Итак, мы видим для первого сообщения здесь человеческое сообщение, а затем сообщение ИИ. Затем мы немного опускаемся вниз, снова то же самое: человеческое сообщение – это первое в нашей истории здесь, затем это системное сообщение. Итак, это в тот момент, когда мы превысили этот лимит в 300 токенов, и тип памяти здесь генерирует эти сводки. Эта сводка поступает как системное сообщение, и мы видим: «Человек по имени Джеймс представился и упомянул, что он изучает различные типы памяти разговора» и так далее. Хорошо. Отлично. Итак, у нас есть это, затем давайте немного опустимся вниз, мы можем видеть сводку там. Итак, это то, что у нас есть. Это реализация для старой версии этой памяти. Снова мы видим, что она устарела. Как нам реализовать это для более новых версий LangChain и, в частности, 0.3? Опять же, мы используем эту историю сообщений runable, и она выглядит немного сложнее, чем то, что мы получали раньше, но на самом деле это ничего слишком сложного. Мы просто создаём сводку, как мы делали с предыдущим типом памяти, но решение о добавлении в эту сводку основано в этом случае на количестве сообщений. Я не стал использовать версию LangChain, где это количество токенов. Мне это не нравится, я предпочитаю использовать сообщения. Итак, я делаю так: хорошо, пусть K сообщений. Как только мы превысим K сообщений, сообщения, выходящие за эти пределы, будут добавлены в память. Отлично. Итак, давайте посмотрим. Сначала мы инициализируем наш класс истории сообщений буфера сводки разговора с llm и K. Итак, эти два здесь. LM, конечно, для создания сводок, а K – это просто предел количества сообщений, которые мы хотим сохранить, прежде чем добавлять их в сводку или удалять их из сообщений и добавлять в сводку. Итак, начнём. Есть ли у нас существующая сводка? Причина, по которой мы устанавливаем это в None, заключается в том, что мы не можем извлечь сводку, существующую сводку, если она уже существует, и единственный способ сделать это – проверить: есть ли у нас какие-либо сообщения. Если да, мы хотим проверить, есть ли среди этих сообщений системное сообщение, потому что мы делаем ту же структуру, что и у нас здесь, где первое системное сообщение на самом деле является нашей сводкой. Итак, это то, что мы делаем здесь, мы проверяем, есть ли сообщение сводки, уже хранящееся в наших сообщениях. Итак, мы проверяем это. Если мы найдём его, мы просто сделаем. У нас есть это небольшое объявление print, чтобы мы могли видеть, что мы что-то нашли, а затем мы просто создаём нашу существующую сводку. Мне следует фактически переместить это в первый экземпляр здесь. Да. Хорошо, поэтому эта существующая сводка будет установлена в первое сообщение. И это будет системное сообщение, а не строка. Отлично. Итак, у нас есть это, затем мы хотим добавить любые новые сообщения в нашу историю. Итак, мы расширяем историю там, а затем говорим: хорошо, если длина нашей истории превышает значение K, которое мы установили, мы скажем: хорошо, мы нашли столько сообщений, мы будем удалять последние. Это будут последние два сообщения. Я скажу здесь об одной вещи или одной проблеме: мы не будем сохранять столько токенов, если мы будем суммировать каждые два сообщения. Поэтому, что я, вероятно, сделал бы в реальном, например, производственном окружении, я бы, вероятно, сказал: давайте дойдём до 20 сообщений, и как только мы достигнем 20 сообщений, давайте возьмём предыдущие 10, мы собираемся суммировать их и поместить их в нашу сводку вместе с любой предыдущей сводкой, которая уже существовала, но в, вы знаете, это тоже нормально. Итак, мы говорим, что мы нашли эти сообщения, мы собираемся удалить последние два сообщения. Итак, мы извлекаем самые старые сообщения, я должен сказать, не последние, это старые, не последние. Я хочу сохранить последние, удалить старые. Итак, мы извлекаем самые старые сообщения и сохраняем только самые последние сообщения. Затем я говорю: хорошо, если у нас нет старых сообщений для суммирования, мы ничего не делаем, мы просто возвращаемся. Итак, в случае, если это не было вызвано, мы бы попали сюда, но в случае, если это было вызвано, и у нас есть старые сообщения, мы перейдём сюда. Хорошо. Хорошо, это мы видим, есть системный шаблон запроса, говорящий: «Учитывая существующую сводку разговора и новые сообщения, сгенерируйте новую сводку разговора, гарантируя сохранение как можно больше релевантной информации». Итак, если вы хотите быть более консервативными с токенами, мы могли бы изменить этот запрос здесь, чтобы сказать: «Держите сводку в пределах длины одного абзаца», например. А затем у нас есть шаблон запроса human M, который скажет: хорошо, вот существующая сводка разговора и новые сообщения. Новые сообщения здесь на самом деле являются старыми сообщениями, но то, как мы представляем это LM здесь, заключается в том, что мы хотим суммировать весь разговор. Ему не нужно иметь самые последние сообщения, которые мы храним в нашем буфере, ему не нужно знать об этом, это не относится к сводке. Поэтому мы просто говорим ему, что у нас есть эти сообщения Zoom, и, насколько это касается этого LM, это как полный набор взаимодействий. Итак, затем мы отформатируем их и вызовем наш LM, а затем мы выведем нашу новую сводку, чтобы мы могли видеть, что происходит там, и мы добавим эту новую сводку к истории нашего разговора. И это будет работать. Мы можем просто добавить её таким образом, потому что мы уже удалили, где это было? Здесь, если у нас есть существующая сводка, мы уже удалили её из списка. Она уже была удалена из этого списка, поэтому для нас нормально просто. Нам не нужно говорить, например, нам не нужно делать это, потому что мы уже удалили это начальное системное сообщение, если оно существовало. А затем у нас есть метод очистки, как и раньше. Это вся логика для нашей памяти буфера сводки разговора. Мы переопределяем нашу функцию get chat history с параметрами LM и K, а затем нам также потребуется установить настраиваемые поля. Итак, это будет, конечно, ID сессии, LM и K. Итак, теперь мы можем вызвать значение K. Для начала оно будет равно четырем. Итак, мы видим: «Нет старых сообщений для обновления сводки». Это хорошо. Давайте вызовем это несколько раз и посмотрим, что мы получим. Итак, теперь «M для сводки с найденными шестью сообщениями, удаление самых старых двух», а затем у нас есть новая сводка в разговоре. Джеймс представился, и первым его интересует исследование различных типов памяти разговора. Итак, вы можете видеть, что сейчас здесь довольно много всего, поэтому мы определённо захотим попросить LM, сводку LM, чтобы она оставалась короткой, иначе мы просто получаем тонну материала, но мы видим, что это, знаете, работает, это функционально. Давайте вернёмся и посмотрим, можем ли мы попросить её быть немного более лаконичной. Мы переходим сюда, гарантируя сохранение как можно больше релевантной информации, однако нам нужно поддерживать нашу сводку лаконичной, предел – один короткий абзац. Что-то вроде этого. Давайте попробуем и посмотрим, что мы получим с этим. Итак, сообщение один снова. Ничего для обновления. Видим это. Итак, новая сводка. Вы можете видеть, что она немного короче, у неё нет всех этих маркированных пунктов. Итак, это кажется лучше. Давайте посмотрим. Итак, вы можете видеть, что первая сводка немного короче, но как только мы добираемся до второй и третьей сводок, вторая сводка на самом деле немного длиннее, чем третья. Итак, мы будем терять немного информации в этом случае больше, чем раньше, но мы экономим массу токенов, что, конечно, хорошо, и, конечно, мы могли бы продолжать и добавлять много взаимодействий здесь, и мы должны увидеть, что эта сводка разговора будет, она должна поддерживать эту длину примерно в один короткий абзац. Вот и всё для этой главы о памяти конкатенации. Мы рассмотрели несколько различных типов памяти, мы реализовали их старую устаревшую версию, поэтому мы можем видеть, какими они были, и затем мы переделали их для последних версий LangChain, и, честно говоря, используя логику, где мы всё больше углубляемся в детали, и это в некотором смысле нормально. Это усложняет вещи, это правда, но в других аспектах это даёт нам массу контроля. Таким образом, мы можем изменять эти типы памяти, как мы сделали с этим окончательным типом памяти буфера сводки, мы можем изменять их по своему усмотрению, что невероятно полезно, когда вы на самом деле создаёте приложения для реального мира. Вот и всё для этой главы. Мы перейдём к следующей. В этой главе мы познакомимся с агентами. Я думаю, что агенты являются одним из самых важных компонентов в мире ИИ, и я не думаю, что это исчезнет в ближайшее время. Я думаю, что большинство приложений ИИ, интеллектуальная часть которых, всегда будет реализацией агента ИИ или агентов ИИ на основе больших языковых моделей. Итак, в этой главе мы просто познакомимся с агентами в контексте LangChain. Мы будем делать это относительно просто. Мы углубимся в агенты в следующей главе, где мы немного углубимся, но мы сосредоточимся на знакомстве с основными концепциями и, конечно, агентами в LangChain здесь. Переходя непосредственно к нашей записной книжке, давайте запустим наши предварительные условия. Вы увидите, что у нас есть дополнительное предварительное условие здесь, а именно результаты поиска Google. Это потому, что мы будем использовать API поиска, чтобы позволить нашему llm в качестве агента искать в Интернете. Одна из замечательных особенностей агентов заключается в том, что они могут делать все эти дополнительные вещи, а LM сам по себе, очевидно, не может. Мы переходим сюда. У нас снова есть параметры LangSmith, конечно. Итак, вы вводите свой API LangChain, если у вас есть один, и теперь мы рассмотрим инструменты, которые являются очень важной частью агентов. Инструменты – это способ расширить наши llm практически всем, что мы можем написать в коде. Мы упомянули, что у нас будет инструмент поиска Google. Этот инструмент поиска Google – это некоторый код, который выполняется нашим llm для поиска в Google и получения результатов. Инструмент можно рассматривать как любую логику кода или любую функцию в C, в случае Python, любую функцию, которая была отформатирована таким образом, чтобы наш LM мог понять, как её использовать, а затем фактически использовать её. Хотя сам LM не использует инструмент, это больше наша логика выполнения агента, которая использует инструмент для llm. Мы собираемся создать несколько простых инструментов. Мы будем использовать так называемый декоратор инструмента из LangChain. Есть несколько моментов, которые следует учитывать при создании инструментов. Для оптимальной производительности наш инструмент должен быть просто очень читаемым. И я имею в виду под читаемым три основные вещи: во-первых, это строка документации, которая написана на естественном языке, и она будет использоваться для объяснения Alm, когда, почему и как он должен использовать этот инструмент. У нас также должны быть чёткие имена параметров. Эти имена параметров должны говорить llm: хорошо, что каждый из этих параметров, они должны быть самоочевидными. Если они не являются самоочевидными, мы должны включить объяснение этих параметров в строке документации. Затем, наконец, у нас должны быть аннотации типов как для наших параметров, так и для того, что мы возвращаем из инструмента. Давайте перейдём и посмотрим, как мы бы реализовали всё это. Мы переходим сюда, и у нас есть LangChain core tools import tool. Итак, это всего лишь четыре невероятно простых инструмента. У нас есть инструмент сложения или добавления, умножения, возведения в степень и вычитания. Итак, несколько инструментов для калькулятора. Теперь, когда мы добавляем этот декоратор инструмента, он превращает каждый из этих инструментов в то, что мы называем структурированным объектом инструмента. Мы видим это здесь. Мы видим, что у нас есть этот структурированный инструмент, у нас есть имя, описание. А затем у нас есть эта схема Al. Мы увидим это через минуту, и функция. Итак, эта функция – это буквально просто исходная функция. Это сопоставление с исходной функцией. Итак, в этом случае это функция add. Теперь описание, как мы видим, поступает из нашей строки документации, и, конечно, имя тоже поступает из имени функции. И затем мы также можем видеть, давайте просто напечатаем имя и описание, но мы также можем видеть схему ARs. Итак, эта вещь здесь, которую мы пока не можем прочитать, чтобы прочитать её, мы просто посмотрим на метод модели Json schema, и затем мы можем увидеть, что она содержит, что является всей этой информацией. На самом деле это содержит всё, включая свойства. Итак, у нас есть X, это C или заголовок для этого, и он также указывает тип. Итак, тип, который мы определяем, – это float. Float для OpenAI сопоставляется с числом, а не просто float. А затем мы также видим, что у нас есть это обязательное поле.
Итак, это показывает, как LM определяет, какие параметры являются обязательными, а какие — необязательными. Так что, да, в некоторых случаях мы можем даже сделать это здесь. Давайте сделаем Z, который будет float или None. Хорошо, и мы просто скажем, что это 0.3. Всё верно. Я удалю это через минуту, потому что это немного странно, но давайте просто посмотрим, как это выглядит. Итак, вы видите, что у нас теперь есть X, Y и Z, но в Z у нас есть некоторая дополнительная информация. Хорошо, так что это может быть любое число, или это может быть просто ничего. Значение по умолчанию для этого — 0.3. Хорошо, и затем, если мы посмотрим сюда, мы можем увидеть, что обязательное поле не включает Z, поэтому это только X и Y. Таким образом, оно описывает для нас полную схему функции, но давайте удалим это. Хорошо, и мы можем видеть это снова с нашим инструментом exponentiate, аналогичная вещь. Хорошо, как, как мы собираемся вызывать наш инструмент? Итак, базовый LM на самом деле будет генерировать строку. Хорошо, это будет выглядеть примерно так. Это будет наш вывод LM. Итак, это строка, которая представляет собой некоторый JSON, и, конечно же, чтобы загрузить строку в формат словаря, мы просто используем json.loads. Хорошо, давайте посмотрим на это. Это может быть вывод. Мы загружаем его в словарь, а затем получаем настоящий словарь, и затем мы можем взять наш инструмент exponentiate, получить доступ к базовой функции и передать ей именованные аргументы из нашего словаря здесь. Хорошо, и это запустит наш инструмент. Это лог выполнения инструмента, который реализует эта цепочка. А позже, в следующей главе, мы будем реализовывать сами. Отлично, давайте перейдём к созданию агента. Теперь мы будем создавать простого агента, вызывающего инструмент. Мы будем использовать язык выражений LangChain для этого. Теперь мы рассмотрим язык выражений LangChain или более подробно в предстоящей главе, но сейчас всё, что нам нужно знать, это то, что наш агент будет построен с использованием синтаксиса и компонентов, подобных этому. Итак, мы начнём с наших входных параметров, которые будут включать наш пользовательский запрос и, конечно же, историю чата, потому что нам нужен наш агент, чтобы он был разговорным и помнил предыдущие взаимодействия в рамках разговора. Эти входные параметры также будут включать заполнитель для того, что мы называем блокнотом агента. Теперь блокнот агента — это, по сути, то место, где мы храним внутренние мысли или внутренний диалог агента, поскольку он использует инструменты, получает наблюдения от этих инструментов и обрабатывает эти несколько внутренних шагов. Итак, в случае, который мы увидим, он будет использовать, например, инструмент сложения, получая результат, используя инструмент умножения, получая результат, а затем предоставляя нам, как пользователям, окончательный ответ. Давайте посмотрим и увидим, как это выглядит. Хорошо, мы просто начнём с определения нашего запроса. Наш запрос будет включать системное сообщение. Ничего, мы ничего особенного не добавляем туда. Мы добавим историю чата, которая является заполнителем сообщений, затем мы добавим наше сообщение от человека, а затем добавим заполнитель для блокнота агента. Теперь способ, которым мы реализуем это позже, будет немного отличаться для блокнота. Мы фактически используем этот заполнитель сообщений, но так мы используем его со встроенным create_tool_agent из LangChain. Далее мы назначим наш LM. Нам нужен наш открытый API-ключ для этого, поэтому мы введём его сюда, вот так. Хорошо, спускаемся. Хорошо, мы будем создавать этого агента, нам нужна память разговора, и мы будем использовать старый класс памяти conversation_buffer_memory, а не новый класс renable_with_message_history. Это просто потому, что мы также используем этот старый create_tool_calling_agent, и это старый способ делать вещи. В следующей главе мы будем использовать более новый. В основном то, что мы уже узнали об истории чата, мы будем использовать всё это для реализации нашей истории чата, но сейчас мы будем использовать старый метод, который устарел, просто как предупреждение. Но опять же, как я уже упоминал в самом начале, конечно, мы начинаем с абстракции, а затем переходим к деталям. Итак, мы инициализируем нашего агента, для этого нам нужны эти четыре вещи: LM, как мы определили, инструменты, как мы определили, запрос, как мы определили, и память, которая является нашей старой памятью conversation_buffer_memory. Итак, со всем этим мы собираемся продолжить и создать агента, вызывающего инструмент, а затем просто предоставим ему всё. Хорошо, вот и всё. Теперь H, вы увидите здесь, я не передал память, я передаю её сюда вместо этого. Итак, мы начнём с этого вопроса: что такое 10.7 * 7.68? Хорошо, учитывая точность этих чисел, наш обычный LM не сможет ответить на это или почти наверняка не сможет ответить на это правильно. Нам нужен внешний инструмент, чтобы ответить на это точно, и мы увидим, что это именно то, что он собирается сделать. Итак, мы видим, что сообщение действия инструментального агента здесь, мы видим, что он решил: хорошо, я собираюсь использовать инструмент умножения, и вот параметры, которые я хочу использовать для этого инструмента. Хорошо, мы видим, что X — это 10.7, а Y — 7.68. Вы можете видеть здесь, что это уже словарь, и это потому, что LangChain взял строку из нашего LM и уже преобразовал её в словарь для нас. Хорошо, это просто происходит за кулисами, и вы можете фактически увидеть, если мы немного углубимся в детали, мы можем увидеть, что у нас есть эти аргументы, и это исходная строка, которая поступала от, хорошо, которая уже была, конечно, обработана LangChain. Итак, вот это. Теперь единственное, чего здесь не хватает, это то, что, хорошо, у нас есть то, что LM хочет, чтобы мы использовали умножение, и у нас есть то, что LM хочет, чтобы мы ввели в умножение, но где ответ? Здесь нет ответа, потому что сам инструмент не был выполнен, потому что он не может быть выполнен LM. Но тогда, хорошо, разве мы уже не определили нашего агента здесь? Да, переопределённая часть нашего агента — это то, как LM имеет наши инструменты, и он будет генерировать, какой инструмент использовать, но он фактически не включает часть выполнения агента, что нормально. Исполнитель агента — это более широкая вещь, это более широкая логика, как просто логика кода, которая выступает в качестве каркаса, в котором у нас есть итерация через несколько шагов наших вызовов LM, за которыми следует вывод LM, какие инструменты использовать, за которыми следует фактическое выполнение этого для LM, а затем предоставление вывода обратно в LM для другого решения или другого шага. Итак, сам агент здесь не является полным агентивным потоком, которого мы могли бы ожидать. Вместо этого для этого нам нужно реализовать этот класс исполнителя агента. Этот исполнитель агента включает нашего агента, как и раньше, а также включает инструменты, и одна вещь здесь: хорошо, мы уже передали инструменты нашему агенту, почему нам нужно передать их снова? Инструменты, передаваемые нашему агенту здесь, используются, поэтому это, по сути, извлечение этих схем функций и передача их нашему LM, чтобы наш LM знал, как использовать инструменты. Затем мы здесь передаём инструменты снова нашему исполнителю агента, и это вместо того, чтобы смотреть на то, как использовать эти инструменты, это просто смотрит: хорошо, мне нужны функции для этих инструментов, чтобы я мог фактически выполнить их для LM или для агента. Хорошо, вот почему это происходит. Теперь мы можем также передать нашу память напрямую. Вы видите, если мы немного прокрутим вверх, мне фактически пришлось передать память вот так с нашим агентом. Это просто потому, что мы не использовали исполнителя агента. Теперь у нас есть исполнитель агента, он будет обрабатывать это для нас, и ещё одна вещь, которую он будет обрабатывать для нас, это промежуточные шаги. Вы увидите через мгновение, что когда мы вызываем исполнителя агента, мы не включаем промежуточные шаги, и это потому, что это уже обрабатывается исполнителем агента. Теперь, чтобы мы спустились, мы установим оба значения равными true, чтобы мы могли видеть, что происходит, и затем мы можем видеть здесь, что больше нет промежуточных шагов, и мы всё ещё передаём историю чата вот так, но добавление этих новых взаимодействий в нашу память будет обрабатываться исполнителем. Итак, позвольте мне фактически показать это очень быстро, прежде чем мы начнём. Хорошо, это пустое. Мы собираемся выполнить это. Мы ввели эту новую цепочку выполнения агента. Давайте просто быстро посмотрим на наши сообщения снова, и теперь вы можете видеть, что исполнитель агента автоматически обработал добавление нашего сообщения человека, а затем отвечающего сообщения ИИ для нас, хорошо, что полезно. Что произошло? Мы видим, что инструмент умножения был вызван с этими параметрами, а затем этот розовый текст, который мы получили, это наблюдение от инструмента, что инструмент вернул нам, хорошо. Затем это последнее сообщение здесь, оно не очень хорошо отформатировано. Ну, это последнее сообщение здесь поступает от нашего LM, поэтому зелёный — это наш вывод LM, розовый — это наш вывод инструмента. Хорошо, поэтому LM, увидев этот вывод, говорит: 10.7 * 7.68 приблизительно равно 82,8. Круто, используй. И мы также можем видеть историю чата, которую мы только что видели. Отлично, поэтому она была использована правильно. Мы можем также подтвердить, что это правильно. Хорошо, 82,1759 периодически, что точно то, что мы получаем здесь. Хорошо, и причина всего этого очевидна, потому что инструмент умножения просто выполняет эту точную операцию. Круто, давайте попробуем это с небольшой памятью. Я собираюсь спросить или сказать агенту: привет, меня зовут Джеймс. Мы оставим это как есть, это не первое взаимодействие, потому что у нас уже есть эти, но это раннее взаимодействие с моим именем там. Затем мы попытаемся выполнить больше вызовов инструментов в рамках одного цикла выполнения, и то, что вы увидите, когда он вызывает эти инструменты, заключается в том, что он фактически может использовать несколько инструментов параллельно. Наверняка, я думаю, два или три из них использовались параллельно, а затем отнимать пришлось ждать предыдущих результатов, поэтому он был бы выполнен позже, и мы фактически должны быть в состоянии увидеть это в LangSmith. Если мы пойдём сюда, да, мы можем видеть, что у нас есть этот начальный вызов, а затем у нас есть сложение и умножение, и экспоненцирование мы все используем параллельно, затем у нас есть ещё один вызов, который использует вычитание, а затем мы получаем ответ, хорошо, что довольно круто. А окончательный результат там — 11. Теперь, когда вы смотрите на то, является ли ответ точным, я думаю, порядок вычислений здесь не совсем правильный. Итак, если мы поместим фактическое вычисление сюда, он получит его правильно, но в противном случае, если я использую естественный язык, это как будто я делаю, может быть, я формулирую это неправильно. Хорошо, я полагаю, это довольно важно. Итак, хорошо, если мы поместим вычисление сюда, мы получим 13, поэтому с этим нужно быть осторожным, и, вероятно, потребуется немного подсказок и, возможно, примеров, чтобы сделать это плавным, чтобы он делал вещи так, как мы могли бы ожидать, или, может быть, мы, как люди, просто плохи и неправильно используем системы, то или другое. Хорошо, теперь мы прошли через это несколько раз, давайте посмотрим, может ли наш агент всё ещё помнить моё имя. Хорошо, он помнит, что меня зовут Джеймс. Хорошо, у него всё ещё есть эта память там, это хорошо. Давайте перейдём к ещё одному быстрому примеру, где мы просто будем использовать поиск Google. Мы будем использовать API SerpAPI. Вы можете, вы можете получить API-ключ, который вам нужен, отсюда: serpapi.com/user/sign_in и просто введите его сюда. Вы получите до 100 запросов в месяц бесплатно, поэтому имейте в виду, что если вы переусердствуете, я не думаю, что они взимают с вас плату, потому что я не думаю, что вы сразу вводите данные своей карты, но да, имейте в виду этот лимит. Теперь есть определённые инструменты, которые LangChain уже создала для нас. Это предварительно созданные инструменты, и мы можем просто загрузить их с помощью функции load_tools. Мы делаем это так. У нас есть наши load_tools, и мы просто передаём инструмент SerpAPI, только мы могли бы передать больше, если бы захотели, а затем мы также передаём наш LM. Теперь я собираюсь использовать этот инструмент, но я также собираюсь определить свой собственный инструмент, который позволит получить текущее местоположение на основе IP-адреса. Теперь это мы сейчас в Colab, поэтому он фактически получит IP-адрес для экземпляра Colab, на котором я сейчас нахожусь, и мы узнаем, где это находится. Это получит IP-адрес, а затем предоставит данные обратно нашему LM в этом формате. Итак, мы получим широту, долготу, город и страну. Мы также получим текущий день и время. Теперь мы переопределим наш запрос. Я не буду включать историю чата здесь, я просто хочу, чтобы это было как одноразовое действие. Я переопределю нашего агента и исполнителя агента, используя наши новые инструменты, которые являются нашим SerpAPI, а также получением текущего времени и даты и местоположения по IP-адресу. Затем я вызову исполнителя нашего агента с несколькими вопросами: какая сейчас дата и время? Какая погода там, где я нахожусь? И пожалуйста, укажите градусы Цельсия. Когда он даст мне эту погоду. Хорошо, и давайте посмотрим, что мы получим. Хорошо, по-видимому, мы находимся в Каунсил-Блафс, США. Температура 13° по Фаренгейту, что, я думаю, совершенно холодно. О боже мой, это да, минус 10, так что там очень холодно. И вы можете видеть, что, хорошо, он дал нам Фаренгейты, потому что инструмент, который мы использовали, предоставил нам Фаренгейты, что нормально, но он перевёл это в оценку Цельсия, что довольно круто. Давайте фактически выведем это. Итак, мы получаем это, что правильно, мы получаем приблизительно это, а также получаем описание условий, а также частично облачно с z% осадков, повезло им, и влажность 66%. Хорошо, довольно круто. Итак, это всё для этого введения в агентов LangChain. Как я уже упоминал, в следующей главе мы углубимся в агентов, а также реализуем это для LangChain версии 0.3. Мы оставим эту главу здесь и перейдём к следующей. В этой главе мы будем углубляться в агенты с LangChain, и мы рассмотрим, что такое агент, мы немного поговорим концептуально об агентах, реактивном агенте и типе агента, который мы собираемся создавать, и на основе этих знаний мы фактически создадим нашу собственную логику выполнения агента, которую мы называем исполнителем агента. Итак, по сравнению с предыдущим видео об агентах в LangChain, которое является скорее введением, это гораздо более подробно. Мы будем гораздо больше вникать в подробности того, что такое агенты, а также агенты в LangChain. Теперь, когда мы говорим об агентах, значительная часть агента на самом деле является относительно простой логикой кода, которая итеративно запускает вызовы LM и обрабатывает их выходы, потенциально запуская или выполняя инструменты. Точная логика для каждого подхода к созданию агента на самом деле будет очень сильно различаться, но мы сосредоточимся на одном из них, который является реактивным агентом. Теперь реактивный — это очень распространённый шаблон, и хотя он относительно старый, большинство агентов инструментов, которые мы видим, используемых OpenAI и, по сути, каждой компанией LM, все они используют очень похожий шаблон. Теперь реактивный агент следует шаблону, подобному этому. Хорошо, у нас будет наш пользовательский ввод здесь. Хорошо, наш ввод здесь — это вопрос. Помимо пульта Apple, какое ещё устройство может управлять программой, для взаимодействия с которой изначально был разработан пульт Apple? Теперь, вероятно, большинство LM фактически смогут ответить на это напрямую. Теперь это из статьи, которая была несколько лет назад. В этом сценарии, предполагая, что наш LM уже не знает ответа, есть несколько шагов, которые LM или агент могут предпринять, чтобы узнать ответ. Хорошо, первым из них является то, что мы говорим, наш вопрос здесь: какое ещё устройство может управлять программой, для взаимодействия с которой изначально был разработан пульт Apple? Итак, первое, что нужно сделать, это хорошо, что это за программа, для взаимодействия с которой изначально был разработан пульт Apple? Это первый вопрос, который у нас есть здесь. Итак, что мы делаем, мне нужно поискать пульт Apple и найти программу, которая для этого использовалась. Это шаг рассуждения, поэтому LM рассуждает о том, что ему нужно сделать. Мне нужно это поискать и найти полезную программу. Итак, мы предпринимаем действие. Это вызов инструмента здесь. Хорошо, мы будем использовать инструмент поиска, и наш запрос будет пульт Apple, а наблюдение — это ответ, который мы получаем при выполнении этого инструмента. Хорошо, ответ здесь будет: пульт Apple предназначен для управления медиацентром Front Row. Теперь мы знаем программу, для взаимодействия с которой он изначально был разработан. Теперь мы собираемся пройти через ещё один. Хорошо, это одна итерация нашего рассуждения, действия и наблюдения. Когда мы говорим о реактивном агенте здесь, хотя опять же этот шаблон очень распространён среди многих агентов, когда мы говорим о реактивном агенте, название фактически происходит от рассуждения или первых двух символов рассуждения, за которыми следует действие. Хорошо, вот откуда берётся реактивный агент. Итак, это один из наших циклов или итераций реактивного агента. Мы собираемся сделать ещё один. Следующим шагом у нас есть эта информация. LM теперь предоставлена эта информация. Теперь мы хотим выполнить поиск Front Row. Итак, мы делаем это. Это шаг рассуждения. Мы выполняем действие поиска Front Row. Хорошо, инструмент поиска запроса Front Row, наблюдение — это ответ: Front Row управляется пультом Apple или функциональными клавишами клавиатуры. Всё хорошо. Итак, мы знаем, что функциональные клавиши клавиатуры — это другое устройство, о котором мы спрашивали здесь. Теперь у нас есть вся необходимая информация. Мы можем предоставить ответ нашему пользователю. Итак, мы проходим через ещё одну итерацию здесь: рассуждение и действие. Наше рассуждение таково: теперь я могу предоставить пользователю ответ: функциональные клавиши клавиатуры. Хорошо, отлично. Затем мы используем инструмент ответа, как окончательный ответ в более распространённом использовании инструментального агента, и ответ будет: функциональные клавиши клавиатуры, которые мы затем выводим нашему пользователю. Хорошо, это цикл реактивного агента. Хорошо, глядя на это, как, где мы фактически вызываем LM и как, и каким образом мы фактически вызываем LM? У нас есть наш шаг рассуждения. Наш LM генерирует текст здесь, верно? LM генерирует, хорошо, что мне нужно сделать? Затем наш LM сгенерирует входные параметры для нашего шага действия здесь, которые будут использовать эти входные параметры, и инструмент, который будет использоваться, будет взят нашей логикой кода, логикой нашего исполнителя агента, и они будут использоваться для выполнения некоторого кода, в котором мы получим выход. Этот выход может быть взят непосредственно в наше наблюдение, или наш LM может взять этот выход, а затем сгенерировать наблюдение на его основе, это зависит от того, как вы всё реализовали. Итак, наш LM потенциально может использоваться на каждом шаге, и, конечно же, это будет повторяться на каждой итерации. Итак, у нас есть дальнейшие итерации здесь. Итак, вы потенциально используете LM много раз на протяжении всего этого процесса, что, конечно, с точки зрения задержки и стоимости токенов означает, что вы будете платить больше за агента, чем за простой LM, но это, конечно, ожидаемо, потому что у вас есть все эти разные вещи, но идея в том, что то, что вы можете получить от агента, конечно, намного лучше, чем то, что вы можете получить от одного только LM. Итак, когда мы смотрим на всё это, на всю эту итеративную цепочку рассуждений и использования инструментов, всё это должно контролироваться тем, что мы называем исполнителем агента. Хорошо, это наша логика кода, которая обращается к нашему LM, обрабатывает его выходы и повторяет этот процесс, пока мы не получим наш ответ. Итак, разбив эту часть, как это на самом деле выглядит? Это выглядит примерно так. Итак, у нас есть наш пользовательский ввод, который поступает в наш LM. Хорошо, а затем мы переходим к шагам рассуждения и действия. Действие — это ответ? Если это ответ, то, как мы видели здесь, где находится ответ? Если действие — это ответ, то истина, мы просто перейдём прямо к нашим выходам, в противном случае мы будем использовать наш инструмент выбора. Исполнитель агента будет обрабатывать всё это. Он будет выполнять наш инструмент, а затем из этого мы получим наши, знаете ли, три входа и выходы рассуждения, действия, наблюдения, а затем мы передаём всю эту информацию обратно в наш LM. Хорошо, в этом случае мы возвращаемся через этот цикл. Мы можем циклически повторяться некоторое время, пока не дойдём до этого финала, но, хорошо, давайте перейдём к коду. Когда мы перейдём к записной книжке исполнителя агента, мы откроем её в Colab и продолжим и просто установим наши предварительные требования. Здесь ничего не изменилось, это просто LangChain, LangSmith, необязательно, как и раньше, опять же необязательно API-ключ LangChain, если вы хотите использовать LangSmith. Хорошо, а затем мы перейдём к нашему первому разделу, где он определит несколько быстрых инструментов. Я не обязательно буду проходить через них, потому что мы уже рассмотрели их во введении в агенты, но очень быстро. LangChain Core Tool — мы просто импортируем этот декоратор инструмента, который преобразует каждую из наших функций здесь в то, что мы назвали бы структурированным объектом инструмента. Эта штука здесь. Хорошо, что мы можем увидеть, просто взглянув сюда, а затем, если мы захотим, мы можем извлечь всю важную информацию из этого структурированного инструмента, используя эти параметры здесь или атрибуты, такие как имя, описание, AR-схема, модель JSON-строки, которая даёт нам, по сути, как LM должна использовать нашу функцию. Хорошо, я буду продолжать это делать. Теперь очень быстро, опять же, мы рассмотрели это во вводном видео, поэтому я не хочу слишком подробно останавливаться на этом снова, но нашей логике выполнения агента понадобится эта часть. Мы будем получать строку от нашего LM, мы будем загружать её в объект словаря, и мы будем использовать её для фактического выполнения нашего инструмента, как мы делаем это здесь, используя именованные аргументы. Хорошо, вот так. Итак, с инструментами покончено, давайте посмотрим, как мы создаём нашего агента. Когда я говорю агент здесь, я конкретно говорю о части, которая генерирует наше рассуждение, затем генерирует, какой инструмент и какие входные параметры для этого инструмента будут использоваться, а остальное фактически не охватывается агентом. Остальное было бы охвачено логикой выполнения агента, которая будет брать инструмент, который нужно использовать, параметры, выполнять инструмент, получать ответ, то есть наблюдение, а затем повторять это, пока LM не будет удовлетворён, и у нас будет достаточно информации, чтобы ответить на вопрос. Итак, глядя на это, наш агент выглядит примерно так. Это довольно просто. У нас есть наши входные параметры, включая историю чата, пользовательский запрос. У нас есть наши входные параметры, включая историю чата, пользовательский запрос, и на самом деле здесь также будут любые промежуточные шаги, которые произошли. У нас есть шаблон запроса, а затем у нас есть наш LM, связанный с инструментами. Давайте посмотрим, как всё это будет выглядеть, начиная с того, что мы определим наш шаблон запроса. Поиск будет выглядеть так: у нас есть наше системное сообщение: ваш полезный помощник, отвечая на эти вопросы, вы должны использовать, чтобы предоставить после использования инструмента. Вывод инструмента будет предоставлен в блокноте ниже. Хорошо, что мы здесь называем. Если у вас есть ответ в блокноте, вы не должны использовать больше инструментов и установить ответ напрямую пользователю. Хорошо, у нас есть это как наше системное сообщение. Мы, очевидно, могли бы изменить это в зависимости от того, что мы на самом деле делаем. Затем после нашего системного сообщения у нас будет наша история чата, любые предыдущие взаимодействия между пользователем и ИИ. Затем у нас есть наше текущее сообщение от пользователя. Хорошо, оно должно быть передано в поле ввода там, а затем после этого у нас есть наш блокнот агента или промежуточные мысли.
Это то, как работают вещи, например, LLМ решает: «Окей, вот что мне нужно сделать, вот как я это сделаю», то есть вызов инструмента, и это наблюдение, куда будет поступать вся эта информация. Таким образом, каждый из них передаётся как сообщение. Окей. И то, как мы смотрим, это то, что любое создание вызова инструмента от LLМ, когда LLМ говорит: «Используй этот инструмент, пожалуйста», будет сообщением ассистента. А ответы от нашего инструмента, наблюдения, будут возвращены как сообщения инструмента. Отлично. Итак, мы запустим это, чтобы определить наш шаблон подсказки. Мы определим наш LLМ, мы будем использовать J2 40 mini с температурой ноль, потому что нам нужно меньше креативности здесь, особенно когда мы делаем вызов тула. Нет необходимости использовать высокую температуру. Итак, нам нужно ввести наш ключ API OpenAI, который мы получим с platform.openai.com. Мы вводим это, затем мы продолжим, и мы просто добавим инструменты к нашему LLМ. Окей. Эти, и мы свяжем их здесь. Затем у нас есть toolChoice any. Мы увидим через минуту, я пройдусь по этому немного подробнее через секунду, но это по существу заставит вызов инструмента. Вы также можете поставить required, что на самом деле немного более… немного понятнее, но я использую any здесь, поэтому я буду придерживаться этого. Итак, это наши инструменты, которые мы проходим. У нас есть входы в агент, запускаемый нами, у нас есть шаблон подсказки, и затем это будет подано в наш LLМ. Давайте запустим это сейчас. Мы вызовем часть агента всего этого с этим. Окей. Итак, давайте посмотрим, что он выведет. Это важно. Я спрашиваю: «Что такое 10 + 10?» Очевидно, что он должен использовать инструмент сложения, и мы можем фактически увидеть, что это происходит. Итак, содержание сообщения агента фактически пусто здесь. Это то место, где вы обычно получаете ответ, но если мы посмотрим, у нас есть ключевое слово addition там, у нас есть вызовы инструментов, и затем у нас есть аргументы функции. Окей. Итак, мы вызываем функцию. Аргументы для этой функции — это. Окей. Итак, мы видим, что это строка снова. Способ, которым мы передадим это, это Json.loads, и это становится словарем, и затем мы можем увидеть, какая функция вызывается, и это функция add, и это всё, что нам нужно, чтобы фактически выполнить нашу функцию или наш… наш инструмент. Окей. Мы видим это немного подробнее сейчас. Что мы делаем дальше? Мы будем сопоставлять имя инструмента с функцией инструмента, и затем мы просто будем выполнять функцию инструмента с сгенерированными ARGS. Я также просто отмечу быстро, что здесь мы получаем словарь напрямую, который, я думаю, идёт откуда-то ещё в этом, который prob, который здесь. Окей. Итак, даже этот шаг здесь, где мы передаём это, нам не обязательно нужно делать это, потому что я думаю, на стороне LChain они делают это за нас, так что мы уже получаем это. Json.loads нам не обязательно нужен здесь. Окей. Итак, мы просто создаём это сопоставление словаря имени инструмента с функцией здесь. Итак, мы берём… ну, имена инструментов, и мы просто сопоставляем их обратно с нашими функциями инструментов, и это идёт из нашего списка инструментов. Этот список инструментов, который мы определили здесь. Окей. Или даже просто быстро посмотреть, что это будет включать всё или каждый из инструментов, которые вы определили там. Окей. Это всё. Теперь мы будем выполнять, используя наше сопоставление имени с инструментом. Окей. Итак, это здесь получит для нас функцию. Это получит для нас эту функцию, и затем этой функции мы передадим аргументы, которые мы сгенерировали. Окей. Давайте посмотрим, как это выглядит. Всё правильно. Итак, ответ, наблюдение — 20. Теперь мы будем подавать это обратно в наш LLМ, используя сообщение инструмента, и мы фактически добавим немного текста вокруг этого, чтобы сделать его немного красивее. Нам не обязательно нужно делать это, если честно, мы могли бы просто вернуть ответ напрямую… я не понимаю… я даже не думаю, что на самом деле будет какая-либо разница. Итак, мы… мы могли бы сделать или то, или другое. В некоторых случаях это может быть очень полезно, в других случаях, как здесь, это на самом деле не имеет большого значения, особенно потому что у нас есть этот идентификатор вызова инструмента, и то, что делает этот идентификатор вызова инструмента, это используется AI, читается LLМ, так что LLМ знает, что ответ, который мы получили здесь, фактически сопоставлен обратно с выполнением инструмента, которое он определил здесь, потому что вы видите, что у нас есть этот ID, верно? У нас есть ID здесь. LLМ увидит ID, он увидит ID, который мы передаём обратно сюда, и он увидит, что эти два связаны. Итак, окей, это инструмент, который я вызвал, и это ответ, который я получил из-за этого. Вам не обязательно нужно говорить, какой инструмент вы использовали здесь, вы можете… это зависит от того, что вы делаете. Окей. Итак, что мы получаем здесь? У нас есть… окей, просто запускаем всё снова. Мы добавили наш вызов инструмента, это исходное сообщение AI, которое включает… окей, пользовательский инструмент добавления, и затем у нас есть выполнение инструмента, сообщение инструмента, которое является наблюдением. Мы сопоставляем их с черновиком агента, и затем что мы получаем? У нас есть сообщение AI, но содержимое снова пустое, что интересно, потому что мы сказали нашему LLМ здесь: «Если у вас есть ответ в черновике, вы не должны использовать больше инструментов и сказать ответ напрямую пользователю». Итак, почему… почему наш LLМ не отвечает? Причина в том, что здесь внизу мы указываем toolChoice = any, что опять же то же самое, что toolChoice required, что говорит LLМ, что он фактически не может ответить напрямую, он должен использовать инструмент. Я обычно делаю это правильно, я обычно ставлю toolChoice = any или required, чтобы LLМ использовал инструмент каждый раз. Итак, тогда вопрос в том, если он должен использовать инструмент каждый раз, как он отвечает нашему пользователю? Мы увидим через минуту. Сначала я просто хочу показать вам два варианта, которые у нас есть. Второй — это то, что я обычно использую, но давайте… давайте начнём с первого. Итак, первый вариант заключается в том, что мы устанавливаем toolChoice равным Auto. Это говорит LLМ, что он может либо использовать инструмент, либо ответить пользователю напрямую, используя окончательный ответ или используя это поле content. Итак, если мы запустим это, как мы указываем выбор инструмента Auto, мы запустим это, давайте вызовем… окей, изначально вы видите… а, подождите, всё ещё нет контента. Это потому, что мы ничего не добавили в черновик агента здесь. Нет информации, верно? Всё пусто… на самом деле, оно пусто, потому что… извините, так что здесь у вас есть история чата, которая пуста. Мы не указали черновик агента, и причина, по которой мы можем сделать это, заключается в том, что мы используем, если вы посмотрите сюда, мы используем get. По существу, это говорит: «Попробуй получить черновик агента из этого словаря, но если он не был предоставлен, мы просто дадим пустой список». Вот почему нам не нужно указывать его здесь, но это означает, что… окей… агент на самом деле ничего не знает здесь, он ещё не использовал инструмент. Итак, мы просто пройдёмся по нашей итерации снова, верно? Итак, мы получим вывод нашего инструмента, мы будем использовать это, чтобы создать сообщение инструмента, и затем мы добавим наш вызов инструмента от AI и наблюдение, мы передадим их в черновик агента, и на этот раз мы видим, что мы запускаем это… окей, теперь мы получаем содержимое. Окей. Итак, теперь он не вызывает… вы видите здесь, нет вызова инструмента или чего-либо ещё, что происходит, мы просто получаем контент. Итак, это… это стандартный способ создания или построения агента, вызывающего инструмент. Другой вариант, который я упомянул, это то, что я обычно использую. Итак, номер два здесь, я обычно создаю инструмент окончательного ответа. Итак, почему бы нам вообще это делать? Почему бы нам создать инструмент окончательного ответа, а не просто… ну, этот метод на самом деле идеально… ну, работает. Итак, почему бы нам просто не использовать это? Есть несколько причин. Основные из них заключаются в том, что с вариантом два, где мы заставляем вызов инструмента, это устраняет возможность агента использовать это поле content напрямую, и причина, по крайней мере, причина, по которой я нашёл это хорошим при создании агентов в прошлом, заключается в том, что иногда, когда вы хотите использовать инструмент, он фактически будет использовать поле content, и это может быть довольно раздражающим, и довольно часто использовать поле content, когда вы на самом деле хотите, чтобы он использовал один из инструментов, и это особенно заметно с меньшими моделями, с большими моделями это не так распространено, хотя это и случается. Теперь вторая вещь, которая мне очень нравится в использовании инструмента как вашего окончательного ответа, заключается в том, что вы можете обеспечить структурированный вывод в вашем ответе. Итак, это то, что мы устанавливаем… я думаю, первый… да, первый пример цепочки LangChain, где мы использовали инструмент структурированного вывода LangChain, и что на самом деле… функция структурированного вывода LangChain, это на самом деле просто вызов инструмента, верно? Это заставляет вызов инструмента от вашего LLМ, это просто абстрагировано, поэтому вы не понимаете, что это то, что он делает, но это то, что он делает. Поэтому я считаю, что структурированные выводы очень полезны, особенно когда у вас много кода вокруг вашего агента, так что когда этот вывод должен перейти вниз по течению в некоторую логику, это может быть очень полезно, потому что вы можете… у вас есть надёжный формат вывода, который, как вы знаете, будет выведен, и это также невероятно полезно, если у вас есть несколько выходов или несколько полей, которые вам нужно сгенерировать. Итак, они могут быть очень полезны. Теперь, чтобы реализовать это… чтобы реализовать вариант два, нам нужно создать инструмент окончательного ответа. Как и с нашими другими инструментами, мы фактически… описание, и вы можете… или не можете сделать это. Итак, вы можете… вы можете также просто вернуть None и фактически просто использовать сгенерированное действие как… по существу, то, что вы собираетесь отправить из вашей логики выполнения агента, или вы можете фактически просто выполнить инструмент и просто передать эту информацию напрямую. Возможно, в некоторых случаях у вас может быть некоторая дополнительная постобработка для вашего окончательного ответа, может быть, вы делаете некоторые проверки, чтобы убедиться, что он не сказал ничего странного, вы можете добавить это в этот инструмент здесь, но да, в этом случае мы просто пытаемся передать их напрямую. Итак, давайте запустим это. Мы добавили… где мы… окончательный ответ… мы добавили инструмент окончательного ответа в наше именованное сопоставление инструментов, поэтому наш агент теперь может его использовать. Мы переопределяем наш агент, устанавливая toolChoice в any, потому что мы заставляем выбор инструмента здесь, и давайте пойдём с «Что такое 10 + 10?» Посмотрим, что произойдёт. Окей, мы получаем это, верно? Мы также… одна хорошая вещь здесь заключается в том, что нам не нужно проверять, находится ли он в поле content или в поле вызовов инструментов. Мы знаем, что он будет в поле вызовов инструментов, потому что мы заставляем использовать этот инструмент, довольно приятно. Окей. Мы знаем, что мы используем инструмент add, и это аргументы. Отлично. Мы идём… или идём через наш процесс снова. Мы создадим наше сообщение инструмента, и затем мы добавим эти сообщения в наш черновик или промежуточные наборы, и затем мы можем увидеть снова… а, окей, поле content пустое. Это ожидаемо. Мы заставляем пользователей инструментов… нет способа, чтобы это могло быть… это могло быть или иметь что-либо внутри него, но затем, если мы придём сюда вниз к нашим вызовам инструментов… хороший окончательный ответ, арбы ответа 10 + 10 = 20. Всё правильно. У нас также есть это инструменты, используемые… откуда берутся инструменты, используемые? Окей, я упомянул ранее, что вы можете добавить дополнительные вещи или… или выходы, когда вы используете эти инструменты, используемые для вашего окончательного ответа. Итак, если вы просто придёте сюда сюда, вы можете увидеть, что я попросил LLМ использовать это поле инструментов, используемых, которое я определил здесь. Это список строк. Используй это, чтобы сказать мне, какие инструменты ты использовал в своём ответе, верно? Итак, я получаю обычный ответ, но я также получаю эту информацию, что довольно приятно. Вот откуда это берётся. Видите это? Окей. Итак, у нас есть наш фактический ответ здесь, и затем у нас есть некоторая дополнительная информация. Окей. И мы также определили тип здесь. Это просто список строк, что действительно здорово. Это даёт нам большой контроль над тем, что мы выводим, что идеально. Это… когда вы строите с агентами, самая большая проблема в большинстве случаев — это контроль над вашим LLМ. Итак, здесь мы получаем честно говоря довольно невероятный контроль над тем, что будет делать наш LLМ, что идеально подходит для построения в реальном мире. Итак, это всё, что нам нужно. Это наш ответ, и мы, конечно же, будем передавать это вниз по течению в любое приложение журнала AI, которое мы будем использовать. Окей. Итак, может быть, это идёт напрямую на фронтенд, и мы отображаем это как наш ответ, и мы, возможно, предоставляем некоторую информацию о… окей, откуда взялся этот ответ, или, может быть, есть некоторые дополнительные шаги вниз по течению, где мы фактически делаем некоторую дополнительную обработку или преобразования, но да, у нас есть это… это здорово. Теперь всё, что мы только что сделали здесь, мы выполняли всё по одному, и это для того, чтобы помочь нам понять, какой процесс мы проходим, когда мы строим исполнителя агента, но мы не будем хотеть делать это всё время, не так ли? В большинстве случаев мы, вероятно, захотим абстрагировать всё это, и это то, что мы собираемся сделать сейчас. Мы собираемся построить… по существу, всё, что мы только что взяли, мы собираемся абстрагировать это и абстрагировать в пользовательский класс исполнителя агента. Давайте быстро посмотрим, что мы делаем здесь, хотя это… это буквально то же самое, что мы только что сделали. Окей. Пользовательский исполнитель maor. Мы инициализируем его. Мы устанавливаем этот mMaxIterations. Я расскажу об этом через минуту. Мы инициализируем его. Это установит нашу историю чата просто как пустую. Окей. Это новый агент, в этом случае не должно быть истории чата. Затем мы фактически определяем нашего агента. Эта логика, которая будет принимать наши входы и генерировать, что делать дальше, то есть какой вызов инструмента делать. Окей. И мы устанавливаем всё как атрибуты нашего класса, и затем мы определим метод invoke. Этот метод invoke будет принимать вход, который просто строка, так что это будет наше сообщение от пользователя, и что он будет делать, так это будет итерироваться через всё, что мы только что сделали, окей, пока мы не дойдём до инструмента окончательного ответа. Окей. Итак, что это значит? У нас есть наш вызов инструмента, верно? Это то, что мы просто вызываем нашего агента, верно? Итак, он сгенерирует, какой инструмент использовать и какие параметры должны быть в него введены. Окей. И это… это сообщение AI. Итак, мы добавим это к нашему черновику агента, и затем мы будем использовать информацию из нашего вызова инструмента, то есть имя инструмента и ARGS, а также ID, мы будем использовать всю эту информацию, чтобы выполнить наш инструмент и затем предоставить наблюдение обратно нашему LLМ. Окей. Мы выполняем наш инструмент здесь. Затем мы форматируем вывод инструмента в сообщение инструмента. Видите здесь, что я просто использую вывод напрямую. Я не добавляю эту дополнительную информацию. Нам нужно… нам нужно всегда передавать идентификатор вызова инструмента, чтобы наш LLМ знал, какой вывод сопоставлен с каким инструментом. Я не упоминал это раньше в этом видео, по крайней мере, но это… это важно, когда у нас есть несколько вызовов инструментов, происходящих параллельно, потому что это может произойти, когда у нас есть несколько вызовов инструментов, происходящих параллельно. Допустим, у нас есть 10 вызовов инструментов, все эти ответы могут вернуться в разное время, поэтому порядок их может быть нарушен, поэтому мы не обязательно всегда будем видеть, что это сообщение AI, начинающее вызов инструмента, за которым следует ответ на этот вызов инструмента, вместо этого это может быть сообщение AI, за которым следует около 10 различных ответов вызова инструмента, поэтому вам нужно иметь эти идентификаторы там. Окей. Затем мы передаём вывод нашего инструмента обратно в наш черновик агента или промежуточные шаги. Я использую print здесь, чтобы мы могли видеть, что происходит, пока всё работает. Затем мы увеличиваем это число count. Мы поговорим об этом через минуту. Итак, com past этого мы говорим: «Окей, если имя инструмента здесь — окончательный ответ, это означает, что мы должны остановиться». Окей. Итак, как только мы получим окончательный ответ, это означает, что мы можем фактически извлечь наш окончательный ответ из окончательного вызова инструмента. Окей. И в этом случае я скажу, что мы собираемся извлечь ответ из вызова инструмента или… наблюдения, мы собираемся извлечь сгенерированный ответ, мы собираемся передать его в нашу историю чата, поэтому мы будем иметь наше сообщение пользователя, которое придумал пользователь, за которым следует наш ответ, который просто… поле natural answer, и это будет сообщение AI, но затем мы фактически будем включать всю информацию. Это… это ответ, естественно-языковой ответ, а также выход инструментов, используемых, мы будем подавать всё это в какой-то процесс вниз по течению, как предпочтительнее. У нас есть это сейчас. Одна вещь, которая может произойти, если мы не будем осторожны, это то, что наш исполнитель агента может… может работать много-много раз, и особенно если мы сделали что-то неправильно в нашей логике, когда мы строим эти вещи, может случиться так, что, может быть, мы не подключили наблюдение обратно в нашу логику исполнителя агента, и в этом случае то, что мы можем увидеть, это наш исполнитель агента работает снова и снова и снова, и я имею в виду, что это нормально, мы остановим его, но если мы не поймём сразу, и мы делаем много вызовов LLМ, это может стать довольно дорогим довольно быстро. Итак, что мы можем сделать, так это установить лимит, верно? Вот что мы сделали здесь сверху с этим MaxIterations. Мы сказали: «Окей, если мы пройдём мимо трёх MaxIterations по умолчанию, я скажу: «Стоп», верно? Итак, вот почему у нас есть count здесь. Пока count меньше MaxIterations, мы будем продолжать. Как только мы достигнем количества MaxIterations, мы остановимся. Окей. Итак, цикл while просто перестанет циклиться. Окей. Итак, это просто защищает нас на случай этого, и это также потенциально… может быть, ваш агент делает слишком много, чтобы ответить на вопрос, поэтому это заставит его остановиться и просто дать ответ, хотя если это произойдёт, я просто понимаю, что здесь есть небольшая ошибка в логике, если это произойдёт, мы не обязательно будем иметь ответ здесь, верно? Итак, мы, вероятно, захотим обработать это хорошо, но в этом сценарии очень простого варианта использования мы не увидим, чтобы это происходило. Итак, мы инициализируем нашего пользовательского исполнителя агента, и затем мы вызываем его. Окей. И давайте посмотрим, что произойдёт. Всё правильно. Итак, это просто завернуло всё в один единственный вызов. Всё обрабатывается для нас… мы могли бы сказать: «Окей, что такое 10…», мы можем изменить это и сказать 7.4, например, и что… мы пройдём, мы будем использовать инструмент умножения, а затем вернёмся к окончательному ответу снова. Окей. Итак, мы можем видеть, что с этим пользовательским исполнителем агента мы построили агента, и у нас есть гораздо больше контроля над всем, что происходит здесь. Одна вещь, которую нам, вероятно, нужно добавить в этом сценарии, это… прямо сейчас я предполагаю, что только один вызов инструмента будет происходить одновременно. Это также почему я спрашиваю здесь… я не задаю сложный вопрос, потому что я не хочу, чтобы он пошёл и попытался выполнить несколько вызовов инструментов одновременно… что… что может произойти. Итак, давайте просто попробуем это. Окей. Итак, это на самом деле совершенно нормально. Итак, это просто выполнилось одно за другим. Итак, вы можете видеть, что при задании этого более сложного вопроса он сначала использовал инструмент возведения в степень, за которым следовал инструмент сложения, и затем они фактически дали нам наш окончательный ответ, что здорово. Он также сказал нам, что он использовал оба этих инструмента, что он и сделал, но одна вещь, о которой мы должны помнить, это то, что от OpenAI OpenAI может фактически выполнять несколько вызовов инструментов параллельно. Указав, что мы просто используем этот ноль здесь, мы фактически предполагаем, что мы всегда будем вызывать только один инструмент в любое время, что не всегда будет так. Поэтому вам, вероятно, потребуется добавить немного дополнительной логики там на случай сценариев, если вы создаёте агента, который, вероятно, будет запускать параллельные вызовы, но да, вы можете видеть здесь, на самом деле, это совершенно нормально. Итак, он работает один за другим. Окей. Итак, с этим мы построили нашего исполнителя агента. Я знаю, что в этом много всего, и, конечно же, вы можете просто использовать очень абстрактного исполнителя агента в LangChain, но я думаю, что очень хорошо понимать, что на самом деле происходит, чтобы построить своего собственного исполнителя агента в этом случае, и это хорошо подготовит вас к созданию более сложной или специфичной для варианта использования логики агента, так что это всё для этой главы. В этой главе мы рассмотрим язык выражений LangChain. Мы рассмотрим запускаемые, сериализуемые и параллельные из них, проходящие запускаемые и по существу то, как мы используем LangChain в полной мере. Теперь, чтобы сделать это… что я хочу сделать, это на самом деле начать с рассмотрения традиционного подхода к построению цепочек в LangChain. Итак, чтобы сделать это, мы перейдём к главе LLМ и откроем этот Курс. Окей. Итак, давайте подойдём… мы сделаем предварительные условия, как и прежде, ничего не измеряется здесь. Одна вещь, которая является новой, это DocArray, потому что позже, как вы увидите, мы будем использовать это в качестве примера параллельных возможностей в LangChain. Если вы хотите использовать LlamaIndex, вам просто нужно добавить ваш ключ API LlamaIndex. Окей. И затем… окей. Итак, теперь давайте погрузимся в традиционный подход к цепочкам в LangChain. Я думаю, что LLМ Chain, вероятно, одна из первых вещей, представленных в LangChain, если я не ошибаюсь. Это берёт подсказку и подаёт её в LLМ, и это всё. Вы также можете… вы можете добавить передачу вывода к этому, но это необязательно, и я не думаю, что мы будем рассматривать это здесь. Итак, как это может выглядеть, это у нас есть, например, этот шаблон подсказки здесь. «Дай мне небольшой отчёт по теме». Окей. Итак, это будет наш шаблон подсказки. Мы настроили его, как обычно, с шаблонами подсказок, как мы видели раньше. Затем мы определяем наш LLМ. Нам нужен ключ OpenAI для этого, который, как обычно, мы получим с platform.openai.com. Затем мы идём… я просто… просто показываю вам, что вы можете… запустить LLМ там. Затем мы идём… фактически определяем вывод POS, мы делаем… делаем это… я не был уверен, что мы делаем, но мы бы тогда определили наш LLМ Chain вот так. Окей. LLМ Chain, мы добавляем нашу подсказку, добавляем наш LLМ, добавляем наш alias. Это традиционный подход. Я бы тогда сказал: «Окей, извлечь Org генерацию», и что он будет делать, это даст мне небольшой отчёт обратно по rag. Такой момент, но вы можете видеть, что это то, что мы получаем здесь. Мы можем отформатировать его красиво, как обычно, и мы получаем… посмотрите, мы получаем хороший небольшой отчёт. Однако LLМ Chain является… он довольно ограничительным, верно? Мы должны иметь определённые параметры, которые были предварительно определены как пригодные для использования, что, вы знаете, ограничительно, и он также устарел, поэтому, вы знаете, это больше не стандартный способ сделать это, но мы всё ещё можем использовать его. Однако предпочтительный метод построения этого и построения чего-либо ещё, действительно, или цепочек в целом в LangChain — это использование LangChain. И это очень просто, верно? Итак, мы просто фактически берём подсказку LLМ, Apple P, которая была у нас раньше, и затем мы просто связываем их вместе с помощью этих операторов pipe. Итак, оператор pipe здесь говорит: «Возьми то, что выводится отсюда, и введи это сюда. Возьми то, что выводится отсюда, и введи это сюда». Это всё, что он делает. Очень просто. Соедини их, и мы вызовем его так же, и мы получим тот же вывод. Окей. И это то, что мы получаем там… на самом деле есть небольшое различие в том, что мы получаем оттуда. Вы можете видеть здесь, мы получили фактически словарь, но это…
Это практически то же самое. Хорошо, мы это понимаем, и как и прежде, мы можем отобразить это в Markdown вот так. Хорошо, мы только что увидели, что у нас есть этот оператор pipe. Это не совсем стандартный синтаксис Python, по крайней мере, он определённо не распространён. Это, это, это отклонение от предполагаемого использования Python, я думаю, но в любом случае, он работает, выглядит круто, и когда вы понимаете, как он работает, я понимаю, почему они так делают, потому что это делает вещи довольно простыми по сравнению с тем, какими они могли бы быть в противном случае. Так что я понимаю, это немного странно, но они так делают, и я это преподаю, так что именно этому мы и будем учиться.
Итак, что же на самом деле делает этот оператор pipe? Ну, как я уже упоминал, он берёт вывод отсюда и помещает его во вход во всё, что находится справа. Но как это на самом деле работает? Давайте реализуем это сами, без LangChain. Мы создадим этот класс, который называется Runnable. Когда мы его инициализируем, он будет принимать функцию. Хорошо, это буквально функция Python. Он возьмёт её и по сути превратит её в то, что мы назвали бы Runnable в LangChain. И что это на самом деле значит? Ну, это на самом деле ничего не значит, это просто означает, что когда вы используете метод invoke, он вызовет эту функцию так, как вы обычно это делали бы. Всё правильно, используя просто функцию, знаете, скобки открываются, параметры, скобки закрываются, он сделает это, но он также добавит этот метод, метод all.
Теперь этот метод all, в типичном синтаксисе Python, этот метод all по существу возьмёт вашу функцию Runnable, ту, которую вы инициализировали, и он также возьмёт другую функцию. Хорошо, эта другая функция на самом деле будет Runnable, я думаю, да, она будет Runnable, как и эта, и она будет запускать этот Runnable на основе вывода вашего текущего Runnable. Вот что будет делать этот or. Кажется немного странным, может быть, но я объясню через минуту. Мы увидим, почему это работает. Итак, я собираюсь связать несколько функций вместе, используя этот метод or. Сначала мы просто превратим их все в Runnable. Хорошо, это обычные функции, как вы можете видеть, обычные функции Python. Затем мы превращаем их в этот Runnable, используя наш класс Runnable. Затем посмотрите, что мы можем сделать. Хорошо, мы создадим цепочку, которая будет нашим Runnable, связанным с другим Runnable, связанным с другим Runnable. Давайте посмотрим, что произойдёт. Итак, мы вызовем эту цепочку Runnable с тремя. Итак, что это будет делать? Хорошо, мы начинаем с пяти, мы добавим пять к трём, получим восемь, затем вычтем пять из восьми, чтобы снова получить три, а затем умножим три на пять, чтобы получить пятнадцать. И мы можем это вызвать, и мы получим пятнадцать. Хорошо, круто.
Это интересно. Как это связано с оператором pipe? Ну, этот оператор pipe в Python на самом деле является сокращением для метода or. Итак, то, что мы только что реализовали, это оператор pipe. Итак, мы можем запустить это сейчас с оператором pipe здесь, и мы получим то же самое, получим 15. Хорошо, это то, что делает LangChain под капотом. Это то, что такое этот оператор pipe. Он просто связывает вместе эти несколько Runnable, как мы их называем, используя свой собственный внутренний оператор or. Хорошо, что круто. Я, я, я дам им это, это довольно крутой способ сделать это, креативно. Я бы сам об этом не подумал. Итак, да, это оператор pipe, а затем у нас есть эти вещи Runnable. Хорошо, это, это отличается от Runnable, который я только что определил здесь. Это, мы определяем это сами, это не вещь LangChain. Мы не получили это от LangChain. Вместо этого этот объект Runnable Lambda здесь на самом деле точно такой же, как тот, который мы только что определили. Всё правильно, то, что мы сделали здесь, этот Runnable, этот Runnable Lambda – это то же самое, но в LangChain. Хорошо, если мы используем это, хорошо, мы используем это, чтобы теперь определить три Runnable из функций, которые мы определили ранее, мы можем фактически соединить их вместе сейчас, используя оператор pipe. Вы также можете соединить их вместе, если хотите, с оператором or. Хорошо, мы могли бы сделать то, что мы делали раньше, мы можем вызвать это, хорошо, или, как мы делали изначально, мы используем оператор pipe, точно так же. Итак, этот Runnable Lambda из LangChain – это просто то, что мы, что мы только что построили с помощью Runnable. Круто, теперь у нас это есть. Давайте попробуем сделать что-нибудь немного интереснее. Мы собираемся сгенерировать отчёт и попробуем отредактировать этот отчёт, используя эту функциональность. Хорошо, дайте мне небольшой отчёт о теме. Хорошо, мы пройдёмся здесь, мы получим наш отчёт об ИИ.
Итак, у нас есть это, вы можете видеть, что ИИ упоминается много раз здесь. Затем мы возьмём очень простую функцию. Хорошо, я извлекаю факт, это по сути возьмёт, э, что это, возьмёт первый. Хорошо, мы фактически пытаемся удалить введение здесь. Я не уверен, будет ли это на самом деле работать так, как ожидалось, но это нормально, попробуем в любом случае. Но что более важно, мы заменим это слово. Хорошо, мы заменим старое слово на новое слово. Наше старое слово будет ИИ, а слово будет Skynet. Итак, мы можем обернуть обе эти функции как Runnable Lambda. Мы можем добавить их в качестве дополнительных шагов внутри всей нашей цепочки. Хорошо, мы собираемся извлечь, попытаться удалить введение, хотя я думаю, что для этого требуется немного больше обработки, чем просто разделение здесь, а затем мы заменим слово. Нам нужно, чтобы это на самом деле было ИИ. Запустите это, запустите это. Хорошо, теперь мы получаем искусственный интеллект. Skynet относится к моделированию человеческого интеллектуального процесса машинами. У нас есть узкий Skynet, слабый Skynet и сильный Skynet. Приложения Skynet применяются во многих областях, включая все эти вещи. Страшно, несмотря на потенциальную угрозу Skynet, ставит перед собой несколько задач. Системы могут увековечивать существующие предвзятые представления. Это создаёт серьёзные проблемы конфиденциальности. Это может быть использовано в злонамеренных целях. Хорошо, у нас есть все эти, знаете, это просто глупый маленький пример. Мы также видим, что введение не сработало здесь. Причина этого в том, что наше введение содержит несколько новых строк здесь. Итак, я бы на самом деле, если бы я хотел удалить введение, я должен удалить его отсюда, я думаю, и это, я бы никогда на самом деле не рекомендовал вам делать это, потому что это не, это не очень гибко, это не очень надёжно, но просто чтобы показать вам, что это на самом деле работает. Итак, этот Runnable extract_fact. Хорошо, теперь мы по существу просто удаляем введение. Зачем, что мы хотим сделать? Я не знаю, но оно есть просто для того, чтобы вы могли видеть, что у нас может быть несколько этих операций Runnable, и они могут быть любыми, какими вы хотите.
Стоит знать, что входные данные для наших функций здесь были всеми одинарными аргументами. Если у вас есть функция, которая принимает несколько аргументов, вы можете сделать это так, как я, вероятно, сделал бы это, или вы можете сделать это несколькими способами. Один из способов, которым вы можете это сделать, – это фактически написать вашу функцию, чтобы она принимала аргументы, но фактически обрабатывала их через один аргумент. Так же как один, например, X, который был бы похож на словарь или что-то в этом роде, а затем просто распакуйте их внутри функции и используйте их по мере необходимости. Это просто, да, это один из способов сделать это. Теперь у нас также есть эти разные объекты Runnable, которые мы можем использовать. Итак, здесь у нас есть Runnable Parallel и Runnable PassThrough. До некоторой степени это самоочевидно. Поэтому позвольте мне просто пройтись по ним. Runnable Parallel позволяет вам запускать несколько экземпляров Runnable параллельно. Runnable PassThrough, может быть, менее очевиден, позволяет нам передавать переменную следующему Runnable без её изменения. Хорошо, давайте посмотрим, как они будут работать. Мы собираемся спуститься сюда, и мы собираемся установить эти два массива документов. Очевидно, эти два источника информации, и нам понадобится наша большая языковая модель, чтобы извлекать информацию из обоих этих источников информации параллельно, что будет выглядеть так. Итак, у нас есть эти два источника информации, Vector Store A, Vector Store B. Это наш массив документов A и массив документов B. Оба они будут подаваться в качестве контекста в наш запрос. Затем наша большая языковая модель будет использовать всё это, чтобы ответить на вопрос. Хорошо, чтобы фактически реализовать это, нам нужно модель встраивания. Итак, откройте наши встраивания. У нас есть наш вектор A, вектор B. Они не, знаете, реальные векторы, они не полноценные векторы, здесь мы просто передаём очень небольшое количество информации в оба. Итак, мы говорим, хорошо, мы собираемся создать в памяти вектор, используя эти два фрагмента информации. Итак, если предположить, что половина информации находится здесь, это будет нерелевантный фрагмент информации, а затем у нас есть релевантная информация, которая заключается в том, что DeepSeek v3 был выпущен в декабре 2024 года. Затем у нас будет другая информация в нашем другом векторе Store, снова нерелевантный фрагмент здесь и релевантный фрагмент здесь. Хорошо, большая языковая модель DeepSeek v3 – это модель смеси экспертов с 671 миллиардом параметров в своём самом большом размере. Хорошо, основываясь на этом, мы также построим эту строку запроса. Мы передадим оба этих контекста в наш запрос, а затем я задам вопрос. Нам на самом деле не нужно, нам не нужно это, и на самом деле нам даже не нужно это. Что я делаю? Итак, нам нужно только это. Итак, у нас есть оба контекста, и мы запустим их через наш шаблон запроса. Хорошо, у нас есть наш системный шаблон запроса, который вот этот, а затем мы просто будем иметь, хорошо, наш вопрос будет помещён сюда как сообщение пользователя. Круто, у нас это есть, и затем, чтобы сделать это проще для чтения, мы преобразуем оба этих вектора в извлекатели, что просто означает, что мы можем извлекать из них информацию, и мы будем использовать этот Runnable Parallel для запуска обоих из них параллельно. Хорошо, оба они запускаются параллельно, но мы также запускаем наш запрос параллельно, потому что это должно быть по существу передано через этот компонент без нашего изменения чего-либо. Итак, когда мы смотрим на это здесь, это почти как, хорошо, эта часть здесь будет нашим Runnable Parallel, и они выполняются параллельно, но также наш запрос передаётся, так что это почти как есть ещё одна строка здесь, которая является нашим Runnable PassThrough. Хорошо, вот что мы делаем здесь. Запускается параллельно, один из них – это PassThrough. Мне нужно запустить здесь, я только что понял, что мы используем устаревшие встраивания, просто переключитесь на это. Итак, LangChain OpenAI, запустите это, запустите это, запустите это, и теперь это настроено. Итак, затем мы помещаем наш начальный, итак, используя наш Runnable Parallel и Runnable PassThrough, это наш начальный шаг. Затем у нас есть наша большая языковая модель запроса, теперь Pass, который будет связан с помощью обычного, знаете, обычного оператора pipe. Хорошо, и теперь мы собираемся вызвать вопрос. Какая архитектура используется моделью DeepSeek, выпущенной в декабре? Хорошо, чтобы большая языковая модель ответила на этот вопрос, ей нужно будет сказать нам, что ей нужна информация о модели DeepSeek, которая была выпущена в декабре, которую мы указали в одной половине здесь, а затем ей также нужно будет знать, какая архитектура используется этой моделью, которая определена в другой половине здесь. Хорошо, давайте запустим это. Хорошо, вот и всё. Модель DeepSeek v3, выпущенная в декабре 2024 года, представляет собой модель смеси экспертов с 671 миллиардом параметров. Хорошо, смесь экспертов и столько параметров. Довольно круто. Итак, мы собрали наш конвейер, используя LangChain, используя оператор pipe, Runnable, в частности, мы рассмотрели Runnable Parallel, Runnable PassThrough, а также Runnable Lambda. Вот и всё для этой главы о LangChain, и мы перейдём к следующей.
В этой главе мы рассмотрим потоковую передачу и асинхронность в LangChain. Использование асинхронного кода и потоковой передачи являются невероятно важными компонентами, я думаю, почти любого разговорного интерфейса чата или, по крайней мере, любого хорошего разговорного интерфейса чата. Для асинхронности, если ваше приложение не асинхронное, и вы тратите много времени в своём API или чём-то ещё, ожидая вызовов большой языковой модели, потому что многие из них находятся за API, вы ждёте, и ваше приложение ничего не делает, потому что вы написали синхронный код, и это, ну, есть много проблем с этим, главным образом, он не масштабируется. Итак, асинхронный код, как правило, работает гораздо лучше, и особенно для ИИ, где большую часть времени мы как бы ждём вызовов API. Итак, асинхронность невероятно важна для этого. Для потоковой передачи. Теперь потоковая передача – это немного другая вещь. Допустим, я хочу, чтобы мне рассказали историю. Хорошо, я использую GPT-4 здесь, он немного медленнее, поэтому мы можем добиться потоковой передачи, мы можем видеть, что токен за токеном этот текст создаётся и отправляется нам. Теперь это не просто визуальная вещь, это большая языковая модель, когда она генерирует токены или слова, она генерирует их по одному. И это потому, что эти большие языковые модели буквально генерируют токены по одному. Итак, они смотрят на все предыдущие токены, чтобы сгенерировать следующий, а затем генерируют следующий, генерируют следующий, вот как они работают. Итак, когда мы реализуем потоковую передачу, мы получаем эту ленту токенов непосредственно от большой языковой модели в наш, знаете, наш бэкэнд или наш фронтенд. Это то, что мы видим, когда, когда мы видим этот интерфейс токен за токеном. Хорошо, это одна вещь. Что ещё я могу сделать, позвольте мне переключиться на GPT-4, я могу сказать, хорошо, мы только что получили эту историю. Я собираюсь спросить, есть ли какие-либо стандартные приёмы повествования, которые следует использовать выше. Пожалуйста, используйте поиск. Хорошо, посмотрите, мы очень быстро увидели, что он искал в Интернете, и способ, это не потому, что мы сказали ему, хорошо, мы сказали большой языковой модели использовать инструмент поиска, но затем большая языковая модель вывела некоторые токены, чтобы сказать, использовать инструмент поиска. Это собирается использовать инструмент поиска, и он также выведет токен, указывающий на то, каким был бы этот поисковый запрос, хотя мы его там не видели, но что делает интерфейс ChatGPT там. Итак, он получил эти токены, говорящие: «Эй, я собираюсь использовать инструмент поиска». Он просто не отправил нам эти токены, как он делает с токенами здесь. Вместо этого он использовал эти токены, чтобы показать нам это поле поиска в Интернете. Итак, потоковая передача – это не только потоковая передача этих прямых токенов, это также потоковая передача этих промежуточных шагов, которые большая языковая модель может прорабатывать, что особенно важно, когда речь идёт об агентах и агентивных интерфейсах. Итак, это также функциональная вещь. Потоковая передача не просто выглядит красиво, это также функция.
И наконец, конечно же, когда мы смотрим на это, хорошо, допустим, мы вернёмся к GPT-4, и я скажу, хорошо, используйте всю эту информацию, чтобы сгенерировать для меня длинную историю. Хорошо, мы получаем первый токен сейчас. Мы знаем, что что-то происходит, и нам нужно начать читать. Представьте, если бы мы ничего не транслировали здесь, и мы просто ждали. Мы всё ещё ждём. Теперь мы всё ещё ждём, и мы ничего не увидим. Мы просто такие: «О, это просто пусто», или может быть, есть небольшой индикатор загрузки. Итак, мы всё ещё будем ждать, и даже сейчас мы всё ещё ждём. Это крайний пример, но можете ли вы себе представить, что просто ждёте так долго и ничего не видите как пользователь? Сейчас, только что мы получили бы наш ответ, если бы мы не использовали потоковую передачу. Я имею в виду, что это было бы болезненно для пользователя. Вы не захотите ждать, особенно в интерфейсе чата. Вы не хотите ждать так долго. Это нормально, например, глубокое исследование занимает много времени для обработки, но, знаете, это займёт много времени для обработки, и это другой случай использования. Вы получаете отчёт. Это интерфейс чата, и да, большинство сообщений не будут занимать так много времени для генерации. Мы, вероятно, также не будем использовать GPT-4, в зависимости от, я не знаю, может быть, некоторые люди всё ещё делают, но в некоторых сценариях болезненно нужно ждать так долго. Хорошо, и это также относится к агентам. Приятно, когда вы используете агентов, обновление, хорошо, мы используем этот инструмент, он использует этот инструмент. Вот как он их использует. Perplexity, например, имеет очень хороший пример этого. Итак, хорошо, что это такое? Основатель OpenAI присоединяется к Morati Sub. Давайте посмотрим. Хорошо, мы видим, это действительно хорошо. Мы используем поиск в Google, он ищет новости, делится результатами, как мы получаем всю эту информацию, пока ждём, что действительно круто, и это помогает понять, что на самом деле происходит. Это не нужно во всех случаях использования, но очень приятно иметь эти промежуточные шаги. Итак, тогда мы не ждём, и я думаю, что этот фрагмент, вероятно, также транслировался, но он был просто очень быстрым, поэтому я не видел его, но это довольно круто. Итак, потоковая передача довольно важна. Давайте углубимся в наш пример. Хорошо, мы откроем это в достаточно большом окне. Итак, начиная с предварительных условий, как всегда, LangChain, необязательно, LLM, мы также введём свой API-ключ LangChain, если вы хотите использовать LLM, мы также введём свой API-ключ OpenAI. Итак, это platform.openai.com, и затем, как обычно, мы можем просто вызвать нашу большую языковая модель. Хорошо, у нас это есть, это работает. Теперь давайте посмотрим, как мы будем использовать потоковую передачу с помощью stream. Хорошо, когда-либо метод, итак, stream на самом деле тоже метод, мы могли бы использовать его, но он не асинхронный. Итак, когда бы мы ни видели метод в LangChain, который имеет префикс a, который будет ещё одним методом, который похож на асинхронную версию этого, поэтому мы можем фактически использовать потоковую передачу асинхронно очень легко, используя просто LLM astream. Теперь это просто пример, честно говоря, вы, вероятно, не сможете использовать это в реальном приложении, но это всего лишь пример, и мы увидим, как мы будем использовать это или как мы будем использовать потоковую передачу асинхронно в приложении дальше в этом ноутбуке. Начиная с этого, вы можете видеть здесь, что мы получаем эти токены. Мы просто добавляем его к токену здесь. Нам на самом деле не нужно это делать, я думаю, мы используем это, но может быть, да, мы делаем это здесь, это нормально. Итак, мы просто добавляем токены по мере их возврата из нашей большой языковой модели, добавляем их сюда. Мы увидим, что это такое через минуту, а затем я просто печатаю содержимое токена. Хорошо, содержимое токена. Итак, в этом случае это будет l, в этом случае это будет LP, это будет Sans и так далее. Итак, вы можете видеть, что в основном это, это имеет тенденцию быть на уровне слов, но это также может быть на уровне подслов, как вы видите, sentiment – это одно слово, конечно, поэтому, знаете, они разбиваются различными способами. Затем добавление этого символа pipe в конце здесь. Итак, мы можем видеть, хорошо, где наши отдельные токены. Затем у нас есть flush. Flush, вы можете фактически отключить это, и потоковая передача всё ещё будет работать, вы всё ещё будете видеть всё, что будет немного больше, вы можете видеть, это как бы понемногу, когда мы используем flush, это заставляет консоль немедленно обновлять то, что нам показывается. Хорошо, мы получаем гораздо более плавную, когда мы смотрим на это, это гораздо более плавно, чем когда flush не установлен в true. Итак, да, когда вы печатаете, это хорошо делать просто для того, чтобы вы могли видеть, вам не обязательно нужно. Хорошо, теперь мы добавили все эти токены в список токенов, поэтому мы можем взглянуть на каждый отдельный объект, который был возвращён нам. Хорошо, и это интересно. Итак, мы видим, что у нас есть фрагмент сообщения ИИ. Хорошо, это объект, а затем у вас есть содержимое. Первый на самом деле пустой, второй имеет этот n для NLP, и да, я имею в виду, что это всё, что нам редко нужно знать. Это очень простые объекты, но они на самом деле довольно полезны, потому что, посмотрите на это, хорошо. Итак, мы можем добавить каждый из наших фрагментов сообщений ИИ. Давайте посмотрим, что это делает. Он не создаёт список, он создаёт это. Итак, у нас всё ещё есть только один фрагмент сообщения ИИ, но он объединил содержимое внутри этих фрагментов сообщений ИИ, что довольно круто. Хорошо, например, мы могли бы удалить эти, а затем мы просто видим NLP. Это довольно приятная маленькая функция. Я действительно, на самом деле, мне это довольно нравится, но вам нужно быть немного осторожным, потому что, очевидно, вы можете сделать это неправильно, и вы получите, я не знаю, что это за какой-то странный токенный салат. Итак, да, вам нужно просто убедиться, что вы собираетесь объединять их в правильном порядке, если только вы, я не знаю, если только вы не делаете что-то странное. Хорошо, круто. Итак, потоковая передача, это была потоковая передача от большой языковой модели. Давайте посмотрим на потоковую передачу с агентами. Итак, это становится немного сложнее, честно говоря, но нам также нужно, вещи станут немного сложнее, чтобы мы могли реализовать это, например, в API. Итак, это как бы необходимо в любом случае. Итак, очень быстро мы построим наш агентский исполнитель, как мы делали в главе о выполнении агента, и для этого, для агентского исполнителя нам понадобятся инструменты, шаблон запроса чата, большая языковая модель, агент и сам агент H. Очень быстро я не буду подробно разбирать их. Мы просто определяем наши инструменты: add, multiply, exponentiate, subtract и инструмент Final Answer. Объединяем их в один список инструментов, затем у нас есть наш шаблон запроса, снова как раньше, у нас есть системное сообщение, у нас есть история чата, у нас есть ваш запрос, а затем у нас есть блокнот агента для этих промежуточных наборов. Затем мы определяем нашего агента, используя LangChain. LangChain работает довольно хорошо как с потоковой передачей, так и с асинхронностью. Кстати, он поддерживает оба варианта из коробки, что приятно. Итак, мы определяем нашего агента, затем, спускаясь сюда, мы собираемся создать агентский исполнитель. Это то же самое, что и раньше. Хорошо, здесь ничего нового, я не думаю, поэтому просто инициализируем нашего агента, вещи там, затем это, знаете, мы перебираем, перебираем, ничего нового там. Итак, мы просто выполняем, вызываем нашего агента, проверяем, есть ли вызов инструмента. Это немного, мы могли бы переместить это до или после, это на самом деле не имеет большого значения. Итак, мы проверяем, является ли это final answer, если нет, мы продолжаем, переходим к нашим инструментам и так далее. Хорошо, круто. Затем мы можем вызвать это. Хорошо, мы идём. Что такое 10 + 10? Вот и всё. Хорошо, у нас есть наш агентский исполнитель, он работает. Теперь, когда мы запускаем нашего агентского исполнителя с каждым новым запросом, если мы помещаем это в API, нам, вероятно, придётся предоставить ему новый обработчик обратного вызова. Хорошо, это обработчик coroutine, который будет обрабатывать взятие токенов, которые генерируются агентом LLM, и передачу их в какой-то другой фрагмент кода, например, потоковый ответ для API, и наш обработчик coroutine будет помещать эти токены в очередь в нашем случае, а затем наш, например, потоковый объект будет брать их из очереди и помещать их туда, куда им нужно. Чтобы позволить нам сделать это с каждым новым запросом, нужно инициализировать всё, когда мы на самом деле инициализируем нашего агента, мы можем добавить настраиваемое поле в нашу большую языковая модель. Хорошо, мы устанавливаем настраиваемые поля здесь. О, ещё одна вещь, это то, как мы устанавливаем streaming = true, это очень важно, но просто чтобы вы видели, что мы это делаем. Итак, мы добавляем некоторые настраиваемые поля в нашу большую языковая модель, что означает, что мы можем по существу передавать объект для них при каждом новом вызове. Мы устанавливаем наше настраиваемое поле, оно будет называться callbacks, и мы просто добавляем описание. Здесь ничего больше нет. Итак, это теперь позволит нам предоставить это поле, когда мы вызываем нашего агента. Хорошо, теперь нам нужно определить наш обработчик обратного вызова, и, как я уже упоминал, что по существу будет происходить, это обработчик обратного вызова будет передавать токены в наш асинхронный объект очереди IO, и затем мы будем брать их.
Из очереди, в другом месте, окей, так что мы можем назвать это обработчиком обратного вызова Q, окей? И он наследуется от асинхронного обработчика обратного вызова, потому что мы хотим, чтобы всё это делалось асинхронно, потому что мы здесь думаем о том, как реализовать всё это внутри API и реального кода, и мы хотим делать всё это асинхронно. Так что позвольте мне выполнить это, и я немного объясню, что мы рассматриваем.
Итак, у нас есть инициализация. Это ничего особенного, здесь мы просто… то, что мы действительно хотим сделать, это установить наш объект Q, присвоив его атрибутам класса. А затем есть также эта сцена Final Answer, которую мы устанавливаем в False. Для чего мы это будем использовать? Наша большая языковая модель будет передавать токены потоком, пока она использует свои инструменты, вызывая их, и мы, возможно, не захотим отображать их немедленно или захотим отображать их по-другому. Поэтому, устанавливая этот Final Answer в False, пока наша большая языковая модель выводит эти токены инструментов, мы можем обрабатывать их по-другому. А как только мы увидим, что она закончила с вызовами инструментов и перешла к окончательному ответу, который на самом деле является ещё одним вызовом инструмента, но как только мы увидим, что она перешла к вызову инструмента окончательного ответа, мы можем установить это в True, и тогда мы можем начать обрабатывать наши токены, знаете, по-другому, по существу.
Итак, у нас есть это. Затем у нас есть этот метод AER. Это необходимо для любого асинхронного объекта-генератора. Так что он будет делать? Он будет итерироваться, это генератор, он будет итерироваться и говорить: окей, если наша очередь пуста, это очередь, которую мы настроили здесь, если она пуста, подождите минуту, мы используем здесь метод Sleep, и это асинхронный метод Sleep, это очень важно, мы используем, мы ждём асинхронного сна. Хорошо? Пока мы ждём эти 0,1 секунды, наш код может делать другие вещи, это важно. Если мы используем, я думаю, стандартный time.sleep, это не асинхронно, и поэтому он фактически заблокирует поток на эти 0,1 секунды. Так что мы не хотим, чтобы это происходило. Обычно наша очередь, вероятно, не должна быть пустой так часто, учитывая, как быстро токены будут добавляться в очередь. Поэтому единственный способ, которым она потенциально может быть пустой, это, может быть, наша большая языковая модель остановится, может быть, произойдёт прерывание соединения на секунду или что-то подобное, и токены не добавляются. В этом случае мы на самом деле ничего не делаем, мы не продолжаем проверять очередь, мы просто ждём минуту, окей? И затем проверяем снова. Теперь, если она была пуста, мы ждём, а затем переходим к следующей итерации. В противном случае она, вероятно, не будет пустой, мы получаем всё, что находится внутри нашей очереди, извлекаем это, затем говорим: окей, если этот токен является токеном done, мы вернём его, мы остановим этот генератор, мы закончили. В противном случае, если это что-то ещё, мы передадим этот токен, что означает, что мы возвращаем этот токен, но затем продолжаем этот цикл снова. Это наша логика генератора.
Затем у нас есть другие методы здесь. Это специфично для LineChain. Окей, у нас есть on_llm_new_token и on_llm_end. Начиная с on_llm_new_token, это, по сути, когда большая языковая модель возвращает нам токен, LineChain запустит или выполнит этот метод. Окей, это метод, который будет вызван. Что он будет делать? Он перейдёт к именованным аргументам и получит объект chunk. Это происходит из… если в этом chunk есть что-то, он проверит наличие вызова инструмента final answer сначала. Окей, мы получаем наши вызовы инструментов и говорим: если имя внутри нашего chunk, вероятно, это очистит большинство токенов, которые мы возвращаем. Так что вы помните, раньше, когда мы смотрели на эти chunk, это то, что мы рассматриваем, содержимое для нас на самом деле всегда будет пустым, и вместо этого мы фактически получим дополнительные именованные объекты здесь, и внутри них у нас будет наш вызов инструментов, наши вызовы инструментов, как мы видели в предыдущих видео. Так что это то, что мы извлекаем, мы извлекаем эту информацию, поэтому мы используем дополнительные именованные аргументы и получаем информацию о вызове инструмента, или это будет None. Итак, если это None, я не думаю, что это когда-либо будет None, честно говоря, было бы странно, если бы это было None, я думаю, это означает, что что-то будет не так.
Итак, здесь мы используем оператор walrus. Что он делает здесь? Пока мы проверяем логику if здесь, одновременно с этим он также присваивает всё, что находится внутри этого, он присваивает это ToolCalls, а затем с помощью if мы проверяем, является ли ToolCalls чем-то или None, потому что мы используем get здесь. Итак, если эта операция get завершится неудачей, и нет вызовов инструментов, этот объект здесь будет равен None, который присваивается ToolCalls здесь, и тогда этот if None вернёт False, и эта логика не будет выполняться. И она просто продолжится. Если это True, то есть, если здесь что-то возвращается, мы проверим, использует ли это что-то возвращённое имя функции или имя инструмента final answer. Если это так, мы установим этот final_answer_seen равным True, иначе мы просто добавим наш chunk в очередь. Мы используем put_nowait здесь, потому что мы используем async, иначе, если бы мы не использовали async, я думаю, вы могли бы просто использовать put_wait или, может быть, даже put. Вы используете put, если это синхронный код, но я не думаю, что я когда-либо реализовывал синхронный, так что это будет просто put_nowait для async. И затем вернуть.
У нас есть это, затем у нас есть on_llm_end. Итак, это когда LineChain видит, что большая языковая модель вернула или указала, что она закончила с ответом. LineChain вызовет это. Вы должны понимать, что это будет происходить много раз во время выполнения агента, потому что, если вы подумаете, внутри нашего исполнителя агента мы много раз обращаемся к большой языковой модели. У нас есть первый шаг, где она решает: о, я буду использовать инструмент add или инструмент multiply, а затем этот ответ возвращается, мы выполняем этот инструмент, а затем передаём вывод из этого инструмента и весь исходный запрос пользователя в истории чата обратно нашей большой языковой модели. Хорошо? Это ещё один вызов нашей большой языковой модели, который вернётся, он завершится, он даст нам что-то ещё. Итак, есть несколько вызовов большой языковой модели в ходе нашей логики выполнения агента. Поэтому этот вызов on_llm_end будет фактически вызываться в конце каждого из этих вызовов большой языковой модели. Теперь, если мы дойдём до конца нашего вызова большой языковой модели, и это был просто вызов инструмента, то есть он вызвал инструмент add, мы не хотим помещать токен done в нашу очередь, потому что когда токен done добавляется в нашу очередь, мы остановим итерацию. Вместо этого, если это был просто вызов инструмента, мы скажем step_end, и мы фактически получим этот токен обратно. Это полезно, например, для интерфейса, вы могли бы сказать: окей, я использовал инструмент add, вот параметры, и это конец шага. Вы могли бы иметь ваших вызывающих инструментов, используемых на каком-то интерфейсе, и как только он увидит step_end, он знает: окей, мы закончили, вот ответ, и он может просто показать вам это. И мы будем использовать это, мы увидим это скоро, но предположим, мы дойдём до инструмента final answer, мы находимся на инструменте final answer, и затем мы получаем этот сигнал, что большая языковая модель завершилась, тогда нам нужно остановить итерацию, иначе наш потоковый генератор будет просто продолжать работать вечно. Ничего не остановит его, или, может быть, он завершится по времени, я не думаю, что это так. Поэтому в этот момент нам нужно сказать: окей, стоп, нам нужно сказать, что мы закончили, и тогда это вернётся сюда, к нашему итератору и к нашему асинхронному итератору, и он вернёт и остановит генератор.
Это основная логика, которая у нас есть внутри. Я знаю, что там много всего происходит, но нам нужно всё это, поэтому важно знать об этом. Итак, теперь давайте посмотрим, как мы можем фактически вызвать нашего агента со всем этим потоковым… таким образом. Мы инициализируем нашу очередь, мы используем её для инициализации стримера, используя пользовательский стример, который мы только что… пользовательский обработчик обратного вызова, как хотите его называть. Затем я определю функцию, это асинхронная функция, она должна быть, если мы используем async, и она будет делать? Она будет вызывать нашего агента с конфигурацией здесь, и мы передадим ей это… вызов обратного вызова, который является стримером. Обратите внимание, здесь я не вызываю исполнителя агента, я просто вызываю агента. Итак, если мы вернёмся сюда, мы вызываем это. Итак, это не будет включать всю логику выполнения инструментов, и что важно, мы вызываем агента с конфигурацией, которая использует обратные вызовы. Итак, это настраиваемое поле здесь из нашей большой языковой модели фактически передаётся, оно распространяется на наш объект агента, а также на исполняемый… исполняемый. Итак, это то, что мы выполняем здесь. Мы видим agent с конфигурацией, и мы передаём эти обратные вызовы, которых на самом деле только один.
Итак, это настраивает нашего агента, а затем мы вызываем его с помощью стрима, как мы делали раньше, и мы просто вернём всё. Давайте запустим это. И мы видим все токены или объекты chunk, которые возвращаются, и это полезно для понимания того, что мы на самом деле делаем здесь. Когда мы делаем это сообщение chunk, дополнительные именованные аргументы, мы видим это здесь. Это будет объект сообщения chunk, мы получаем дополнительные именованные объекты, переходим к tool_calls и получаем информацию здесь. У нас есть ID для этого вызова инструмента, как мы видели в предыдущих главах, затем у нас есть наша функция, поэтому функция включает имя, поэтому мы знаем, какой инструмент мы вызываем из этого первого chunk, но мы не знаем аргументы, эти аргументы будут передаваться нам потоком, поэтому мы можем видеть, как они начинают поступать в следующем chunk. Следующий chunk - это просто первый токен для функции add, и мы видим, как всё это объединяется за несколько шагов, и мы фактически получаем все наши аргументы. Это довольно круто.
Итак, на самом деле одна вещь, которую я хотел бы вам показать здесь, так это… если мы просто сделаем token = token, извините, и сделаем tokens.append(token), у нас теперь есть все наши токены здесь, вы видите, что все они являются фрагментами сообщений AI, поэтому мы можем фактически объединить их. Итак, давайте… мы возьмём эти здесь, и на основе этих мы получим все аргументы. Итак, это довольно интересно, это с одного до, я думаю, предпоследнего, может быть. Итак, у нас есть эти, и на самом деле мы просто хотим сложить их вместе, поэтому я возьму tokens[1], я просто пройдусь… для token в… мы пойдём со второго и далее, я добавлю token к tk, и давайте посмотрим, как выглядит tk в конце. tk. Итак, теперь вы видите, что я объединил все эти аргументы, извините, +=. Итак, запустим это, и вы увидите здесь, что он объединил эти аргументы, он не получил все из них, поэтому я как бы пропустил некоторые в конце, но он объединяет их. Итак, мы видим, что эта логика, где раньше она добавляла контент из разных фрагментов, она также делает то же самое для других параметров внутри вашего объекта chunk, что, я думаю, довольно круто. Вы видите здесь, что имя не было включено, это потому, что мы начали с token[1] или с token[0], где было имя. Поэтому, если мы на самом деле начнём с token[0], и давайте просто… давайте просто добавим их туда, с первого и далее мы получим полный фрагмент сообщения AI, который включает имя здесь и все эти аргументы, и вы также увидите здесь… заполнит всё, что довольно круто.
Итак, у нас есть это. Теперь, основываясь на этом, мы захотим изменить наш пользовательский исполнитель агента, потому что мы передаём всё потоком. Итак, мы хотим добавить поток внутри нашего исполнителя агента, который мы делаем здесь. Это async def stream, и мы используем async for token in stream. Итак, это как самый первый экземпляр, если output равен None, мы просто будем добавлять наш токен… фрагмент, извините, к нашему output, первый токен становится нашим output, иначе мы просто добавляем наши токены к output. Если содержимое токена пустое, что оно и должно быть, потому что мы всё время используем вызовы инструментов, мы просто выведем содержимое. Я просто добавил эти, чтобы мы видели… вывести всё, я просто хочу… чтобы иметь возможность видеть это. Я не ожидаю, что это будет работать, потому что мы говорим, что оно должно использовать вызов инструментов. Итак, внутри нашего агента, если мы вернёмся сюда, мы сказали tool_choice="any", поэтому он был вынужден использовать вызов инструментов, поэтому он никогда на самом деле не должен возвращать ничего внутри поля content, но на всякий случай, если он там есть. Итак, мы увидим, так ли это на самом деле, затем мы просто получаем информацию о наших tool_calls из нашего фрагмента, и мы скажем: окей, если там что-то есть, мы выведем, что там есть. Затем мы извлечём имя нашего инструмента, если есть… если есть имя инструмента, я покажу вам имя инструмента, затем мы перейдём к args, и если args не пусты, мы посмотрим, что мы получим там. И затем из всего этого мы фактически… мы объединяем всё это в наше сообщение AI, потому что мы объединяем всё по мере прохождения, мы объединяем всё в output, как я показывал вам раньше. Круто. И затем мы просто ждём наш поток, который, как… запустит его. А затем мы снова делаем стандартную работу исполнителя агента здесь. Мы просто извлекаем имя инструмента, журналы инструментов, ID вызова инструмента, а затем используем всё это для выполнения нашего инструмента здесь, а затем создаём новое сообщение инструмента и передаём его обратно, а также здесь я переместил break для final answer на последний шаг.
Это наш пользовательский исполнитель агента с потоковой передачей, и давайте посмотрим, что… давайте посмотрим, что он делает. set verbose=True. Итак, мы видим все эти операторы печати. Итак, вы можете видеть, что это немного неряшливо, но вы можете видеть, что у нас есть вызовы инструментов, внутри которых было что-то, здесь был add, и то, что мы выводим здесь, это полный фрагмент сообщения AI с вызовами инструментов, а затем я просто вывожу: окей, что мы на самом деле извлекаем из… из этого. Это на самом деле происходит из одного и того же, окей? А затем то же самое здесь, мы смотрим на полное сообщение, а затем мы смотрим: окей, мы извлекаем этот аргумент из него. Итак, мы можем видеть всё, что извлекается, фрагмент за фрагментом или токен за токеном, и всё. Итак, мы могли бы просто получить всё так. Однако, я вывожу всё, чтобы мы могли видеть, что это поток. А что, если я не буду выводить? Итак, мы устанавливаем bo… по умолчанию both=False здесь. Что произойдёт, если мы вызовем now + c? Круто, мы ничего не получили. Причина, по которой мы ничего не получили, заключается в том, что мы не выводим, но мы не… если вы строите, например, API, вы извлекаете ваши токены, вы не можете выводить их на ваш… как на ваш интерфейс или выводить их как вывод вашего API. Вывод идёт в ваш терминал, ваше окно консоли, он никуда больше не идёт. Вместо этого мы хотим извлечь эти токены, но как нам это сделать? Хорошо, мы вывели их, но ещё одно место, где находятся эти токены, это наша очередь, потому что мы настроили их на отправку в очередь, поэтому мы можем фактически извлечь их из нашей очереди, пока работает наш исполнитель агента, и затем мы можем делать с ними всё, что захотим, потому что наш код асинхронный, поэтому он может делать много вещей одновременно. Пока наш код выполняет исполнитель агента, пока это происходит, наш код также может извлекать из нашей очереди токены, которые находятся там, и отправлять их, например, в API, верно? Или куда-нибудь ещё. Давайте посмотрим, как это выглядит. Мы начинаем с инициализации нашей очереди, инициализации нашего стримера с помощью этой очереди, затем создаём задачу. Это, по сути, означает: окей, я хочу запустить это, но не запускайте это прямо сейчас, я ещё не готов. Причина, по которой я говорю, что я ещё не готов, заключается в том, что я также хочу определить здесь свой асинхронный цикл, который будет выводить эти токены. Но это асинхронно, поэтому мы… мы настраиваем это, это как приготовиться запустить это, потому что это асинхронно, это выполняется, это просто выполняется… оно уже выполняется, мы получаем это, мы продолжаем, ничего из этого на самом деле ещё не выполняется, только здесь, когда мы ждём задачу, которую мы настроили здесь, только тогда запускается наш исполнитель агента, и наш асинхронный объект здесь начинает получать токены. И здесь снова вывод, но мне не нужно выводить, я мог бы… скажем, где это находится внутри API или чего-то подобного, скажем, я говорю: окей, отправить токен в XYZ токен. Это отправка токена куда-то, или, может быть, мы передаём это нашему… какому-то объекту стримера внутри нашего API. Мы можем делать с этими токенами всё, что захотим. Я просто вывожу их, потому что хочу видеть их. Но важно здесь то, что мы не выводим их внутри нашего исполнителя агента, мы выводим их за пределами исполнителя агента, мы получили их, и мы можем поместить их куда угодно, что идеально подходит, когда вы создаёте реальное приложение, используя API или что-то ещё. Итак, давайте запустим это, давайте посмотрим, что мы получим. Посмотрите на это, мы получаем всю необходимую информацию и немного больше, потому что теперь мы используем исполнитель агента, и теперь мы можем также видеть: о, у нас есть этот step_end. Итак, я знаю, просто глядя на это, это моё первое использование инструмента, так какой это инструмент? Давайте посмотрим, это инструмент add, а затем у нас есть эти аргументы, я могу затем передать их… дальше, затем у нас есть следующее использование инструмента, которое находится здесь, внизу, поэтому мы можем затем передать их так, как нам нравится. Это довольно круто. Я имею в виду… давайте посмотрим. Мы получаем эти аргументы… можем ли мы… можем ли мы что-то сделать с ними, прежде чем я… прежде чем я выведу их и покажу? Да, давайте посмотрим.
Теперь мы изменяем наш цикл. То же самое, мы всё ещё инициализируем нашу очередь, инициализируем наш стример, инициализируем нашу задачу. И мы всё ещё делаем это async for token in stream. Но затем мы делаем что-то с нашими токенами. Я говорю: окей, если мы на stream_end, я на самом деле не буду выводить stream_end, я буду выводить новую строку. В противном случае, если мы получаем вызов инструмента здесь, мы скажем: если этот вызов инструмента является именем инструмента, я выведу вызов имени инструмента. Если это аргументы, я выведу аргумент инструмента, и я закончу ничем, чтобы мы не перешли на новую строку. Мы фактически будем передавать всё потоком. Итак, давайте просто посмотрим, как это выглядит. Ой, я просто добавил это. Вы видите это? Итак, он идёт очень быстро, поэтому его трудно увидеть, я замедлю его, чтобы вы могли… чтобы вы могли видеть, что как только мы получим имя инструмента, мы передаём его потоком, мы вызываем инструмент add, затем передаём токен за токеном фактические аргументы для этого инструмента, затем для следующего снова делаем то же самое, мы вызываем это имя инструмента, затем снова передаём токен за токеном, мы обрабатываем всё дальше снаружи исполнителя агента, и это важно уметь делать, когда мы на самом деле реализуем потоковую передачу и async и всё остальное в реальном приложении. Я знаю, что это много, но это важно. Это всё для нашей главы о потоковой передаче и async. Надеюсь, всё это было полезно. Спасибо.
Теперь мы переходим к заключительной главе, мы будем брать всё, что мы узнали до сих пор, и использовать это для создания реального приложения чата. Теперь приложение чата - это то, что вы видите прямо сейчас, и мы можем войти в него и задать довольно интересные вопросы, и потому что это агент, потому что он использует инструменты, он сможет ответить на них для нас. Итак, мы увидим в нашем приложении, что мы можем задавать вопросы, которые требуют использования инструментов, такие как этот, и благодаря потоковой передаче, которую мы реализовали, мы можем видеть всю эту информацию в реальном времени. Мы видим, что используется инструмент sub API, это запросы, мы видели, что всё это было параллельно, поэтому каждый из этих инструментов использовался параллельно, мы немного изменили код, чтобы включить это, и мы видим, что у нас есть ответ, мы также видим используемый структурированный вывод здесь, поэтому мы видим наш ответ, за которым следуют используемые инструменты здесь, а затем мы могли бы также задавать последующие вопросы, потому что это разговорно. Мы говорим: как погода в каждом из этих городов? Это довольно круто. Это то, что мы будем строить. Мы, конечно, будем сосредоточены на API, бэкенде. Я не фронтенд-инженер, поэтому я не могу провести вас через это, но код есть, поэтому те из вас, кто хочет пройти через код фронтенда, конечно, могут это сделать, но мы будем сосредоточиться на том, как мы строим API, который управляет всем этим, используя, конечно, всё, что мы узнали до сих пор. Давайте перейдём к этому. Первое, что нам нужно сделать, это клонировать этот репозиторий, поэтому мы скопируем этот URL, это репозиторий orelio-labs/langchain-course, и вы просто клонируете свой репозиторий так. Я уже сделал это, поэтому я не буду делать это снова, вместо этого я просто перейду к репозиторию langchain-course. Теперь есть несколько вещей для настройки, которые вам нужно сделать, все они можно найти в файле README, поэтому мы просто откроем новую вкладку здесь, и я открою README. Итак, это объясняет всё, что нам нужно. У нас есть… если вы уже запускали это локально, вы уже видели это или уже сделали всё это, но для тех из вас, кто не сделал, мы быстро пройдёмся сейчас. Вам нужно будет установить библиотеку venv, поэтому так мы управляем нашим окружением Python, нашими пакетами, мы используем venv на Mac, вы установите его так. Если вы используете Windows или Linux, просто дважды проверьте, как вы установите это здесь. После того, как вы установили это, вы затем устанавливаете Python, поэтому venv python install, а затем мы хотим создать нашу виртуальную машину, нашу виртуальную среду, используя эту версию Python, поэтому venv здесь. Затем, как вы можете видеть здесь, нам нужно активировать эту виртуальную среду, что я пропустил здесь, поэтому позвольте мне быстро добавить это. Итак, вы просто запустите это для меня, я использую fish, поэтому я просто добавляю fish в конце, но если вы используете bash или zsh, я думаю, вы можете… вы можете просто запустить это напрямую, и наконец, нам нужно синхронно установить все наши пакеты, используя venv sync.
И вы видите, что всё установится автоматически, отлично. Так что это у нас есть, и мы можем перейти и фактически открыть Cursor или VS Code, и тогда мы должны оказаться внутри Cursor или VS Code. Здесь вы найдёте несколько вещей, которые нам понадобятся. Во-первых, это переменные среды. Мы можем перейти сюда, и у нас есть OpenAI API Key, LangChain API Key и SerpAPI API Key. Создайте копию этого, и вы можете сделать это вашим .env файлом, или, если вы хотите запустить его с помощью Source, вы можете. Я люблю использовать .env на Mac, и я просто добавляю export в начало, а затем ввожу свои API ключи. На самом деле, у меня уже есть они в этом local.env файле, который в моём терминале я просто активирую с помощью Source снова, вот так. Это понадобится нам, когда мы будем запускать наш API и приложение позже, но сейчас давайте сосредоточимся на понимании того, как выглядит API.
Перейдя в главу 09 Capstone, мы найдём несколько вещей. На чём мы сосредоточимся, так это на API, и у нас есть пара записных книжек, которые помогут нам понять, что мы здесь делаем. Позвольте мне дать вам краткий обзор API. API, который мы используем, это FastAPI. В нём несколько функций. С той, с которой мы начнём, это вот эта. Итак, это наш POST endpoint для invoke, и он по сути отправляет что-то нашему LLM и начинает потоковый ответ. Мы можем перейти и фактически запустить API, и мы можем просто увидеть, как это выглядит. Мы перейдём в главу 09 Capstone, API после установки наших переменных среды, и мы просто хотим сделать uvicorn main:app --reload. Нам не нужно перезагружать, но если мы изменяем код, это может быть полезно. И мы видим, что наш API теперь запущен на localhost:8000. И если мы перейдём в наш браузер, мы можем фактически открыть документацию для нашего API. Мы переходим на 8000/docs. Хорошо, мы просто видим, что у нас есть этот единственный метод invoke. Он выводит содержимое и даёт нам небольшое количество информации. Теперь мы можем попробовать здесь. Если мы скажем "Привет", мы можем запустить это, и мы увидим, что получим ответ. Мы получим это.
Теперь то, чего нам здесь не хватает, это то, что это фактически передаётся нам потоком. Хорошо, это не просто прямой ответ, это поток. Чтобы увидеть это, мы перейдём сюда, к этой записной книжке с потоковым тестом, и запустим её. Мы используем requests здесь, мы не просто делаем стандартный POST запрос, потому что мы хотим передавать вывод потоком и затем выводить вывод по мере его получения. Хорошо, поэтому это выглядит немного сложнее, чем просто типичный request.post или request.get. Итак, что мы здесь делаем, мы запускаем нашу сессию, которая является нашим POST запросом, а затем мы просто итерируемся по содержимому по мере его получения от этого запроса. Когда мы получаем токен, потому что иногда это может быть None, мы выводим это. Хорошо, и у нас есть flush=True, который мы использовали в прошлом. Давайте определим это, а затем зададим простой вопрос: "Что такое 5 + 5?". Хорошо, и мы увидели, что это было довольно быстро. Итак, он сгенерировал этот ответ сначала, а затем продолжил потоковую передачу со всем этим. Хорошо, и мы видим, что эти специальные токены предоставляются. Это помогает front-end'у по сути решить, что куда должно идти. Здесь, где мы показываем эти несколько шагов использования инструментов, и параметры, то, как front-end решает, как отобразить их, это просто… ему предоставляется один поток, но у него есть токены SE, имеет SE, имеет имя SE, затем у него есть параметры, за которыми следует своего рода токен окончания шага, и он смотрит на каждый из них, а один шаг, имя которого он обрабатывает по-другому, это когда он увидит имя шага "Окончательный ответ". Когда он видит имя шага "Окончательный ответ", вместо отображения этого интерфейса инструмента он вместо этого начинает потоковую передачу токенов напрямую, как в типичном интерфейсе чата. И если мы посмотрим на то, что мы фактически получаем в нашем окончательном ответе, это не просто сам ответ. Хорошо, у нас есть ответ здесь, это передаётся в типичный вывод чата, но у нас также есть используемые инструменты, и это добавляется в маленькие поля, которые у нас есть под чатом. Здесь происходит очень много всего внутри этого маленького потока.
Теперь мы можем попробовать с другими вопросами. Мы можем сказать: "Хорошо, расскажи мне о последних новостях в мире". Вы можете видеть, что здесь есть небольшое ожидание, пока он ждёт получения ответа, и затем да, он передаёт много информации довольно быстро. Хорошо, поэтому здесь поступает много информации. Хорошо, и затем мы можем задать другие вопросы, например, вот этот: "Насколько холодно в Осаке прямо сейчас?" "Пять умножить на пять?". Хорошо, эти два будут выполнены параллельно, а затем, после того как у агента будут ответы на них, агент использует другой инструмент умножения, чтобы умножить эти два значения вместе, и всё это будет передано потоком. Хорошо, и как мы видели ранее, у нас есть "Какая текущая дата и время в этих местах?". То же самое. Три вопроса. Три вопроса здесь: "Какая текущая дата и время в Дубае?", "Какая текущая дата и время в Токио?" и "Какая текущая дата и время в Берлине?". Эти три вопроса выполняются параллельно с помощью поиска в SerpAPI, и все ответы возвращаются в этом окончательном ответе. Хорошо, вот как работает наш API. Теперь давайте немного углубимся в код и поймём, как он работает. Здесь много важных вещей, есть определённая сложность, но в то же время мы постарались сделать это как можно проще.
Итак, давайте рассмотрим синтаксис FastAPI здесь, с app.post(invoke). Наш endpoint invoke потребляет некоторое содержимое, которое просто строка, и если вы помните из глубокого погружения в выполнение агента, что мы реализовали здесь, или модифицированную версию этого, нам нужно инициализировать наш async Q и наш streamer, который является QCallbackHandler, который, я считаю, точно такой же, как тот, который мы определили в предыдущей главе. Там нет никаких различий. Итак, мы определяем это, а затем возвращаем этот объект streaming response. Опять же, это функция FastAPI. Это для того, чтобы вы передавали ответ потоком. Этот streaming response имеет несколько атрибутов, которые опять же являются функциями FastAPI или просто общими вещами API. Некоторые заголовки, дающие инструкции API, а затем тип носителя здесь, который является text/event-stream. Вы можете также использовать, я думаю, text/plain, возможно, также, но я считаю, что стандарт здесь будет использовать event-stream. А более важная часть для нас - это этот token generator. Хорошо, что это за token generator? Это функция, которую мы определили здесь. Теперь, если вы опять же помните предыдущую главу, в конце главы мы настроили цикл for, где мы выводили разные токены в различных форматах, поэтому мы как бы проводили их постобработку, прежде чем решить, как их отображать. Это именно то, что мы делаем здесь. В этом блоке мы перебираем каждый токен, который мы получаем от нашего streamer'а. Мы перебираем и просто говорим: "Хорошо, если это конец шага, мы будем передавать этот токен конца шага, который мы видели здесь". Хорошо, это этот токен конца шага. В противном случае, если это вызов инструмента, снова у нас есть этот оператор 'W', поэтому мы делаем следующее: "Хорошо, получить вызовы инструментов из нашего текущего сообщения. Если что-то есть, то, если это не None, мы собираемся выполнить то, что находится внутри. И что выполняется внутри, это проверка на имя инструмента. Если у нас есть имя инструмента, мы возвращаем это". Хорошо, у нас есть токен начала шага, токен имени начала шага, имя инструмента или имя набора, как хотите называть, а затем токен конца имени набора. Хорошо, и это, конечно, приходит на front-end вот так. Хорошо, вот что у нас есть. В противном случае мы должны видеть только имя инструмента, возвращаемое как часть первого токена. После этого это должны быть просто аргументы инструмента. В этом случае мы говорим: "Хорошо, если у нас есть эти аргументы инструмента или функции, мы просто вернём их напрямую". Затем это та часть, которая будет передавать всё это потоком. Хорошо, например, это будут отдельные токены. Хорошо, так что мы можем иметь открытые фигурные скобки, за которыми следует 'query', это может быть токен, 'latest' может быть токеном, 'world' может быть токеном, 'news' может быть токеном и т.д. Хорошо, вот что происходит. Это не должно выполняться, но у нас есть… мы просто обрабатываем это на всякий случай. Если у нас есть какие-либо проблемы с возвращаемыми токенами, мы просто выведем ошибку и продолжим потоковую передачу, но это на самом деле не должно происходить. Отлично, это наш цикл потоковой передачи токенов. Теперь способ, которым мы получаем токены из нашего объекта stream, это, конечно, через нашу логику выполнения агента, которая происходит параллельно. Хорошо, всё это асинхронно. У нас есть это асинхронное определение. Таким образом, всё это происходит асинхронно. Итак, что произошло здесь, здесь мы создали задачу, которая является agent_executor.invoke, и мы передаём наше содержимое, мы передаём этот streamer, из которого мы будем получать токены, и мы также устанавливаем verbose=True. Мы можем фактически удалить это, но это просто позволит нам увидеть дополнительный вывод в нашем окне терминала, если мы хотим. Я не думаю, что там есть что-то особенно интересное, на что можно посмотреть, но особенно если вы отлаживаете, это может быть полезно. Итак, мы создаём нашу задачу здесь, но это не запускает задачу. Хорошо, это асинхронный asyncio.create_task, но это не начинается, пока мы не ждём его здесь. Итак, что происходит здесь, по существу, этот код здесь всё ещё выполняется, как… мы находимся в асинхронном цикле здесь, но затем мы ждём эту задачу. Как только мы ждём эту задачу, токены начнут помещаться в нашу очередь, которая затем будет получена объектом streamer здесь. Затем это начинает получать токены. Я знаю, что асинхронный код всегда немного более запутанный, учитывая странный порядок вещей, но это по сути то, что происходит. Вы можете представить, что всё это по сути выполняется одновременно.
У нас есть… есть ли что-то ещё, что нужно пройти здесь? Я так не думаю. Это всё своего рода заготовка для FastAPI, а не сам код ИИ. Итак, у нас есть это, это наша функция потоковой передачи. Теперь давайте посмотрим на сам код агента. Хорошо, код агента, где бы он ни был. Мы используем этот agent_executor.invoke, и мы импортируем это из файла agent. Мы можем взглянуть сюда на это. Теперь вы сразу же видите, что мы извлекаем наши API ключи здесь. Да, убедитесь, что у вас они есть. Все наши… хорошо, это то, что мы видели раньше в главе "Глубокое погружение в выполнение агента". Это практически то же самое. У нас есть наш LLM. Мы установили эти настраиваемые поля, как мы делали в предыдущих главах. Это настраиваемое поле для наших обратных вызовов. У нас есть наш prompt. Он немного изменён. По существу, он просто говорит: "Убедитесь, что вы используете предоставленные инструменты". Мы говорим: "Вы должны использовать инструмент окончательного ответа, чтобы дать окончательный ответ пользователю". И одна вещь, которую я добавил, которую я замечаю время от времени, поэтому я явно сказал: "Добавить ответ на текущий вопрос пользователя, а не на предыдущие вопросы". Я обнаружил, что с этой настройкой время от времени, если я просто немного поболтаю с агентом, и до этого я задавал вопросы о том, какая погода была в том или ином месте, агент будет как бы цепляться за эти предыдущие вопросы и пытаться снова использовать инструмент для ответа, и это просто то, что вы можете более или менее попросить его сделать. Хорошо, у нас есть это. Это всё точно такое же, как раньше. Хорошо, у нас есть наша история чата, чтобы сделать это разговорным. У нас есть наше сообщение от человека и наш блокнот агента, чтобы агент мог обдумывать сообщения об использовании нескольких инструментов. Отлично. У нас также есть класс Article. Это для обработки результатов из SerpAPI. У нас есть наша функция SerpAPI здесь. Я расскажу об этом немного подробнее через минуту, потому что это также немного отличается от того, что мы рассматривали раньше. То, что мы рассматривали раньше с SerpAPI, если вы помните, было синхронным, потому что мы используем клиент SerpAPI или инструмент SerpAPI напрямую из LangChain, и поскольку мы хотим, чтобы всё было асинхронным, нам пришлось пересоздать этот инструмент асинхронным способом, о котором мы поговорим немного позже, но пока давайте перейдём от этого. Мы видим, что наш окончательный ответ используется здесь. Это, я думаю, мы определили точно такую же вещь раньше, вероятно, в той главе "Глубокое погружение", снова, где у нас есть только ответ и инструменты, которые были использованы. Отлично. У нас есть это. Одна вещь, которая немного отличается здесь, это когда мы определяем нашу функцию name_to_tool. Она принимает имя инструмента и сопоставляет его с функцией инструмента. Когда у нас есть синхронные инструменты, мы фактически используем tool_func здесь. Хорошо, вместо tool_coroutine это будет tool_func. Однако мы используем асинхронные инструменты, поэтому это фактически tool_coroutine, и поэтому это… поэтому, если вы… если вы посмотрите сюда, я сделал каждый инструмент асинхронным. Это не обязательно для инструмента, такого как "Окончательный ответ", потому что нет… нет вызовов API. Вызов API - это очень типичный сценарий, когда вы хотите использовать async, потому что, если вы сделаете вызов API с помощью синхронной функции, ваш код просто будет ждать ответа от API, пока API обрабатывается и делает всё, что он делает. Таким образом, это идеальный сценарий, когда вы захотите использовать async, потому что вместо того, чтобы ваш код просто ждал ответа от API, он может вместо этого делать что-то ещё, пока он ждёт. Хорошо, это идеальный сценарий, когда вы бы использовали async, поэтому мы бы использовали его, например, с инструментом SerpAPI здесь, но для "Окончательного ответа" и для всех этих инструментов калькулятора, которые мы создали, на самом деле нет необходимости делать их асинхронными, потому что наш код просто выполняется, он выполняет этот код, нет никакого ожидания. Поэтому нет необходимости делать их асинхронными. Однако, сделав их асинхронными, это означает, что я могу использовать tool_coroutine для всех них, а не говорить: "О, если этот инструмент синхронный, используйте tool.func, тогда как если этот асинхронный, используйте tool.coroutine". Это просто упрощает код для нас намного больше. Но да, не обязательно напрямую, но это помогает нам писать более чистый код. Это также верно позже, потому что нам фактически нужно ждать нашего кода инструмента, который мы видим здесь. Хорошо, нам нужно ждать этих вызовов инструмента, что было бы более сложным, если бы мы использовали некоторые синхронные инструменты, некоторые асинхронные инструменты. Итак, у нас есть это. У нас есть наш QCallbackHandler. Это снова то же самое, что и раньше. Поэтому я не буду проходить через… я не буду проходить через это. Мы рассмотрели это в предыдущей главе "Глубокое погружение". У нас есть наша функция execute_tool здесь. Снова, это асинхронно. Это просто помогает нам, знаете, немного очистить код. Я думаю, в главе "Глубокое погружение" у нас это было размещено непосредственно в нашей функции agent_executor, и вы можете сделать это, всё в порядке, это просто немного чище, чтобы как бы выделить это, и мы можем также добавить больше аннотаций типов здесь, что мне нравится. execute_tool ожидает, что мы предоставим сообщение ИИ, которое включает в себя вызов инструмента, и оно вернёт сообщение инструмента. Хорошо, agent_executor. Это всё то же самое, что и раньше, и мы на самом деле даже не используем verbose здесь, поэтому мы могли бы полностью удалить его, но я оставлю его, конечно, если вы хотите использовать это, вы можете просто добавить if verbose и затем вывести или распечатать некоторые вещи там, где вам это нужно. Хорошо, что у нас есть здесь? У нас есть наша функция потоковой передачи, это то, что фактически вызывает нашего агента. Хорошо, у нас есть запрос. Это вызовет нашего агента прямо здесь, и мы могли бы даже сделать это немного понятнее. Например, это может быть configured_agent, потому что это… это не ответ, это настроенный агент. Я думаю, это может быть намного понятнее. Итак, мы настраиваем нашего агента с нашими обратными вызовами. Хорошо, это просто наш streamer, затем мы итерируемся по токенам, возвращаемым нашим агентом, используя поток здесь. Хорошо, и по мере того, как мы итерируемся через это, потому что мы передаём наш streamer в обратный вызов здесь, то, что он будет делать, это каждый токен, который возвращает наш агент, будет обрабатываться нашим QCallbackHandler здесь. Хорошо, эти on_llm_new_token, on_llm_end будут выполнены, а затем все эти токены, которые вы видите здесь, будут переданы в нашу очередь. Затем мы приходим сюда, и у нас есть этот await. Итак, этот метод await здесь используется нашим генератором в нашем API, используется этим token_generator для получения из очереди токенов, которые были помещены в очередь этими другими методами. Хорошо, он помещает токены в очередь и извлекает их с помощью этого. Хорошо, это тоже происходит параллельно, а также выполняется этот код. Теперь причина, по которой мы извлекаем токены здесь, заключается в том, что мы хотим извлечь наши токены, и мы добавляем их все в наши выходы. Теперь эти выходы становятся списком сообщений ИИ, которые по сути говорят нам, какой инструмент использовать и какие параметры передавать каждому из этих инструментов. Это очень похоже на то, что мы рассматривали в той главе "Глубокое погружение", но одна вещь, которую я изменил здесь, это то, что я позволил использовать параллельные вызовы инструментов. Это то, что мы видим здесь с этими четырьмя строками кода. Мы говорим: "Хорошо, если наш вызов инструмента включает в себя ID, это означает, что у нас есть новый вызов инструмента или новое сообщение ИИ". Итак, мы делаем следующее: мы добавляем это сообщение ИИ, которое является фрагментом сообщения ИИ, в наши выходы, а затем после этого, если мы не получим ID, это означает, что мы получаем аргументы инструмента. После этого мы просто добавляем наш фрагмент сообщения ИИ к самому последнему фрагменту сообщения ИИ из наших выходов. Хорошо, что это будет делать, это… это создаст этот список сообщений ИИ, это будет как сообщение ИИ один, а затем это просто добавит всё к этому сообщению ИИ один, затем мы получим наш следующий фрагмент сообщения ИИ, это затем просто добавит всё к нему, пока мы не получим полное сообщение ИИ и так далее. Хорошо, что мы делаем здесь, здесь мы собрали все наши объекты фрагментов сообщений ИИ, затем, наконец, что мы делаем, это просто преобразуем все эти объекты фрагментов сообщений ИИ в фактические объекты сообщений ИИ, а затем возвращаем их из нашей функции, которую мы затем получаем здесь. Итак, в переменную tool_calls.
Теперь это очень похоже на главу "Глубокое погружение". Снова мы проходим через этот счётчик, этот цикл, где у нас есть max_iterations, после чего мы просто остановимся, но до тех пор мы продолжаем итерироваться и делать больше вызовов инструментов, выполнять эти вызовы инструментов и так далее. Итак, что… что происходит здесь? Давайте посмотрим. Мы получили наши вызовы инструментов. Это будет список объектов сообщений ИИ. Затем то, что мы делаем с этими объектами сообщений ИИ, это передаём их этой функции execute_tool. Если вы помните, что это такое, это эта функция. Итак, мы передаём каждое сообщение ИИ по отдельности этой функции, и это выполнит вызовы инструментов, а затем вернёт нам это наблюдение от инструмента. Хорошо, это то, что вы видите, что происходит здесь, но это асинхронный метод. Таким образом, обычно то, что вам нужно было бы сделать, это сделать await execute_tool, и мы могли бы сделать это. Итак, мы могли бы сделать… давайте… давайте сделаем это немного больше для нас. Хорошо, итак, что мы могли бы сделать, например, что могло бы быть немного понятнее, это вы могли бы сделать tool_obs = [], и что вы можете сделать, это сказать: for tool_call in tool_calls: tool_observation = Мы собираемся добавить execute_tool, который должен быть в await, поэтому мы фактически поместим ваш await туда, и что это будет делать, это фактически то же самое, что мы делаем здесь, разница в том, что мы делаем это инструмент за инструментом. Хорошо, мы выполняем async здесь, но мы делаем это последовательно, тогда как то, что мы можем сделать, что лучше, это использовать asyncio.gather. Что это делает, так это собирает все эти корутины, а затем мы ждём их все одновременно, чтобы запустить их все асинхронно. Они все начинают одновременно или почти одновременно, и мы получаем эти ответы как бы параллельно, но, конечно, это… поэтому это не полностью параллельно, но практически параллельно. Отлично, у нас есть это, а затем это… хорошо, мы получаем все наши наблюдения за инструментами от этого. Итак, это все наши сообщения об инструментах, и затем одна интересная вещь здесь, это если мы… скажем, у нас есть все наши сообщения ИИ, все наши ядра инструментов, и мы просто добавляем все эти в наш блокнот агента. Хорошо, скажем, здесь мы просто… о, хорошо, scratchpad.extend, и тогда у нас будет… у нас будут наши вызовы инструментов, а затем мы сделаем agent_scratchpad.extend(tool_obs). Хорошо, что происходит здесь, это по существу дало бы нам что-то, что выглядит так: у нас есть наше сообщение ИИ, скажем, я просто собираюсь поставить… мы просто поставим ID вызова инструмента сюда, чтобы немного упростить. Это будет ID вызова инструмента A, затем у нас будет сообщение ИИ, ID вызова инструмента B, затем у нас будет сообщение инструмента, давайте просто удалим это поле content, я не хочу этого, и сообщение инструмента, ID вызова инструмента B. Хорошо, это будет выглядеть примерно так. Итак, порядок… сообщение инструмента не следует за сообщением ИИ, что вы подумаете. Хорошо, у нас есть этот ID вызова инструмента, это, вероятно, нормально. На самом деле, когда мы запускаем это, если вы добавляете это в свой блокнот агента в этом порядке, вы увидите, что ваш ответ просто зависает, как ничего… ничего не происходит, когда вы приходите к своей второй итерации вызова агента. Поэтому на самом деле то, что вам нужно сделать, это их нужно отсортировать, чтобы они были фактически в порядке, и на самом деле это не имеет значения в каком порядке с точки зрения a или b или c или чего-либо ещё, что вы используете, поэтому у вас может быть этот порядок, у нас есть сообщение ИИ, сообщение инструмента, сообщение ИИ, сообщение инструмента, пока у вас есть ваши ID вызовов инструментов вместе, или вы можете, знаете, инвертировать это, например. Хорошо, вы могли бы иметь это, хорошо, и это тоже будет работать. Это по существу просто так долго, как у вас есть ваше сообщение ИИ, за которым следует ваше сообщение инструмента, и оба они имеют этот ID вызова инструмента, вам нужно убедиться, что у вас есть этот порядок. Хорошо, это, конечно, не произойдёт, если мы сделаем это, и вместо этого нам нужно сделать что-то вроде этого. Хорошо, я сделал это намного проще для чтения. Хорошо, мы берём ID вызова инструмента, мы указываем его на наблюдение за инструментом, и мы делаем это для каждого вызова инструмента и наблюдения за инструментом внутри, как бы, zip этих. Хорошо, затем мы говорим: для каждого вызова инструмента в наших вызовах инструментов мы расширяем наш блокнот агента этим вызовом инструмента, за которым следует сообщение наблюдения за инструментом, которое является сообщением инструмента. Итак, это будет наше… это сообщение ИИ, а это сообщения инструмента.
Там, окей, так вот что происходит, и так мы получаем этот правильный порядок, который будет работать, иначе всё не будет работать. Так что это важно помнить. Окей, теперь мы почти закончили. Я знаю, мы только что прошли через многое, поэтому мы продолжаем, мы увеличиваем наш счётчик, как мы делали раньше. Затем нам нужно проверить окончательный ответ инструмента. Окей, и поскольку мы запускаем эти инструменты параллельно, окей, поскольку мы допускаем множественные вызовы инструментов за один шаг, мы не можем просто посмотреть на самый последний инструмент и посмотреть, является ли он, имеет ли он имя «Окончательный ответ». Вместо этого нам нужно пройти итерации по всем нашим вызовам инструментов и проверить, имеет ли какой-либо из них имя «Окончательный ответ». Если они это делают, мы говорим, окей, мы извлекаем этот вызов окончательного ответа, мы также извлекаем окончательный ответ. Итак, это прямой текстовый контент, и мы говорим, окей, мы нашли, нашли окончательный ответ. Итак, это будет установлено в True, окей, что должно происходить каждый раз, но допустим, если наш агент застрянет в цикле вызова нескольких инструментов, это может не произойти, прежде чем мы прервёмся на основе максимального количества итераций здесь. Поэтому мы можем закончить тем, что прервёмся на основе максимального количества итераций, а не на том, что мы нашли окончательный ответ. Окей, так что это может произойти. Так что в любом случае, если мы найдём этот окончательный ответ, мы выходим из этого цикла for здесь, и, конечно же, нам нужно выйти из нашего цикла while, который находится здесь. Поэтому мы говорим, если мы нашли окончательный ответ, прервать. Окей, круто, так что у нас это есть, наконец, после всего этого. Итак, это наш, вы знаете, мы выполнили наш инструмент, шаги нашего агента, итерации прошли обработку, мы прошли через них. Наконец, мы спускаемся сюда, где говорим, окей, мы собираемся добавить этот окончательный вывод в нашу историю чата. Итак, это будет просто текстовое содержимое, верно? Итак, это здесь получает прямой ответ, но затем мы делаем, мы возвращаем полный вызов окончательного ответа. Полный вызов окончательного ответа — это в основном это здесь, верно? Итак, этот ответ и используемые инструменты, но, конечно, заполненные. Мы говорим здесь, что если у нас есть окончательный ответ, окей, если у нас он есть, мы собираемся вернуть вызов окончательного ответа, который был сгенерирован нашей большой языковой моделью, иначе мы вернём этот. Итак, это в сценарии, что, может быть, агент попал в цикл и просто продолжал итерироваться. Если это произойдёт, мы скажем, что он вернётся с, окей, ответ не найден, и он просто вернёт, окей, мы не использовали никаких инструментов, что технически неверно, но это как событие обработки исключений. Идеально, этого не должно происходить, но это не очень большая проблема, если мы говорим, окей, никаких инструментов не использовалось, по моему мнению. В любом случае, круто, так что у нас есть всё это, и да, мы просто инициализируем наш агент-исполнитель, и я имею в виду, что это наш код выполнения агента. Последнее, что мы хотим пройти, это инструмент Ser API, который мы сделаем через минуту. Окей, Ser API, давайте посмотрим, что, давайте посмотрим, как мы строим наш инструмент Ser API. Окей, поэтому мы начнём с синхронного Ser API. Причина, по которой мы начинаем с этого, заключается в том, что это на самом деле, это просто немного проще. Поэтому я быстро покажу вам это, прежде чем мы перейдём к асинхронной реализации, которую мы используем в нашем приложении. Поэтому мы хотим получить наш ключ API set API. Поэтому я запущу это, и мы просто введём его вверху, и это будет R. Итак, мы собираемся использовать SDK sub API сначала. Мы импортируем Google search, и это входные параметры. У нас есть наш API-ключ, который мы используем, мы говорим, хотим использовать Google, наш вопрос — это запрос, поэтому Q для запроса. Мы ищем последние новости в мире. Он вернёт довольно много вещей. Вы можете видеть, что сейчас там целая куча вещей. Сейчас то, что мы хотим, содержится в этом ключе organic results. Поэтому мы можем запустить это, и мы увидим, K говорит о различных вещах, довольно свежих на данный момент. Поэтому мы можем сказать, окей, это на самом деле работает. Сейчас это довольно беспорядочно, поэтому первое, что я хотел бы сделать, это просто немного очистить это. Поэтому мы определяем эту базовую модель статьи, которая является пантичной, и мы говорим, окей, из набора результатов, окей. Итак, мы собираемся пройти итерации по каждому из них, мы собираемся извлечь заголовок, источник ссылки и сниппет. Вы можете видеть заголовок, источник ссылки и сниппет здесь. Окей, так что это всё полезно. Мы запустим это, и что мы делаем, это проходим через каждый из результатов в organic results, и мы просто загружаем их в нашу статью, используя этот метод класса здесь, и затем мы можем увидеть, окей, давайте посмотрим, как они выглядят. Это намного приятнее. Окей, мы получаем этот красиво отформатированный объект здесь. Круто, это здорово. Теперь всё это, что мы только что сделали здесь, итак, это использование SDK sub apis, что здорово, очень просто в использовании. Проблема в том, что они не предлагают асинхронный SDK, что позор, но нам не так уж сложно настроить его самим. Поэтому обычно с синхронными запросами мы можем использовать библиотеку aiio HTTP. Это, ну, вы можете видеть, что мы делаем здесь. Итак, это эквивалентно requests.get. Окей, это в основном то, что мы делаем здесь, и эквивалент буквально это. Окей, это эквивалент, используя requests, который мы запускаем здесь, но мы используем асинхронный код. Мы используем AI Hep client session, а затем session.get, окей, с этим async with здесь, а затем мы просто ждём нашего ответа. Итак, это всё, да, это то, что мы делаем, а не это, чтобы сделать наш код асинхронным. Это действительно просто, и затем вывод, который мы получаем, точно такой же, верно? Итак, мы всё ещё получаем этот точно такой же вывод. Это означает, конечно, что мы можем использовать этот метод articles так же, точно так же, и мы получаем, мы получаем тот же результат. Нет необходимости делать эту статью из sub API result asnc, потому что снова, как это, эта часть кода здесь полностью локальна, это просто наш python, запускающий всё, поэтому это не должно быть асинхронным. Окей, и мы видим, что мы получаем буквально тот же самый результат. Итак, с этим у нас есть всё, что нам нужно, чтобы построить полностью асинхронный инструмент Ser API, который мы делаем здесь для Langchain. Мы импортируем эти инструменты, и я имею в виду, есть ли здесь что-то другое? Нет, это именно то, что мы только что сказали, но я запущу это, потому что я хотел бы очень быстро показать вам это. Окей, поэтому так мы изначально вызывали наши инструменты в предыдущих главах, потому что мы были, окей, в основном согласны с использованием синхронных инструментов. Однако вы можете видеть, что функция здесь просто пуста, верно? Если я делаю тип просто не типа, это потому что, ну, это асинхронная функция. Окей, это асинхронный инструмент, извините. Поэтому он был определён с async здесь. Что происходит, когда вы делаете это, вы получаете этот объект корутины. Поэтому вместо функции, которой её здесь нет, вы получаете эту корутину. Если мы затем изменим это, что было бы неплохо, давайте просто удалим все async здесь и await. Если мы изменим это так, а затем посмотрим на инструмент set API structure, мы пройдёмся, мы увидим, что теперь мы получаем эту функцию. Окей, это просто разница между асинхронным структурированным инструментом и синхронным структурированным инструментом. Мы, конечно, на асинхронном. Окей, теперь у нас снова K. Важно помнить об этом, и, конечно же, мы запускаем, используя корутину sub API. Итак, вот как мы создаём инструмент sub API. Э-э, нет ничего, я имею в виду, это точно то, что мы сделали здесь, поэтому мне не нужно, я не думаю, что нам нужно проходить через это дальше. Итак, да, я думаю, что это в основном весь наш код за этим API. Со всем этим мы можем затем продолжить, поэтому наш API уже работает. Давайте продолжим и фактически запустим также наш фронтенд. Мы собираемся перейти в документы orelo line chain course, а затем мы хотим перейти в Chapters 09 Capstone app, и вам нужно будет установить npm. Чтобы сделать это, что мы делаем? Мы можем посмотреть на этот ответ, например, это, вероятно, то, что я бы порекомендовал. Окей, поэтому я бы запустил Brew install node, а затем Brew install mpm, если вы на Mac, конечно, это отличается, если вы на Linux или Windows. После того, как у вас есть они, вы можете сделать npm install, и это просто установит все oop, извините, mpm install, и это просто установит все пакеты node, которые нам нужны, и затем мы можем просто запустить npm run Dev. Окей, и теперь наше приложение работает на locost 3000. Поэтому мы можем перейти сюда, открыть это, и у нас есть наше приложение. Можно игнорировать это. Итак, здесь мы можем начать просто задавать вопросы. Окей, поэтому мы можем начать с быстрого вопроса: что такое 5 + 5? И вы видите, поэтому у нас происходит потоковая передача здесь. Он сказал, что агент хочет использовать инструмент ad, и это входные параметры для инструмента ad, а затем мы получаем потоковый ответ. Итак, это инструмент окончательного ответа, где мы выводим этот ключ и значение ответа, а затем здесь мы выводим этот ключ и значение используемых инструментов, который является просто массивом используемых инструментов, которые просто функции add. У нас это есть, затем давайте зададим другой вопрос. На этот раз мы запустим Ser API с: расскажи мне о последних новостях в мире. Окей, поэтому мы видим, что используется C API, а запрос — это последние мировые новости, а затем он спускается сюда, и мы фактически получаем здесь некоторые цитаты, что довольно круто. Вы также можете пройти сюда, окей, и он переходит сюда, так что это довольно круто. К сожалению, я просто потерял свой чат, так что хорошо, позвольте мне, я могу задать этот вопрос снова. Окей, мы видим, что использует set API там. Теперь давайте продолжим со следующим вопросом из нашего блокнота, который звучит: как холодно в Айлайт прямо сейчас? Что вы получаете при умножении этих двух чисел? Я просто изменю это на градусы Цельсия, чтобы я мог понять. Спасибо. Окей, для этого мы можем увидеть, что мы получили? Итак, мы получили текущую температуру в ow, мы умножили 5 на 5, что наш второй вопрос, а затем мы также вычли. Интересно, что я, я не знаю, почему он так сделал, это как-то странно. Итак, он, он решил использовать, а, окей. Итак, это, окей, это имеет смысл. Имеет ли это смысл примерно? Окей, поэтому я думаю, что преобразование для Фаренгейта Цельсия — это, скажем, вычесть 32. Окей, да, чтобы перейти от Фаренгейта к Цельсию, вы делаете в основном Фаренгейт минус 32, а затем вы умножаете на это число здесь, которое iume AI не, он примерно сделал. Окей, вычитание 36, например, 32, дало бы нам 4, и оно дало нам примерно 2. Поэтому если вы думаете, окей, умножьте на это, это практически умножение на 0,55, так что половина значения, и это дало бы нам примерно 2°. Вот что это делало здесь, довольно интересно. Окей, круто, поэтому мы прошли, мы увидели, как создать полнофункциональное чат-приложение, используя то, что мы узнали на протяжении курса, и мы создали довольно много. Если вы подумаете об этом приложении, вы получаете обновления в реальном времени о том, какие инструменты используются, параметры, передаваемые этим инструментам, и всё это возвращается в потоковом выводе и даже в структурированном выводе для вашего окончательного ответа, включая ответ и инструменты, которые мы используем. Поэтому, конечно, то, что мы создали здесь, довольно ограничено, но это очень легко расширить. Например, что-то, что вы, возможно, захотите сделать, это взять то, что мы построили здесь, например, форкнуть это приложение и просто добавить к нему разные инструменты и посмотреть, что произойдёт, потому что это очень расширяемо. Вы можете сделать с этим многое, но да, это конец курса. Конечно, это только начало того, что вы хотите изучить или построить с помощью ИИ. Рассматривайте это как начало и просто идите и найдите все другие интересные вещи, которые вы можете построить. Поэтому я надеюсь, что этот курс был полезным, информативным и даст вам преимущество в том, что вы собираетесь построить. Поэтому большое спасибо за просмотр и прохождение курса и за то, что вы дошли до конца. Я знаю, что это довольно долго, поэтому я очень ценю это, и я надеюсь, что вы получите из этого много пользы. Спасибо, до свидания.