Интеграция ИИ уровня предприятия: встраивание LLM в бизнес-процессы крупных компаний — redb.Route.Llm 3.1.1. .NET.. .NET. ai.. .NET. ai. Apache Camel.. .NET. ai. Apache Camel. EIP.. .NET. ai. Apache Camel. EIP. esb.. .NET. ai. Apache Camel. EIP. esb. llm.. .NET. ai. Apache Camel. EIP. esb. llm. Open source.. .NET. ai. Apache Camel. EIP. esb. llm. Open source. redb.Route.. .NET. ai. Apache Camel. EIP. esb. llm. Open source. redb.Route. tool use.. .NET. ai. Apache Camel. EIP. esb. llm. Open source. redb.Route. tool use. агенты.. .NET. ai. Apache Camel. EIP. esb. llm. Open source. redb.Route. tool use. агенты. искусственный интеллект.. .NET. ai. Apache Camel. EIP. esb. llm. Open source. redb.Route. tool use. агенты. искусственный интеллект. Системное программирование.
redb.route llm

redb.route llm

Серия: redb ecosystem (часть 2 к LLM-анонсу)

В предыдущей статье я анонсировал redb.Route.Llm как 24-й транспорт redb.Route — мы делали LLM ещё одним endpoint’ом наравне с Kafka, RabbitMQ и HTTP, чтобы выкинуть отдельную «AI-инфраструктуру», стоящую рядом с интеграционной. Заодно я повесил в конец статьи «честный skip-list» — список того, что в 3.1.0 ещё не доделано: streaming, ToolCacheStore, KnowledgeStore, BatchStore, EvalRunStore, sliding-window память, sandbox-инструменты.

Из этого skip-list’а делано больше, чем я планировал. Но не это главное. Главное — что в процессе доделывания обнаружилась настоящая ценность всей затеи: LLM-транспорт оказался не очередным чат-фреймворком, а недостающим звеном в ESB, после которого «бизнес-агент в проде» перестаёт быть отдельным проектом. Эта статья — про то, как чат-демо превращается в enterprise-агентскую платформу, не переписываясь и не превращаясь в «AI-монолит сбоку».

Всё, что ниже — реальный код из репозитория, не псевдокод. Ссылки на демо-маршруты в конце.

Это вторая часть серии про redb.Route.Llm. Первая часть — здесь:

Контекст серии redb.Route целиком:

Маленькая ремарка про EIP. В этой статье я многократно опираюсь на Enterprise Integration Patterns — MulticastAggregatorScatter-GatherWire-TapChoiceAggregate-by-window — не разбирая каждый из них подробно. Это сознательный выбор: статей серии «EIP в .NET через redb.Route» с детальным разбором каждого паттерна (с диаграммами, кодом и сравнениями) ещё не вышло, я их пишу параллельно. Здесь я немного забегаю вперёд.

Если вы работали с Apache Camel или WSO2 Micro Integrator — узнаете паттерны на ходу, имена и семантика идентичны. Я сам много лет писал и поддерживал интеграционные проекты на WSO2 MI (и ESB-предке), архитектурно redb.Route ближе всё-таки к Camel — компилируемая DSL поверх типизированного Exchange, а не XML-конфиг. Сопоставления «Camel ↔ redb.Route» и «WSO2 MI ↔ redb.Route» — это отдельные большие статьи, тоже в работе. Если что-то по EIP-части в коде ниже окажется непонятным, спрашивайте в комментариях — отвечу сразу, заодно соберу материал для тех самых будущих статей.


Чем это всё отличается от «ещё одного агентского фреймворка»

В индустрии много инструментов, которые делают LLM-tool-use из коробки: LangChain, langchain4j, Semantic Kernel, AutoGen, LlamaIndex и десятки нишевых. Все они решают одну проблему: «как у меня модель будет вызывать функции и помнить разговор». И все они оставляют решать вам остальные восемнадцать: retry, idempotency, audit, multi-tenant, бюджет, observability, governance, approval, batch, scheduling.

Идея redb.Route.Llm — сделать LLM транспортом в ESB, у которого эти восемнадцать уже решены на уровне фреймворка. Не «у нас есть библиотека интеграций, плюс отдельная LLM-библиотека», а ровно та же DSL, ровно тот же runtime, ровно те же hook’и для governance. Если у вас в redb.Route уже есть policy-перехватчик, который пишет в Kafka каждое действие — он автоматически пишет туда и каждый tool-вызов агента, потому что это всё один и тот же Exchange.

Это не теоретическая красота. Это снимает главную боль продакшена с агентами: «а что произойдёт, когда LLM попросит удалить production-таблицу?» В конструкции «LangChain + хуки» ответ — «подключите наши approval-callbacks и не забудьте». В конструкции «LLM как транспорт в ESB» ответ — «перехватите Exchange до .To("exec://...") тем же .Process(...), которым вы перехватываете любой потенциально опасный шаг в любом другом маршруте». Не «новый паттерн для AI», а тот же паттерн, что для FTP и для Kafka.


Что доделано из «честного skip-list» 3.1.0

Я обещал — я говорю.

Skip-list пункт

Статус 3.1.1

Streaming end-to-end (HTTP SSE + WS per-frame)

Готово. IAsyncEnumerable<string> в Out.Body → SSE-кадр на HTTP-side, отдельный WS-message на WS-side.

ToolCacheStore (REDB)

Готово. ToolCacheProps, политика ToolCachingPolicy.Memoize на уровне DSL.

KnowledgeStore — RAG-чанки в REDB

Готово (частично). KnowledgeChunkProps + индексирование, embeddings будут отдельным релизом.

BatchStore + LlmCallbackProcessor

Готово. Anthropic Message Batches и OpenAI Batch — асинхронный webhook-консьюмер, идемпотентная обработка, retry.

EvalRunStore

Готово. Прогоны eval’ов сохраняются как первоклассные сущности с trace-id’ами.

PromptTemplateStore

Готово. Версионированный реестр промптов, ссылка #name в DSL.

Sliding-window память

Не сделано. Сделано иначе: tree-branching конверсаций как REDB-tree (см. ниже).

Sandbox-инструменты

Не сделано в виде «контейнер на каждый вызов». Сделан redb.Route.Exec с allowlist + working-dir + timeout + cap-by-bytes — практичный enterprise-минимум.

