Эх, помню времена, когда все мы восхищались простыми чат-ботами на основе больших языковых моделей! Напишешь запрос, получишь ответ — и вроде бы магия. Но потом наступает разочарование: модель не знает о последних событиях, путается в математике и вообще живёт в своём замкнутом мире тренировочных данных. И тут на сцену выходят агенты LangChain — те самые ребята, которые превращают статичную модель в настоящего цифрового помощника.
В чём же суть этого подхода? Представьте, что у вас есть не просто языковая модель, а целая система принятия решений, которая может использовать различные инструменты. Спросите у обычной модели "Какую музыку сейчас слушают больше всего?" — и она даст вам устаревший ответ на основе своих тренировочных данных. А агент сначала поймет, что нужно обратиться к актуальным данным, выберет подходящий инструмент (например, API музыкального сервиса), получит свежую информацию и только потом сформирует ответ.
Когда я впервые попробовал работать с LangChain агентами, меня поразила их способность к рассуждению и планированию. Агент не просто выдаёт заготовленный ответ — он анализирует задачу, разбивает её на шаги, выбирает инструменты и выполняет последовательность действий для достижения цели. Это принципиально новый уровень взаимодействия с ИИ. Возможно, вы спросите: "Почему именно сейчас агенты стали так важны?" Ответ прост — мы достигли точки, когда базовые возможности языковых моделей уже не вызывают восторга. Мы хотим большего: чтобы модель могла находить актуальную информацию, работать с кодом, выполнять вычисления, обращаться к базам данных и другим сервисам. И вся эта функциональность реализуется через агентную архитектуру.
При этом важно понимать, что агенты — это не просто обертка вокруг LLM. Это сложная система, включающая механизмы планирования, принятия решений, выбора инструментов и выполнения действий. Она требует тщательного проектирования и настройки, но результат стоит усилий — вы получаете по-настоящему полезного цифрового ассистента, а не просто генератор текста. В ходе своей работы я неоднакратно сталкивался с тем, что агенты решают проблемы, которые невозможно было решить с использованием "голой" языковой модели. Это и работа с реальным временем, и выполнение сложных математических расчетов, и обработка данных из нескольких источников.
Архитектура агентов в LangChain
Агенты в LangChain устроены хитрее, чем может показаться на первый взгляд. Это не просто обертка над языковой моделью, а многокомпонентная система, где каждая деталь имеет своё предназначение.
Компоненты системы: из чего собран агент
В основе любого LangChain агента лежит несколько ключевых компонентов, и понимание их взаимодействия критически важно для разработки эффективных приложений.
Прежде всего, это большая языковая модель (LLM). Она выступает мозгом всей системы, обеспечивая способности к пониманию контекста, рассуждению и генерации текста. В своих проектах я использую разные модели — от OpenAI GPT до Gemini Pro от Google. Выбор модели сильно влияет на способность агента к рассуждению, так что не стоит экономить на этом компоненте.
Второй критический элемент — инструменты (tools). Это те самые внешние функции, которые позволяют агенту взаимодействовать с миром за пределами своего контекстного окна. Инструментом может быть что угодно: калькулятор для математических операций, API поисковой системы, подключение к базе данных или даже кастомная Python-функция.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| from langchain_community.tools import DuckDuckGoSearchResults
from langchain.chains import LLMMathChain
from langchain.agents import Tool
# Создаем инструмент для поиска
search_tool = DuckDuckGoSearchResults()
# Создаем инструмент для математических вычислений
math_chain = LLMMathChain.from_llm(llm=my_llm)
math_tool = Tool.from_function(
name="Calculator",
func=math_chain.run,
description="Выполняет математические вычисления"
) |
|
Третий важный компонент — шаблоны промптов (prompt templates). Они определяют, как агент будет интерпретировать входные данные и формировать своё поведение. Хороший шаблон промпта — это, пожалуй, 50% успеха агента. Я сам неоднократно убеждался, что одно изменение в шаблоне может кардинально улучшить способность агента к рассуждению.
Четвертый компонент — память (memory). Она позволяет агенту хранить информацию о предыдущих взаимодействиях и использовать её для принятия решений. Без памяти агент будет каждый раз начинать с чистого листа, что сильно ограничивает его полезность в длительных взаимодействиях.
И, наконец, исполнитель агента (agent executor) — компонент, который координирует взаимодействие между всеми остальными частями системы. Он управляет жизненным циклом выполнения задачи, решает, когда вызывать инструменты, когда задействовать LLM для рассуждения и когда выдавать финальный результат.
Механизм принятия решений
То, что делает агентов по-настоящему особенными — это их способность самостоятельно принимать решения. В отличие от обычной цепочки, где каждый шаг предопределен, агент динамически выбирает свои действия.
Когда агенту поступает запрос, происходит нечто интересное. Вместо того чтобы сразу генерировать ответ, агент сначала рассуждает о задаче. Я называю это «внутренним диалогом» — модель спрашивает себя: «Что мне нужно узнать для ответа? Какой инструмент использовать? Каков следующий шаг?»
Взглянем на типичный цикл работы ReAct агента (Reasoning + Acting):
1. Получение запроса: пользователь задает вопрос или ставит задачу.
2. Анализ и рассуждение: агент анализирует запрос и решает, что делать дальше.
3. Выбор инструмента: если нужна дополнительная информация, агент выбирает подходящий инструмент.
4. Выполнение действия: агент запускает выбранный инструмент с подготовленным запросом.
5. Анализ результата: агент оценивает полученную информацию.
6. Принятие решения: агент решает, нужно ли еще что-то сделать или можно выдать ответ.
7. Итерация или завершение: агент либо возвращается к шагу 2 для продолжения работы, либо формирует финальный ответ.
На практике это выглядит примерно так:
| Code | 1
2
3
4
5
6
7
8
9
10
| Запрос: "Какую должность занимает Илон Маск в правительстве США?"
Внутренний диалог агента:
1. Я не уверен в текущей должности Илона Маска. Нужно проверить актуальную информацию.
2. Использую поисковик для получения последних данных.
3. Поиск: "Илон Маск должность правительство США"
4. Из результатов поиска видно, что Илон Маск возглавляет Департамент правительственной эффективности (DOGE).
5. Теперь у меня есть вся необходимая информация для ответа.
Ответ: "Илон Маск возглавляет Департамент правительственной эффективности (DOGE) в администрации президента США." |
|
Важнейшую роль в этом процессе играет шаблон промпта, который фактически программирует логику рассуждений агента. Для ReAct агентов типичный шаблон включает инструкции по пошаговому мышлению, описание доступных инструментов и указания, как формировать действия.
Жизненный цикл агента
Жизненный цикл агента начинается с его инициализации — момента, когда мы определяем модель, инструменты и другие компоненты. Вот как это выглядит в коде:
| Python | 1
2
3
4
5
6
7
8
| from langchain.agents import AgentType, initialize_agent
agent = initialize_agent(
tools=[search_tool, math_tool],
llm=my_llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
) |
|
После инициализации агент готов к работе. Каждый запрос запускает новый цикл обработки, в котором агент взаимодействует с инструментами, рассуждает и формирует ответ. Это продолжается до тех пор, пока не будет достигнут один из стоп-критериев: найден ответ, превышено максимальное число итераций или произошла ошибка. Интересная особенность работы агентов — они склонны к самокоррекции. Если агент получает неожиданный или нелогичный результат от инструмента, он может изменить свой подход. Я наблюдал случаи, когда агент понимал, что задал неправильный запрос к поисковику, и пробовал переформулировать его более эффективно.
В отличии от обычных чат-ботов, агенты способны выполнять многошаговые задачи с несколькими зависимыми действиями. Например, если вы спросите: "Какая погода в самом населенном городе Франции?", агент должен сначала определить самый населенный город (Париж), а затем узнать погоду именно в нем. Такая декомпозиция задачи — одно из ключевых преимуществ агентной архитектуры.
Отличия от обычных чат-ботов
Чем же агенты так радикально отличаются от обычных чат-ботов на базе LLM? Работая над десятками проектов, я выделил несколько фундаментальных различий, которые делают агентов незаменимыми для решения сложных задач.
Во-первых, целенаправленность. Чат-бот просто реагирует на ваш запрос и выдаёт ответ на основе своей тренировки. Агент же стремится решить поставленную задачу и способен спланировать путь к этому решению. Если обычная модель не знает ответа, она признается в незнании или попытается придумать что-то правдоподобное. Агент же активно ищет способы получить недостающую информацию.
Во-вторых, адаптивность. Агенты способны менять свою стратегию в зависимости от промежуточных результатов. Я как-то тестировал агента, который пытался найти определённую статистику. Первый поисковый запрос не дал результатов, и агент самостоятельно переформулировал вопрос, чтобы получить более точные данные.
В-третьих, мультиинструментальность. Чат-бот ограничен своей моделью, а агент использует арсенал инструментов. Помню случай, когда один запрос заставил агента последовательно использовать калькулятор, поисковик и преобразователь дат — это было впечатляюще!
Наконец, прозрачность процесса. Включив режим подробного вывода (verbose=True), вы можете наблюдать за рассуждениями агента в реальном времени. Это не только увлекательно, но и полезно для отладки.
| Python | 1
2
3
4
5
6
7
| # Создание агента с включенным режимом подробного вывода
verbose_agent = initialize_agent(
tools=[search_tool, math_tool],
llm=my_llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True # Включаем подробный вывод процесса рассуждений
) |
|
Правда, я заметил интересную особенность: агенты иногда "перемудривают" с рассуждениями. Бывает, что агент использует поисковик для вопроса, на который мог бы ответить напрямую. Это связано с тем, что граница между знаниями модели и необходимостью внешнего поиска не всегда чётко определена в промпте. На практике я часто настраиваю шаблоны промптов так, чтобы агент сначала пытался использовать свои знания, и только при недостаточной уверенности обращался к инструментам. Это экономит токены и ускоряет работу.
Важный нюанс в архитектуре агентов — баланс между автономностью и контролем. Чем больше свободы вы даёте агенту в выборе инструментов и действий, тем более гибким он становится, но возрастает и риск непредсказуемого поведения. В критически важных системах я предпочитаю более строгие ограничения, четко определяющие, какие инструменты и в каких ситуациях можно использовать.
LLM GPT Инструкция вернуть 1 слово Здравствуйте. Вот строка с инструкцией:
getStream('determine best category to which corresponds... Звёздочки в выводе LLM Модель - xtuner llava-llama int4 7B Q4_KM.
Подал изображение, в первый вывод только *******... Вывести всех свобоных агентов Здравствуйте.
Есть сайт... Создание exe файла из файла python для работы на компьютере, где нет Python В ходе работы использую python 3.8, библиотеку pyodbc, драйвер ODBC Driver 17 for SQL Server.
...
Создание первого агента
Теперь, когда мы разобрались с теорией и пониманием архитектуры, давайте замарать руки реальным кодом. Создание первого LangChain агента — это как сборка первого мотоцикла: сначала кажется сложным, но когда все детали встают на свои места, испытываешь настоящий восторг.
Выбор модели и настройка окружения
Прежде чем писать хоть строчку кода, нам нужно определиться с языковой моделью. Я перепробовал десятки разных LLM для своих агентов, и могу сказать, что выбор зависит от нескольких факторов:
1. Мощность рассуждений. Для агентов критически важна способность модели к логическому мышлению. GPT-4, Claude и Gemini Pro демонстрируют отличные результаты в этом аспекте.
2. Размер контекстного окна. Чем больше окно, тем лучше агент справляется со сложными многошаговыми задачами.
3. Стоимость. При активном использовании агента счета могут быстро расти.
4. Латентность. Некоторые модели генерируют ответы быстрее других, что важно для интерактивных приложений.
Я обычно начинаю с Gemini Pro от Google — эта модель предлагает хороший баланс между качеством и стоимостью. Вот как выглядит базовая установка и настройка окружения:
| Python | 1
2
3
4
5
6
7
8
9
10
11
| # Установка необходимых пакетов
[H2]pip install langchain langchain-google-genai duckduckgo-search[/H2]
from langchain_google_genai import ChatGoogleGenerativeAI
import os
# Установка ключа API
os.environ['GOOGLE_API_KEY'] = "ваш_API_ключ"
# Создание экземпляра модели
llm = ChatGoogleGenerativeAI(model="gemini-pro") |
|
Важный момент, который я обнаружил на практике — не все модели одинаково хорошо работают с агентами. Некоторые из них с трудом следуют инструкциям по использованию инструментов или не умеют правильно анализировать вывод. Поэтому если вы заметили, что агент ведет себя странно, попробуйте сменить базовую модель.
Инструменты — расширяем возможности агента
Мой первый агент был довольно примитивным — он умел только искать информацию в интернете. Но даже это уже давало огромное преимущество перед обычной LLM. Давайте создадим простой инструмент для поиска:
| Python | 1
2
3
4
| from langchain_community.tools import DuckDuckGoSearchResults
# Создаем инструмент поиска
search_tool = DuckDuckGoSearchResults() |
|
Я выбрал DuckDuckGo вместо Google, потому что он не требует API-ключа и отлично подходит для тестирования. В продакшене можно использовать и другие поисковые системы.
Теперь добавим калькулятор для математических операций:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| from langchain.chains import LLMMathChain
from langchain.agents import Tool
# Создаем цепочку для математических вычислений
math_chain = LLMMathChain.from_llm(llm=llm)
# Оборачиваем ее в инструмент
math_tool = Tool.from_function(
name="Calculator",
func=math_chain.run,
description="Используй этот инструмент для математических операций. Вводи только математические выражения."
) |
|
Обратите внимание на параметр description — это критически важная часть. Именно эту строку агент будет использовать, чтобы понять, когда и как применять инструмент. Я потратил немало времени, экспериментируя с описаниями инструментов, и могу сказать, что чем конкретнее и яснее описание, тем лучше работает агент.
Создание и запуск агента
Теперь, когда у нас есть модель и инструменты, мы можем создать агента:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| from langchain.agents import AgentType, initialize_agent
# Создаем агента
agent = initialize_agent(
tools=[search_tool, math_tool],
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True # Включаем подробный вывод
)
# Запускаем агента с вопросом
result = agent.invoke("Какой департамент возглавляет Илон Маск и сколько будет 234 * 15?")
print(result['output']) |
|
Параметр verbose=True особенно полезен при разработке — он позволяет видеть весь процесс мышления агента, включая выбор инструментов и промежуточные рассуждения.
Когда я впервые запустил подобный код, я был поражён тем, как агент самостоятельно разбил вопрос на две части, сначала воспользовался поиском, а затем калькулятором. Вот как выглядит примерный вывод:
| Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| > Entering new AgentExecutor chain...
Я должен ответить на два вопроса: 1) Какой департамент возглавляет Илон Маск, и 2) сколько будет 234 * 15.
Для первого вопроса мне нужно найти актуальную информацию о должности Илона Маска.
Action: DuckDuckGoSearchResults
Action Input: "Илон Маск департамент правительство США 2025"
Observation: [результаты поиска о том, что Илон Маск возглавляет Департамент правительственной эффективности (DOGE)]
Теперь я знаю, что Илон Маск возглавляет Департамент правительственной эффективности (DOGE).
Для второго вопроса мне нужно выполнить умножение.
Action: Calculator
Action Input: 234 * 15
Observation: 3510
Теперь я могу ответить на оба вопроса.
Final Answer: Илон Маск возглавляет Департамент правительственной эффективности (DOGE). А 234 * 15 = 3510.
> Finished chain. |
|
Шаблоны промптов — секретный ингредиент
Если вы когда-нибудь задумывались, что происходит за кулисами функции initialize_agent(), то ответ — она создаёт шаблон промпта, который управляет поведением агента. Этот шаблон включает инструкции по мышлению, правила использования инструментов и формат вывода. В простых случаях стандартные шаблоны работают отлично, но для более сложных сценариев я предпочитаю создавать собственные. Вот пример кастомного шаблона промпта для агента:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| from langchain.prompts import PromptTemplate
# Создаем шаблон промпта
template = """Ты - полезный ассистент, который отвечает на вопросы, используя доступные инструменты.
Доступные инструменты:
{tools}
Используй следующий формат:
Вопрос: вопрос пользователя
Мысли: подумай, как лучше ответить на вопрос
Действие: название инструмента (одно из [{tool_names}])
Входные данные: данные для инструмента
Наблюдение: результат действия
... (повторяй Мысли/Действие/Входные данные/Наблюдение сколько нужно)
Мысли: теперь я знаю финальный ответ
Финальный ответ: ответ на вопрос пользователя
Вопрос: {input}
Мысли:"""
prompt = PromptTemplate.from_template(template) |
|
Затем этот шаблон можно передать при создании агента:
| Python | 1
2
3
4
5
| from langchain.agents import create_react_agent
# Создаем агента с кастомным промптом
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) |
|
На своём опыте могу сказать, что тонкая настройка промптов — это искусство, которое дает огромные преимущества. Например, добавление фразы "Сначала проверь, можешь ли ты ответить на вопрос, используя свои знания" может значительно ускорить работу агента для простых вопросов.
Токенизация и контекстное окно
Работая с агентами, я быстро понял, что размер контекстного окна становится критически важным. В отличие от обычной LLM, агент постоянно накапливает контекст: входной запрос, рассуждения, результаты инструментов — всё это должно уместиться в контекстное окно. Вот пример проблемы, с которой я столкнулся: агент выполнил несколько поисковых запросов, каждый из которых вернул большой объем текста. В какой-то момент контекстное окно переполнилось, и агент начал терять предыдущие шаги рассуждений.
Чтобы решить эту проблему, я применяю несколько подходов:
1. Лимитирование вывода инструментов. Например, для поисковика можно ограничить количество результатов и их длину:
| Python | 1
| search_tool = DuckDuckGoSearchResults(num_results=3, snippets_count=3) |
|
2. Использование моделей с большим контекстным окном. Модели вроде Claude 2 или GPT-4 Turbo могут работать с гораздо большими контекстами.
3. Подход "разделяй и властвуй". Для сложных задач лучше разбить процесс на несколько агентов, каждый из которых отвечает за свою часть.
Важно также учитывать, что токенизация разных моделей работает по-разному. То, что занимает 100 токенов в GPT-4, может занять совсем другое количество в Gemini или Claude. Я обычно оставляю некоторый запас прочности, чтобы избежать неожиданных проблем при смене модели.
Интеграция с внешними API
Один из самых интересных аспектов работы с агентами — возможность интеграции с любыми внешними API. В одном из своих проектов я создал агента, который мог проверять погоду, курсы валют и даже управлять умным домом.
Вот простой пример создания кастомного инструмента для получения текущей погоды:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import requests
def get_weather(location):
"""Получает текущую погоду для указанного местоположения."""
api_key = "ваш_ключ_API_погоды"
url = f"https://api.weatherapi.com/v1/current.json?key={api_key}&q={location}"
response = requests.get(url)
data = response.json()
return f"Погода в {location}: {data['current']['temp_c']}°C, {data['current']['condition']['text']}"
# Создаем инструмент
weather_tool = Tool.from_function(
name="WeatherInfo",
func=get_weather,
description="Получает текущую погоду для указанного местоположения. Вводи название города или региона."
) |
|
Теперь мы можем добавить этот инструмент к нашему агенту:
| Python | 1
2
3
4
5
6
| agent = initialize_agent(
tools=[search_tool, math_tool, weather_tool],
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
) |
|
С таким агентом можно задавать вопросы вроде "Какая сейчас погода в столице Франции?", и он правильно определит, что нужно использовать инструмент погоды для Парижа.
Конечно, создание инструментов — это только часть работы. Необходимо также обеспечить правильную обработку ошибок, валидацию входных данных и другие аспекты надежного программирования. Я обычно оборачиваю вызовы внешних API в блоки try-except и добавляю подробные сообщения об ошибках, чтобы агент мог корректно реагировать на проблемы.
Я не могу не поделиться одним хитрым приёмом, который обнаружил после нескольких месяцев работы с агентами. Вместо того чтобы использовать стандартные шаблоны инструментов, можно создать "композитные инструменты", которые последовательно выполняют несколько операций. Например, инструмент для анализа акций может сначала получить текущую цену, затем исторические данные и, наконец, провести простой анализ — всё в рамках одного вызова.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| def analyze_stock(ticker):
"""Получает и анализирует данные акции."""
# Получаем текущую цену
current_price = get_current_price(ticker)
# Получаем исторические данные
historical_data = get_historical_data(ticker)
# Проводим анализ
analysis = perform_simple_analysis(current_price, historical_data)
return f"Анализ акции {ticker}: {analysis}"
stock_tool = Tool.from_function(
name="StockAnalyzer",
func=analyze_stock,
description="Анализирует данные акции по её тикеру (например, AAPL для Apple)."
) |
|
Такой подход сокращает количество шагов, которые должен выполнить агент, и упрощает общий процесс.
Обработка ошибок и граничные случаи
В идеальном мире агенты всегда получают корректные данные и всегда правильно их интерпретируют. Но реальность, как вы понимаете, совсем другая. Я потратил бессчетное количество часов, отлаживая странное поведение агентов в нестандартных ситуациях. Вот несколько типичных проблем и способы их решения:
1. Некорректные входные данные для инструментов. Агент может передать инструменту данные в неожиданном формате. Решение — тщательная валидация:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| def get_weather(location):
"""Получает текущую погоду для указанного местоположения."""
# Валидация входных данных
if not location or not isinstance(location, str):
return "Ошибка: укажите корректное местоположение в виде строки."
try:
api_key = "ваш_ключ_API_погоды"
url = f"https://api.weatherapi.com/v1/current.json?key={api_key}&q={location}"
response = requests.get(url)
response.raise_for_status() # Вызовет исключение при HTTP-ошибках
data = response.json()
return f"Погода в {location}: {data['current']['temp_c']}°C, {data['current']['condition']['text']}"
except requests.exceptions.RequestException as e:
return f"Ошибка при запросе погоды: {str(e)}"
except (KeyError, ValueError) as e:
return f"Ошибка при обработке данных погоды: {str(e)}" |
|
2. Зацикливание агента. Иногда агент может начать повторять одни и те же действия без прогресса. Для этого я использую ограничение на максимальное количество шагов:
| Python | 1
2
3
4
5
6
| agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=10 # Ограничиваем число итераций
) |
|
3. Неправильный выбор инструмента. Агент может выбрать неподходящий инструмент для задачи. Улучшение описаний инструментов и промпта агента обычно помогает решить эту проблему.
Однажды я работал над агентом для сервиса поддержки, и он постоянно пытался использовать поисковик вместо базы знаний. Проблема решилась после того, как я переписал описание инструмента базы знаний, сделав его более конкретным и добавив примеры использования.
Отладка агентов
Отладка агентов — это отдельное искусство. Благодаря параметру verbose=True мы можем видеть все этапы рассуждений, но иногда этого недостаточно. Я разработал несколько техник, которые помогают эффективно отлаживать агентов:
1. Логирование всех промежуточных результатов. Создайте обертки для инструментов, которые записывают все входные и выходные данные:
| Python | 1
2
3
4
5
6
7
8
9
10
11
| def logged_tool(func, name):
def wrapper(*args, **kwargs):
print(f"[DEBUG] Вызов инструмента {name}")
print(f"[DEBUG] Аргументы: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"[DEBUG] Результат: {result[:100]}...") # Показываем только начало для длинных результатов
return result
return wrapper
# Оборачиваем инструмент в логгер
weather_tool.func = logged_tool(weather_tool.func, weather_tool.name) |
|
2. Пошаговое тестирование. Вместо того чтобы сразу давать агенту сложную задачу, разбейте её на простые подзадачи и проверьте каждую отдельно.
3. Анализ промптов. Иногда полезно увидеть точный промпт, который генерируется для LLM:
| Python | 1
2
3
4
5
6
7
| from langchain.callbacks import get_openai_callback
# Фиксируем все запросы к OpenAI
with get_openai_callback() as cb:
result = agent.invoke("Ваш запрос")
print(f"Токенов использовано: {cb.total_tokens}")
print(f"Стоимость: ${cb.total_cost}") |
|
Продвинутые функции агентов
Когда базовый агент уже работает, можно добавить продвинутые функции, которые сделают его ещё полезнее:
1. Человек в контуре (Human-in-the-loop). Позволяет агенту обращаться к человеку, когда он не уверен:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| from langchain.tools import HumanInputRun
human_input_tool = HumanInputRun()
tools = [search_tool, math_tool, human_input_tool]
agent = initialize_agent(
tools=tools,
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
) |
|
2. Параллельные вызовы инструментов. В некоторых ситуациях агент может выполнить несколько независимых действий одновременно:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| from langchain.agents import Tool, AgentExecutor
from langchain.agents.agent_toolkits import create_spark_dataframe_agent
from langchain.agents.agent_types import AgentType
from langchain.memory import ConversationBufferMemory
# Создаем память для агента
memory = ConversationBufferMemory(memory_key="chat_history")
# Включаем возврат промежуточных шагов
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
memory=memory,
verbose=True,
return_intermediate_steps=True # Это позволит нам анализировать шаги
) |
|
3. Кэширование результатов инструментов. Для инструментов, результаты которых не меняются часто (например, математические вычисления), имеет смысл использовать кэширование:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| from functools import lru_cache
@lru_cache(maxsize=100)
def cached_math_operation(expression):
# Вычисление математического выражения
return eval(expression)
# Создаем инструмент с кэшированием
cached_math_tool = Tool.from_function(
name="CachedCalculator",
func=cached_math_operation,
description="Выполняет математические вычисления с кэшированием результатов."
) |
|
Я заметил, что кэширование может существенно ускорить работу агента, особенно если одни и те же вычисления повторяются в разных запросах.
Работая с агентами, помните, что их настройка — это итеративный процесс. Не ожидайте идеальных результатов с первой попытки. Тщательно тестируйте агента на разных сценариях, анализируйте его рассуждения и постепенно улучшайте промпты и инструменты.
Кстати, в своих проектах я часто создаю целый набор тестовых случаев с ожидаемыми результатами и автоматически прогоняю их после каждого значительного изменения. Это помогает убедиться, что улучшения в одной области не привели к регрессии в другой.
Типы агентов и их применение
Правильный выбор типа агента может существенно повлиять на эффективность вашего решения, поэтому давайте рассмотрим основные виды и разберемся, когда их лучше использовать.
Zero-shot ReAct Agent для универсальных задач
Zero-shot ReAct — это, пожалуй, самый универсальный тип агента, с которым я начинаю большинство своих проектов. Его главная особенность в том, что он не требует примеров для понимания, как использовать инструменты — отсюда и название "zero-shot" (без примеров).
| Python | 1
2
3
4
5
6
7
8
| from langchain.agents import AgentType, initialize_agent
zero_shot_agent = initialize_agent(
tools=[search_tool, math_tool],
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
) |
|
Ключевое слово здесь — ReAct (Reasoning + Acting). Этот агент сначала рассуждает о проблеме, затем действует, анализирует результат и продолжает этот цикл до получения ответа. Помню, как я создавал информационную систему для юридической фирмы — Zero-shot ReAct прекрасно справился с поиском и анализом законодательных актов, даже не будучи специально обученным для юридической сферы. Его гибкость поражает!
Conversational ReAct для диалоговых систем
Если вы разрабатываете чат-бота или любую другую систему, требующую поддержания контекста диалога, то Conversational ReAct — ваш выбор.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| from langchain.memory import ConversationBufferMemory
from langchain.agents import AgentType, initialize_agent
memory = ConversationBufferMemory(memory_key="chat_history")
conversational_agent = initialize_agent(
tools=[search_tool, math_tool],
llm=llm,
agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION,
memory=memory,
verbose=True
) |
|
Главное отличие от Zero-shot — наличие памяти, которая хранит историю взаимодействий. Это позволяет агенту понимать контекст и отвечать на вопросы типа "А что насчет второго варианта?" или "Почему ты так считаешь?".
В одном из проектов я создавал ассистента для подбора инвестиционного портфеля. Без Conversational ReAct пользователю пришлось бы каждый раз повторять свои предпочтения и ограничения. С ним же система "запоминала" все детали и строила рекомендации на основе всего контекста беседы.
Self-ask with search для сложных вопросов
Self-ask with search — интересный подход, который заставляет агента разбивать сложные вопросы на подвопросы и последовательно отвечать на них с помощью поиска.
| Python | 1
2
3
4
5
6
7
| from langchain.agents.agent_toolkits import create_self_ask_with_search_agent
self_ask_agent = create_self_ask_with_search_agent(
llm=llm,
search_tool=search_tool,
verbose=True
) |
|
Этот тип агента особенно хорош для многоступенчатых запросов типа "Какая средняя температура в столице страны, где был изобретен первый компьютер?". Агент сначала выяснит, где был изобретен первый компьютер, определит столицу этой страны, а затем найдет информацию о температуре. Правда, есть один нюанс — Self-ask лучше работает с мощными моделями вроде GPT-4, которые хорошо справляются с декомпозицией задач. С более слабыми моделями результаты могут разочаровать.
ReAct + DocStore агенты для работы с большими документами
Когда вам нужно работать с набором документов или базой знаний, пригодятся агенты, интегрированные с хранилищем документов.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from langchain.agents import Tool
from langchain.agents.agent_toolkits import create_retrieval_agent
# Создаем инструмент для работы с документами
retriever = your_vector_db.as_retriever()
retrieval_tool = Tool(
name="DocSearch",
func=retriever.get_relevant_documents,
description="Ищет информацию в базе документов. Используй это для поиска конкретных фактов."
)
# Создаем агента для работы с этим инструментом
retrieval_agent = create_retrieval_agent(
llm=llm,
tools=[retrieval_tool],
verbose=True
) |
|
Я использовал такой подход для создания системы поддержки врачей, которая могла анализировать медицинские протоколы и научные статьи. Агент искал в базе документов информацию о похожих случаях и предоставлял врачу релевантные данные для принятия решений.
Plan-and-execute агенты для долгосрочного планирования
Для сложных задач, требующих четкого планирования, я предпочитаю использовать Plan-and-execute агентов. Они работают в два этапа: сначала создают план действий, а потом последовательно его выполняют.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| from langchain.agents import load_tools
from langchain.agents.agent_toolkits import create_plan_and_execute_agent
# Загружаем набор стандартных инструментов
tools = load_tools(["serpapi", "llm-math"], llm=llm)
# Создаем агента с планированием
planner_agent = create_plan_and_execute_agent(
llm=llm,
tools=tools,
verbose=True
) |
|
Такой подход особенно эффективен для сложных аналитических задач. Например, я использовал Plan-and-execute агента для анализа финансовой отчетности компаний. Агент сначала планировал, какие показатели нужно извлечь и как их проанализировать, а затем последовательно выполнял этот план.
Минус данного подхода — он может быть медленнее из-за двухэтапного процесса, но качество результатов часто оправдывает затраченное время.
Structured chat агенты для форматированного вывода
Если вам нужно получать ответы в строго определенном формате (JSON, XML или любой другой), то стоит обратить внимание на Structured chat агентов.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| from langchain.agents import AgentType, initialize_agent
from langchain.prompts import StructuredChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from typing import List
# Определяем структуру ответа
class MovieRecommendation(BaseModel):
title: str = Field(description="Название фильма")
year: int = Field(description="Год выпуска")
rating: float = Field(description="Рейтинг от 0 до 10")
reasons: List[str] = Field(description="Причины рекомендации")
# Создаем агента с структурированным выводом
structured_agent = initialize_agent(
tools=[search_tool],
llm=llm,
agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
) |
|
Я активно использую такой подход в проектах, где результаты работы агента должны напрямую подаваться в другие системы. Например, для рекомендательной системы онлайн-кинотеатра агент выдавал рекомендации в формате JSON, который сразу обрабатывался фронтендом.
OpenAI Functions агенты для структурированного взаимодействия
Если вы работаете с моделями OpenAI, то можете воспользоваться специальными агентами, оптимизированными под функционал OpenAI Functions.
| Python | 1
2
3
4
5
6
7
8
| from langchain.agents import AgentType, initialize_agent
openai_functions_agent = initialize_agent(
tools=[search_tool, math_tool],
llm=llm,
agent=AgentType.OPENAI_FUNCTIONS,
verbose=True
) |
|
Этот тип агентов использует встроенную способность новых моделей OpenAI вызывать функции, что делает взаимодействие более структурированным и предсказуемым. Лично я заметил, что такие агенты реже ошибаются при выборе инструмента и формировании запроса к нему.
Однако, есть и обратная сторона — они работают только с моделями OpenAI, поддерживающими функции, что ограничивает возможности выбора базовой модели.
Custom Tool агенты для специализированных задач
Иногда стандартные типы агентов не подходят для специфических задач. В таких случаях я создаю кастомных агентов, настраивая каждый аспект их поведения.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| from langchain.agents import ZeroShotAgent, AgentExecutor
from langchain.memory import ConversationBufferMemory
# Создаем шаблон промпта для кастомного агента
prefix = """Ты - эксперт по анализу данных. Используй следующие инструменты для анализа данных клиентов:"""
suffix = """Начинай!
Вопрос: {input}
{agent_scratchpad}"""
# Определяем промпт для агента
prompt = ZeroShotAgent.create_prompt(
tools=[data_analysis_tool, visualization_tool],
prefix=prefix,
suffix=suffix,
input_variables=["input", "agent_scratchpad"]
)
# Создаем агента с кастомным промптом
agent = ZeroShotAgent(llm_chain=LLMChain(llm=llm, prompt=prompt), tools=[data_analysis_tool, visualization_tool])
agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=[data_analysis_tool, visualization_tool], verbose=True) |
|
Такой подход я использовал для создания специализированного аналитического агента, который помогал маркетологам анализировать данные о кампаниях. Кастомный промпт содержал специфические инструкции по интерпретации маркетинговых метрик и визуализации результатов.
Multi-agent системы для параллельного выполнения задач
Самый продвинутый подход, который я использую для особо сложных проектов — создание систем из нескольких взаимодействующих агентов, каждый из которых отвечает за свою область.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # Создаем агента для поиска информации
research_agent = initialize_agent(
tools=[search_tool],
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
# Создаем агента для анализа данных
analysis_agent = initialize_agent(
tools=[math_tool, data_analysis_tool],
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
# Создаем агента-координатора
def coordinator(task):
"""Распределяет задачи между специализированными агентами"""
if "найди" in task.lower() or "поиск" in task.lower():
return research_agent.run(task)
elif "анализ" in task.lower() or "рассчитай" in task.lower():
return analysis_agent.run(task)
else:
# Если не ясно, какой агент нужен, используем эвристику или спрашиваем у модели
task_type = llm.predict(f"Определи тип задачи: '{task}'. Это поиск информации или анализ данных?")
if "поиск" in task_type.lower():
return research_agent.run(task)
else:
return analysis_agent.run(task) |
|
Такой мульти-агентный подход я успешно применил при создании аналитической платформы для финтех-стартапа. Там был агент для сбора данных с финансовых API, агент для анализа временных рядов, агент для визуализации и агент-координатор, который интегрировал все результаты в итоговый отчет. Пользователи взаимодействовали только с координатором, не подозревая о сложной системе за кулисами.
Практические рекомендации по выбору типа агента
После нескольких лет экспериментов с разными типами агентов, я выработал несколько практических правил для их выбора:
1. Для новичков и быстрого прототипирования: Zero-shot ReAct — отличная отправная точка благодаря своей универсальности.
2. Для разговорных интерфейсов: Conversational ReAct с хорошо настроенной памятью обеспечивает естественное взаимодействие.
3. Для аналитических задач: Plan-and-execute дает наиболее структурированные и продуманные результаты.
4. Для интеграции с API и сервисами: OpenAI Functions или Structured Chat агенты обеспечивают предсказуемый формат вывода.
5. Для работы с документами: ReAct + DocStore или специализированный агент с векторной базой данных.
6. Для сложных проектов: Многоагентная система, где каждый агент решает специализированную подзадачу.
Кстати, в одном проекте я совершил типичную ошибку новичка — использовал Conversational агента для задачи, где не требовалось сохранение контекста диалога. Это привело к ненужному раздуванию промптов и замедлению работы. Помните: более сложный агент не всегда означает лучший результат.
Сравнение производительности разных типов агентов
Я провел небольшое исследование, сравнив различные типы агентов на стандартном наборе задач. Результаты оказались довольно интересными:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # Функция для оценки производительности агента
def evaluate_agent(agent, tasks, metrics=["accuracy", "steps", "time"]):
results = {}
for metric in metrics:
results[metric] = []
for task in tasks:
start_time = time.time()
response = agent.invoke(task)
end_time = time.time()
# Оцениваем точность (это субъективная метрика в реальности)
accuracy = evaluate_accuracy(response, task)
# Количество шагов (для агентов с verbose=True)
steps = len(response.get("intermediate_steps", []))
results["accuracy"].append(accuracy)
results["steps"].append(steps)
results["time"].append(end_time - start_time)
# Возвращаем средние значения
return {k: sum(v)/len(v) for k, v in results.items()} |
|
Самые неожиданные выводы:
1. Zero-shot ReAct оказался самым быстрым, но уступал в точности Plan-and-execute агентам.
2. Conversational агенты требовали больше всего токенов из-за хранения истории.
3. Self-ask with search показал лучшие результаты для многоступенчатых вопросов, но был медленнее других.
4. OpenAI Functions агенты делали меньше ошибок при выборе инструментов.
Эти результаты помогли мне выработать более обоснованный подход к выбору агента для конкретной задачи. Если критична скорость — Zero-shot ReAct, если важна точность для сложных вопросов — Self-ask или Plan-and-execute.
Динамическое переключение между агентами
Одна из самых интересных техник, которую я разработал в процессе работы — динамическое переключение между типами агентов в зависимости от задачи. Это позволяет использовать преимущества разных подходов.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| def dynamic_agent_selector(query, llm):
"""Выбирает подходящий тип агента на основе анализа запроса"""
# Анализируем запрос с помощью LLM
analysis = llm.predict(
f"""Проанализируй запрос и определи, какой тип агента лучше использовать:
Запрос: {query}
1. Если это простой фактический вопрос - "basic"
2. Если это многоступенчатый сложный вопрос - "self_ask"
3. Если это диалог, требующий контекста - "conversational"
4. Если это задача, требующая планирования - "planner"
Верни только одно слово - тип агента."""
)
# Создаем соответствующий агент
if "basic" in analysis.lower():
return initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION)
elif "self_ask" in analysis.lower():
return create_self_ask_with_search_agent(llm, search_tool)
elif "conversational" in analysis.lower():
memory = ConversationBufferMemory(memory_key="chat_history")
return initialize_agent(tools, llm, agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION, memory=memory)
elif "planner" in analysis.lower():
return create_plan_and_execute_agent(llm, tools)
else:
# По умолчанию используем универсальный агент
return initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION) |
|
Этот подход я применил в проекте виртуального ассистента для образовательной платформы. Когда студент задавал простой вопрос по материалу, система использовала базовый агент. Когда требовалось решить сложную задачу — включался агент с планированием. А для поддержания диалога использовался conversational агент.
Гибридные агенты и кастомные расширения
Стандартные типы агентов не всегда полностью соответствуют требованиям проекта. В таких случаях я создаю гибридные решения, комбинируя лучшие аспекты разных типов. Например, для проекта в сфере медицины я разработал гибрид Plan-and-execute и DocStore агентов. Он сначала планировал процесс диагностики, а затем для каждого шага плана искал информацию в медицинской базе знаний.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| # Создаем инструменты для работы с документами
retriever = your_medical_db.as_retriever()
doc_tool = Tool(
name="MedicalKnowledge",
func=retriever.get_relevant_documents,
description="Ищет информацию в медицинской базе знаний."
)
# Создаем кастомный промпт для планирования
planning_prompt = """Ты - медицинский ИИ-ассистент.
Составь план диагностики для пациента с описанными симптомами.
План должен содержать шаги, каждый с конкретным диагностическим вопросом или проверкой.
Симптомы: {input}
План:"""
# Функция планирования
def create_diagnostic_plan(symptoms):
return llm.predict(planning_prompt.format(input=symptoms))
# Функция выполнения плана с поиском информации
def execute_diagnostic_step(step):
# Ищем информацию для этого шага
documents = doc_tool.run(step)
# Анализируем найденную информацию
analysis = llm.predict(f"Шаг диагностики: {step}\n\nИнформация: {documents}\n\nАнализ:")
return analysis
# Основная функция диагностики
def medical_diagnosis(symptoms):
# Создаем план
plan = create_diagnostic_plan(symptoms).split("\n")
results = []
# Выполняем каждый шаг плана
for step in plan:
if step.strip():
step_result = execute_diagnostic_step(step)
results.append(f"- {step}: {step_result}")
# Формируем итоговое заключение
final_diagnosis = llm.predict(f"""Симптомы: {symptoms}
Результаты диагностики:
{''.join(results)}
Предположительный диагноз и рекомендации:""")
return final_diagnosis |
|
Такой подход позволил создать специализированную систему, которая сочетала структурированность Plan-and-execute агентов с информационным поиском DocStore агентов.
Ограничения и вызовы при работе с агентами
Работая с разными типами агентов, я столкнулся с рядом ограничений, о которых стоит знать:
1. Галлюцинации в рассуждениях. Агенты иногда "придумывают" несуществующие ограничения или возможности инструментов. Например, один агент постоянно пытался использовать несуществующий параметр в инструменте калькулятора.
2. Зацикливание. Без правильно настроенных стоп-критериев агент может начать повторять одни и те же действия. Я однажды наблюдал, как агент пять раз подряд искал в Google одну и ту же информацию, слегка переформулируя запрос.
3. Проблема "последней мили". Иногда агент собирает всю необходимую информацию, но формирует неверный итоговый ответ. Это особенно заметно в математических задачах, где агент может правильно выполнить все промежуточные вычисления, но ошибиться в финальном результате.
4. Неоптимальное использование инструментов. Агенты не всегда выбирают самый эффективный инструмент или последовательность действий. Например, агент может выполнить несколько поисковых запросов, когда достаточно было одного.
Для борьбы с этими ограничениями я применяю несколько техник:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| # Предотвращение зацикливания
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=10, # Ограничиваем число итераций
max_execution_time=60, # Ограничиваем время выполнения в секундах
early_stopping_method="generate" # Генерируем ответ, если достигнут лимит
)
# Валидация финального ответа
def validate_final_answer(agent_output):
"""Проверяет финальный ответ агента на соответствие собранной информации"""
steps = agent_output.get("intermediate_steps", [])
final_answer = agent_output.get("output", "")
# Собираем всю информацию из промежуточных шагов
collected_info = "\n".join([f"{action}: {observation}" for action, observation in steps])
# Проверяем соответствие финального ответа собранной информации
validation = llm.predict(f"""
Собранная информация:
{collected_info}
Финальный ответ:
{final_answer}
Проверь, соответствует ли финальный ответ собранной информации.
Если есть несоответствия или ошибки, напиши исправленный ответ.
Если ответ корректен, напиши "Ответ корректен".
""")
if "ответ корректен" in validation.lower():
return final_answer
else:
return validation |
|
Эти техники значительно повышают надежность агентов, особенно для критически важных приложений.
Подводя итог обзору типов агентов, хочу подчеркнуть, что выбор конкретного типа должен основываться на специфике задачи, требованиях к формату вывода, необходимости поддержания контекста и доступных вычислительных ресурсах. Не существует универсального агента, идеального для всех сценариев, поэтому важно экспериментировать и комбинировать разные подходы.
Продвинутые возможности
Я уже много лет работаю с языковыми моделями, и если есть что-то, что я точно усвоил — так это то, что разница между посредственным и выдающимся агентом лежит именно в продвинутых возможностях. Базовая реализация, которую мы рассмотрели ранее, может решать простые задачи, но для реальных производственных сценариев требуется куда больше.
Кастомные инструменты — ваш секретный козырь
Стандартные инструменты LangChain покрывают базовые потребности, но настоящая магия начинается, когда вы создаёте собственные инструменты. За годы работы я разработал десятки специализированных инструментов — от парсеров финансовой отчётности до систем мониторинга серверов. Создание кастомного инструмента начинается с понимания того, какую именно функциональность вы хотите добавить агенту. Вот пример инструмента для работы с API погоды, но с более продвинутой обработкой ошибок и валидацией:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| from langchain.agents import Tool
from pydantic import BaseModel, Field
from typing import Optional
import requests
class WeatherParams(BaseModel):
"""Параметры для запроса погоды."""
location: str = Field(..., description="Город или координаты")
days: Optional[int] = Field(1, description="Количество дней для прогноза")
class WeatherAPI:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.weatherapi.com/v1"
def get_weather(self, params: WeatherParams):
"""Получает информацию о погоде с валидацией и обработкой ошибок."""
try:
# Валидация параметров
if params.days < 1 or params.days > 10:
return "Ошибка: количество дней должно быть от 1 до 10."
url = f"{self.base_url}/forecast.json"
response = requests.get(url, params={
"key": self.api_key,
"q": params.location,
"days": params.days
}, timeout=5)
if response.status_code == 400:
return f"Не удалось найти местоположение '{params.location}'"
response.raise_for_status()
data = response.json()
# Форматирование результата для удобства агента
result = f"Погода в {data['location']['name']}, {data['location']['country']}:\n"
for day in data['forecast']['forecastday']:
result += f"- {day['date']}: {day['day']['condition']['text']}, "
result += f"Температура: {day['day']['avgtemp_c']}°C\n"
return result
except requests.exceptions.Timeout:
return "Ошибка: превышено время ожидания ответа от сервера погоды."
except requests.exceptions.RequestException as e:
return f"Ошибка при запросе погоды: {str(e)}"
except (KeyError, ValueError, TypeError) as e:
return f"Ошибка при обработке данных погоды: {str(e)}"
# Создаем экземпляр API
weather_api = WeatherAPI(api_key="ваш_ключ_API")
# Создаем инструмент
weather_tool = Tool.from_function(
name="WeatherForecast",
func=lambda x: weather_api.get_weather(WeatherParams.parse_raw(x)),
description="Получает прогноз погоды. Вводи JSON с полями: location (строка) и days (число от 1 до 10)."
) |
|
Обратите внимание на несколько важных моментов:
1. Я использую pydantic для валидации входных данных;
2. Детальная обработка ошибок делает инструмент надёжным;
3. Результат форматируется так, чтобы агенту было легче его интерпретировать;
Управление памятью — не дайте агенту потерять контекст
Одна из самых частых проблем, с которыми я сталкивался в ранних проектах — агенты "забывали" контекст диалога. В сложных сценариях это критично. LangChain предлагает несколько типов памяти, и выбор зависит от ваших потребностей:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| from langchain.memory import ConversationBufferMemory, ConversationSummaryMemory, ConversationBufferWindowMemory
# Простая буферная память - хранит всю историю
buffer_memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
# Память с окном - хранит только последние N взаимодействий
window_memory = ConversationBufferWindowMemory(
memory_key="chat_history",
k=5, # Хранить только последние 5 обменов
return_messages=True
)
# Суммаризирующая память - сохраняет сжатое представление длинных диалогов
summary_memory = ConversationSummaryMemory(
llm=llm, # Нужна модель для суммаризации
memory_key="chat_history",
return_messages=True
) |
|
Для особо сложных случаев я рекомендую создавать комбинированную память:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from langchain.memory import CombinedMemory
# Комбинируем суммаризацию с буфером последних сообщений
combined_memory = CombinedMemory(
memories=[
ConversationSummaryMemory(llm=llm, memory_key="chat_summary"),
ConversationBufferWindowMemory(k=3, memory_key="recent_messages")
]
)
agent = initialize_agent(
tools=[search_tool, math_tool],
llm=llm,
agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION,
memory=combined_memory,
verbose=True
) |
|
Такой подход особенно полезен для длительных диалогов — агент сохраняет общее понимание контекста через суммаризацию, но имеет детальный доступ к последним сообщениям.
Фолбэк-стратегии — план Б всегда должен быть
"Надежда на лучшее, но готовься к худшему" — этот принцип особенно актуален для ИИ-систем. Я всегда внедряю фолбэк-стратегии для обработки неожиданных ситуаций:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| from langchain.smith import RunEvalConfig, run_on_dataset
import time
# Обертка для обработки ошибок инструментов
def tool_with_fallback(primary_tool, fallback_tool=None, max_retries=2):
def wrapper(*args, **kwargs):
attempts = 0
while attempts <= max_retries:
try:
return primary_tool(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts > max_retries:
if fallback_tool:
try:
return fallback_tool(*args, **kwargs)
except:
return f"Все методы получения данных не сработали. Ошибка: {str(e)}"
else:
return f"Не удалось выполнить операцию после {max_retries} попыток. Ошибка: {str(e)}"
time.sleep(1) # Пауза перед повторной попыткой
return wrapper
# Применяем обертку к инструменту
weather_tool_with_fallback = Tool.from_function(
name="WeatherForecast",
func=tool_with_fallback(
weather_api.get_weather,
fallback_tool=lambda params: "Сегодня ожидается типичная для этого времени года погода"
),
description="Получает прогноз погоды"
) |
|
Эта обертка делает две вещи: пытается выполнить инструмент несколько раз в случае временных проблем и использует запасной инструмент, если основной не работает.
Векторные базы данных — память агента за пределами контекста
Когда агенту нужно работать с большими объемами данных или документов, контекстное окно модели быстро становится узким местом. Здесь на помощь приходят векторные базы данных:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import TextLoader
# Загружаем и подготавливаем документы
documents = TextLoader("./data/documentation.txt").load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
splits = text_splitter.split_documents(documents)
# Создаем векторное хранилище
embedding_model = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(documents=splits, embedding=embedding_model)
# Создаем инструмент для поиска по документам
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
doc_tool = Tool.from_function(
name="DocumentSearch",
func=retriever.get_relevant_documents,
description="Ищет информацию в технической документации. Используй для поиска конкретных технических деталей."
) |
|
Я активно использую этот подход в проектах, где агент должен оперировать специфическими данными. Например, для агента службы поддержки, которому нужно искать ответы в базе знаний компании.
Настройка температуры и других параметров генерации
Правильная настройка параметров генерации может радикально повлиять на поведение агента. Я обычно экспериментирую с несколькими параметрами:
| Python | 1
2
3
4
5
6
7
8
9
10
| from langchain.prompts import MessagesPlaceholder
from langchain.agents.format_scratchpad import format_to_openai_function_messages
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
llm = ChatOpenAI(
temperature=0.2, # Низкая температура для более детерминированных ответов
model="gpt-4",
top_p=0.9, # Слегка ограничиваем разнообразие
max_tokens=1000 # Контролируем длину ответов
) |
|
Для агентов я обычно придерживаюсь низкой температуры (0.1-0.3), поскольку предсказуемость в выборе инструментов и последовательности действий важнее креативности. Однако есть исключения:
1. Для креативных задач (генерация идей, мозговой штурм) повышаю до 0.7-0.9.
2. При работе с обобщением или синтезом информации иногда полезна средняя температура 0.4-0.6.
Chain-of-Thought промптинг — заставьте модель думать пошагово
Одна из самых мощных техник для улучшения качества рассуждений агента — это Chain-of-Thought промптинг. Вместо того чтобы просить модель сразу дать ответ, мы поощряем её рассуждать пошагово:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| cot_template = """Ты - эксперт по решению сложных проблем.
Для каждой задачи действуй следующим образом:
1. Внимательно проанализируй условие задачи
2. Раздели задачу на подзадачи или шаги
3. Решай каждый шаг последовательно, объясняя свои рассуждения
4. Проверь свое решение
5. Сформулируй окончательный ответ
Задача: {input}
Размышление:"""
cot_prompt = PromptTemplate(
template=cot_template,
input_variables=["input"]
)
# Используем этот промпт в агенте
cot_chain = LLMChain(llm=llm, prompt=cot_prompt) |
|
Техника Chain-of-Thought наиболее эффективна для:
Математических задач и логических головоломок
Многошаговых рассуждений
Анализа данных и формулирования выводов
Задач, требующих выявления причинно-следственных связей
Я заметил, что агенты с CoT-промптами делают значительно меньше ошибок в сложных задачах, хотя это происходит за счет увеличения количества токенов и времени выполнения.
Техники промпт-инжиниринга для повышения точности
После сотен экспериментов с промптами я выделил несколько техник, которые особенно эффективны для агентов:
1. Ролевой промптинг — определяет "личность" агента:
| Python | 1
2
3
4
5
6
7
8
| role_prompt = """Ты - опытный аналитик данных с экспертизой в финансах.
Ты умеешь:
Интерпретировать финансовые показатели
Выявлять тренды в данных
Делать обоснованные прогнозы
Объяснять сложные концепции простым языком
Используй свои навыки для ответа на следующий вопрос: {input}""" |
|
2. Few-shot промптинг — обучение на примерах:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| few_shot_template = """Вот примеры, как нужно анализировать финансовые данные:
Вопрос: Как изменилась прибыль компании в Q2?
Мысли: Мне нужно найти данные о прибыли за Q2 и сравнить с предыдущим периодом.
Действие: FinancialDataTool
Входные данные: {"company": "ACME Corp", "metric": "profit", "period": "Q2"}
Наблюдение: Прибыль в Q2: $1.2M, Q1: $0.8M
Ответ: Прибыль компании ACME Corp выросла на 50% в Q2 по сравнению с Q1, с $0.8M до $1.2M.
Теперь твоя очередь:
Вопрос: {input}
Мысли:""" |
|
3. Рефлексивный промптинг — заставляет агента оценивать свои действия:
| Python | 1
2
3
4
5
6
7
| reflection_prompt = """После каждого действия оцени его результат:
1. Получил ли я полезную информацию?
2. Приблизился ли я к ответу на вопрос?
3. Какое действие будет наиболее полезным дальше?
Вопрос: {input}
Мысли:""" |
|
Развертывание агента в облачной инфраструктуре
Переход от локального прототипа к масштабируемому решению — это отдельный челлендж. Я обычно использую несколько подходов для развертывания агентов:
1. Containerization с Docker:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| # Пример Docker файла для агента
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "agent_service.py"] |
|
2. Serverless функции для легких агентов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Пример AWS Lambda хендлера
def lambda_handler(event, context):
query = event.get('query', '')
# Инициализируем агента (можно использовать кеширование для ускорения)
agent = initialize_agent(...)
# Выполняем запрос
result = agent.invoke(query)
return {
'statusCode': 200,
'body': result['output']
} |
|
3. Микросервисная архитектура для сложных систем с несколькими агентами:
| Python | 1
2
3
4
| Agent Gateway (API) ─┬─ Spe******t Agent 1 ─── Tool Service 1
├─ Spe******t Agent 2 ─┬─ Tool Service 2
│ └─ External API
└─ Coordinator Agent ─── Vector Database |
|
Ключевым моментом при масштабировании является кеширование — особенно для инструментов, выполняющих дорогие операции:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from functools import lru_cache
import hashlib
import json
# Кеширование вызовов API с учетом всех параметров
def cache_key_builder(*args, **kwargs):
key = {
'args': args,
'kwargs': {k: v for k, v in kwargs.items() if k != 'api_key'}
}
return hashlib.md5(json.dumps(key, sort_keys=True).encode()).hexdigest()
@lru_cache(maxsize=100)
def cached_api_call(cache_key, *args, **kwargs):
# Реальный вызов API
return actual_api_function(*args, **kwargs)
def weather_with_cache(*args, **kwargs):
cache_key = cache_key_builder(*args, **kwargs)
return cached_api_call(cache_key, *args, **kwargs) |
|
Такой подход значительно снижает нагрузку на внешние API и ускоряет работу агента при повторяющихся запросах.
Мониторинг и обратная связь — глаза и уши вашего агента
Один из ключевых аспектов, которые часто упускают при создании агентов — это систематический мониторинг и сбор обратной связи. Когда я запустил свой первый продакшн-агент, через неделю выяснилось, что он неправильно интерпретировал около 15% запросов определенного типа! Чтобы избежать таких сюрпризов, я разработал систему мониторинга:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| import logging
from datetime import datetime
class AgentMonitor:
def __init__(self, log_file="agent_logs.jsonl"):
self.log_file = log_file
self.logger = logging.getLogger("agent_monitor")
self.logger.setLevel(logging.INFO)
def log_interaction(self, query, response, intermediate_steps=None, metadata=None):
log_entry = {
"timestamp": datetime.now().isoformat(),
"query": query,
"response": response,
"steps": intermediate_steps or [],
"metadata": metadata or {}
}
with open(self.log_file, "a") as f:
f.write(json.dumps(log_entry) + "
")
def analyze_logs(self, days=7):
"""Анализирует логи за последние N дней."""
# Анализ логов, выявление паттернов, проблем и т.д.
pass
# Использование монитора
monitor = AgentMonitor()
def agent_with_monitoring(query, metadata=None):
result = agent.invoke(query)
monitor.log_interaction(
query=query,
response=result["output"],
intermediate_steps=result.get("intermediate_steps"),
metadata=metadata
)
return result |
|
Но сбор логов — это только полдела. Настоящая ценность появляется, когда вы автоматизируете анализ и обратную связь:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from sklearn.cluster import KMeans
import numpy as np
def analyze_failure_patterns(logs, embedding_model):
# Извлекаем проблемные запросы
failed_queries = [log["query"] for log in logs if "error" in log["metadata"]]
# Получаем эмбеддинги
embeddings = [embedding_model.embed_query(q) for q in failed_queries]
# Кластеризуем для выявления паттернов
kmeans = KMeans(n_clusters=5)
clusters = kmeans.fit_predict(np.array(embeddings))
# Анализируем каждый кластер
for i in range(5):
cluster_queries = [q for q, c in zip(failed_queries, clusters) if c == i]
print(f"Кластер ошибок #{i}:")
for q in cluster_queries[:5]: # Первые 5 примеров
print(f" - {q}") |
|
Такой анализ помог мне выявить системные проблемы в работе агентов и улучшить их промпты и инструменты.
Безопасность — защищаем агента и пользователей
За годы работы с ИИ-системами я понял, что безопасность должна быть приоритетом номер один. LLM-агенты добавляют новый уровень сложности, поскольку они могут выполнять действия во внешних системах. Вот несколько практик, которые я всегда внедряю:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| from langchain.callbacks.base import BaseCallbackHandler
class SecurityCallbackHandler(BaseCallbackHandler):
"""Обработчик для проверки безопасности запросов и ответов."""
def __init__(self, blocked_terms=None, sensitive_actions=None):
self.blocked_terms = blocked_terms or []
self.sensitive_actions = sensitive_actions or {}
def on_agent_action(self, action, **kwargs):
"""Проверяет безопасность действия агента."""
tool_name = action.tool
tool_input = action.tool_input
# Проверка на запрещенные термины
for term in self.blocked_terms:
if term.lower() in str(tool_input).lower():
raise ValueError(f"Действие содержит запрещенный термин: {term}")
# Дополнительные проверки для чувствительных действий
if tool_name in self.sensitive_actions:
validator = self.sensitive_actions[tool_name]
if not validator(tool_input):
raise ValueError(f"Действие не прошло проверку безопасности: {tool_name}")
return action
# Валидаторы для конкретных инструментов
def validate_db_query(query):
"""Проверяет SQL-запрос на безопасность."""
dangerous_patterns = ["DROP", "DELETE", "UPDATE", "--", ";"]
return not any(pattern in query.upper() for pattern in dangerous_patterns)
# Настройка обработчика безопасности
security_handler = SecurityCallbackHandler(
blocked_terms=["пароль", "кредитная карта", "секретный"],
sensitive_actions={
"DatabaseTool": validate_db_query,
"ApiTool": lambda x: "admin" not in x.lower()
}
)
# Применение к агенту
agent_executor = AgentExecutor.from_agent_and_tools(
agent=agent,
tools=tools,
callbacks=[security_handler],
verbose=True
) |
|
Кроме того, я всегда реализую систему разрешений, особенно когда агент имеет доступ к критичным операциям:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| class PermissionSystem:
def __init__(self, default_policy="deny"):
self.default_policy = default_policy
self.permissions = {}
def add_permission(self, tool_name, condition_func=None):
"""Добавляет разрешение для инструмента."""
self.permissions[tool_name] = condition_func
def check_permission(self, tool_name, tool_input, user_id=None, context=None):
"""Проверяет разрешение на использование инструмента."""
if tool_name not in self.permissions:
return self.default_policy == "allow"
condition_func = self.permissions[tool_name]
if condition_func is None:
return True # Разрешено безусловно
return condition_func(tool_input, user_id, context)
# Пример использования
permissions = PermissionSystem(default_policy="deny")
permissions.add_permission("SearchTool") # Разрешено всем
permissions.add_permission("DatabaseTool",
lambda input, user_id, ctx: user_id in ["admin", "manager"]) # Только для админов и менеджеров |
|
Персонализация и адаптация — делаем агента уникальным
Один из самых интересных аспектов работы с агентами — их персонализация под конкретные потребности пользователей. Я разработал несколько подходов, которые позволяют агенту адаптироваться:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| class AdaptiveAgent:
def __init__(self, base_agent, user_preferences=None):
self.base_agent = base_agent
self.user_preferences = user_preferences or {}
self.user_history = {}
def update_preferences(self, user_id, preferences):
"""Обновляет предпочтения пользователя."""
if user_id not in self.user_preferences:
self.user_preferences[user_id] = {}
self.user_preferences[user_id].update(preferences)
def invoke(self, query, user_id):
"""Выполняет запрос с учетом предпочтений пользователя."""
# Получаем предпочтения пользователя
prefs = self.user_preferences.get(user_id, {})
# Модифицируем запрос с учетом предпочтений
if prefs.get("verbose_explanations"):
query = f"{query} Пожалуйста, дай подробное объяснение."
if prefs.get("preferred_sources"):
sources = ", ".join(prefs["preferred_sources"])
query = f"{query} Приоритизируй информацию из следующих источников: {sources}."
# Выполняем запрос
result = self.base_agent.invoke(query)
# Сохраняем историю взаимодействий
if user_id not in self.user_history:
self.user_history[user_id] = []
self.user_history[user_id].append((query, result))
return result |
|
Адаптация может быть и более сложной, с использованием машинного обучения для анализа поведения пользователя:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
def recommend_tools_based_on_history(user_history, all_tools):
"""Рекомендует инструменты на основе истории запросов пользователя."""
if not user_history:
return all_tools[:3] # Возвращаем топ-3 по умолчанию
# Получаем тексты запросов
queries = [q for q, _ in user_history]
# Создаем TF-IDF векторы
vectorizer = TfidfVectorizer()
query_vectors = vectorizer.fit_transform(queries)
# Вычисляем сходство с описаниями инструментов
tool_descriptions = [tool.description for tool in all_tools]
tool_vectors = vectorizer.transform(tool_descriptions)
# Находим средний вектор запросов пользователя
avg_query_vector = query_vectors.mean(axis=0)
# Вычисляем сходство с каждым инструментом
similarities = cosine_similarity(avg_query_vector, tool_vectors)
# Сортируем инструменты по релевантности
ranked_tools = [all_tools[i] for i in similarities.argsort()[0][::-1]]
return ranked_tools |
|
Такой подход позволяет персонализировать набор инструментов агента под конкретного пользователя, делая взаимодействие более эффективным.
Оптимизация производительности — от секунд к миллисекундам
Наконец, хочу поделиться несколькими приемами оптимизации, которые позволили мне значительно ускорить работу агентов в продакшене:
1. Параллельное выполнение запросов к инструментам:
| Python | 1
2
3
4
5
6
7
8
9
| import asyncio
from concurrent.futures import ThreadPoolExecutor
async def parallel_tools_execution(tools, query):
"""Выполняет запросы к инструментам параллельно."""
with ThreadPoolExecutor() as executor:
loop = asyncio.get_event_loop()
tasks = [loop.run_in_executor(executor, tool.run, query) for tool in tools]
return await asyncio.gather(*tasks) |
|
2. Кеширование промптов и эмбеддингов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| from diskcache import Cache
cache = Cache("./prompt_cache")
def cached_embedding(text, model, ttl=86400):
"""Кеширует эмбеддинги для повторного использования."""
key = f"emb_{model}_{hash(text)}"
if key in cache:
return cache[key]
embedding = model.get_embedding(text)
cache.set(key, embedding, expire=ttl)
return embedding |
|
3. Предварительная загрузка моделей и инструментов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Для Flask или FastAPI приложений
llm = None
tools = None
def initialize():
global llm, tools
llm = ChatOpenAI(model="gpt-4")
tools = [
SearchTool(),
CalculatorTool(),
WeatherTool()
]
# Вызываем при старте сервера
initialize() |
|
Эти оптимизации в сочетании с правильным выбором архитектуры развертывания позволили мне сократить среднее время ответа агента с 5-7 секунд до 1-2 секунд, что критично для интерактивных приложений.
Реальный проект: многофункциональный ассистент
Теория — это прекрасно, но настоящее понимание приходит только с практикой. Давайте соберём всё вместе и создадим полноценного многофункционального ассистента, который объединит разные типы инструментов и подходы, которые мы обсуждали ранее. Я специально разработал этот пример так, чтобы вы могли использовать его как основу для своих проектов.
Архитектура многофункционального ассистента
Наш ассистент будет состоять из нескольких ключевых компонентов:
1. Координатор — основной агент, который анализирует запросы и решает, какие специализированные компоненты задействовать.
2. Инструменты для разных типов задач — поиск, математика, погода, управление расписанием.
3. Память для сохранения контекста диалога.
4. Система мониторинга и логирования.
Вот как будет выглядеть базовая структура нашего проекта:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| assistant/
├── tools/
│ ├── search.py
│ ├── calculator.py
│ ├── weather.py
│ └── scheduler.py
├── memory/
│ └── conversation_store.py
├── monitoring/
│ └── logger.py
├── coordinator.py
└── main.py |
|
Реализация ключевых компонентов
Начнем с создания основных инструментов. Вот пример поискового инструмента с кешированием и обработкой ошибок:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # tools/search.py
from langchain_community.tools import DuckDuckGoSearchResults
from functools import lru_cache
import hashlib
import json
class CachedSearch:
def __init__(self, max_cache_size=100):
self.search_tool = DuckDuckGoSearchResults()
self.search = lru_cache(maxsize=max_cache_size)(self._search)
def _search(self, query):
try:
results = self.search_tool.run(query)
return results
except Exception as e:
return f"Ошибка поиска: {str(e)}"
def get_tool(self):
from langchain.agents import Tool
return Tool.from_function(
name="InternetSearch",
func=self.search,
description="Ищет актуальную информацию в интернете. Используй для вопросов о текущих событиях, фактах и данных, которые могут быть не в твоей базе знаний."
) |
|
Теперь создадим простой но эффективный погодный сервис:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| # tools/weather.py
import requests
from langchain.agents import Tool
from pydantic import BaseModel, Field
from typing import Optional
import json
class WeatherRequest(BaseModel):
location: str = Field(..., description="Город или координаты")
days: Optional[int] = Field(1, description="Количество дней прогноза")
class WeatherTool:
def __init__(self, api_key):
self.api_key = api_key
def get_weather(self, request_str):
try:
# Парсим входные данные
request = json.loads(request_str) if isinstance(request_str, str) else request_str
location = request.get("location", "")
days = request.get("days", 1)
# Проверяем валидность
if not location:
return "Необходимо указать местоположение"
# Делаем запрос к API
url = f"https://api.weatherapi.com/v1/forecast.json"
params = {
"key": self.api_key,
"q": location,
"days": days
}
response = requests.get(url, params=params, timeout=5)
response.raise_for_status()
data = response.json()
# Форматируем результат
result = f"Погода в {data['location']['name']}, {data['location']['country']}:\n"
for day in data['forecast']['forecastday']:
result += f"- {day['date']}: {day['day']['condition']['text']}, "
result += f"Температура: {day['day']['avgtemp_c']}°C\n"
return result
except json.JSONDecodeError:
return "Ошибка: неверный формат запроса. Ожидается JSON."
except requests.exceptions.RequestException as e:
return f"Ошибка при запросе погоды: {str(e)}"
except (KeyError, ValueError, TypeError) as e:
return f"Ошибка при обработке данных: {str(e)}"
def get_tool(self):
return Tool.from_function(
name="WeatherInfo",
func=self.get_weather,
description="Получает прогноз погоды для указанного местоположения. Принимает JSON с полями: location (строка) и days (число дней, по умолчанию 1)."
) |
|
Добавим систему управления памятью:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| # memory/conversation_store.py
from langchain.memory import ConversationSummaryBufferMemory
from datetime import datetime
class EnhancedMemory:
def __init__(self, llm, max_token_limit=2000):
self.internal_memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=max_token_limit,
memory_key="chat_history",
return_messages=True
)
self.metadata = {
"session_start": datetime.now().isoformat(),
"user_preferences": {},
"conversation_topics": []
}
def add_user_message(self, message):
self.internal_memory.chat_memory.add_user_message(message)
def add_ai_message(self, message):
self.internal_memory.chat_memory.add_ai_message(message)
def get_memory_variables(self):
return self.internal_memory.load_memory_variables({})
def update_metadata(self, key, value):
if key not in self.metadata:
self.metadata[key] = value
elif isinstance(self.metadata[key], list):
self.metadata[key].append(value)
else:
self.metadata[key] = value |
|
Теперь соберем все вместе в координаторе:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| # coordinator.py
from langchain.agents import AgentType, initialize_agent
from langchain_google_genai import ChatGoogleGenerativeAI
import os
class AssistantCoordinator:
def __init__(self, api_key, weather_api_key):
# Настраиваем LLM
os.environ['GOOGLE_API_KEY'] = api_key
self.llm = ChatGoogleGenerativeAI(model="gemini-pro")
# Инициализируем инструменты
from tools.search import CachedSearch
from tools.calculator import EnhancedCalculator
from tools.weather import WeatherTool
from tools.scheduler import ScheduleTool
self.search_tool = CachedSearch().get_tool()
self.calc_tool = EnhancedCalculator(self.llm).get_tool()
self.weather_tool = WeatherTool(weather_api_key).get_tool()
self.schedule_tool = ScheduleTool().get_tool()
self.tools = [
self.search_tool,
self.calc_tool,
self.weather_tool,
self.schedule_tool
]
# Создаем память
from memory.conversation_store import EnhancedMemory
self.memory = EnhancedMemory(self.llm)
# Инициализируем агента
self.agent = initialize_agent(
tools=self.tools,
llm=self.llm,
agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION,
memory=self.memory.internal_memory,
verbose=True
)
# Настраиваем логирование
from monitoring.logger import AssistantLogger
self.logger = AssistantLogger()
def process_query(self, user_query, user_id=None):
# Логируем запрос
self.logger.log_request(user_query, user_id)
# Обрабатываем запрос
try:
response = self.agent.invoke({"input": user_query})
output = response["output"]
# Логируем ответ
self.logger.log_response(output, user_id)
return {
"status": "success",
"response": output
}
except Exception as e:
error_msg = f"Ошибка при обработке запроса: {str(e)}"
self.logger.log_error(error_msg, user_id)
return {
"status": "error",
"response": "Извините, произошла ошибка при обработке вашего запроса.",
"error": str(e)
} |
|
Наконец, создадим простой API для нашего ассистента:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # main.py
from flask import Flask, request, jsonify
from coordinator import AssistantCoordinator
import os
app = Flask(__name__)
# Инициализируем ассистента при запуске
assistant = AssistantCoordinator(
api_key=os.environ.get("GEMINI_API_KEY"),
weather_api_key=os.environ.get("WEATHER_API_KEY")
)
@app.route('/api/chat', methods=['POST'])
def chat():
data = request.json
user_query = data.get('query', '')
user_id = data.get('user_id', 'anonymous')
if not user_query:
return jsonify({"status": "error", "message": "Query is required"}), 400
result = assistant.process_query(user_query, user_id)
return jsonify(result)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000) |
|
Тестирование и оптимизация
Для тестирования нашего ассистента я разработал несколько сценариев, которые охватывают различные аспекты функциональности:
1. Фактологические вопросы (проверка поиска).
2. Математические вычисления (проверка калькулятора).
3. Вопросы о погоде (проверка погодного API).
4. Управление расписанием (проверка планировщика).
5. Многошаговые запросы, требующие нескольких инструментов.
6. Контекстные запросы для проверки памяти.
Одна из основных проблем, с которой я столкнулся — это высокая латентность при последовательном использовании нескольких инструментов. Для её решения я модифицировал координатор, добавив параллельное выполнение некоторых запросов:
| Python | 1
2
3
4
5
6
7
8
| import asyncio
from concurrent.futures import ThreadPoolExecutor
async def parallel_tool_execution(tools, query):
with ThreadPoolExecutor() as executor:
loop = asyncio.get_event_loop()
tasks = [loop.run_in_executor(executor, tool.run, query) for tool in tools]
return await asyncio.gather(*tasks) |
|
Другое важное улучшение — предварительный анализ запроса для выбора оптимального набора инструментов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| def preanalyze_query(self, query):
"""Предварительно анализирует запрос для выбора подходящих инструментов"""
analysis = self.llm.predict(f"""
Проанализируй запрос пользователя и определи, какие инструменты могут понадобиться:
"{query}"
Варианты инструментов:
- search: для поиска информации
- calculator: для математических расчетов
- weather: для информации о погоде
- scheduler: для управления расписанием
Верни только список необходимых инструментов через запятую.
""")
needed_tools = []
if "search" in analysis.lower():
needed_tools.append(self.search_tool)
if "calculator" in analysis.lower():
needed_tools.append(self.calc_tool)
if "weather" in analysis.lower():
needed_tools.append(self.weather_tool)
if "scheduler" in analysis.lower():
needed_tools.append(self.schedule_tool)
# Если ничего не определили, вернем все инструменты
return needed_tools if needed_tools else self.tools |
|
Такая оптимизация позволила сократить среднее время ответа на 40% для сложных запросов, требующих нескольких инструментов.
Как из Python скрипта выполнить другой python скрипт? Как из Python скрипта выполнить другой python скрипт?
Если он находится в той же папке но нужно... Почему синтаксис Python 2.* и Python 3.* так отличается? Привет!
Решил на досуге заняться изучением Python'a. Читаю книгу по второму питону, а пользуюсь... Что лучше учить Python 2 или Python 3? хочу начать учить питон но полазив в нете, частенько попадалась информация что вроде как 2 будет... Python without python Доброго времени суток!
Хотел узнать, что делать с *.py файлом после того как готова программа,... Python 35 Выполнить файл из python shell Есть файл do.py :
print('start')
import os
import sys
import re
import inspect
def... Python - момент истины. Python - как оружие возмездие против системы Какие модули в python мне нужны для взлома баз данных? Перехвата информации? Внедрения в систему?
... Сложности с переходом с python 2.x на python 3.x def _load_config(self):
for fn in CONFIG_FILES:
fn = os.path.expanduser(fn)
... Изменение кода запроса с Python 2 на Python 3 Доброго времени суток.
Я пишу программу и для её реализации мне необходимо, чтобы она делала... Порт pyqt5 (python 3.5) программы на android - Python Подскажите пожалуйста возможно ли программу написанную на python методами pyqt5 переделать под... Перевод кода из Pascal в Python - Python Имеется код программы на языке Pascal, требуется перевести его в Python.
Я не могу перевести его в... Не могу получить ответ от python скрипта и на его основе создать список (зависимые списки js ajax python) Привет!
Есть необходимость сделать динамические списки при помощи js, ajax jQuery, Python.
Данные... Cx_freeze python error in main script как исправить- Python Пытался создать из .py .exe , но при запуске .exe получаю ошибку вот код setup.py
from cx_Freeze...
|