schedule5 мин чтения

Полная наблюдаемость агентов в собственной базе данных с LangChain, LangSmith SDK и DeepAgents

Как отслеживать каждый вызов LLM, вызов инструмента и подагента в LangChain DeepAgents — и хранить всё в собственной MongoDB вместо облака LangSmith.

translate
Доступно на:
infoЭта статья переведена с помощью ИИ

Каждый вызов агента LangChain создаёт дерево запусков: корневой агент, подагенты, вызовы инструментов, вызовы LLM. LangSmith отслеживает это автоматически. Но что если данные трассировки вам нужны в собственной базе данных?

Мы построили продакшен-систему, которая отслеживает каждый вызов LLM в LangChain DeepAgents и хранит его в MongoDB — используя только официальные типы SDK, без пользовательских подклассов BaseTracer, без переобявления полей. Вот как.


Проблема

Облако LangSmith даёт трассировку из коробки. Установите LANGSMITH_TRACING=true, и каждый model.invoke(), agent.stream() и вызов инструмента будет захвачен.

Но нам нужно было:

  1. Данные остаются в нашей MongoDB — промпты, ответы, бизнес-логика никогда не покидают нашу инфраструктуру
  2. Бизнес-контекст на запусках — ID тенанта, тип агента, ID сущности, привязанные к каждой трассировке
  3. Без пользовательского кода трассировщика — повторное использование управления деревом запусков SDK, наследования метаданных, извлечения токенов
  4. Работает с DeepAgents — подагенты, вызовы инструментов, полное дерево

Наивный подход — наследовать BaseTracer и реализовывать persistRun / _endTrace. Это не учитывает распространение dotted_order, наследование метаданных, извлечение токенов и отслеживание времени до первого токена. Не делайте так.


Решение: LangSmithTracingClientInterface

SDK langsmith определяет интерфейс из 2 методов для персистентности:

interface LangSmithTracingClientInterface {
  createRun(run: RunCreate): Promise<void>;
  updateRun(runId: string, run: RunUpdate): Promise<void>;
}

LangChainTracer из @langchain/core берёт на себя всё — управление деревом запусков, распространение dotted_order, trace_id, наследование метаданных/тегов, извлечение токенов из выводов LLM — и делегирует хранение этому интерфейсу.

Реализуя его для MongoDB, вы получаете полный SDK бесплатно.

graph TD
  A["Ваш код агента"] -->|"callbacks: [tracer]"| B["LangChainTracer"]
  B -->|"управление деревом запусков"| C["RunTree"]
  C -->|"плоские DTO"| D["MongoDBLangSmithClient"]
  D -->|"createRun(RunCreate)"| E["MongoDB"]
  D -->|"updateRun(RunUpdate)"| E
  B -->|"наследование метаданных"| F["Все дочерние запуски получают traceId, tags"]
  B -->|"извлечение токенов"| G["usage_metadata на запусках LLM"]

Шаг 1: MongoDB-клиент

Реализуйте LangSmithTracingClientInterface. Метод createRun получает плоский DTO RunCreate — без циклических ссылок, без дерева child_runs, без sanitizeForBson. Просто вставьте его.

import type { LangSmithTracingClientInterface } from 'langsmith';
import type { RunCreate, RunUpdate } from 'langsmith/schemas';

class MongoDBLangSmithClient implements LangSmithTracingClientInterface {
  constructor(
    private readonly storage: ObservabilityStorage,
    private readonly businessContext?: { tenantId: ObjectId; entity: ObjectId },
  ) {}

  async createRun(run: RunCreate): Promise<void> {
    const { child_runs, ...doc } = run;
    await this.storage.insertLangChainRun({
      ...doc,
      businessContext: this.businessContext,
    });
  }

  async updateRun(runId: string, run: RunUpdate): Promise<void> {
    await this.storage.updateLangChainRun(runId, run);
  }
}

businessContext — это необязательные метаданные, которые вы прикрепляете к каждому запуску — тенант, тип агента, всё что нужно вашему приложению. Это не часть модели LangChain; это ваше.


Шаг 2: Фабричная функция

Создайте фабрику, которая возвращает стандартный LangChainTracer с вашим MongoDB-клиентом:

import { LangChainTracer } from '@langchain/core/tracers/tracer_langchain';

function createMongoDBTracer(
  traceId: string,
  storage: ObservabilityStorage,
  businessContext?: { tenantId: ObjectId; entity: ObjectId },
  metadata?: Record<string, unknown>,
): LangChainTracer {
  const client = new MongoDBLangSmithClient(storage, businessContext);
  return new LangChainTracer({
    client,
    projectName: 'default',
    metadata: { traceId, ...metadata },
  });
}

Используйте его в любом месте, где вы вызываете runnable LangChain:

const traceId = uuid7(); // UUID в порядке времени из SDK langsmith
const tracer = createMongoDBTracer(traceId, storage, {
  tenantId, entityId, agentType: 'sales-assistant',
});

await agent.stream(
  { messages: [{ role: 'user', content: message }] },
  { callbacks: [tracer] },
);

