Анализ вывода LLM вызов функции против цепи языков
Анализ вывода LLM функции против языков
Как постоянно разбирать выводы из LLM, используя Open AI API и функцию LangChain вызов: оценка преимуществ и недостатков методов

Создание инструментов с использованием LLM требует нескольких компонентов, таких как векторные базы данных, цепочки, агенты, разделители документов и многие другие новые инструменты.
Однако одним из самых важных компонентов является разбор вывода LLM. Если вы не можете получить структурированные ответы от своего LLM, вам будет трудно работать с генерациями. Это становится еще более очевидным, когда мы хотим, чтобы один вызов LLM выдавал более одной информации.
Давайте проиллюстрируем проблему на гипотетическом сценарии:
Мы хотим, чтобы LLM выдавал из одного вызова ингредиенты и шаги для приготовления определенного рецепта. Но мы хотим иметь эти элементы отдельно, чтобы использовать их в двух разных частях нашей системы.
- Преобразование генеративных задач в задачи классификации
- Как United Airlines создала эффективную по стоимости трубопровод активного обучения оптического распознавания символов
- Генеральный директор NVIDIA Дженсон Хуанг станет главным говорящим на AI-саммите в Тель-Авиве
import openairecipe = 'Рыба с жареными картофельными дольками'query = f"""Какой рецепт для {recipe}? Вернуть список ингредиентов и шаги отдельно."""response = openai.ChatCompletion.create( model="gpt-3.5-turbo-0613", messages=[{"role": "user", "content": query}])response_message = response["choices"][0]["message"]print(response_message['content'])
Это возвращает следующее:
Ингредиенты для рыбы с жареными картофельными дольками:- 1 фунт белой рыбы (такой как треска или пикша)- 1 чашка муки- 1 чайная ложка разрыхлителя- 1 чайная ложка соли- 1/2 чайной ложки черного перца- 1 чашка холодного пива- Растительное масло для жарки- 4 крупных картофелины- Соль по вкусуШаги для приготовления рыбы с жареными картофельными дольками:1. Разогрейте духовку до 200°C (400°F).2. Очистите картофель и нарежьте его на толстые равномерные полоски. Промойте полоски картофеля в холодной воде, чтобы удалить излишек крахмала. Высушите их с помощью чистого кухонного полотенца.3. В большой кастрюле или глубокой фритюрнице разогрейте растительное масло до 175°C (350°F). Убедитесь, что масла достаточно, чтобы полностью погрузить картофель и рыбу.4. В миске смешайте муку, разрыхлитель, соль и черный перец. Постепенно взбивайте холодное пиво, пока не получится гладкое тесто. Отложите тесто в сторону.5. Возьмите высушенные полоски картофеля и жарьте их партиями примерно 5-6 минут или до золотистого цвета. Вынимайте картофель фритюрницей с дырчатой ложкой и выкладывайте на бумажное полотенце, чтобы удалить избыточное масло. Держите картофель горячим в разогретой духовке.6. Обмакните каждый кусок рыбы в приготовленное тесто, убедившись, что он хорошо покрыт. Перед тем как аккуратно поместить кусок в горячее масло, удаляйте лишнее тесто.7. Жарьте рыбные филе с каждой стороны 4-5 минут или до золотистого цвета и хрустящего. Вынимайте их из масла с дырчатой ложкой и выкладывайте на бумажное полотенце, чтобы удалить избыточное масло.8. Посолите рыбу и картофель по желанию пока они горячие.9. Подавайте рыбу и картофель горячими с соусом тар-тар, яблочным уксусом или кетчупом по желанию.Приятного аппетита!
Это большая строка, и ее разбор будет сложным, потому что LLM может возвращать немного разные структуры, нарушая любой код, который вы напишете. Можно сказать, что запросить всегда возвращать “Ингредиенты:” и “Шаги:” в промпт-фразе может решить проблему, и вы не ошибетесь. Это может сработать, однако вам все равно придется обрабатывать строку вручную и быть готовым к возможным вариациям и галлюцинациям.
Решение
Существует несколько способов решить эту проблему. Один из них был упомянут выше, но есть несколько протестированных способов, которые могут быть лучше. В этой статье я покажу два варианта:
- Вызов функций Open AI;
- Разбор вывода LangChain.
Вызов функций Open AI
Это метод, который я пробовал и который дает наиболее последовательные результаты. Мы используем возможность вызова функций Open AI API, чтобы модель возвращала ответ в виде структурированного JSON.
Эта функциональность имеет целью предоставить LLM возможность вызывать внешнюю функцию, предоставляя входные данные в формате JSON. Модели были настроены для понимания, когда им необходимо использовать определенную функцию. Примером может служить функция для текущей погоды. Если вы спросите у GPT текущую погоду, он не сможет вам ответить, но вы можете предоставить функцию, которая это делает, и передать ее GPT, чтобы он знал, что она может быть использована при определенном вводе.
Если вы хотите погрузиться глубже в эту функциональность, вот объявление от Open AI и вот отличная статья.
Итак, давайте посмотрим на код и посмотрим, как это будет выглядеть в нашей конкретной задаче. Разберем код:
functions = [ { "name": "return_recipe", "description": "Возвращает запрошенный рецепт", "parameters": { "type": "object", "properties": { "ingredients": { "type": "string", "description": "Список ингредиентов." }, "steps": { "type": "string", "description": "Шаги рецепта." }, }, }, "required": ["ingredients","steps"], }]
Первое, что нам нужно сделать, это объявить функции, которые будут доступны LLM. Мы должны дать им имя и описание, чтобы модель понимала, когда использовать функцию. Здесь мы говорим, что эта функция используется для возврата запрошенного рецепта.
Затем мы переходим к параметрам. Сначала мы говорим, что это объект определенного типа, и у него могут быть свойства “ингредиенты” и “шаги”. Оба из них также имеют описание и тип, чтобы помочь LLM определить выходные данные. Наконец, мы указываем, какие из этих свойств являются обязательными для вызова функции (это означает, что у нас может быть необязательные поля, которые LLM будет оценивать, хочет ли он использовать их).
Давайте теперь воспользуемся этим в вызове LLM:
import openairecipe = 'Рыба с картофельными чипсами'query = f"Какой рецепт для {recipe}? Вернуть список ингредиентов и шаги отдельно."response = openai.ChatCompletion.create( model="gpt-3.5-turbo-0613", messages=[{"role": "user", "content": query}], functions=functions, function_call={'name':'return_recipe'})response_message = response["choices"][0]["message"]print(response_message)print(response_message['function_call']['arguments'])
Здесь мы начинаем с создания нашего запроса к API, форматируя базовый промпт с тем, что может быть переменным вводом (рецепт). Затем мы объявляем наш вызов API, используя “gpt-3.5-turbo-0613”, мы передаем наш запрос в аргументе messages, и теперь мы передаем наши функции.
Есть два аргумента, касающихся наших функций. Первым мы передаем список объектов в указанном выше формате с функциями, к которым модель имеет доступ. И второй аргумент “function_call” мы указываем, как модель должна использовать эти функции. Есть три варианта:
- “Авто” -> модель сама решает, вызывать ли функцию или использовать ответ пользователя;
- “none” -> модель не вызывает функцию и возвращает ответ пользователя;
- {“name”: “имя_моей_функции”} -> указание имени функции заставляет модель ее использовать.
Официальную документацию вы можете найти здесь.
В нашем случае и для использования в качестве вывода разбора мы использовали последний вариант:
function_call={'name':'return_recipe'}
Теперь мы можем посмотреть на наши ответы. Ответ, который мы получаем (после этого фильтра [“choices”][0][“message”]), выглядит следующим образом:
{ "role": "assistant", "content": null, "function_call": { "name": "return_recipe", "arguments": "{\n \"ingredients\": \"Для рыбы:\\n- 1 фунт белой рыбы\\n- 1 чашка пшеничной муки\\n- 1 ч.л. разрыхлителя\\n- 1 ч.л. соли\\n- 1/2 ч.л. черного перца\\n- 1 чашка холодной воды\\n- Растительное масло для жарки\\nДля чипсов:\\n- 4 больших картофелины\\n- Растительное масло для жарки\\n- Соль по вкусу\",\n \"steps\": \"1. Начните с приготовления рыбы. В мелкой тарелке смешайте муку, разрыхлитель, соль и черный перец.\\n2. Постепенно взбивайте холодную воду, пока тесто не станет гладким.\\n3. Разогрейте растительное масло в большой сковороде или глубоком жаровне.\\n4. Обмакните куски рыбы в тесто, равномерно покрывая их.\\n5. Осторожно поместите покрытые куски рыбы в горячее масло и жарьте по 4-5 минут с каждой стороны, пока они не станут золотистыми и хрустящими.\\n6. Выньте жареную рыбу из масла и выложите на тарелку, выстиланную бумажным полотенцем, чтобы удалить лишнее масло.\\n7. Для приготовления чипсов очистите картофель и нарежьте его крупными кусками.\\n8. Разогрейте растительное масло в глубокой жаровне или большой сковороде.\\n9. Жарьте чипсы порциями до золотистого цвета и хрустящести.\\n10. Выньте чипсы из масла и выложите на тарелку, выстиланную бумажным полотенцем, чтобы удалить лишнее масло.\\n11. Посолите чипсы по вкусу.\\n12. Подавайте рыбу и чипсы вместе и наслаждайтесь!" }}
Если мы разберем его дальше на “function_call”, то мы увидим наш предполагаемый структурированный ответ:
{ "ingredients": "Для рыбы:\n- 1 фунт белых рыбных филе\n- 1 чашка муки\n- 1 ч.л. разрыхлителя\n- 1 ч.л. соли\n- 1/2 ч.л. черного перца\n- 1 чашка холодной воды\n- Растительное масло для жарки\nДля чипсов:\n- 4 крупных картофеля\n- Растительное масло для жарки\n- Соль по вкусу", "steps": "1. Начните с приготовления рыбы. В мелкой посуде смешайте муку, разрыхлитель, соль и черный перец.\n2. Постепенно взбивайте холодную воду, пока тесто не станет гладким.\n3. Разогрейте растительное масло в большой сковороде или глубокой фритюрнице.\n4. Обмакните рыбные филе в тесто, равномерно покрывая его.\n5. Осторожно поместите покрытые филе в раскаленное масло и жарьте 4-5 минут с каждой стороны, или пока они не станут золотистыми и хрустящими.\n6. Выньте жареную рыбу из масла и выложите на бумажное полотенце, чтобы удалить лишнее масло.\n7. Для чипсов очистите картофель и нарежьте его толстыми чипсами.\n8. Разогрейте растительное масло в глубокой фритюрнице или большой сковороде.\n9. Жарьте чипсы пачками до золотистого цвета и хрустящести.\n10. Выньте чипсы из масла и выложите на бумажное полотенце, чтобы удалить лишнее масло.\n11. Посолите чипсы.\n12. Подавайте рыбу и чипсы вместе и наслаждайтесь!"}
Заключение по вызову функции
Возможно использование функции вызова непосредственно из Open AI API. Это позволяет нам получить ответ в формате словаря с одинаковыми ключами каждый раз при вызове LLM.
Использование этого довольно просто, вам просто нужно объявить объект функций, указав имя, описание и свойства, сосредоточенные на вашей задаче, но указав (в описании), что это должен быть ответ модели. Также, при вызове API, мы можем заставить модель использовать нашу функцию, что делает ее еще более последовательной.
Основным недостатком этого метода является то, что он не поддерживается всеми моделями LLM и API. Поэтому, если мы хотим использовать Google PaLM API, нам придется использовать другой метод.
Парсеры вывода LangChain
Одна из альтернатив, которую у нас есть, и которая не зависит от модели, это использование LangChain.
Сначала, что такое LangChain?
LangChain – это фреймворк для разработки приложений на основе языковых моделей.
Это официальное определение LangChain. Этот фреймворк был создан недавно и уже используется в качестве отраслевого стандарта для создания инструментов, основанных на LLM.
У него есть функциональность, которая отлично подходит для нашего случая использования, называемая “Парсеры вывода”. В этом модуле можно создать несколько объектов для возврата и разбора различных типов форматов из вызовов LLM. Он достигает этого, сначала объявляя формат и передавая его ввод в LLM. Затем он использует ранее созданный объект для разбора ответа.
Давайте разберем код:
from langchain.prompts import ChatPromptTemplatefrom langchain.output_parsers import ResponseSchema, StructuredOutputParserfrom langchain.llms import GooglePalm, OpenAIingredients = ResponseSchema( name="ingredients", description="Ингредиенты из рецепта, в виде строки.", )steps = ResponseSchema( name="steps", description="Шаги приготовления рецепта, в виде строки.", )output_parser = StructuredOutputParser.from_response_schemas( [ingredients, steps])response_format = output_parser.get_format_instructions()print(response_format)prompt = ChatPromptTemplate.from_template("Какой рецепт для {recipe}? Вернуть список ингредиентов и шаги отдельно. \n {format_instructions}")
Первое, что мы делаем здесь, это создаем нашу схему ответа, которая будет входом для нашего парсера. Мы создаем одну для ингредиентов и одну для шагов, каждая содержит имя, которое будет ключом словаря, и описание, которое будет указывать LLM на ответ.
Затем мы создаем наш StructuredOutputParser из этих схем ответов. Есть несколько способов сделать это с разными стилями парсеров. Посмотрите здесь, чтобы узнать больше о них.
Наконец, мы получаем наши инструкции по форматированию и определяем нашу подсказку, которая будет содержать название рецепта и инструкции по форматированию в качестве входных данных. Инструкции по форматированию выглядят так:
"""Вывод должен быть фрагментом кода в формате Markdown, отформатированным в следующей схеме, включая ведущие и завершающие "```json" и "```":```json{ "ingredients": string // Ингредиенты из рецепта, в виде уникальной строки. "steps": string // Шаги по приготовлению рецепта, в виде уникальной строки.} """
Теперь у нас осталось только вызвать API. Здесь я продемонстрирую как с использованием Open AI API, так и с использованием Google PaLM API.
llm_openai = OpenAI()llm_palm = GooglePalm()recipe = 'Рыба с чипсами'formated_prompt = prompt.format(**{"recipe":recipe, "format_instructions":output_parser.get_format_instructions()})response_palm = llm_palm(formated_prompt)response_openai = llm_openai(formated_prompt)print("PaLM:")print(response_palm)print(output_parser.parse(response_palm))print("Open AI:")print(response_openai)print(output_parser.parse(response_openai))
Как вы можете видеть, очень легко переключаться между моделями. Вся структура, определенная ранее, может использоваться точно так же для любых моделей, поддерживаемых LangChain. Мы также использовали один и тот же парсер для обеих моделей.
Это сгенерировало следующий вывод:
# PaLM:{'ingredients': '''- 1 стакан пшеничной муки\n- 1 чайная ложка разрыхлителя\n- 1/2 чайной ложки соли\n- 1/2 стакана холодной воды\n- 1 яйцо\n- 1 фунт белой рыбной филе, такой как треска или пикша\n- Растительное масло для жарки\n- 1 стакан соуса тартар\n- 1/2 стакана соли-меди\n- Ломтики лимона''', 'steps': '''1. В большой миске взбейте муку, разрыхлитель и соль.\n2. В отдельной миске взбейте яйцо и воду.\n3. Обмакните рыбные филе в яичную смесь, затем обваляйте их в муке.\n4. Разогрейте масло в глубокой фритюрнице или большой сковороде до 190 градусов по Фаренгейту (375 градусов Цельсия).\n5. Жарьте рыбные филе 3-5 минут с каждой стороны, или до золотисто-коричневого цвета и полного готовности.\n6. Выложите рыбные филе на бумажные полотенца, чтобы удалить лишнее масло.\n7. Подавайте рыбные филе немедленно с соусом тартар, солью-медью и ломтиками лимона.'''}# Open AI{'ingredients': '1 ½ фунта филе трески, нарезанное на 4 куска, 2 стакана пшеничной муки, 2 чайные ложки разрыхлителя, 1 чайная ложка соли, 1 чайная ложка свежемолотого черного перца, ½ чайной ложки чесночного порошка, 1 стакан пива (или воды), растительное масло для жарки, соус тартар для подачи', 'steps': '1. Предварительно нагрейте духовку до 200 градусов Цельсия (400 градусов по Фаренгейту) и выстелите противень пергаментной бумагой. 2. В большой миске смешайте муку, разрыхлитель, соль, перец и чесночный порошок. 3. Добавьте пиво и взбейте до получения густого теста. 4. Обмакните треску в тесто, покрывая со всех сторон. 5. Разогрейте около 5 см масла в большой кастрюле или сковороде на сильном огне. 6. Жарьте треску 3-4 минуты с каждой стороны, пока она не станет золотисто-коричневой. 7. Переложите треску на приготовленный противень и запекайте 5-7 минут. 8. Подавайте горячим с соусом тартар.'}
Заключение: Разбор вывода LangChain
Этот метод также очень хорош и имеет гибкость как основную характеристику. Мы создаем несколько структур, таких как Response Schema, Output Parser и Prompt Templates, которые можно легко объединить и использовать с разными моделями. Еще одним преимуществом является поддержка нескольких форматов вывода.
Основной недостаток заключается в передаче инструкций форматирования через промпт. Это может привести к случайным ошибкам и галлюцинациям. Один реальный пример был в этом конкретном случае, когда мне пришлось указать “как уникальную строку” в описании схемы ответа. Если я не указал это, модель возвращала список строк с шагами и инструкциями, что вызывало ошибку при разборе в Output Parser.
Заключение
Существует несколько способов использования парсера вывода в вашем приложении на основе LLM. Однако ваш выбор может измениться в зависимости от конкретной проблемы. Лично я предпочитаю следовать этой идее:
Я всегда использую парсер вывода, даже если у меня есть только один вывод от LLM. Это позволяет мне контролировать и указывать свои выходные данные. Если я работаю с Open AI, мой выбор – Function Calling, потому что он обладает наибольшим уровнем контроля и позволяет избежать случайных ошибок в производственном приложении. Однако, если я использую другую модель LLM или требуется другой формат вывода, мой выбор – LangChain, но с большим количеством тестирования выходных данных, чтобы составить промпт с наименьшим количеством ошибок.
Спасибо за чтение.
Полный код можно найти здесь.
Если вам нравится контент и вы хотите поддержать меня, вы можете угостить меня кофе:
Габриэль Кассимиро – это Data Scientist, который бесплатно делится контентом с сообществом
Я люблю поддерживать творцов!
www.buymeacoffee.com
Вот несколько других статей, которые могут вас заинтересовать:
Асинхронные вызовы для цепочек с Langchain
Как сделать, чтобы цепочки Langchain работали с асинхронными вызовами к LLM, ускоряя время выполнения последовательного долгого…
towardsdatascience.com
Решение среды Unity с помощью глубинного обучения с подкреплением
Проект от начала до конца с кодом реализации агента глубинного обучения с подкреплением на PyTorch.
towardsdatascience.com