Бонусом, чего в skip-list не было:

  • ?redb=<name> per-exchange — мульти-tenant: одна и та же конфигурация маршрута, REDB-instance выбирается из заголовка/URI на лету;

  • DSL/tool split — пакеты redb.Route.Llm.Abstractions (контракты) и redb.Route.Llm.Tools (готовые утилитарные инструменты), чтобы 22 коннектора могли регистрировать .AsLlmTool() без bump’а минорки на каждый чих;

  • 6 утилитарных инструментов из коробкиHttpFetchJsonPathXPathRegexExtractMathEvalTavilyWebSearch — каждый и как DSL-расширение, и как standalone tool-route;

  • Bug fixes, которые нашлись только в проде: orphan-tool_use (когда модель просит инструмент, а Anthropic-провайдер обрывает на ошибке) и OEM-codepage в Process.StandardOutput (windows-1251 в выхлопе cmd /c ломает UTF-8 контракт инструмента).

11 REDB-схем теперь стоят за всем этим: ConversationPropsMessagePropsApprovalPropsCostBudgetPropsToolCachePropsToolAuditPropsKnowledgeChunkPropsPromptTemplatePropsEvalRunPropsLlmBatchPropsToolIdempotencyProps. Это не «база данных под чат-историю». Это операционный слой агентской платформы, который вы получаете за AddRedbLlmStorage() в одну строку.


Десять enterprise-паттернов, которые становятся одной строчкой DSL

Дальше — паттерны. Не «Hello, world», а то, ради чего вообще существует ESB-подход к LLM.

1. Жёсткий бюджет на разговор как circuit-breaker

В пет-проекте «бюджет токенов» — это лог. В проде это техника защиты от инцидентов: модель не должна иметь права съесть 10к долларов из-за бага в промпте. CostBudgetProps — это не observability-сущность, это превентивное правило. На каждом провайдер-вызове агент-engine считает потраченное по conversation-id и прибавляет ожидаемое (по max_tokens). Если суммарно превысит лимит — LlmBudgetExceededException бросается до отправки запроса, а не после.

From("kafka://support-tickets")
    .To(Llm.Factory("claude")
        .Conversation(e => "ticket-" + e.In.Headers["TicketId"])
        .CostBudget(usd: 0.50)            // hard ceiling per conversation-id
        .CostBudgetExceeded(BudgetPolicy.FailFast)
        .AsUri())
    .To("kafka://support-replies");

Поверх этого паттерна сразу появляются производные: «бюджет на тенант», «бюджет на промпт-шаблон», «бюджет на модель». Все они — это просто разные ключи в CostBudgetProps. Конструкция остаётся одна.

2. Approval-gate с человеком в цикле

Самый недооценённый enterprise-паттерн в LLM. Если модель имеет право удалять записи или отправлять платежи — между tool-вызовом и реальным выполнением должен стоять человек, и эта пауза должна быть нативной частью runtime, а не «костылём из webhook’ов».

From("direct:tool-payments-refund")
    .AsLlmTool("issue_refund")
        .Description("Refund a payment by id and reason.")
        .Input(/* JSON Schema */)
        .SideEffect(ToolSideEffect.Mutating)         // hook governance reads this
        .Cost(ToolCostClass.Expensive)
    .Then()
    .Process<ApprovalGate>()                          // suspend exchange, write ApprovalProps,
                                                       // notify Slack, wait for HTTP callback
    .To("kafka://payments.refund.commands");

ApprovalProps хранит exchange-id, conversation-id, входные аргументы, человека-аппрувера, время ожидания, исход. Slack-bot/email/web-form подключаются как обычный HTTP-маршрут в том же redb.Route. Когда аппрувер кликает «approve» — webhook будит exchange и доставляет результат в агент-engine. Если timeout — agent получает tool_result со status:"timeout" и сам решает, что делать.

Главное здесь — это не AI-логика. Это паттерн EIP «Aggregator + Correlation Identifier + Wire-Tap + JMS Reply-To», который применим и к refund-engine’у без LLM, и к LLM-агенту, потому что они оба — Exchange’и в одном runtime.

3. Идемпотентный retry с ToolIdempotencyProps

Webhook-консьюмеры повторяются. Сетевые таймауты — повторяются. Anthropic Batch иногда доставляет дважды. Если ваш tool-вызов «issue refund $50» обрабатывается два раза — это плохая среда.

ToolIdempotencyProps хранит idempotency-key → tool-result с TTL. В DSL:

From("direct:tool-issue-invoice")
    .AsLlmTool("issue_invoice")
        .Caching(ToolCachingPolicy.Idempotent)
    .Then()
    .Process(BuildIdempotencyKey)                    // sha256(args + customer-id + day)
    .To("...");

Когда агент-engine видит Caching = Idempotent — он до запуска маршрута проверяет ToolIdempotencyProps по ключу. Hit — отдаёт сохранённый tool_result, маршрут не запускается. Miss — запускается, результат записывается. На уровне фреймворка, а не на уровне «не забудьте».

4. Multi-tenant через ?redb=

Один воркер обслуживает 50-100 клиентов. Каждый клиент — это свой REDB-instance (свои conversations, свой cost-budget, свои approvals, своя knowledge-base). Раньше это значило бы «50 -100 ServiceProvider’ов или ScopedFactory с маршрутом на каждый». В 3.1.1 это per-exchange hint:

From("http:0.0.0.0:5088/api/llm/ask?inOut=true")
    .Process(e =>
        e.In.Headers[LlmHeaders.Redb] = e.In.Headers["X-Tenant"]?.ToString())
    .To("llm://claude?conversationFromHeader=true");

?redb=acme или заголовок — engine выбирает named REDB-instance из реестра, на текущем Exchange, без смены маршрута, без фабрики, без перезапуска. Conversations, ToolAudit, ApprovalProps — всё пишется в нужный tenant. Каждый tenant получает свой биллинг и свой governance, не зная про существование других.

5. Audit trail без отдельной интеграции

ToolAuditProps — это REDB-объект с tenant-id, conversation-id, exchange-id, tool-name, входными аргументами (или хэшом, если PII), выходом (или хэшом), длительностью, статусом. Каждый вызов инструмента пишется автоматически, потому что инструмент это всё равно маршрут, а у redb.Route есть стандартные post-processors.

Запрос вида «покажи мне все tool-вызовы Claude в tenant acme за последние 7 дней с side-effect=mutating и cost=expensive, упорядочены по времени» — это не отдельный аналитический пайплайн. Это value_string индекс на REDB-объекте + один SQL (см. предыдущую серию «REDB storage», ссылки в конце).

6. Async batch + callback-consumer

