local-corporate-kb
by arptra
README.md
# Локальная корпоративная база знаний для Qwen Code
Это локальный MVP корпоративного RAG: документы индексируются Python-процессом, embeddings
сохраняются в проверяемый файловый кэш, а при поиске целиком находятся в RAM. Qwen Code остаётся
единственной генеративной моделью и получает найденные фрагменты через read-only MCP tools по
локальному `stdio` или удалённому Streamable HTTP. MCP-сервер не формулирует финальные ответы,
не исполняет shell-команды и не изменяет документы.
Проект рассчитан на Python 3.12 и standalone `FastMCP==3.4.4`. Запуск после установки не зависит
от `uv`: все runtime-скрипты вызывают Python из `.venv` напрямую. Для разработки доступна
воспроизводимая установка через `uv.lock`, а для корпоративных машин — отдельная установка через
обычный `pip`.
Отдельные пошаговые инструкции:
- [подключение Qwen на клиентском компьютере](README.client.md);
- [развёртывание базы и API на удалённом сервере](README.server.md);
- [быстрый запуск RAG с уменьшенным контекстом](README.low-context.md);
- [отдельный общий SSOT-индекс всех сервисов](README.ssot.md).
## Архитектура
```text
Confluence export
↓
knowledge/*.md, *.html, *.txt
↓
loader + normalizer + structural chunker
↓
local feature hashing (по умолчанию) или локальная embedding-модель
↓
NumPy matrix in RAM
↓
MCP stdio / Streamable HTTP
↓
Qwen Code CLI
```
В удалённом режиме тот же индекс один раз загружается в память серверного процесса, после чего к
нему одновременно подключаются Qwen Code CLI с разных машин:
```text
Qwen CLI ─┐
Qwen CLI ─┼─ HTTPS / Bearer token ─ MCP Streamable HTTP ─ in-memory index
Qwen CLI ─┘
```
`DocumentLoader` безопасно обходит только `KB_KNOWLEDGE_DIR`, нормализует Markdown/TXT и переводит
экспортированный HTML в Markdown-подобный текст. `StructuralChunker` сохраняет путь заголовков,
списки, таблицы и code fences. `KnowledgeService` координирует кэш и работает только через
интерфейс `KnowledgeStore`; MCP-слой не знает о NumPy.
В RAM находятся документы, чанки, отображение `chunk_id -> index` и нормализованная NumPy-матрица
`[chunk_count, embedding_dimension]`. Cosine similarity считается как `matrix @ query_vector`.
### Экономия контекста Qwen
Поиск не передаёт модели все найденные тексты. Сервер сначала находит до 12 кандидатов внутри
индекса, затем отдаёт максимум 3 наиболее релевантные выдержки из разных документов: до 260
условных токенов на выдержку и до 1000 на один ответ инструмента. Выдержка выбирается по словам
вопроса и сохраняет ссылку на источник. Это ограничивает расход контекста, даже если в базе десятки
тысяч страниц.
Если выбранный результат требует деталей, Qwen вызывает `kb_get_chunk` с `chunk_id`; полный текст
не загружается автоматически. `kb_get_document` также возвращает ограниченный извлекаемый фрагмент.
Лимиты настраиваются через `KB_SEARCH_*` и `KB_DOCUMENT_CONTEXT_TOKENS` в `.env.example`.
Защищённый `kb_run_context_benchmark` сравнивает прежние `top-5` полных чанков с текущими `top-3`
выдержками: Hit@K, оценку токенов, точные JSON bytes, процент сжатия и latency. Инструмент требует
отдельный `KB_BENCHMARK_PASSWORD`, который Qwen запрашивает перед каждым вызовом.
В HTTP-режиме по `/admin` доступна встроенная парольная панель: usage-счётчики, живая системная
нагрузка, память и uptime процесса, список документов, загрузка Markdown/HTML/TXT и фоновая
инкрементальная переиндексация. Декларативные MCP search-tools по-прежнему доступны через
защищённый API, но технический редактор schemas из основной панели убран.
На диске в `.cache/kb/` находятся только:
- `manifest.json` — версии схемы, идентичность модели, chunking config и knowledge hash;
- `documents.json` — нормализованные документы и metadata;
- `chunks.json` — чанки без отдельной копии embedding;
- `embeddings.npy` — матрица без pickle.
Это не Vector DB: нет отдельного сервиса хранения, индекса ANN или SQL. Удалённый HTTP — только
read-only MCP-фасад; поиск выполняется полным cosine scan по NumPy-матрице в памяти, а диск
используется для ускорения старта.
## Первый запуск
### Подключение сотрудника к удалённой базе
RAG, индекс и документы находятся только на сервере. Для старых версий Qwen сотруднику
передаётся один файл [`clients/corporate_kb_stdio_proxy.py`](clients/corporate_kb_stdio_proxy.py).
Qwen запускает его локально через `uv` как stdio MCP, а Python-процесс ходит к удалённому
серверу через обычные HTTP GET-запросы. Node.js, `npx`, `mcp-remote`, Nginx, копия проекта,
`.venv`, документы и индекс на клиенте не нужны.
Готовый settings находится в
[`examples/qwen-uv-stdio-settings.example.json`](examples/qwen-uv-stdio-settings.example.json), а полная
инструкция для сотрудника — в [`README.client.md`](README.client.md).
Новые версии Qwen также могут подключаться к `/mcp` напряму через Streamable HTTP; скрипт
`install.sh` оставлен как опциональный способ для таких клиентов.
Скопируйте из неё `mcpServers` в `~/.qwen/settings.json` и замените четыре placeholder:
- `REPLACE_WITH_ABSOLUTE_UV_PATH` — результат `which uv` или `where uv` (в Windows `uv.exe`);
- `REPLACE_WITH_ABSOLUTE_PATH` — каталог, в котором сотрудник сохранил единственный `.py`-файл;
- `REPLACE_WITH_SERVER_IP_OR_DOMAIN` — адрес удалённого сервера;
- `REPLACE_WITH_SERVER_TOKEN` — Bearer-токен.
Qwen запускает этот файл как локальный MCP по `stdio` командой `uv run`. Скрипт содержит inline
dependency на FastMCP, поэтому `uv` сам создаёт изолированное кэшированное окружение. Локальный MCP
обращается к удалённому RAG только через обычные авторизованные JSON GET endpoints `/api/v1/*`.
`node`, `npx`, `mcp-remote`, локальная копия документов и локальный индекс не нужны.
### Установка серверной части
Убедитесь, что доступен Python 3.12. На корпоративной машине рекомендуется pip-вариант: он не
читает `uv.lock`, не запускает `uv` и не зависит от установленной в системе версии `uv`.
Hugging Face, PyTorch и `sentence-transformers` в базовую установку не входят:
```bash
./scripts/setup-pip.sh
source ./scripts/activate-venv.sh
```
Для разработки с точными версиями из lock-файла остаётся вариант:
```bash
./scripts/setup-venv.sh
source ./scripts/activate-venv.sh
```
Оба варианта создают одинаковую `.venv`; runtime-команды используют только `.venv/bin/python`.
Полностью локальный режим по умолчанию использует hash provider и не требует модели или сети:
```bash
./scripts/dev.sh index-hash
./scripts/dev.sh search-hash
```
Hash provider строит локальные lexical vectors из слов и символьных триграмм. Он пригоден для
полностью автономного поиска по совпадающей терминологии, но не понимает смысл и синонимы так же
хорошо, как semantic embedding model.
Для качественного semantic search сначала положите заранее полученные и одобренные model files в
локальный каталог. Этот проект не скачивает их. Например:
```bash
models/Qwen3-Embedding-0.6B/
```
После этого активируйте окружение, укажите только локальный путь и постройте индекс:
```bash
./scripts/dev.sh install-pip-semantic
source ./scripts/activate-venv.sh
export KB_EMBEDDING_PROVIDER=sentence_transformers
export KB_EMBEDDING_MODEL="$KB_PROJECT_ROOT/models/Qwen3-Embedding-0.6B"
export KB_EMBEDDING_LOCAL_FILES_ONLY=true
./scripts/dev.sh index-semantic
./scripts/dev.sh search "Какой сервис владеет дневными лимитами?"
```
`local_files_only=true`, `HF_HUB_OFFLINE=1` и `TRANSFORMERS_OFFLINE=1` запрещают обращения к
Hugging Face. Если model files отсутствуют, индексирование завершится понятной ошибкой без попытки
скачивания. По умолчанию выбирается CUDA, затем MPS, затем CPU.
`scripts/start-mcp.sh` по умолчанию запускает MCP с `KB_EMBEDDING_PROVIDER=hash`, поэтому обычное
подключение Qwen полностью offline. Для локальной semantic-модели явно передайте provider и путь в
environment Qwen-конфигурации. Все runtime wrappers вызывают Python из готовой `.venv` напрямую:
после установки они не обращаются к package registry и не меняют окружение.
## CLI
```bash
./.venv/bin/python -m corporate_kb.cli index
./.venv/bin/python -m corporate_kb.cli index --force
./.venv/bin/python -m corporate_kb.cli search "Как рассчитывается дневной лимит?" --top-k 5
./.venv/bin/python -m corporate_kb.cli search "Как рассчитывается дневной лимит?" --service limits-service
./.venv/bin/python -m corporate_kb.cli documents
./.venv/bin/python -m corporate_kb.cli stats
./.venv/bin/python -m corporate_kb.cli eval --top-k 5
```
У `search`, `documents`, `stats` и `eval` есть `--json`. В этом режиме stdout содержит только JSON,
а логи остаются в stderr.
Если кэша нет или он несовместим, обычный поиск при `KB_AUTO_INDEX=false` завершится практичным
сообщением `Run: ./scripts/dev.sh index`. Это предотвращает неожиданную сетевую активность во время
MCP discovery.
## Подключение к Qwen Code
Если MCP-серверы хранятся в отдельном каталоге, установите туда автономную runtime-копию. Скрипт
создаёт подкаталог `corporate-kb`, копирует только необходимые файлы, создаёт собственный `.venv`,
ставит locked runtime dependencies без dev-пакетов, строит hash-индекс и печатает готовый server
entry для Qwen:
```bash
./scripts/install-mcp-server.sh /absolute/path/to/mcp-servers
```
Если версия `uv` на целевой машине отличается или `uv` запрещён политиками, установите ту же
runtime-копию через pip:
```bash
./scripts/install-mcp-server.sh /absolute/path/to/mcp-servers --pip
```
Для закрытого окружения можно сначала только скопировать файлы, затем настроить корпоративный Python
package registry и завершить установку командами, которые напечатает скрипт:
```bash
./scripts/install-mcp-server.sh /absolute/path/to/mcp-servers --copy-only
```
Скопируйте `examples/qwen-settings.example.json` в `.qwen/settings.json` проекта и замените все
`/ABSOLUTE/PATH/...` реальными абсолютными путями. Не рассчитывайте на раскрытие `${PROJECT_ROOT}`
в JSON. В `command` указан абсолютный путь к `.venv/bin/python`, а в `args` — запуск модуля
`corporate_kb.mcp.server`. Поэтому Qwen не зависит от глобальных `python`, `uv`, `PATH`, shell
activation или wrapper-скрипта.
Минимальная форма server entry:
```json
{
"command": "/absolute/path/to/repository/.venv/bin/python",
"args": ["-m", "corporate_kb.mcp.server"],
"cwd": "/absolute/path/to/repository",
"env": {
"PYTHONPATH": "/absolute/path/to/repository/src"
}
}
```
Альтернатива через CLI (выполняйте из корня этого репозитория, подставив абсолютные пути):
```bash
qwen mcp add \
--scope project \
--timeout 120000 \
-e KB_KNOWLEDGE_DIR=/absolute/path/to/repository/knowledge \
-e KB_CACHE_DIR=/absolute/path/to/repository/.cache/kb \
-e KB_EMBEDDING_PROVIDER=hash \
-e KB_EMBEDDING_LOCAL_FILES_ONLY=true \
-e HF_HUB_OFFLINE=1 \
-e TRANSFORMERS_OFFLINE=1 \
-e PYTHONUNBUFFERED=1 \
-e PYTHONNOUSERSITE=1 \
-e PYTHONPATH=/absolute/path/to/repository/src \
-e KB_AUTO_INDEX=false \
local-corporate-kb \
/absolute/path/to/repository/.venv/bin/python \
-m corporate_kb.mcp.server
```
`stdio` — транспорт по умолчанию, поэтому `--transport http` здесь не нужен. Синтаксис команды
сверен с [официальной документацией Qwen Code](https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/),
но в среде разработки этого репозитория `qwen` не был установлен, и команда локально не выполнялась.
JSON-конфигурация также задаёт `cwd` и `trust: false`; статического фильтра tools в ней нет.
Проверка подключения:
```text
qwen
/mcp
```
Тестовый запрос:
```text
Используй corporate knowledge MCP.
Найди, какой сервис владеет дневными лимитами,
объясни правило и обязательно укажи использованные источники.
```
Встроенные tools сервера:
- `kb_search` — поиск с `top_k`, `min_score`, metadata filters и компактными выдержками;
- `kb_get_chunk` — лениво загружает один ограниченный фрагмент по `chunk_id`;
- `kb_run_context_benchmark` — защищённый паролем read-only замер качества и сжатия;
- `kb_get_document` — ограниченный извлекаемый фрагмент документа по `document_id`;
- `kb_list_documents` — metadata документов без embeddings;
- `kb_stats` — состояние индекса и абсолютные пути.
Не задавайте статический `includeTools` в Qwen settings, если используете управляемые tools из UI:
клиентский allowlist скроет новые схемы от LLM. Прямой HTTP-клиент обновляет `/mcp` discovery, а
однофайловый stdio proxy перечитывает каталог после перезапуска Qwen.
Ручной запуск stdio server:
```bash
KB_LOG_LEVEL=DEBUG ./.venv/bin/python -m corporate_kb.mcp.server
```
stdout зарезервирован для MCP-протокола; все application logs направляются в stderr.
## Удалённый MCP по HTTP
Удалённый режим заранее загружает готовый индекс и только после этого открывает порт. Поэтому все
подключённые Qwen CLI используют один прогретый процесс и не строят embeddings при каждом запросе.
Endpoint реализует рекомендованный для удалённых MCP-серверов Streamable HTTP, а не устаревший SSE.
### 1. Подготовить сервер
Скопируйте репозиторий и документы на сервер, установите runtime и один раз постройте индекс:
```bash
cd /opt/corporate-kb
./scripts/setup-pip.sh --no-dev
./scripts/dev.sh index-hash
```
Сгенерируйте отдельный секрет длиной не менее 32 символов:
```bash
openssl rand -hex 32
```
Для прямого запуска внутри доверенной сети или VPN задайте секрет и запустите listener на всех
сетевых интерфейсах:
```bash
export KB_MCP_HTTP_BEARER_TOKEN='PASTE_GENERATED_TOKEN'
export KB_MCP_HTTP_HOST='0.0.0.0'
export KB_MCP_HTTP_PORT='8000'
export KB_AUTO_INDEX='false'
./scripts/start-mcp-http.sh
```
Публичный health check не раскрывает тексты документов:
```bash
curl http://10.0.0.5:8000/health
```
Для stdio-клиента сервер также предоставляет защищённый read-only JSON API. Например, проверка
поиска использует обычный GET и тот же Bearer-токен:
```bash
curl -G 'http://10.0.0.5:8000/api/v1/search' \
-H 'Authorization: Bearer PASTE_GENERATED_TOKEN' \
--data-urlencode 'query=какой сервис владеет дневными лимитами' \
--data-urlencode 'top_k=3'
```
Доступны `/api/v1/search`, `/api/v1/document`, `/api/v1/chunk`, защищённый
`/api/v1/admin/context-benchmark`, `/api/v1/documents` и `/api/v1/stats`. Они используют
тот же прогретый индекс, что и MCP tools, не строят embeddings на клиенте и не изменяют документы.
Сам `/mcp` требует заголовок `Authorization: Bearer ...`. Ограничения по Host, Origin, домену или IP
нет: сервер принимает клиента с любого адреса, если передан правильный токен. `KB_AUTO_INDEX=false`
гарантирует, что удалённый процесс не начнёт неожиданную переиндексацию.
Проверяйте с клиентской машины не только `/health`, но и настоящий MCP `initialize`:
```bash
curl -i --max-time 15 \
'http://10.0.0.5:8000/mcp' \
-H 'Authorization: Bearer PASTE_GENERATED_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}'
```
Ожидается `HTTP/1.1 200`. `401` означает неверный токен, `404` — неверный путь, а `421` — что
запущена старая сборка с Host allowlist. Прямой FastMCP listener, запущенный через Python или `uv`,
использует обычный HTTP. `https://` указывайте только при наличии TLS reverse proxy; иначе Qwen
обычно сообщает `TypeError: fetch failed`.
### 2. Подключить Qwen CLI
Перед раздачей впишите в корневой `install.sh` публичный HTTPS-адрес сервера и созданный токен:
```bash
default_mcp_url="https://kb.company.example/mcp"
default_mcp_token="THE_SERVER_TOKEN"
```
Сотруднику передаётся только этот один файл. В любом каталоге он выполняет:
```bash
bash install.sh
```
Скрипт не скачивает репозиторий, документы или Python-зависимости и не создаёт каталог RAG. Он
только добавляет подключение `corporate-kb` в пользовательскую конфигурацию уже установленного
Qwen Code. После запуска сотрудник перезапускает `qwen` и проверяет соединение через `/mcp`.
Bearer-токен внутри готового скрипта является секретом: раздавайте файл через защищённый
корпоративный канал. Для отзыва доступа замените токен на сервере и выпустите новый скрипт.
### 3. Доступ через интернет
Не передавайте Bearer-токен по открытому интернету через обычный HTTP. Оставьте backend на
`127.0.0.1:8000`, а наружу опубликуйте его как HTTPS через Nginx, Caddy, ingress или корпоративный
API gateway:
```bash
export KB_MCP_HTTP_BEARER_TOKEN='PASTE_GENERATED_TOKEN'
export KB_MCP_HTTP_HOST='127.0.0.1'
./scripts/start-mcp-http.sh
```
Минимальные существенные параметры location для Nginx:
```nginx
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 3600s;
}
```
После этого клиент подключается к `https://kb.example.com/mcp`. TLS-сертификат и сетевой доступ
настраиваются на reverse proxy; порт `8000` не должен быть открыт наружу.
Когда документы изменились, выполните `./scripts/dev.sh index-hash` и перезапустите HTTP-процесс.
Уже работающий процесс намеренно продолжает обслуживать согласованную старую версию индекса до
рестарта.
## Добавление Confluence-страницы
Экспортируйте страницу в HTML либо сохраните её как Markdown и положите внутрь `knowledge/`.
Поддерживаются `.md`, `.markdown`, `.html`, `.htm`, `.txt`. Скрытые каталоги, `.git`, `.cache`,
`__pycache__`, `node_modules`, бинарные и неподдерживаемые файлы игнорируются. После изменения
перестройте индекс; при обычном запуске несовпадение `knowledge_hash` также инвалидирует кэш.
Пример front matter:
```yaml
---
document_type: service
service: limits-service
domain: payments
status: current
authority: confluence
authority_priority: 80
owner: limits-team
source_id: "confluence-12345"
source_url: "https://confluence.example.com/pages/12345"
last_reviewed: "2026-07-20"
custom_field: "неизвестные поля тоже сохраняются"
---
# Limits Service
```
Без front matter заголовок берётся из первого H1 или имени файла, `source_id` — из относительного
пути, `status=current`, `authority=local_file`, `authority_priority=50`.
## Кэш и конфигурация
Пересобрать кэш:
```bash
./scripts/dev.sh index
```
Полностью удалить его можно командой `rm -rf .cache/kb`, после чего снова выполнить `kb index`.
Запись каждого файла атомарна, а `manifest.json` заменяется последним. Повреждение JSON/NumPy,
несовпадение схемы, модели, dimension, query instruction, chunking config или knowledge hash приводит
к понятной invalidation, а не к неясной NumPy-ошибке.
Все параметры перечислены в `.env.example`. Основные:
- `KB_EMBEDDING_PROVIDER=sentence_transformers|hash`;
- `KB_EMBEDDING_MODEL=./models/Qwen3-Embedding-0.6B` — локальный каталог model files;
- `KB_EMBEDDING_LOCAL_FILES_ONLY=true` — fail-closed запрет сетевой загрузки модели;
- `KB_EMBEDDING_DEVICE=auto|cpu|mps|cuda`;
- `KB_EMBEDDING_DIMENSION=1024`;
- `KB_CHUNK_SIZE_TOKENS=700`, `KB_CHUNK_HARD_MAX_TOKENS=900`,
`KB_CHUNK_OVERLAP_TOKENS=80`;
- `KB_AUTO_INDEX=false`.
- `KB_MCP_HTTP_HOST`, `KB_MCP_HTTP_PORT`, `KB_MCP_HTTP_PATH`;
- `KB_MCP_HTTP_BEARER_TOKEN` — обязательный секрет для HTTP-режима;
Относительные пути разрешаются относительно текущего project working directory; `kb stats`
показывает итоговые абсолютные пути.
## Проверки
```bash
./scripts/dev.sh lint
./scripts/dev.sh typecheck
./scripts/dev.sh test
./scripts/dev.sh check
```
Обычный shell-скрипт `scripts/dev.sh` также объединяет повседневные команды:
```bash
./scripts/dev.sh install
./scripts/dev.sh install-pip
./scripts/dev.sh install-semantic
./scripts/dev.sh install-pip-semantic
./scripts/dev.sh test
./scripts/dev.sh lint
./scripts/dev.sh typecheck
./scripts/dev.sh index-hash
./scripts/dev.sh search-hash
./scripts/dev.sh index
./scripts/dev.sh search
./scripts/dev.sh index-semantic
./scripts/dev.sh eval
./scripts/dev.sh serve
./scripts/dev.sh serve-http
```
Тесты всегда инжектируют hash provider и не требуют интернета, Hugging Face, GPU, Qwen Code, Docker
или внешней БД. Интеграционные тесты проверяют как in-memory MCP transport, так и HTTP handshake
через ASGI без открытия сетевого порта.
## Ограничения MVP и развитие
- Полный brute-force cosine scan подходит для небольшой локальной базы, но не для миллионов чанков.
- При изменении документов неизменившиеся chunks и их embeddings переиспользуются из предыдущего
кэша; полный пересчёт нужен только для новых или изменившихся chunks.
- Нет Confluence REST API, OAuth, фоновой синхронизации и HTML-адаптеров под каждый вариант экспорта.
- Нет reranker, hybrid/BM25 retrieval и отдельной оценки authority при ранжировании.
- Точный token counter реальной модели не используется для предварительного chunking: интерфейс
`TokenCounter` отделён, поэтому его можно подключить без связи chunker с SentenceTransformer.
- Статический Bearer-токен даёт всем клиентам одинаковые права; для персональных учётных записей,
отзыва сессий и аудита нужен внешний gateway/IdP либо полноценный OAuth.
Для перехода на настоящую Vector DB нужно реализовать `PostgresKnowledgeStore` или
`QdrantKnowledgeStore` с тем же контрактом `KnowledgeStore`, выбрать реализацию при сборке
`KnowledgeService` и сохранить API сервиса/MCP без изменений. Следующим этапом стоит добавить
инкрементальный cache manifest, batch upsert, hybrid retrieval и production evaluation corpus.
TDQS
A3.8/5.0
Scored across 4 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: kb_search for searching, kb_get_document for retrieving full documents, kb_list_documents for listing metadata, and kb_stats for index statistics. No overlap or ambiguity.
Naming Consistency5/5
All tools follow a consistent 'kb_verb' pattern with snake_case (e.g., kb_search, kb_get_document). Naming is predictable and uniform.
Tool Count5/5
With 4 tools, the server is well-scoped for a corporate knowledge base: search, retrieval, listing, and statistics. Each tool adds value without redundancy.
Completeness5/5
The tool surface covers the core operations for a read-only knowledge base: search, get full document, list metadata, and view stats. No obvious gaps given the intended use case.
Maintenance
ActivitySlowing
ResponsivenessNo issues