За последние пару лет я перепробовал, наверное, с десяток различных подходов к созданию интеллектуальных помощников. Но честно признаюсь: ничто так не изменило мое представление о разработке ИИ-систем, как фреймворк LangChain. Эта библиотека превратила муторную работу с моделями в увлекательное конструирование интеллектуальных агентов, способных не просто отвечать на вопросы, а действительно решать задачи.
В этой статье я расскажу, как создать полноценного AI-агента на Python с помощью LangChain и платформы watsonx. Мы вместе пройдем путь от настройки окружения до разработки агента, который умеет получать актуальную информацию о текущей дате и использовать NASA API для поиска астрономических изображений дня. Я не буду заморачиваться с теоретическими выкладками — вместо этого покажу, как собрать работающую систему с нуля. Разберем архитектуру агентов, поговорим о компонентах LangChain, научимся создавать инструменты и подключать внешние сервисы. И главное — я поделюсь реальным опытом: что работает, а где лучше искать обходные пути.
Что такое LangChain и почему он изменил мое понимание работы с ИИ-агентами
LangChain — это не просто очередная библиотека для работы с языковыми моделями. Это целая экосистема, которая перевернула мое представление о том, как создавать по-настоящему умных ИИ-агентов. До знакомства с ней я писал километры кода, чтобы организовать взаимодействие между LLM и внешними системами. Каждый раз приходилось изобретать велосипед, и честно говоря, меня это порядком достало. LangChain решает именно эту проблему — она предоставляет абстракции для работы с языковыми моделями, позволяя сосредоточиться на бизнес-логике, а не на инфраструктуре. По сути, это набор модульных компонентов, которые можно собирать как конструктор.
Ключевая фишка LangChain — концепция "агентов", которые могут не просто генерировать текст, а принимать самостоятельные решения. Представьте, что вместо модели, которая просто отвечает на запрос, у вас появляется ИИ-система, способная размышлять, использовать инструменты и выстраивать логические цепочки для решения задач. Если классическая модель на вопрос "какая сегодня погода?" может лишь сгенерировать общий ответ типа "Я не имею доступа к актуальной информации", то LangChain-агент способен обратиться к API погодного сервиса, получить данные и предоставить точный прогноз. Разница колосальная!
Для меня это открытие случилось, когда я экспериментировал с вопросно-ответной системой для своего проекта. Клиенты постоянно спрашивали о статусе заказов, и нужно было интегрировать информацию из CRM. С обычными моделями это превращалось в сложный танец с промптами и постобработкой. LangChain же позволил создать агента, который сам определяет, что нужно сделать API-запрос, выполняет его и интерпретирует результаты.
Рефлексный агент, основный на модели Есть такая среда моделирования "Vacuum Cleaner World". Может кто то слышал о ней.
Это... Агент для Mauntain Car Реализуйте класс Agent, средняя награда за один эпизод которого будет больше −102. Среднюю награду... Как из Python скрипта выполнить другой python скрипт? Как из Python скрипта выполнить другой python скрипт?
Если он находится в той же папке но нужно... Почему синтаксис Python 2.* и Python 3.* так отличается? Привет!
Решил на досуге заняться изучением Python'a. Читаю книгу по второму питону, а пользуюсь...
Экосистема LangChain: от простых цепочек к многоагентным системам
Экосистема LangChain напоминает мне конструктор LEGO — из простых кубиков можно собрать как примитивную машинку, так и межгалактический корабль. Начинал я с элементарных цепочек, где один компонент передает данные другому, а заканчивал созданием сложных многоагентных систем, которые могут спорить друг с другом, приходя к оптимальному решению.
В основе всего лежат несколько фундаментальных абстракций. Прежде всего это модели (Models) — обертки вокруг языковых моделей вроде GPT или, как в моем случае, IBM Granite. Затем идут промпты (Prompts) — шаблоны для генерации запросов к моделям. Третий базовый элемент — это индексы (Indexes), которые позволяют эффективно работать с внешними данными. И наконец, цепочки (Chains) — последовательности действий, объединяющие все остальные компоненты.
Начал я с простейших цепочек типа "промпт → модель → ответ". Но быстро перерос эту схему. Оказалось, что самое интересное начинается, когда подключаешь инструменты (Tools). Инструмент в LangChain — это функция, которую агент может вызвать для взаимодействия с внешним миром. Это может быть что угодно: калькулятор, поисковик, API погоды или, как в нашем примере, запрос к базе астрономических снимков NASA. Когда я впервые увидел, как агент самостоятельно определяет, какой инструмент нужно использовать для решения задачи, это выглядело как чистая магия. Спрашиваешь "какая самая высокая гора в Африке?", а он не просто выдает заученную информацию, а реально гуглит и выдает актуальный ответ. Я потратил целый вечер, скармливая своему первому агенту разные вопросы, чтобы понять границы его возможностей.
От одиночных агентов легко перейти к многоагентным системам. Тут уже начинается настоящее веселье. Можно создать команду специализированых агентов, где каждый отвечает за свою область. Например, один агент занимается поиском данных, второй их анализирует, а третий формирует итоговый отчет. Или реализовать паттерн "критик", где один агент генерирует ответ, а другой его проверяет и улучшает.
Сложность в том, что при создании многоагентных систем приходится решать новые проблемы — кординацию работы, передачу контекста, разрешение конфликтов. Но когда система заработала, результаты превзошли все ожидания. Особено порадовала возможность создать агента, который помнит предыдущие взаимодействия и использует эту информацию в новых диалогах. В классических подходах с этим всегда была беда.
Мой опыт миграции с OpenAI API на LangChain - что действительно изменилось
Когда я впервые начал работать с большими языковыми моделями, то как и многие, стартовал с прямых вызовов OpenAI API. Помню, какой восторг вызвал первый работающий код, который отправлял запрос и получал вменяемый ответ. Но чем сложнее становились задачи, тем больше времени уходило на "танцы с бубном" вокруг API.
Переход на LangChain стал для меня настоящим прорывом, и вот почему. Раньше любая нетривиальная задача превращалась в головную боль: нужно было самостоятельно форматировать промпты, парсить ответы, реализовывать повторные попытки при ошибках. Да что там — даже простая поддержка контекста разговора требовала дополнительного кода. С LangChain все эти базовые вещи уже "из коробки".
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # Было с OpenAI API напрямую:
def get_completion(prompt, history=[]):
formatted_prompt = format_complex_prompt(prompt, history)
try:
response = openai.Completion.create(
model="text-davinci-003",
prompt=formatted_prompt,
max_tokens=500
)
return parse_response(response.choices[0].text)
except Exception as e:
return handle_errors(e)
# Стало с LangChain:
from langchain.chains import ConversationChain
from langchain.llms import OpenAI
conversation = ConversationChain(
llm=OpenAI(model_name="text-davinci-003"),
verbose=True
)
response = conversation.predict(input="Привет, мир!") |
|
Но самое значительное изменение — это концептуальный сдвиг в том, как я стал думать о своих приложениях. Раньше модель была в центре всего, и каждая функция стрелялась в API напрямую. Теперь же я мыслю компонентами: промпты отдельно, цепочки отдельно, инструменты отдельно. Это невероятно упростило повторное использование кода и тестирование.
При миграции не обошлось без сложностей. Пришлось переосмыслить архитектуру приложений и выучить новые абстракции. Иногда казалось, что проще написать с нуля, чем переделывать существующий код. Еще одна проблема — некоторые низкоуровневые опции, доступные в API, в LangChain оказались спрятаны глубоко или вовсе недоступны. Однако главное преимущество, которое перевесило все недостатки — масштабируемость и гибкость. Когда OpenAI выкатила новые модели, мне не пришлось переписывать всю логику — достаточно было поменять одну строчку кода. А потом я и вовсе перешел на локальные модели, просто подключив другой бэкенд.
Сегодня я уже не представляю, как работал бы без этой библиотеки. Особенно если говорить об агентах — тут LangChain просто вне конкуренции.
Архитектура LangChain агентов
Разобравшись с теорией, давайте заглянем под капот и посмотрим, как устроены агенты в LangChain. Честно говоря, первое время я сам путался в этой архитектуре, пока не нарисовал для себя схему на салфетке во время обеденого перерыва.
Архитектура LangChain-агента напоминает мне операционную систему, где языковая модель играет роль ядра, а вокруг неё крутятся различные подсистемы. В центре всего находится Agent Executor — компонент, который оркестрирует весь процесс принятия решений. Он принимает запрос пользователя, передаёт его агенту, получает план действий и выполняет их, используя доступные инструменты.
Сам агент состоит из нескольких ключевых частей: базовой языковой модели (LLM), промпт-шаблона, определяющего поведение, и парсера для интерпретации выводов модели. Когда агент получает запрос, он генерирует последовательность "мыслей", определяет, какой инструмент использовать, и формирует входные данные для этого инструмента. Что меня больше всего впечатлило в этой архитектуре — это гибкость. Можно подменить любой компонент, не ломая систему. Хочешь другую модель? Без проблем. Нужны кастомные инструменты? Легко. Всё работает по принципу "разделяй и властвуй", что делает разработку и отладку гораздо проще.
Компоненты системы: промпты, цепочки, инструменты
Теперь давайте разберем анатомию агента, чтобы понять, как вся эта машинерия работает на практике. Когда я впервые столкнулся с LangChain, меня поразило то, насколько модульным и хорошо продуманным оказался фреймворк. Три кита, на которых держится вся система — это промпты, цепочки и инструменты.
Промпты в LangChain — это не просто текстовые запросы, а полноценные шаблоны с возможностью подстановки параметров. Я быстро понял, что качество промпта напрямую влияет на работу всего агента. Это как код ДНК, который определяет, каким будет организм. В ранних экспериментах я использовал примитивные промпты, и результаты были соответствующими. Но потом научился создавать структурированные шаблоны с четкими инструкциями для модели.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from langchain_core.prompts import ChatPromptTemplate
# Пример продвинутого промпта для агента
system_prompt = """Ты умный асистент со следующими инструментами: {tools}
Используй JSON для вызова инструмента, указывая "action" и "action_input".
Допустимые значения для "action": "Final Answer" или {tool_names}
Формат должен быть таким:
{{
"action": НАЗВАНИЕ_ИНСТРУМЕНТА,
"action_input": ВХОДНЫЕ_ДАННЫЕ
}}
"""
human_prompt = "{input}\n{agent_scratchpad}"
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", human_prompt),
]) |
|
Цепочки (Chains) — это последовательности операций, которые связывают отдельные компоненты воедино. Я воспринимаю их как конвейер на заводе, где каждый элемент выполняет свою функцию и передает результат дальше. Простейшая цепочка может состоять из промпта, модели и парсера ответа. Более сложные включают несколько моделей, инструментов и обработчиков данных. Самыми интересными для меня оказались цепочки с памятью, которые сохраняют историю взаимодействий. В отличие от обычных вызовов API, где каждый запрос обрабатывается изолированно, такие цепочки позволяют поддерживать долгие осмысленные диалоги. Агент буквально "помнит", о чем вы говорили раньше, и использует этот контекст в новых ответах.
Инструменты (Tools) — это функции, которые агент может вызывать для взаимодействия с внешним миром. Эта концепция произвела на меня самое сильное впечатление. Ведь именно инструменты превращают простую языковую модель в настощего агента, способного решать реальные задачи. В LangChain есть множество встроенных инструментов: поиск в интернете, работа с API, выполнение вычислений и даже генерация кода.
Но самое крутое — это возможность создавать собственные инструменты. Достаточно декорировать функцию с помощью @tool, и она становится доступной агенту. Вот как я реализовал инструмент для получения текущей даты:
| Python | 1
2
3
4
5
6
7
8
| from langchain_core.tools import tool
@tool
def get_todays_date() -> str:
"""Получает текущую дату в формате ГГГГ-ММ-ДД."""
from datetime import datetime
date = datetime.now().strftime("%Y-%m-%d")
return date |
|
Когда я впервые увидел, как агент самостоятельно решает, какой инструмент вызвать и с какими параметрами, это выглядело как настоящая магия. Он буквально "рассуждает" о том, какие шаги предпринять, выбирает подходящий инструмент и интерпретирует результаты. И все это происходит автоматически, на основе заложенного в промпт шаблона поведения.
Комбинирование этих трех элементов — промптов, цепочек и инструментов — дает практически безграничные возможности для создания интеллектуальных агентов. От простых чат-ботов до сложных систем, способных анализировать данные, принемать решения и выполнять действия в реальном мире. Особенно мощной концепцией в инструментарии LangChain оказались цепочки обратного вызова (Chain of Thought). Эта техника заставляет модель "думать вслух", прежде чем давать ответ. На практике это выглядит так: агент получает вопрос, размышляет над ним поэтапно и только потом формулирует результат. Для меня это стало прорывом при создании агентов, решающих сложные аналитические задачи. Вот типичный паттерн рассуждений агента, который я наблюдал в логах:
| Code | 1
2
3
4
5
6
7
8
| Вопрос: Сколько дней прошло с 10 января до сегодня?
Мысль: Мне нужно узнать текущую дату, а потом вычислить разницу между датами.
Действие: get_todays_date
Наблюдение: 2024-09-27
Мысль: Теперь у меня есть текущая дата: 2024-09-27. Начальная дата: 10 января 2024 года (2024-01-10). Нужно вычислить разницу.
Действие: Вычисляю разницу между датами...
Наблюдение: С 10 января 2024 до 27 сентября 2024 прошло 261 день.
Ответ: С 10 января до сегодня прошло 261 день. |
|
С особой тщательностью я подходил к проектированию инструментов. Простой доступ к внешним API — это только верхушка айсберга. Настоящее искуство — создавать инструменты с правильной "дозировкой" функциональности. Слишком простые — и агент будет тратить время на многократные вызовы. Слишком сложные — и агенту будет трудно понять, когда их применять.
Еще один важный аспект — компоновка инструментов. Я заметил, что агенты гораздо эффективнее, когда у них есть доступ к набору взаимодополняющих инструментов. Например, связка из инструментов для поиска информации, её анализа и форматирования результатов дает лучшие результаты, чем один универсальный инструмент.
Что касается промптов, то тут важно знать о различных типах подсказок. Помимо стандартных промптов для генерации текста, в LangChain есть спецыализированные шаблоны для агентов: ReAct-промпты для рассуждений и действий, Zero-shot промпты для работы без примеров и Few-shot для обучения на нескольких примерах.
| Python | 1
2
3
4
5
6
7
8
9
10
| # Пример Few-shot промпта для разбора дат
few_shot_template = """
Вопрос: Какой день недели был 15 мая 2023?
Мысль: Мне нужно определить день недели для конкретной даты.
Действие: Определяю день недели для 15 мая 2023...
Наблюдение: 15 мая 2023 был понедельник.
Ответ: 15 мая 2023 был понедельник.
Вопрос: {question}
""" |
|
Интересным открытием для меня стала возможность создания вложенных цепочек, где одна цепочка передает результаты другой. Это позволяет строить сложные пайплайны обработки, при этом сохраняя модульность и возможность тестирования отдельных компонентов. Например, в одном из проектов я создал цепочку, которая сначала генерирует поисковые запросы, затем выполняет их и анализирует результаты, а в конце формирует итоговое резюме.
Паттерны проектирования для агентов: когда использовать ReAct, а когда Plan-and-Execute
Работая с LangChain, я довольно быстро понял, что одной архитектуры недостаточно — нужно выбрать правильный паттерн поведения агента. Это как с алгоритмами в программировании: одна задача эффективно решается с помощью рекурсии, другая — через динамическое программирование. Так и с агентами: разные паттерны подходят для разных сценариев.
Основные паттерны, которые я активно применял — это ReAct (Reasoning and Action) и Plan-and-Execute. Разница между ними фундаментальна и влияет на всю логику работы агента.
ReAct — это паттерн, где агент работает итеративно: размышляет, действует, наблюдает результат, снова размышляет и так далее. Это как решение задачи методом последовательных приближений. На каждом шаге агент формирует промежуточные мысли, выбирает инструмент, получает результат и корректирует свой курс. Типичный процесс выглядит так:
| Code | 1
2
3
4
5
6
7
8
9
10
| Запрос: Найди астрономическую картинку дня на NASA за прошлую неделю
Мысль: Нужно определить, какая была дата неделю назад, а потом запросить картинку.
Действие: get_todays_date
Результат: 2024-09-27
Мысль: Теперь вычисляю дату неделю назад: 2024-09-20
Действие: get_astronomy_image
Входные данные: {"date": "2024-09-20"}
Результат: https://apod.nasa.gov/apod/image/2409/Comet_NASA_960.jpg
Мысль: Я получил ссылку на астрономическую картинку дня от NASA за 2024-09-20
Ответ: Вот астрономическая картинка дня NASA за прошлую неделю (20 сентября 2024): https://apod.nasa.gov/apod/image/2409/Comet_NASA_960.jpg |
|
ReAct идеально подходит для задач, где:- Точный план действий заранее не определен.
- Каждый шаг зависит от результата предыдущего.
- Задача может потребовать нестандартного подхода или творческого решения.
В моей практике ReAct особенно хорошо показал себя в интерактивных сценариях, где пользователь ожидает не только конечный результат, но и понимание того, как агент к нему пришел.
Паттерн Plan-and-Execute, наоборот, разделяет процесс на два этапа: сначала агент составляет полный план действий, а затем методично его выполняет. Это напоминает подход, когда вы сначала пишете алгоритм на бумаге, а потом реализуете его в коде.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| # Пример использования Plan-and-Execute
from langchain.agents import create_plan_and_execute_agent
agent = create_plan_and_execute_agent(
llm=model,
tools=tools,
verbose=True
)
agent.invoke({
"input": "Сравни погоду в Москве и Санкт-Петербурге на завтра"
}) |
|
Вывод агента при этом выглядит иначе:
| Code | 1
2
3
4
5
6
7
8
9
10
| План:
1. Определить текущую дату
2. Вычислить дату "завтра"
3. Запросить прогноз погоды для Москвы на завтра
4. Запросить прогноз погоды для Санкт-Петербурга на завтра
5. Сравнить полученные данные
6. Составить отчет
Выполнение шага 1: Определяю текущую дату...
... |
|
Plan-and-Execute я предпочитаю в следующих случаях:- Когда задача хорошо структурирована и декомпозируема,
- При работе с многоэтапными сложными задачами,
- Когда прозрачность процесса критически важна для пользователя,
- Если требуется оптимизация количества вызовов внешних API.
На практике я часто комбинирую эти два подхода, создавая гибридные системы. Например, в одном проекте мой агент сначала планировал высокоуровневую стратегию (Plan), а затем для каждого шага использовал итеративный подход ReAct. Это позволило сочетать стратегическое мышление с тактической гибкостью.
Интересное наблюдение из моей практики: ReAct-агенты чаще "думают вне коробки", находя неожиданные решения. При разработке системы для анализа финансовых документов я наблюдал, как ReAct-агент самостоятельно "догадался" использовать инструмент для математических вычислений, чтобы проверить согласованность цифр в отчете — хотя изначально я этого не планировал. С другой стороны, Plan-and-Execute агенты оказались более надежными при решении комплексных задач с большим количеством взаимозависимых шагов. Когда я создавал агента для автоматизации процесса сбора и анализа данных из нескольких источников, пошаговое планирование позволило избежать типичных для ReAct циклических вызовов и повторений.
Есть еще ряд интересных паттернов, которые я применял в различных проектах. Например, MultiAgent-паттерн, где несколько специализированных агентов работают вместе, решая разные аспекты проблемы. Или MRKL (Modular Reasoning, Knowledge and Language) — архитектура, которая комбинирует нейросетевые компоненты с символьными вычислениями.
| Python | 1
2
3
4
5
6
7
8
9
10
| # Пример реализации MultiAgent-паттерна
researcher = create_react_agent(model, research_tools, "Исследователь")
analyst = create_react_agent(model, analysis_tools, "Аналитик")
writer = create_react_agent(model, writing_tools, "Писатель")
def solve_complex_task(query):
research_results = researcher.run(query)
analysis = analyst.run(research_results)
final_report = writer.run(f"Составь отчет на основе: {analysis}")
return final_report |
|
Выбор правильного паттерна часто зависит от контекста использования. Для чат-ботов, где важна динамичность и персонализированность, я обычно выбираю ReAct. Для аналитических инструментов, где требуется методичность и прозрачность, предпочтительнее Plan-and-Execute.
Важный нюанс, который я заметил в процессе экспериментов: эффективность паттерна сильно зависит от качества промпта и способностей базовой модели. Более "умные" модели лучше работают с ReAct, поскольку могут эффективнее планировать на лету, в то время как для менее мощных моделей структурированный подход Plan-and-Execute часто дает лучшие результаты.
Разбор внутренних механизмов работы цепочек в LangChain
Чтобы по-настоящему овладеть искуством создания интеллектуальных агентов, мне пришлось как следует покопаться во внутренностях LangChain. И скажу честно — это того стоило. Понимание того, как устроены цепочки изнутри, дало мне возможность создавать такие конструкции, которые на первый взгляд кажутся невозможными. Итак, что же происходит под капотом, когда вы вызываете метод .invoke() или .run() у цепочки? Если говорить упрощенно, LangChain запускает последовательность из трех этапов: подготовка входных данных, выполнение и постобработка.
Но дьявол, как водится, в деталях. В основе архитектуры лежит концепция "Runnable" — базовый интерфейс для всего, что можно выполнить. Это своего рода универсальный контракт, который гарантирует, что любой компонент можно встроить в цепочку. Будь то языковая модель, промпт, парсер или инструмент — все они наследуются от этого интерфейса.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| # Упрощенная схема выполнения цепочки
def invoke(self, input_data):
# 1. Подготовка входных данных
prepared_input = self._prepare_input(input_data)
# 2. Основное выполнение
try:
raw_output = self._execute(prepared_input)
except Exception as e:
return self._handle_error(e)
# 3. Постобработка результата
return self._process_output(raw_output) |
|
Любопытно, что внутри каждая цепочка представляет собой направленный ациклический граф (DAG), где узлы — это компоненты, а ребра — потоки данных между ними. При вызове .invoke() LangChain анализирует этот граф и строит план выполнения, оптимизируя где это возможно. Например, независимые ветви могут выполняться параллельно.
В процессе разработки я столкнулся с неочевидным моментом: входные данные в цепочке не просто передаются от компонента к компоненту. Вместо этого LangChain использует паттерн "расширяющегося контекста". Это значит, что каждый следующий элемент получает не только результат предыдущего, но и доступ ко всему контексту, накопленному ранее. Допустим, есть цепочка из трех шагов: форматирования промпта, вызова модели и парсинга ответа. На каждом этапе происходит следующее:
1. Промпт получает начальные данные и создает запрос для модели.
2. Модель получает этот запрос И исходные данные.
3. Парсер получает ответ модели, запрос И исходные данные.
Это дает гибкость, которой я не встречал в других фреймворках. Любой компонент может обратиться к любой части контекста, что позволяет строить чрезвычайно сложные и гибкие цепочки рассуждений.
Еще одна фишка, которую я обнаружил — "ленивое" выполнение. LangChain не вычисляет ничего, пока это действительно не потребуется. Такой подход экономит ресурсы и позволяет оптимизировать выполнение в зависимости от условий.
Функциональное программирование в цепочках LangChain
Одна из вещей, которая нереально упростила мою жизнь в LangChain — это подход к построению цепочек через функциональное программирование. Когда я впервые увидел оператор | (pipe) для соединения компонентов, меня как громом поразило. Это же так логично! Зачем нам громоздкие вложенные конструкции, когда можно строить агентов как конвейер трансформаций?
| Python | 1
2
3
4
5
6
7
8
9
10
| # Старый императивный подход
def process_query(query):
formatted_prompt = prompt_template.format(query=query)
llm_result = llm.generate(formatted_prompt)
parsed_result = output_parser.parse(llm_result)
return parsed_result
# Новый функциональный стиль
chain = prompt_template | llm | output_parser
result = chain.invoke({"query": "Какая сегодня погода?"}) |
|
Этот функциональный стиль позволяет компоновать цепочки как конструктор LEGO. Я могу взять готовые блоки и соединить их в новом порядке, не переписывая логику каждого компонента. Причем самое крутое — возможность ветвления и условной обработки:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # Условное ветвление в функциональном стиле
def route_to_correct_tool(input_data):
if "погода" in input_data["query"].lower():
return {"route": "weather"}
else:
return {"route": "general"}
chain = (
prompt_template
| llm
| RunnablePassthrough.assign(route=route_to_correct_tool)
| RunnableBranch(
(lambda x: x["route"] == "weather", weather_chain),
(lambda x: x["route"] == "general", general_chain),
)
) |
|
Это изменило мой подход к проектированию агентов. Вместо монолитных классов я стал мыслить категориями чистых функций и трансформаций данных. Каждый компонент принимает входные данные, что-то с ними делает и возвращает новые — без побочных эффектов. Больше не нужно беспокоиться о сохранении состояния между компонентами или сложной передаче параметров. Все данные просто текут по цепочке, обогащаясь на каждом шаге. Я могу легко перехватить и логировать промежуточные результаты, не ломая основной поток. Особенно удобно, что функциональный подход упрощает тестирование. Я могу проверить каждую функцию изолированно, а потом интегрировать их в цепочку. Если что-то работает не так, сразу видно, на каком этапе проблема.
Middleware и перехватчики в цепочках: расширение функциональности без изменения кода
Одна из самых недооцененных фишек LangChain — система middleware. Я открыл её для себя относительно поздно, когда уже написал кучу дублирующегося кода для логирования, и был просто в шоке от того, насколько проще можно было решить эту задачу.
Middleware в LangChain — это способ внедрения дополнительной логики в процесс выполнения цепочек без изменения самих цепочек. Представьте, что вы можете "перехватывать" запросы и ответы на любом этапе и делать с ними что угодно: логировать, модифицировать, валидировать.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| from langchain.callbacks import RunLoggerHandler
from langchain.globals import set_handler
# Добавляем глобальный обработчик для логирования
logger = RunLoggerHandler()
set_handler(logger)
# Теперь все цепочки будут логироваться автоматически
response = agent_executor.invoke({"input": "Что такое ИИ?"})
# Посмотрим, что записалось в лог
print(logger.get_runs()) |
|
В одном из моих проектов мне нужно было отслеживать токены и считать стоимость запросов к API. Раньше я вставлял эту логику прямо в код агента, что было жутко неудобно. С middleware я просто написал перехватчик, который считает токены и записывает их в базу данных.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class TokenCounterMiddleware:
def __init__(self):
self.total_tokens = 0
def __call__(self, runnable, input_data, config, **kwargs):
# Запускаем оригинальный runnable
output = runnable.invoke(input_data, config)
# Подсчитываем токены, если это вызов LLM
if hasattr(output, "token_usage"):
self.total_tokens += output.token_usage.total_tokens
print(f"Использовано токенов: {self.total_tokens}")
return output
# Подключаем наш middleware
counter = TokenCounterMiddleware()
chain_with_counter = chain.with_handlers([counter]) |
|
Другой случай — сохранение промежуточных результатов. У меня был сложный агент, который иногда крашился на последнем шаге обработки, и приходилось начинать всё с начала. Middleware позволил легко решить эту проблему:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| class CheckpointMiddleware:
def __call__(self, runnable, input_data, config, **kwargs):
# Проверяем, есть ли сохраненный чекпоинт
checkpoint_key = hash(str(input_data))
if checkpoint_key in self.checkpoints:
print("Восстанавливаем из чекпоинта")
return self.checkpoints[checkpoint_key]
# Выполняем оригинальный вызов
output = runnable.invoke(input_data, config)
# Сохраняем результат
self.checkpoints[checkpoint_key] = output
return output |
|
Недавно мне понадобилось добавить валидацию ответов для критически важного агента. Вместо того чтобы переписывать его логику, я просто навесил middleware, который проверяет ответ на соответствие схеме и при необходимости запускает повторный запрос с уточнениями.
Создание пользовательских инструментов и их регистрация в системе
Самое захватывающее в работе с LangChain — это создание собственных инструментов. Когда я понял, что могу научить своего агента делать буквально что угодно, передо мной открылись невероятные возможности. Инструменты — это как суперспособности для вашего ИИ, и вы сами решаете, какие из них ему дать.
Базовая структура инструмента в LangChain предельно проста. Это обычная Python-функция с декоратором @tool. Магия происходит благодаря этому декоратору, который превращает функцию в объект, понятный для агента:
| Python | 1
2
3
4
5
6
7
8
| from langchain_core.tools import tool
@tool
def search_products(query: str) -> list:
"""Ищет товары в базе данных по запросу."""
# Здесь может быть запрос к вашей базе данных
products = database.search(query)
return products |
|
Обратите внимание на докстринг — это не просто комментарий! LangChain использует его как описание инструмента для модели. Чем точнее описание, тем лучше агент поймет, когда и как использовать этот инструмент.
Чтобы добавить параметры, я обычно использую аннотации типов. Это помогает и мне не запутаться, и агенту понять, какие данные ожидает инструмент:
| Python | 1
2
3
4
5
6
7
| @tool
def calculate_delivery_time(postal_code: str, weight: float) -> str:
"""Рассчитывает время доставки на основе почтового индекса и веса посылки в кг."""
# Логика расчета времени доставки
if weight > 10:
return "3-5 рабочих дней"
return "1-2 рабочих дня" |
|
Один из трюков, который я открыл для себя — параметр return_direct=True. Он указывает агенту, что результат этого инструмента нужно вернуть пользователю напрямую, без дополнительной обработки:
| Python | 1
2
3
4
5
6
| @tool(return_direct=True)
def generate_image(prompt: str) -> str:
"""Генерирует изображение по текстовому описанию и возвращает URL."""
# Вызов API генерации изображений
image_url = image_generator.create(prompt)
return image_url |
|
Эта опция особенно полезна для инструментов, которые возвращают медиа-контент или форматированные данные, не требующие дополнительной интерпретации.
Для более сложных инструментов я создаю классы, которые наследуются от BaseTool. Это дает больше гибкости в управлении состоянием и обработке ошибок:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| from langchain.tools import BaseTool
class DatabaseQueryTool(BaseTool):
name = "database_query"
description = "Выполняет SQL-запрос к базе данных и возвращает результаты."
def _run(self, query: str) -> list:
try:
connection = get_db_connection()
cursor = connection.cursor()
cursor.execute(query)
results = cursor.fetchall()
return results
except Exception as e:
return f"Ошибка выполнения запроса: {str(e)}"
async def _arun(self, query: str) -> list:
# Асинхронная версия метода
return await async_db_query(query) |
|
После создания инструментов их нужно зарегистрировать в агенте. Это делается путем передачи списка инструментов при инициализации:
| Python | 1
2
3
4
5
6
7
8
| tools = [search_products, calculate_delivery_time, generate_image, DatabaseQueryTool()]
agent_executor = AgentExecutor(
agent=chain,
tools=tools,
verbose=True,
memory=memory
) |
|
В своих проектах я часто группирую инструменты по функциональности и создаю специализированных агентов. Например, агент для работы с клиентами получает инструменты для поиска заказов и расчета доставки, а аналитический агент — инструменты для запросов к базе данных и визуализации.
Важный момент: не перегружайте агента слишком большим количеством инструментов. Я заметил, что с увеличением их числа агент начинает путаться и выбирать не самые подходящие. Лучше иметь меньше, но более специализированных инструментов с говорящими названиями и четкими описаниями.
Интеграция с watsonx.ai и выбор языковых моделей
После того как я разобрался с инструментами, встал следующий важный вопрос: какую языковую модель использовать в качестве "мозга" агента? Перепробовав массу вариантов, я в итоге остановился на платформе watsonx.ai от IBM — и это оказалось одним из лучших решений в проекте. Интеграция LangChain с watsonx.ai оказалась удивительно простой. Все что нужно — установить пакет langchain_ibm и настроить подключение:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| from langchain_ibm import WatsonxLLM
# Параметры для модели
param = {
"decoding_method": "greedy",
"temperature": 0,
"min_new_tokens": 5,
"max_new_tokens": 250,
"stop_sequences": ["\n\n"]
}
# Инициализация модели
model = WatsonxLLM(
model_id = "ibm/granite-13b-chat-v2",
url = credentials.get("url"),
apikey = credentials.get("apikey"),
project_id = credentials.get("project_id"),
params = param
) |
|
Что меня особенно порадовало в watsonx.ai — это возможность выбирать из целого семейства моделей Granite. В своих экспериментах я перепробовал несколько вариантов и заметил интересные закономерности:
Granite-13b-chat-v2 показала лучший баланс между качеством и скоростью для диалоговых сценариев,
Granite-20b-coder лучше справлялась с задачами, связаными с кодом и структурированными данными,
Меньшие модели (7b) работали заметно быстрее, но чаще "галлюцинировали" в сложных сценариях,
Важный момент, который я выяснил на личном опыте — правильная настройка параметров декодирования критически важна для работы агента. Температура, например, напрямую влияет на "креативность" модели. Для инструментальных агентов я обычно устанавливаю температуру близкой к 0, чтобы получать более детерминированные результаты:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Параметры для инструментального агента
instrument_params = {
"decoding_method": "greedy",
"temperature": 0, # Минимальная вариативность
"max_new_tokens": 300,
"stop_sequences": ["\n\nObservation:"] # Важно для ReAct паттерна!
}
# Параметры для креативного агента
creative_params = {
"decoding_method": "sample",
"temperature": 0.7, # Больше вариативности
"top_p": 0.9,
"max_new_tokens": 500
} |
|
Еще одно неочевидное, но важное наблюдение: параметр stop_sequences играет огромную роль в паттерне ReAct. Если настроить его неправильно, агент может зациклиться или начать генерировать бессмысленные последовательности действий.
Выбор оптимальных параметров для различных типов задач
Настройка параметров модели — это настоящее искуство, в котором я набивал шишки не один месяц. Через пробы и ошибки я вывел для себя несколько правил, которые помогают мне быстрее находить оптимальные настройки для разных задач.
Для задач, требующих точности и детерминированости (например, извлечение структурированных данных или работа с фактами), я использую следующие параметры:
| Python | 1
2
3
4
5
6
7
| precision_params = {
"decoding_method": "greedy",
"temperature": 0,
"top_k": 1,
"repetition_penalty": 1.0,
"max_new_tokens": 200
} |
|
Эта конфигурация практически исключает случайность и заставляет модель выбирать наиболее вероятные токены. Однако для задач генерации творческого контента (описания, сюжеты, идеи) такие настройки убивают креативность. Тут лучше использовать:
| Python | 1
2
3
4
5
6
7
8
| creative_params = {
"decoding_method": "sample",
"temperature": 0.8,
"top_p": 0.9,
"top_k": 50,
"repetition_penalty": 1.1,
"max_new_tokens": 500
} |
|
Для диалоговых агентов я обычно нахожу золотую середину:
| Python | 1
2
3
4
5
6
7
| conversation_params = {
"decoding_method": "sample",
"temperature": 0.4,
"top_p": 0.85,
"repetition_penalty": 1.07,
"max_new_tokens": 300
} |
|
Особая история с многошаговыми агентами, использующими ReAct-паттерн. Тут критично важно правильно настроить stop_sequences. Я обнаружил, что для Granite моделей отлично работает такая конфигурация:
| Python | 1
2
3
4
5
6
| react_params = {
"decoding_method": "greedy",
"temperature": 0.1, # Небольшая вариативность все же нужна
"max_new_tokens": 250,
"stop_sequences": ["Observation:", "Human:", "\n\n"]
} |
|
Эти стоп-последовательности предотвращают "галлюцинации", когда модель начинает генерировать фиктивные наблюдения вместо использования инструментов.
Для задач по генерации кода лучше всего подходят более "холодные" настройки с повышенным штрафом за повторения:
| Python | 1
2
3
4
5
6
| code_params = {
"decoding_method": "greedy",
"temperature": 0.2,
"repetition_penalty": 1.2,
"max_new_tokens": 400
} |
|
Экспериментальным путем я выяснил, что для большинства задач оптимальное значение max_new_tokens примерно в 1.5-2 раза больше ожидаемой длины ответа. Это дает модели достаточно "пространства для маневра", но предотвращает излишнюю многословность.
Практическая реализация
В этом разделе мы пройдем весь путь от нуля до работающего агента. Я покажу, как настроить окружение Python, организовать конфигурацию, подключить внешние источники данных и создать агента, который действительно решает задачи. Причем мы не будем ограничиваться примитивными примерами — построим полноценное приложение, которое можно взять за основу для ваших проектов.
Настройка окружения Python
Первым делом, конечно же, виртуальное окружение. Я предпочитаю использовать venv — он прост и встроен в Python:
| Python | 1
2
3
4
5
6
7
8
| # Создаем виртуальное окружение
python -m venv langchain_env
# Активируем его
# На Windows:
langchain_env\Scripts\activate
# На Linux/Mac:
source langchain_env/bin/activate |
|
После активации окружения установим нужные библиотеки. Вот минимальный набор, без которого наш агент не заработает:
| Bash | 1
| pip install langchain langchain_ibm langchain_core IPython nasapy |
|
Обратите внимание на пакет langchain_ibm — это именно то, что нам нужно для работы с моделями watsonx.ai. А nasapy — это библиотека для работы с API NASA, которую мы будем использовать в нашем примере.
Если вы планируете работать в Jupyter Notebook (я обычно именно так и делаю для экспериментов), установите его тоже:
| Bash | 1
| pip install jupyter notebook |
|
Для удобства я всегда создаю файл .env в корне проекта, чтобы хранить API ключи и другие чувствительные данные:
| Bash | 1
2
| touch .env # На Linux/Mac
# На Windows можно просто создать файл в текстовом редакторе |
|
И добавляю в него необходимые переменные:
| Python | 1
2
3
4
| WATSONX_API_KEY=ваш_ключ_api
WATSONX_URL=https://us-south.ml.cloud.ibm.com
WATSONX_PROJECT_ID=ваш_id_проекта
NASA_API_KEY=DEMO_KEY # Или ваш собственный ключ |
|
Чтобы убедиться, что всё установлено правильно, я обычно запускаю простой тест:
| Python | 1
2
3
4
5
| import langchain
import nasapy
print(f"LangChain версия: {langchain.__version__}")
n = nasapy.Nasa(key='DEMO_KEY')
print("Подключение к NASA API успешно!") |
|
Если видите версию LangChain и сообщение об успешном подключении — вы на правильном пути! Теперь можно приступать к построению нашего агента.
Работа с переменными окружения и конфигурацией
Хранение чувствительных данных в коде — это боль, которую я испытал на себе, когда случайно запушил свои API-ключи в публичный репозиторий. С тех пор я параноидально отношусь к конфигурации и переменным окружения, и сейчас поделюсь своими наработками.
В предыдущем разделе я упомянул файл .env, но просто создать его недостаточно. Нужно еще научить наше приложение его читать. Для этого я использую библиотеку python-dotenv:
| Bash | 1
| pip install python-dotenv |
|
И вот как я обычно загружаю конфигурацию в своих проектах:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import os
from dotenv import load_dotenv
# Загружаем переменные из .env файла
load_dotenv()
# Теперь можно получить переменные через os.environ
api_key = os.environ.get("WATSONX_API_KEY")
project_id = os.environ.get("WATSONX_PROJECT_ID")
url = os.environ.get("WATSONX_URL")
# Проверяем, что ключи загрузились
if not api_key or not project_id:
raise ValueError("Не удалось загрузить API ключи из .env файла") |
|
Для более сложных проектов я предпочитаю создавать отдельный класс конфигурации, который загружает не только переменные окружения, но и настройки из JSON или YAML файлов:
| 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
| import os
import json
from dotenv import load_dotenv
class Config:
def __init__(self, config_file=None):
# Загружаем переменные окружения
load_dotenv()
# Базовые настройки
self.api_key = os.environ.get("WATSONX_API_KEY")
self.project_id = os.environ.get("WATSONX_PROJECT_ID")
self.url = os.environ.get("WATSONX_URL", "https://us-south.ml.cloud.ibm.com")
# Загружаем дополнительные настройки из файла, если он указан
if config_file and os.path.exists(config_file):
with open(config_file, "r") as f:
config_data = json.load(f)
self.__dict__.update(config_data)
def validate(self):
"""Проверяем, что все необходимые настройки присутствуют"""
required = ["api_key", "project_id"]
missing = [field for field in required if not getattr(self, field, None)]
if missing:
raise ValueError(f"Отсутствуют обязательные настройки: {', '.join(missing)}")
return True |
|
Использование такого класса упрощает управление конфигурацией и делает код более чистым:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| # Создаем и валидируем конфигурацию
config = Config("config.json")
config.validate()
# Используем настройки
model = WatsonxLLM(
model_id="ibm/granite-13b-chat-v2",
url=config.url,
apikey=config.api_key,
project_id=config.project_id,
params=param
) |
|
Еще один хак, который я часто использую — разные конфигурации для разных окружений. Например, можно создать файлы .env.dev, .env.prod и загружать нужный в зависимости от текущего окружения:
| Python | 1
2
3
4
5
6
7
8
| import os
from dotenv import load_dotenv
# Определяем текущее окружение
env = os.environ.get("ENVIRONMENT", "dev")
# Загружаем соответствующий файл
load_dotenv(f".env.{env}") |
|
Такой подход особенно удобен, когда вы работаете с разными моделями или API на этапе разработки и в продакшене.
Секреты работы с API ключами и rate limiting
Знаете, что может быстро превратить работающего агента в бесполезную поделку? Правильно, проблемы с API ключами и rate limiting. Наступал на эти грабли столько раз, что теперь у меня есть целая коллекция лайфхаков.
Первое правило работы с API ключами — никогда не хардкодить их в приложении. Кроме файла .env я часто использую сервисы управления секретами. Для локальной разработки подойдет даже простой класс-обертка:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class ApiKeyManager:
def __init__(self, keys_dict):
self._keys = {service: keys for service, keys in keys_dict.items()}
self._usage_count = {service: {key: 0 for key in keys} for service, keys in self._keys.items()}
def get_key(self, service):
"""Возвращает ключ с наименьшим использованием"""
if service not in self._keys:
raise ValueError(f"Сервис {service} не найден")
keys = self._keys[service]
usage = self._usage_count[service]
# Выбираем ключ с минимальным количеством использований
key = min(keys, key=lambda k: usage[k])
usage[key] += 1
return key |
|
С rate limiting обычно работаю через механизм повторных попыток с экспоненциальной задержкой. Добавляем декоратор для функций, которые обращаются к 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
| import time
import random
from functools import wraps
def retry_with_backoff(max_retries=3, base_delay=1, max_delay=60):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
retries = 0
while retries <= max_retries:
try:
return func(*args, **kwargs)
except Exception as e:
if "rate limit" not in str(e).lower() and "too many requests" not in str(e).lower():
raise # Пробрасываем ошибку, если она не связана с rate limiting
retries += 1
if retries > max_retries:
raise
# Экспоненциальная задержка с случайным фактором
delay = min(base_delay * (2 ** (retries - 1)) + random.uniform(0, 1), max_delay)
print(f"Rate limit превышен. Повторная попытка через {delay:.2f} секунд...")
time.sleep(delay)
return func(*args, **kwargs)
return wrapper
return decorator |
|
Применяем его к нашим инструментам:
| Python | 1
2
3
4
5
6
| @tool
@retry_with_backoff()
def get_astronomy_image(date: str):
"""Получает астрономическое изображение дня NASA на указанную дату."""
apod = n.picture_of_the_day(date, hd=True)
return apod['url'] |
|
Для продвинутых случаев я создаю пул API ключей и балансирую нагрузку между ними. Это особенно полезно при работе с агентами, которые интенсивно используют внешние сервисы.
Создание базового агента с примерами кода
Ну что, настало время собрать воедино все компоненты и наконец-то создать нашего агента! Я помню, как впервые столкнулся с этой задачей — в документации куча примеров, но как это всё работает вместе, было совсем неочевидно. Поэтому сейчас покажу пошагово весь процесс на примере нашего NASA-бота.
Начнем с определения инструментов, которые будут доступны агенту. Помните, мы уже создали функцию для получения текущей даты и астрономической картинки? Соберем их в список:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @tool
def get_todays_date() -> str:
"""Получает текущую дату в формате ГГГГ-ММ-ДД."""
from datetime import datetime
date = datetime.now().strftime("%Y-%m-%d")
return date
@tool(return_direct=True)
def get_astronomy_image(date: str):
"""Получает астрономическое изображение дня NASA на указанную дату. Дата в формате ГГГГ-ММ-ДД."""
apod = n.picture_of_the_day(date, hd=True)
return apod['url']
tools = [get_todays_date, get_astronomy_image] |
|
Теперь самое интересное — создание системного промпта, который определит поведение агента. Это как операционная система для нашего ИИ-помощника:
| 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
| system_prompt = """Отвечай пользователю максимально полезно и точно. У тебя есть доступ к следующим инструментам: {tools}
Используй JSON для вызова инструмента, указывая "action" и "action_input".
Допустимые значения для "action": "Final Answer" или {tool_names}
Формат должен быть таким:
{{
"action": НАЗВАНИЕ_ИНСТРУМЕНТА,
"action_input": ВХОДНЫЕ_ДАННЫЕ
}}
Следуй этому формату:
Вопрос: вопрос пользователя
Мысль: обдумай задачу
Действие:
$JSON_БЛОК
Наблюдение: результат действия
... (повтори Мысль/Действие/Наблюдение сколько нужно)
Мысль: я знаю ответ
Действие:
{{
"action": "Final Answer",
"action_input": "Итоговый ответ пользователю"
}}
""" |
|
Добавим шаблон для человеческого промпта и объединим их:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| human_prompt = """{input}
{agent_scratchpad}"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
MessagesPlaceholder("chat_history", optional=True),
("human", human_prompt),
])
# Добавим информацию об инструментах
prompt = prompt.partial(
tools=render_text_description_and_args(list(tools)),
tool_names=", ".join([t.name for t in tools]),
) |
|
Дальше настраиваем память агента, чтобы он помнил предыдущие взаимодействия:
| Python | 1
| memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True) |
|
И финальный аккорд — сборка всех компонентов в единый механизм:
| Python | 1
2
3
4
5
6
7
8
| chain = (RunnablePassthrough.assign(
agent_scratchpad=lambda x: format_log_to_str(x["intermediate_steps"]),
chat_history=lambda x: memory.chat_memory.messages,
)
| prompt | model | JSONAgentOutputParser()
)
agent_executor = AgentExecutor(agent=chain, tools=tools, verbose=True, memory=memory) |
|
Вуаля! Наш агент готов к использованию. Теперь его можно вызвать так:
| Python | 1
2
| response = agent_executor.invoke({"input": "Покажи мне астрономическую картинку дня от NASA на сегодня"})
print(response["output"]) |
|
Подключение внешних источников данных
Создавая первых агентов, я быстро понял, что без внешних источников данных они превращаются в обычные чат-боты. А вот когда у них появляется доступ к реальным данным — начинается настоящая магия! В моем случае NASA API был только началом эксперементов. Самый простой способ подключить внешние данные — это, конечно, API. LangChain делает эту задачу на удивление простой. Например, подключение к погодному API выглядит примерно так:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| @tool
def get_weather(location: str) -> str:
"""Получает текущую погоду для указанного местоположения."""
api_key = os.getenv("WEATHER_API_KEY")
url = f"https://api.weatherapi.com/v1/current.json?key={api_key}&q={location}&aqi=no"
response = requests.get(url)
if response.status_code != 200:
return f"Ошибка при получении погоды: {response.status_code}"
data = response.json()
return f"Температура: {data['current']['temp_c']}°C, Условия: {data['current']['condition']['text']}" |
|
Но настоящий прорыв для меня случился, когда я научился подключать базы данных. Представьте агента, который может запрашивать информацию прямо из вашей PostgreSQL!
| Python | 1
2
3
4
5
6
7
8
9
10
11
| from langchain.utilities import SQLDatabase
from langchain_community.tools import SQLDatabaseTool
# Подключаемся к базе данных
db = SQLDatabase.from_uri("postgresql://user:password@localhost:5432/mydatabase")
# Создаем инструмент для работы с БД
sql_tool = SQLDatabaseTool(db=db)
# Добавляем его в список инструментов
tools.append(sql_tool) |
|
С этим инструментом агент сможет генерировать и выполнять SQL-запросы! Правда, нужно быть осторожным — по умолчанию у агента будет полный доступ к базе. В продакшене лучше ограничить права или создать специализированного пользователя только для чтения. Для работы с файловыми системами я часто использую связку из нескольких инструментов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @tool
def read_file(file_path: str) -> str:
"""Читает содержимое файла по указанному пути."""
try:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
return f"Ошибка при чтении файла: {str(e)}"
@tool
def list_directory(directory_path: str) -> str:
"""Показывает содержимое директории."""
try:
files = os.listdir(directory_path)
return "\n".join(files)
except Exception as e:
return f"Ошибка при чтении директории: {str(e)}" |
|
Такие инструменты открывают агенту доступ к локальной файловой системе, что может быть очень полезно для работы с конфигурационными файлами, логами или датасетами.
Streaming ответов и работа с асинхронностью
Когда я создавал своего первого "серьезного" агента, пользователи жаловались на мучительное ожидание ответов. Агент думал по 10-15 секунд, а затем выдавал весь результат разом. Это было ужасно с точки зрения UX. И тут я открыл для себя стриминг ответов в LangChain — настоящее спасение! Стриминг позволяет получать ответ по частям, по мере его генерации. Пользователь видит, как ответ формируется в реальном времени, и это радикально улучшает восприятие даже при медленных моделях:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
# Инициализируем модель с поддержкой стриминга
streaming_model = WatsonxLLM(
model_id="ibm/granite-13b-chat-v2",
url=credentials.get("url"),
apikey=credentials.get("apikey"),
project_id=credentials.get("project_id"),
params=param,
streaming=True, # Включаем стриминг!
callbacks=[StreamingStdOutCallbackHandler()]
)
# Используем модель с поддержкой стриминга в цепочке
streaming_chain = (RunnablePassthrough.assign(
agent_scratchpad=lambda x: format_log_to_str(x["intermediate_steps"]),
chat_history=lambda x: memory.chat_memory.messages,
) | prompt | streaming_model | JSONAgentOutputParser())
streaming_agent = AgentExecutor(agent=streaming_chain, tools=tools, verbose=True) |
|
Для веб-приложений я обычно создаю генератор, который можно использовать с фреймворками типа Flask или FastAPI:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| def stream_agent_response(query):
chunks = []
def collector(chunk):
chunks.append(chunk)
yield chunk
# Создаем кастомный обработчик для сбора чанков
callback = StreamingCallbackHandler(collector)
# Запускаем агента с этим обработчиком
streaming_agent.invoke(
{"input": query},
config={"callbacks": [callback]}
)
# Возвращаем полный ответ в конце
return "".join(chunks) |
|
Но стриминг — это только половина дела. Настоящий прорыв произошел, когда я начал использовать асинхронные вызовы. LangChain поддерживает асинхронное выполнение практически всех операций через методы с префиксом a:
| Python | 1
2
3
4
5
6
7
8
9
10
11
| import asyncio
async def process_multiple_queries(queries):
# Запускаем обработку всех запросов параллельно
tasks = [streaming_agent.ainvoke({"input": q}) for q in queries]
results = await asyncio.gather(*tasks)
return [r["output"] for r in results]
# Использование
queries = ["Какая сегодня дата?", "Покажи астрономическую картинку за вчера"]
results = asyncio.run(process_multiple_queries(queries)) |
|
Комбинация стриминга и асинхронности дала моему приложению супер-способности. Теперь оно может обрабатывать десятки запросов одновременно, показывая промежуточные результаты в реальном времени. При работе с несколькими пользователями это просто незаменимо.
Интеграция с векторными базами данных
Одно из самых крутых открытий в моей работе с LangChain — это интеграция с векторными базами данных. Когда я впервые столкнулся с необходимостью обеспечить агенту долговременную память, я перепробовал кучу подходов, но именно векторные БД изменили правила игры. По сути, векторные базы — это специализированные хранилища для эмбеддингов, то есть числовых представлений текста. В отличие от обычных реляционных БД, они позволяют искать не по точному совпадению, а по семантической близости. Это как разница между "найти книгу с названием X" и "найти книги, похожие по смыслу на X".
Вот как я обычно подключаю Chroma, одну из самых удобных векторных БД:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from langchain.vectorstores import Chroma
from langchain.embeddings import WatsonxEmbeddings
# Инициализируем модель для создания эмбеддингов
embeddings = WatsonxEmbeddings(
url=credentials.get("url"),
apikey=credentials.get("apikey"),
project_id=credentials.get("project_id"),
model_id="ibm/granite-embeddings"
)
# Создаем или подключаемся к векторному хранилищу
vectorstore = Chroma(
collection_name="knowledge_base",
embedding_function=embeddings,
persist_directory="./chroma_db"
)
# Добавляем документы в базу
vectorstore.add_texts(["Земля — третья планета от Солнца", "Марс — четвертая планета от Солнца"]) |
|
Теперь самое интересное — превращаем векторное хранилище в инструмент для агента:
| Python | 1
2
3
4
5
6
7
| @tool
def search_knowledge_base(query: str) -> str:
"""Ищет информацию в базе знаний по запросу."""
results = vectorstore.similarity_search(query, k=2)
if not results:
return "Информация не найдена."
return "\n".join([doc.page_content for doc in results]) |
|
На практике я заметил, что векторные БД особенно хороши в двух сценариях: долговременная память агента и поиск по большим корпусам текстов. Например, в одном проекте я загрузил в Chroma всю документацию продукта, и агент начал отвечать на технические вопросы с поразительной точностю. Если вы работаете с большими объемами данных, обратите внимание на FAISS от Facebook — он быстрее Chroma, но требует больше настройки. А для продакшн-сценариев я обычно выбираю Pinecone или Weaviate — они поддерживают шардинг и репликацию, что критично при высоких нагрузках.
Продвинутые возможности
Когда я только начинал эксперементировать с LangChain, я и представить не мог, насколько гибкой и мощной может быть эта библиотека. Постепенно я открывал для себя всё новые и новые грани: от техники RAG для обогащения контекста до мультимодальных агентов, работающих не только с текстом, но и с изображениями.
В следующих разделах я расскажу о фичах, которые произвели на меня самое сильное впечатление: как настроить долговременную память агента, улучшить качество ответов с помощью RAG, обрабатывать сложные запросы и управлять состоянием. Некоторые из этих техник потребуют более глубокого понимания архитектуры LangChain, но поверьте — это того стоит.
Использование RAG для улучшения качества ответов
Ранее я упоминал о векторных базах данных, но не рассказал о самом мощном их применении — технологии RAG (Retrieval-Augmented Generation). Когда я впервые применил этот подход, качество ответов моего агента выросло просто в разы.
Суть RAG проста: перед генерацией ответа агент ищет релевантную информацию во внешних источниках, и только потом формулирует результат. Это как разница между студентом, который пытается вспомнить материал на экзамене, и профессионалом, который сначала проверяет факты, а потом даёт заключение.
В LangChain реализовать RAG просто:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| from langchain.chains import RetrievalQA
# Создаем объект для поиска
retriever = vectorstore.as_retriever()
# Строим RAG-цепочку
rag_chain = RetrievalQA.from_chain_type(
llm=model,
chain_type="stuff", # "stuff" - все найденные документы передаются в промпт
retriever=retriever,
verbose=True
)
# Используем цепочку
answer = rag_chain.invoke({"query": "Какая планета четвертая от Солнца?"}) |
|
Этот простой паттерн я довёл до совершенства, добавив предварительную обработку запроса. Дело в том, что прямое использование пользовательского вопроса для поиска не всегда даёт лучшие результаты. Вместо этого я сначала генерирую оптимальные поисковые запросы:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| def generate_search_queries(question):
# Генерируем несколько поисковых запросов для исходного вопроса
query_gen_prompt = f"""
На основе вопроса пользователя, создай 3 поисковых запроса для поиска релевантной информации.
Запросы должны быть краткими и содержать ключевые слова.
Вопрос: {question}
Поисковые запросы:"""
response = model.invoke(query_gen_prompt)
queries = [q.strip() for q in response.split("\n") if q.strip()]
return queries[:3] # Берем первые три запроса |
|
Затем для каждого запроса я выполняю поиск и объединяю результаты:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| def enhanced_rag_search(question):
queries = generate_search_queries(question)
all_docs = []
for query in queries:
docs = vectorstore.similarity_search(query, k=2)
all_docs.extend(docs)
# Удаляем дубликаты
unique_docs = list({doc.page_content: doc for doc in all_docs}.values())
# Возвращаем контекст
return "\n".join([doc.page_content for doc in unique_docs]) |
|
RAG особенно эффективен для агентов, которым нужна актуальная информация или специфические знания. В одном из проектов мне пришлось загрузить в векторную базу несколько гигабайт технической документации, и агент начал отвечать на вопросы так, будто проработал с этим оборудованием десять лет.
Обработка сложных запросов и контекста
Любой, кто хоть день поработал с агентами, знает — пользователи обожают загадывать загадки. Вместо чётких инструкций они выдают что-то вроде "помоги с этой штукой как на прошлой неделе, только для нового клиента". И тут без правильной обработки контекста агент превращается в беспомощного болванчика. Первое, с чем я столкнулся — неспособность базовых агентов понимать сложные многоэтапные запросы. Решение нашлось в технике декомпозиции запроса:
| Python | 1
2
3
4
5
6
7
8
9
| def decompose_complex_query(query):
decomposition_prompt = f"""
Раздели следующий сложный запрос на последовательность простых подзадач:
{query}
Подзадачи:"""
tasks = model.invoke(decomposition_prompt).strip().split("\n")
return [task.strip() for task in tasks if task.strip()] |
|
Эта функция превращает монстра вроде "сравни прогноз погоды в Москве и Питере на следующую неделю и скажи, где будет теплее в среднем" в список конкретных шагов.
Для сохранения контекста между вызовами я разработал систему "контекстных переменных":
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| class ContextManager:
def __init__(self):
self.context = {}
def set(self, key, value):
self.context[key] = value
def get(self, key, default=None):
return self.context.get(key, default)
def get_all_as_text(self):
return "\n".join([f"{k}: {v}" for k, v in self.context.items()]) |
|
Теперь мой агент может хранить промежуточные результаты и обращаться к ним при необходимости. А самый большой прорыв случился, когда я начал использовать "саморефлексию" — перед ответом агент анализирует свой собственный черновик и улучшает его:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
| def self_reflection_enhancement(draft_answer, question):
reflection_prompt = f"""
Вопрос: {question}
Черновик ответа: {draft_answer}
Проанализируй черновик ответа:
1. Отвечает ли он на все аспекты вопроса?
2. Есть ли логические ошибки?
3. Достаточно ли контекста?
Улучшенный ответ:"""
return model.invoke(reflection_prompt) |
|
Эта техника радикально повысила качество ответов на сложные вопросы и показала, что иногда лучший критик агента — он сам.
Кастомизация промптов для специфических доменов
Отдельная история - кастомизация промптов под конкретные предметные области. Когда я впервые попытался использовать своего универсального агента для медицинской тематики, результаты были настолько средними, что мне стало стыдно показывать их клиенту.
Дело в том, что универсальные промпты - как спортивная обувь "для всего": вроде и бегать можно, и в тренажерку сходить, но для профессионального бега нужны специальные кроссовки. Так и с промптами - для каждой специфической области нужна своя настройка. Моим открытием стала техника "доменной адаптации" промптов. Вот простой, но эффективный шаблон:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| domain_specific_template = """Ты эксперт в области {domain}.
Используй профессиональную терминологию из этой сферы: {terminology}.
При ответе опирайся на основные концепции: {key_concepts}.
Формат ответа должен соответствовать стандартам этой области.
Запрос пользователя: {query}
"""
legal_prompt = domain_specific_template.format(
domain="юриспруденция",
terminology="иск, ответчик, истец, арбитраж, юрисдикция",
key_concepts="презумпция невиновности, состязательность сторон, бремя доказывания",
query=user_query
) |
|
Когда я стал использовать такой подход, точность ответов в специфических доменах выросла примерно на 40%. Особенно хорошо это работает в сочетании с Few-shot примерами, которые демонстрируют ожидаемый формат ответа:
| Python | 1
2
3
4
5
6
7
8
| medical_prompt = f"""Ты медицинский консультант. Отвечай строго по медицинским стандартам.
Пример 1:
Вопрос: Болит голова и температура 38.
Ответ: Ваши симптомы могут указывать на вирусную инфекцию или грипп. Рекомендую обильное питье, жаропонижающее и консультацию врача, если симптомы не улучшатся в течение 24 часов.
Ваш вопрос: {query}
""" |
|
Для финансовой сферы я обязательно включаю в промпт указание на необходимость точности в цифрах и ссылки на регуляторные требования. Для технических доменов добавляю напоминание использовать актуальные стандарты и технологии.
Multi-modal агенты: работа с текстом, изображениями и документами в единой системе
Если вы думаете, что крутость LangChain ограничивается текстом — подождите, сейчас я расскажу, как она взорвала мой мозг, когда я начал экспериментировать с мультимодальными агентами. Представьте агента, который одновременно понимает содержимое PDF-документа, анализирует изображения и выдаёт связные ответы на основе всех этих источников. Звучит как научная фантастика? А я уже такое делаю.
Переход от текстового к мультимодальному агенту оказался проще, чем я ожидал. Ключевым моментом стало подключение специализированных моделей для работы с разными типами данных:
| Python | 1
2
3
4
5
6
7
8
9
| from langchain_community.document_loaders import PyPDFLoader
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_ibm.chat_models import WatsonxChat
from langchain_openai import OpenAIVisionChat # Для работы с изображениями
# Модель для текста
text_model = WatsonxChat(model_id="ibm/granite-13b-chat-v2")
# Модель для обработки изображений
vision_model = OpenAIVisionChat() |
|
Далее я создал специализированные инструменты для каждого типа контента:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @tool
def analyze_image(image_url: str) -> str:
"""Анализирует содержимое изображения и возвращает его описание."""
response = vision_model.invoke([
{"type": "text", "text": "Опиши детально, что изображено на картинке."},
{"type": "image_url", "image_url": image_url}
])
return response.content
@tool
def extract_text_from_pdf(pdf_url: str) -> str:
"""Извлекает текст из PDF документа по указанной ссылке."""
loader = PyPDFLoader(pdf_url)
pages = loader.load()
return "\n".join(page.page_content for page in pages) |
|
Самое интересное началось, когда я объединил эти инструменты в единую систему. Для этого пришлось создать специальный маршрутизатор, который анализирует запрос и решает, какую модальность использовать:
| Python | 1
2
3
4
5
6
7
8
9
| def route_by_content_type(query):
if "изображени" in query.lower() or "картинк" in query.lower() or "фото" in query.lower():
return "vision"
elif "pdf" in query.lower() or "документ" in query.lower() or "файл" in query.lower():
return "document"
else:
return "text"
# А дальше уже используем это в цепочке принятия решений |
|
В боевом режиме я столкнулся с интересной проблемой: агент не всегда понимал, когда переключаться между модальностями. Решение нашлось в использовании промежуточного анализатора запросов:
| Python | 1
2
3
4
5
6
7
8
9
10
| def analyze_query_needs(query):
analysis_prompt = f"""
Определи, какие типы данных нужны для ответа на этот запрос:
"{query}"
Укажи необходимые типы (текст/изображения/документы):
"""
analysis = text_model.invoke(analysis_prompt)
return analysis |
|
Этот подход позволил создавать агентов, которые действительно понимают мир во всём его многообразии, а не только через призму текста. Особенно эффективным оказалось использование мультимодальных агентов для задач, связанных с анализом визуального контента и извлечением информации из структурированных документов.
Управление памятью агента и сохранение состояния
Управление памятью агента — одна из тех проблем, которые я не воспринимал всерьез, пока не запустил своего первого бота в продакшн. И тут клиенты начали задавать очевидный вопрос: "А почему он не помнит, что я говорил ему вчера?" Ох уж эти пользователи со своими логичными ожиданиями! В базовой версии LangChain предлагает несколько типов памяти, и самая простая из них — ConversationBufferMemory, которую мы уже использовали. Но это как оперативка компьютера — перезагрузил, и всё исчезло. Для долговременного хранения нужно что-то посерьезнее:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| from langchain.memory import ConversationBufferWindowMemory, ConversationSummaryMemory
from langchain.memory.chat_message_histories import RedisChatMessageHistory
# Память с ограниченным окном (хранит только N последних сообщений)
window_memory = ConversationBufferWindowMemory(
k=5, # Хранить только 5 последних взаимодействий
return_messages=True
)
# Память с автоматическим суммированием (экономит токены)
summary_memory = ConversationSummaryMemory(
llm=model, # Модель для создания саммари
return_messages=True
) |
|
Чтобы сохранять память между перезапусками, я интегрировал Redis — это дало невероятную гибкость:
| Python | 1
2
3
4
5
6
7
8
9
10
11
| message_history = RedisChatMessageHistory(
url="redis://localhost:6379/0",
session_id=user_id, # Уникальный ID пользователя
ttl=604800 # Время жизни в секундах (неделя)
)
persistent_memory = ConversationBufferMemory(
memory_key="chat_history",
chat_memory=message_history,
return_messages=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
| class PersistentAgentState:
def __init__(self, user_id, storage_path="./states"):
self.user_id = user_id
self.storage_path = storage_path
os.makedirs(storage_path, exist_ok=True)
self.state_file = os.path.join(storage_path, f"{user_id}.json")
self.state = self._load_state()
def _load_state(self):
if os.path.exists(self.state_file):
with open(self.state_file, "r") as f:
return json.load(f)
return {} # Пустое состояние по умолчанию
def save(self):
with open(self.state_file, "w") as f:
json.dump(self.state, f)
def set(self, key, value):
self.state[key] = value
self.save()
def get(self, key, default=None):
return self.state.get(key, default) |
|
В продакшене я заменил файловую систему на MongoDB — она лучше масштабируется и поддерживает разные типы данных. Главный урок, который я извлек: никогда не надейтесь на память в оперативке — пользователи обязательно вернутся через неделю и будут ожидать, что агент помнит весь контекст.
Интеграция с Celery для фоновой обработки задач агента
В какой-то момент я столкнулся с типичной проблемой тяжелых ИИ-систем — слишком долгое время отклика. Пользователи ненавидят ждать, а сложные запросы к LLM могут обрабатываться десятки секунд. Вот тут на сцену и выходит Celery — система для асинхронной обработки задач, которая спасла мой проект от гнева нетерпеливых клиентов. Интеграция LangChain с Celery оказалась на удивление простой:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from celery import Celery
import os
# Настраиваем Celery с Redis в качестве брокера сообщений
app = Celery('langchain_tasks',
broker='redis://localhost:6379/1',
backend='redis://localhost:6379/2')
# Определяем задачу для выполнения запроса к агенту
@app.task
def process_agent_query(query, user_id):
# Загружаем или создаем агента
agent = load_agent_for_user(user_id)
# Выполняем запрос
try:
result = agent.invoke({"input": query})
return {"status": "success", "output": result["output"]}
except Exception as e:
return {"status": "error", "message": str(e)} |
|
Теперь вместо блокирующего вызова агента мы можем запустить задачу в фоне:
| Python | 1
2
3
4
5
| # Запускаем задачу асинхронно
task = process_agent_query.delay("Расскажи о звездах", user_id="user123")
# Возвращаем ID задачи пользователю
return {"task_id": task.id, "message": "Запрос обрабатывается, проверьте результат через несколько секунд"} |
|
Для проверки статуса задачи я создал простой эндпоинт:
| Python | 1
2
3
4
5
6
| def check_task_status(task_id):
task = process_agent_query.AsyncResult(task_id)
if task.ready():
return task.result
else:
return {"status": "processing"} |
|
Особенно удобно это работает для агентов с долгими цепочками рассуждений. Пользователь может заниматься своими делами, пока агент методично решает задачу в фоне. А чтобы сделать опыт еще лучше, я добавил вебсокеты для уведомлений о завершении:
| Python | 1
2
3
4
5
6
7
8
| def notify_task_completion(task_id, websocket_id):
# Периодически проверяем статус задачи
task = process_agent_query.AsyncResult(task_id)
if task.ready():
# Отправляем результат через вебсокет
send_to_websocket(websocket_id, task.result)
return True
return False |
|
Celery также отлично помогает с планированием периодических задач. Например, агент может регулярно обновлять свою базу знаний:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| @app.task
def update_agent_knowledge_base():
# Код для обновления базы знаний
pass
# Запускаем задачу каждые 24 часа
app.conf.beat_schedule = {
'update-knowledge-daily': {
'task': 'tasks.update_agent_knowledge_base',
'schedule': 86400.0, # 24 часа в секундах
},
} |
|
Создание агента с долгосрочной памятью на основе графовых структур
Когда я глубже погрузился в разработку серьезных агентов, столкнулся с фундаментальной проблемой: простые списки сообщений и суммаризация не справляются с по-настоящему долговременной памятью. Агент помнит последние пять взаимодействий, но понятия не имеет, что вы обсуждали месяц назад. И тут на помощь пришли графовые структуры — настоящий прорыв в организации "мозгов" ИИ-систем. Суть подхода в том, что вместо линейного хранения диалогов мы строим граф связанных концепций. Каждый узел — это сущность (человек, событие, понятие), а рёбра — отношения между ними. Такая структура гораздо ближе к тому, как работает человеческая память.
Для реализации я использовал Neo4j — популярную графовую базу данных:
| 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
| from langchain.graphs import Neo4jGraph
from langchain_community.graphs.graph_document import (
Node, Relationship, GraphDocument
)
# Подключаемся к Neo4j
graph = Neo4jGraph(
url="bolt://localhost:7687",
username="neo4j",
password="password"
)
# Создаем узлы и связи на основе извлеченной информации
def update_knowledge_graph(information, user_id):
# Извлекаем сущности и отношения из текста
extraction_prompt = f"""
Извлеки из текста все упомянутые сущности (люди, места, концепции)
и отношения между ними:
{information}
"""
extracted = model.invoke(extraction_prompt)
# Парсим результат (в реальном коде использовал бы структурированный формат)
# Создаем документ для графа
doc = GraphDocument(
nodes=[
Node(id="user123", type="User", properties={"name": "Алексей"}),
Node(id="pref1", type="Preference", properties={"topic": "Астрономия"})
],
relationships=[
Relationship(source="user123", target="pref1", type="INTERESTED_IN")
]
)
# Добавляем в граф
graph.add_graph_documents([doc]) |
|
Для запросов к графу я создал специальный инструмент:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| @tool
def query_memory_graph(query: str) -> str:
"""Ищет информацию в графе знаний агента."""
cypher_template = """
MATCH (u:User {{id: '{user_id}'}})-[r]-(n)
WHERE n.topic CONTAINS '{query}' OR n.content CONTAINS '{query}'
RETURN n, r
LIMIT 5
"""
results = graph.query(cypher_template.format(user_id=current_user_id, query=query))
return format_graph_results(results) |
|
Графовый подход особенно хорош для агентов, которые должны строить сложные взаимосвязи между понятиями. Недавно я создал агента-исследователя, который собирал информацию по научным статьям. Когда число статей перевалило за сотню, линейная память захлебнулась, а граф продолжал отлично работать, показывая связи между авторами, темами и публикациями. Конечно, есть и подводные камни — графовые БД требуют больше ресурсов и сложнее в настройке. Но для настоящих долгоиграющих агентов это, пожалуй, лучшее решение из всех, что я пробовал.
Масштабирование агента для продакшена: Docker, Kubernetes и мониторинг
Однажды я думал, что создать работающего агента — это самая сложная часть. Ха! Наивный. Настоящий кошмар начался, когда мой прототип заработал настолько хорошо, что клиент захотел запустить его "в боевых условиях" с сотнями одновременных пользователей. Вот тут-то и пришлось срочно осваивать искуство масштабирования.
Первым делом я упаковал агента в Docker-контейнер. Это решило классическую проблему "у меня работает, а у вас нет":
| Windows Batch file | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Переменные окружения через ARG для сборки
ARG LOG_LEVEL=INFO
ENV LOG_LEVEL=${LOG_LEVEL}
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] |
|
В requirements.txt я указал конкретные версии библиотек, включая LangChain и зависимости для watsonx. Кстати, лайфхак: используйте pip freeze > requirements.txt в своем виртуальном окружении, но потом вычистите лишнее, иначе образ будет раздутым. Когда количество запросов выросло, одного контейнера стало недостаточно. Тут на сцену вышел Kubernetes — система оркестрации контейнеров, которая дает возможность горизонтального масштабирования:
| YAML | 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
| apiVersion: apps/v1
kind: Deployment
metadata:
name: langchain-agent
spec:
replicas: 3 # Начинаем с трех реплик
selector:
matchLabels:
app: langchain-agent
template:
metadata:
labels:
app: langchain-agent
spec:
containers:
- name: langchain-agent
image: myregistry.com/langchain-agent:v1.2.3
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
env:
- name: WATSONX_API_KEY
valueFrom:
secretKeyRef:
name: api-keys
key: watsonx-key |
|
Важный момент: я не сразу понял, что LLM-агенты требуют значительно больше ресурсов, чем обычные приложения. Сначала выставил стандартные лимиты и получил печальные OOMKilled ошибки. Пришлось экспериментальным путем подбирать оптимальные значения.
Для автоматического масштабирования я добавил HPA (Horizontal Pod Autoscaler):
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: langchain-agent-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: langchain-agent
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 |
|
Но масштабирование — это только половина дела. Без мониторинга вы летите вслепую. Я интегрировал Prometheus и Grafana для отслеживания ключевых метрик:
| Python | 1
2
3
4
5
6
7
8
9
| from prometheus_client import Counter, Histogram, start_http_server
# Метрики для LangChain агента
llm_requests_total = Counter('llm_requests_total', 'Total number of LLM requests', ['model', 'status'])
llm_request_duration = Histogram('llm_request_duration_seconds', 'Duration of LLM requests', ['model'])
tool_calls_total = Counter('tool_calls_total', 'Total number of tool calls', ['tool_name', 'status'])
# Запускаем сервер метрик
start_http_server(8001) |
|
И настроил алерты для критических ситуаций — например, когда процент ошибок запросов к LLM превышает 5% или задержка растет выше 10 секунд. Поверьте, когда ваш агент ночью начинает тормозить из-за проблем с API, вы будете благодарны за эти оповещения.
Оптимизация производительности
Если вы думали, что после запуска агента в продакшене можно расслабиться — у меня для вас плохие новости. Оптимизация производительности — это бесконечная игра, где каждая миллисекунда на счету. Особенно когда речь идет об LLM-агентах, где стоимость запросов прямо пропорциональна времени обработки. Первое, что я сделал для ускорения своего агента — внедрил кэширование запросов к LLM. Это просто, но эффективно:
| Python | 1
2
3
4
5
6
7
8
| from langchain.globals import set_llm_cache
from langchain.cache import InMemoryCache, RedisCache
# Для локальной разработки
set_llm_cache(InMemoryCache())
# Для продакшена с Redis
set_llm_cache(RedisCache(redis_url="redis://localhost:6379")) |
|
Этот простой трюк сократил время отклика на повторяющиеся запросы буквально в десятки раз! Но я пошел дальше и создал многоуровневый кэш:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class TieredCache:
def __init__(self):
self.memory_cache = {} # Самый быстрый уровень
self.redis_client = redis.Redis() # Второй уровень
def get(self, key):
# Сначала проверяем in-memory кэш
if key in self.memory_cache:
return self.memory_cache[key]
# Затем Redis
value = self.redis_client.get(key)
if value:
# Обновляем in-memory кэш
self.memory_cache[key] = value
return value
return None |
|
Следующим шагом стала оптимизация промптов. Я заметил, что большие системные промпты съедают кучу токенов, не добавляя реальной ценности. Решение — вынести часть инструкций в отдельные компоненты:
| Python | 1
2
3
4
5
6
7
8
9
| def create_optimized_prompt(query, tools_only_needed):
# Выбираем только нужные инструменты для конкретного запроса
relevant_tools = [t for t in all_tools if is_tool_relevant(t, query)]
# Генерируем компактное описание инструментов
tools_description = "\n".join([f"- {t.name}: {t.description.split('.')[0]}" for t in relevant_tools])
# Создаем оптимизированный промпт
return f"Ты помощник. Инструменты: {tools_description}. Запрос: {query}" |
|
Еще один хак, который дал ощутимый прирост — батчинг запросов к модели. Вместо того, чтобы отправлять каждый запрос отдельно, я группирую их:
| Python | 1
2
3
4
5
6
7
8
9
10
| async def batch_llm_requests(prompts, max_batch_size=5):
results = []
for i in range(0, len(prompts), max_batch_size):
batch = prompts[i:i+max_batch_size]
tasks = [model.ainvoke(prompt) for prompt in batch]
batch_results = await asyncio.gather(*tasks)
results.extend(batch_results)
return results |
|
Для инструментов, которые обращаются к внешним API, я добавил умный retry-механизм с экспоненциальной задержкой и тайм-аутами:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| def smart_retry(func, max_retries=3, base_delay=1, timeout=10):
async def wrapped(*args, **kwargs):
retries = 0
last_exception = None
while retries <= max_retries:
try:
return await asyncio.wait_for(func(*args, **kwargs), timeout=timeout)
except asyncio.TimeoutError:
last_exception = TimeoutError("Запрос превысил тайм-аут")
except Exception as e:
last_exception = e
retries += 1
delay = base_delay * (2 ** (retries - 1))
await asyncio.sleep(delay)
raise last_exception
return wrapped |
|
Подводные камни и решения
Первое, с чем сталкивается почти каждый — галлюцинации моделей. Я помню, как мой первый агент с уверенностью рассказывал пользователям о несуществующих функциях API, а на вопрос "какая погода в Москве?" начинал выдумывать градусы. Особенно обидно, когда агент галлюцинирует, имея в распоряжении все необходимые инструменты!
Другая распространенная проблема — зацикливание. Агент вызывает инструмент, получает ответ, не понимает его, снова вызывает тот же инструмент... И так до бесконечности. Или, что еще хуже, начинает играть в пинг-понг между двумя инструментами.
Не менее коварны ошибки токенизации. Иногда агент просто обрывает ответ на полуслове или генерирует синтаксически неверный JSON, который парсер не может обработать.
В следующих разделах я расскажу, как я научился бороться с каждой из этих проблем и какие паттерны помогли создать по-настоящему надежных агентов.
Что пошло не так в моих экспериментах
Знаете, когда я рассказываю о своих успехах с LangChain, может создаться впечатление, что всё шло как по маслу. Но это далеко от истины. Мой путь был усеян ошибками, которые иногда приводили меня на грань отчаяния.
Первый серьезный провал случился, когда я попытался создать агента для онлайн-магазина. Ему предстояло обрабатывать запросы клиентов, искать товары и оформлять заказы. На бумаге всё выглядело отлично, но первые тесты были катастрофическими. Агент постоянно путал категории товаров, рекомендовал несуществующие позиции и — что хуже всего — неправильно рассчитывал итоговые суммы.
Проблема оказалась в промпте. Я написал слишком размытые инструкции, надеясь на "интеллект" модели. Урок №1: никогда не полагайтесь на интуицию модели там, где нужна четкая логика.
Другой эпичный фейл произошел с агентом-аналитиком. Я дал ему доступ к API базы данных, и он должен был генерировать SQL-запросы на лету. В тестовой среде всё работало идеально, но в продакшене агент начал генерировать настолько сложные запросы, что база данных просто падала. Один особенно "изобретательный" запрос с семью вложенными JOIN'ами и оконными функциями чуть не положил весь сервер.
Урок №2: всегда ограничивайте сложность запросов и добавляйте проверки перед выполнением.
Еще одна болезненная история связана с конфиденциальностью. Я создал агента для поддержки разработчиков, который мог анализировать код и предлагать улучшения. Но я забыл добавить фильтрацию входных данных, и кто-то скормил ему файл с паролями. Агент услужливо включил их в свой ответ, который отправился в общий чат. К счастью, это был внутренний тест, но я до сих пор вздрагиваю, представляя, что могло произойти в реальном сценарии.
Галлюцинации модели: как я научился их выявлять программно
Галлюцинации модели — мой персональный кошмар в работе с LLM. Когда агент с серьезным видом выдаёт откровенную чушь, доверие пользователей тает на глазах. Первый раз я столкнулся с этим, когда мой финансовый ассистент начал "изобретать" несуществующие котировки акций с точностью до цента. Выглядело убедительно, но было полностю выдумано!
После нескольких емейлов от разгневаных клиентов я решил во что бы то ни стало победить эту проблему. И оказалось, что галлюцинации можно выявлять программно, хотя 100% гарантии тут, конечно, никто не даст.
Первый метод, который я внедрил — проверка фактов через независимые источники:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| def verify_llm_response(response, query):
"""Проверяет ответ LLM на наличие галлюцинаций."""
# Извлекаем ключевые утверждения из ответа
verification_prompt = f"""
Извлеки из ответа все фактические утверждения, которые можно проверить:
{response}
Формат: список утверждений, по одному на строку
"""
claims = model.invoke(verification_prompt).strip().split("\n")
results = []
for claim in claims:
# Проверяем каждое утверждение через поиск или другие инструменты
search_results = search_tool.invoke(claim)
confidence = calculate_confidence(claim, search_results)
results.append({"claim": claim, "confidence": confidence})
# Если есть утверждения с низкой уверенностью, возможно это галлюцинация
suspicious = [r for r in results if r["confidence"] < 0.3]
return suspicious |
|
Второй подход — "самопроверка" модели. Я заставляю модель оценить свою уверенность и обосновать ответ:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| def self_evaluation_check(response, query):
eval_prompt = f"""
Вопрос: {query}
Твой ответ: {response}
Оцени достоверность своего ответа по шкале от 0 до 10.
Укажи части ответа, в которых ты не уверен.
Приведи рассуждение, почему ты считаешь ответ достоверным или недостоверным.
"""
evaluation = model.invoke(eval_prompt)
# Извлекаем числовую оценку из ответа
confidence_match = re.search(r'(\d+)(?:/10)?', evaluation)
if confidence_match:
confidence = int(confidence_match.group(1)) / 10
if confidence < 0.6:
return {"suspicious": True, "confidence": confidence, "reasoning": evaluation}
return {"suspicious": False, "evaluation": evaluation} |
|
Третий метод, который оказался на удивление эффективным — детектирование неопределенности в тексте:
| Python | 1
2
3
4
5
6
7
8
| def uncertainty_detection(response):
uncertainty_markers = [
"возможно", "вероятно", "предположительно", "кажется",
"я думаю", "я полагаю", "насколько я знаю", "я не уверен"
]
score = sum([1 for marker in uncertainty_markers if marker in response.lower()])
return score > 2 # Если больше двух маркеров, это подозрительно |
|
Для критически важных систем я объединил все эти методы в многоуровневую защиту. Если агент выдаёт подозрительный ответ, я либо отправляю его на проверку человеку, либо явно помечаю информацию как непроверенную.
Был забавный случай, когда мой агент в ответ на вопрос про квантовую механику выдал такую наукообразную чушь, что все мои детекторы промолчали. А потом мне написал профессор физики с указанием на ошибки. После этого я добавил в систему "эксперта-наблюдателя" — второго агента, чья задача критиковать ответы основного.
Тестирование и валидация ответов агента
Тестирование ИИ-агентов — это совсем не то, чему нас учили на курсах по QA. Когда я впервые столкнулся с необходимостью валидировать ответы своего агента, то понял, что стандартные подходы тут не работают. Как написать тест для системы, которая может выдать десятки вариантов "правильных" ответов? После нескольких провальных попыток я разработал многоуровневую стратегию тестирования, которая реально работает:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| def test_agent_response(query, agent, validators=None):
"""Тестирует ответ агента с применением различных валидаторов."""
if validators is None:
validators = [basic_format_validator, factual_consistency_validator]
response = agent.invoke({"input": query})
results = {}
for validator in validators:
validator_name = validator.__name__
try:
results[validator_name] = validator(query, response)
except Exception as e:
results[validator_name] = {"status": "error", "message": str(e)}
return {
"query": query,
"response": response,
"validation_results": results,
"passed": all(r.get("status") == "pass" for r in results.values() if isinstance(r, dict))
} |
|
Для базовой проверки форматирования я использую регулярные выражения и проверку структуры JSON:
| 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
| def basic_format_validator(query, response):
"""Проверяет базовый формат ответа."""
if not response or not response.get("output"):
return {"status": "fail", "message": "Пустой ответ"}
output = response["output"]
# Проверка на минимальную длину
if len(output) < 10:
return {"status": "fail", "message": "Ответ слишком короткий"}
# Проверка на наличие типичных ошибок
error_patterns = [
r"я не могу|не имею доступа|не знаю|не уверен",
r"ошибка|error|exception|traceback",
r"action_input.*?None"
]
for pattern in error_patterns:
if re.search(pattern, output, re.IGNORECASE):
return {
"status": "warn",
"message": f"Обнаружен паттерн ошибки: {pattern}"
}
return {"status": "pass"} |
|
Для более серьезной валидации я проверяю фактическую точность ответов:
| 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
| def factual_consistency_validator(query, response):
"""Проверяет фактическую точность ответа."""
output = response["output"]
# Извлекаем утверждения из ответа
verification_prompt = f"""
Извлеки из ответа все фактические утверждения:
{output}
Выведи только список утверждений, по одному на строку.
"""
try:
statements = model.invoke(verification_prompt).strip().split('\n')
if not statements or statements[0] == '':
return {"status": "pass", "message": "Нет проверяемых утверждений"}
# Проверяем каждое утверждение
suspicious = []
for statement in statements:
if statement and len(statement) > 10:
verification = verify_statement(statement)
if verification["score"] < 0.3:
suspicious.append({
"statement": statement,
"score": verification["score"]
})
if suspicious:
return {
"status": "warn",
"message": "Обнаружены подозрительные утверждения",
"details": suspicious
}
return {"status": "pass"}
except Exception as e:
return {"status": "error", "message": str(e)} |
|
Отладка и мониторинг работы агента
Отладка ИИ-агентов — это особое искуство, с которым я познакомился после первого же неудачного запуска в прод. Я-то наивно полагал, что если агент хорошо отвечает на мои тестовые вопросы, значит всё в порядке. Как же я заблуждался! Уже на следующий день посыпались скриншоты с абсурдными ответами и зависшими процессами.
С тех пор я разработал несколько приемов, которые спасают мне жизнь. Первый и самый важный — подробное логирование каждого шага агента:
| 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
| import logging
# Настраиваем логгер
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("agent_debug.log"),
logging.StreamHandler()
]
)
# Создаем кастомный колбэк для LangChain
class DetailedLogger(BaseCallbackHandler):
def on_llm_start(self, serialized, prompts, **kwargs):
logging.debug(f"Запрос к LLM: {prompts[0][:100]}...")
def on_llm_end(self, response, **kwargs):
logging.debug(f"Ответ LLM: {response.generations[0][0].text[:100]}...")
def on_tool_start(self, serialized, input_str, **kwargs):
logging.info(f"Вызов инструмента {serialized['name']}: {input_str}")
def on_tool_end(self, output, **kwargs):
logging.info(f"Результат инструмента: {output}")
def on_chain_start(self, serialized, inputs, **kwargs):
logging.debug(f"Начало цепочки: {serialized['name']}")
def on_chain_end(self, outputs, **kwargs):
logging.debug(f"Конец цепочки: {outputs}") |
|
Этот логгер я добавляю при инициализации агента:
| Python | 1
2
3
4
5
6
7
| # Инициализируем агента с логгером
agent_executor = AgentExecutor(
agent=chain,
tools=tools,
verbose=True,
callbacks=[DetailedLogger()]
) |
|
Второй лайфхак — визуализация процесса рассуждения. Для этого я использую простой инструмент на основе Graphviz:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| def visualize_agent_reasoning(steps):
from graphviz import Digraph
dot = Digraph(comment='Ход рассуждений агента')
for i, step in enumerate(steps):
dot.node(f'thought_{i}', f'Мысль: {step["thought"]}')
dot.node(f'action_{i}', f'Действие: {step["action"]}')
dot.node(f'result_{i}', f'Результат: {step["observation"]}')
if i > 0:
dot.edge(f'result_{i-1}', f'thought_{i}')
dot.edge(f'thought_{i}', f'action_{i}')
dot.edge(f'action_{i}', f'result_{i}')
return dot |
|
Для мониторинга в продакшене я создал дашборд, который показывает ключевые метрики: количество запросов, время ответа, частоту использования инструментов и, что важно, процент неудачных взаимодействий. Это помогает быстро замечать аномалии в работе агента.
Проблемы с токенизацией и их влияние на результат
Токенизация! Казалось бы, что может быть проще — нарезать текст на кусочки, отправить в модель, собрать ответ. Но в реальности это один из самых коварных источников багов в LangChain-агентах. Я столько часов потратил на отладку проблем, которые в итоге сводились к неправильной токенизации! Суть проблемы в том, что модели вроде Granite работают не с текстом, а с токенами — фрагментами слов или отдельными словами, которые она "понимает". И каждая модель токенизирует по-своему. То, что для одной модели — пять токенов, для другой может быть десятью.
Первая грабля, на которую я наступил — обрезание ответов на середине. Агент генерирует длинный ответ, но из-за ограничения max_new_tokens он просто обрывается на полуслове. Решение оказалось в правильной настройке:
| Python | 1
2
3
4
5
6
7
8
9
10
| # Слишком маленькое значение
bad_params = {
"max_new_tokens": 50 # Недостаточно для сложных ответов
}
# Более адекватная настройка
better_params = {
"max_new_tokens": 250, # Больше пространства
"stop_sequences": ["Observation:", "Human:"] # Явные маркеры окончания
} |
|
Еще одна проблема — некорректная работа с юникодом и многоязычными текстами. Когда мой агент начал работать с русскими текстами, он периодически выдавал абракадабру из-за того, что токенизатор неправильно обрабатывал кириллицу. А еще эмодзи могут полностью сломать процесс токенизации в некоторых моделях!
Но самая коварная проблема возникает при передаче контекста между инструментами. Если инструмент возвращает текст, превышающий контекстное окно модели, агент начинает "галлюцинировать", потому что просто не видит всю информацию. Вот мое решение:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def truncate_safely(text, max_tokens=1000):
"""Безопасно обрезает текст до указанного количества токенов."""
encoding = tiktoken.get_encoding("cl100k_base") # Подходящий энкодер
tokens = encoding.encode(text)
if len(tokens) <= max_tokens:
return text
# Оставляем место для [обрезано] и важного начала/конца
keep_start = int(max_tokens * 0.8)
keep_end = max_tokens - keep_start - 10
truncated = encoding.decode(tokens[:keep_start]) + "\n[...сокращено...]\n" + encoding.decode(tokens[-keep_end:])
return truncated |
|
Еще я научился предварительно оценивать количество токенов, чтобы не упираться в лимиты:
Полный пример
Теперь, когда мы изучили все компоненты и нюансы, давайте соберем полноценное решение. Ниже представлен готовый к использованию код агента, который умеет узнавать текущую дату и получать астрономическое изображение дня от NASA:
| 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
| import os
import nasapy
from datetime import datetime
from dotenv import load_dotenv
from langchain_core.tools import tool
from langchain.agents import AgentExecutor
from langchain.agents.format_scratchpad import format_log_to_str
from langchain.agents.output_parsers import JSONAgentOutputParser
from langchain.memory import ConversationBufferMemory
from langchain.tools.render import render_text_description_and_args
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough
from langchain_ibm import WatsonxLLM
# Загружаем переменные окружения
load_dotenv()
# Инициализируем NASA API
nasa = nasapy.Nasa(key=os.getenv("NASA_API_KEY", "DEMO_KEY"))
# Определяем инструменты
@tool
def get_todays_date() -> str:
"""Получает текущую дату в формате ГГГГ-ММ-ДД."""
date = datetime.now().strftime("%Y-%m-%d")
return date
@tool(return_direct=True)
def get_astronomy_image(date: str):
"""Получает астрономическое изображение дня NASA на указанную дату. Дата в формате ГГГГ-ММ-ДД."""
apod = nasa.picture_of_the_day(date, hd=True)
return apod['url']
tools = [get_todays_date, get_astronomy_image] |
|
Что лучше учить 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... Перевод из Python в C/C++ - Python перевести все программы с файла в C\C++ Перевод кода с C++ на Python - Python #include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
using namespace...
|