Anthropic Message Batches и OpenAI Batch — это до 50% дешевле, но цена за это — задержка до 24 часов. Для офлайн-обработок (классификация миллиона тикетов, экстракция полей из миллиона PDF) это идеальный режим.

LlmBatchProps хранит batch-id, statuses, мета-связь с исходной коллекцией Exchange’ов. LlmCallbackProcessor — это обычный From("http://...?inOut=true")-маршрут, который провайдер вызывает по готовности batch’а. Маршрут читает batch-id, забирает результаты, диспетчит каждый ответ обратно в нужный Exchange (через correlation-id) — и они продолжают идти по своему пути, как будто синхронный вызов закончился.

Поверх — идемпотентность (тот же ToolIdempotencyProps), retry (стандартный circuit-breaker redb.Route), backpressure. Не нужно писать свой батчер. Не нужно писать свой webhook-handler. Не нужно отдельно тестировать «а что если callback пришёл два раза».

7. Версионированный реестр промптов: #-refs

Один из самых тоскливых багов прода: «модель стала отвечать иначе». Идёшь смотреть git-blame, оказывается — кто-то 3 недели назад тронул системный промпт, и тесты прошли. Промпт — это код, и должен жить в реестре с версией.

// Where you register prompts:
promptRegistry.Register("triage-system", version: "v3", body: """
    You are a support triage agent. Classify into [billing, tech, sales, abuse]...
    """);

// In the route:
.To(Llm.Factory("claude").SystemPromptRef("#triage-system@v3").AsUri())

PromptTemplateProps — это REDB-объект с именем, версией, телом, метаданными (автор, дата, эксперимент). Engine при разрешении #name фиксирует именно эту версию в MessageProps для каждого вызова. Через полгода вы можете полностью точно сказать: «этот разговор шёл на версии v3 промпта triage-system», а не «вероятно, v3, мы тогда так делали».

И — самое полезное — EvalRunStore фиксирует прогоны eval-test’ов в привязке к версии промпта. «Версия v4 даёт +12% accuracy на golden-set’е» — это не excel-табличка, это запрос в REDB.

8. Tree-branching конверсаций для A/B и «что было бы, если»

ConversationProps хранится как REDB-tree через нативный parent_id. Это значит, что разговор — не плоский список сообщений, а дерево. Вы можете:

  • ветвить разговор от любого сообщения, чтобы запустить «альтернативный» прогон с другой моделью или температурой;

  • хранить пользовательскую ветку и эксперимент-ветку параллельно;

  • считать метрики по парам веток («с-инструментами vs без-инструментов на одном и том же контексте»).

В sliding-window памяти вы отрезаете прошлое. В tree-branching памяти вы пишете альтернативное прошлое и сравниваете. Для production-агентов второе ценнее на порядок, потому что улучшение промптов в проде — это и есть «возьми реальный разговор, проиграй с новым промптом, сравни оценки».

И технически здесь ничего не было сделано «специально для LLM». Tree через parent_id — это штатная возможность REDB, которая используется для иерархий объектов с самого начала. LLM просто написала на этом свою сущность.

9. Jury из младших моделей с arbiter’ом — Scatter-Gather + Aggregator

Один из самых недооценённых production-приёмов: не верить одной модели. Одна и та же задача отправляется одновременно в несколько дешёвых моделей (Haiku, GPT-4o-mini, Mistral-Small, Gemini-Flash, Llama-3.1-70b через Groq), их ответы собираются, и старшая модель (Sonnet, Opus, GPT-4o) получает оригинальный запрос плюс пять кандидатов и работает как арбитр: выбирает лучший, синтезирует новый или говорит «никто не справился, переспроси». В литературе это называется mixture-of-agents или ensemble voting — паттерн, который даёт +10..20% accuracy на сложных задачах при цене ниже, чем «всё через Opus».

В чистой LangChain-конструкции это превращается в стостраничный orchestrator с try/catch, retry на каждую модель, таймаутами на каждый вызов и собственной aggregation-логикой. В ESB-конструкции это штатный EIP-паттерн Scatter-Gather + Aggregator, у которого 25-летняя история в интеграционных шинах. LLM просто становится одним из endpoint’ов, по которому Scatter-Gather разлетается:

From("kafka://contract-clauses-to-classify")
    .RouteId("contract-jury")
    .Multicast()                                          // Scatter
        .Parallel()
        .StopOnException(false)                           // одна модель упала — терпим
        .Timeout(TimeSpan.FromSeconds(30))                // на каждую ветку
        .To(Llm.Factory("haiku")
            .SystemPromptRef("#contract-classify@v3")
            .Temperature(0.0).MaxTokens(200).AsUri())
        .To(Llm.Factory("gpt-4o-mini")
            .SystemPromptRef("#contract-classify@v3")
            .Temperature(0.0).MaxTokens(200).AsUri())
        .To(Llm.Factory("groq-llama-70b")
            .SystemPromptRef("#contract-classify@v3")
            .Temperature(0.0).MaxTokens(200).AsUri())
        .To(Llm.Factory("mistral-small")
            .SystemPromptRef("#contract-classify@v3")
            .Temperature(0.0).MaxTokens(200).AsUri())
    .End()                                                // Gather: ответы собрались в Exchange.Properties["multicast.results"]
    .Process<JuryAggregator>()                            // склеиваем кандидатов в один промпт для арбитра
    .To(Llm.Factory("sonnet")                             // Arbiter
        .SystemPromptRef("#jury-arbiter@v2")
        .Temperature(0.1).MaxTokens(500)
        .CostBudget(usd: 0.05)
        .AsUri())
    .To("kafka://contract-clauses-classified");

JuryAggregator — это короткий процессор, который из четырёх ответов и оригинального запроса собирает один промпт вида «вот задача; вот ответы кандидатов A/B/C/D; верни итоговую классификацию или скажи unclear». Arbiter получает всё в одном вызове, отвечает структурированным JSON. Хедеры llm.tokens.in/out пишутся отдельно для каждой ветки, плюс отдельно для арбитра — стоимость прозрачна на уровне маршрута.

