LLM-архивариус для SAP: настройка, ABAP-код суммаризация чатов | AiManual
AiManual Logo Ai / Manual.
06 Янв 2026 Гайд

Персональный LLM-архивариус для SAP: ABAP-код, чаты и спасение от документационного ада

Пошаговый гайд по созданию персонального LLM-архивариуса для SAP с анализом ABAP-кода и суммаризацией Telegram-чатов. Практические решения для консультантов.

Консультант 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 содержит миллионы страниц. Брать все - бессмысленно.

💡
Собирайте только то, что реально используете. Начните с модулей, с которыми работаете: FI, CO, MM, SD. Экспортируйте Help Pages в HTML, конвертируйте в текст. Добавляйте внутренние Wiki-страницы клиента.

Для обработки документации используйте готовые решения вроде семантического пайплайна. Главное - сохраняйте иерархию: модуль → транзакция → справка → пример кода.

Шаг 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}

Ответ:"""

Эта система делает несколько умных вещей:

  1. Классифицирует запрос (код, ошибка, инструкция)
  2. Подбирает соответствующий тип документов
  3. Строит специализированный промпт
  4. Фильтрует маловероятные результаты

Шаг 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 лет карьеры. Теперь посчитайте, сколько времени вы уже потратили на поиск информации, которую уже однажды находили. Архивариус окупается за первый месяц.