# LangChain ↔ RAGRAF

Тонкий адаптер: RAGRAF остаётся ядром, LangChain получает его данные в своих
типах. Логика поиска, ранжирования, исключений и провенанса не дублируется —
все пути идут через фасад RAGRAF (`src/ragraf/facade.py`, 67 операций `op_*`).

Три уровня, и это разные вопросы, а не три способа сделать одно и то же:

| Уровень | Класс | Когда | Что даёт |
|---|---|---|---|
| Обязательный поиск | `RAGRAFRetriever` | «каждый ответ опирается на индекс» | `Document[]` с адресом `path:line` |
| Инструменты агента | `build_tools()` | «агент сам решает, когда искать» | 7 узких операций |
| MCP | `load_mcp_tools()` | несколько клиентов / изоляция зависимостей | 74 инструмента сервера |

Выбор «retriever или tool» — это выбор между 2-step RAG и agentic RAG, а не
вкусовщина: retriever вызывается пайплайном до генерации, инструмент — моделью
по её решению. Если требование звучит как «ответ обязан опираться на индекс»,
инструмента недостаточно: модель может его не вызвать.

## Подключение

### 1. Окружение (отдельное от рабочего окружения RAGRAF)

```powershell
# <каталог окружения> замените на свой, например подкаталог .venvs в удобном месте.
$venv = '<каталог окружения>'
& python -m venv $venv
$py = Join-Path $venv 'Scripts\python.exe'
& $py -m pip install --no-user -r integrations\langchain\requirements.txt `
                            -r requirements.txt
```

Замерено 03.11.2026 в этом дереве: Python 3.14.7, langchain 1.4.8,
langchain-core 1.7.4, fastmcp 4.1.6, mcp 2.3.0, fastembed 0.8.1, sqlite-vec 0.1.9.

Два предупреждения, оба проверены на живом окружении:

* `pip` в этом окружении подставляет `--user` из своей конфигурации, и в venv это
  падает (`Can not perform a '--user' install`). Лечится `--no-user` или
  `PIP_USER=0`.
* Пакет `ragraf` здесь **не устанавливается**: он подключается через `src`-шим
  (`_bridge._import_ragraf`, тот же приём, что в `tools/ragraf.py:27-33`).
  Editable-установка положила бы `*.egg-info` внутрь корпуса и сдвинула бы
  контрольную сумму — тогда сторож переиндексировал бы проект из-за самого факта
  установки.

### 2. Обязательный поиск

```python
from ragraf_langchain import RAGRAFRetriever

retriever = RAGRAFRetriever(config_path=r"config/ragraf.yaml",   # или RAGRAF_CONFIG
                           limit=6, kinds=["code", "doc"])
docs = retriever.invoke("где считается контрольная сумма корпуса")
for doc in docs:
    print(doc.metadata["ref"])      # src/ragraf/corpus.py:75
```

`RAGRAFRetriever` самодостаточен: он подходит и для `create_retrieval_chain`, и
для ручного 2-step RAG. `ainvoke` работает: SQLite в RAGRAF синхронный, поэтому
запрос уводится в поток, а не блокирует цикл событий.

### 3. Инструменты агента

```python
from ragraf_langchain import build_tools

tools = build_tools()          # ragraf_search, ragraf_symbol, ragraf_callers,
                              # ragraf_impact, ragraf_read, ragraf_write_check,
                              # ragraf_freshness
```

Схема аргументов у каждого инструмента описана моделью pydantic — именно по ней
модель понимает, когда и как его звать. Универсального `ragraf_execute(action,
payload)` здесь нет намеренно: такая схема не подсказывает модели ничего.

### 4. MCP (когда нужен отдельный процесс)

```python
import asyncio
from ragraf_langchain import load_mcp_tools

async def main():
    tools = await load_mcp_tools()          # 74 инструмента сервера ragraf
    print([t.name for t in tools][:5])

asyncio.run(main())
```

`load_mcp_tools(compact=True)` — по умолчанию: ответ сервера приводится к тому же
тексту с адресом `path:line`, что и у прямого вызова. `compact=False` — сырые
блоки сервера для тех, кто разбирает `structuredContent` сам.

Сервер: `integrations/langchain/ragraf_mcp.py` (запускатор, подставляет `src` в
`sys.path`) → `src/ragraf/mcp_server.py`, JSON-RPC 2.0 поверх stdio, протокол
`2024-11-05`. Шаблон подключения без Python-кода — `mcp.template.json`.

Альтернатива «написать HTTP API и звать его из retriever» в этом дереве не
нужна: у RAGRAF нет HTTP-поверхности поиска (панель `admin_server` — интерфейс с
токеном, а не API), а второй сервер означал бы вторую реализацию поиска, доступа
и провенанса. MCP уже даёт межпроцессную границу и изоляцию зависимостей, ничего
не переопределяя.

## Проверка

```powershell
# L2-смоук проекта: получен ли адрес path:line (коды 0/1/2/3)
pwsh -NoProfile -File integrations\langchain\smoke.ps1