Что здесь делает ESB-форма ценным:

  • Параллелизм бесплатно. Multicast EIP уже умеет fan-out по N веткам, ждать всех или N из M, обрабатывать таймауты и частичные сбои. Не нужно писать Task.WhenAll с custom-обработкой ошибок.

  • Идемпотентность бесплатно. Если одна из веток упала и retry дал тот же запрос — ToolIdempotencyProps (или provider-side cache по prompt-hash) дедуплицирует. Не платим за дубль.

  • Бюджет бесплатно. .CostBudget(usd: 0.05) на arbiter’е стоит как отдельный circuit-breaker; на ветках можно поставить свой .CostBudget(usd: 0.01). Cap на всё суммарно — через CostBudgetProps с другим ключом. Если jury вышло за лимит — failfast, классификация откатывается на rule-based fallback (его же пишем как ещё одну ветку маршрута).

  • Audit бесплатно. Каждый из пяти LLM-вызовов автоматически попадает в ToolAuditProps, привязанный к одному exchange-id. Через месяц можно ответить «модель Х согласилась с арбитром в 73% случаев, можно её отключить и сэкономить».

  • Eval бесплатно. EvalRunProps записывает прогон с пятью моделями + одним арбитром — потом на golden-set’е можно подобрать оптимальный состав jury (заменить gpt-4o-mini на gemini-flash, проверить, упало ли accuracy).

В реальности этот паттерн даёт две ощутимые победы. Первая — точность: в задачах вида «классифицировать пункт договора как риск/не-риск», где одна модель регулярно ошибается, jury из четырёх дешёвых + арбитр-Sonnet уверенно обгоняет одиночный Opus, в 2.5 раза дешевле. Вторая — робастность: если Anthropic лежит, ветка через Anthropic упала, остальные четыре дали ответ, арбитр получил четыре кандидата вместо пяти и спокойно вынес вердикт. Деградация мягкая, а не «упало всё».

Тонкость, которую видишь только в проде: арбитру нельзя давать имена моделей. Если в промпте арбитра написано «вот ответ Claude Haiku, вот ответ GPT-4o-mini» — арбитр развивает фавориты (как и человек). Поэтому в JuryAggregator мы анонимизируем кандидаты как A/B/C/D, перемешиваем порядок (Latin square по exchange-id для воспроизводимости) и только после ответа арбитра мапим обратно «кто был A». Это даёт чистый сигнал для post-hoc анализа «какая модель чаще угадывает с арбитром».

10. Sub-agents: агент как инструмент другого агента

Прямое следствие архитектуры «инструмент = маршрут»: если маршрут может содержать .To("llm://..."), то инструмент — это сам по себе агент. Главный агент не «знает», что под капотом инструмента сидит ещё одна LLM. Для него research_topic — это обычный tool, как web_search или math_eval. Внутри же — полноценный second-tier агент со своими: моделью, промптом, набором инструментов, бюджетом, итерационным потолком, политиками retry, RAG-источниками.

В коде это выглядит так:

// Subagent: исследовательский специалист с собственными инструментами
From("direct:research-subagent")
    .AsLlmTool("research_topic")
        .Description("Глубокое исследование темы. Принимает {topic, depth}. " +
                     "Возвращает структурированный summary с источниками.")
        .Input("""{"type":"object","properties":{
                    "topic":{"type":"string"},
                    "depth":{"type":"string","enum":["short","deep"]}},
                  "required":["topic"]}""")
        .SideEffect(ToolSideEffect.ReadOnly)
        .Cost(ToolCostClass.Expensive)               // главный агент видит, что вызов недешёвый
    .Then()
    .Knowledge("research-corpus", k: 12)             // sub-agent имеет свою RAG-базу
    .To(Llm.Factory("sonnet")                        // средняя модель — research-специалист
        .SystemPromptRef("#research-specialist@v3")
        .Tools("tavily_web_search", "http_fetch", "regex_extract")
        .MaxIterations(8)
        .CostBudget(usd: 0.30)                       // sub-agent имеет свой бюджет
        .Temperature(0.1).AsUri())
    .Process<ExtractResearchSummary>();

// Главный агент использует sub-agent как обычный tool в своём наборе
From("kafka://complex-business-questions")
    .To(Llm.Factory("opus")                          // planner — старшая модель
        .SystemPromptRef("#senior-analyst@v1")
        .Tools("research_topic",                     // ← наш sub-agent
               "sql_query",                          // ← обычный data-tool
               "math_eval",                          // ← вычисления
               "draft_report")                       // ← ещё один sub-agent (черновик отчёта)
        .MaxIterations(15)
        .CostBudget(usd: 2.00)                       // budget верхнего уровня
        .AsUri())
    .To("kafka://business-answers");

Что здесь происходит технически: когда Opus решает позвать research_topic, агент-engine строит JSON-вход и через RouteToolBridge отправляет его на direct:research-subagent. Этот маршрут запускается как новый Exchange, наследуя transaction-scope, principal, headers и DI-scope от родительского. Внутри него Sonnet делает свой собственный tool-use loop (web search → fetch → extract), потенциально с десятью итерациями и тремя инструментами. Возвращает структурированный summary в Out.Body, который агент-engine упаковывает обратно как tool_result для Opus. Opus видит результат, продолжает свою работу, при необходимости зовёт ещё.

Иными словами: архитектура рекурсивна без единой строки спецкода. Sub-agent внутри sub-agent’а работает по тем же правилам. На третьем уровне всё ещё работают audit, budget, idempotency, prompt-versioning, RAG, governance — потому что они не привязаны к «уровню агента», они привязаны к Exchange’у, а Exchange один и тот же примитив на любой глубине.

Это даёт три производных паттерна, которые в плоской «один-агент-много-инструментов» архитектуре собирать неудобно:

(а) Hierarchical agents — planner и workers. Старшая модель (Opus, GPT-4o) играет роль планировщика: разбивает задачу на куски и распределяет по специалистам. Каждый специалист — sub-agent с узким промптом и узким набором инструментов. У planner’а собственного доступа к данным может вообще не быть — только через subagent’ов. Это даёт жёсткое разграничение полномочий: planner не может случайно дёрнуть delete_records, потому что у него такого инструмента нет; его есть только у data-cleanup-subagent, к которому planner обращается явно.

(б) Specialist subagents с собственной памятью. Sub-agent может иметь свой собственный conversationId (другая ветка REDB-tree), свой собственный набор Knowledge(...)-источников, свой собственный реестр промптов. Условный legal-review-subagent живёт внутри корпоративной legal-knowledge базы, видит только её, отвечает строго в правовом регистре, и старший агент никогда не получает доступ к этим документам напрямую. ACL соблюдается на уровне маршрута, не на уровне «надеемся, модель не процитирует».

