Консультант SAP тонет в документах? Пора строить спасательный плот
Представьте: вы в десятый раз ищете тот специфичный пример из SAP Note 123456, который видели полгода назад. Параллельно в пяти Telegram-чатах коллеги спорят о методе BAPI_TRANSACTION_COMMIT. А завтра - демо для клиента, где нужно быстро объяснить разницу между RFC и IDoc. Знакомо? Значит, вы готовы к LLM-архивариусу.
Архивариус - не просто поисковик. Это система, которая понимает контекст SAP, помнит ваши обсуждения, знает специфику проекта и отвечает на вопросы типа "Как мы решали проблему с Z-отчетом в прошлом месяце?"
Что нам нужно собрать и зачем это работает
Типичная ошибка - пытаться запихнуть все в один промпт к ChatGPT. Не работает. Информация теряется, контекст сбивается, а конфиденциальные данные утекают в облако. Правильный путь - локальная система с четким разделением обязанностей.
- Документация SAP: Help Portal, SCN, внутренние Wiki - структурированные, но огромные
- ABAP-код: Ваши разработки, стандартные программы, сниппеты - семантически сложные
- Чаты и обсуждения: Telegram, Slack, email - неструктурированные, но ценные
- Личные заметки: OneNote, текстовые файлы - субъективные, но релевантные
Ключевая идея: разные типы данных требуют разных подходов к обработке. Код нужно анализировать с пониманием синтаксиса ABAP. Чаты - суммировать, выделяя суть. Документацию - индексировать с учетом иерархии.
Шаг 1: Выбираем движок - локальный или облачный?
Здесь все зависит от двух факторов: бюджета и требований к конфиденциальности. Если клиент из банковской сферы - только локальное решение. Если бюджет ограничен - облачные API.
| Вариант | Плюсы | Минусы | Когда выбирать |
|---|---|---|---|
| Ollama + Llama 3.1 8B | Полная приватность, бесплатно, работает офлайн | Требует 8-16 ГБ RAM, медленнее облачных | Конфиденциальные проекты, частые запросы |
| LM Studio | Удобный GUI, поддержка GGUF | Только Windows/Mac, ресурсоемкий | Для тестирования разных моделей |
| OpenAI API + GPT-4 | Лучшее качество, быстро | Дорого, данные уходят в облако | Не критичные данные, разовые задачи |
| Claude Code | Отлично работает с кодом | Дорогой, ограниченный контекст | Анализ сложного ABAP |
Мой выбор для старта - Ollama с моделью Llama 3.1 8B. Почему? Она бесплатная, понимает код достаточно хорошо, а главное - работает на моем ноутбуке во время перелетов между клиентами. Если нужна максимальная производительность - смотрите гайд по сборке LLM-станции.
# Установка Ollama (Linux/Mac)
curl -fsSL https://ollama.ai/install.sh | sh
# Запуск модели Llama 3.1 8B
ollama run llama3.1:8b
# Или для работы с кодом лучше codellama
ollama pull codellama:7b
ollama run codellama:7b
Не берите самые большие модели "на всякий случай". 8B параметров достаточно для анализа ABAP и суммаризации чатов. 70B-модели будут тормозить без специального железа.
Шаг 2: Готовим данные - три разных подхода для трех типов информации
Вот где большинство ошибается. Бросают все в одну кучу и удивляются, почему модель путает документацию SAP с шутками из чата.
1 ABAP-код: режем на осмысленные куски
ABAP - язык с жесткой структурой. Просто разбить по строкам - преступление. Нужно сохранять логические блоки: классы, методы, функции, FORM-ы.
# Пример парсера ABAP для Python
import re
from typing import List, Dict
def parse_abap_file(content: str) -> List[Dict]:
"""Разбирает ABAP файл на логические блоки"""
chunks = []
# Ищем классы
class_pattern = r'CLASS\s+(\w+)\s+DEFINITION.*?ENDCLASS'
classes = re.findall(class_pattern, content, re.DOTALL | re.IGNORECASE)
for class_block in classes:
# Ищем методы внутри класса
method_pattern = r'METHODS?\s+(\w+).*?ENDMETHOD'
methods = re.findall(method_pattern, class_block, re.DOTALL | re.IGNORECASE)
for method in methods:
chunks.append({
'type': 'METHOD',
'class': extract_class_name(class_block),
'name': extract_method_name(method),
'content': method,
'metadata': {
'has_select': 'SELECT' in method.upper(),
'has_update': 'UPDATE' in method.upper() or 'MODIFY' in method.upper(),
'lines': method.count('\n') + 1
}
})
# Ищем FUNCTION modules
function_pattern = r'FUNCTION\s+(\w+).*?ENDFUNCTION'
functions = re.findall(function_pattern, content, re.DOTALL | re.IGNORECASE)
for func in functions:
chunks.append({
'type': 'FUNCTION',
'name': extract_function_name(func),
'content': func,
'metadata': {'is_rfc': 'RFC' in func.upper()}
})
return chunks
# Использование:
abap_code = open('z_report_program.abap').read()
chunks = parse_abap_file(abap_code)
print(f"Найдено {len(chunks)} логических блоков")
Ключевой момент: каждый чанк получает метаданные. Потом при поиске мы можем фильтровать: "покажи только методы с SELECT запросами" или "найди все RFC-функции".
2 Telegram-чаты: выжимаем суть, отбрасываем мусор
Чаты - это 90% воды. "Привет", "спасибо", мемы, обсуждения погоды. Нам нужно 10% полезной информации. Решение - иерархическая суммаризация.
import json
from datetime import datetime, timedelta
def summarize_telegram_chat(messages: List[Dict], timeframe_hours: int = 24) -> List[Dict]:
"""Группируем сообщения по временным окнам и темам"""
# Фильтруем: удаляем короткие сообщения, стикеры, голосовые
filtered = []
for msg in messages:
text = msg.get('text', '')
if isinstance(text, list): # Telegram иногда возвращает массив для форматирования
text = ' '.join([item for item in text if isinstance(item, str)])
# Пропускаем мусор
if len(text) < 15 or text.startswith(('https://', 'http://')):
continue
if any(word in text.lower() for word in ['привет', 'пока', 'спасибо', 'ок', '👍']):
continue
filtered.append({
'id': msg['id'],
'date': datetime.fromisoformat(msg['date']),
'text': text,
'from': msg.get('from', 'Unknown')
})
# Группируем по временным окнам
windows = []
current_window = []
window_start = None
for msg in sorted(filtered, key=lambda x: x['date']):
if window_start is None:
window_start = msg['date']
current_window.append(msg)
elif (msg['date'] - window_start) < timedelta(hours=timeframe_hours):
current_window.append(msg)
else:
if len(current_window) >= 3: # Окно имеет смысл только если есть несколько сообщений
windows.append({
'start': window_start,
'messages': current_window,
'participants': len(set(m['from'] for m in current_window))
})
window_start = msg['date']
current_window = [msg]
# Для каждого окна создаем суммаризационный промпт
summaries = []
for window in windows:
context = '\n'.join([f"{m['from']}: {m['text']}" for m in window['messages'][-10:]]) # Берем последние 10 сообщений
prompt = f"""Анализируй обсуждение из чата SAP-консультантов. Выдели:
1. Основную проблему/вопрос
2. Предложенные решения
3. Итоговый вывод или решение
4. Ключевые технические термины (ABAP, RFC, BAPI, IDoc и т.д.)
Обсуждение:
{context}
Ответ в формате JSON:"""
summaries.append({
'window': window['start'].strftime('%Y-%m-%d %H:%M'),
'prompt': prompt,
'message_count': len(window['messages'])
})
return summaries
Этот подход решает две проблемы: сокращает объем данных в 10-20 раз и структурирует информацию. Вместо 500 сообщений за день вы получаете 5-10 суммаризаций с ключевыми insights.
3 Документация SAP: строим semantic пайплайн
С документацией проще всего - она уже структурирована. Но есть нюанс: Help Portal содержит миллионы страниц. Брать все - бессмысленно.
Для обработки документации используйте готовые решения вроде семантического пайплайна. Главное - сохраняйте иерархию: модуль → транзакция → справка → пример кода.
Шаг 3: Векторизуем и индексируем - выбираем базу
Здесь вариантов много, но для персонального использования достаточно простого решения.
# Минимальная система с ChromaDB
import chromadb
from sentence_transformers import SentenceTransformer
import numpy as np
class SAPVectorStore:
def __init__(self, persist_dir="./sap_db"):
self.client = chromadb.PersistentClient(path=persist_dir)
self.collection = self.client.get_or_create_collection("sap_docs")
self.embedder = SentenceTransformer('intfloat/multilingual-e5-large')
def add_document(self, doc_id: str, content: str, metadata: dict, chunk_size: int = 500):
"""Разбиваем документ на чанки и добавляем в векторную БД"""
# Простое разбиение по предложениям (для документации)
sentences = content.split('. ')
chunks = []
current_chunk = []
for sentence in sentences:
current_chunk.append(sentence)
if len(' '.join(current_chunk)) > chunk_size:
chunks.append('. '.join(current_chunk))
current_chunk = []
if current_chunk:
chunks.append('. '.join(current_chunk))
# Векторизуем каждый чанк
embeddings = self.embedder.encode(chunks, normalize_embeddings=True)
# Генерируем ID для чанков
chunk_ids = [f"{doc_id}_chunk_{i}" for i in range(len(chunks))]
# Добавляем в коллекцию
self.collection.add(
ids=chunk_ids,
embeddings=embeddings.tolist(),
documents=chunks,
metadatas=[{**metadata, 'chunk_index': i} for i in range(len(chunks))]
)
return len(chunks)
def search(self, query: str, n_results: int = 5, filters: dict = None) -> list:
"""Поиск по векторной БД с фильтрами"""
query_embedding = self.embedder.encode([query], normalize_embeddings=True)
results = self.collection.query(
query_embeddings=query_embedding.tolist(),
n_results=n_results,
where=filters if filters else {}
)
return zip(results['documents'][0], results['metadatas'][0], results['distances'][0])
Почему ChromaDB, а не Pinecone или Weaviate? Потому что она:
- Работает локально
- Не требует облачной подписки
- Достаточно быстрая для персонального использования
- Поддерживает фильтрацию по метаданным (тип документа, дата, автор)
Не экономьте на эмбеддинг-модели. multilingual-e5-large отлично работает с русским и английским, понимает технические термины. Более простые модели будут путать "BAPI" с "bachelor party".
Шаг 4: Собираем RAG-систему - где магия встречается с реальностью
RAG (Retrieval Augmented Generation) - это не просто "найди и вставь в промпт". Для SAP нужно учитывать контекст запроса.
class SAPRAGSystem:
def __init__(self, vector_store, llm_client):
self.vs = vector_store
self.llm = llm_client
def answer_question(self, question: str, context_type: str = "auto") -> dict:
"""Отвечаем на вопрос с учетом типа контекста"""
# Определяем тип запроса
query_type = self._classify_query(question)
# Ищем релевантные документы с фильтрацией
filters = self._build_filters(query_type, context_type)
results = self.vs.search(question, n_results=7, filters=filters)
# Формируем контекст
context_parts = []
for doc, metadata, score in results:
if score < 0.3: # Порог релевантности
source_type = metadata.get('type', 'unknown')
context_parts.append(f"[{source_type.upper()}] {doc}")
context = '\n\n'.join(context_parts[-5:]) # Берем 5 самых релевантных
# Строим промпт в зависимости от типа
prompt = self._build_prompt(question, context, query_type)
# Запрашиваем LLM
response = self.llm.generate(prompt, max_tokens=1000)
return {
'answer': response,
'sources': [m for _, m, _ in results],
'query_type': query_type
}
def _classify_query(self, question: str) -> str:
"""Классифицируем запрос"""
question_lower = question.lower()
if any(word in question_lower for word in ['как', 'how to', 'настроить', 'configure']):
return 'howto'
elif any(word in question_lower for word in ['ошибк', 'error', 'исключен', 'exception']):
return 'error'
elif any(word in question_lower for word in ['код', 'code', 'программ', 'abap', 'function']):
return 'code'
elif any(word in question_lower for word in ['что', 'what is', 'определен', 'meaning']):
return 'definition'
else:
return 'general'
def _build_filters(self, query_type: str, context_type: str) -> dict:
"""Строим фильтры для поиска"""
filters = {}
if context_type != "auto":
filters['type'] = context_type
elif query_type == 'code':
filters['type'] = {'$in': ['abap_method', 'abap_function', 'abap_class']}
elif query_type == 'howto':
filters['type'] = {'$in': ['sap_doc', 'wiki', 'chat_summary']}
return filters if filters else None
def _build_prompt(self, question: str, context: str, query_type: str) -> str:
"""Строим промпт в зависимости от типа запроса"""
base_prompt = """Ты - опытный SAP консультант. Ответь на вопрос на основе предоставленного контекста.
Если информации недостаточно - скажи об этом. Не выдумывай.
"""
if query_type == 'code':
base_prompt += """\nПри анализе кода:
1. Объясни логику
2. Укажи потенциальные проблемы
3. Предложи оптимизации если видишь
4. Форматируй код с отступами
"""
elif query_type == 'error':
base_prompt += """\nПри анализе ошибки:
1. Объясни причину
2. Предложи конкретные шаги решения
3. Укажи транзакции для диагностики
"""
return f"""{base_prompt}
Контекст:
{context}
Вопрос: {question}
Ответ:"""
Эта система делает несколько умных вещей:
- Классифицирует запрос (код, ошибка, инструкция)
- Подбирает соответствующий тип документов
- Строит специализированный промпт
- Фильтрует маловероятные результаты
Шаг 5: Интеграция с рабочим процессом - где это живет?
Самый важный шаг. Если система не встроена в ваш workflow, вы будете ей пользоваться неделю и забросите.
Вариант A: Telegram-бот
Самый удобный вариант. Бот живет в тех же чатах, где и обсуждения.
# Минимальный Telegram бот на Python-Telegram-Bot
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
import asyncio
class SAPBot:
def __init__(self, rag_system, token):
self.rag = rag_system
self.app = Application.builder().token(token).build()
# Регистрируем обработчики
self.app.add_handler(CommandHandler("start", self.start))
self.app.add_handler(CommandHandler("ask", self.ask_command))
self.app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message))
# Контекстные чаты (чтобы бот помнил историю)
self.chat_contexts = {}
async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Привет! Я твой SAP-архивариус. Задавай вопросы про ABAP, ошибки, настройки. "
"Используй /ask для сложных запросов."
)
async def ask_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработка запросов через команду /ask"""
query = ' '.join(context.args)
if not query:
await update.message.reply_text("Использование: /ask <ваш вопрос>")
return
# Показываем "печатает..."
await update.message.reply_chat_action("typing")
# Получаем ответ
result = self.rag.answer_question(query)
# Форматируем ответ
response = f"*{result['query_type'].upper()}*\n\n{result['answer']}\n\n"
if result['sources']:
response += "*Источники:*\n"
for source in result['sources'][:3]:
doc_type = source.get('type', 'doc')
doc_date = source.get('date', '')
response += f"- {doc_type} ({doc_date})\n"
await update.message.reply_text(response, parse_mode='Markdown')
async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработка обычных сообщений - автоопределение запросов"""
message = update.message.text
chat_id = update.effective_chat.id
# Сохраняем контекст (последние 10 сообщений)
if chat_id not in self.chat_contexts:
self.chat_contexts[chat_id] = []
self.chat_contexts[chat_id].append({
'role': 'user',
'text': message,
'time': update.message.date
})
# Держим только последние 10
self.chat_contexts[chat_id] = self.chat_contexts[chat_id][-10:]
# Автоматически отвечаем только на явные вопросы
if any(word in message.lower() for word in ['?', 'как', 'почему', 'что значит', 'какой']):
await update.message.reply_chat_action("typing")
result = self.rag.answer_question(message)
await update.message.reply_text(result['answer'][:4000]) # Ограничение Telegram
def run(self):
"""Запуск бота"""
print("Бот запущен...")
self.app.run_polling(allowed_updates=Update.ALL_TYPES)
# Запуск:
# bot = SAPBot(rag_system, "YOUR_TELEGRAM_TOKEN")
# bot.run()
Вариант B: VS Code расширение
Если вы много кодите на ABAP, интеграция с редактором - must have.
// Пример расширения VS Code для работы с ABAP
const vscode = require('vscode');
const { RAGClient } = require('./rag-client');
class SAPCodeAssistant {
constructor() {
this.rag = new RAGClient('http://localhost:8000');
this.context = vscode.extensions.getExtension('SAPSE.abap') ? 'abap' : 'general';
}
activate(context) {
// Команда для анализа выделенного кода
let analyzeCommand = vscode.commands.registerCommand('sap-assistant.analyzeCode', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const selection = editor.selection;
const code = editor.document.getText(selection);
if (!code.trim()) {
vscode.window.showWarningMessage('Выделите код для анализа');
return;
}
// Показываем прогресс
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'Анализирую ABAP код...',
cancellable: false
}, async (progress) => {
const response = await this.rag.ask(`Проанализируй этот ABAP код:\n\n${code}`);
// Создаем новую вкладку с анализом
const panel = vscode.window.createWebviewPanel(
'sapCodeAnalysis',
'Анализ кода',
vscode.ViewColumn.Two,
{ enableScripts: true }
);
panel.webview.html = this._getAnalysisHtml(response);
});
});
// Автодополнение на основе документации
let completionProvider = vscode.languages.registerCompletionItemProvider(
'abap',
{
async provideCompletionItems(document, position) {
const linePrefix = document.lineAt(position).text.substr(0, position.character);
// Если пользователь начинает писать CALL METHOD или CALL FUNCTION
if (linePrefix.includes('CALL') || linePrefix.includes('CALL FUNCTION')) {
const suggestions = await this.rag.getCodeSuggestions(linePrefix);
return suggestions.map(s => {
const item = new vscode.CompletionItem(s.name, vscode.CompletionItemKind.Method);
item.documentation = new vscode.MarkdownString(s.description);
return item;
});
}
return [];
}
}
);
context.subscriptions.push(analyzeCommand, completionProvider);
}
_getAnalysisHtml(analysis) {
return `
Анализ ABAP кода
${analysis.overview}
${analysis.issues ? `Потенциальные проблемы:
${analysis.issues.map(i => `${i}`).join('')}` : ''}
${analysis.optimizations ? `Возможные оптимизации:
${analysis.optimizations.map(o => `${o}`).join('')}` : ''}
${analysis.examples ? `Примеры использования:
${analysis.examples.map(e => `${e}`).join('')}` : ''}
`;
}
}
module.exports = SAPCodeAssistant;
Типичные ошибки и как их избежать
Я видел десятки попыток построить подобные системы. Вот что ломается чаще всего:
| Ошибка | Почему происходит | Как исправить |
|---|---|---|
| Модель "галлюцинирует" кодом | Слишком мало контекста или плохие чанки | Добавляйте в промпт "Если не знаешь - скажи 'не знаю'". Улучшайте разбиение кода. |
| Поиск находит нерелевантное | Плохие эмбеддинги или нет фильтрации | Используйте multilingual модель. Добавьте фильтрацию по типу документа. |
| Система медленная | Слишком большая модель или много чанков | Используйте кэширование. Берите 8B модели вместо 70B. Оптимизируйте количество чанков. |
| Конфиденциальные данные утекают | Использование облачных API без фильтрации | Только локальные модели для sensitive данных. Или строгая предварительная фильтрация. |
Что дальше? Эволюция архивариуса
Базовая система работает. Но настоящая магия начинается, когда вы добавляете:
- Автоматическое обучение: Система сама предлагает добавить в базу часто искомые, но отсутствующие темы
- Контекст проектов: Разные базы для разных клиентов или проектов
- Интеграцию с SAP GUI: Плагин, который подсказывает прямо в транзакции
- Прогностику: "На основе твоих чатов, вот что тебе стоит почитать на следующей неделе"
Самое интересное - система начинает обучаться на ваших паттернах. Если вы часто спрашиваете про ALV отчеты - она предложит создать "скилл" для работы с ALV. Если постоянно сталкиваетесь с ошибками RFC - соберет базу решений. Это уже не просто поисковик, а персональный наставник.
Через 3 месяца использования вы обнаружите, что система знает о вашей работе с SAP больше, чем вы сами помните. Она становится цифровым двойником вашего экспертного опыта.
Начните с малого. Возьмите один проект, один тип данных (например, ABAP-код). Постройте минимальную работающую систему. Потом добавляйте чаты, потом документацию. Через месяц у вас будет инструмент, который сэкономит 2-3 часа в день. Через полгода - вы забудете, как выглядит SAP Help Portal.
P.S. Если думаете, что это слишком сложно - попробуйте неделю вести детальный лог всех поисковых запросов в SAP Help. Умножьте на 5 лет карьеры. Теперь посчитайте, сколько времени вы уже потратили на поиск информации, которую уже однажды находили. Архивариус окупается за первый месяц.