Почему каждый второй агент в Slack сходит с ума (и как этого избежать)
Вы видели это: коллега добавляет в общий чат "умного помощника". Первые два вопроса он отрабатывает блестяще. На третий — начинает цитировать внутренний wiki, на пятый — путает контекст и предлагает уволить тимлида. К седьмому сообщению чат превращается в цифровую помойку.
Проблема не в модели. Проблема в мосте между ней и Slack. Этот мост часто строят на скорую руку: вебхук, пара строк кода, надежда на лучшее. Контекст теряется, безопасность — на уровне "ну вроде работает", управление состояниями — в лучшем случае через глобальную переменную. Результат — агент-шизофреник, которого стыдно показывать клиентам.
Решение — не просто "отправить запрос в API". Нужна архитектура. Та, что держит контекст диалога, проверяет права доступа, обрабатывает таймауты и умеет сказать "я не знаю". Сегодня мы построим её на Amazon Bedrock AgentCore и AWS CDK. Это не quickstart, который сломается через неделю. Это production-решение, которое я выстрадал на трёх проектах.
1 Готовим площадку: Slack App и AWS аккаунт
Сначала забудьте про код. Настройте стороны диалога. В Slack это означает создание приложения с правильными scope и вебхуками. В AWS — создание IAM-роли, которая не будет иметь прав на весь интернет.
В Slack: создаем App с Event Subscription
- Идите на api.slack.com/apps и жмите "Create New App". Выбирайте "From scratch".
- В "Basic Information" добавьте
app_mentions:read,chat:write, иim:historyв OAuth & Permissions -> Scopes. - Перейдите в "Event Subscriptions". Включите их. В "Request URL" пока введите любой URL (например,
https://example.com). Мы обновим его позже, после развертывания API Gateway. - В "Subscribe to bot events" добавьте
app_mention. Это даст боту слушать сообщения, где его упомянули. - Установите приложение в workspace (OAuth & Permissions -> Install App). Сохраните Bot User OAuth Token и Signing Secret. Они понадобятся Lambda.
Первая ловушка: Не берите все scope подряд "на всякий случай". Каждый лишний scope — дыра в безопасности и лишние вопросы при утверждении приложения. Берите минимум. Если позже нужно больше — добавите.
В AWS: готовим IAM политику для агента
Агент в Bedrock должен уметь вызывать свои инструменты (Actions) и, возможно, писать в CloudWatch для логирования. Создайте политику вручную через IAM Console или сразу опишите её в CDK-коде. Пример минимальной политики:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeAgent",
"bedrock:RetrieveAndGenerate"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
Запоминайте: политика для агента и политика для Lambda, которая будет вызывать агента, — это две разные сущности. Не смешивайте.
2 Лепим агента в Bedrock: не просто кнопки нажать
Теперь создадим самого агента. Если вы раньше не работали с AgentCore, посмотрите мой гайд про полный стек для агентов на Amazon Bedrock. Там основы. Здесь — кратко и по делу.
- В консоли AWS найдите "Bedrock" -> "AgentCore".
- "Create agent". Выберите последнюю доступную модель (на март 2026 это, скорее всего, Claude 3.7 Sonnet или что-то мощнее).
- В "Instructions" пропишите роль агента. Например: "Ты — помощник в Slack-чате компании. Отвечай кратко, по делу. Если не уверен в ответе, уточни. Не придумывай информацию о внутренних процессах, которых нет в предоставленных данных."
- Здесь же, в разделе "Knowledge Bases", привяжите базу знаний, если она есть. Без неё агент будет работать только на общей эрудиции модели.
- В "Action Groups" определите инструменты, которые может использовать агент. Например, для интеграции с Salesforce или Redshift.
- Сохраните агента. Запишите его Agent ID и Agent Alias ID. Они понадобятся для вызова из Lambda.
3 Lambda-функция: мозг, который всё соединит
Эта функция — сердце интеграции. Она получает события от Slack, проверяет подпись, дергает агента в Bedrock, форматирует ответ и отправляет его обратно в чат. Пишем на Python 3.12 (самая свежая LTS на 2026 год).
Вот скелет функции. Полный код я выложу в конце, а пока разберем ключевые части.
import os
import json
import hmac
import hashlib
import time
import boto3
from botocore.exceptions import ClientError
from typing import Dict, Any
# Инициализация клиентов
def get_bedrock_agent_client():
return boto3.client('bedrock-agent-runtime', region_name='us-east-1')
def lambda_handler(event: Dict[str, Any], context) -> Dict[str, Any]:
# 1. Верификация подписи от Slack
slack_signing_secret = os.environ['SLACK_SIGNING_SECRET'].encode('utf-8')
request_body = event.get('body', '{}')
timestamp = event['headers'].get('x-slack-request-timestamp', '')
if abs(time.time() - int(timestamp)) > 60 * 5:
return {'statusCode': 403, 'body': 'Request timeout'}
sig_basestring = f'v0:{timestamp}:{request_body}'.encode('utf-8')
my_signature = 'v0=' + hmac.new(slack_signing_secret, sig_basestring, hashlib.sha256).hexdigest()
slack_signature = event['headers'].get('x-slack-signature', '')
if not hmac.compare_digest(my_signature, slack_signature):
return {'statusCode': 403, 'body': 'Invalid signature'}
# 2. Парсим событие от Slack
body = json.loads(request_body)
if body.get('type') == 'url_verification':
return {'statusCode': 200, 'body': body.get('challenge')}
# 3. Извлекаем текст сообщения, где упомянули бота
event_data = body.get('event', {})
if event_data.get('type') != 'app_mention':
return {'statusCode': 200, 'body': 'Not an app_mention event'}
user_text = event_data.get('text', '').replace('<@UXXXBOTID>', '').strip()
channel_id = event_data.get('channel')
# 4. Вызываем агента Bedrock
try:
agent_client = get_bedrock_agent_client()
response = agent_client.invoke_agent(
agentId=os.environ['AGENT_ID'],
agentAliasId=os.environ['AGENT_ALIAS_ID'],
sessionId=channel_id, # Используем ID канала как сессию
inputText=user_text,
)
# 5. Собираем потоковый ответ от агента
completion = ""
for event in response.get('completion', []):
chunk = event.get('chunk', {})
completion += chunk.get('bytes', b'').decode('utf-8')
except ClientError as e:
completion = f"Ошибка при обращении к агенту: {str(e)}"
# 6. Отправляем ответ обратно в Slack
slack_token = os.environ['SLACK_BOT_TOKEN']
# ... (код для вызова chat.postMessage)
return {'statusCode': 200, 'body': 'OK'}
Обратите внимание на sessionId=channel_id. Это хак, который позволяет агенту хранить контекст в пределах одного канала. Bedrock AgentCore автоматически управляет состоянием сессии. Если хотите персональный контекст для каждого пользователя, используйте user_id вместо channel_id.
Вторая ловушка: Не храните токены и секреты в коде. Используйте Environment Variables Lambda или, лучше, AWS Secrets Manager. В CDK-шаблоне ниже я покажу, как их безопасно внедрить.
4 API Gateway: дверь, в которую стучится Slack
Slack будет отправлять события на публичный URL. Мы создадим HTTP API Gateway, который проксирует запросы в нашу Lambda. Это просто, но есть деталь: Slack требует ответ на challenge при верификации URL.
Мы уже обработали это в Lambda (блок url_verification). В CDK мы создадим API с одним POST-методом, привязанным к нашей функции.
# Фрагмент CDK-кода для API Gateway
from aws_cdk import (
aws_apigatewayv2 as apigw,
aws_apigatewayv2_integrations as integrations,
)
# ...
slack_integration = integrations.HttpLambdaIntegration(
"SlackIntegration",
handler=slack_lambda
)
api = apigw.HttpApi(self, "SlackAgentApi",
default_integration=slack_integration
)
# Выведем URL, чтобы скопировать его в настройки Slack
CfnOutput(self, "ApiEndpoint", value=api.url)
После развертывания CDK вы получите URL типа https://xxx.execute-api.region.amazonaws.com. Вот его нужно вставить в настройки Slack App в "Event Subscriptions -> Request URL". Slack сразу проверит его и скажет "Verified".
5 CDK: развернуть всё одной командой
Теперь соберем все компоненты в один CDK-стек. Я использую TypeScript, но вы можете адаптировать под Python. Основные моменты:
// lib/slack-agent-stack.ts - основные части
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigatewayv2';
import * as secrets from 'aws-cdk-lib/aws-secretsmanager';
export class SlackAgentStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Секреты из Secrets Manager (создайте заранее)
const slackSecrets = secrets.Secret.fromSecretNameV2(this, 'SlackSecrets', 'slack/credentials');
const bedrockAgentSecrets = secrets.Secret.fromSecretNameV2(this, 'BedrockAgentSecrets', 'bedrock/agent');
// Lambda функция
const slackHandler = new lambda.Function(this, 'SlackAgentHandler', {
runtime: lambda.Runtime.PYTHON_3_12,
code: lambda.Code.fromAsset('lambda'),
handler: 'slack_handler.handler',
timeout: cdk.Duration.seconds(30),
memorySize: 256,
environment: {
SLACK_SIGNING_SECRET: slackSecrets.secretValueFromJson('signing_secret').unsafeUnwrap(),
SLACK_BOT_TOKEN: slackSecrets.secretValueFromJson('bot_token').unsafeUnwrap(),
AGENT_ID: bedrockAgentSecrets.secretValueFromJson('agent_id').unsafeUnwrap(),
AGENT_ALIAS_ID: bedrockAgentSecrets.secretValueFromJson('agent_alias_id').unsafeUnwrap(),
},
});
// Даем Lambda право читать секреты и вызывать Bedrock
slackSecrets.grantRead(slackHandler);
bedrockAgentSecrets.grantRead(slackHandler);
slackHandler.addToRolePolicy(new iam.PolicyStatement({
actions: ['bedrock:InvokeAgent', 'bedrock:InvokeModel'],
resources: ['*'],
}));
// HTTP API Gateway
const api = new apigw.HttpApi(this, 'SlackAgentApi', {
defaultIntegration: new integrations.HttpLambdaIntegration('SlackIntegration', slackHandler),
});
new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url! });
}
}
Развертывание: cdk deploy. После успеха скопируйте вывод ApiEndpoint и обновите URL в настройках Slack App. Всё, интеграция жива.
Где собака зарыта: ошибки, которые съедят ваше время
| Ошибка | Почему происходит | Как исправить |
|---|---|---|
| "Invalid signature" от Slack | Lambda получает тело запроса в формате строки, а не JSON. Проверка подписи использует сырую строку, но API Gateway может её модифицировать. | В настройках API Gateway отключите "Request validation" и убедитесь, что Lambda получает тело как строку (в событии body). |
| Агент не отвечает, таймаут Lambda | Bedrock AgentCore может думать больше 30 секунд, особенно с большими контекстами. Стандартный таймаут Lambda — 3 секунды. | Увеличьте таймаут Lambda до 30-60 секунд. И настройте в Slack задержку ответа (используйте response_url для асинхронного ответа). |
| Контекст сбрасывается между сообщениями | Вы используете случайный sessionId или не сохраняете его между вызовами. |
Используйте стабильный идентификатор (например, ID канала Slack) как sessionId. Bedrock сам хранит историю сессии до 24 часов. |
| Агент не использует базу знаний | База знаний не привязана к агенту или у неё нет прав на чтение. | В консоли Bedrock проверьте, что агенту назначена Knowledge Base и у его IAM-роли есть bedrock:Retrieve права на её индекс. |
Частые вопросы от тех, кто уже обжёгся
Можно ли использовать не app_mention, а все сообщения в канале?
Можно, но не нужно. Подписка на все сообщения (message.channels) создаст лавину событий и счёт от AWS. Бот будет пытаться отвечать на каждый пост. Используйте app_mention или, в крайнем случае, реагируйте только на сообщения с определёнными ключевыми словами.
Как сделать, чтобы бот отвечал в треде, а не в общем чате?
В API Slack есть параметр thread_ts. При отправке ответа через chat.postMessage передавайте thread_ts: event_data.get('thread_ts') || event_data.get('ts'). Это привяжет ответ к треду.
Bedrock AgentCore дорогой? Как контролировать стоимость?
Цена складывается из вызовов модели (per token) и хранения сессий. Контролируйте: 1) Установите максимальную длину ответа в агенте. 2) Очищайте сессии через 1 час, если не нужна долгая память. 3) Используйте более лёгкие модели (например, Claude 3 Haiku) для простых вопросов. Настройте маршрутизацию между моделями в зависимости от сложности.
А если я хочу, чтобы агент запускал workflows в Slack (например, создавал тикет)?
Опишите это как Action в агенте. Action будет вызывать другую Lambda, которая через Slack API создаст тикет в Jira или запустит workflow через интеграцию с Notion. Только дайте агенту чёткие инструкции, когда этот Action использовать.
Финальный совет: Первый запуск — всегда в отдельном тестовом канале с парой доверенных коллег. Дайте им инструкцию: "Если бот начинает нести чушь, пишите @bot reset". Реализуйте эту команду в Lambda — она будет очищать сессию. Это спасёт репутацию вашего проекта, пока вы отлаживаете prompt'ы.
Интеграция готова. Ваш агент теперь не просто игрушка в консоли AWS, а полноправный участник рабочих чатов. Он помнит контекст, не болтает лишнего и умеет вызывать реальные инструменты. Осталось самое сложное — убедить коллег, что это не шпион от руководства.