(в) Cost-shaped escalation. Дешёвый агент (Haiku) пытается решить задачу первым. Если он возвращает unclear или confidence ниже порога, он сам вызывает escalate_to_senior как tool — и под этим инструментом сидит маршрут к Opus с полным контекстом. Большая часть запросов оседает на Haiku за копейки; в Opus уходят только сложные. На big-volume workloads это меняет экономику в разы.

Сравнение со static-jury из паттерна №9: jury — статически зашитый subagent pattern. Маршрут заранее знает, что должно быть N кандидатов и один арбитр; DAG зафиксирован в DSL. Sub-agents-as-tools — динамическая версия: главный агент сам решает, кого позвать и сколько раз. Jury лучше для предсказуемых конвейеров классификации/прогноза. Sub-agents — для open-ended исследовательских задач, где количество шагов и состав инструментов заранее неизвестны.

Подводный камень — циклы. Если sub-agent A может звать B, а B может звать A, теоретически возможен бесконечный спуск. На практике он гасится тремя ограничителями: MaxIterations на каждом уровне (sub-agent не может крутиться вечно), CostBudgetProps (родительский бюджет тратится на каждый вложенный вызов), и опциональный depth-limit в headers (LlmHeaders.SubAgentDepth инкрементируется на каждом вложении, маршрут отбрасывает запросы при превышении). На практике после depth = 3-4 реальные задачи уже не выигрывают.

И ещё одна архитектурная красота: subagent — это просто маршрут, а значит у него есть стандартный URI. Разные родительские агенты могут шарить одного research-subagent, у которого собственный ?redb=acme для tenant-isolation, собственный rate-limiter (?throttle=5/sec), собственный circuit-breaker. Sub-agent ведёт себя как внутренний микросервис LLM-уровня, который вы переиспользуете между маршрутами, не дублируя промпт и не плодя клиенты.


Streaming: что на самом деле меняется, когда токен покидает провайдера

В 3.1.0 streaming был у провайдеров, но не доходил до клиента — мы аккумулировали токены в строку и возвращали целиком. В 3.1.1 закрыли всю цепочку:

  • провайдер отдаёт IAsyncEnumerable<string> — кадры по мере прибытия;

  • агент-engine кладёт это в Out.Body как IAsyncEnumerable<string>не материализуя;

  • HTTP-консьюмер видит IAsyncEnumerable<string> и переключается в SSE-режим — каждый кадр отдельным data: ...nn;

  • WS-консьюмер — каждый кадр отдельным WebSocket message;

  • non-streaming consumer’ы (Kafka, RabbitMQ, ActiveMQ) — материализуют в строку, как раньше.

Что это даёт архитектурно: streaming перестаёт быть отдельным режимом «у нас есть streaming-API и обычный API». Это просто тип значения в Out.Body, который consumer-side обрабатывает в зависимости от своих возможностей. Тот же exchange может быть отправлен и в SSE, и в Kafka одновременно (multicast EIP) — Kafka-сторона дождётся материализации, SSE-сторона увидит кадры по мере поступления.

Под капотом — стандартный паттерн «Iterator вместо Collection в payload»: на pipe’ах он давно живёт. Единственное специфичное — это, что async-iteration должен работать внутри end-to-end Exchange-tracking’а, чтобы tracing/metrics видели весь путь, а не «получили один Exchange и отвалились».


RAG: чанки в REDB как первоклассные объекты

В 3.1.1 KnowledgeChunkProps — это REDB-объект с:

  • содержимым (text),

  • источником (source-uritenant-iddoc-idchunk-index),

  • метаданными (язык, дата, теги, ACL — кто имеет право видеть),

  • placeholder’ом для embedding (вектор поедет отдельным релизом, MVP — keyword search через value_string-индексы и FTS).

Это значит, что RAG-источник = маршрут, а не «отдельный векторный сервис». Маршрут From("file://docs?include=*.md").To("knowledge://acme") индексирует документы. Маршрут From("kafka://support-tickets").Knowledge("acme", k: 5) подмешивает топ-5 чанков в системный промпт перед .To("llm://claude"). ACL и tenant-фильтрация — это value_*-индексы, которые применяются в SQL до отправки чанков в промпт.

Когда подключим vector-store — он встанет рядом с keyword-search через тот же IKnowledgeStore интерфейс, без изменений в маршрутах. Это главная цель архитектуры: будущие фичи не ломают сегодняшние маршруты.


Три реальных enterprise-кейса: отчёты, прогнозы, оповещения

Паттерны выше — это атомарные кирпичики. Дальше — три полноценных сценария, в которых эти кирпичики складываются в реально работающий enterprise-маршрут. Без AI-хайпа, без «когнитивной автоматизации», без слов «трансформирует индустрию». Просто рутина, которую люди делают руками каждый день, и которая снимается агентом, потому что он живёт в той же шине, что и данные.

Кейс 1. Ежедневный финансовый отчёт CFO на email — со сравнением с прошлым кварталом

Каждый рабочий день в 7:00 финансовый аналитик собирает: выручку за вчера, расходы по статьям, отклонения от плана, top-5 крупнейших операций, остатки по счетам, курсы валют. Потом пишет короткую сопроводиловку — «выручка +3.2% к плану, расходы +1.7%, отклонения объясняются Х». Уходит на это час-полтора. Есть ли смысл это автоматизировать? Однозначно. Можно ли это сделать на отдельном AI-сервисе? Можно. Стоит ли? Нет.

Маршрут целиком:

// Каждое утро в 07:00 по локальному времени
From("cron://daily-cfo-report?schedule=0 0 7 ? * MON-FRI")
    .RouteId("daily-cfo-report")
    .Process<LoadYesterdayMetrics>()                       // тянет из 1С/SAP/банковского API
                                                            // → e.In.Body = {revenue, expenses, accounts, fx, top5}
    .Process<LoadQuarterContext>()                         // из того же ETL: план, прошлый квартал, MTD/QTD
    .ConvertBody<FinancialDailySnapshot>()                 // strongly-typed payload
    .Multicast().Parallel()
        .To(Llm.Factory("haiku")                           // ветка A: коротко по-русски, для CFO
            .SystemPromptRef("#cfo-daily-summary-ru@v7")
            .Temperature(0.1).MaxTokens(800)
            .CostBudget(usd: 0.02).AsUri())
        .To(Llm.Factory("haiku")                           // ветка B: english, для board chat
            .SystemPromptRef("#cfo-daily-summary-en@v7")
            .Temperature(0.1).MaxTokens(800)
            .CostBudget(usd: 0.02).AsUri())
    .End()
    .Process<RenderHtmlReport>()                           // mustache-шаблон: цифры таблицей,
                                                            // текст моделей вверху как summary
    .To("smtp://mail.acme.com?to=cfo@acme.com,board@acme.com" +
        "&subject=Daily%20FY%20snapshot%20${date:yyyy-MM-dd}")
    .To("teams://board-channel?card=adaptive")             // та же html-секция → Teams adaptive card
    .Wiretap("kafka://reports.cfo-daily.archive");         // копия в архив для аудита и обучения

