Полная наблюдаемость агентов в собственной базе данных с LangChain, LangSmith SDK и DeepAgents
Как отслеживать каждый вызов LLM, вызов инструмента и подагента в LangChain DeepAgents — и хранить всё в собственной MongoDB вместо облака LangSmith.
Каждый вызов агента LangChain создаёт дерево запусков: корневой агент, подагенты, вызовы инструментов, вызовы LLM. LangSmith отслеживает это автоматически. Но что если данные трассировки вам нужны в собственной базе данных?
Мы построили продакшен-систему, которая отслеживает каждый вызов LLM в LangChain DeepAgents и хранит его в MongoDB — используя только официальные типы SDK, без пользовательских подклассов BaseTracer, без переобявления полей. Вот как.
Проблема
Облако LangSmith даёт трассировку из коробки. Установите LANGSMITH_TRACING=true, и каждый model.invoke(), agent.stream() и вызов инструмента будет захвачен.
Но нам нужно было:
- Данные остаются в нашей MongoDB — промпты, ответы, бизнес-логика никогда не покидают нашу инфраструктуру
- Бизнес-контекст на запусках — ID тенанта, тип агента, ID сущности, привязанные к каждой трассировке
- Без пользовательского кода трассировщика — повторное использование управления деревом запусков SDK, наследования метаданных, извлечения токенов
- Работает с 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 с пользовательским клиентом.
Не храните объекты Run (с child_runs). Они имеют циклические ссылки. LangChainTracer уже преобразует их в плоские DTO RunCreate / RunUpdate.
Не переобявляйте поля BaseRun в пользовательских типах. Наследуйте RunCreate из langsmith/schemas напрямую.
Не снапшотьте конфигурацию LLM при каждом запуске. Провайдер и модель находятся в extra.metadata.ls_provider и extra.metadata.ls_model_name. Выводите при запросе.
Не забывайте thread_id во всех запусках. Передавайте его в RunnableConfig.metadata. LangChainTracer распространяет его на дочерние запуски. Без него запросы по потокам не работают.
Дополнительные материалы
- Исполняемые навыки для агентов лучше, чем документация — почему агентам нужен исполняемый код, а не документация
- OpenWiki vs QuotyAI — два проекта DeepAgents, две архитектуры
- Детерминизм как инфраструктура — почему детерминированное выполнение важно для продакшен AI
- Codex для продаж — запуск AI-генерируемой бизнес-логики в продакшене
Часто задаваемые вопросы
Почему не использовать облако 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. Техническое сравнение архитектуры агентов, паттернов субагентов и исполнения кода.
Читать статьюSpec-Driven vs Code-First vs Chat-to-Code: Три философии обучения ИИ вашего бизнесу
Три подхода к построению знаний ИИ-агентов — разработка на основе спецификаций, документация на основе кода и генерация кода через чат. Сравнение Spec Kit, OpenWiki, Mintlify и QuotyAI с реальными подводными камнями, продуктами и сигналами конвергенции.
Читать статьюCodex for Sales: Как Запускать Сгенерированный ИИ Бизнес-Код в Продакшене
Сгенерированные ИИ код для ценообразования и планирования требует управления жизненным циклом как в git: стейджинг, валидация, утверждение, откат. Реальные паттерны из продакшен-системы с мульти-агентной архитектурой.
Читать статью