# то же без PowerShell, с подробностями по каждому уровню
& $py -m ragraf_langchain.smoke `
    --config ..\..\config\ragraf.yaml      # из integrations\langchain

# тесты: работают и как скрипты (штатный стиль RAGRAF), и через pytest
cd integrations\langchain
& $py tests\test_retriever_document.py     # контракт метаданных, без корпуса
& $py tests\test_tool_text.py              # доказано против выведено
& $py tests\test_live_index.py             # живой индекс: адреса, кэш, async
& $py tests\test_mcp_path.py               # MCP: 74 инструмента и вызов
& $py -m pytest tests -q                   # то же одним прогоном
```

Замеры на этом корпусе (rev-1364, 1058 файлов, 03.11.2026, 5 повторов):

| Что | Значение |
|---|---|
| первый `invoke` в процессе (фасад + эмбеддинг) | 2 975 мс |
| повторный `invoke` | 660 мс (медиана; 650–772) |
| обнаружение 74 MCP-инструментов | 310 мс |
| первый вызов `ragraf_search` через MCP | 3 371 мс |
| повторный вызов через MCP | 675 мс (медиана; 656–766) |

Главный вывод из замера: транспорт почти ничего не стоит. Разница между прямым
вызовом и MCP на прогретом индексе — 15 мс (660 против 675), то есть протокол не
является узким местом. Основное время уходит на эмбеддинг запроса и лексический
поиск, и оно одинаково с обеих сторон. Поэтому выбирать между путями надо по
свойствам (изоляция зависимостей, переиндексация, число клиентов), а не по
скорости.

## Границы

Названы по измерениям, а не по опасениям.

* **`langchain.mcp` — beta.** Пространство есть в langchain 1.4, но требует
  дополнение `langchain[mcp]` (тянет `fastmcp`) и выдаёт `LangChainBetaWarning`
  при импорте. Привязывайтесь к версии LangChain, а не к примеру из статьи.
* **`langchain-mcp-adapters` несовместим с `mcp` 2.x.** Пакет 0.3.2 объявляет
  `mcp>=1.24,<2`, но его код ждёт `RequestContext` из `mcp.shared.context`, а
  `mcp` 2.3.0 его не отдаёт: импорт падает. Два MCP-пути в одном окружении не
  живут — выбирайте один.
* **MCP-сервер переиндексирует сам.** Он собран с `watch=True`
  (`src/ragraf/mcp_server.py:2163`) и переключателя нет, поэтому первый вызов
  после правки кода длится столько же, сколько переиндексация (замер: ≈ 290 с на
  1058 файлах). Прямой путь этого не делает: `watch=False` по умолчанию, а
  свежесть возвращается полем `stale` в метаданных и инструментом
  `ragraf_freshness`. Для воспроизводимых тестов предпочтителен прямой путь.
* **Фасад — на поток, а не на процесс.** Соединения `sqlite3` привязаны к создавшему
  их потоку, поэтому интеграция кэширует фасад по паре «конфигурация + поток».
  Практическое следствие: `ainvoke` работает (запрос уходит в поток, там свой
  фасад), но каждый новый поток пула открывает свои соединения. Замер: без этого
  правила `ainvoke` падал с `SQLite objects created in a thread can only be used in
  that same thread` — ошибка поймана собственным тестом
  `test_async_invoke_matches_sync`, а не в бою.
* **`score` не вероятность.** Это взвешенное RRF-слияние лексики (вес 1.0) и
  вектора (вес 0.6) — `src/ragraf/lexvec.py:562-598`. Значение сравнимо только
  внутри одного ответа.
* **Пустой ответ — не ошибка.** По контракту RAGRAF «ничего не найдено» — это
  `found=false`, а не исключение (`src/ragraf/cli.py:87`). Интеграция возвращает
  пустой список и текст с причиной.
* **MCP-ответ не содержит `path:line` как строки.** Сервер отдаёт два текстовых
  блока: сводку и весь ответ в JSON, а адрес в JSON разложен на `path` и
  `start_line` (`src/ragraf/mcp_server.py:3688-3706`). Проверка адреса на MCP-пути
  падала именно на этом, хотя поиск отработал (замер 03.11.2026). С `compact=True`
  интеграция разбирает JSON и рендерит тот же текст, что и прямой путь.
* **Связи бывают выведенными.** В этом дереве 75.7 % рёбер — эвристика
  (`index_status`, rev-1364). Инструменты помечают такие связи словом
  «ВЫВЕДЕНО»; считать их доказательством нельзя.
* **Канал в статистике.** Фасад создаётся с `tool="dev"` из закрытого перечня
  (`mcp`, `cli`, `admin`, `dev`, `selftest`). Шестое значение интеграция не
  заводит, чтобы статистика по каналам не расходилась с документацией; агент
  виден как `agent="langchain"`, `agent_kind="external"`.
* **Только чтение.** Интеграция не пишет в корпус: у фасада нет операций записи
  индекса. `ragraf_write_check` проверяет план SQL против схемы, но сам SQL не
  выполняет.