Семь шагов, и в этих семи шагах закрыто всё:

  • Расписание — cron-выражение в URI, без отдельного шедулера; redb.Route уже умеет.

  • Сбор данных — LoadYesterdayMetrics это обычный процессор, который дёргает существующие интеграции через тот же redb.Route (Kafka/REST/JDBC), и любые retry/circuit-breaker’ы у этих интеграций уже стоят.

  • Двуязычная генерация — multicast в две ветки одного дешёвого Haiku с разными промпт-шаблонами. Стоит копейки, бюджет фиксирован.

  • Версионированный промпт — #cfo-daily-summary-ru@v7. Через полгода CFO скажет «отчёт стал хуже» — открываем EvalRunProps, видим, что v7 регрессирует на эталоне → откатываемся на v6 без redeploy.

  • Канал доставки — два transport’а (SMTP + Teams), оба штатные redb.Route-коннекторы. Завтра CFO попросит «ещё в Slack» — добавляется одна строка .To("slack://...").

  • Аудит — Wiretap копирует в Kafka-архив. Регулятор через год спрашивает «покажи отчёт за 2026-04-15» — вынимается из архива с метаданными «модель X, версия промпта Y, исходные данные Z» (потому что MessageProps хранит и input, и output).

  • Бюджет — .CostBudget(usd: 0.02) на ветке. 250 рабочих дней × 2 ветки × $0.02 = $10/год на отчёты. Финансовая редактура за час времени аналитика — $50. Окупается за день первого месяца.

Что это делает enterprise-кейсом, а не игрушкой: в момент, когда CFO жалуется на «странную цифру в отчёте», аудитор по trace-id за 30 секунд показывает: какой snapshot был на входе, какая версия промпта применилась, какой именно output пришёл от модели, какой файл ушёл на email, кто прочитал в Teams. Не «давайте я завтра разберусь и пришлю». А прямо сейчас, потому что всё это — REDB-объекты, привязанные к одному exchange-id.

Кейс 2. Прогноз cash-flow на 30 дней с jury из моделей и арбитром

Финансовый прогноз — это типичная задача, где одна модель вреднее, чем ноль моделей: ошибка модели уверенным голосом приводит к решению, которое стоит дороже, чем месяц работы аналитика. Поэтому здесь — паттерн №9 (jury + arbiter) во весь рост.

From("cron://weekly-cashflow-forecast?schedule=0 0 9 ? * MON")
    .RouteId("weekly-cashflow-forecast")
    .Process<BuildCashflowFeatures>()                      // банковские счета, дебиторка/кредиторка,
                                                            // плановые платежи, сезонность, FX-позиции
    .Knowledge("acme-finance", k: 8)                       // RAG: внутренние гайдлайны прогнозирования,
                                                            // прошлые отчёты, методология
    .Multicast().Parallel().Timeout(TimeSpan.FromMinutes(2))
        .To(Llm.Factory("sonnet")                          // 4 разные модели, чтобы ошибки были
            .SystemPromptRef("#cashflow-forecast@v4")      // независимыми, а не коррелированными
            .Temperature(0.2).MaxTokens(2000).AsUri())
        .To(Llm.Factory("gpt-4o")
            .SystemPromptRef("#cashflow-forecast@v4")
            .Temperature(0.2).MaxTokens(2000).AsUri())
        .To(Llm.Factory("gemini-pro")
            .SystemPromptRef("#cashflow-forecast@v4")
            .Temperature(0.2).MaxTokens(2000).AsUri())
        .To(Llm.Factory("mistral-large")
            .SystemPromptRef("#cashflow-forecast@v4")
            .Temperature(0.2).MaxTokens(2000).AsUri())
    .End()
    .Process<JuryAggregator>()                             // анонимизация A/B/C/D + перемешивание
    .To(Llm.Factory("opus")                                // arbiter — самая мощная модель
        .SystemPromptRef("#cashflow-arbiter@v2")
        .Temperature(0.1).MaxTokens(3000)
        .CostBudget(usd: 0.50).AsUri())
    .Process<ExtractStructuredForecast>()                  // парсим JSON: 30 дней с low/mid/high
    .Choice()
        .When(e => e.In.Body<Forecast>().ConfidenceLow < 0.6)
            .Process<ApprovalGate>()                       // низкая уверенность → жди CFO
            .To("smtp://...?to=cfo@acme.com&priority=high")
        .Otherwise()
            .Process<RenderForecastReport>()
            .To("smtp://...?to=treasury@acme.com")
    .End()
    .Wiretap("kafka://reports.cashflow.archive");

Этот маршрут стоит ~$2-3 за прогон, прогон делается раз в неделю — то есть ~$150/год. Аналитик потратил бы день на ту же задачу. Ключевая ценность не в экономии — в робастности: четыре независимых ответа от разных провайдеров (Anthropic, OpenAI, Google, Mistral) дают модель «согласия» как сигнала уверенности. Если четыре модели сошлись — арбитр Opus просто кодифицирует. Если разъехались — арбитр пишет «модель A видит риск Х, остальные нет, рекомендую человеческую проверку», и Choice-ветка сама уходит в approval-gate.

Через год treasury-команда смотрит на EvalRunProps и видит: «модель Gemini-Pro расходится с консенсусом в 18% случаев, и в 70% этих случаев права именно она» — отличный сигнал, что её надо повысить в весе в jury, а не убрать. Это не догадка, а запрос в REDB.

Кейс 3. Производственный прогноз и проактивный алерт инженеру

В производстве (не финансовом, а реальном — заводы, склады, логистика) типичная задача: по SCADA-метрикам предсказать, что через 4-6 часов случится сбой, и заранее уведомить инженера. Раньше для этого делали отдельные ML-сервисы с feature-store и моделями градиентного бустинга. Это всё ещё корректный путь, и LLM не заменяет его. Но LLM хорошо закрывает «последнюю милю» — превращение «вектор отклонений + контекст» в внятное человеческое объяснение.