Всё. Каждый запуск в дереве — корневой агент, подагенты, вызовы LLM, вызовы инструментов — отслеживается с правильным dotted_order, trace_id, наследованием метаданных и подсчётом токенов.


Шаг 3: Схема хранения

Наследуйте RunCreate из langsmith/schemas напрямую. Не переобявляйте его поля:

import type { RunCreate } from 'langsmith/schemas';

interface LangChainRunDoc extends RunCreate {
  _id: ObjectId;
  businessContext?: {
    tenantId: ObjectId;
    entityId: ObjectId;
    agentType?: string;
  };
  createdAt: Date;
}

RunCreate уже определяет id, name, start_time, run_type, inputs, outputs, extra, tags, events, trace_id, dotted_order, parent_run_id. Вы добавляете только свои бизнес-поля.


Шаг 4: Что вы получаете автоматически

Управление деревом запусков

LangChainTracer использует RunTree внутри. Когда запуск начинается, он вызывает createRun с плоским DTO. Когда заканчивается — вызывает updateRun с выводами, ошибкой, end_time и дополнительными метаданными. Вы не управляете деревом — это делает SDK.

Наследование метаданных

Передайте метаданные трассировщику, и они распространятся на все дочерние запуски:

const tracer = createMongoDBTracer(traceId, storage, ctx, {
  thread_id: conversationId,  // для запросов по потокам
  agentType: 'pricing',
});

Каждый дочерний запуск (подагенты, инструменты, вызовы LLM) получает thread_id в extra.metadata. Запрашивайте по потоку в MongoDB:

db.langchain_runs.find({ 'extra.metadata.thread_id': conversationId })

Извлечение токенов

LangChainTracer.onLLMEnd извлекает usage_metadata из выводов.chat-моделей и хранит его в run.extra.metadata.usage_metadata. Пользовательское извлечение не требуется.

Время до первого токена

Для стриминговых запусков LLM LangChainTracer отслеживает first_token_time через RunTree. Ваш вызов updateRun получает это в RunUpdate.


Шаг 5: Запросы

Корневой запуск (где parent_run_id отсутствует) И ЕСТЬ сессия трассировки. Запрашивайте его напрямую:

// Получить все запуски для трассировки
async findRunsByTraceId(traceId: string) {
  return db.langchain_runs.find({ traceId }).sort({ dotted_order: 1 });
}

// Получить корневой запуск (сессию)
async findRootRun(traceId: string) {
  return db.langchain_runs.findOne({
    traceId,
    parent_run_id: { $exists: false },
  });
}

// Получить запуски по ID сообщения (из extra.metadata)
async findRunsByMessageId(messageId: string) {
  return db.langchain_runs.find({
    'extra.metadata.message_id': messageId,
  });
}

Без джойнов, без отдельной коллекции сессий. Одна коллекция, один запрос.


Шаг 6: Что в данных

Каждый документ в langchain_runs выглядит так:

{
  "_id": "...",
  "id": "run-uuid-7",
  "name": "ChatOpenAI",
  "run_type": "llm",
  "trace_id": "root-run-uuid",
  "parent_run_id": "parent-uuid",
  "dotted_order": "20260722T150322...1234.20260722T150322...5678",
  "start_time": 1753216202000,
  "end_time": 1753216203500,
  "inputs": { "messages": [...] },
  "outputs": { "generations": [...] },
  "extra": {
    "metadata": {
      "ls_provider": "openai",
      "ls_model_name": "gpt-4o",
      "thread_id": "conversation-123",
      "usage_metadata": {
        "input_tokens": 1250,
        "output_tokens": 340,
        "total_tokens": 1590
      }
    }
  },
  "tags": ["sales-assistant", "pricing"],
  "businessContext": {
    "tenantId": "...",
    "entityId": "...",
    "agentType": "pricing"
  },
  "createdAt": "2026-07-22T15:03:22.000Z"
}

Конфигурация LLM (провайдер, модель) берётся из extra.metadata.ls_provider и extra.metadata.ls_model_name — снапшотить не нужно.


Интеграция с DeepAgents

DeepAgents построен на LangGraph, который использует runnables LangChain. Тот же паттерн callbacks: [tracer] отслеживает полное дерево deep-агента:

graph TD
  A["DeepAgent"] -->|"callbacks"| B["LangChainTracer"]
  B --> C["Корневой запуск: agent"]
  C --> D["Подагент: pricing"]
  C --> E["Подагент: scheduling"]
  D --> F["Вызов LLM: ChatOpenAI"]
  D --> G["Инструмент: calculatePrice"]
  E --> H["Вызов LLM: ChatOpenAI"]
  E --> I["Инструмент: checkAvailability"]
  F -->|"parent_run_id = D"| J["langchain_runs"]
  G -->|"parent_run_id = D"| J
  H -->|"parent_run_id = E"| J
  I -->|"parent_run_id = E"| J
  C -->|"parent_run_id отсутствует"| J
  J -->|"dotted_order сортирует дерево"| K["Отрисовка водопада"]