// Слушаем поток метрик из SCADA-шины (через MQTT/Kafka), окно 5 минут
From("kafka://scada.metrics?groupId=plant-anomaly-watcher")
    .RouteId("plant-anomaly-watcher")
    .Aggregate(by: e => e.In.Headers["LineId"],
               window: TimeSpan.FromMinutes(5))            // окно по линии производства
    .Process<RunAnomalyModel>()                            // классический ML — XGBoost, ничего нового;
                                                            // выдаёт {score, top-features, line-context}
    .Choice()
        .When(e => e.In.Body<AnomalyReport>().Score > 0.8)
            .Knowledge("plant-runbooks", k: 3)             // RAG: runbook'и по этой линии,
                                                            // история похожих инцидентов
            .To(Llm.Factory("haiku")
                .SystemPromptRef("#plant-incident-explainer@v9")
                .Temperature(0.0).MaxTokens(600)
                .Tools("metrics-history", "fetch-shift-log")  // агент сам дотянет данные
                .MaxIterations(4)
                .CostBudget(usd: 0.10).AsUri())
            .Process<EnrichWithOnDutyEngineer>()           // кто сегодня в смене на этой линии
            .Multicast()
                .To("teams://engineering-shift-{LineId}?card=adaptive")
                .To("sms://twilio?to={engineer.phone}")
                .To("kafka://incidents.predicted.archive")
            .End()
        .Otherwise()
            .To("kafka://scada.metrics.normal")            // просто архивируем
    .End();

Что здесь происходит и почему это работает:

  • ML-модель не выкидывается. Anomaly score считает старый добрый XGBoost, который всю жизнь хорошо это делал. LLM встаёт после него.

  • LLM объясняет, а не предсказывает. Промпт: «вот аномалия на линии 7, top-3 фичи такие-то, runbook говорит так-то, история инцидентов такова. Объясни на одной странице, что вероятно произошло, что инженеру делать в ближайший час, и какие три параметра проверить первыми». Это работа, в которой LLM объективно сильна — синтез.

  • Tools у агента — это доступ к данным завода. metrics-history — это direct:metrics-history маршрут, который запрашивает Influx/Prometheus. fetch-shift-log — REDB-запрос к журналу смены. Агент сам решит, нужно ли ему дёргать эти инструменты, и сколько раз. MaxIterations(4) — потолок, чтобы не зацикливался.

  • Доставка — multi-channel. Teams-карточка с кнопкой «принял в работу» (которая, кстати, дёрнет ApprovalGate-подобный webhook), SMS на дежурный телефон через Twilio (этот transport уже есть в redb.Route), архив в Kafka.

  • Aggregate by LineId, window 5min — это EIP Aggregator, который буферизует метрики по группам. Никакой redb.Route.Llm-специфики; это обычная интеграционная логика, которая работает у вас уже годы.

Что это даёт бизнесу: время от появления аномалии до «инженер на линии знает, что искать» падает с 40 минут (старый процесс — оператор увидел, позвонил, описал, инженер пришёл, начал искать) до 4-5 минут. На дорогостоящих линиях каждый час простоя — это десятки тысяч долларов; этот маршрут окупается за первое предотвращённое срабатывание.

И заметьте, что во всех трёх кейсах роль LLM — узкая. Не «AI управляет фабрикой». Не «AI ведёт финансы компании». LLM делает одну конкретную вещь в маршруте — суммаризацию, синтез, выбор из кандидатов, объяснение. А вокруг неё стоит вся обычная enterprise-инфраструктура: расписания, ETL, RAG, мульти-канальная доставка, аудит, governance, бюджеты. Эта инфраструктура — и есть redb.Route. LLM — последний кирпич, который раньше требовал отдельного сервиса.


Storytelling: как чат-демо вырастает в платформу

Расскажу путь, как он происходит у живой команды (видел его уже несколько раз).

День 1. Кто-то показывает коллегам, что Claude может ответить на тикет. Файл LlmHttpRoutes.cs — 6 строк, From("http://...").To("llm://haiku"), и demo работает. Это проект, не платформа.

From("http:0.0.0.0:5088/api/llm/ask?inOut=true")
    .ConvertBody<string>()
    .To(Llm.Factory("haiku").MaxIterations(1).AsUri());

День 7. Коллеги говорят: «модель не помнит, что я писал минуту назад». Добавляется X-Chat-Id header → ConversationId-поле → ConversationFromHeader(). Один Process(...)-шаг, никакой инфраструктуры — AddRedbLlmStorage() уже в Program.csMessageProps уже пишутся.

.Process(e => e.In.Headers[LlmHeaders.ConversationId] =
    e.In.Headers["X-Chat-Id"]?.ToString() ?? "default")

День 14. «Хочу, чтобы он мог запускать команды на сервере». Появляется tool-shell — отдельный маршрут с .AsLlmTool("shell"), через redb.Route.Exec с allowlist [cmd, pwsh], working-dir в temp, timeout 5 сек, cap 8KB на stdout. Безопасность — в DSL, а не в советах в промпте.

From("direct:tool-shell")
    .AsLlmTool("shell").SideEffect(ToolSideEffect.ReadOnly).Cost(ToolCostClass.Cheap)
    .Then()
    .To(ExecDsl.Run().AllowedCommands("pwsh", "cmd").TimeoutMs(5000).MaxStdoutBytes(8192));

День 21. «Финансы говорят, что счёт за токены превысил план в 4 раза». Добавляется .CostBudget(usd: 0.50) на conversation-id. Без новых сервисов, без новых дашбордов — он попадает в CostBudgetProps, и tsak.web уже умеет его показывать.

День 30. «Нужно, чтобы каждое действие агента было audit-able». Уже работает: ToolAuditProps пишется автоматически с момента, когда инструмент стал маршрутом. Аудитор открывает SQL-запрос — все вызовы за квартал, фильтрованные по tenant и side-effect. Никакого «mes давайте интегрируем».

День 45. «Юристы хотят human-in-loop на refund-инструмент». Добавляется .Process<ApprovalGate>() перед exec. ApprovalProps пишется, Slack-bot подключается как ещё один HTTP-маршрут. Всё.

День 60. «Запускаем второго клиента — отдельная база, отдельный биллинг». Добавляется Process(e => e.In.Headers[LlmHeaders.Redb] = e.In.Headers["X-Tenant"]). Один маршрут, два tenant’а, ноль изменений в логике.

День 90. «Хочу прогнать ночью миллион тикетов через классификатор». Маршрут уже есть. Меняется один аргумент: .Mode(LlmMode.Batch) — вместо синхронного вызова открывается Anthropic Batch, LlmBatchProps хранит id, LlmCallbackProcessor ждёт webhook, результаты идут в тот же Kafka, что и днём. -50% к токен-счёту.

День 120. «Нужны метрики по версиям промптов: которая хуже». EvalRunProps уже хранит прогоны. Открыли SQL, посчитали accuracy по PromptTemplateRef, увидели регрессию в v4, откатились на v3 — без redeploy.

В этом нарративе нет сюжета «и тут мы переписали всё на Kubernetes-операторе». Нет момента «а потом мы добавили AI-платформу». Те шесть строк чат-демо, которые работали в День 1, — это буквально тот же файл, который в День 120 ведёт двух тенантов с jury-арбитражем, batch-классификацией миллиона тикетов за ночь и audit’ом регуляторного уровня. Не было миграции. Не было переписывания. Не было «архитектуры v2». Просто в маршрут добавлялись строчки по мере появления требований. Чат-демо не «вырастает» в платформу — он с первого дня стоит на платформе и постепенно включает её фичи.


Demo-маршруты: где это посмотреть живьём

Все паттерны выше — не concept-art. В репозитории redbase-app/redb-route есть два демо-файла:

  • LlmDemoRoutes.cs — три формы LLM-вызова: inline-step (.Llm("demo-stub")), endpoint (.To("llm://demo-stub")), tool (.AsLlmTool("echo_tool")). Все на stub-провайдере, без API-ключей.

  • LlmHttpRoutes.cs — два HTTP endpoint’а (/api/llm/ask без инструментов, /api/llm/shell с shell-инструментом через redb.Route.Exec), оба с conversation-id из заголовка, оба с реальным Claude Haiku.

Запускается одной командой dotnet run в redb.Route.Demo — открывается порт 5088, можно curl’ом проверять прямо сейчас:

curl -d "what time is it on this host?" -H "X-Chat-Id: test1" 
     http://localhost:5088/api/llm/shell

Repo: github.com/redbase-app/redb-route (Apache 2.0).


Что не сделано, и почему я это говорю

Sliding-window память не сделана. Полноценный векторный store ещё не сделан. Нет «AI-graph editor» в tsak.web — runtime-инспекция конверсаций показывается как обычные REDB-объекты в существующем UI, без отдельного pane. Нет встроенной интеграции с одним из коммерческих eval-сервисов — EvalRunStore хранит прогоны, но «нажми кнопку и сравни с продом» нет.

Это нормально. Skip-list — это техника, не оправдание. Open-source-проект, который врёт в README про скоп — это проект, в который никто не зайдёт во второй раз. Я лучше скажу «не сделано» и сделаю в следующей минорке, чем «сделано» и буду отбиваться от issue про падающий feature.


Zoom out: почему именно ESB

В индустрии популярен термин «AI-нативная архитектура». Под ним обычно понимают «у нас всё построено вокруг LLM», и обычно за этим стоит новый dev-stack рядом со старым. Проблема не в стеке — проблема в дублировании политики.

Если у вас retry — в двух местах. Если у вас audit — в двух местах. Если у вас governance — в двух местах. Если у вас tenant-isolation — в двух местах. И эти две реализации разъезжаются через год: AI-stack добавил bounded-context-аудит, integration-stack — нет. Юристы спрашивают «один отчёт за всё» — никто не может его собрать.

ESB-подход — это сознательный выбор одной точки контроля для I/O всех видов. LLM — это вид I/O (асинхронный, с инструментами, с контекстом, но всё ещё I/O). Поместить его в ESB — это не философская поза, а инженерная экономия: одна governance-политика покрывает всё.

Это не новый поинт, на эту тему писали Гарланд и Рипли (см. SOA-литературу 2000-х). Новое — что в 2026 этот поинт стал применим к LLM, потому что у LLM появились стандартные интерфейсы (tool-use, streaming, batch-API, embeddings). До этого момента «LLM в ESB» означало «налепить REST-обёртку и молиться». Сейчас означает «использовать существующие EIP-паттерны с минорными адаптациями».

И именно в эту экономию упирается весь смысл redb.Route.Llm. Не «у нас лучший агент-движок». Лучший — у Microsoft и LangChain. У нас — тот же средненький агент-движок, но в правильном месте архитектуры. Я считаю, что это важнее.


Roadmap

3.1.2:

  • sliding-window память как штатная политика;

  • vector-store interface поверх существующего IKnowledgeStore;

  • интеграция с pgvector и Qdrant как первые back-end’ы;

  • EvalCompare-DSL для side-by-side прогонов между версиями промптов.

3.2:

  • ConversationProps-tree UI в tsak.web;

  • streaming-aware aggregator EIP (буферизация частичных кадров до семантических границ);

  • distributed batch — несколько worker’ов на один LlmCallbackProcessor.

Потом:

  • multi-modal (image input/output как вариант Out.Body);

  • voice-агенты как ещё один transport (voice://...);

  • routing по cost/latency/accuracy SLA per-message.


Ссылки


TL;DR. В 3.1.0 я обещал — в 3.1.1 сделал. Streaming end-to-end, ToolCache, KnowledgeStore (RAG-чанки), BatchStore с webhook-консьюмером, EvalRunStore, PromptTemplate-реестр с версионированными #-refs, мульти-tenant ?redb=<name>, идемпотентные tool-вызовы, approval-gates с человеком в цикле, audit trail. Поверх — два мощных паттерна: jury из дешёвых моделей с arbiter-моделью сверху через штатный Scatter-Gather + Aggregator EIP, и sub-agents as tools — рекурсивная архитектура, где инструмент агента сам по себе агент со своей моделью/промптом/бюджетом/RAG, реализованная без единой строки спецкода благодаря RouteToolBridge → direct: → llm://. И три полноценных enterprise-кейса с кодом: ежедневный финансовый отчёт CFO на email, недельный cash-flow forecast с jury-арбитражем, проактивный production-алерт инженеру через гибрид XGBoost + LLM. Главный поинт не в фичах — enterprise-уровневые свойства появляются ровно в тот день, когда они вам нужны, без переписывания, потому что LLM лежит в ESB вместе со всем остальным I/O. Те шесть строк чат-демо, которые вы написали в День 1, — это буквально тот же файл, который в День 90 работает как мульти-tenant платформа с audit’ом, бюджетами и jury-арбитражем. Точки «а теперь давайте перепишем» в этой истории нет.

Автор: grelikt

Источник