Каждый запуск имеет parent_run_id, связывающий его с родителем, и dotted_order для иерархической сортировки. Полное дерево восстанавливается одним запросом по traceId.


Индексы MongoDB

db.langchain_runs.createIndex({ runId: 1 }, { unique: true });
db.langchain_runs.createIndex({ traceId: 1, dotted_order: 1 });
db.langchain_runs.createIndex({ parent_run_id: 1 });
db.langchain_runs.createIndex({ 'extra.metadata.message_id': 1 }, { sparse: true });

Индекс traceId + dotted_order покрывает самый частый запрос: получить все запуски для трассировки в порядке выполнения.


Что мы удалили

После перехода на этот подход мы удалили:

Что Почему
Пользовательские подклассы BaseTracer LangChainTracer берёт на себя всё
Функция sanitizeForBson Плоские DTO не имеют циклических ссылок
Ручное распространение dotted_order / trace_id SDK делает это сам
Отдельная коллекция trace_sessions Корневой запуск И ЕСТЬ сессия
Поля снапшотов конфигурации LLM Выводятся из extra.metadata при запросе
13 методов CRUD для сессий Заменены запросами к корневому запуску

Типичные ошибки

Не наследуйте BaseTracer напрямую. Он абстрактный и не учитывает управление деревом запусков. Используйте LangChainTracer с пользовательским клиентом.

Не храните объекты Runchild_runs). Они имеют циклические ссылки. LangChainTracer уже преобразует их в плоские DTO RunCreate / RunUpdate.

Не переобявляйте поля BaseRun в пользовательских типах. Наследуйте RunCreate из langsmith/schemas напрямую.

Не снапшотьте конфигурацию LLM при каждом запуске. Провайдер и модель находятся в extra.metadata.ls_provider и extra.metadata.ls_model_name. Выводите при запросе.

Не забывайте thread_id во всех запусках. Передавайте его в RunnableConfig.metadata. LangChainTracer распространяет его на дочерние запуски. Без него запросы по потокам не работают.


Дополнительные материалы


Часто задаваемые вопросы

Почему не использовать облако LangSmith для трассировки? Суверенитет данных. Ваши промпты LLM, ответы и бизнес-логика покидают вашу инфраструктуру. Для регулируемых отраслей или конкурентной чувствительности хранение данных трассировки в собственной базе данных является обязательным. Вы также избегаете построчной оплаты при масштабировании.

Что такое LangSmithTracingClientInterface? Интерфейс из 2 методов от SDK langsmith: createRun и updateRun. LangChainTracer делегирует всю персистентность этому интерфейсу. Реализуя его для MongoDB, вы получаете полное управление деревом запусков SDK, наследование метаданных и извлечение токенов — при этом храня данные локально.

Почему расширять RunCreate вместо создания пользовательских типов? RunCreate из langsmith/schemas — это официальный DTO персистентности LangChain. Это то, что LangChainTracer отправляет клиенту. Расширяя его напрямую, вы устраняете переобявление полей, избегаете расхождений при обновлениях SDK и получаете типобезопасную персистентность бесплатно.

Работает ли это с подагентами DeepAgents? Да. DeepAgents построен на LangGraph, который использует runnables LangChain. Тот же LangChainTracer с callbacks отслеживает полное дерево: корневой агент, подагенты, вызовы инструментов, вызовы LLM. Запуски каждого подагента связаны через parent_run_id и dotted_order.

Как группировать по потокам без LangSmith? Передайте thread_id в метаданных RunnableConfig. LangChainTracer распространяет его на все дочерние запуски. Запрашивайте по thread_id в коллекции MongoDB. Это идентично работе потоков LangSmith — вы просто владеете хранилищем.


Теги: langchain langsmith deep-agents наблюдаемость mongodb llm-tracing ai-agents typescript langsmith-tracing-client runcreate langchain-tracer

Было полезно? Поделитесь.

Похожие статьи

OpenWiki vs QuotyAI: два проекта на LangChain DeepAgents, две архитектуры

OpenWiki использует LangChain DeepAgents для генерации документации к кодовым базам. QuotyAI использует тот же API createDeepAgent для генерации исполняемой бизнес-логики на TypeScript. Техническое сравнение архитектуры агентов, паттернов субагентов и исполнения кода.

Читать статьюarrow_forward

Spec-Driven vs Code-First vs Chat-to-Code: Три философии обучения ИИ вашего бизнесу

Три подхода к построению знаний ИИ-агентов — разработка на основе спецификаций, документация на основе кода и генерация кода через чат. Сравнение Spec Kit, OpenWiki, Mintlify и QuotyAI с реальными подводными камнями, продуктами и сигналами конвергенции.

Читать статьюarrow_forward

Codex for Sales: Как Запускать Сгенерированный ИИ Бизнес-Код в Продакшене

Сгенерированные ИИ код для ценообразования и планирования требует управления жизненным циклом как в git: стейджинг, валидация, утверждение, откат. Реальные паттерны из продакшен-системы с мульти-агентной архитектурой.

Читать статьюarrow_forward
Спасибо за чтение!
Читать другие статьи