[{"content":"Память — фундаментальный компонент LLM-агентов, но индустрия застряла на изолированной парадигме: агент помнит только свой собственный опыт. Статья MemRec (ACL 2026) вводит следующий milestone — collaborative memory, где память агентов связана в граф и обменивается реляционными сигналами. Я разберу MemRec до формул и промптов, сравню три production-инструмента (Mem0, Letta, Zep) и покажу, где research опережает tooling.\n1. Почему изолированная память — это тупик Зачем агенту память вообще? Казалось бы, контекстное окно LLM растёт — GPT-4o держит 128K токенов, Gemini — миллион. Но контекстное окно — это не память. Это рабочий стол. Положить на рабочий стол все когда-либо увиденные диалоги, все предпочтения пользователя, все выводы инструментов — рабочий стол сломается. LLM начнёт терять релевантные факты в шуме, как показало исследование Lost in the Middle (Liu et al., 2024): производительность падает, когда нужная информация находится в середине длинного контекста.\nПамять решает другую задачу — она делает агента stateful (состояние сохраняется между запусками), позволяет накапливать опыт и адаптироваться. Без памяти агент — чистая функция: одни и те же входы дают одни и те же выходы. С памятью агент эволюционирует. Этот принцип хорошо описан в работе Generative Agents (Park et al., 2023), где симуляция жителей виртуального города показала, что именно memory stream делает поведение агентов правдоподобным.\nЭволюция памяти агентов прошла три milestone\u0026rsquo;а. Покажу на схеме:\ntimeline title Эволюция парадигм памяти LLM-агентов section No Memory 2023 : Vanilla LLM : Полагается на inherent knowledgeкаждый запуск с чистого листа section Static Memory 2024 : iAgent, Chat-Rec : Retrieval из фиксированногохранилища, без обновления section Dynamic Memory 2025 : AgentCF, i2Agent : Агент итеративно обновляетсвоё понимание section Collaborative Memory 2026 : MemRec (ACL 2026) : Память связана в графколлаборативные сигналы Каждый milestone решал конкретную проблему, но все три держали память в изоляции. Что это значит на практике? Рассмотрим на примере рекомендательных систем — именно там эта проблема выражена наиболее чётко, и именно там MemRec показывает решение, которое обобщается на произвольных агентов.\nВ agentic recommender systems (RS) агент хранит память о пользователе M_u — нарратив о прошлых взаимодействиях, предпочтениях. И память об айтеме M_i — семантическое описание. Когда агент рекомендует книгу пользователю, он видит только M_u этого пользователя. Но что если другой пользователь с похожими вкусами только что оценил книгу, которую наш пользователь ещё не видел? Изолированный агент об этом не знает — collaborative signal (коллаборативный сигнал, реляционная информация от сообщества пользователей) отрезан.\nЭто не специфика рекомендательных систем. Представьте команду агентов, работающих над кодом: один агент нашёл решение проблемы с зависимостями, другой сталкивается с той же проблемой — но не знает, потому что память изолирована. Или агент поддержки: пользователь рассказал о проблеме агенту A, а потом обращается к агенту B — и B начинает с нуля. Изоляция памяти — это потеря коллективного интеллекта.\n2. MemRec: два архитектурных сдвига Статья MemRec (Chen et al., ACL 2026) решает проблему изоляции через два архитектурных сдвига. Но прежде чем их разбирать, нужно понять, почему naive решение не работает.\n2.1 Наивный brute-force и почему он падает Идея «просто дай агенту доступ к памяти всех соседей» приходит в голову первой. Если пользователь смотрел книги вместе с тысячей других пользователей — скормим агенту память всех тысячи. Проблема в том, что это вызывает две катастрофы.\nCognitive overload (когнитивная перегрузка). LLM не может дистиллировать сигнал из изобилия. Lost in the Middle показал, что при росте контекста LLM теряет способность находить релевантные факты. Загрузить в контекст память десятков соседей — и агент начнёт галлюцинировать, терять instruction adherence, путать чьё предпочтение чьё.\nProhibitive collaborative updates (неподъёмные обновления). Коллаборативная память должна эволюционировать. Когда пользователь взаимодействует с айтемом, знание должно распространиться на всех связанных соседей. Naive подход требует отдельного LLM-вызова для каждого соседа — обновить память одного пользователя значит вызвать LLM десятки раз. При real-time serving это непозволительно.\nПокажу, как naive и MemRec подходят к одной задаче:\ngraph LR subgraph \"Naive: один проход, monolithic\" RAW[\"Raw graph context1000 neighborsverbose memories\"] MONO[\"Single LLM(does everything)\"] RES1[\"Result:cognitive overload,hallucinations\"] RAW --\u003e MONO --\u003e RES1 end subgraph \"MemRec: два прохода, decoupled\" GRAPH[\"Raw graphG=(V,E)\"] CUR[\"Curate(LLM rules filter)\"] SYN[\"Synthesize(LM_Mem distill)\"] FACETS[\"M_collab7 structured facets\"] REC[\"LLM_Rec(reasoning only)\"] RES2[\"Result:high-signal context,grounded rationales\"] GRAPH --\u003e CUR --\u003e SYN --\u003e FACETS --\u003e REC --\u003e RES2 end style MONO fill:#c62828,color:#fff style RES1 fill:#c62828,color:#fff style CUR fill:#2e7d32,color:#fff style SYN fill:#2e7d32,color:#fff style REC fill:#1565c0,color:#fff style RES2 fill:#2e7d32,color:#fff Naive подход скормит весь сырой контекст одной модели — и получит information bottleneck: модель не может одновременно поглощать и reasoning. MemRec делит задачу: curate (отфильтровать шум через domain-adaptive rules), synthesize (дистиллировать в structured facets), потом reasoning на чистом контексте. Это и есть «Curate-then-Synthesize» — два прохода сжатия по принципу Information Bottleneck.\nMemRec решает обе проблемы через два архитектурных сдвига.\n2.2 Сдвиг #1: Разделение управления памятью и вывода MemRec разделяет две функции, которые naive агенты выполняют в одной модели:\nLM_Mem (lightweight language model) — управляет памятью в фоне: курирует граф, синтезирует контекст, обновляет соседей. LLM_Rec (heavyweight large language model) — выполняет финальный reasoning: ранжирует кандидатов, генерирует обоснования. Почему нельзя мешать это в одной тяжёлой модели? Тут работает принцип Information Bottleneck (IB, Information Bottleneck — принцип сжатия, сохраняющий максимум информации, релевантной задаче, при минимизации избыточных сигналов; Tishby et al.). Задача LM_Mem — сжать сырой граф-контекст в компактное представление, которое сохраняет максимум полезного сигнала для reasoning и отбрасывает шум. Если та же модель должна и сжимать, и reasoning — возникает information bottleneck: она не может одновременно поглощать verbose context и выполнять сложное ранжирование. Эксперимент это подтверждает: naive monolithic agent (одна модель делает всё) упирается в плато, а MemRec с decoupling даёт +34% относительного прироста H@1 на датасете Books (см. раздел 5, RQ2).\nЭто инженерный принцип, выходящий за рамки рекомендательных систем. Не заставляйте дорогую reasoning-модель заниматься библиотечным делом. Разделите: лёгкая модель копит контекст и дистиллирует, тяжёлая — рассуждает. Cost-эффект тоже значительный — на expensive output tokens (которые в 3-4 раза дороже input) MemRec тратит минимум: Stage-R и Stage-W heavily input-biased (input составляет ~80% от общего usage), что радикально снижает effective cost.\n2.3 Сдвиг #2: Граф коллаборативной памяти MemRec не просто даёт агенту доступ к памяти соседей. Он строит memory graph G = (\\mathcal{V}, E) . Узлы \\mathcal{V} = \\mathcal{U} \\cup \\mathcal{I} — это пользователи и айтемы, каждый узел v хранит evolving semantic memory M_v . Рёбра E кодируют взаимодействия и производные отношения.\ngraph LR subgraph \"Изолированная память\" U1[\"User AM_A\"] U2[\"User BM_B\"] I1[\"Item XM_X\"] I2[\"Item YM_Y\"] end subgraph \"Collaborative memory graph G=(V,E)\" UA[\"User AM_A\"] UB[\"User BM_B\"] UC[\"User CM_C\"] IX[\"Item XM_X\"] IY[\"Item YM_Y\"] IZ[\"Item ZM_Z\"] UA -.-\u003e|\"co-engagement\"| IX UB -.-\u003e|\"co-engagement\"| IX UA -.-\u003e|\"peer similarity\"| UB IX -.-\u003e|\"related\"| IY UC -.-\u003e|\"co-engagement\"| IY UB -.-\u003e|\"co-engagement\"| IZ end style UA fill:#1565c0,color:#fff style UB fill:#1565c0,color:#fff style UC fill:#1565c0,color:#fff style IX fill:#c62828,color:#fff style IY fill:#c62828,color:#fff style IZ fill:#c62828,color:#fff Разница фундаментальная. Изолированная парадигма — это набор disconnected narratives \\{M_u\\} \\cup \\{M_i\\} . Collaborative парадигма — граф с high-order connectivity: сигнал может переходить от пользователя к пользователю через общие айтемы, от айтема к айтему через co-engagement. Для data-sparse пользователей (мало истории) это критично — коллаборативный граф компенсирует дефицит личной истории сигналом от похожих peers. И MemRec подтверждает: +91.4% относительного прироста H@1 для low-activity пользователей над Vanilla LLM.\n3. Глубокий разбор MemRec: трёхстадийный конвейер Теперь разберём архитектуру MemRec до деталей — уравнений, промптов, engineering trade-offs. Это «раскрытие» статьи, как есть.\nMemRec работает в три стадии. Покажу pipeline целиком, потом каждую стадию отдельно.\ngraph TB subgraph \"Background (LM_Mem, lightweight)\" G[\"Memory GraphG = (V, E)\"] C[\"Stage 1:Collaborative MemoryRetrieval\"] P[\"Stage 3:Async CollaborativePropagation\"] end subgraph \"Foreground (LLM_Rec, heavyweight)\" R[\"Stage 2:Grounded Reasoning\"] end G --\u003e|\"raw neighbormemories\"| C C --\u003e|\"M_collabdistilled facets\"| R R --\u003e|\"scores s_i,rationales r_i\"| OUT[\"Rankedrecommendations\"] R -.-\u003e|\"new interaction\"| P P -.-\u003e|\"ΔM updatesto neighbors\"| G style C fill:#2e7d32,color:#fff style P fill:#2e7d32,color:#fff style R fill:#c62828,color:#fff style G fill:#1565c0,color:#fff Зелёное — LM_Mem работает в фоне, лёгкая модель. Красное — LLM_Rec работает в foreground, тяжёлая модель, только когда нужен reasoning. Синее — memory graph, персистентное состояние. Обратите внимание: propagation (Stage 3) — async, не блокирует foreground reasoning.\n3.1 Стадия 1: Извлечение коллаборативной памяти Цель первой стадии — взять expansive memory graph и извлечь сжатую коллаборативную память M_\\text{collab} для текущей задачи. Задача: не перегрузить reasoning-агента. Стратегия — «Отфильтровать-затем-Синтезировать», два прохода сжатия по принципу IB.\n3.1.1 LLM-генератор правил: офлайн-фильтрация Первый проход — фильтрация. Проблема: как выбрать top- k наиболее релевантных соседей из графа? Традиционные подходы — эвристики на правилах (random walk, DeepWalk) или обучаемые нейросетевые скореры (GNN attention). Оба плохи для LLM-агентов: эвристики не адаптируются к семантике домена, обучаемые скореры требуют дорогого обучения.\nMemRec предлагает zero-shot парадигму: LLM-генератор правил. В офлайн-фазе LM_Mem анализирует статистику домена \\mathcal{D}_\\text{domain} и генерирует интерпретируемые эвристические правила R_\\text{domain} :\nR_\\text{domain} \\leftarrow \\text{LM}_\\text{Mem}(\\mathcal{D}_\\text{domain} \\| P_\\text{meta}) \\quad \\text{(offline)} P_\\text{meta} — meta-prompt, который направляет генерацию правил. Вот он (источник: MemRec, Appendix F.1):\nMeta-Prompt Template for Rule Generation You are an expert AI engineer specializing in recommender systems and graph-based memory networks. Your task is to generate a set of domain-specific heuristic rules for a collaborative neighbor pruning algorithm. The goal is to select the top-k most relevant neighbors (users or items) from a candidate graph to build a compact, high-signal context for a downstream LLM recommender (MemRec). DOMAIN CONTEXT • Domain Name: {Domain Name} • Primary Interaction: {Primary Interaction with example} • Key Metadata: {Key Metadata} • Available Features: – edge_weight: {Domain-specific explanation} – recency_days: {Domain-specific explanation} – co_interaction_count: {Domain-specific explanation} – metadata_overlap_score: {Domain-specific explanation} – memory_similarity_score: {Domain-specific explanation} INSTRUCTIONS: 1. Based only on the domain context provided, generate 3-5 high-priority, interpretable ranking rules. 2. The rules should explain how to combine or prioritize the available features to find the best neighbors for this specific domain. 3. Be specific about thresholds and weights. 4. Consider domain-specific characteristics (e.g., books are content-driven with long-term preferences; movies are recency-critical). OUTPUT FORMAT: Rule 1: [Your rule here] Rule 2: [Your rule here] ... Что важно: правила адаптивны к домену. Для Books (контент-driven) LM_Mem генерирует boost для metadata_overlap (\u0026gt;0.6 → множитель 2.5x) — книги читают по жанру/автору. Для MovieTV (зависит от давности) — сильное затухание по давности (exp(-0.018 × recency_days)). Для Yelp (категорийный) — доминирование категорий (metadata_overlap \u0026gt; 0.7 → 3.5x). Это zero-shot: никакого обучения, только семантическое понимание LLM.\nВо время вывода правила работают как высокоскоростной фильтр — выбирают top- k соседей за миллисекунды:\nN'_k(u) = \\text{Curate}(N(u), R_\\text{domain}, k) Количественная валидация: LLM-фильтрация сокращает иррелевантных item-соседей на 73.8% против generic-эвристики, при этом сохраняет на 6.4% больше user-соседей с низким ID-overlap — потому что LLM находит семантическое сходство в нарративах памяти (два пользователя, любящих «dystopian YA novels», без общих кликов).\n3.1.2 Синтез коллаборативной памяти: дистилляция в грани Второй проход — синтез. Отобранные соседи N'_k всё ещё слишком подробные. LM_Mem дистиллирует их в структурированные грани предпочтений \\{F\\} — это и есть M_\\text{collab} :\nM_\\text{collab} = \\{F\\} \\leftarrow \\text{LM}_\\text{Mem}(\\text{Rep}(N'_k) \\| M_u^{t-1} \\| P_\\text{synth}) \\text{Rep}(N'_k) — многоуровневое представление: целевой пользователь u представлен полной памятью M_u^{t-1} , а соседи — компактными контекстными представлениями (сжатые сигналы, а не развёрнутые истории). P_\\text{synth} — промпт синтеза (источник: MemRec, Appendix F.3):\nStage-R Prompt: Collaborative Memory Synthesis You are an intelligent memory retrieval system for personalized recommendation. Your task is to analyze the user\u0026#39;s personal memory and collaborative memories from their neighbors to extract preference facets. Target User: User {user_id} User\u0026#39;s Personal Memory: User Memory Summary: {user_memory_summary} Collaborative Neighbor Memories: The following neighboring users and items provide collaborative signals: Collaborative Neighbors: {formatted_neighbor_list} Your Task: Analyze the user\u0026#39;s personal memory and the collaborative memories to identify {n_facets} distinct preference facets. For each preference facet, provide: 1. A concise natural language description of the preference 2. A confidence score between 0 and 1 3. A list of supporting neighbors (user IDs or item IDs) Output: JSON with \u0026#34;facets\u0026#34; array and \u0026#34;support_edges\u0026#34; array. Результат — вместо «Пользователь предпочитает антиутопии» (изолированная память) получаем:\nТема: Киберпанк и корпоративная антиутопия (уверенность: 0.9)\nОснование: Сосед-пользователь (ID: 2057) проявляет глубокий интерес к корпоративному контролю; сосед-айтем («1984») разделяет фундаментальные антиутопические темы.\nТема: Выживание с высокими ставками (уверенность: 0.75)\nОснование: Сосед-айтем («Battle Royale») обладает сильными элементами выживания, соответствующими недавним взаимодействиям.\nКаждая грань обоснована конкретными соседями. Это и есть дистиллированный высокоинформативный контекст, который пойдёт в LLM_Rec.\n3.2 Стадия 2: Обоснованный вывод Вторая стадия — финальное рассуждение. LLM_Rec получает синтезированную память M_\\text{collab} , инструкцию пользователя \\mathcal{I}_u и память кандидатов C_\\text{info} :\n\\{s_i, r_i\\}_{i=1}^{N} \\leftarrow \\text{LLM}_\\text{Rec}(\\mathcal{I}_u \\| M_\\text{collab} \\| C_\\text{info} \\| P_\\text{rerank}) Для каждого кандидата LLM_Rec генерирует оценку релевантности s_i (0-1) и обоснование на естественном языке r_i . Обоснование в M_\\text{collab} гарантирует, что rationale подкреплён свидетельствами сообщества, а не выдуман. Промпт ранжирования (источник: MemRec, Appendix F.4):\nStage-ReRank Prompt (MemRec Mode) You are an intelligent recommendation scoring system. Your task is to evaluate how well each candidate item matches the target user\u0026#39;s preferences based on their personal memory and collaborative signals. Target User: User {user_id} User\u0026#39;s Current Request: {instruction} User Preferences (Extracted from Collaborative Memories): {formatted_facets} Candidate Item Memories: {formatted_item_memories} Your Task: For each candidate item, provide a relevance score between 0 and 1: • 1.0 = Excellent match, highly aligned with user\u0026#39;s facets • 0.5 = Moderate match, partially relevant • 0.0 = Poor match, not aligned For each item, provide a brief rationale explaining your scoring. Output: JSON with \u0026#34;scores\u0026#34; array of {item_id, score, rationale}. И тут начинается самое интересное. LLM_Rec не видит сырой граф. Он видит только дистиллированные грани с оценками уверенности и свидетельствами. Когнитивная перегрузка невозможна по построению — контекст уже отфильтрован и синтезирован на стадии 1.\n3.3 Стадия 3: Асинхронное коллаборативное распространение Третья стадия — эволюция графа. Когда пользователь u взаимодействует с айтемом i_c , память должна обновиться: пользователь узнал что-то новое, айтем получил новый сигнал, и — ключевое — связанные соседи должны получить распространение знаний.\nПроблема: наивный синхронный подход вызывает LLM для каждого соседа отдельно — O(|N'_k|) вызовов на одно взаимодействие, с повторением контекста пользователя в каждом. MemRec добивается сложности O(1) .\nКак? Вдохновлено Label Propagation (Zhu \u0026amp; Ghahramani, 2002) — алгоритмом, где метки распространяются по графу от узла к узлу. MemRec распространяет «семантические метки» (инсайты, выводы) по графу памяти. Но вместо отдельных вызовов для каждого узла, MemRec концептуально декомпозирует обновление:\nM_u^t, M_{i_c}^t \\leftarrow \\text{LM}_\\text{Mem}(M_\\text{collab} \\| M_u^{t-1} \\| M_{i_c}^{t-1} \\| P_\\text{update}) \\quad \\text{(self-reflection)} \\{\\Delta M_\\text{neigh}\\} \\leftarrow \\text{LM}_\\text{Mem}(M_\\text{collab} \\| M_u^{t-1} \\| M_{i_c}^{t-1} \\| N'_k(u) \\| P_\\text{update}) \\quad \\text{(neighbor propagation)} И выполняет оба шага в одном пакетном асинхронном вызове с единым промптом P_\\text{update} (источник: MemRec, Appendix F.5):\nStage-W Prompt: Asynchronous Collaborative Propagation You are an intelligent memory management system for collaborative recommendation. Your task is to update the personal memories of the user, the clicked item, and relevant collaborative neighbors based on this new interaction. Interaction Context: User {user_id} has just interacted with (clicked) Item {item_id}. User Preferences (Extracted from Collaborative Memories): {formatted_facets} Current Personal Memory of User {user_id}: {current_user_memory} Current Memory of Item {item_id}: {current_item_memory} Collaborative Neighbors Available for Memory Propagation: {n_neighbors} neighbors: {formatted_neighbors} Your Task — Generate UPDATED memories for: 1. The current user (synthesize current memory + facets + clicked item) 2. The clicked item (describe what it is and who might enjoy it) 3. Selected neighbors (collaborative propagation is key!) * Select neighbors RELEVANT to this interaction * Update their memories to reflect new insights * This helps the system learn collaboratively! Output: JSON with \u0026#34;user_memory\u0026#34;, \u0026#34;item_memory\u0026#34;, \u0026#34;neighbor_updates\u0026#34;. Один вызов LM_Mem обновляет пользователя, айтем и выбранных соседей одновременно. Асинхронно — не блокирует основной вывод. Сложность O(1) . Это разрешает проблему неподъёмных обновлений.\ngraph TB subgraph \"Naive: O(|N_k'|) calls\" N1[\"User updatecall 1\"] N2[\"Item updatecall 2\"] N3[\"Neighbor 1call 3\"] N4[\"Neighbor 2call 4\"] N5[\"Neighbor kcall k+2\"] N1 --\u003e N2 --\u003e N3 --\u003e N4 --\u003e N5 end subgraph \"MemRec: O(1) batched async\" B[\"Single LM_Mem callP_update\"] B --\u003e|\"user_memory\"| UU[\"User M_u^t\"] B --\u003e|\"item_memory\"| II[\"Item M_i^t\"] B --\u003e|\"neighbor_updates\"| NN[\"ΔM_neigh(selected neighbors)\"] end style B fill:#2e7d32,color:#fff Наивный подход: линейное число вызовов, каждый повторяет контекст пользователя. MemRec: один пакетный вызов, всё в одном промпте. Разница в задержке и стоимости — на порядки.\n4. Эксперименты MemRec: что показали цифры MemRec оценивается на четырёх benchmark-датасетах: Amazon Books, Amazon Goodreads, MovieTV, Yelp. Разная плотность взаимодействия, разные domain characteristics. Метрики — Hit Rate (H@K) и NDCG (N@K) для K ∈ {1, 3, 5}. Реализация: gpt-4o-mini для обеих LM_Mem и LLM_Rec, k=16 соседей, N_f=7 facets.\n4.1 Главный результат (RQ1) Полная таблица результатов для всех четырёх датасетов:\nModel Books H@1 Books H@5 Goodreads H@1 Goodreads H@5 MovieTV H@1 MovieTV H@5 Yelp H@1 Yelp H@5 LightGCN 0.1753 0.5703 0.2499 0.7903 0.3482 0.6883 0.3444 0.7546 SASRec 0.0914 0.4845 0.1324 0.5407 0.3399 0.6382 0.2305 0.5597 P5 0.2192 0.5273 0.1569 0.5060 0.1696 0.5008 0.1444 0.5220 Vanilla LLM 0.3138 0.7270 0.2864 0.7390 0.4050 0.8603 0.1692 0.6861 iAgent (static) 0.3925 0.6905 0.2617 0.6591 0.4253 0.7420 0.3995 0.7300 RecBot (dynamic) 0.3984 0.6786 0.2705 0.6495 0.4367 0.7309 0.4007 0.7169 AgentCF (dynamic) 0.3457 0.7403 0.2951 0.7726 0.3906 0.7864 0.1925 0.6374 i2Agent (dynamic) 0.4453 0.7708 0.3099 0.7675 0.4912 0.8221 0.4205 0.7648 MemRec 0.5117 0.8007 0.3997 0.8052 0.5882 0.8817 0.4868 0.7908 Источник: MemRec, Tables 2-3\nКлючевой takeaway — иерархия парадигм: Collaborative \u0026gt; Dynamic \u0026gt; Static \u0026gt; No Memory. Динамические агенты (AgentCF, i2Agent) стабильно бьют статические (iAgent), те — Vanilla LLM. Но даже SOTA dynamic agent (i2Agent) сильно уступает MemRec: на Goodreads H@1 +28.98% относительного прироста, на MovieTV +19.75%, на Yelp +15.77%.\n4.2 Валидация когнитивной перегрузки (RQ2) Это самый инженерно интересный эксперимент. MemRec сравнивается с:\nVanilla LLM — без памяти Naive Collaborative Agent — monolithic, обрабатывает uncurated context в одной модели MemRec — decoupled, curate-then-synthesize Naive Agent упирается в плато: одна модель не может одновременно поглощать verbose context и выполнять сложное ранжирование. MemRec ломает плато через decoupling — LLM_Rec получает только high-signal distilled context. Результат: +34% относительного прироста H@1 на Books над Naive Agent. Это прямое подтверждение, что architectural decoupling — не оптимизация, а необходимость.\n4.3 Абляции (RQ4) Что произойдёт, если убрать компоненты MemRec? Ablation на Books:\nAblation H@1 Drop Что значит MemRec (Full) 0.527 — baseline w/o Collab. Read 0.475 −9.9% Без collaborative retrieval — агент видит только личную историю. Самый большой drop w/o LLM Curation 0.498 −5.5% Generic heuristics вместо domain-adaptive LLM rules. Больше шума w/o Collab. Write 0.505 −4.2% Без async propagation. Static graph всё ещё работает, но теряется evolving precision Collaborative retrieval — самый важный компонент. LLM curation бьёт generic heuristics. Async propagation критичен для top-1 precision, даже без него broad recall (H@5) остаётся высоким (0.814).\n4.4 Устойчивость (RQ5) MemRec не просто устойчив к data sparsity — он наиболее полезен именно для data-sparse пользователей. Low-activity группа получает +91.4% относительного прироста H@1 над Vanilla LLM. Коллаборативный граф компенсирует дефицит личной истории.\nПри 30% noise injection (фейковые айтемы в истории) MemRec держит H@1 = 0.491 — resilience за счёт «Curate-then-Synthesize», LLM curation фильтрует иррелевантных peers до того, как они достигнут reasoning-агента.\n4.5 Граница Парето (RQ3) MemRec не просто лучше — он устанавливает новый Pareto frontier между reasoning quality, online inference cost и deployment flexibility. Ключевое: тяжёлая cognitive load обработки collaborative graph offloaded в async offline batches. Online inference видит только distilled M_\\text{collab} .\nКонфигурации:\nConfiguration LLM_Rec / LM_Mem H@1 Latency Cost Примечание Vanilla LLM 4o-mini / — 0.330 ~5.1s lowest Без памяти Standard 4o-mini / 4o-mini 0.524 ~16.5s low Базовый MemRec Cloud-OSS 4o-mini / OSS-120B 0.561 ~11.8s low Near-ceiling, open-weights LM_Mem Local-Qwen 4o-mini / Qwen-2.5-7B 0.470 ~34.0s low* Privacy-sensitive, on-premise Ceiling gpt-4o / 4o-mini 0.580 ~10.4s high Peak performance *Локальное развёртывание — нулевая стоимость API для обслуживания памяти.\nCloud-OSS особенно интересен: открытые веса LM_Mem (gpt-oss-120b) даёт результаты, близкие к потолку, при низкой стоимости. Это значит, что управление памятью можно полностью держать on-premise без потери качества — развёртывание с сохранением приватности реально.\nРаспределение токенов даёт инженерный инсайт: входные токены составляют ~80% от общего использования (вход в 3-4 раза дешевле выхода). Стадии R и W сильно смещены в сторону входа — они поглощают подробный контекст (дёшево) и продуцируют сжатые инсайты (дорого, но мало). На пользователя: ~5,100 входных + ~1,300 выходных = ~6,400 всего токенов. Эффективная стоимость радикально ниже, чем наивная оценка по общему числу токенов.\n5. От рекомендательных систем к произвольным агентам MemRec — статья про рекомендательные системы. Но её архитектурные паттерны универсальны. Давайте перенесём.\nЯвное отображение концептов:\nРекомендательные системы Произвольные LLM-агенты Пользователь u Агент a с личной памятью M_a Айтем i Задача, артефакт, документ t с памятью M_t Взаимодействие пользователь-айтем Выполнение агентом задачи, чтение документа агентом Совместная вовлечённость (общие айтемы) Общий контекст (агенты работали над одной задачей) Похожие пользователи Агенты-коллеги (члены команды) Сигнал коллаборативной фильтрации Передача организационного знания Два конкретных сценария, где collaborative memory применима к произвольным агентам.\nОбщая память нескольких агентов. Команда агентов работает над проектом. Агент A решает проблему с конфликтом зависимостей в Go-модулях. Агент B сталкивается с той же проблемой через неделю. Без коллаборативной памяти — B начинает с нуля, тратит те же часы. С графом коллаборативной памяти — взаимодействие A→«решение конфликта зависимостей» распространяется к агенту B через общее ребро (оба работают с Go-модулями). Это не общая файловая система, это семантическое распространение: B получает дистиллированный инсайт от A, обоснованный свидетельствами.\nОрганизационная память. Агент поддержки обработал 100 тикетов по интеграции SSO. Граф памяти связывает эти тикеты через общие темы. Новый агент поддержки приходит — и вместо чтения 100 тикетов получает синтезированные грани: «Подводные камни интеграции SSO: цепочка сертификатов (уверенность 0.9), несоответствие redirect URI (0.85), гонка обновления токенов (0.7)». Это и есть M_\\text{collab} , дистиллированный из организационного графа.\nЗвучит как фантастика? MemRec показывает, что это работает на рекомендательных системах. Перенос на произвольных агентов — инженерная задача, не исследовательский прорыв. Архитектура та же: LM_Mem курирует граф, LLM_Rec рассуждает, асинхронное распространение обновляет.\n6. Ландшафт: три парадигмы production-памяти Пока research прокладывал путь к collaborative memory, индустрия строила инструменты. Три парадигмы, у каждой arxiv-статья и production-след. Я выбрал именно эти три, потому что они фундаментально разные — не feature-list конкуренты, а разные модели памяти.\ngraph TB subgraph \"Mem0: Managed Memory Layer\" M0[\"Vector store+ hybrid retrieval\"] M0 --\u003e|\"semantic + BM25+ entity\"| M0R[\"Fused results\"] end subgraph \"Letta: OS-Tiered Memory\" L0[\"Core memory(in-context, RAM)\"] L1[\"Archival memory(external, disk)\"] L0 \u003c--\u003e|\"page viafunction calls\"| L1 end subgraph \"Zep: Temporal Knowledge Graph\" Z0[\"Graphiti enginetime-aware facts\"] Z0 --\u003e|\"graph traversal+ temporal\"| Z0R[\"Context withhistory\"] end style M0 fill:#e65100,color:#fff style L0 fill:#4a148c,color:#fff style L1 fill:#4a148c,color:#fff style Z0 fill:#004d40,color:#fff 6.1 Mem0: управляемый слой памяти Mem0 (Chhikara et al., 2025) — универсальный слой памяти для AI-агентов. 59.6k звёзд на GitHub, YC S24. Идеология: память как управляемый сервис — агент не думает о хранении, Mem0 извлекает, консолидирует и находит.\nАрхитектурно — multi-level memory с layer\u0026rsquo;ами:\nLayer Lifetime Что хранит Conversation один turn In-flight сообщения, tool outputs Session минуты-часы Контекст текущей задачи User недели-навсегда Персонализация, preferences Organizational глобально Shared FAQs, policies retrieval многосигнальный (после April 2026 update): semantic search + BM25 keyword + entity matching, всё fused в parallel. Это даёт benchmark\u0026rsquo;и: LoCoMo 91.6 (+20 над предыдущим алгоритмом), LongMemEval 94.8 (+27), BEAM 1M 64.1, BEAM 10M 48.6.\nНовый алгоритм (April 2026, migration guide) — single-pass ADD-only: один LLM-вызов для extraction, без UPDATE/DELETE. Memories накапливаются, ничего не перезаписывается. Agent-generated facts — first-class citizen: если агент подтвердил действие, информация сохраняется с равным весом. Entity linking — сущности извлекаются, эмбеддятся, связываются между memories для retrieval boosting.\nМинимальный SDK snippet:\nfrom mem0 import Memory memory = Memory() # Добавить память memory.add( [\u0026#34;Я Алексей, предпочитаю YAML и тёмную тему.\u0026#34;], user_id=\u0026#34;alex\u0026#34; ) # Retrieval — hybrid: semantic + BM25 + entity results = memory.search( \u0026#34;Какие предпочтения у Алексея?\u0026#34;, filters={\u0026#34;user_id\u0026#34;: \u0026#34;alex\u0026#34;}, top_k=3 ) 6.2 Letta (MemGPT): многоуровневая память по образу ОС MemGPT (Packer et al., UC Berkeley, 2023) — seminal paper, который представил LLM как операционную систему. Идея: virtual context management, вдохновлённый hierarchical memory в традиционных ОС. Core memory (in-context, как RAM) + archival memory (external storage, как disk). Агент сам управляет data movement между tier\u0026rsquo;ами через function calls.\nLetta — production framework на базе MemGPT. White-box, model-agnostic. Базовая абстракция — memory blocks: structured sections контекстного окна, которые персистят между взаимодействиями.\ngraph TB subgraph \"Context Window (Limited)\" SYS[\"System Prompt\"] BLOCKS[\"Memory Blocks(Core Memory = RAM)\"] CONV[\"Conversation(working memory)\"] end subgraph \"External Storage\" ARCH[\"Archival Memory(= Disk)unlimited\"] end LLM[\"LLM\"] SYS --\u003e LLM BLOCKS --\u003e LLM CONV --\u003e LLM LLM --\u003e|\"core_memory_appendcore_memory_replace\"| BLOCKS LLM \u003c--\u003e|\"archival_memory_searcharchival_memory_insert\"| ARCH style BLOCKS fill:#4a148c,color:#fff style ARCH fill:#311b92,color:#fff style LLM fill:#1565c0,color:#fff Core memory (memory blocks) — всегда в контексте, как RAM. Архив — external storage, агент «пейджит» данные через function calls (archival_memory_search, archival_memory_insert). Если block переполняется (chars_limit), агент сам решает, что вытеснить в archival. Вот что видит LLM:\n\u0026lt;memory_blocks\u0026gt; \u0026lt;persona\u0026gt; \u0026lt;description\u0026gt;The persona block: Stores details about your current persona, guiding how you behave and respond.\u0026lt;/description\u0026gt; \u0026lt;metadata\u0026gt;- chars_current=128 - chars_limit=5000\u0026lt;/metadata\u0026gt; \u0026lt;value\u0026gt;I am a helpful assistant named Sam. I enjoy helping users solve problems.\u0026lt;/value\u0026gt; \u0026lt;/persona\u0026gt; \u0026lt;human\u0026gt; \u0026lt;description\u0026gt;The human block: Stores key details about the person you are conversing with.\u0026lt;/description\u0026gt; \u0026lt;value\u0026gt;The user\u0026#39;s name is Alice. She is a software engineer who prefers concise answers.\u0026lt;/value\u0026gt; \u0026lt;/human\u0026gt; \u0026lt;/memory_blocks\u0026gt; Ключевые свойства memory blocks:\nAgent-managed — агент автономно организует информацию по block labels Always visible — блоки в контексте всегда, retrieval не нужен Shareable — несколько агентов могут читать один block (shared memory, multi-agent coordination) Read-only option — политики, read-only блоки, агент не может их менять Benchmark: DMR (Deep Memory Retrieval) 93.4% — baseline, который Zep превзошёл.\nМинимальный snippet:\nfrom letta import Letta client = Letta() agent = client.agents.create( name=\u0026#34;memory_agent\u0026#34;, memory_blocks=[ {\u0026#34;label\u0026#34;: \u0026#34;human\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;User prefers Go and concise answers.\u0026#34;}, {\u0026#34;label\u0026#34;: \u0026#34;persona\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;I am a senior engineering assistant.\u0026#34;}, ], # archival memory — external, agent pages via function calls ) 6.3 Zep: темпоральный граф знаний Zep (Rasmussen et al., 2025) — memory layer service на базе Graphiti. Graphiti — open-source framework для temporal knowledge graphs (контекстных графов с временной осью). В отличие от статического RAG, Graphiti делает real-time incremental updates: отношения и факты эволюционируют вместе с данными, без batch recomputation.\nАрхитектура: dynamic synthesis — одновременно обрабатывает unstructured conversational data и structured business data, сохраняя historical relationships. Факт в графе имеет time validity: когда он стал истинным, когда перестал. Запрос может reasoning\u0026rsquo;ить над тем, как факты менялись во времени, а не только что истинно сейчас.\ngraph LR subgraph \"Input\" CONV[\"Conversational data(unstructured)\"] BIZ[\"Business data(structured)\"] end subgraph \"Graphiti Engine\" ENT[\"EntityExtraction\"] EDGE[\"EdgeCreation\"] TEMP[\"TemporalAnnotation\"] GRAPH[\"TemporalKnowledge Graph\"] end subgraph \"Output\" FACTS[\"Facts withtime validity\"] Q[\"Query:'what changedsince X?'\"] end CONV --\u003e ENT BIZ --\u003e ENT ENT --\u003e EDGE --\u003e TEMP --\u003e GRAPH GRAPH --\u003e FACTS FACTS --\u003e Q style GRAPH fill:#004d40,color:#fff style TEMP fill:#00695c,color:#fff Каждый факт в графе annotated временными метками: когда стал истинным (valid_from), когда перестал (valid_to). Запрос «что пользователь делал в марте?» traversal\u0026rsquo;ит граф по temporal edges, а не просто semantic similarity. Это качественно другой retrieval — не «найди похожее», а «найди, что было истинно в момент X».\nBenchmark\u0026rsquo;и:\nDMR: 94.8% vs MemGPT 93.4% — Zep превосходит на benchmark\u0026rsquo;е, который MemGPT team установила как primary metric LongMemEval: +18.5% accuracy, −90% latency vs baseline — особенно сильна в cross-session synthesis и long-term context maintenance Минимальный snippet:\nfrom zep_cloud import Zep client = Zep(api_key=\u0026#34;...\u0026#34;) # Добавить episode — Graphiti синхронизирует граф client.memory.add( session_id=\u0026#34;session-1\u0026#34;, messages=[{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Я перешёл с PostgreSQL на SQLite для проекта X.\u0026#34;}] ) # Retrieval — graph traversal + temporal reasoning context = client.memory.get(session_id=\u0026#34;session-1\u0026#34;) # context содержит facts с temporal validity 6.4 Новые направления: A-MEM и LangMem Mem0, Letta, Zep — три production-инструмента. Но research не стоит на месте.\nA-MEM (arXiv:2502.12110, NeurIPS 2025) — agentic memory system following Zettelkasten principles. Память организует себя: dynamic indexing и linking создают interconnected knowledge networks. Это research-направление, production-следа пока нет, но идея — memory как self-organizing систему — перекликается с collaborative propagation в MemRec.\nRecMem (arXiv:2605.16045, ACL 2026 Findings) — переосмысливает не ЧТО хранить, а КОГДА консолидировать. Существующие системы eagerly вызывают LLM для каждого взаимодействия — дорого. RecMem копит взаимодействия в subconscious-слое (дешёвые embeddings), и вызывает LLM для извлечения эпизодической и семантической памяти только при устойчивом повторении семантически похожих взаимодействий — аналог фазового перехода. Результат: до 87% снижения стоимости токенов при превышении точности трёх SOTA-систем. Это прямой ответ на тот же cost-вызов, что и MemRec, но через density-triggered consolidation вместо async propagation. Наглядный разбор архитектуры — на YouTube: «Фазовые переходы в памяти ИИ-агентов: архитектура REC Memory».\nLangMem (langchain-ai/langmem) — SDK-level primitives для LangGraph. Semantic, episodic, procedural memory как functional building blocks. Не standalone система, а библиотека для тех, кто строит свою memory architecture поверх LangGraph storage layer.\n7. Сравнение TOP-3: девять осей Теперь сравним Mem0, Letta и Zep системно — по девяти осям. Каждая ось — архитектурное решение, не feature.\nОсь Mem0 Letta (MemGPT) Zep 1. Модель памяти Vector store + entity graph OS-tiered (core=RAM, archival=disk) Temporal knowledge graph (Graphiti) 2. Архитектура Managed layer (service/SDK) In-process agent (framework) Service (Graphiti engine) 3. Retrieval Hybrid: semantic + BM25 + entity (parallel fused) Hierarchical paging (function calls) Graph traversal + temporal reasoning 4. Temporal awareness Added Apr 2026 (time-aware retrieval) Нет First-class (факты с time validity) 5. Update cost Single-pass ADD-only (1 LLM call) Agent self-edits via function calls Real-time incremental (no batch) 6. Collaborative memory Нет (entity linking — intra-agent) Partial (shared blocks — multi-agent) Нет (graph per-session) 7. Benchmarks LoCoMo 91.6, LongMemEval 94.8, BEAM 1M 64.1 DMR 93.4 DMR 94.8, LongMemEval +18.5% 8. Production Self-host (Docker) / Cloud / CLI Self-host (framework) / Cloud Self-host / Cloud 9. Ergonomics SDK (Python/TS), CLI, agent skills SDK (Python/TS), white-box, model-agnostic SDK (Python/TS/Go) 7.1 Сводная таблица бенчмарков Benchmarks — болезненная тема. LoCoMo, LongMemEval, DMR, BEAM — разные benchmark\u0026rsquo;и, измеряющие разные вещи. Direct comparison валиден только на shared benchmark\u0026rsquo;ах.\nBenchmark Что измеряет Mem0 Letta Zep LoCoMo Long conversational memory (single/multi-hop, temporal, open-domain) 91.6 — — LongMemEval Cross-session synthesis, long-term context 94.8 — +18.5% acc, −90% latency DMR Deep Memory Retrieval — 93.4% 94.8% BEAM Million-token scale memory 64.1 (1M), 48.6 (10M) — — Shared benchmark\u0026rsquo;и: LongMemEval у Mem0 и Zep (прямое сравнение возможно); DMR у Letta и Zep (Zep превосходит 94.8 vs 93.4). LoCoMo и BEAM — Mem0-only. Глобальное утверждение «X лучше Y» невозможно — только per-benchmark.\n7.2 Подсветка: коллаборативная память — разрыв Ось 6 — самая важная для этой статьи. Ни Mem0, ни Letta, ни Zep не реализуют collaborative memory в том смысле, как её определяет MemRec (memory graph с cross-agent/cross-entity реляционным signal transfer).\nУточню нюансы:\nMem0 entity linking — intra-agent: сущности связываются внутри памяти одного пользователя, не между агентами. Это boosting retrieval, не collaborative signal transfer. Letta shared blocks — multi-agent coordination: несколько агентов читают один read-only block. Это shared state, но не collaborative propagation — нет эволюции графа. Zep temporal graph — per-session: граф строится для одной сессии/пользователя, нет cross-agent graph с propagation. MemRec показывает, что collaborative memory даёт +14-29% H@1. Это не маржинальная оптимизация — это качественный скачок, и ни один production-инструмент его не делает. Подробнее в разделе 8.\n8. Research vs Tools: где разрыв Коллаборативная память — не единственный gap. Research frontier (MemRec) опережает tooling (TOP-3) по четырём направлениям. Разберу каждое: что говорит research, что делают инструменты, что внедрять сейчас.\ngraph TB subgraph \"Research Frontier (MemRec)\" R1[\"Collaborative memorygraph G=(V,E)\"] R2[\"Architectural decouplingLM_Mem / LLM_Rec\"] R3[\"Temporal-awarefacts + history\"] R4[\"Curate-then-SynthesizeIB-grounded distillation\"] end subgraph \"Tooling Reality\" T1[\"Mem0: intra-agententity linking\"] T2[\"Letta: in-processno decoupling\"] T3[\"Zep: first-classMem0: added 2026Letta: none\"] T4[\"Mem0: consolidationLetta: agent-managedZep: graph synthesis\"] end R1 -.-\u003e|\"GAP\"| T1 R2 -.-\u003e|\"PARTIAL\"| T2 R3 -.-\u003e|\"Zep: COVERED\"| T3 R4 -.-\u003e|\"AD HOC\"| T4 style R1 fill:#c62828,color:#fff style T1 fill:#c62828,color:#fff style R2 fill:#f9a825,color:#000 style T2 fill:#f9a825,color:#000 Разрыв 1: Коллаборативная память — самый большой Что говорит исследование: MemRec показывает +14-29% H@1 от collaborative signal. Memory graph G=(V,E) с high-order connectivity передаёт реляционные сигналы между агентами/айтемами (Chen et al., 2026).\nСтатус инструментов: Ни один из TOP-3 не делает коллаборативную память. Entity linking у Mem0 — внутри одного агента. Shared blocks у Letta — общее состояние без распространения. Темпоральный граф у Zep — в рамках сессии.\nВнедрять сейчас: Если нужен коллаборативный сигнал — собирать самостоятельно. Архитектура MemRec переносима: LM_Mem курирует граф, LLM_Rec рассуждает, асинхронное распространение обновляет. Никакой магии, чистая инженерия. Либо ждать, пока исследования дойдут до инструментов — судя по темпам (MemRec ACL 2026, A-MEM NeurIPS 2025), 12-18 месяцев.\nРазрыв 2: Разделение архитектуры — частичное Что говорит исследование: Разделение управления памятью (LM_Mem) и вывода (LLM_Rec) разрешает когнитивную перегрузку (+34% H@1 над наивной монолитной моделью). Основанный на IB «Отфильтровать-затем-Синтезировать» — два прохода сжатия.\nСтатус инструментов: Частично. У Mem0 извлечение/поиск отделены от reasoning-LLM, но не как архитектурный принцип первого класса с IB-обоснованием. Letta — в процессе, агент сам редактирует memory blocks, никакого разделения. Zep — движок Graphiti отделён от вывода, но без явных стадий дистилляции.\nВнедрять сейчас: Mem0 ближе всех. Если важен trade-off стоимость/задержка — разделение Mem0 даёт часть выгоды. Полное разделение как в MemRec — собирать самостоятельно.\nРазрыв 3: Темпоральность — Zep first-class Что говорит исследование: Факты эволюционируют. Запрос должен рассуждать над тем, как факты менялись, не только что истинно сейчас.\nСтатус инструментов: Zep — первого класса (темпоральный граф Graphiti, факты со временем жизни). Mem0 — добавлено в апреле 2026 (поиск с учётом времени, но не первого класса как у Zep). Letta — нет темпоральности.\nВнедрять сейчас: Zep. Если темпоральный вывод критичен (enterprise-сценарии, синтез между сессиями) — Zep единственный вариант первого класса. LongMemEval +18.5% точности / −90% задержки — прямое подтверждение.\nРазрыв 4: Дистилляция — ad hoc Что говорит исследование: «Отфильтровать-затем-Синтезировать» — основанный на IB, доменно-адаптивные LLM-правила фильтрации, структурированные грани с уверенностью и свидетельствами. Единый подход.\nСтатус инструментов: У каждого по-своему. Mem0 — консолидация (однослойная экстракция, но без структурированных граней). Letta — агент сам управляет (агент решает, что в core memory). Zep — синтез графа (Graphiti строит граф, но не в формате граней). Нет единого подхода к дистилляции.\nВнедрять сейчас: Зависит от сценария. Консолидация Mem0 — для простой персонализации. Синтез графа Zep — для сложного реляционного вывода. Структурированные грани как в MemRec — собирать самостоятельно.\n9. Инженерный фреймворк выбора Какой инструмент выбрать? Зависит от четырёх характеристик use case.\ngraph TB START[\"Use case needsagent memory?\"] Q1{\"Temporal reasoningcritical?\"} Q2{\"Scale \u003e 1Mmemories?\"} Q3{\"Multi-agentcoordination?\"} Q4{\"Collaborativememory needed?\"} ZEP[\"Zep(temporal graph)\"] MEM0[\"Mem0(managed layer)\"] LETTA[\"Letta(OS-tiered)\"] CUSTOM[\"Custom buildMemRec-style architectureor wait for tooling\"] START --\u003e Q1 Q1 --\u003e|\"Да\"| ZEP Q1 --\u003e|\"Нет\"| Q2 Q2 --\u003e|\"Да\"| MEM0 Q2 --\u003e|\"Нет\"| Q3 Q3 --\u003e|\"Да\"| LETTA Q3 --\u003e|\"Нет\"| Q4 Q4 --\u003e|\"Да\"| CUSTOM Q4 --\u003e|\"Нет\"| MEM0 style ZEP fill:#004d40,color:#fff style MEM0 fill:#e65100,color:#fff style LETTA fill:#4a148c,color:#fff style CUSTOM fill:#c62828,color:#fff Краткая summary по веткам:\nХарактеристика сценария Рекомендация Почему Темпоральный вывод критичен Zep Темпоральный граф первого класса, DMR 94.8%, LongMemEval +18.5% Масштаб \u0026gt; 1M памятей, персонализация Mem0 BEAM 1M 64.1, управляемый слой, гибридный поиск, 59.6k звёзд, обкатан в продакшене Координация нескольких агентов Letta Общие memory blocks, многоуровневая память, агент сам управляет Нужна коллаборативная память Собирать самостоятельно Ни один из TOP-3 не делает коллаборативную память. Архитектура MemRec переносима. Простая персонализация, быстрый старт Mem0 Лучшая эргономика, CLI, agent skills, cloud + self-host Важная оговорка: decision tree упрощён. Реальный выбор зависит от deployment model (on-premise vs cloud), latency requirements, compliance. Mem0 Cloud-OSS config показывает, что open-weights LM_Mem даёт near-ceiling results — privacy-preserving deployment реален.\n10. Открытые проблемы и куда движется research Статья подходит к концу, но тема — нет. Несколько открытых проблем, которые определят следующие 12-18 месяцев.\nCollaborative memory across agents. MemRec показал collaborative memory в рамках одной системы (рекомендательной). Перенос на multi-agent systems — открытая задача. Как propagate insights между агентами с разными specializations? Как избежать noise при propagation? A-MEM (NeurIPS 2025) указывает направление — self-organizing memory через Zettelkasten — но production-инструментов пока нет.\nMemory consolidation / dreaming. Anthropic и OpenAI в 2026 shipped background memory consolidation — так называемое «Dreaming». Идея: агент «спит» между сессиями и консолидирует память — компрессия, deduplication, extracting patterns. Это перекликается с async propagation MemRec, но на другом масштабе: не propagation между узлами графа, а consolidation всей памяти агента. Подробно механика Dreaming (Anthropic Auto Dream, OpenAI Dreaming V3 с 82.8% factual recall) разобрана в серии AI Agent Design Patterns, Part 6 — не буду дублировать.\nEvaluation gaps. Нет единого benchmark\u0026rsquo;а для collaborative memory. LoCoMo, LongMemEval, DMR — single-agent benchmarks. MemRec использует RS-specific metrics (H@K, N@K). Как оценить collaborative memory для arbitrary agents? Это research-задача — без benchmark\u0026rsquo;а нет сравнения, без сравнения нет прогресса.\nPrivacy-preserving federated memory. MemRec в limitations указывает: future work — federated memory updates. Если коллаборативный граф распределён между организациями (каждая со своим NDA) — как transfer insights без transfer raw data? Federated learning для memory graphs — открытая область.\nЗаключение Память LLM-агентов прошла путь от полного отсутствия к dynamic isolated memory. MemRec (ACL 2026) указывает следующий milestone — collaborative memory, где память агентов связана в граф и обменивается реляционными сигналами. Архитектурно это означает: decouple memory management от reasoning, build memory graph, curate-then-synthesize для distillation, async propagation для эволюции.\nТри production-инструмента — Mem0, Letta, Zep — представляют три разные парадигмы изолированной памяти: управляемый слой, многоуровневая по образу ОС, темпоральный граф. Все три годятся для однозадачных сценариев. Но ни один не делает коллаборативную память, которую MemRec доказал как следующий шаг (+14-29% H@1).\nИсследования опережают инструменты. Это нормально — исследовательский фронт двигается быстрее внедрения в продакшен. Для инженера сегодня: выбирайте из TOP-3 по характеристикам сценария, но помните, что коллаборативная память — следующая волна, и MemRec показывает, как её строить.\nСсылки по статье:\nMemRec: Collaborative Memory-Augmented Agentic Recommender System — Chen et al., ACL 2026 Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory — Chhikara et al., 2025 MemGPT: Towards LLMs as Operating Systems — Packer et al., 2023 Zep: A Temporal Knowledge Graph Architecture for Agent Memory — Rasmussen et al., 2025 A-MEM: Agentic Memory for LLM Agents — Xu et al., NeurIPS 2025 RecMem: Recurrence-based Memory Consolidation for Efficient and Effective Long-Running LLM Agents — Dai et al., ACL 2026 Findings Lost in the Middle — Liu et al., 2024 Generative Agents — Park et al., 2023 ","permalink":"https://triumphpc.github.io/blog/ru/posts/agent-memory-collaborative-deep-dive/","summary":"Глубокий разбор архитектуры памяти LLM-агентов на основе статьи MemRec (ACL 2026): collaborative memory, decoupling управления памятью от reasoning, Information Bottleneck. Сравнение TOP-3 production-инструментов — Mem0, Letta (MemGPT) и Zep — по девяти осям. Подсветка gap\u0026rsquo;а: research опережает tooling.","title":"Память агентов: от изолированного контекста к коллаборативной памяти"},{"content":"1. Введение В Part 1 я разбирал ReAct — агент, который думает и действует. В Part 4 — пять архитектур оркестрации нескольких агентов. В Part 6 — глубокий разбор памяти. Шесть частей, десятки паттернов, сотни строк кода на Eino. Но всё это — код, который работает внутри пода. ReAct-цикл крутится в одном процессе. Supervisor делегирует под-агентам в том же контейнере. Память живёт в in-process map.\n23 марта 2026 CNCF AI TCG (AI Technical Community Group — техническая рабочая группа по ИИ внутри Cloud Native Computing Foundation) выпустил документ «Cloud Native Agentic Standards» — попытку собрать в одном месте лучшие практики для agentic-систем в Kubernetes. Документ охватывает пять разделов: General (контейнерные основы), Control and Communication (протоколы и обнаружение сервисов), Observability (метрики, трейсы, логи), Governance (оценка и аудит), Security (идентичность и доступ к данным).\nДокумент хороший как карта территории. Но он написан плотно — каждый пункт это одна-две фразы, за которыми стоит много контекста. «Enforce the principle of least privilege» — что именно? «Use MCP for tool exposure» — а что такое MCP, какая версия, где спецификация? «Agent-as-a-Judge» — это что, тоже агент?\nВ этой статье я разбираю документ пункт за пунктом. По каждой рекомендации: что CNCF имеет в виду, как это реализовано сегодня в Kubernetes-экосистеме, какие технологии и спецификации существуют, куда читать глубже. Где уместно — примеры кода на Go и K8s-манифесты, Mermaid-диаграммы, ссылки на arxiv-статьи и спецификации.\nУровень абстракции меняется — мы поднимаемся от паттернов внутри агента к паттернам эксплуатации агентов в cloud-native среде. Но принцип серии сохраняется: deep dive, honest limitations, code examples.\n2. General — контейнерные основы Первый раздел CNCF-документа — не agent-специфичный. Это введение в лучшие практики работы с контейнерами: безопасность, наблюдаемость, доступность. Ничего, чего не было бы в обычном Kubernetes-чеклисте. Но именно эта основа нужна, чтобы дальше говорить об agent-специфичных вещах.\n2.1 Security — безопасность контейнеров CNCF рекомендует: принцип минимальных привилегий, сокрытие информации, multi-stage builds, безопасные образы из доверенных репозиториев, сканирование уязвимостей, подпись образов, управление секретами через Kubernetes Secrets или внешние менеджеры, запуск от non-root пользователя, distroless-образы, мониторинг runtime-поведения.\nЧто это значит на практике. Принцип минимальных привилегий (least privilege) — агент получает только те права, которые ему нужны для работы, ничего больше. Для K8s-пода это значит: securityContext.runAsNonRoot: true, runAsUser с конкретным UID, readOnlyRootFilesystem: true, allowPrivilegeEscalation: false. NetworkPolicy, ограничивающая исходящие соединения только нужными хостами. ServiceAccount с RBAC-ролью, где перечислены конкретные ресурсы и глаголы — не cluster-admin, а get pods, list services в конкретном namespace.\nDistroless-образы (от Google) — образы контейнеров без операционной системы: только приложение и его зависимости, без shell, package manager, утилит. Меньше атакуемая поверхность — если злоумышленник получит доступ к контейнеру, ему некуда развернуться. Для Go-агента это естественно: статически слинкованный бинарник + CA-сертификаты, всё.\nMulti-stage builds — в Dockerfile несколько этапов: первый собирает бинарник (с компилятором, зависимостями), второй копирует только результат. Сборочные инструменты не попадают в финальный образ.\nПодпись образов — OCI-аннотации (Open Container Initiative — спецификация формата образов) + криптографическая подпись через Sigstore (cosign). Позволяет проверить, что образ не был подменён в registry.\nКак сегодня. Kubernetes security checklist, Pod Security Standards, CNCF cloud native security whitepaper v2. Для сканирования уязвимостей — Trivy, Grype. Для runtime-мониторинга — Falco, Tetragon.\nЧто почитать. Linux kernel security constraints — как K8s использует seccomp, AppArmor, SELinux для изоляции подов.\n2.2 Observability — наблюдаемость контейнеров CNCF рекомендует: использовать стандартный observability-стек MELT (Metrics, Events, Logs, Traces — метрики, события, логи, трейсы), сетевую наблюдаемость через flow logs, мониторинг CPU/GPU и диска, alerting на основе SLO/SLA, cost observability для GPU и LLM-бенчмаркинга.\nЧто это значит. MELT — четыре сигнала наблюдаемости. Метрики — числовые показатели (CPU, память, RPS, latency). События — дискретные факты (под перезапустился, deployment завершён). Логи — текстовые записи работы приложения. Трейсы — распределённая трассировка запроса через несколько сервисов. Для агентов это особенно важно: один запрос пользователя может пройти через 5-10 tool-call\u0026rsquo;ов в разных подах, и без трейсов понять где что сломалось — невозможно.\nCost observability для GPU и LLM — агент-специфичное расширение. GPU-инстансы дороги ($2-12/час для A100/H100), LLM API вызовы тарифицируются по токенам. Без cost observability один runaway-агент (агент, вышедший из-под контроля — например, зациклившийся в retry-loop) может потратить тысячи долларов за часы. Нужны метрики: tokens per request, cost per trajectory, GPU utilization per agent instance.\nКак сегодня. Prometheus — метрики, Loki — логи, Jaeger/Tempo — трейсы, всё через OpenTelemetry Collector. Для GPU — NVIDIA DCGM exporter. Для LLM-cost — кастомные метрики (подробнее в разделе 4).\nЧто почитать. CNCF «What is Observability 2.0» — почему MELT недостаточно и что дальше.\n2.3 Availability and fault tolerance — доступность и отказоустойчивость CNCF рекомендует: resource limits и requests, PodDisruptionBudgets, Pod Anti-Affinity и Topology Spread Constraints, Horizontal Pod Autoscaler.\nЧто это значит. resources.requests — сколько CPU/памяти под гарантированно получит. resources.limits — потолок. Без requests под может попасть на перегруженный узел (noisy neighbor — «шумный сосед», когда один под потребляет ресурсы и мешает другим). Для GPU — nvidia.com/gpu: 1 в limits.\nPodDisruptionBudget (PDB) — K8s-ресурс, гарантирующий минимум доступных подов при добровольных сбоях (voluntary disruptions — обновления, drain узлов, масштабирование). minAvailable: 2 значит, что всегда останется минимум 2 пода агента.\nPod Anti-Affinity / Topology Spread Constraints — распределение подов по разным узлам и зонам доступности. Если все поды агента на одном узле — его отказ убивает всю систему. topologySpreadConstraints с maxSkew: 1 гарантирует равномерное распределение.\nHorizontal Pod Autoscaler (HPA) — автоматическое масштабирование. CNCF упоминает custom metrics — для агентов это может быть «количество активных сессий» или «длина очереди tool-call\u0026rsquo;ов», не только CPU.\n2.4 Availability — inference-specific CNCF рекомендует: Gateway API Inference Extensions для path-based правил маршрутизации к inference-моделям.\nЧто это значит. Inference (инференс — процесс выполнения модели, генерация ответа) — отдельный класс нагрузки. В отличие от обычных HTTP-сервисов, inference-запросы долгие (секунды, не миллисекунды), потребляют GPU (не CPU), и чувствительны к batch size и KV-cache состоянию. Обычный round-robin load balancer здесь не оптимален.\nGateway API Inference Extensions — официальный Kubernetes-проект от WG-Serving (Working Group по обслуживанию моделей) и SIG-Network. Расширяет Gateway API двумя концепциями:\nflowchart LR CLIENT[\"Client(agent)\"] --\u003e GW[\"Gateway(Envoy/NGINX)\"] GW --\u003e EPP[\"Endpoint Picker(smart load balancing)\"] EPP --\u003e|\"metrics:queue depthKV cache state\"| IP[\"InferencePoolvLLM / Triton\"] IP --\u003e EP1[\"Endpoint 1model server\"] IP --\u003e EP2[\"Endpoint 2model server\"] IP --\u003e EP3[\"Endpoint 3model server\"] style EPP fill:#1565c0,color:#fff style IP fill:#e65100,color:#fff InferencePool — набор endpoint\u0026rsquo;ов (подов с model server — vLLM, Triton), рассматриваемых как единый пул. Endpoint Picker (EPP) — расширяемый компонент, выбирает оптимальный endpoint на основе метрик от model server: глубина очереди, состояние KV-cache (кэш контекста диалога в памяти GPU), доступность LoRA-адаптеров (низкоранговая адаптация — техника дообучения модели без изменения всех весов). Это «умный» load balancing — не round-robin, а выбор endpoint\u0026rsquo;а, который быстрее всего ответит.\nServing priority — Gateway API IE поддерживает приоритеты: chat (чувствителен к задержкам) — высокий приоритет, summarization (пакетная обработка) — низкий. Model rollouts — канареечное выкатывание новых версий моделей через traffic splitting по имени модели.\nКак сегодня. Проект активно развивается, интеграция с vLLM (высокопроизводительный inference-сервер) и Triton (NVIDIA inference server). Поддерживается Envoy Gateway, kgateway, GKE Gateway. Совместим с OpenAI API — можно интегрировать self-hosted модели с LiteLLM, Gloo AI Gateway, Apigee.\n3. Control and Communication — управление и коммуникация Самый насыщенный раздел CNCF-документа. Протоколы, identity-фреймворки, messaging, discovery — всё, что связано с тем, как агенты общаются с инструментами, моделями и друг с другом.\n3.1 Orchestration flow — оркестрация CNCF рекомендует: проектировать оркестрацию по принципам GitOps, учитывая архитектурные паттерны (centralized, decentralized, star, ring) и их влияние на безопасность и отказоустойчивость.\nЧто это значит. GitOps — подход, где конфигурация системы хранится в Git-репозитории как source of truth, а контроллер (Flux, ArgoCD) синхронизирует состояние кластера с репозиторием. Для агентов это значит: системные промпты, конфигурация инструментов, RBAC-политики, версии моделей — всё в Git, с историей изменений, ревью через pull requests, откатом через revert.\nАрхитектурные паттерны оркестрации (из Part 4):\nflowchart TB subgraph PATTERNS[\"Паттерны оркестрации (из Part 4)\"] direction LR CENT[\"Centralized(Supervisor)один координатор\"] DEC[\"Decentralized(Swarm/Handoff)агенты передают друг другу\"] STAR[\"Star(Hub-and-spoke)центральный хаб\"] RING[\"Ring(каскаднаяпередача по кругу)\"] end style CENT fill:#2e7d32,color:#fff style DEC fill:#e65100,color:#fff style STAR fill:#1565c0,color:#fff style RING fill:#6a1b9a,color:#fff Каждый паттерн имеет разные trade-offs для безопасности и отказоустойчивости. Centralized (Supervisor) — один координатор, легко аудировать, но single point of failure. Decentralized (Swarm) — нет единой точки отказа, но сложнее отладка (distributed traces). Star — хаб как bottleneck. Ring — задержка растёт с числом участников.\nКак сегодня. Flux и ArgoCD — оба CNCF-проекты, оба поддерживают GitOps для K8s. Для agent-конфигурации — Helm-чарты или Kustomize с промптами как ConfigMaps/Secrets.\n3.2 Tools and services — инструменты и сервисы CNCF рекомендует: единый подход к доступу к инструментам (MCP, A2A, ACP), избегать избыточной вариативности («tool sprawl» — «разрастание инструментов»), продумать поведение при недоступности системы контроля доступа.\nЧто это значит. «Tool sprawl» — когда в системе 5 разных способов вызвать инструменты: MCP для одних, custom REST для других, gRPC для третьих. Каждая вариативность — это сложность эксплуатации и мониторинга. CNCF призывает: выберите один-два протокола и держитесь их.\nПоведение при недоступности Access Control (системы контроля доступа — например, OPA, Keycloak): что должен делать агент, если не может проверить права? Отказать? Работать с кэшированными правами? Какие сервисы остаются доступны? Это нужно продумать заранее, не в момент сбоя.\nКак сегодня. MCP стал де-факто стандартом для доступа к инструментам (подробнее в 3.6). Для access control — OPA (Open Policy Agent, CNCF Graduate) с Rego-политиками, или Kyverno для K8s-native политики.\n3.3 Agent connectivity to AI Models — связь агента с моделями CNCF рекомендует: продумать, какие процессы могут продолжаться при недоступности модели, а какие — нет. Рассмотреть Kubernetes custom watcher/controller для мониторинга критических ресурсов. Использовать gateway/proxy с observability для обнаружения сбоев.\nЧто это значит. Агент зависит от LLM-модели (GPT-4, Claude, self-hosted Llama). Если модель недоступна — что делать? В multi-agent системе с разными моделями: какие агенты могут работать без модели X, какие — нет?\nНапример: supervisor-агент использует GPT-4 для reasoning, coder-агент использует локальный CodeLlama для генерации кода. Если GPT-4 недоступен — supervisor не может делегировать, весь pipeline встаёт. Если CodeLlama недоступен — coder не работает, но researcher может продолжать.\nKubernetes custom watcher/controller — кастомный контроллер, который следит за состоянием model provider\u0026rsquo;а (health endpoint, метрики) и при сбое переключает на fallback-модель или приостанавливает работу.\nКак сегодня. Gateway API Inference Extensions (раздел 2.4) решает часть этой проблемы — InferencePool с несколькими endpoint\u0026rsquo;ами. Для cross-provider fallback (OpenAI → Anthropic) — кастомная логика в агенте или AI Gateway (LiteLLM, Gloo AI Gateway).\n3.4 Agents to other Agents — связь между агентами CNCF рекомендует: протокол A2A от Google для peer-to-peer взаимодействия агентов, даже в гетерогенных экосистемах.\nЧто это значит. Peer-to-peer (P2P — равноправная сеть, где каждый узел может быть и клиентом, и сервером) взаимодействие агентов. В отличие от MCP (где агент-клиент вызывает инструмент-сервер), A2A предполагает, что оба участника — агенты, со своим reasoning, своим state, своими инструментами. Они могут быть построены на разных фреймворках (LangChain, AutoGen, Eino), на разных языках, в разных кластерах.\nКак сегодня. A2A — open-source протокол под Linux Foundation, разработан Google. Версия v1.0.1 (май 2026), 24.4k звёзд на GitHub. Ключевые концепции:\nAgent Card — JSON-документ с описанием возможностей агента (навыки, поддерживаемые модальности, endpoint). Похож на service discovery в микросервисах. Транспорт: JSON-RPC 2.0 поверх HTTP(S) — текстовый протокол удалённого вызова процедур. Модальности: синхронный request/response, streaming через SSE (Server-Sent Events), асинхронные push-уведомления. Принцип opacity (непрозрачность): агенты взаимодействуют без раскрытия внутреннего state, памяти, логики, инструментов. Это и для безопасности, и для защиты IP. SDK для Python, Go, JavaScript, Java, .NET, Rust. DeepLearning.AI выпустил бесплатный курс по A2A в партнёрстве с Google Cloud и IBM Research.\n3.5 Filtering and schema validation — валидация схем CNCF рекомендует: определять схемы через JSON Schema, Protobuf или OpenAPI для валидации payloads при tool calls и вызовах внешних сервисов. Это повышает предсказуемость и избегает cascading failures (каскадных сбоев — когда отказ одного компонента вызывает отказы других по цепочке).\nЧто это значит. Агент вызывает инструмент send_email(to, subject, body). Без schema validation агент может передать to: null или body: 123 (число вместо строки). Инструмент упадёт, агент получит непонятную ошибку, попробует retry — и так до исчерпания budget.\nСо schema validation инструмент заранее определяет: to — строка, email-формат; subject — строка, max 200 символов; body — строка. Если аргументы не соответствуют — ошибка на этапе валидации, понятная агенту, без вызова инструмента.\nТри технологии схем:\nТехнология Формат Где используется Сильная сторона JSON Schema JSON-документ REST API, MCP Читаемость, повсеместность Protobuf Бинарный + .proto gRPC, внутренние сервисы Производительность, типизация OpenAPI YAML/JSON REST API спецификация Богатая экосистема инструментов Как сегодня. MCP поддерживает schema validation нативно — каждый tool определяет inputSchema как JSON Schema. Для gRPC — protoc генерирует Go-код с типами. Для REST — OpenAPI-спецификация + code generation.\nПример валидации tool call на Go:\ntype ToolValidator struct { schemas map[string]*jsonschema.Schema } func (v *ToolValidator) ValidateCall(toolName string, args json.RawMessage) error { schema, ok := v.schemas[toolName] if !ok { return fmt.Errorf(\u0026#34;неизвестный инструмент: %s\u0026#34;, toolName) } if err := schema.Validate(args); err != nil { return fmt.Errorf(\u0026#34;валидация схемы для %s: %w\u0026#34;, toolName, err) } return nil } 3.6 Protocols today — протоколы сегодня CNCF перечисляет четыре протокола: MCP, A2A, AP2, и упоминает ACP. Разберём каждый подробно.\nMCP (Model Context Protocol) MCP — разработан Anthropic, передан в Agentic AI Foundation (AAIF — фонд поддержки agentic AI проектов под эгидой Linux Foundation). «Де-факто стандарт» для доступа агентов к инструментам и ресурсам, по оценке Yang et al. (2025).\nАрхитектура: client-server. MCP Server предоставляет доступ к инструментам (tools), ресурсам (resources) и промптам (prompts). MCP Client (обычно внутри агента) вызывает их.\nflowchart LR AGENT[\"Agent(MCP Client)\"] --\u003e|\"JSON-RPC 2.0over HTTPS\"| SERVER[\"MCP Server(tool provider)\"] SERVER --\u003e TOOL1[\"Tool: github_search\"] SERVER --\u003e TOOL2[\"Tool: db_query\"] SERVER --\u003e RES[\"Resource: docs\"] style AGENT fill:#2e7d32,color:#fff style SERVER fill:#1565c0,color:#fff Транспорт: JSON-RPC 2.0 (протокол удалённого вызова процедур в формате JSON) поверх HTTPS + streamable HTTP. Версия спецификации: 2025-06-18.\nAuthorization: OAuth 2.1 (стандарт авторизации, где клиент получает access token от authorization server и предъявляет его resource server). MCP Server выступает как OAuth resource server, MCP Client — как OAuth client. PKCE обязателен (Proof Key for Code Exchange — защита от перехвата authorization code). Audience binding через RFC 8707 (Resource Indicators — привязка токена к конкретному ресурсу, чтобы токен для одного MCP-сервера нельзя было использовать с другим). Dynamic Client Registration через RFC 7591.\nКлючевая особенность: star architecture — агент в центре, MCP-серверы по периферии. Yang et al. отмечают: «весь обмен данными должен проходить через центрального агента, что потенциально создаёт узкое место производительности» (bottleneck). Для большинства use cases это не проблема (bottleneck — LLM inference, не IPC), но для high-throughput pipeline\u0026rsquo;ов — учитывать.\nA2A (Agent2Agent) Разобран в разделе 3.4. Peer-to-peer взаимодействие агентов, JSON-RPC 2.0 поверх HTTPS, Agent Card для discovery. Linux Foundation, v1.0.1.\nAP2 (Agent Payments Protocol) AP2 — протокол платежей, совершаемых AI-агентами. Разработан Google. Версия v0.2.0 (апрель 2026), 3.1k звёзд на GitHub.\nЧто это значит. Агент покупает товар, заказывает услугу, оплачивает подписку — autonomously, без человека в цикле. AP2 определяет, как это делать безопасно: криптографически подписанные сертификаты, real-time и delegated operation models.\nКлючевые технологии:\nx402 extensions — расширения протокола x402 для decentralized environments (децентрализованные среды — без центрального посредника) Verifiable Credentials (VC — проверяемые удостоверения, W3C-стандарт: цифровые удостоверения, криптографически подписанные, которые можно проверить без обращения к эмитенту) Decentralized Identifiers (DID — децентрализованные идентификаторы, W3C-стандарт: идентификаторы, не привязанные к центральному реестру, как did:web:example.com или did:ethr:0x...) Зрелость: ранняя. v0.2 — значит API может меняться. Niche use case — агенты-покупатели. Но направление перспективное: по мере роста agent-автономности платежи станут необходимостью.\nACP (Agent Communication Protocol) ACP — разработан IBM Research и BeeAI (комьюнити под Linux Foundation). REST-based, HTTP-native протокол для связи между агентами.\nЧто это значит. В отличие от A2A (JSON-RPC 2.0), ACP использует привычный REST — HTTP-запросы к endpoint\u0026rsquo;ам агентов. Поддерживает синхронные и асинхронные паттерны (async-first, sync supported). Discovery через capability descriptions, объявляемые при сборке (offline discovery — метаданные встраиваются в пакет агента, discovery работает даже когда агент не запущен, что удобно для scale-to-zero окружений). Не требует SDK — можно вызывать через cURL или Postman.\nВажное обновление: ACP официально слит с A2A под Linux Foundation. Команда ACP сворачивает активную разработку и передаёт технологии и экспертизу в A2A. Текущие пользователи ACP должны мигрировать на A2A. Таким образом, ACP и A2A конвергируют — A2A как umbrella-протокол, ACP как один из подходов внутри.\nЗрелость: разработка сворачивается, миграция на A2A. BeeAI (i-am-bee на GitHub) остаётся активным как reference implementation, framework-agnostic SDK для Python и TypeScript.\n3.7 Authentication and Authorization protocols — протоколы идентичности CNCF перечисляет три: SPIFFE/SPIRE, Agntcy, и упоминает OAuth2/OIDC. Разберём подробно.\nSPIFFE / SPIRE SPIFFE (Secure Production Identity Framework For Everyone — «безопасный фреймворк идентичности для всех») и SPIRE (SPIFFE Runtime Environment — среда выполнения SPIFFE) — CNCF Graduate projects с 2022 года. Production-ready, используются в Amazon, Google, Netflix, Uber, Bloomberg, Twilio.\nЧто это значит. В zero-trust архитектуре каждый сервис нуждается в криптографической идентичности — не в статичных API-ключах, а в динамически выдаваемых, короткоживущих удостоверениях. SPIFFE определяет формат такой идентичности, SPIRE — реализацию.\nSVID (SPIFFE Verifiable Identity Document — проверяемый документ идентичности SPIFFE) — криптографическое удостоверение workload. Два формата:\nJWT-SVID — JWT-токен (JSON Web Token) с полем sub (subject) содержащим SPIFFE ID. Подходит для crossing trust domains, проверки через публичный ключ. X.509-SVID — сертификат X.509 с SPIFFE ID в URI SAN (Subject Alternative Name). Подходит для mTLS (mutual TLS — двустороннее TLS), где оба участника аутентифицируют друг друга. SPIFFE ID — URI-формат:\nspiffe://{trust-domain}/{workload-identifier} Например:\nspiffe://prod.example.com/agent/payments/supervisor Trust domain — домен доверия (кластер, организация), внутри которого SVID\u0026rsquo;ы валидны.\nsequenceDiagram participant P as Agent Pod participant SR as SPIRE Agent participant SS as SPIRE Server participant T as Tool Server Note over P: Запуск пода P-\u003e\u003eSR: Workload API(attest via pod UID, namespace, SA) SR-\u003e\u003eSS: Аттестация workload SS--\u003e\u003eSR: Подтверждение + trust bundle SR--\u003e\u003eP: JWT-SVID(TTL=1ч, audience=mcp-tools) Note over P: Первый вызов инструмента P-\u003e\u003eT: Запрос + Bearer SVID T-\u003e\u003eT: Проверка SVID(подпись, audience, TTL) T--\u003e\u003eP: Ответ Note over P: TTL истёк P-\u003e\u003eSR: Новый SVID(авторотация) SR--\u003e\u003eP: Свежий JWT-SVID Federation — доверие между trust domains. SPIRE Server одного домена публикует свой trust bundle (набор публичных ключей), SPIRE Server другого домена его скачивает. Так агенты из разных кластеров могут проверять SVID\u0026rsquo;ы друг друга.\nAgntcy identity framework Agntcy — проект, донированный в Linux Foundation в июле 2025. Сам CNCF-документ признаёт: «still in relatively nascent stages» (в относительно ранних стадиях).\nЧто это значит. BYOI (Bring Your Own Identity — «приноси свою идентичность») — подход, где разные identity providers могут использоваться в одной системе. Не привязка к одному решению (как SPIFFE), а каркас, в который можно подключить разные провайдеры.\nУникальная особенность: поддержка Web3 DID-стандарта (W3C Decentralized Identifiers — децентрализованные идентификаторы, как в блокчейн-экосистеме). Это позволяет агентам иметь идентичность, не привязанную к центральному реестру — полезно для cross-организационного взаимодействия.\nAgent Directory Service (ADS) — реестр агентов с метаданными: возможности, состояние, endpoint\u0026rsquo;ы. Использует OCI registry infrastructure (Open Container Initiative — та же инфраструктура, что для Docker-образов) и криптографическую подпись.\nЗрелость: nascent. Production-деплой преждевременен, но за проектом стоит Linux Foundation, направление перспективное. Complementary к SPIFFE, не замена — SPIFFE для workload identity внутри кластера, Agntcy для cross-org agent discovery.\nOWASP Agent Name Service (ANS) ANS v1.0 — разработан под эгидой OWASP (Open Web Application Security Project — открытый проект безопасности веб-приложений) GenAI Security Project, Agentic Security Initiative.\nЧто это значит. DNS-подобный сервис для обнаружения агентов — но с криптографической проверкой идентичности. Когда агент ищет другого агента по имени, ANS гарантирует, что найденный агент — тот, за кого себя выдаёт.\nТехнологии:\nPKI (Public Key Infrastructure — инфраструктура открытых ключей) — система сертификатов для верификации Structured JSON schemas — структурированные схемы для описания агентов Zero-Knowledge Proofs (ZKP — доказательства с нулевым разглашением: можно доказать, что утверждение истинно, не раскрывая никаких дополнительных данных) — для проверки идентичности и возможностей агента без раскрытия лишнего Защита от атак: agent impersonation (подмена агента — когда злоумышленник выдаёт свой агент за легитимный) и registry poisoning (отравление реестра — когда злоумышленник модифицирует записи в реестре, подменяя endpoint\u0026rsquo;ы).\nЗрелость: research stage. IETF draft, IEEE paper. Protocol-agnostic — поддерживает A2A, MCP, ACP. Перспективно для agent discovery, но в production рано.\n3.8 Message and communication design — проектирование обмена сообщениями CNCF рекомендует: Kafka/Flink для асинхронной event-driven коммуникации, gRPC для streaming, REST для простого request/response. Обращать внимание на влияние streaming data на token limits.\nЧто значит каждый вариант:\nKafka — распределённая event streaming платформа. CNCF упоминает для асинхронной коммуникации (long-running tasks — долгие задачи) и event-driven архитектуры (телеметрия, логи решений, координационные сигналы). Гарантии доставки: at-least-once (хотя бы один раз — сообщение может доставиться дважды, но не потеряется). Flink — stream processing, обработка данных в потоке, на лету.\ngRPC — бинарный RPC-протокол на основе HTTP/2 и Protobuf. Эффективнее REST (бинарный, не текстовый), поддерживает bidirectional streaming. CNCF обращает внимание: при streaming data нужно оценить ELT vs ETL подход (Extract-Load-Transform vs Extract-Transform-Load — порядок загрузки и трансформации данных). И влияние объёма streaming data на token limits агента — если агент получает данные через gRPC-stream, они попадают в context window, и при большом объёме токены заканчиваются.\nREST — простой, interoperable, request/response. Минусы: большие payloads влияют на latency, менее производителен чем gRPC, нет native streaming (нужно для long-lived actions — долгоживущих действий, когда агент ждёт ответа минутами).\nКак сегодня. Kafka — Strimzi (CNCF, Kafka на K8s), Confluent. gRPC — нативно в Go. Для multi-agent event-driven — Kafka как event bus, агенты как consumers/producers.\n3.9 Discovery / agent registries — обнаружение сервисов CNCF рекомендует: DNS-based discovery, service meshes, purpose-built agent registries, static registration для air gap сценариев.\nЧто это значит. Discovery — как агент находит другие агенты и инструменты. Четыре подхода:\nDNS-based — Kubernetes-native DNS (CoreDNS) или service mesh registry. Агент обращается к supervisor-agent.agents.svc.cluster.local, DNS возвращает IP. Просто, но только сетевой адрес — без метаданных.\nService meshes (Istio, Linkerd) — динамический реестр запущенных сервисов с метаданными и endpoint\u0026rsquo;ами. Плюс mTLS, traffic management, observability из коробки.\nPurpose-built agent registries — специализированные реестры агентов и инструментов, хранящие не только endpoint, но и метаданные: возможности, здоровье, статус. Позволяют агентам выбирать оптимальный ресурс во время выполнения.\nStatic registration / multicast — для air gap (изолированные среды без доступа к центральным реестрам). Ручная конфигурация или локальное обнаружение.\nКак сегодня. DNS + service mesh — для K8s-internal discovery. Agntcy Directory Service (раздел 3.7) — emerging purpose-built registry. OWASP ANS — research stage. Для production сейчас: DNS + service mesh, кастомный реестр при необходимости.\nЧто почитать. OWASP Secure Agent Registry, Agntcy Directory, Agent Skills.\n4. Observability — наблюдаемость CNCF расширяет классический MELT-стек на agent-специфичные метрики: токены, TTFT, стоимость, точность. И добавляет trajectory correlation через OpenTelemetry baggage.\n4.1 Metrics — метрики CNCF рекомендует метрики: токены для inference (input/output/reasoning), TTFT/TPOT/ITL, взаимодействия с внешними tool/LLM, длительность выполнения, стоимость, точность (precision), rate limits hits, use-case-специфичные метрики (галлюцинации, success rate).\nЧто значит каждая метрика:\nМетрика Расшифровка Зачем Input tokens Токены на входе модели Стоимость запроса Output tokens Токены на выходе Стоимость ответа Reasoning tokens Токены reasoning (o1-style) Стоимость размышления модели TTFT Time To First Token — время до первого токена Latency для пользователя TPOT Time Per Output Token — время на выходной токен Throughput генерации ITL Inter-Token Latency — задержка между токенами Smoothness streaming Cost Стоимость inference FinOps, budget enforcement Precision Точность ответов Качество модели Rate limit hits Попадания в лимиты API Адаптация архитектуры Token accounting — основа cost governance (управление затратами). Каждый вызов LLM тарифицируется по токенам: input ~$0.01-0.06/1K, output ~$0.03-0.12/1K (зависит от модели). Один ReAct-цикл — 5-20K токенов, multi-agent debate (Part 4) — 50-200K токенов. Без per-trajectory token accounting невозможно понять, сколько стоит один запрос пользователя.\nКак сегодня. OpenTelemetry с gen-ai semantic conventions (experimental, dedicated repo) определяет атрибуты для gen-ai spans: gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.system, gen_ai.request.model. Для rate limits и cost — кастомные метрики поверх.\nПример инструментирования на Go:\nfunc (a *Agent) callLLM(ctx context.Context, messages []Message) (Response, error) { ctx, span := otel.Tracer(\u0026#34;agent\u0026#34;).Start(ctx, \u0026#34;llm.call\u0026#34;, trace.WithAttributes( attribute.String(\u0026#34;gen_ai.system\u0026#34;, \u0026#34;openai\u0026#34;), attribute.String(\u0026#34;gen_ai.request.model\u0026#34;, a.model), attribute.String(\u0026#34;gen_ai.session.id\u0026#34;, a.sessionID), ), ) defer span.End() resp, err := a.client.Complete(ctx, messages) if err != nil { span.RecordError(err) return resp, err } span.SetAttributes( attribute.Int(\u0026#34;gen_ai.usage.input_tokens\u0026#34;, resp.Usage.InputTokens), attribute.Int(\u0026#34;gen_ai.usage.output_tokens\u0026#34;, resp.Usage.OutputTokens), attribute.Float64(\u0026#34;gen_ai.cost.usd\u0026#34;, resp.Cost), ) return resp, nil } 4.2 Traces and spans — трейсы и спаны CNCF рекомендует: custom instrumentation через OpenTelemetry, auto instrumentation в K8s, hooks для per-process executions (SPANs), baggage для propagation идентификаторов (user ID, session ID, context activity, container-ID).\nЧто это значит. Трейс — дерево вызовов, показывающее полный путь запроса через систему. Span — одна операция в дереве. Для агентов один пользовательский запрос порождает дерево: request → supervisor → researcher → web_search → LLM call → coder → code_gen → LLM call → reviewer → LLM call → response. Без трейсов это чёрный ящик.\nflowchart TB REQ[\"Пользовательский запрос\"] --\u003e SP1[\"Span: supervisor.run\"] SP1 --\u003e SP2[\"Span: researcher.run\"] SP2 --\u003e SP3[\"Span: web_search.execute\"] SP2 --\u003e SP4[\"Span: llm.calltokens: 1523 in / 847 out\"] SP1 --\u003e SP5[\"Span: coder.run\"] SP5 --\u003e SP6[\"Span: code_gen.execute\"] SP5 --\u003e SP7[\"Span: llm.calltokens: 2100 in / 3400 out\"] SP1 --\u003e SP8[\"Span: reviewer.run\"] SP8 --\u003e SP9[\"Span: llm.calltokens: 3200 in / 1200 out\"] SP1 --\u003e SP10[\"Span: response.generate\"] style SP4 fill:#1565c0,color:#fff style SP7 fill:#1565c0,color:#fff style SP9 fill:#1565c0,color:#fff Baggage — механизм OpenTelemetry для propagation метаданных через границы сервисов. Когда supervisor-агент вызывает researcher-агента, baggage содержит: trajectory.id (идентификатор траектории — всей последовательности шагов), session.id (идентификатор сессии пользователя), user.id. Это позволяет в Jaeger отфильтровать все спаны одной траектории, даже если они прошли через 5 разных подов.\nКак сегодня. OpenTelemetry GenAI semantic conventions — dedicated repo, 83 star, без официальных релизов (experimental). Определяет span attributes для gen-ai: gen_ai.system, gen_ai.request.model, gen_ai.usage.*, gen_ai.tool.*. OpenTelemetry LLM observability blog — практическое руководство. OTEL baggage — спецификация.\n4.3 Logs — логи CNCF рекомендует: логи в «естественном языке» (natural language — для будущей обработки AI-системами), общесистемное время, структурированный формат данных, canonical logging.\nЧто это значит. «Естественный язык» — необычная рекомендация. CNCF предполагает, что логи будут читаться не только людьми, но и AI-моделями (для автоматического дебага). Формулировка: «authoring of viable and usable logs which are authored with a natural language in mind to allow for future re-usability by co-op AI model based systems for debugging».\nCanonical logging — единый формат логов во всей системе: одинаковые поля, одинаковые значения, одинаковые единицы измерения. Позволяет строить log-based metrics (метрики на основе логов), упрощает post-mortem анализ.\nОбщесистемное время — NTP-синхронизация всех узлов. Без этого логи с разных подов нельзя выстроить в хронологию.\nКак сегодня. Structured logging (JSON) — стандарт. В Go — slog (structured logging, стандартная библиотека с Go 1.21). Для K8s — Loki (CNCF, лог-агрегатор, интеграция с Prometheus и Grafana).\n4.4 Continuous monitoring and adaptive control — мониторинг и адаптация CNCF рекомендует: OTEL с semantic conventions для непрерывного мониторинга agent trajectories, периодическая переоценка для обнаружения drift (дрейфа — постепенного изменения поведения), feedback loops с reinforcement learning для self-correction.\nЧто это значит. Drift — когда поведение агента постепенно меняется со временем. Причины: обновление модели (GPT-4 → GPT-4o), изменение данных в RAG (Retrieval-Augmented Generation — генерация с дополнением извлечёнными данными), накопление episodic memory. Без мониторинга drift незаметен — агент «чуть хуже» работает каждую неделю, пока не станет критичным.\nFeedback loops — агент получает обратную связь (от пользователя, от Agent-as-a-Judge) и адаптирует своё поведение. Reinforcement learning (обучение с подкреплением) — технически это fine-tuning модели на основе reward signal. В production для агентов это сложно (нужен training pipeline), но концептуально CNCF указывает правильное направление.\nКак сегодня. OTEL pipeline → Prometheus → Alerting на основе SLO. Для drift detection — сравнение метрик (success rate, token usage, latency) по временным окнам: эта неделя vs прошлая. Для feedback loops — storing user feedback (thumbs up/down) в Postgres, periodic analysis.\n5. Governance — управление CNCF определяет governance как обязательный foundational слой для agentic-систем. Это не «добавим потом», а «проектируем с начала» — из-за regulatory adherence (соответствие регуляторным требованиям, как EU AI Act).\n5.1 Agentic governance foundations — основы CNCF утверждает: governance — mandatory foundational layer, нужен динамический и гибкий подход (в отличие от статичного software governance), regulatory adherence нужно проектировать с начала.\nЧто это значит. Традиционный software governance — статичные политики: code review, CI/CD checks, access control. Для агентов этого недостаточно: агенты проявляют emergent behavior (эмерджентное поведение — поведение, не предусмотренное явно, но возникающее из взаимодействия компонентов). Нужно continuously мониторить и адаптировать политики.\nРегуляторный контекст: EU AI Act (закон ЕС об ИИ), EU AI Data Act — требуют explainability (объяснимости: способность объяснить, почему принято конкретное решение) и auditability (аудируемости: возможность проверить историю решений). Проектировать с начала — значит: с первого дня хранить audit trail (историю решений), логировать reasoning (ход рассуждений), поддерживать data lineage (происхождение данных — какие данные использовались для решения).\nЧто почитать. Reuel et al. (2024) «Open Problems in Technical AI Governance» — TMLR 2025, 35 авторов включая Bengio, Hooker, Solaiman. Определяет Technical AI Governance через три функции: (a) выявление областей, требующих вмешательства, (b) оценка эффективности действий, (c) разработка механизмов compliance. CNCF ссылается на эту работу как академическое обоснование governance-секции.\n5.2 Evaluation approach — подход к оценке CNCF рекомендует: учитывать специфику use case (конкретного сценария применения), чёткие критерии успеха, полную стоимость использования (вычислительная, финансовая, экологическая, человеческая, на хранение данных), надёжность и устойчивость, безопасность и соответствие ценностям, качество взаимодействия, единые и унифицированные протоколы оценки.\nЧто значит каждая категория оценки:\nКатегория Что измеряет Пример метрики Success rate Доля успешных выполнений 87% задач решено корректно Computational cost Ресурсы CPU-часы, GPU-часы, MB-RAM Financial cost Деньги $0.50 за ReAct-цикл, $15 за multi-agent debate Environmental cost Углерод CO2-след, carbon credits Human cost Человеко-часы Время oversight, setup, калибровки Reliability Стабильность Success rate в edge cases, при adversarial атаках Safety Безопасность Отсутствие harmful outputs, alignment с человеческими ценностями Interaction quality UX Естественность, связность, user-centeredness Standard and uniform evaluation protocols — CNCF подчёркивает: нужны чёткие правила проведения тестов, scoring, конфигурации окружения, чтобы результаты были сравнимы и воспроизводимы. Без этого «87% success rate» из одного теста несравнимо с «87%» из другого.\n5.3 Synthetic data for testing — синтетические данные CNCF рекомендует: diverse, policy-driven синтетические датасеты и fault scenarios, тесты выровнены на real-world scenarios, HITL (Human-in-the-loop — человек в цикле), генерация с entropy (энтропией — случайностью).\nЧто это значит. Синтетические данные — сгенерированные, не реальные. Нужны для тестирования агента: вместо реальных пользовательских запросов (которые могут содержать PII — Personal Identifiable Information, персональные данные) — синтетические, которые покрывают те же сценарии, но без рисков.\nEntropy — случайность в синтетических данных. Если все тестовые запросы похожи друг на друга — агент может «выучить» их (overfit), и тесты перестанут ловить реальные баги. Entropy обеспечивает разнообразие.\nHITL — человек участвует в тестовом цикле: калибрует тест-кейсы, оценивает edge cases, помечает некорректные результаты. Полностью автоматическая оценка недостаточна — особенно для subjective категорий (interaction quality, safety).\n5.4 Granular and trajectory-based assessment — детальная оценка CNCF рекомендует два подхода: stepwise evaluation (постепенная оценка — детальный анализ каждого шага агента), и trajectory-based assessment (оценка по траектории — анализ всей последовательности шагов относительно ожидаемого оптимального пути).\nЧто это значит. Stepwise — агент сделал шаг 1 (вызвал tool X), шаг 2 (получил observation Y), шаг 3 (сделал reasoning Z). Каждый шаг оценивается: правильно ли выбран tool? Правильно ли интерпретирован результат? Правильно ли reasoning? Это помогает найти root cause (корневую причину) ошибок.\nTrajectory-based — сравнение реальной траектории с оптимальной. Агент должен был: query DB → filter results → generate answer. А сделал: web_search → web_search → query DB → filter → generate → filter again. Траектория длиннее оптимальной — агент «блуждал». Это может быть допустимо (агент исследовал), или признаком проблемы (агент не понимает задачу).\nКак сегодня. τ-bench (Yao et al., 2024) — benchmark для tool-agent-user interaction, эмулирует динамические диалоги с domain-specific APIs. Метрика pass^k — reliability over multiple trials: прогоняем K раз, success = все K passed. gpt-4o показывает \u0026lt;50% success rate, pass^8 \u0026lt;25% в retail domain — то есть тот же агент даёт разные результаты на одних и тех же данных. Это важный finding: один прогон недостаточен для оценки.\nSWE-bench (Jimenez et al., 2024) — 2,294 real-world SE problems из GitHub issues, 12 Python репозиториев. На момент публикации Claude 2 решал лишь 1.96%. Сейчас state-of-the-art значительно выше, но benchmark страдает от contamination (загрязнения — когда модель видела тестовые данные при обучении). SWE-bench Verified — revisited version с исправленными annotation errors.\nAgentBench (Liu et al., 2023) — 8 environments, оценивает reasoning + decision-making. Significant disparity: top commercial LLMs сильны, open-source ≤70B — большой gap.\n5.5 Data privacy and minimization — приватность данных CNCF рекомендует: data minimization (минимизация данных — собирать только необходимое), transparent data governance policies, layered security.\nЧто это значит. Data minimization — принцип «не храни то, что тебе не нужно». Агент получает доступ к данным для конкретной задачи — после выполнения задачи, данные не сохраняются в памяти агента. Это и security (меньше данных = меньше риск утечки), и compliance (GDPR, CCPA требуют minimization).\nLayered security — несколько слоёв защиты: NetworkPolicy (сетевая изоляция), RBAC (ролевой доступ), encryption at rest (шифрование на диске), encryption in transit (шифрование при передаче), audit logging (журналирование доступа).\n5.6 Explainability and auditability — объяснимость и аудируемость CNCF рекомендует: Model Openness Framework (MOF) для прозрачной документации, model cards и data cards, криптографическая подпись артефактов через Sigstore, automated auditing через Agent-as-a-Judge.\nModel Openness Framework (MOF) MOF (White et al., 2024) — трёхуровневая классификация моделей по полноте и открытости. Разработан Linux Foundation, University of Oxford, Columbia University, IBM.\nТри класса (по возрастанию полноты):\nКласс Название Что включено Class III Open Model Архитектура, финальные параметры, технический отчёт, результаты оценки, model card, data card Class II Open Tooling Всё из Class III + код обучения/валидации/тестирования, код инференса, код оценки, данные оценки, вспомогательные библиотеки Class I Open Science Всё из Class II + исследовательская работа, наборы данных, код препроцессинга, промежуточные контрольные точки, метаданные Model card — документация возможностей и ограничений модели, характера обучающих данных. Data card — документация наборов данных. Оба — обязательные компоненты Class III (минимальный уровень).\n17 компонентов суммарно, плюс конфигурационный файл mof.json. Self-reporting (самоотчётность) — нет внешнего аудита, продюсеры моделей сами заполняют. Model Openness Tool (MOT) — веб-форма для оценки.\nВажный нюанс: MOF явно исключает model provenance (происхождение модели — криптографическую верификацию целостности артефактов) из scope (раздел 9.2). CNCF композитит MOF (документация) с Sigstore (криптографическая подпись) — это правильная композиция, которой нет ни в одной из работ по отдельности.\nСтатистика для контекста: 64.67% моделей и 72.13% датасетов на Hugging Face Hub не имеют лицензии. OSS (open-source software) используется в 96% кодовых баз и составляет до 90% программных стеков — MOF хочет того же для AI.\nAgent-as-a-Judge CNCF рекомендует: «explore and implement automated evaluation approaches using Agent-as-a-Judge».\nAgent-as-a-Judge (Zhuge et al., 2024, Meta/FAIR) — расширение LLM-as-a-Judge на agentic системы. В отличие от LLM-as-a-Judge (которая оценивает только финальный результат), Agent-as-a-Judge оценивает intermediate feedback на весь task-solving process — использует planning, tool-augmented verification, оценивает каждый шаг.\nLLM-as-a-Judge (Zheng et al., 2023, NeurIPS 2023) — канонический источник. GPT-4 как judge достигает \u0026gt;80% agreement с human preferences — на уровне inter-human agreement. Но имеет известные biases:\nPosition bias — judge предпочитает первый/последний вариант. Mitigation: swapping positions, averaging. Verbosity bias — judge предпочитает более длинные ответы. Mitigation: Length-Controlled AlpacaEval (Dubois et al., 2024) — GLM-regression, «what would preference be if outputs had same length». Spearman correlation с Chatbot Arena поднимается с 0.94 → 0.98. Self-enhancement bias — judge предпочитает ответы, похожие на свои собственные. Mitigation: cross-model judging. Agent-as-a-Judge «dramatically outperforms LLM-as-a-Judge» и as reliable as human baseline — по Zhuge et al. Но Ming et al. (2025) показывают: judge может hallucinate, exhibit bias, act adversarially. Even strongest agents switch correct answers after a single round of misleading feedback — deceptive judge (обманчивый судья). Taxonomy по intent (constructive→malicious) × knowledge (parametric→RAG).\nПрактический вывод: Agent-as-a-Judge — один из сигналов, не единственный. Комбинируется с deterministic rules (schema validation, policy checks) и periodic human review. Использовать с осторожностью, особенно для high-stakes решений.\n5.7 Integrated lifecycle governance — интегрированное управление жизненным циклом CNCF рекомендует: управление (governance) — не разовая акция, а непрерывный процесс на протяжении всего жизненного цикла эксплуатации LLM (LLMOps). Техническая реализация, политические рамки и постоянный надзор должны работать в симбиозе. На уровне агента — повторные попытки (retry) с ограничением числа, предохранители (circuit breakers — автоматическое отключение при сбоях) и постепенная деградация (graceful degradation — переключение на упрощённый режим при отказе основного).\nЧто это значит. Жизненный цикл эксплуатации LLM (LLMOps — практики эксплуатации LLM-систем, аналог DevOps для LLM): подготовка данных → обучение модели → оценка → упаковка → развёртывание → мониторинг → обратная связь → переобучение. Управление (governance) должно присутствовать на каждом этапе, не только в production.\nПовторные попытки и предохранители на уровне агента — в отличие от обычных микросервисов (где повтор обычно безопасен), повтор для агентов может быть дорогим (каждый повтор — вызов LLM = токены = деньги). Предохранитель (circuit breaker) — если N последовательных вызовов упали, перестать пытаться и вернуть запасной вариант (fallback). Постепенная деградация (graceful degradation) — если основной агент недоступен, переключиться на упрощённый.\n6. Security — безопасность CNCF определяет три цели: authentication (аутентификация — проверка, кто ты), authorization (авторизация — проверка, что тебе можно), trust (доверие). И три области: agent identity, tenancy, data access.\n6.1 Agent identity — идентичность агента CNCF рекомендует: назначать каждому агенту уникальную идентичность нагрузки (workload identity), короткоживущие автоматически обновляемые учётные данные, аудит и логирование использования идентичности, проверку идентичности перед каждым действием, границы принуждения (service meshes — сервисные сетки, NetworkPolicy — сетевые политики, API gateways — API-шлюзы), безопасное именование (OWASP ANS).\nКогда user identity alone, когда agent identity?\nCNCF даёт чёткое различение:\nflowchart TB START[\"Какая идентичность нужна?\"] --\u003e Q1{\"Агент живёт толькопока user logged in?\"} Q1 --\u003e|\"Да\"| USER[\"User identity aloneАгент наследуетправа пользователя\"] Q1 --\u003e|\"Нет\"| Q2{\"Агент делаетautonomous decisions?\"} Q2 --\u003e|\"Да\"| AGENT[\"Agent identity requiredСобственная идентичность+ own permissions\"] Q2 --\u003e|\"Нет\"| Q3{\"Действия за пределамиправ пользователя?\"} Q3 --\u003e|\"Да\"| AGENT Q3 --\u003e|\"Нет\"| USER style USER fill:#2e7d32,color:#fff style AGENT fill:#c62828,color:#fff User identity alone: short-lived, user-initiated tasks. Агент живёт пока пользователь залогинен. Пример: «объясни этот код» — агент использует права пользователя, никаких autonomous actions. Когда пользователь logout — агент прекращает работу.\nТребуется идентичность агента: автономные решения (запуск рабочих процессов, вызовы API, размещение заказов), сохраняется после завершения сессии пользователя, межотдельческий доступ (доступ к данным других отделов). Пример: «разверни этот сервис в staging» — агент должен иметь права на развёртывание, не наследуемые от пользователя (который может не иметь прав на deploy).\nПрактики agent identity:\nUnique workload identity — каждый agent instance получает уникальный SVID (SPIFFE, раздел 3.7). Не shared service accounts с broad permissions. Scoped, ephemeral, least privileged. Short-lived credentials — OIDC tokens с TTL, SPIFFE SVID certificates, ephemeral API credentials. Время жизни привязано к lifetime агента: агент умер — креды истекли. Audit and log — какой агент какую идентичность использовал, когда, для какой цели. Append-only logs (логи только для добавления — нельзя изменить или удалить запись) для non-repudiation (неотрицаемости — невозможности отрицать действие). Verify before each action — re-authenticate и re-authorize mid-session для чувствительных действий. Не «проверил при старте и забыл». Enforcement boundaries — service meshes (mTLS, identity-aware routing), NetworkPolicy (агенты общаются только с авторизованными tools/services), API gateways. Layered defense против lateral movement (перемещения злоумышленника по сети). Secure naming — OWASP ANS (раздел 3.7) для криптографически верифицируемого discovery и naming. MCP Authorization. MCP specification 2025-06-18 определяет OAuth 2.1 flow: MCP server как resource server, MCP client как OAuth client. PKCE обязателен. Audience binding через RFC 8707. Token — Bearer в каждом request. Это для user-scoped tool access — в дополнение к SVID (который для service-to-service).\n6.2 Agent tenancy — тенантство CNCF рекомендует: JIT access provisioning, PoLP, ABAC/PBAC, strict workload partitioning (namespace, container, network, hardware isolation).\nЧто значит.\nJIT (Just-in-Time access — доступ «точно в срок») — права выдаются на время конкретной задачи, после выполнения — отзываются. Агент не имеет постоянных прав, только ephemeral (короткоживущие).\nPoLP (Principle of Least Privilege — принцип минимальных привилегий) — агент получает минимальные необходимые права. CNCF особенно подчёркивает: «just in case» permissions («на всякий случай») опасны для агентов, потому что агенты сконструированы исследовать варианты (designed to explore options). Если у агента есть лишнее разрешение «на всякий случай» — он может его использовать.\nABAC (Attribute-Based Access Control — доступ на основе атрибутов) — права определяются атрибутами: кто (agent identity), что (resource type), когда (time of day), откуда (network location), зачем (task context). Более гибкий чем RBAC (Role-Based — по ролям).\nPBAC (Policy-Based Access Control — доступ на основе политик) — права определяются политиками, описанными в декларативном виде. OPA/Rego — пример.\nIsolation patterns:\nNamespace separation — разные агенты в разных K8s namespaces Container isolation — разные контейнеры, желательно с sandboxing (gVisor, Kata Containers) Network segmentation — NetworkPolicy, ограничивающая трафик Hardware partitioning — особенно GPU: MIG (Multi-Instance GPU, раздел 2.4) для изоляции Service mesh capabilities — mTLS (двустороннее TLS), identity-aware routing (маршрутизация с учётом идентичности), authorization policies (политики авторизации). Istio/Linkerd предоставляют это из коробки.\n6.3 Agent data access — доступ к данным CNCF рекомендует: ограниченный доступ к хранилищам данных, защиту от prompt injection, ограничение вызова инструментов, нулевое доверие (zero-trust) для внутренних API, изолированные среды выполнения (TEE — доверительные среды выполнения, secure enclaves — защищённые анклавы, GPU confidential computing — конфиденциальные вычисления на GPU), защиту среды выполнения агента (утечка системного промпта, доступ к исходному коду).\nЗащита от prompt injection Что значит. Prompt injection (внедрение промпта — атака, при которой злоумышленник внедряет вредоносную инструкцию в данные, которые LLM обрабатывает) — экзистенциальная угроза для агентных систем. Агент, который может вызывать инструменты и принимать автономные решения — главная мишень.\nGreshke et al. (2023) — семинальная работа: косвенное внедрение промпта (indirect prompt injection), демонстрация атак на Bing Chat/GPT-4. Обработка полученных данных равносильна «произвольному выполнению кода» — то есть обработка результатов (например, web_search) эквивалентна выполнению произвольного кода.\nCNCF рекомендует в общем виде: «строгая валидация и очистка входных данных, контекстно-зависимые шаблоны промптов и защитные ограждения (guardrails), мониторинг и обнаружение аномалий». Но конкретные техники эшелонированной защиты (defence-in-depth — несколько слоёв, чтобы пробить один было недостаточно):\nflowchart LR INPUT[\"Tool Output(untrusted)\"] --\u003e L1[\"Layer 1: Input Screeningrule-based patterns+ anomaly classifier\"] L1 --\u003e L2[\"Layer 2: Instruction Hierarchysystem above user above tool output\"] L2 --\u003e L3[\"Layer 3: Output Sanitizationvalidate LLM outputbefore tool execution\"] L3 --\u003e L4[\"Layer 4: Tool Sandboxingpre-approved tools only+ audit log\"] L4 --\u003e SAFE[\"Safe Execution\"] style L1 fill:#c62828,color:#fff style L2 fill:#e65100,color:#fff style L3 fill:#f57f17,color:#000 style L4 fill:#2e7d32,color:#fff Layer 1: Input Screening. Saleem et al. (2026) — layered defense framework: rule-based patterns (фильтрация известных атак) + fine-tuned anomaly classifier (ML-модель для обнаружения аномалий). Attack Success Rate (ASR — доля успешных атак) снижен с 71.4% → 11.3%, median latency overhead 61.2 ms.\nLayer 2: Instruction Hierarchy. Wallace et al. (OpenAI, 2024) — «The Instruction Hierarchy»: system \u0026gt; user \u0026gt; tool output. LLM обучается selectively ignore lower-privileged instructions. «Drastically increases robustness» даже для unseen attack types, с минимальной деградацией capabilities. Это каноническая техника: модель знает, что instructions в tool output — lowest priority, и может их игнорировать.\nLayer 3: Output Sanitization. OWASP LLM Top 10: LLM02 — Insecure Output Handling. Валидация LLM output перед передачей в tools. Schema validation (раздел 3.5) — если output не соответствует ожидаемой схеме, reject.\nLayer 4: Tool Sandboxing. Pre-approved tools only (allowlist — белый список, не blacklist — чёрный). Audit log всех tool executions. Restricted permissions: каждый tool имеет минимальные права.\nISE (Instructional Segment Embedding, ICLR 2025) — +18.68% robust accuracy на Instruction Hierarchy benchmark. RETA — per-attack ASR \u0026lt;10%, average ASR 2.92%.\nTool hijacking prevention Что значит. Tool hijacking (перехват инструмента — злоумышленник получает контроль над tool\u0026rsquo;ом агента, заставляя его возвращать вредоносные данные). CNCF рекомендует: strict permission boundaries на tool invocation (только authorized tools), только pre-approved tool interfaces, audit и log всех tool execution requests.\nВ практике это значит: агент имеет allowlist инструментов (не может вызвать любой tool, только одобренные). Каждый tool call логируется: кто вызвал, когда, с какими аргументами, какой результат. Если tool начинает возвращать аномальные данные (например, web_search вдруг возвращает injection-payloads) — alert.\nZero-trust для internal APIs CNCF рекомендует: limit API surface area, segregate APIs по agent roles/tasks, network segmentation, firewall rules, continuously monitor API usage, mTLS для всех inter-service и agent-tool communications.\nЧто значит. Internal APIs (внутренние API — сервисы внутри кластера, не exposed наружу) часто считаются «безопасными по умолчанию». В zero-trust — нет. Каждый API аутентифицирует и авторизует каждый запрос, даже от «своих» сервисов. mTLS (mutual TLS — двустороннее TLS, где обе стороны проверяют сертификаты друг друга) — обязательный.\nIsolated runtime environments — TEEs CNCF рекомендует: deploy agents в изолированных runtime-окружениях, hardware-based isolation: TEEs, secure enclaves, GPU-based confidential computing. Минимизировать system prompt leakage, restrict access к source code.\nЧто значит. TEE (Trusted Execution Environment — доверительная среда выполнения) — аппаратно-изолированная область процессора, где код выполняется с гарантиями конфиденциальности и целостности. Даже администратор хоста не может прочитать данные внутри TEE. Secure enclave — разновидность TEE (Intel SGX, AMD SEV-SNP, ARM CCA).\nGPU-based confidential computing — NVIDIA H100/Blackwell поддерживают CC mode: GPU execution, memory, register states изолированы. Модели, training data, inference prompts защищены.\nКогда оправданы. PipeLLM (Tan et al., ASPLOS 2025) измеряет: naive H100 CC = 52.8% throughput drop (30B model), 88.2% (66B). С PipeLLM optimization — \u0026lt;19.6%. Multi-GPU training с TEE — 8-41× runtime overhead (Lee et al., 2025).\nОправданы для: multi-tenant inference с sensitive prompts (Apple Private Cloud Compute — canonical example), model IP protection, regulated industries (healthcare/finance). Не оправданы для: internal tooling, non-sensitive workloads, когда cost \u0026gt; compliance benefit.\nSide-channel caveat: OTRO демонстрирует end-to-end recovery of user prompts из tokenizer access patterns на Intel TDX. TEE ≠ full protection. Defence-in-depth, не silver bullet.\nConfidential Containers — CNCF Sandbox project, pod-level CC для K8s. Стандартизирует CC на уровне пода, vendor-neutral.\nЧто почитать. AMD EPYC Confidential Computing, NVIDIA GPU Operator Confidential Containers.\nProtect agent execution environment CNCF рекомендует: minimize system prompt leakage (не экспонировать system prompts через APIs, logs, client-side code), restrict access к source code и runtime binaries, redact sensitive flows.\nЧто значит. System prompt (системный промпт — инструкция, задающая поведение агента) — часто содержит proprietary logic, business rules, security constraints. Если злоумышленник получит system prompt — он понимает, как агент принимает решения, и может искать уязвимости.\nПрактики:\nНе логировать system prompts целиком (redact, логировать только hash) Не ship readable scripts (использовать compiled artifacts, signed containers, encrypted packages) Context-scoped prompts (разные prompts для разных контекстов, не один monolithic) Prompt moderation/transformation слой до/после LLM (дополнительная фильтрация) 7. Заключение CNCF «Cloud Native Agentic Standards» — карта территории agentic-систем в Kubernetes. Документ охватывает пять областей: от контейнерных основ до TEEs, от MCP до OWASP ANS. Написан плотно — каждый пункт одна-две фразы, за которыми стоит много контекста.\nВ этой статье я разобрал каждый пункт подробно. Что CNCF рекомендует, что это значит на практике, как реализовано сегодня, куда читать глубже. Главные выводы:\nДокумент — хороший фундамент. Zero-trust как базис, MELT для наблюдаемости, SPIFFE для идентичности, MCP для инструментов — правильные технологические выборы. Production-ready сегодня.\nПротоколы — в активной разработке. MCP — де-факто стандарт (передан в AAIF). A2A — v1.0.1 под Linux Foundation, перспективен для cross-framework коммуникации. AP2 — ранняя стадия (v0.2), niche use case. ACP — конвергирует с A2A. Выбирайте MCP для tool access, A2A для agent-to-agent, остальное — watch.\nИдентичность — три варианта с разной зрелостью. SPIFFE/SPIRE — CNCF Graduate, готово для production (Amazon, Netflix, Uber). Agntcy — ранняя стадия (nascent), Linux Foundation, BYOI («принеси свою идентичность») с Web3 DID (децентрализованные идентификаторы). OWASP ANS — стадия исследования, ZKP (доказательства с нулевым разглашением). Для production сегодня: SPIFFE. Agntcy и ANS — следить.\nУправление (governance) — обязательно с начала. EU AI Act требует объяснимости (explainability) и аудируемости (auditability). Agent-as-a-Judge (агент-судья) — перспективен, но имеет предвзятости (position bias — позиционная, verbosity bias — к многословию, self-enhancement bias — к собственным ответам) и риск обманчивого судьи (deceptive judge). Комбинировать с детерминированными правилами и человеческой проверкой. MOF (Model Openness Framework) + Sigstore — правильная композиция для документации модели и проверки целостности.\nБезопасность — эшелонированная защита (defence-in-depth). Prompt injection (внедрение промпта) — экзистенциальная угроза. Четыре слоя: фильтрация входных данных (input screening), иерархия инструкций (instruction hierarchy), очистка выходных данных (output sanitization), песочница для инструментов (tool sandboxing). TEE (доверительные среды выполнения) — для нагрузок, обусловленных compliance (соответствием требованиям), но накладные расходы 52-88% (без оптимизации) или \u0026lt;19.6% (с оптимизацией). Не серебряная пуля — побочные каналы утечки (side-channels) существуют.\nСерия «AI Agent Design Patterns» прошла путь от ReAct (Part 1) до cloud-native standards (Part 7). Parts 1-6 — паттерны внутри агента. Part 7 — паттерны эксплуатации агентов в Kubernetes. Следующий уровень абстракции — но принцип тот же: deep dive, honest limitations, code examples.\nЧто почитать дополнительно:\nCNCF «Cloud Native Agentic Standards» — первоисточник Yang et al. (2025) «A Survey of AI Agent Protocols» — таксономия 16 протоколов White et al. (2024) «Model Openness Framework» — 3 класса, 17 компонентов Reuel et al. (2024) «Open Problems in Technical AI Governance» — TMLR 2025, 35 авторов Zheng et al. (2023) «Judging LLM-as-a-Judge» — biases, MT-bench, Chatbot Arena Zhuge et al. (2024) «Agent-as-a-Judge» — extension на agentic systems Wallace et al. (2024) «Instruction Hierarchy» — OpenAI, system \u0026gt; user \u0026gt; tool output Greshke et al. (2023) «Indirect Prompt Injection» — семинальная работа по prompt injection Saleem et al. (2026) «Layered Defense Framework» — ASR 71.4% → 11.3% Khan (2026) «63 LLM-Agent Budget-Overrun Incidents» — каталог production инцидентов Jimenez et al. (2024) «SWE-bench» — 2,294 SE problems Yao et al. (2024) «τ-bench» — pass^k metric Liu et al. (2023) «AgentBench» — 8 environments Tan et al. (ASPLOS 2025) «PipeLLM» — TEE overhead numbers ","permalink":"https://triumphpc.github.io/blog/ru/posts/ai-agent-design-patterns-7-cloud-native-standards/","summary":"Подробный разбор CNCF \u0026lsquo;Cloud Native Agentic Standards\u0026rsquo; (март 2026): пять разделов документа — General, Control and Communication, Observability, Governance, Security — пункт за пунктом. Что значит каждая рекомендация, как реализована сегодня в Kubernetes-экосистеме, куда копать глубже. От контейнерных основ до OWASP Agent Name Service, от MCP до Trusted Execution Environments.","title":"AI Agent Design Patterns. Part 7: Cloud-Native Standards"},{"content":"Когда ты пишешь mu.Lock() в Go, ты, скорее всего, думаешь «блокировка». Когда пишешь atomic.AddInt64(\u0026amp;x, 1) — «атомарный инкремент». Два разных инструмента, две разные ментальные модели, два разных раздела документации стандартной библиотеки. Но вот странная вещь: это одно и то же. Точнее — это один стек абстракций, где каждый слой построен поверх предыдущего.\nВ этой статье я пройду по всему стеку — от самого низа до верха и обратно. Мы начнём со сломанного counter++, спустимся на уровень инструкций процессора, поднимемся обратно через CAS (compare-and-swap, сравнение с обменом), атомарные операции, futex (fast userspace mutex, «быстрый мьютекс пространства пользователя») и наконец придём к sync.Mutex — и обнаружим, что это в основном оптимизация поверх атомарного CAS плюс механизм честности, придуманный для исправления реального продакшен-бага из 2015 года.\nСтек Прежде чем нырять, вот картина, которую я хочу, чтобы ты держал в голове. Пять слоёв, каждый построен поверх предыдущего:\ngraph TB L5[\"Уровень 5: Что когда использовать\nБенчмарки, шпаргалка, ловушки\"] L4[\"Уровень 4: sync.Mutex\nstate, sema, spinning, starvation\"] L3[\"Уровень 3: Futex\nuserspace fast path + kernel slow path\"] L2[\"Уровень 2: Атомарные операции\nCAS, Load, Store, Add — sync/atomic\"] L1[\"Уровень 1: Железо\nLOCK CMPXCHG (x86), LDADD/CAS (ARM LSE)\"] L0[\"Уровень 0: Проблема\ndata race, x++ это 3 инструкции\"] L0 --\u003e L1 --\u003e L2 --\u003e L3 --\u003e L4 --\u003e L5 style L0 fill:#c62828,color:#fff style L1 fill:#ad1457,color:#fff style L2 fill:#6a1b9a,color:#fff style L3 fill:#4527a0,color:#fff style L4 fill:#283593,color:#fff style L5 fill:#1565c0,color:#fff Главное утверждение этой статьи: нельзя по-настоящему понять уровень 4 (sync.Mutex), не поняв уровень 2 (атомарные операции), и нельзя понять уровень 2, не взглянув хотя бы краем глаза на уровень 1 (железо). И наоборот — единственная причина интересоваться уровнем 1 в том, что он порождает практические рекомендации уровня 5.\nПоехали снизу.\n1. Проблема: почему x++ ломается Вот простейший возможный data race (состояние гонки). Тысяча горутин инкрементирует общий счётчик:\nvar counter int64 var wg sync.WaitGroup func main() { for i := 0; i \u0026lt; 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter++ }() } wg.Wait() fmt.Println(counter) } Запусти. Ответ не 1000. Что-то вроде 987, или 993, или 971. Иногда ровно 1000 — и это хуже, потому что значит баг прячется.\nПочему? Потому что counter++ — не одна операция. В исходнике Go это выглядит как одна операция, но компилятор разворачивает её в три. Вот реальный ассемблер ARM64, который Go генерирует для counter++:\nMOVD main.counter(SB), R0 ; загрузить counter из памяти в регистр R0 ADD $1, R0, R0 ; прибавить 1 к R0 MOVD R0, main.counter(SB) ; сохранить R0 обратно в память Три инструкции: загрузка, модификация, сохранение. Каждая по отдельности атомарна, но последовательность — нет. Между загрузкой и сохранением может вклиниться другая горутина.\nВот таймлайн столкновения двух горутин:\nsequenceDiagram participant G1 as Горутина 1 participant Mem as Память (counter=5) participant G2 as Горутина 2 G1-\u003e\u003eMem: MOVD counter → R0 (R0=5) Note over G1: сейчас будет ADD G2-\u003e\u003eMem: MOVD counter → R0 (R0=5) G1-\u003e\u003eG1: ADD → R0=6 G1-\u003e\u003eMem: MOVD R0 → counter (counter=6) G2-\u003e\u003eG2: ADD → R0=6 G2-\u003e\u003eMem: MOVD R0 → counter (counter=6) Note over Mem: Один инкремент потерян Обе горутины прочитали 5, обе вычислили 6, обе сохранили 6. Мы сделали два инкремента, счётчик вырос на единицу. Умножь на тысячу горутин — получишь 987.\nЭто data race, и модель памяти Go (go.dev/ref/mem) определяет его формально. Read-write data race для памяти в локации x состоит из операции чтения r и операции записи w над x, хотя бы одна из которых не является синхронизирующей, и при этом ни одна не происходит-до (happens-before) другой. Перечитай это дважды: определение про порядок, а не про время. Две операции над одной локацией без синхронизации между ними — это гонка, даже если на практике они почти никогда не перекрываются во времени.\n1.1. DRF-SC: обещание и цена Модель памяти Go даёт одно большое обещание, называемое DRF-SC (data-race-free → sequentially consistent). Если программа свободна от data race, её выполнение эквивалентно некоторому последовательно-согласованному (sequentially consistent) чередованию горутин — как если бы все они мультиплексировались на один процессор по очереди. Не нужно беспокоиться о переупорядочивании компилятора, store buffers (буферах записи), эффектах кэша и прочей аппаратной странности, которая мучает программистов на C++.\nЦена в том, что тебе действительно нужно устранить все data race. Детектор гонок Go (go test -race, построенный на ThreadSanitizer) поймает их на этапе выполнения, но код без гонок нужно писать изначально. А для этого нужна синхронизация.\nФормальная модель под DRF-SC — та же, что в C++, Java, JavaScript, Rust и Swift. Она происходит из статьи Ханса-Й. Бёма и Сариты В. Адве \u0026ldquo;Foundations of the C++ Concurrency Memory Model\u0026rdquo; (PLDI 2008). Go привёл свою модель памяти в соответствие с этой формализацией в редакции от июня 2022 года, которая вышла вместе с Go 1.19.\n1.2. Happens-before: отношение, на котором всё держится Формальное определение data race опирается на отношение happens-before (происходит-до). Неформально: операция A происходит-до операции B, если можно доказать, что A гарантированно завершилась до того, как B началась. Отношение — транзитивное замыкание двух вещей:\nSequenced-before (следует-после в последовательности): внутри одной горутины операторы выполняются в порядке исходника. Оператор 5 следует-после оператора 4, оператор 6 — после 5. Synchronized-before (синхронизировано-до): между горутинами определённые операции создают рёбра синхронизации. ch \u0026lt;- x происходит-до соответствующего \u0026lt;-ch. mu.Unlock() происходит-до следующего mu.Lock(). Атомарная запись происходит-до атомарного чтения, которое наблюдает записанное значение. Если A происходит-до B, то всё, что A записал в память, видимо для B. Если ни одно не происходит-до другого — у тебя гонка.\nВот почему counter++ сломан: в программе нет ничего, что устанавливало бы happens-before отношение между инкрементами разных горутин. Они не упорядочены, поэтому чтение в горутине A может перекрыть запись в горутине B.\nЧтобы исправить гонку, нужно добавить синхронизацию. Есть два основных пути:\nСделать сам инкремент атомарным — единой операцией, которую нельзя прервать. Обернуть инкремент в мьютекс, чтобы только одна горутина могла выполнять его за раз. Это уровни 2 и 4 в нашем стеке. Рассмотрим их по порядку.\n2. Первый путь: атомарные операции Самое чистое исправление для counter++ — сделать его атомарным:\nvar counter int64 // ... atomic.AddInt64(\u0026amp;counter, 1) atomic.AddInt64 выполняет загрузку, сложение и сохранение как одну неделимую операцию. Ни одна другая горутина не может наблюдать промежуточное состояние, ни одна не может вклиниться между загрузкой и сохранением — потому что «между» просто нет.\nКак это возможно? Потому что под капотом atomic.AddInt64 компилируется в одну инструкцию процессора.\n2.1. CAS: универсальный примитив Фундаментальный строительный блок всех атомарных операций — CAS (Compare-And-Swap, сравнение с обменом). Его сигнатура в псевдокоде:\nCAS(addr, expected, new) → bool если *addr == expected: *addr = new вернуть true иначе: вернуть false Всё это атомарно: сравнение и сохранение происходят как один неделимый шаг. Если *addr равно expected, новое значение записывается и CAS возвращает true. Иначе ничего не записывается и CAS возвращает false.\nCAS особенный. В 1991 году Морис Херлихи опубликовал статью \u0026ldquo;Wait-Free Synchronization\u0026rdquo; в ACM Transactions on Programming Languages and Systems (Транзакции ACM по языкам и системам программирования). Он классифицировал примитивы синхронизации по их consensus number (числу консенсуса) — максимальному числу потоков, для которых примитив может решить задачу консенсуса (все потоки соглашаются на одном значении). Иерархия безжалостна:\nПримитив Consensus number Чтение/запись регистров 1 Test-and-set 2 Fetch-and-add 2 Очередь, стек 2 CAS ∞ У CAS consensus number ∞. Это делает его универсальным примитивом: любую wait-free или lock-free структуру данных можно построить поверх CAS. Остальные — нет. Test-and-set решает консенсус для 2 потоков, fetch-and-add для 2, но только CAS масштабируется на произвольное число. Вот почему каждая современная архитектура предоставляет CAS в железе, и почему Go строит всё остальное поверх него.\nЧто это вообще значит, «consensus number»? Задача консенсуса: N потоков начинают, у каждого есть своё входное значение v_i. Нужно, чтобы все потоки в итоге согласились на одном и том же значении — причём это значение должно быть равно v_i хотя бы для одного из потоков (то есть нельзя просто всегда возвращать константу). Это абстрактная модель для «кто-то предложил, остальные согласились» — то, на чём строятся выборы лидера, распределённые блокировки, реплицированные конечные автоматы.\nХерлихи доказал: если у примитива consensus number равен N, то с его помощью можно решить консенсус для N потоков, но не больше. Test-and-set может решить для 2 — потому что ровно один поток увидит «старое» значение и поймёт, что выиграл; но для 3 потоков уже не получается. А CAS может для любого N — каждый поток CASает свою пару (proposal, my_value) в общую ячейку, и тот, чей CAS успешен, становится выбранным значением.\nПрактическое следствие: невозможно построить wait-free очередь или стек для произвольного числа потоков, используя только fetch-and-add. Нужен либо CAS, либо какой-то другой примитив с consensus number ∞. Поэтому все современные CPU предоставляют CAS — без него нельзя реализоватьscalабельные конкурентные структуры.\n2.2. CAS-loop: как на самом деле реализован Add Можешь задаться вопросом: если CAS — универсальный примитив, как из него построить Add? Нельзя «CAS-нуть и прибавить» одним действием, потому что CAS записывает только конкретное новое значение, а не «старое плюс один». Ответ — цикл:\nfunc addInt64(addr *int64, delta int64) int64 { for { old := atomic.LoadInt64(addr) new := old + delta if atomic.CompareAndSwapInt64(addr, old, new) { return new } // CAS провалился — кто-то записал между нашей загрузкой и CAS. // Повторяем цикл, перечитываем текущее значение, пробуем снова. } } Это называется CAS-loop (цикл с CAS) или compare-and-swap retry loop (цикл повторных попыток compare-and-swap). Загружаем текущее значение, вычисляем новое, пытаемся CAS. Если CAS провалился — значит, другая горутина модифицировала память между нашей загрузкой и CAS, поэтому повторяем с новым текущим значением.\nflowchart TD Start([Прибавить delta к addr]) --\u003e Load[\"old = *addr\"] Load --\u003e Compute[\"new = old + delta\"] Compute --\u003e CAS{\"CAS(addr, old, new)\"} CAS --\u003e|успех| Done([готово]) CAS --\u003e|неудача — кто-то записал| Load style Done fill:#2e7d32,color:#fff style CAS fill:#1565c0,color:#fff Вот что делает CAS универсальным. Из CAS можно построить Add, Subtract, Exchange, Min, Max — любую операцию. Все они становятся атомарными за счёт повторения до успешного CAS.\nЕсть цена. Под тяжёлой нагрузкой (много горутин одновременно пытаются обновить один адрес) CAS-loop может тратить CPU впустую. Каждая неудачная CAS — потраченная попытка, и цикл крутится, пока не выиграет. Мы вернёмся к этому в разделе 4.\n2.3. Железо: LOCK CMPXCHG и ARM LSE Когда ты пишешь atomic.CompareAndSwapInt64, компилятор Go генерирует одну инструкцию процессора. На x86 — это LOCK CMPXCHG:\nlock cmpxchg [addr], new Префикс LOCK важен. В ранних процессорах x86 LOCK буквально активировал аппаратный пин LOCK#, который замораживал всю шину памяти на время инструкции. Это было катастрофически дорого. В современных x86 (Pentium Pro и новее) LOCK работает умнее: если целевой адрес помещается в одну строку кэша (cache line), процессор использует блокировку строки кэша — инвалидирует эту одну строку на всех остальных ядрах и выполняет операцию, не трогая шину. И только если операция跨越 две строки кэша (чего не должно быть при правильном выравнивании), он откатывается к старой блокировке шины.\nНа ARM64 эволюция прошла в два этапа. До LSE (Large System Extensions, до ARMv8.1) CAS эмулировался парой инструкций LDXR/STXR (exclusive load и exclusive store, эксклюзивная загрузка и сохранение). Загрузка помечала строку кэша для мониторинга; если ни одно другое ядро не записало в неё до момента сохранения, сохранение успешно. Иначе — провал и повтор. Это в точности CAS-loop, но в железе.\nARMv8.1 добавил LSE (Large System Extensions, расширения для больших систем), который ввёл одноинструкционные атомарные операции: CAS, LDADD, LDCLR, STSET и компанию. Они быстрее, потому что нет цикла повторов внутри CPU — операция завершается за один раз. Apple Silicon (M1 и новее), AWS Graviton 3 и 4 — все поддерживают LSE, поэтому атомарно-тяжёлый код на этих чипах намного быстрее, чем в до-LSE эру.\nГлавный вывод: каждый вызов atomic.X в Go — это одна инструкция CPU. Никакого вызова функции, никакого цикла (если CAS не провалился), никакого syscall. Просто одна инструкция. Это и делает атомарные операции быстрыми.\n2.4. Пакет sync/atomic в Go Пакет sync/atomic предлагает два стиля API.\nФункции (исходная форма, с Go 1):\nvar x int64 atomic.LoadInt64(\u0026amp;x) atomic.StoreInt64(\u0026amp;x, 42) atomic.AddInt64(\u0026amp;x, 1) atomic.SwapInt64(\u0026amp;x, 99) atomic.CompareAndSwapInt64(\u0026amp;x, expected, new) Плюс эквиваленты для int32, uint32, uint64, uintptr, unsafe.Pointer и особый случай atomic.Value для произвольных типов.\nТипы (введены в Go 1.19, август 2022):\nvar x atomic.Int64 x.Load() x.Store(42) x.Add(1) x.Swap(99) x.CompareAndSwap(expected, new) var p atomic.Pointer[Config] // обобщённый, типобезопасный var f atomic.Bool var u atomic.Uint64 var flags atomic.Uint32 Типы новее и приятнее. Они оборачивают те же инструкции процессора, но с тремя преимуществами:\nБольше никакого \u0026amp;x — ты вызываешь методы прямо у значения, что проясняет владение. Никаких проблем с выравниванием — см. следующий раздел. Типобезопасность указателей — atomic.Pointer[T] обобщённый, без кастинга unsafe.Pointer. Release notes к Go 1.19 (go.dev/doc/go1.19) описывают эти типы как прямое следствие ревизии модели памяти: «Вместе с обновлением модели памяти Go 1.19 вводит новые типы в пакете sync/atomic, которые упрощают использование атомарных значений, такие как atomic.Int64 и atomic.Pointer[T]». До 1.19 на 32-битных платформах (GOARCH=386, GOARCH=arm) int64 должен был быть выровнен по 8-байтовой границе для атомарных операций — иначе atomic.LoadInt64 паниковал в рантайме. Нужно было вручную располагать поля структуры или добавлять padding. Новый тип atomic.Int64 гарантирует выравнивание автоматически, что убивает целый класс багов.\n2.5. Memory ordering: в Go только один режим Это одно из главных отличий Go от C++/Rust. C++11 ввёл шесть упорядочиваний памяти: memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst. Rust наследует те же шесть. Более слабые упорядочивания (relaxed, acquire, release) позволяют компилятору и CPU агрессивнее переупорядочивать операции, что может дать производительность ценой более аккуратного рассуждения.\nВ Go — одно. Sequentially consistent (последовательная согласованность), всегда. Каждая атомарная операция в Go — полный барьер памяти. Нет atomic.LoadAcquire, нет atomic.StoreRelease. Как пояснял Рассел Кокс, пересматривая модель памяти для Go 1.19: Go намеренно не предоставляет расслабленные упорядочивания, потому что их слишком легко применить неправильно, а прирост производительности обычно невелик.\nЭто тот же компромисс, что и на уровне 1: проще модель — меньше производительность — меньше багов. Если ты приходишь из C++ и ищешь acquire/release в Go — остановись. Используй sync/atomic как есть, прими цену полного барьера, и твой код будет корректен по умолчанию.\nОдно тонкое, но важное следствие: атомарные операции синхронизируют не только атомарную переменную, но и всё, что произошло-до атомарной записи в той же горутине. Это основа паттерна публикации (publication pattern):\ntype Config struct { Hosts []string TTL time.Duration // ... много полей } var configPtr atomic.Pointer[Config] // Горутина-писатель (применяет обновление конфигурации) func updateConfig(c *Config) { // Все записи в c.Hosts, c.TTL и т.д. происходят-до этого Store. configPtr.Store(c) } // Горутина-читатель (миллионы вызовов) func getConfig() *Config { // Load синхронизируется с соответствующим Store. // Возвращаемый *Config полностью инициализирован — никаких частичных чтений. return configPtr.Load() } Поля Config — не атомарные. Обычный []string, обычный time.Duration. Но писатель полностью конструирует c перед вызовом Store, а Load читателя синхронизируется с этим Store, поэтому читатель видит полностью инициализированную структуру. Никаких блокировок, никакого копирования на пути чтения, никакой конкуренции. Это канонический способ работы с конфигурацией, которую часто читают.\nСравнение с C++ и Rust: почему в Go только sequentially consistent Если ты пришёл из C++, тебе может быть непривычно. В C++ у std::atomic\u0026lt;T\u0026gt; есть шесть упорядочиваний:\nstd::atomic\u0026lt;int\u0026gt; x{0}; x.store(42, std::memory_order_relaxed); // нет барьеров вообще x.store(42, std::memory_order_release); // release: записи до этого станут видны загрузчику int v = x.load(std::memory_order_acquire); // acquire: загрузки после этого увидят release int v = x.load(std::memory_order_seq_cst); // полный барьер (как в Go) Эти различия позволяют тонко оптимизировать. Например, счётчик посещений, для которого не важен точный порядок инкрементов между потоками, можно делать memory_order_relaxed — и компилятор с CPU могут переупорядочивать окружающий код гораздо свободнее, что иногда даёт 20-30% прироста на hot paths.\nВ Rust те же шесть упорядочиваний через std::sync::atomic::Ordering:\nuse std::sync::atomic::{AtomicU64, Ordering}; let x = AtomicU64::new(0); x.store(42, Ordering::Relaxed); x.store(42, Ordering::Release); let v = x.load(Ordering::Acquire); let v = x.load(Ordering::SeqCst); В Go — только SeqCst. Всегда. Любой вызов atomic.AddInt64, atomic.LoadPointer, atomic.CompareAndSwap — полный барьер. Не существует atomic.LoadInt64Acquire или atomic.StorePointerRelease.\nПочему? Из объяснения Рассела Кокса при ревизии модели памяти Go 1.19: расслабленные упорядочивания слишком легко применить неправильно. В C++-сообществе накоплены годы опыта, статьи, инструменты (TSan, лит тесты), и всё равно эксперты регулярно ошибаются. Go целенаправленно отказался от этой гибкости ради простоты. Цена — иногда лишний барьер, который можно было бы избежать. Выгода — модель памяти, которую можно удержать в голове.\nЭто не значит, что Go «медленнее». На практике разница между seq_cst и acquire/release почти всегда теряется в шуме. Зато код, который компилируется и проходит go test -race, ведёт себя так, как написан — без сюрпризов от переупорядочивания. Для backend-сервисов с пайплайном CI и код-ревью это важнее теоретических 20%.\nЕсли тебе действительно нужна предельная производительность и ты готов платить внимательностью — в Go можно через unsafe и ручной ассемблер сделать что угодно, но это уже не Go-стиль. Для 99% задач sequentially consistent — то, что нужно.\n3. Второй путь: Mutex, построенный поверх atomic Теперь мы наконец можем объяснить sync.Mutex. Короткая версия: это в основном оптимизация поверх атомарного CAS плюс механизм честности. Посмотрим, как именно.\n3.1. Структура Mutex Вот реальное определение из Go 1.24 (исходник):\ntype Mutex struct { state int32 sema uint32 } Два поля. Восемь байт всего. state — упакованное битовое поле, несущее четыре фрагмента информации. sema — семафор, который рантайм использует для парковки и пробуждения горутин.\nПоле state устроено так:\n┌───────────────────────────────────────────────────────────────┐ │ state (int32) │ ├───────┬────────┬─────────────┬─────────────────────────────────┤ │ бит 0 │ бит 1 │ бит 2 │ биты 3 .. 31 │ │ │ │ │ │ │Locked │ Woken │ Starving │ Счётчик waiter\u0026#39;ов (29 бит, ~536M)│ └───────┴────────┴─────────────┴─────────────────────────────────┘ 1 2 4 8, 16, 32, ... Константы в исходнике:\nconst ( mutexLocked = 1 \u0026lt;\u0026lt; iota // 1 mutexWoken // 2 mutexStarving // 4 mutexWaiterShift = iota // 3 starvationThresholdNs = 1e6 // 1 миллисекунда ) Четыре вещи, закодированные в 32 битах:\nLocked (бит 0): сейчас заперт мьютекс или нет? 1 = да. Woken (бит 1): был ли waiter разбужен и теперь пытается захватить? Это подсказка-оптимизация для Unlock — он знает не будить ещё одного waiter\u0026rsquo;а, потому что один уже активен. Starving (бит 2): находится ли мьютекс в режиме голодания? См. раздел 3.6. Счётчик waiter\u0026rsquo;ов (биты 3-31): сколько горутин сейчас припарковано в ожидании этого мьютекса. 29 бит, до ~536 миллионов. Ты никогда не упрёшься в потолок. Поле sema не имеет внутренней структуры. Это непрозрачный токен, который код семафоров рантайма использует для управления очередью припаркованных горутин.\n3.2. Fast path: одна CAS, без syscall Горячий путь через Lock() короткий:\nfunc (m *Mutex) Lock() { // Fast path: grab unlocked mutex. if atomic.CompareAndSwapInt32(\u0026amp;m.state, 0, mutexLocked) { // ... бухгалтерия детектора гонок ... return } // Slow path (вынесен, чтобы fast path можно было инлайнить) m.lockSlow() } Это всё. Одна CAS. Если мьютекс свободен (state == 0), CAS\u0026rsquo;аем в mutexLocked (state == 1) и возвращаемся. Если CAS провалился — попадаем в lockSlow.\nFast path инлайнится. Можешь проверить сам:\n$ go build -gcflags=\u0026#34;-m\u0026#34; main.go ./main.go:13:12: inlining call to sync.(*Mutex).Lock ./main.go:15:14: inlining call to sync.(*Mutex).Unlock Инлайнинг здесь важен. Он означает, что в неконкурируемом случае вызов mu.Lock() не несёт даже накладных расходов на вызов функции — инструкция CAS встаёт прямо в месте вызова. Поэтому uncontended mu.Lock() — это примерно 20-60 наносекунд: по сути одна инструкция CAS плюс несколько циклов бухгалтерии.\nЕсли ты не запомнишь из этой статьи ничего больше, запомни это: uncontended блокировка мьютекса — это одна инструкция CAS. Та же инструкция, что использует atomic.CompareAndSwapInt32. Разница в стоимости между uncontended мьютексом и атомарной операцией сводится к нескольким лишним циклам бухгалтерии, а не к качественному различию.\n3.3. Slow path: spinning Когда CAS на fast path проваливается, мьютекс уже захвачен, и мы входим в lockSlow. Здесь начинается самое интересное.\nПервое, что пробует lockSlow, — spinning (спиннинг, активное ожидание): активное ожидание в плотном цикле CPU в надежде, что текущий владелец скоро освободит блокировку. Обоснование простое: если владелец собирается отпустить блокировку в ближайшие несколько сотен наносекунд, дешевле покрутиться и захватить её сразу, чем парковать горутин и потом будить (цикл парковки/пробуждения горутины через рантайм стоит сотни наносекунд до микросекунд).\nВот релевантный кусок lockSlow:\nfunc (m *Mutex) lockSlow() { var waitStartTime int64 starving := false awoke := false iter := 0 old := m.state for { // Не spinning в режиме голодания — владение передаётся // waiter\u0026#39;ам, поэтому мы всё равно не сможем захватить мьютекс. if old\u0026amp;(mutexLocked|mutexStarving) == mutexLocked \u0026amp;\u0026amp; runtime_canSpin(iter) { // Активный spinning имеет смысл. // Установим флаг woken, чтобы сообщить Unlock, что он нам понадобится. if !awoke \u0026amp;\u0026amp; old\u0026amp;mutexWoken == 0 \u0026amp;\u0026amp; old\u0026gt;\u0026gt;mutexWaiterShift != 0 \u0026amp;\u0026amp; atomic.CompareAndSwapInt32(\u0026amp;m.state, old, old|mutexWoken) { awoke = true } runtime_doSpin() iter++ old = m.state continue } // ... пытаемся захватить или встать в очередь ... } } Функция runtime_doSpin в конечном счёте вызывает runtime.procyield(cycles), которая на ARM64 разворачивается в такой ассемблер:\nTEXT runtime·procyield(SB),NOSPLIT,$0-0 MOVWU cycles+0(FP), R0 again: YIELD SUBW $1, R0 CBNZ R0, again RET Плотный цикл, который выдаёт инструкцию YIELD (подсказку CPU, что это цикл ожидания — ядро может временно освободить ресурсы выполнения для собрата по hyperthreading или снизить собственный приоритет) и считает от заданного числа циклов. Go вызывает procyield(30) — 30 YIELD за одну итерацию спина.\nruntime_canSpin(iter) ограничивает spinning: максимум 4 итерации, и только если GOMAXPROCS \u0026gt; 1 (spinning на одноядерной машине — чистая потеря, работать больше нечему, можно сразу парковаться), и есть больше одной runnable горутины (чтобы рантайму было что делать, если спин провалится). В худшем случае — 4 спина × 30 YIELD = 120 YIELD до сдачи. Это порядка нескольких сотен наносекунд реального времени — достаточно коротко, чтобы ты почти не заметил, но достаточно длинно, чтобы владелец с короткой критической секцией вероятно освободил блокировку в этом окне.\nПосле spinning горутина строит новое значение state (отмечая себя как waiter при необходимости, возможно отмечая себя как starving, если ждёт слишком долго), CAS\u0026rsquo;ает его на место, и если всё ещё не может захватить — попадает в самый медленный путь: парковку.\n3.4. Самый медленный путь: futex и парковка Когда spinning провалился, горутине нужно действительно уснуть, пока блокировку не освободят. Здесь вступает futex.\nFutex — сокращение от \u0026ldquo;fast userspace mutex\u0026rdquo; («быстрый мьютекс пространства пользователя») — это системный вызов Linux (futex(2)), появившийся в Linux 2.6 (2003) благодаря Хубертусу Франке, Мэттьё Кирквуду, Инго Молнару и Ульриху Дрепперу (да, тому самому Дрепперу, который написал \u0026ldquo;What Every Programmer Should Know About Memory\u0026rdquo;). Оригинальная статья \u0026ldquo;Fuss, Futexes and Furwocks: Fast Userlevel Locking in Linux\u0026rdquo; (OLS 2002) — каноническая ссылка.\nИдея гениальна в своей простоте. Большую часть времени блокировка либо не конкурируется, либо конкурируется лишь кратко. В этих случаях можно захватывать и отпускать её целиком в пространстве пользователя через CAS, без участия ядра. Только когда действительно нужно уснуть (потому что блокировку держат долго) — мы обращаемся к ядру. Задача ядра — только поддерживать очередь waiter\u0026rsquo;ов и будить их в нужный момент.\nДве основные операции futex:\nFUTEX_WAIT(addr, expected): проверить, что *addr == expected. Если да — припарковать вызывающий поток в очередь, ассоциированную с addr, и уснуть. Если нет — немедленно вернуться. Проверка-и-парковка атомарны — нет гонки, где значение меняется между проверкой и засыпанием. FUTEX_WAKE(addr, n): разбудить не более n потоков, припаркованных в очереди, ассоциированной с addr. Рантайм Go строит собственную систему семафоров поверх futex (на Linux) или эквивалентного syscall на других ОС (umtx на FreeBSD, futex на OpenBSD и т.д.). Функция runtime_SemacquireMutex(\u0026amp;m.sema, ...) в конечном счёте паркует горутину; runtime_Semrelease(\u0026amp;m.sema, ...) — будит одну.\ngraph LR subgraph US[\"Userspace\"] CAS[\"CAS(state, 0, locked)\"] Spin[\"Spin: procyield(30) × 4\"] Slow[\"lockSlow: строим новый state\"] end subgraph K[\"Ядро\"] Wait[\"FUTEX_WAIT(sema)\"] Queue[\"Очередь waiter'ов\"] Wake[\"FUTEX_WAKE(sema)\"] end Lock[\"mu.Lock()\"] --\u003e CAS CAS --\u003e|успех| Done[\"готово (без syscall)\"] CAS --\u003e|неудача — занято| Spin Spin --\u003e|получилось| Done Spin --\u003e|всё ещё занято| Slow Slow --\u003e Wait Wait --\u003e Queue Queue -.-\u003e|припаркован, спит| SleepZzz[\"...\"] Wake -.-\u003e|от Unlock другой горутины| Queue Queue --\u003e|разбужен| CAS style CAS fill:#2e7d32,color:#fff style Done fill:#2e7d32,color:#fff style Wait fill:#c62828,color:#fff style Wake fill:#c62828,color:#fff Разница в стоимости между этими путями колоссальна. Неконкурируемая CAS — одна инструкция, несколько наносекунд. Круг futex (парковка и пробуждение) — два syscall плюс переключения контекста, порядка микросекунд. Это разница в 1000 раз. Поэтому Mutex так старается оставаться в userspace: spinning, оптимистичная CAS, всё это спроектировано, чтобы не попадать в ядро.\nM:N планировщик и почему парковка горутины — не то же, что парковка потока Тут есть тонкость, которую легко упустить. Futex в Linux работает с потоками ядра — FUTEX_WAIT усыпляет вызывающий поток. Но Go использует M:N планировщик: много горутин (M) мультиплексируются на меньшее число потоков ядра (N, по умолчанию равно числу CPU).\nПрежде чем разбирать механику парковки, посмотрим на сами компоненты планировщика Go — так называемую GMP-модель:\ngraph TB subgraph Gs[\"G — горутины (тысячи)\"] G1[\"G1 running\"] G2[\"G2 runnable\"] G3[\"G3 runnable\"] Gn[\"... Gn\"] end subgraph Ps[\"P — логические процессоры (GOMAXPROCS)\"] P1[\"P1\nlocal runq: [G2, G3]\"] P2[\"P2\nlocal runq: [G4, G5]\"] end subgraph Ms[\"M — потоки ядра (число ≤ GOMAXPROCS + блокированных)\"] M1[\"M1 ← P1\"] M2[\"M2 ← P2\"] end subgraph SYS[\"Ядро ОС\"] SCHED[\"Linux scheduler\nуправляет M\"] FUTEX[\"futex syscall\nпаркует M\"] end G1 -.-\u003e|выполняется на| M1 G2 -.-\u003e|в очереди| P1 G3 -.-\u003e|в очереди| P1 M1 ===\u003e|связан с| P1 M2 ===\u003e|связан с| P2 M1 -.-\u003e|системные вызовы| SCHED M2 -.-\u003e|системные вызовы| SCHED SCHED --- FUTEX style G1 fill:#2e7d32,color:#fff style P1 fill:#1565c0,color:#fff style P2 fill:#1565c0,color:#fff style M1 fill:#c62828,color:#fff style M2 fill:#c62828,color:#fff style FUTEX fill:#6a1b9a,color:#fff Три буквы:\nG (Goroutine) — пользовательская горутина. Лёгкая, стартовый стек 2КБ, растёт по надобности. Их могут быть сотни тысяч на процесс. M (Machine) — поток ядра ОС. Настоящий, тяжёлый, с собственным стеком (минимум несколько КБ). Число M ограничено — обычно равно числу CPU плюс несколько spare для заблокированных в syscall. P (Processor) — логический процессор. Несёт локальную очередь runnable-горутин (runq) и контекст для выполнения Go-кода. Число P = GOMAXPROCS. Чтобы M мог выполнять Go-код, ему нужен P. Связь: M хватает P → берёт G из runq этого P → выполняет → берёт следующий G. Если runq пуста, P пытается украсть горутины у другого P (work stealing) или берёт из глобальной очереди.\nТеперь — что происходит, когда G1 на M1/P1 вызывает mu.Lock(), а мьютекс уже занят. Рантайм Go проходит по такому дереву решений:\nflowchart TD Start([G1 вызывает mu.Lock\nмьютекс занят]) --\u003e Mark[\"Рантайм:\nG1 → состояние _Gwaiting\nубрать из runq P1\nдобавить в sema-очередь мьютекса\"] Mark --\u003e Check{\"На P1 есть\nдругие runnable G?\"} Check --\u003e|да — например G2, G3| Switch[\"M1 переключается на G2\n~200 нс, без syscall\nP1 не освобождается\"] Check --\u003e|нет| Global{\"В глобальной runq\nесть G?\"} Global --\u003e|да| Take[\"M1 берёт G из globalq\nили ворует у другого P\"] Global --\u003e|нет| ParkM[\"M1 отпускает P1\nдругой M может его забрать\nM1 вызывает FUTEX_WAIT\nkernel thread спит\"] Switch --\u003e Work[\"G2 выполняется\"] Take --\u003e Work ParkM --\u003e Kernel[\"... ядро держит M1 спящим\nв очереди futex\"] Kernel -.-\u003e|\"где-то в другом потоке:\nGx отпускает мьютекс\"| Wake Wake[\"runtime_Semrelease\nG1 → _Grunnable\nкладётся в runq P\n(того же или другого)\"] Wake --\u003e Rerun[\"G1 снова в очереди\nдождётся своего M/P\nи продолжит\"] style Start fill:#c62828,color:#fff style Switch fill:#2e7d32,color:#fff style Take fill:#2e7d32,color:#fff style ParkM fill:#ad1457,color:#fff style Wake fill:#1565c0,color:#fff Ключевой момент — первая проверка: «есть ли на P1 ещё runnable G?». В типичной Go-программе ответ почти всегда да — в локальной очереди сидят ещё несколько горутин. Поэтому M1 не засыпает, а просто переключается на G2. Это переключение контекста происходит целиком в user-space (сотня наносекунд), без захода в ядро.\nТолько если локальная очередь пуста и глобальная пуста и нечего украсть у других P — только тогда M1 вызывает FUTEX_WAIT и засыпает на уровне ядра. Это и есть та самая оптимизация, которая отличает Go от C++/Java: вместо парковки тяжёлого потока ядра (с переключением контекста ОС, порядка микросекунд) мы парковываем лёгкую горутину и переиспользуем поток ядра для другой работы.\nСравни:\nПлатформа Что паркуется при Lock под нагрузкой Стоимость Масштаб C++ std::mutex Поток ядра (через futex) ~1-5 мкс (syscall + ctx switch) ≤ тысячи потоков Java synchronized Поток ядра (через JVM monitor) ~1-5 мкс ≤ тысячи потоков Go sync.Mutex Горутина (в user-space) ~100-200 нс сотни тысяч горутин Поэтому Go может иметь сотни тысяч горутин, ожидающих мьютексов, не расходуя сотни тысяч потоков ядра — это была бы катастрофа по памяти (каждый поток ядра имеет стек минимум несколько КБ) и по планировщику ОС (ему пришлось бы бегать по огромной очереди потоков).\nСтоимость «парковки горутины» в Go — это не стоимость syscall, а стоимость переключения контекста в user-space планировщике, порядка сотни наносекунд. Syscall FUTEX_WAIT случается только когда поток ядра действительно не нашёл другой работы. Это ещё одна причина, почему mu.Lock() под нагрузкой в Go часто быстрее, чем эквивалент в C++/Java — мы платим только за то, что реально используем.\nПодробнее о планировщике Go — в design doc Дмитрия Вюкова \u0026ldquo;Scalable Go Scheduler Design Doc\u0026rdquo; (май 2012, до сих пор основа runtime/sched).\n3.5. Unlock: симметричный, с сюрпризом handoff Unlock выглядит симметрично Lock, но с одним поворотом:\nfunc (m *Mutex) Unlock() { // ... бухгалтерия детектора гонок ... // Fast path: drop lock bit. new := atomic.AddInt32(\u0026amp;m.state, -mutexLocked) if new != 0 { // Вынесенный slow path, чтобы позволить инлайнинг fast path. m.unlockSlow(new) } } Fast path — atomic.AddInt32(\u0026amp;m.state, -mutexLocked) — одна атомарная инструкция вычитания, очищающая бит блокировки. Если результирующее состояние нулевое (нет waiter\u0026rsquo;ов, нет флагов) — мы готовы: никакого syscall, никакого пробуждения. Поэтому uncontended unlock, как и uncontended lock, — это всего несколько наносекунд.\nЕсли есть waiter\u0026rsquo;ы, запускается unlockSlow, и его поведение зависит от того, находится ли мьютекс в режиме голодания:\nfunc (m *Mutex) unlockSlow(new int32) { if (new+mutexLocked)\u0026amp;mutexLocked == 0 { fatal(\u0026#34;sync: unlock of unlocked mutex\u0026#34;) } if new\u0026amp;mutexStarving == 0 { // Нормальный режим. old := new for { // Если waiter\u0026#39;ов нет, или кто-то уже одного разбудил, // или кто-то захватил блокировку — никого будить не нужно. if old\u0026gt;\u0026gt;mutexWaiterShift == 0 || old\u0026amp;(mutexLocked|mutexWoken|mutexStarving) != 0 { return } // Хватаем право кого-то разбудить. new = (old - 1\u0026lt;\u0026lt;mutexWaiterShift) | mutexWoken if atomic.CompareAndSwapInt32(\u0026amp;m.state, old, new) { runtime_Semrelease(\u0026amp;m.sema, false, 2) return } old = m.state } } else { // Режим голодания: напрямую передаём владение мьютексом // следующему waiter\u0026#39;у и отдаём наш квант времени, чтобы // следующий waiter мог немедленно начать работу. // Замечание: mutexLocked не установлен, waiter установит его после пробуждения. runtime_Semrelease(\u0026amp;m.sema, true, 2) } } Посмотри на последний аргумент runtime_Semrelease: false в нормальном режиме, true в режиме голодания. Этот булев флаг — флаг handoff (передачи владения). Когда true, рантайм напрямую передаёт владение мьютексом разбуженному waiter\u0026rsquo;у — тот просыпается уже владея блокировкой, с предустановленным битом locked. Когда false — разбуженный waiter должен конкурировать за блокировку наравне со всеми.\nЭтот единственный булев флаг — вся суть режима голодания. Чтобы понять, почему он важен, нужно посмотреть на баг, для которого он был придуман.\n3.6. Режим голодания: баг за issue #13086 28 октября 2015 года Рассел Кокс открыл Go issue #13086 с заголовком \u0026ldquo;runtime: fall back to fair locks after repeated sleep-acquire failures\u0026rdquo; («рантайм: откатываться к честным блокировкам после повторных неудач засыпание-захват»). Открывающая строка была прямолинейной: «Блокировки Go не дают гарантий справедливости».\nБаг-репорт описывал простую программу с двумя горутинами. Горутина 1 держит блокировку почти всё время, отпуская её лишь на 100 микросекунд за раз. Горутина 2 хочет блокировку лишь ненадолго, каждые 100 микросекунд. Наивное ожидание — что горутина 2 должна получать блокировку хоть иногда, возможно в течение секунды-двух.\nРеальность, на Linux-станции Рассела, — горутина 2 тратила от 100 до 600 секунд на одну попытку захвата. Не миллисекунды. Секунды. Минуты. Десять минут на одно получение блокировки.\nАнализ Рассела точно идентифицировал проблему. Когда горутина 1 вызывает Unlock, она помечает блокировку свободной и сообщает рантайму «разбуди горутину 2». Но горутина 2 не запускается немедленно. Горутина 1 продолжает работать, проходит цикл, снова вызывает Lock — и поскольку блокировка теперь свободна и горутина 1 уже на CPU, горутина 1 захватывает её обратно. К моменту, когда горутина 2 наконец получает квант и пытается захватить, горутина 1 снова держит блокировку. Цикл повторяется. Миллионы раз.\nРассел назвал проблему barging («втесание»). Альтернативу, при которой Unlock оставляет блокировку запертой и явно передаёт владение разбуженному waiter\u0026rsquo;у, он назвал handoff («передача»). В более ранних работах Дага Ли по java.util.concurrent (статья AQS, 2003) barging улучшал пропускную способность. Измерения Ли были на потоках ОС с планировщиком Linux 2.4 NPTL, и его аргумент: barging помогает избежать плохих решений планировщика ОС — если ОС медленно планирует разбужденный поток, то оставление блокировки свободной позволяет другому потоку делать полезную работу тем временем.\nНо Go не использует потоки ОС напрямую. Он использует горутины и собственный планировщик в пространстве пользователя, где переключение горутины стоит десятки наносекунд, а не микросекунды переключения контекста потока. Компромисс, оправдывавший barging в Java, не обязательно применим к Go.\nРассел предложил гибрид. Напомню два режима, которые он противопоставлял:\nBarging (от «barge in» — врываться): при Unlock мьютекс помечается свободным, и любой новый кандидат может его захватить. Тот, кого только что разбудили, должен соревноваться с новоприбывшими. Если он не успел — возвращается в очередь. Это быстрее в среднем (не нужно ждать пробуждения), но может бесконечно ущемлять конкретного waiter\u0026rsquo;а. Handoff (передача владения): при Unlock мьютекс остаётся запертым, и владение напрямую передаётся следующему в очереди. Никакой конкуренции — следующий waiter просыпается уже владельцем. Это честно, но требует yield\u0026rsquo;а кванта времени от текущей горутины, что в теории дороже. Идея Рассела: по умолчанию жить в режиме barging (быстрее), но переключаться на handoff, когда unfairness становится патологической. Конкретный критерий «патологичности», который он предложил в issue: waiter, который был разбужден, но обнаружил блокировку уже занятой (то есть проиграл barging-конкуренцию), увеличивает внутренний счётчик. После нескольких таких последовательных проигрышей мьютекс переключается в режим handoff — чтобы гарантированно дать голодному waiter\u0026rsquo;у дождаться своего.\nНиже на схеме — как эти два режима выглядят рядом. После неё разберём, как финальная реализация Вюкова в Go 1.9 воплотила эту идею (с одним изменением: вместо подсчёта проигрышей — замер времени ожидания).\ngraph LR subgraph Normal[\"Нормальный режим (по умолчанию)\"] N1[\"Unlock: пометить свободной\"] --\u003e N2[\"Разбудить waiter W\"] N2 --\u003e N3[\"Продолжить работу\"] N3 --\u003e N4[\"Другая горутина вызывает Lock\"] N4 --\u003e N5[\"Barge! Захватить блокировку\"] N2 -.-\u003e|W спланирован слишком поздно| NLost[\"W просыпается, блокировка уже занята\"] NLost --\u003e N1 end subgraph Starvation[\"Режим голодания (после 1мс ожидания)\"] S1[\"Unlock: оставить запертой\"] --\u003e S2[\"Передать владение следующему waiter W\"] S2 --\u003e S3[\"Отдать квант времени\"] S3 --\u003e S4[\"W просыпается уже владея\"] S4 --\u003e S5[\"W выполняет критическую секцию\"] S5 --\u003e S1 end Normal -.-\u003e|\"waiter ждал \u003e 1мс\"| Starvation Starvation -.-\u003e|\"последний waiter ИЛИ ждал \u003c 1мс\"| Normal style Normal fill:#1565c015 style Starvation fill:#c6282815 style N5 fill:#c62828,color:#fff style S4 fill:#2e7d32,color:#fff Реализация, попавшая в Go 1.9 (август 2017), написанная Дмитрием Вюковым, отличалась в деталях, но та же по духу. Вместо подсчёта неудач отслеживается время ожидания по стенным часам (wall-clock wait time). Если горутина ждёт дольше starvationThresholdNs = 1e6 (1 миллисекунда), чтобы захватить блокировку, она устанавливает бит mutexStarving. В режиме голодания:\nНовые прибывшие не пытаются захватить блокировку. Они идут прямо в конец очереди waiter\u0026rsquo;ов. Unlock напрямую передаёт владение (тот самый true, что мы видели в runtime_Semrelease(\u0026amp;m.sema, true, 2)), и отдаёт квант времени, чтобы разбужденный waiter запустился немедленно. Spinning отключается — он бесполезен, потому что блокировку передают, а не втесаются. Мьютекс выходит из режима голодания, когда выполняется любое из двух условий:\nТекущий waiter — последний в очереди (счётчик waiter\u0026rsquo;ов после этого захвата стал бы 0), ИЛИ Текущий waiter ждал менее 1мс в этом раунде. Это самокорректирующийся механизм. Когда нагрузка спадает, мьютекс естественно возвращается к быстрому режиму barging. Когда патологическая нагрузка вызывает голодание, он переключается в режим handoff ровно настолько, чтобы честно опустошить очередь.\nЭффект, измеренный Расселом на его бенчмарке lockskew, — ускорение в 500000 раз в патологическом случае: от 100+ секунд на захват до 162 микросекунд. А на бенчмарке общего случая (генерация случайных чисел под тяжёлой конкуренцией) производительность даже слегка выросла (на 1-12%), опровергая мудрость эпохи Java о том, что handoff всегда вредит пропускной способности. Планирование горутин достаточно дёшево, что стоимость handoff незаметна на фоне остальной работы.\nВот почему твой Go-код почти никогда не голодает: внутри sync.Mutex тихо сидит порог в 1мс, защищая тебя от целого класса багов, которые вырубали реальные продакшен-системы в 2015 году.\nЦифры из issue #13086: насколько всё было плохо Чтобы почувствовать масштаб проблемы, посмотри на исходные замеры Рассела Кокса с его бенчмарка lockskew. Две горутины, одна держит мьютекс по 100мкс и отпускает на 100мкс, вторая хочет его на мгновение каждые 100мкс. Время до получения блокировки для горутины 2:\nlock#0 взяла 180.773982s lock#1 взяла 484.603155s (среднее 332s) lock#2 взяла 107.852508s lock#3 взяла 223.592777s lock#4 взяла 600.067715s ← 10 минут на один lock lock#5 взяла 579.896472s Не 100 миллисекунд. Не 100 секунд. До 10 минут на одно получение блокировки. И это не toy-пример — Рассел упоминает, что два независимых реальных репорта привели к этому issue.\nПосле применения решения (в final виде — порог в 1мс с переключением в starvation), те же самые замеры:\nlock#0 взяла 0.000155s lock#2934 взяла 0.000165s (среднее) lock#5861 взяла 0.000163s 500000x ускорение в патологическом случае. И при этом на стандартном бенчмарке пропускной способности (go test -bench) регрессии нет — местами даже небольшой прирост.\nЭто, кстати, хороший инженерный урок: «общепринятое правило» может быть основано на измерениях для другой платформы и не переноситься на твою. Doug Lea измерял barging vs handoff для Java на Linux 2.4 с потоками ядра. Go измеряет на горутинах с собственным планировщиком — и компромисс оказывается иным. В enterprise-разработке такие перекрёстные ссылки на «авторитеты» без проверки применимости — частый источник мифов.\n3.7. Правила: не копировать, не реентерить Два операционных правила, на которых люди спотыкаются:\nНикогда не копируй Mutex. Вот это:\ntype Server struct { mu sync.Mutex // ... } func (s Server) Handle(req Request) { // БАГ: приёмник по значению s.mu.Lock() defer s.mu.Unlock() // ... } …компилируется, но содержит баг. Приёмник метода — копия Server, а значит s.mu — копия оригинального мьютекса: свежий, незапертый мьютекс без связи с оригинальным state или sema. Две горутины, вызывающие Handle на одном Server, каждая заблокируют свою собственную приватную копию — нулевая взаимная эксклюзия. go vet ловит это:\n$ go vet ./server.go:12:20: Handle passes lock by value: Server contains sync.Mutex Исправление: сделай приёмник указателем (func (s *Server) Handle(...)), или встраивай *sync.Mutex вместо sync.Mutex.\nТо же касается типов, встраивающих Mutex, — sync.WaitGroup, sync.RWMutex, чего угодно с ним внутри. Общее правило: значение, содержащее Mutex, нельзя копировать после первого использования.\nМьютексы не реентерабельны. Сначала — что вообще значит «реентерабельный» (reentrant, «с возможностью повторного входа»). Реентерабельная функция или объект — тот, который можно безопасно вызвать ещё раз до того, как закончился предыдущий вызов. Например, чистая функция sum(a, b) реентерабельна: её можно вызвать рекурсивно, прервать, вызвать из другого потока — ничего не сломается, потому что у неё нет состояния, которое могло бы «запутаться».\nК мьютексам это применяется так: реентерабельный мьютекс (recursive mutex, как PTHREAD_MUTEX_RECURSIVE в POSIX, или synchronized в Java) позволяет той же горутине/потоку вызывать Lock несколько раз подряд без дедлока. Внутри ведётся счётчик глубины: первый Lock действительно захватывает мьютекс, второй — увеличивает счётчик до 2, третий — до 3. Каждый Unlock уменьшает счётчик, и только когда он доходит до 0, мьютекс реально отпускается. Это удобно для рекурсивного кода:\n// Гипотетический пример с реентерабельным мьютексом // (в Go такого нет — это псевдокод) func (s *Server) Outer() { s.reentrantMu.Lock() // счётчик: 0 → 1 (мьютекс захвачен) defer s.reentrantMu.Unlock() // счётчик: 1 → 0 (мьютекс отпущен) s.Inner() } func (s *Server) Inner() { s.reentrantMu.Lock() // счётчик: 1 → 2 (та же горутина, нет дедлока) defer s.reentrantMu.Unlock() // счётчик: 2 → 1 // ... } В Go мьютекс не реентерабельный — sync.Mutex ведёт себя как PTHREAD_MUTEX_NORMAL в POSIX: второй вызов Lock из той же горутины, не отпустив первый, сразу дедлочит. Никакого счётчика глубины нет:\nfunc (s *Server) Outer() { s.mu.Lock() defer s.mu.Unlock() s.Inner() // БАГ: Inner снова вызывает s.mu.Lock — deadlock } func (s *Server) Inner() { s.mu.Lock() defer s.mu.Unlock() // ... } Второй Lock блокирует навсегда в ожидании первого Unlock, который никогда не наступит — потому что горутина застряла во втором Lock и не дойдёт до defer Unlock. Классический self-deadlock.\nРазработчики Go намеренно отказались от рекурсивного мьютекса и неоднократно отклоняли предложения его добавить. Их аргумент: если функция берёт блокировку, которую вызывающая сторона уже держит, — это почти всегда симптом непродуманной дисциплины блокировок. Реентерабельный мьютекс не лечит, а прячет проблему: код компилируется, «работает», но логика блокировок запутана, и в следующий раз (когда кто-то добавит ещё одну функцию) дедлок вернётся в менее очевидной форме. Лучше сразу увидеть проблему и переструктурировать код.\nТипичные исправления:\nРазделить функцию на «внешнюю» (берёт блокировку) и «внутреннюю» (предполагает, что блокировка уже взята). Это самый частый паттерн в стандартной библиотеке Go — например, sync.Map имеет публичные методы Load/Store и приватные loadLocked/storeLocked: func (s *Server) Outer() { s.mu.Lock() defer s.mu.Unlock() s.innerLocked() // блокировка уже взята — innerLocked её не трогает } // innerLocked предполагает, что s.mu уже захвачен вызывающим. // Имя *Locked — конвенция, общепринятая в Go-кодбейзах. func (s *Server) innerLocked() { // ... работа с защищённым состоянием ... } Перенести блокировку выше — туда, где понятна её область. Если Outer и Inner оба хотят блокировку, возможно, блокировку должен брать только Outer, а Inner должна принимать уже защищённые данные по значению. Переструктурировать так, чтобы одной горутине не нужно было брать одну блокировку дважды. Часто это признак того, что критическая секция слишком широка или что данные надо разделить. Исправление структурное: разбей Inner на две функции — одна предполагает, что блокировка уже взята, другая берёт её. Или документируй дисциплину блокировок явно. Или переструктурируй так, чтобы одной горутине не приходилось брать одну блокировку дважды.\n4. Что когда использовать Теперь ты понимаешь стек. Оставшийся вопрос — практический: глядя на кусок кода, тянешься ли ты к atomic, к мьютексу или к чему-то ещё? Разберём.\n4.1. Публичные бенчмарки: цифры, которые стоит запомнить Большинство опубликованных бенчмарков mutex-vs-atomic на Go сходятся вокруг одних и тех же чисел. Из бенчмарков thecodinggopher, goperf.dev и других, на современном x86:\nОперация Без конкуренции Под нагрузкой atomic.AddInt64(\u0026amp;x, 1) 4-8 нс десятки нс до мкс (повторы CAS) atomic.LoadInt64(\u0026amp;x) 1-2 нс 1-2 нс (только чтение) mu.Lock(); mu.Unlock() 20-60 нс сотни нс до мкс mu.Lock(); работа; mu.Unlock() (короткая кс) ~30 нс + работа масштабируется с конкуренцией CAS-loop под сильной нагрузкой — микросекунды сжигания CPU Под нагрузкой картина инвертируется тонким образом. CAS-loop под тяжёлой конкуренцией может сжигать сотни наносекунд до микросекунд CPU на операцию, потому что каждая неудачная CAS — потраченная работа. Мьютекс же, припарковав проигравшего, позволяет победителю продолжать без постоянных помех. Для устойчивой конкуренции мьютекс часто быстрее наивного атомарного подхода.\nОткуда берётся разрыв в 5-10x Выше я писал, что uncontended mu.Lock() — это «почти одна CAS, как atomic». И в то же время таблица выше показывает разрыв в 5-10x. Кажется противоречием — давайте разберём честно, по инструкциям.\natomic.AddInt64(\u0026amp;x, 1) компилируется в одну инструкцию LOCK XADD на x86 (LDADD на ARM LSE). Это read-modify-write за один такт, всегда успешный с первого раза — нет ни retry, ни проверок.\nА пара mu.Lock(); mu.Unlock() делает минимум две атомарные операции:\n// Lock, fast path: atomic.CompareAndSwapInt32(\u0026amp;m.state, 0, mutexLocked) // CAS // Unlock, fast path: new := atomic.AddInt32(\u0026amp;m.state, -mutexLocked) // atomic subtract if new != 0 { // branch m.unlockSlow(new) // потенциальный slow path } То есть структурно уже 2x — операций вдвое больше. Откуда остальное до 5-10x?\nCAS дороже Add. CAS — это «прочитать, сравнить, может быть записать». Add — «прочитать, прибавить, записать». На x86 LOCK CMPXCHG дороже LOCK XADD из-за дополнительной логики сравнения. Под капотом LOCK CMPXCHG в худшем случае делает две попытки (сравнение провалилось — ничего не пишет), а LOCK XADD всегда завершает работу за один проход. Ветки даже на fast path. После CAS в Lock и после Add в Unlock стоят проверки — «получилось ли?» и «есть ли waiter\u0026rsquo;ы?». Это дополнительные branch prediction такты. На горячем цикле предсказатель обычно угадывает, но это не бесплатное угадывание. Race-детектор хуки. В каждый Lock/Unlock вшиты вызовы race.Acquire/race.Release. Когда -race выключен — они no-op, но вызов всё равно сгенерирован компилятором. В атомарных операциях этих хуков значительно меньше. Inlining overhead. Хотя fast path инлайнится, граница функции оставляет след — компилятор сохраняет часть регистров, перестраховывается. У atomic.AddInt64 inline-след минимальный. Итого: вместо одной плотной atomic-инструкции (LOCK XADD, 4-8 нс) мы получаем связку «CAS + branch + atomic Add + branch + race hooks + inline overhead» (20-60 нс на пару Lock/Unlock). На «uncontended» benchmark-е, где нет вообще никакой конкуренции. Под реальной нагрузкой, когда появляются другие горутины, разрыв может и вырасти — CAS начнёт периодически проваливаться, требуя retry.\nПарадокс разрешается так: на одиночной операции atomic выигрывает в 5-10x. Но на нагрузке с многими писателями mutex выигрывает — он паркует проигравших и не даёт им жечь CPU на retry-циклах. Это два разных режима, и выбирать надо по профилю реальной работы, а не по микробенчмарку без contention.\n4.2. Когда atomic — правильно Используй atomic, когда операция над одной ячейкой памяти и выразима как одна из атомарных примитивов.\nСценарий Рекомендация Счётчик (запросы/сек, ошибки/сек) atomic.Int64.Add(1) Флаг done/started atomic.Bool.Store(true) / .Load() Single-word состояние (idle/active/stopped) atomic.Int32 или atomic.Uint32 Указатель на иммутабельный снэпшот atomic.Pointer[T] Sequence number atomic.Uint64.Add(1) Если твой горячий путь — счётчик, инкрементируемый из множества горутин, atomic в 5-10 раз быстрее мьютекса и столь же корректен. Не оборачивай счётчик в мьютекс «для безопасности» — это медленнее без выгоды.\nХитрее случай, когда нужно более сложная атомарная операция, вроде «атомарно обновить два счётчика вместе». Это напрямую не выразимо как один атомарный примитив. У тебя два варианта:\nУпаковать оба значения в один uint64 через битовые сдвиги и CASать объединённое значение. Использовать мьютекс. Упаковка быстрее, но ограничивает 64 битами суммарно. Мьютекс проще и работает для любого размера. Большую часть времени мьютекс — правильный ответ.\n4.3. Когда мьютекс — правильно Используй мьютекс, когда операция охватывает несколько ячеек памяти, или когда нужно поддерживать инвариант между несколькими полями.\nКлассический пример: обновление map.\ntype Cache struct { mu sync.Mutex items map[string]*Entry size int } func (c *Cache) Put(k string, e *Entry) { c.mu.Lock() defer c.mu.Unlock() if _, ok := c.items[k]; !ok { c.size++ } c.items[k] = e } Нельзя атомарно обновить items и size вместе. Это отдельные поля. Если пробовать atomic для каждого, другая горутина может увидеть size обновлённым, а items — нет, или наоборот. Мьютекс гарантирует, что вся операция Put атомарна относительно других горутин: они видят либо состояние до, либо состояние после — никогда смесь.\nЭто обобщается: любая операция, требующая read-modify-write нескольких полей при сохранении инварианта между ними, требует мьютекса. Атомарные операции тут не помогают, потому что синхронизируют только одно слово.\n4.4. atomic.Value vs RWMutex для read-heavy данных Для данных, которые много читают — конфигурация, feature flags, справочные таблицы — есть два распространённых паттерна:\nПаттерн A: atomic.Pointer[T] (copy-on-write).\nvar config atomic.Pointer[Config] func Get() *Config { return config.Load() } func Update(c *Config) { config.Store(c) } Чтения — одна атомарная загрузка: несколько наносекунд, никакой конкуренции никогда. Записи требуют построения полного нового *Config и атомарного сохранения; старый указатель собирается GC, когда последний читатель с ним закончит.\nПаттерн B: sync.RWMutex.\nvar ( mu sync.RWMutex config *Config ) func Get() *Config { mu.RLock() defer mu.RUnlock() return config } func Update(c *Config) { mu.Lock() defer mu.Unlock() config = c } Чтения берут read lock — дешевле write lock, но не бесплатно (всё равно атомарный add на счётчик читателей, и записи должны ждать, пока все читатели отпустят).\nЧто быстрее? Зависит от соотношения чтение/запись:\nМиллионы чтений в секунду, пара записей в минуту: atomic решает decisively. Чтения в 5-10 раз дешевле, и no contention на запись. Много записей в секунду, данные большие: мьютекс может выиграть, потому что строить полную новую копию на каждую запись дорого. Нужно мутировать данные на месте (не заменять целиком): мьютекс, без вопросов. atomic.Value/Pointer работает только для тотальной замены. Для большинства конфигурационных use case\u0026rsquo;ов atomic.Pointer[T] — правильный ответ.\n4.5. False sharing: когда независимые атомарные операции воюют Вот тонкий баг производительности. Допустим, у тебя два счётчика, обновляемые независимо из разных горутин:\ntype Stats struct { Requests int64 Errors int64 } var stats Stats // горутина A: atomic.AddInt64(\u0026amp;stats.Requests, 1) // горутина B: atomic.AddInt64(\u0026amp;stats.Errors, 1) Выглядит нормально. Счётчики независимые. Должны линейно масштабироваться по ядрам.\nНе масштабируются. Чтобы понять почему, надо посмотреть на размеры. Сначала — базовые единицы.\nМашинное слово (machine word) — это разрядность регистра CPU. На 64-битных системах (x86-64, ARM64 — все современные серверы, десктопы, Apple Silicon) машинное слово = 8 байт = 64 бита. Это также размер указателя (unsafe.Sizeof(uintptr(0)) == 8), размер int на 64-битных (int в Go platform-dependent, но GOARCH=amd64/arm64 → 8 байт), и размер int64.\nСтрока кэша (cache line) — минимальная порция данных, которую CPU читает из памяти в L1-кэш. На всех современных x86 и ARM — 64 байта = 8 машинных слов. CPU не умеет читать из памяти один байт или одно слово; он всегда тащит целиком 64 байта. И когда пишет — тоже инвалидирует (помечает устаревшей) целиком строку в 64 байта на всех остальных ядрах.\nПосмотрим, как наша структура Stats без padding ложится на строки кэша:\n←─────── 64 байта = 1 cache line ───────→ смещение 0 1 2 3 4 5 6 7 8 9 ... 15 16 17 ... 63 ┌───────────────────────────────────┬──────────────────┬─────┐ байты │ Requests (8 байт) │ Errors (8 байт) │ ... │ └───────────────────────────────────┴──────────────────┴─────┘ ↑ ↑ int64 int64 машинное слово машинное слово (атомарный Add трогает (атомарный Add трогает эти 8 байт) эти 8 байт) ↑─────── обе переменные в ОДНОЙ строке кэша ───────↑ Структура без padding занимает 16 байт — Requests (8) + Errors (8). Оба поля уютно умещаются в одну строку кэша. Когда горутина A на ядре 0 делает atomic.AddInt64(\u0026amp;stats.Requests, 1), CPU инвалидирует всю строку (все 64 байта) на всех остальных ядрах — даже если A трогала только первые 8 байт. Когда горутина B на ядре 1 делает atomic.AddInt64(\u0026amp;stats.Errors, 1), ей приходится перезагружать строку из памяти, делать add, инвалидировать её на ядре 0. Две горутины перекидывают строку кэша туда-сюда, хотя касаются разных байтов. Это и есть false sharing (ложное разделение), и оно может замедлить параллельный код в 5-10 раз.\nИсправление — padding (выравнивание): вставить неиспользуемые байты между горячими полями, чтобы вытолкнуть Errors за границу 64-байтной строки:\ntype Stats struct { Requests int64 // 8 байт _ [56]byte // 56 байт — добить так, чтобы Errors попал в следующую строку Errors int64 // 8 байт } // Размер структуры: 8 + 56 + 8 = 72 байта Почему именно 56? Нам нужно, чтобы поле Requests (8 байт) + padding заняли ровно 64 байта — целую строку кэша. Тогда Errors начнётся со смещения 64, то есть с начала следующей строки. Считаем: 64 − 8 = 56. Это и есть размер padding-массива.\nКак это выглядит в памяти:\n←─────── 64 байта = cache line #1 ───────→ ←─── cache line #2 ───→ 0 ... 7 8 ... 63 64 ... 71 ┌───────────┬─────────────────────────────────┬───────────┬───── │ Requests │ _ [56]byte (мусор, padding) │ Errors │ ... │ (8 байт) │ (56 байт) │ (8 байт) │ └───────────┴─────────────────────────────────┴───────────┴───── ↑ atomic.Add ↑ atomic.Add трогает только cache line #1 трогает только cache line #2 ↑─── ядро 0 инвалидирует только эту строку ───↑ ↑─── ядро 1 — только эту ───↑ Теперь Requests и Errors гарантированно на разных строках кэша. Ядро 0, делая add к Requests, инвалидирует только line #1 на остальных ядрах — line #2 остаётся валидной на ядре 1. И наоборот. Две горутины могут обновлять свои счётчики независимо, не мешая друг другу.\nЦена — структура занимает 72 байта вместо 16 (в 4.5 раза больше), и в каждой строке кэша 56 байт «мусора». Для нескольких структур это незаметно; для массива из миллионов Stats — может раздуть потребление памяти и ухудшить cache locality при последовательном чтении. Поэтому padding применяют точечно, только для горячих полей.\nЭто забота уровня 1 (железо), но проявляется на уровне 5 (практика). Большая часть Go-кода никогда об этом не думает. Но если ты пишешь высоконагруженный коллектор метрик или горячую lock-free структуру данных, false sharing — одно из первых, что стоит поискать.\n4.6. Капкан double-checked locking Вот классический баг конкурентности, в инициализаторе синглтона:\nvar ( instance *Config mu sync.Mutex ) func GetConfig() *Config { if instance == nil { // БАГ: неатомарное чтение mu.Lock() defer mu.Unlock() if instance == nil { instance = loadConfig() // БАГ: неатомарная запись } } return instance } «Двойная проверка» — это две проверки instance == nil. Идея в том, чтобы избегать взятия мьютекса на общем пути (после инициализации), оставаясь при этом потокобезопасным во время инициализации.\nЭто сломано двумя способами:\nПервая instance == nil — неатомарное чтение. Модель памяти Go не гарантирует, что оно увидит запись, сделанную под мьютексом. Компилятор и CPU свободно переупорядочивают, кэшируют и иным образом плохо себя ведут. Даже если бы чтение сработало, instance = loadConfig() — неатомарная запись. Другая горутина в первой проверке может наблюдать частично сконструированный *Config. Правильный ответ в Go — sync.Once:\nvar ( instance *Config once sync.Once ) func GetConfig() *Config { once.Do(func() { instance = loadConfig() }) return instance } sync.Once использует атомарные операции внутренне, чтобы сделать общий путь (после инициализации) lock-free, и корректно обрабатывает все вопросы упорядочивания памяти. Это канонический ответ на «ленивая инициализация в конкурентном контексте».\nЕсли по какой-то причине нельзя использовать sync.Once, корректный ручной вариант использует атомарные load и store:\nvar instanceP atomic.Pointer[Config] func GetConfig() *Config { if c := instanceP.Load(); c != nil { return c } // ... берём мьютекс, перепроверяем через Load, строим, Store ... } Атомарные Load и Store дают гарантии упорядочивания памяти, которых нет у обычных чтения и записи.\n4.7. Шпаргалка Вот одностраничная выжимка. Когда ты смотришь на кусок кода и спрашиваешь «atomic или мьютекс?», пройдись по этой таблице.\nСценарий Рекомендация Почему Счётчик (requests++) atomic.Int64.Add(1) В 5-10 раз быстрее мьютекса, столь же корректен Булев флаг (stopped) atomic.Bool Однословное состояние, нет инварианта Single-word конечный автомат atomic.Int32 или atomic.Uint32 Кодируй состояния маленькими int Указатель на иммутабельный снэпшот atomic.Pointer[T] Чтения без блокировок, атомарная замена Обновить map + счётчик вместе sync.Mutex Мультиполевой инвариант требует критической секции Обновить slice на месте sync.Mutex Заголовок slice + backing array — несколько слов Read-heavy конфигурация (редкие обновления) atomic.Pointer[T] или sync.RWMutex atomic.Pointer если можешь заменить целиком; RWMutex если мутируешь на месте Per-goroutine состояние Без синхронизации Если трогает одна горутина — нет гонки Ленивая инициализация синглтона sync.Once Ручной double-checked locking — сломан Горячий путь с двумя независимыми счётчиками atomic.Int64 × 2 с padding False sharing, если в одной структуре Если из статьи ничего больше не запомнишь, запомни это: single-word состояние — atomic. Мультиполевой инвариант — мьютекс. Всё остальное — измеряй и думай.\n4.8. Как работает go test -race Мы уже несколько раз упоминали детектор гонок. Стоит понять, как он устроен, — это знание часто помогает быстрее находить причину ложных срабатываний или, наоборот, понимать, почему детектор что-то пропустил.\nДетектор гонок Go основан на ThreadSanitizer (TSan) — технологии, изначально разработанной в Google для C++ (статья Константина Серебряного и Тимура Исходжанова, WBIA 2009, \u0026ldquo;ThreadSanitizer – data race detection in practice\u0026rdquo;). Идея TSan: для каждого доступа к памяти сохранять в shadow memory (теневой памяти) след — какой поток, в какой момент, под какой блокировкой касался этой ячейки. При каждом новом доступе проверяем: было ли когда-то раньше конкурирующее обращение без happens-before отношения? Если да — сообщаем о гонке.\nКонкретно в Go реализация описана в официальном посте Go Blog \u0026ldquo;Introducing the Go Race Detector\u0026rdquo; (соавтор — Дмитрий Вюков, июнь 2013) и в документации Go. Она интегрирована в компилятор:\nПри сборке с -race компилятор instrumentation каждую инструкцию чтения/записи памяти, добавляя вызовы в runtime-часть TSan. TSan отслеживает happens-before отношения, опираясь на синхронизационные операции: mu.Lock/Unlock, ch \u0026lt;- / \u0026lt;-ch, atomic.*, sync.Once.Do, создание горутины. Shadow memory тратит примерно 8x RAM — поэтому детектор гонок дорогой по памяти (обычно 5-10x overhead на CPU и RAM). Запускать с -race в продакшене — плохая идея; в CI и нагрузочных тестах — обязательно. Типичные ловушки:\nЛожные срабатывания на коде, который не является гонкой по бизнес-логике, но нарушает формальную модель памяти. Например, чтение общей переменной без atomic «потому что мы знаем, что другой поток уже закончил» — TSan сработает, потому что формально гонка есть, даже если она benign. Правильно: либо закрыть синхронизацией (channel close + receive), либо atomic. Пропуск гонок из-за недостаточного покрытия тестами. TSan ловит только те гонки, которые фактически произошли во время тестового прогона. Если в продакшене есть редкий путь, который не покрыт тестами, — гонка ускользнёт. Поэтому детектор гонок — это дополнение к тестам, не замена им. Очень длинные трейсы. TSan выдаёт два стека для каждой гонки — оба доступа. Стеки могут быть глубокими, особенно в HTTP/gRPC обработчиках. Читать их нужно с конца (точка доступа) к началу (точка входа). Полезный паттерн: запускать go test -race в CI всегда, на каждый PR. И отдельно — нагрузочные тесты с -race на staging-е перед релизом, потому что многие гонки проявляются только под определённой нагрузкой.\n4.9. Сравнение с другими языками Полезно понимать, как то же самое решается в других mainstream-языках. Это и расширяет кругозор, и помогает при ревью мультиязычного кода.\nRust — std::sync::Mutex\u0026lt;T\u0026gt;. Идеологически близок к Go: тоже non-reentrant, тоже не копируемый (что в Rust ещё и enforced через ownership систему — Mutex не реализует Copy). Главное отличие — Rust хранит защищаемые данные внутри Mutex\u0026lt;T\u0026gt;:\nlet counter = std::sync::Mutex::new(0u64); *n.lock().unwrap() += 1; // lock() возвращает MutexGuard, который解锁ается при drop Типобезопасность на уровне компилятора: невозможно получить доступ к внутренним данным без вызова lock(). В Go ты можешь случайно обратиться к полю мимо мьютекса, и компилятор промолчит.\nJava — synchronized блоки и java.util.concurrent.locks.ReentrantLock. Мьютекс реентерабельный по умолчанию — тот же поток может войти в synchronized метод несколько раз. Это удобнее в некоторых случаях (recursive AST walkers), но скрывает ошибки проектирования. На уровне JVM есть встроенная поддержка мониторов (instrinsics), в HotSpot оптимизировано до lock-biased и thin-lock паттернов — фактически Java идёт по тому же пути userspace CAS + kernel slow path, что и Go.\nC++ — std::mutex. Non-reentrant (как в Go), копирование запрещено через = delete. Главный плюс C++ — std::lock_guard и RAII: блокировка освобождается автоматически при выходе из scope. В Go приходится писать defer mu.Unlock(), что эквивалентно, но более verbose. Зато в C++ есть и std::recursive_mutex, и std::shared_mutex (аналог RWMutex) — больше выбора, но и больше шансов ошибиться.\nPython — threading.Lock(). GIL (Global Interpreter Lock) делает большинство операций фактически однопоточными, поэтому мьютекс нужен в основном при работе с I/O или C-расширениями. Семантика простая, но производительность в CPython ограничена GIL.\nОбщий паттерн: во всех языках мьютексы построены поверх CAS и оптимизированы для uncontended case. Идея одна — userspace fast path + kernel slow path. Разница в API и гарантиях: Rust строгий, Java реентерабельный, C++ гибкий, Go минимальный. Выбор Go — минимум примитивов, отсутствие реентерабельности, последовательно-согласованное упорядочивание — последовательный набор компромиссов, направленных на простоту рассуждения ценой иногда потерянной производительности.\n4.10. Частые баги из практики Несколько антипаттернов, которые я видел в реальном Go-коде. Большая часть — variations на темы из этой статьи, но стоят отдельного упоминания, потому что встречаются часто.\nАнтипаттерн 1: sync.Mutex в field структур, которая передаётся по значению.\ntype Task struct { mu sync.Mutex state int } func process(t Task) { // БАГ: приёмник по значению t.mu.Lock() defer t.mu.Unlock() t.state++ } go vet ловит, но если поле скрыто в nested struct (например, t.inner.mu), проверка может не сработать на старых версиях Go. Исправление — всегда pointer receiver для типов с Mutex.\nАнтипаттерн 2: sync.WaitGroup после копирования.\nWaitGroup содержит noCopy маркер и go vet его ловит, но копирование в closure тоже может пройти мимо:\nfunc runAll(tasks []Task) { var wg sync.WaitGroup for _, t := range tasks { wg.Add(1) go func(wg sync.WaitGroup, t Task) { // БАГ: wg передан по значению defer wg.Done() t.Do() }(wg, t) } wg.Wait() // зависнет, потому что Add делался на другой копии } Правильно — передавать указатель, или использовать defer в outer scope:\ngo func(t Task) { defer wg.Done() t.Do() }(t) Антипаттерн 3: чтение map под RLock, запись под Lock, без дополнительных гарантий.\ntype Store struct { mu sync.RWMutex data map[string]int } func (s *Store) Get(k string) int { s.mu.RLock() defer s.mu.RUnlock() return s.data[k] // OK под RLock } func (s *Store) Inc(k string) { s.mu.Lock() defer s.mu.Unlock() s.data[k]++ // OK под Lock } Само по себе это не баг — RWMutex корректно разделяет readers и writer. Проблема появляется, когда кто-то начинает оптимизировать: «у нас же 99% чтений, давайте RLock и для записи». Если несколько горутин одновременно делают s.data[k] = v под RLock — concurrent map writes, panic. RWMutex не делает map потокобезопасным; он лишь сериализует writers относительно readers.\nАнтипаттерн 4: блокировка, захваченная в одной горутине, отпущенная в другой.\nfunc (s *Server) Handle(req Request) { s.mu.Lock() go func() { // ... долгая работа ... s.mu.Unlock() // формально разрешено, но капкан }() } Go разрешает отпустить мьютекс из другой горутины, но это нарушает принцип «владелец блокировки — тот, кто её взял», и часто приводит к:\nГонкам, если кто-то между Lock и goroutine start решит, что владеет критической секцией. Дедлокам, если родительская горутина завершится раньше, чем успеет запустить goroutine. Сложностям в рассуждении: при ревью непонятно, кто же владеет блокировкой. Исправление — либо держать блокировку в той же горутине, либо использовать канал для явной передачи владения.\n4.11. Кратко про sync.RWMutex В этой статье мы intentionally не разбираем sync.RWMutex подробно — это тема для отдельного материала. Но для полноты картины стоит понимать главное отличие от sync.Mutex.\nsync.RWMutex разделяет блокировки на два типа:\nRLock/RUnlock — разделяемая блокировка для читателей. Несколько читателей могут держать RLock одновременно. Lock/Unlock — эксклюзивная блокировка для писателей. Только один writer, и ни одного reader, пока writer держит Lock. Идея: для read-heavy данных (90%+ чтений) RWMutex позволяет параллельное чтение, что должно быть быстрее, чем serialize всех через Mutex.\nРеальность — интереснее:\ntype RWMutex struct { w Mutex // serialize writers writerSem uint32 // semaphore для writers readerSem uint32 // semaphore для readers readerCount atomic.Int32 // количество активных readers (или -1 если есть writer) readerWait atomic.Int32 // количество readers, которых должен дождаться writer } Поле readerCount тут — ключ. При RLock читатель делает atomic.AddInt32(\u0026amp;readerCount, 1). Если результат ≥ 0 — reader спокойно работает. Если результат \u0026lt; 0 — значит, есть активный writer (writer при Lock делает atomic.AddInt32(\u0026amp;readerCount, -rwmutexMaxReaders), делая все новые RLock\u0026rsquo;и видимыми как отрицательные), и читатель паркуется.\nПодводные камни:\nRWMutex не всегда быстрее Mutex. На каждый RLock делается atomic add — это не бесплатно. Если критическая секция короткая (несколько наносекунд), Mutex может быть быстрее за счёт более простого кода. RWMutex выигрывает только когда читателей много и критическая секция длинная. Writer starvation. В старых версиях Go writer мог ждать бесконечно, если постоянно приходили новые readers. С Go 1.0 RWMutex решает это: новый reader, увидев ожидающего writer, паркуется, давая writer\u0026rsquo;у шанс. Не используй RWMutex «на всякий случай». Если профайлинг не показывает contention на Mutex — RWMutex скорее потеряет, чем выиграет. Сначала измерь. Подробнее о RWMutex internals — в исходниках Go src/sync/rwmutex.go (хорошо прокомментированы) и в общем в серии статей VictoriaMetrics про sync.\n4.12. TryLock: используй осторожно С Go 1.18 у sync.Mutex появился метод TryLock() bool — попытаться захватить блокировку без блокирования, сразу вернуться с результатом. Если удалось — true, владение получено, нужно вызвать Unlock. Если нет — false, ничего не произошло.\nДокументация Go предупреждает прямо: «правильные применения TryLock существуют, но редки, и его использование часто признак более глубокой проблемы в работе с мьютексами». Вот типичные сценарии, которые хочется решать через TryLock, и почему обычно это плохая идея:\nСценарий 1: «попробовать обновить кэш, не блокируя обработку запросов».\nfunc (c *Cache) MaybeRefresh() { if c.mu.TryLock() { // попробуем захватить defer c.mu.Unlock() c.refresh() } } Кажется разумным: если кэш уже обновляется, не ждём — просто пропускаем. Проблема в том, что TryLock под нагрузкой будет часто проваливаться даже когда блокировка свободна — из-за нечестности (см. barging в разделе 3.6), TryLock может проигрывать конкурентам. Если обновление критично, нужно ждать; если некритично — лучше вообще не делать его в горячем пути.\nСценарий 2: «проверить, занят ли ресурс, без ожидания».\nif !resource.mu.TryLock() { return errors.New(\u0026#34;resource busy\u0026#34;) } defer resource.mu.Unlock() // ... работа с ресурсом ... Это легитимный use case — особенно в административных API (например, «не запускать миграцию, если уже идёт»). Но чаще правильный ответ — использовать явный флаг atomic.Bool, обозначающий состояние «занято», а не блокировку как индикатор занятости. Блокировка — это механизм эксклюзии, а не состояние; смешивать их — частый источник багов.\nКогда TryLock действительно нужен:\ndeadlock detection в сложных иерархиях блокировок (reverse lock ordering checks) реализация cancellable operations (когда ждать блокировку нужно с таймаутом) библиотечный код, где нельзя блокироваться надолго (например, внутри finalizer) В большинстве прикладного Go-кода TryLock — лишнее. Если ты дотягиваешься до него, остановись и подумай: возможно, правильный ответ — переструктурировать код, чтобы не нужно было «пробовать без блокировки».\n4.13. Практический пример: счётчик запросов в HTTP-сервисе Чтобы закрепить материал, вот типичная задача из backend-разработки и три способа её решить, с объяснением, что когда выбирать.\nЗадача: HTTP-сервис обрабатывает миллионы запросов в секунду. Нужно считать:\nобщее количество запросов количество ошибок (4xx + 5xx) количество запросов в полёте (active) Метрики экспонируются через /metrics endpoint раз в 15 секунд.\nРешение 1: atomic для всего.\ntype Metrics struct { Total atomic.Int64 Errors atomic.Int64 Active atomic.Int64 } func (m *Metrics) Handle(req *http.Request, next http.Handler) { m.Total.Add(1) m.Active.Add(1) defer m.Active.Add(-1) rw := \u0026amp;statusRecorder{ResponseWriter: w, status: 200} next.ServeHTTP(rw, req) if rw.status \u0026gt;= 400 { m.Errors.Add(1) } } Это лучшее решение для данного сценария. Три независимых счётчика, каждый обновляется атомарно. Никаких блокировок, минимальный overhead (несколько наносекунд на запрос). На hot path — atomic.Add × 3, что в сумме даёт ~20-30 наносекунд на запрос — незаметно на фоне обработки.\nРешение 2: Mutex для всего.\ntype Metrics struct { mu sync.Mutex total int64 errors int64 active int64 } func (m *Metrics) Handle(req *http.Request, next http.Handler) { m.mu.Lock() m.total++ m.active++ m.mu.Unlock() defer func() { m.mu.Lock() m.active-- m.mu.Unlock() }() // ... обработка ... } Хуже — на каждый запрос две пары Lock/Unlock. Под высокой нагрузкой (~1M RPS) это может вылиться в 5-10% CPU только на управление блокировками. atomic быстрее в 5-10 раз. Не делай так для счётчиков.\nРешение 3: RWMutex для чтения метрик.\ntype Metrics struct { Total atomic.Int64 Errors atomic.Int64 Active atomic.Int64 } func (m *Metrics) Snapshot() (total, errors, active int64) { // Читаем три значения. Между ними могут быть гонки — // total может быть прочитан до errors, а errors после. // Это нормально для метрик — небольшая несогласованность допустима. return m.Total.Load(), m.Errors.Load(), m.Active.Load() } Здесь не нужен RWMutex — метрики tolerate eventual consistency (несогласованность в несколько мс между total и errors не критична). Snapshot через три atomic.Load — самое дешёвое решение.\nЕсли бы нужна была строгая согласованность (например, total \u0026gt;= errors всегда, чтобы избежать артефактов на графиках), тогда — Mutex вокруг чтения трёх значений. Но для метрик это overkill.\nВывод: для счётчиков в горячем пути — atomic всегда. Для редко обновляемой конфигурации — atomic.Pointer[T]. Mutex — когда инвариант между полями важен (например, в кэше: размер и содержимое map должны быть согласованы).\n5. Что почитать дальше Прежде чем перейти к заключению — annotated список для углублённого изучения. Я разделил на категории.\nКлассические papers:\nMaurice Herlihy, Nir Shavit, \u0026ldquo;The Art of Multiprocessor Programming\u0026rdquo; (книга, 2012, 2nd edition 2020). Библия lock-free и wait-free программирования. Herlihy — тот самый автор статьи про consensus number. Книга требует математической зрелости, но даёт фундаментальное понимание. Hans-J. Boehm, \u0026ldquo;Threads Cannot Be Implemented as a Library\u0026rdquo; (PLDI 2005). Аргумент, почему threading должен быть на уровне языка, а не библиотеки. Объясняет, почему C++0x потребовалась memory model. Paul McKenney, \u0026ldquo;Memory Barriers: a Hardware View for Software Hackers\u0026rdquo; (2010). Глубокий разбор того, что на самом деле делает CPU при memory barrier. Для тех, кто хочет понять железо. Go-specific:\nRuss Cox, \u0026ldquo;Hardware Memory Models\u0026rdquo; и последующие статьи в серии — модель памяти от железа до языка. Best explanation из существующих, на любом языке. Dmitry Vyukov, \u0026ldquo;Scalable Go Scheduler Design Doc\u0026rdquo; (May 2012) — design doc планировщика, основа всего runtime. Блог VictoriaMetrics — серия статей про sync: Mutex, WaitGroup, Pool, Cond, Map, Once. Очень качественный разбор исходников (статьи про RWMutex пока нет). Benchmarking и performance:\ngoperf.dev — официальный Go Optimization Guide от команды Go. Раздел Atomic Operations — must-read. benchstat — инструмент для статистически корректного сравнения бенчмарков. Без него сравнивать ns/op бессмысленно. Dave Cheney, \u0026ldquo;High Performance Go\u0026rdquo; — talks и статьи про оптимизацию Go-кода. Для продвинутых — lock-free структуры:\nDmitry Vyukov, \u0026ldquo;Mpsc queue\u0026rdquo; — каноническая multi-producer single-consumer очередь, на которой построены многие Go runtime internals. Tony Tene, \u0026ldquo;Lock-free hash table\u0026rdquo; и блог 1024cores.net в целом — глубокий материал по lock-free программированию. 6. Заключение Мы прошли весь стек. От counter++, разворачивающегося в три инструкции ARM64, через CAS как универсальный примитив (Херлихи, 1991), через LOCK CMPXCHG и ARM LSE, через пакет sync/atomic с его единственным последовательно-согласованным упорядочиванием памяти, через futex как мост между userspace и ядром, через упакованное поле state в sync.Mutex с его spinning и режимом голодания — и наконец к практическим рекомендациям.\nДва главных вывода:\nMutex и atomic — один стек, а не два инструмента. Mutex построен на атомарном CAS, со слоем честности поверх. Uncontended mu.Lock() — это одна инструкция CAS — едва дороже, чем atomic.CompareAndSwapInt32. Разница в стоимости появляется только под нагрузкой, и именно там парковка мьютекса (вместо активного spinning) реально выигрывает. Режим голодания — не теория. Он существует, потому что реальный продакшен-код в 2015 году получал время захвата блокировки в 100 секунд (issue #13086). Порог в 1мс внутри sync.Mutex молча защищает твой код от этой патологии каждый день. Если хочешь копнуть глубже, ссылки ниже — канонический список для чтения. Исходники Go (sync/mutex.go, runtime/sema.go) читаемые, хорошо прокомментированные и не такие страшные, как выглядят. Заметки Рассела Кокса о ревизии модели памяти и дискуссия в issue #13086 — два исторических контекста, объясняющих, почему код выглядит именно так.\nСледующая статья серии Go Internals будет про sync.Pool — он строится поверх sync/atomic и per-P локального хранилища, давая Go одну из самых дешёвых стратегий аллокации объектов среди мейнстримных языков. Оставайтесь с нами.\nСсылки Модель памяти Go — go.dev/ref/mem Release notes к Go 1.19 (атомарные типы, ревизия модели памяти) — go.dev/doc/go1.19 Рассел Кокс, заметки о ревизии модели памяти Go — research.swtch.com/gomm Go issue #13086 — \u0026ldquo;runtime: fall back to fair locks after repeated sleep-acquire failures\u0026rdquo; — github.com/golang/go/issues/13086 Ханс-Й. Бём, Сарита В. Адве, \u0026ldquo;Foundations of the C++ Concurrency Memory Model,\u0026rdquo; PLDI 2008 — dl.acm.org/doi/10.1145/1375581.1375591 Морис Херлихи, \u0026ldquo;Wait-Free Synchronization,\u0026rdquo; ACM TOPLAS 13(1), 1991 — cs.brown.edu/people/mph/Herlihy91/p124-herlihy.pdf Даг Ли, \u0026ldquo;The java.util.concurrent Synchronizer Framework\u0026rdquo; (статья AQS), 2003 — gee.cs.oswego.edu/dl/papers/aqs.pdf Хубертус Франке, Мэттью Кирквуд, Инго Молнар, Ульрих Дреппер, \u0026ldquo;Fuss, Futexes and Furwocks: Fast Userlevel Locking in Linux,\u0026rdquo; OLS 2002 — kernel.org/doc/ols/2002/ols2002-pages-479-494.pdf Linux futex(2) man page — man7.org/linux/man-pages/man2/futex.2.html Ульрих Дреппер, \u0026ldquo;What Every Programmer Should Know About Memory,\u0026rdquo; 2007 — people.freebsd.org/~lstewart/articles/cpumemory.pdf Исходник Go sync/mutex.go (Go 1.24) — cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/internal/sync/mutex.go Блог VictoriaMetrics, \u0026ldquo;Go sync.Mutex: Normal and Starvation Mode\u0026rdquo; — victoriametrics.com/blog/go-sync-mutex goperf.dev, \u0026ldquo;Atomic Operations and Synchronization Primitives\u0026rdquo; — goperf.dev/01-common-patterns/atomic-ops \u0026ldquo;Mutex or Atomics? Choosing the Right Tool in Go\u0026rdquo; — thecodinggopher.substack.com/p/mutex-or-atomics-choosing-the-right ","permalink":"https://triumphpc.github.io/blog/ru/posts/sync-mutex-atomic/","summary":"Mutex и atomic — это не два независимых инструмента, а один стек абстракций. Я разбираю этот стек снизу вверх и обратно: почему counter++ — это три инструкции ARM64, как CAS стал универсальным примитивом (Herlihy, 1991), почему Mutex построен поверх atomic CAS, и как дискуссия Рассела Кокса о barging vs handoff (issue #13086) дала Go порог голодания в 1 миллисекунду. С публичными бенчмарками, битовой раскладкой состояния Mutex и шпаргалкой, что когда использовать.","title":"Go Internals: sync.Mutex и sync/atomic — от data race до starvation mode"},{"content":"В Части 5 я обозрел подходы к памяти агента: от файлового мозга OpenClaw до управляемого слоя mem0. Мы выбрали гибрид — слои памяти как у MemPalace, файловая структура как у OpenClaw. Но обзор — это одно, а инженерная механика — совсем другое. Как именно файлы попадают в контекст? Почему одни инжектятся всегда, а другие — по требованию? Как агент вообще узнаёт, что ему нужно что-то прочитать? И что происходит, когда память превращается в помойку — кто её чистит?\nВ этой статье — deep dive в механику. Пять файлов памяти, четыре стратегии контекст-сборки, TOOLS.md с двумя источниками ответственности, и — самое интересное — Dreaming: фоновая консолидация памяти, которую Anthropic и OpenAI реализовали в 2026 году. Я покажу, какие инженерные решения принимаются за кулисами, почему мы выбрали именно такой путь, и когда придётся его менять.\n1. Пять файлов памяти: полный каталог В первой части я описал четыре слоя памяти и упомянул SOUL.md, ABOUT.md, TOOLS.md. Но реальная файловая модель — богаче. Пять файлов, каждый со своей зоной ответственности, своим способом попадания в контекст и своим scope-ом изоляции.\n1.1. Исчерпывающая таблица # Файл Предназначение Слой (Scope) Когда попадает в контекст Как попадает Кто пишет 1 SOUL.md Идентичность и характер агента: ценности, стиль общения, личность agent Всегда (каждый запрос) Bootstrap-инъекция в system prompt Пользователь через UI / Агент через memory tools 2 ABOUT.md Профиль и описание агента: роль, специализация, задачи agent Всегда (каждый запрос) Bootstrap-инъекция в system prompt Пользователь через UI / Агент через memory tools 3 TOOLS.md Пользовательские заметки по инструментам: quirks, примеры, ограничения agent Всегда (каждый запрос) Bootstrap-инъекция (мердж с auto-generated) Пользователь через UI / Агент через memory tools. Lazy lifecycle: файл не создаётся по умолчанию 4 USER.md Профиль пользователя: имя, предпочтения, контекст работы — для конкретного агента agent_user По требованию (агент решает) Через memory tools Агент через memory tools / Пользователь через чат 5 MEMORY.md Персистентная память: факты, решения, контекст диалогов для конкретного агента+пользователя agent_user По требованию (агент решает) Через memory tools Агент через memory tools (auto-capture) Ключевое разделение — injection vs on-demand. Три файла (SOUL, ABOUT, TOOLS) инжектятся в system prompt при каждом запросе. Это «горячая» память — агент всегда видит свою идентичность и описание инструментов. Два файла (USER, MEMORY) — по требованию, через memory tools. Агент решает сам, когда ему нужен контекст пользователя.\ngraph TB subgraph \"Bootstrap Injection (~1-2K tokens, КАЖДЫЙ запрос)\" SOUL[\"SOUL.md\nидентичность\"] ABOUT[\"ABOUT.md\nпрофиль\"] TOOLS[\"TOOLS.md\nзаметки по тулам\"] end subgraph \"On-demand (через memory tools, по решению агента)\" USER[\"USER.md\nпрофиль пользователя\"] MEMORY[\"MEMORY.md\nдолговременная память\"] end SOUL --\u003e SP[\"System Prompt\"] ABOUT --\u003e SP TOOLS --\u003e SP SP --\u003e LLM[\"LLM\"] LLM --\u003e|\"memory tools\"| USER LLM --\u003e|\"memory tools\"| MEMORY style SOUL fill:#6a1b9a,color:#fff style ABOUT fill:#6a1b9a,color:#fff style TOOLS fill:#6a1b9a,color:#fff style USER fill:#e65100,color:#fff style MEMORY fill:#e65100,color:#fff 1.2. SOUL.md — идентичность SOUL.md — это «кто я». Ценности, характер, стиль общения. Лимит — 2000 символов. Инжектится в каждый запрос как секция ## Agent Identity в system prompt.\nПочему не больше? Потому что каждый символ в SOUL.md оплачивается на каждый запрос. 2000 символов ≈ 500 токенов. Если увеличить до 10K — агент будет тратить 2.5K токенов только на собственную идентичность, прежде чем скажет хоть слово. Для платформы с сотнями агентов это неприемлемо.\nКто пишет? Пользователь через UI — задаёт характер при создании агента. Или сам агент через memory tools — когда пользователь просит «будь более формальным» или «используй русский язык по умолчанию».\n1.3. ABOUT.md — профиль ABOUT.md — это «чем я занимаюсь». Роль, специализация, типичные задачи. Тот же лимит — 2000 символов, та же инъекция в каждый запрос.\nЗачем отдельный файл, если можно объединить с SOUL? Разделение identity и profile — не случайно. Identity (SOUL) — это постоянная величина, она редко меняется. Profile (ABOUT) — более динамичный: «сейчас я помогаю с миграцией на gRPC» → через месяц → «помогаю с настройкой мониторинга». Раздельные файлы позволяют обновлять профиль, не трогая ядро личности.\n1.4. USER.md — профиль пользователя для конкретного агента А вот тут начинается интересное. USER.md хранится в scope agent_user — это значит, что у каждого агента свой профиль для каждого пользователя. Агент А знает, что «Алиса предпочитает YAML» — но агент Б в том же проекте этого не знает, пока сам не прочитает.\nЧто хранится:\nКатегория Примеры Идентификация Имя, timezone, язык, роль Предпочтения «Отвечай кратко», «используй YAML», «формат таблиц» Рабочий контекст «Работает с Python, использует FastAPI», «PM проекта X» Инструменты «Основной календарь — Google», «Todoist для задач» Постоянные заметки «Стендап в 9:00 Пн-Пт», «Избегать встреч в пятницу» Лимит — 2000 символов. И вот почему USER.md — on-demand, а не injection: не всем агентам нужен профиль пользователя. Coding-ассистент — да, ему важно знать, что вы предпочитаете Go. А агент-суммаризатор текстов — ему всё равно, какой у вас timezone. Зачем тратить 500 токенов на профиль, который агент не использует?\n1.5. MEMORY.md — долговременная память MEMORY.md — это «что я знаю». Ключевые решения, контекст клиентов, текущие задачи, выводы из диалогов. Scope agent_user — привязан к паре «агент + пользователь». Лимит — 5000 символов (soft limit — при превышении агент получает инструкцию сжать).\nMEMORY.md реализует паттерн write-manage-read, который я описывал в первой части:\nWrite (кто и когда сохраняет):\nАгент — через memory tools, когда выявляет важный факт Auto-capture (future): перед compaction/завершением сессии — автоматический flush важных фактов Manage (сжатие, дедупликация):\nАгент получает инструкцию: «Регулярно сжимай MEMORY.md, удаляй устаревшие факты» Если MEMORY.md \u0026gt; 5000 символов — инструкция агенту сжать Future: автоматическая дедупликация через LLM Read (когда поднимается):\nАгент читает через memory tools по требованию Инструкция в system prompt: «search memory before acting» (паттерн OpenClaw) НЕ инжектится в system prompt на boot (экономия токенов) Почему MEMORY.md — on-demand, а не injection? Потому что MEMORY.md растёт. Сегодня — 500 символов, через месяц — 5000. Если инжектить — каждый запрос будет тратить всё больше токенов на память. OpenClaw решает это truncation\u0026rsquo;ом (обрезает \u0026gt; 20K символов), но обрезание означает потерю данных. Мы предпочитаем, чтобы агент сам решал, что ему нужно из памяти прямо сейчас.\n1.6. Сводка по слоям ┌──────────────────────────────────────────────────────────┐ │ СЛОИ ПАМЯТИ АГЕНТА │ ├──────────────┬──────────────┬───────────────┬───────────┤ │ Слой │ Файлы │ Загрузка │ │ ├──────────────┼──────────────┼───────────────┤ │ │ Agent │ SOUL.md │ Всегда │ │ │ │ ABOUT.md │ Всегда │ Injection │ │ │ TOOLS.md │ Всегда │ │ ├──────────────┼──────────────┼───────────────┤ │ │ Agent+User │ USER.md │ По требованию │ On-demand │ │ │ MEMORY.md │ По требованию │ (tools) │ └──────────────┴──────────────┴───────────────┴───────────┘ Агент получает Prompt Hint в system prompt: \u0026#34;У тебя есть memory tools. Загрузи USER.md и MEMORY.md при необходимости.\u0026#34; Два scope\u0026rsquo;а, два уровня изоляции. agent — общие файлы агента, видны всем пользователям. agent_user — персональная память, изолированная для пары «агент + пользователь». Никакой утечки контекста между пользователями.\n2. Стратегия контекст-сборки: почему Hybrid v2 В первой части я сказал, что мы выбрали гибридную стратегию — инъекция для горячего контекста, инструменты для остального. Но я не рассказал, какие варианты мы рассматривали и почему отбросили. А это, пожалуй, самое важное архитектурное решение во всей системе памяти.\n2.1. Вариант 1: Full Injection (как OpenClaw) Все файлы памяти читаются из хранилища и инжектятся в system prompt при каждом запросе.\ngraph LR S3[(Storage)] --\u003e|\"read all files\"| ASM[Context Assembler] ASM --\u003e SP[SystemPrompt] SP --\u003e LLM[LLM] subgraph \"Всегда инжектится (~5-10K tokens)\" SOUL[\"SOUL.md\"] ABOUT[\"ABOUT.md\"] USER[\"USER.md\"] MEMORY[\"MEMORY.md\"] TOOLS[\"TOOLS.md\"] end SOUL --\u003e ASM ABOUT --\u003e ASM USER --\u003e ASM MEMORY --\u003e ASM TOOLS --\u003e ASM Плюсы: простая реализация (~30-50 строк), предсказуемый контекст, легко отлаживать.\nМинусы: 5-10K токенов на каждый запрос, несколько reads из хранилища, MEMORY.md растёт → truncation → потеря данных, агент не может обновлять файлы через инструменты.\nOpenClaw именно так и делает. И это работает — для coding-ассистента с одним пользователем. Но на платформе с сотнями агентов, где каждый запрос оплачивается, сжигать 10K токенов на boot — роскошь.\n2.2. Вариант 2: On-Demand (Tool-Based) Файлы памяти НЕ инжектятся. Агент получает memory tools и сам решает, что читать.\nПлюсы: 0 токенов от памяти на boot, агент может обновлять файлы, масштабируется неограниченно.\nМинусы: агент может «забыть» прочитать контекст → некачественные ответы. Каждое чтение = итерация react-loop. Сложнее отлаживать — контекст зависит от поведения агента.\nЭто крайность, противоположная Full Injection. Дёшево на старте, но ненадёжно. Агент без идентичности — как человек без имени: может работать, но не знает, кто он.\n2.3. Вариант 3: Hybrid v1 — L0/L1 Boot + L2 Tools L0 (SOUL, ABOUT) — всегда инжектятся. L1 (USER) — тоже инжектится. L2+ (MEMORY) — через инструменты.\nПлюсы: ~2-3K на boot, предсказуемая база, близко к MemPalace pattern.\nМинусы: USER.md тратит токены, даже если агенту не нужен профиль пользователя. На платформе с 50+ типами агентов далеко не всем нужен USER.md — суммаризатор, переводчик, ревьюер кода не взаимодействуют с пользователем персонально.\n2.4. Вариант 3b: Hybrid v2 — Minimal Injection + Prompt Hint ← выбран Ключевой инсайт из исследования OpenClaw: даже OpenClaw не инжектит MEMORY.md в system prompt. Если memory tools доступны, агент получает hint «use memory_search», а MEMORY.md загружает самостоятельно. USER.md в Codex-интеграции тоже не всегда инжектится.\nОтсюда — Hybrid v2: минимальная инъекция (SOUL + ABOUT + TOOLS) + prompt hint, направляющий агента к memory tools для USER.md и MEMORY.md.\ngraph TD subgraph \"System Prompt Injection (~1-2K tokens)\" SOUL2[\"SOUL.md / ABOUT.md\nидентичность агента\"] TOOLS2[\"TOOLS.md\nзаметки по инструментам\"] AUTO[\"Автогенерация тулов\nописание инструментов\"] end subgraph \"On-demand через memory tools\" USER2[\"USER.md\nпрофиль пользователя\"] MEMORY2[\"MEMORY.md\nдолговременная память\"] end SOUL2 --\u003e SP2[\"System Prompt\"] TOOLS2 --\u003e SP2 AUTO --\u003e SP2 SP2 --\u003e LLM2[\"LLM\"] LLM2 --\u003e|\"memory tools\"| USER2 LLM2 --\u003e|\"memory tools\"| MEMORY2 Prompt Hint (добавляется в system prompt):\nУ тебя есть доступ к memory tools. При необходимости загрузи USER.md (профиль пользователя) и MEMORY.md (долговременная память).\n2.5. Сравнение четырёх вариантов Критерий Full Injection On-Demand Hybrid v1 Hybrid v2 Токены на boot ~5-10K 0 ~2-3K ~1-2K Предсказуемость контекста ✅ Все файлы всегда ❌ Агент может забыть ✅ L0/L1 всегда ⚠️ L1 через prompt hint Агент обновляет файлы ❌ Только API ✅ Через tools ⚠️ L2+ через tools ✅ Все через tools Reads из хранилища на boot 5+ 0 3-4 2-3 Масштабируемость ❌ MEMORY.md растёт ✅ ⚠️ USER.md жжёт токены ✅ Сложность реализации Простой Средний Сложный Средний Почему не Hybrid v1? Потому что USER.md в injection — это токены, которые тратятся впустую, когда агенту не нужен профиль. На платформе с десятками типов агентов (суммаризатор, переводчик, аналитик кода) далеко не каждый агент взаимодействует с пользователем персонально. Зачем грузить USER.md в каждый запрос?\nИтог: Hybrid v2 — минимальный токен-бюджет на boot, максимальная гибкость, согласованность с тем, как OpenClaw реально работает (не инжектит MEMORY.md, а даёт hint).\n2.6. Prompt Hint — как агент знает, что читать Агент не прочитает USER.md или MEMORY.md сам по себе — ему нужно сказать об этом. Prompt Hint — это инструкция, добавляемая в system prompt:\nУ тебя есть memory tools. Загрузи USER.md (профиль пользователя) и MEMORY.md (долговременная память) при необходимости.\nЭто аналог подхода OpenClaw: агент получает hint «use memory_search», а MEMORY.md загружает самостоятельно. Мы добавили «при необходимости» — агент решает, нужен ли ему контекст пользователя в данном запросе.\nРиск: агент может «забыть» загрузить USER.md → ответы без персонального контекста. Митигация: чёткий Prompt Hint + примеры в system prompt («В начале сессии загрузи USER.md и MEMORY.md»). На практике — USER.md маленький (~200-500 токенов), загрузка занимает одну итерацию react-loop.\n3. TOOLS.md: два источника — две ответственности TOOLS.md — самый нетривиальный файл памяти. В системе уже существует программный механизм описания инструментов: ToolSet.RegisterInAgent() генерирует SystemPromptInjection с описанием инструментов для LLM. Зачем ещё один источник?\n3.1. Проблема: механика ≠ практика Система автоматически генерирует описание каждого инструмента: название, параметры, типы, краткое описание. Этого достаточно, чтобы LLM понял как вызвать инструмент. Но auto-generated описание не содержит практического опыта — а именно он определяет, когда и зачем агент будет обращаться к инструменту.\nЧто auto-generated даёт (механика):\nНазвания инструментов и их параметров Типы данных и обязательность параметров Краткое описание из ToolSettings Чего auto-generated НЕ даёт (практика):\nКогда лучше использовать конкретный инструмент, а когда другой Особенности работы с конкретными данными (quirks) Типичные паттерны использования в контексте проекта Ограничения, неочевидные из описания (rate limits, размер ответа) Предпочтения пользователя по работе с инструментами Разрыв между «как вызвать» и «когда использовать» — это разрыв между документацией API и реальным опытом работы с ним. TOOLS.md заполняет этот разрыв.\n3.2. Решение: auto-generated + user extensions System Prompt (tools section) формируется из двух источников:\n{auto-generated description from RegisterInAgent()} ← механика (в памяти) --- ## Tool Notes {contents of TOOLS.md, if exists} ← практика (из хранилища) Два источника — две ответственности:\nИсточник Где хранится Кто обновляет Что содержит Auto-generated В памяти (system prompt) Runtime при каждом запросе Механика: имена, параметры, типы, списки TOOLS.md В хранилище Пользователь (UI) / Агент (memory tools) Практика: tips, quirks, примеры, warnings Ключевой инвариант: auto-generated часть существует только в памяти и никогда не записывается в хранилище. TOOLS.md хранится персистентно и никогда не перезаписывается автоматически. Два источника не конфликтуют.\n3.3. Примеры Knowledge DB (RAG) Auto-generated (формируется при каждом запросе):\nsearch_knowledge_db: Search documents in knowledge databases. Parameters: query (string), db_name (string, optional) Available knowledge databases: - \u0026#34;Product Docs\u0026#34; (product documentation) - \u0026#34;Internal Wiki\u0026#34; (internal processes) TOOLS.md (пользовательские заметки):\n## Knowledge DB Notes - Для поиска по Product Docs используй точные названия продуктов, не описания - Internal Wiki обновляется раз в неделю — если не нашёл актуальную информацию, попроси пользователя подтвердить - Рекомендуемый паттерн: сначала search → затем read_knowledge_db_document - Предел результата search — 5 чанков, если нужно больше — уточни запрос 3.4. Почему не «полностью авто» и не «полностью ручное» Полностью авто (вариант, который мы рассматривали): только auto-generated описание, без TOOLS.md. Плюс — всегда актуально, нет рассинхронизации. Минус — пользователь не может дописать quirks, tips, контекст использования. Агент видит «search_knowledge_db(query, db_name)», но не знает, что «для Product Docs нужны точные названия».\nПолностью ручное (как OpenClaw): пользователь/агент пишет TOOLS.md вручную, система ничего не генерирует. Плюс — полная кастомизация. Минус — при добавлении нового скилла или MCP-инструмента TOOLS.md устаревает. Пользователь должен не забыть обновить. А если забыл — агент видит описание трёх инструментов, хотя подключено пять.\nРазделение на auto-generated + user extensions решает обе проблемы: механика всегда актуальна (генерируется из ToolSettings), практика добавляется вручную (TOOLS.md). И когда появится MCP — auto-generated часть автоматически включит MCP-описания.\n3.5. Lazy lifecycle Ещё один нюанс: TOOLS.md не создаётся по умолчанию. Файл появляется в хранилище только когда пользователь или агент впервые запишет в него контент. «Пустой» TOOLS.md — это отсутствующий файл, а не пустая строка.\nЭто же правило действует для всех файлов памяти. Отсутствие файла — не ошибка:\nСценарий Поведение TOOLS.md не существует при injection Секция ## Tool Notes не добавляется SOUL.md / ABOUT.md не существуют Соответствующие секции опускаются Агент запрашивает несуществующий файл через memory tools Пустой результат, не ошибка Агент записывает новый файл через memory tools Файл создаётся Graceful degradation — новый агент без файлов памяти не падает, а работает с минимальным контекстом. Prompt hint добавляется всегда — даже если файлов ещё нет, агент знает, что инструменты существуют.\n4. Dreaming: фоновая консолидация памяти А теперь — самое интересное. Всё, что я описал выше, — это inline-механизм: агент пишет в MEMORY.md во время сессии, управляет размером через инструкцию «сжимай регулярно». Консолидации (дедупликация, разрешение противоречий, удаление устаревшего) — нет.\nВопрос: стоит ли добавить фоновый процесс консолидации, запускаемый после завершения сессий? И если да — как? Anthropic и OpenAI уже ответили «да» и реализовали это в 2026 году. Давай разберёмся, как они это делают.\n4.1. Anthropic: Auto Dream (Claude Code) 4-фазный фоновый процесс, запускаемый между сессиями отдельным sub-agent\u0026rsquo;ом (solaius, 2026):\nФаза Действие Результат 1. Orient Читает текущее состояние MEMORY.md и topic files Базовая карта памяти 2. Gather Signal Ищет в логах завершённых сессий новые факты, дрейф, противоречия. Grep, не полный read (экономия токенов) Сигналы к обновлению 3. Consolidate Мержит дубли, разрешает противоречия, нормализует время («вчера» → «2026-03-15»), удаляет устаревшее Обновлённые файлы памяти 4. Prune \u0026amp; Index Пересобирает MEMORY.md как индекс, обновляет ссылки, обеспечивает 200-line cap Чистый индекс graph LR LOGS[\"Логи завершённых\nсессий\"] --\u003e|\"Phase 1: Orient\"| MAP[\"Базовая карта\nпамяти\"] MAP --\u003e|\"Phase 2: Gather Signal\"| SIGNAL[\"Сигналы:\nновые факты, дрейф,\nпротиворечия\"] SIGNAL --\u003e|\"Phase 3: Consolidate\"| UPDATED[\"Обновлённые\nфайлы памяти\"] UPDATED --\u003e|\"Phase 4: Prune \u0026 Index\"| CLEAN[\"Чистый индекс\nMEMORY.md\"] style LOGS fill:#1565c0,color:#fff style CLEAN fill:#2e7d32,color:#fff Триггер: двойной гейт — 24+ часов с последней консолидации И 5+ новых сессий. Это не «после каждого диалога», а «когда накопилось достаточно материала». Умно: частые короткие сессии не запускают dreaming каждую минуту.\nБезопасность: sub-agent пишет только в memory/, не имеет git/npm/MCP инструментов. Не может сломать код, не может уйти в интернет — только чистка памяти.\nРезультаты (Harvey, legal AI): 6x рост task completion rate (solaius, 2026). Но Harvey — юридический AI, где память критична. Реалистичная оценка для типичных сценариев — 1.5–3x.\nСтоимость: стандартные токен-рейты Claude. 913 сессий консолидируются за 8–9 минут. При частоте «раз в сутки при 5+ сессиях» — это копейки на фоне основных LLM-вызовов.\n4.2. Anthropic: Dreams API (Managed Agents, Enterprise) Enterprise-версия Auto Dream с дополнительными возможностями (solaius, 2026):\nСвойство Значение Масштаб До 100 прошлых сессий за один dream Изоляция Входной store не модифицируется — создаётся новый output store Review gate Авто-применение или ревью перед применением Типы паттернов Recurring mistakes, workflow convergence, shared preferences Версионирование Каждая мутация — immutable memver_... с audit trail Review gate — ключевое отличие от базового Auto Dream. Enterprise не может позволить себе «LLM молча переписала память». Каждое изменение — на ревью. Версионирование — immutable: можно откатиться к любой предыдущей версии. Это критично для regulated workflows (healthcare, legal, finance).\n4.3. KAIROS (unreleased, внутренний Anthropic) А вот это — предварительный взгляд на архитектуру постоянных (always-on) агентов (solaius, 2026). Append-only логи + nightly dreaming.\nЛоги: logs/YYYY/MM/YYYY-MM-DD.md — append-only, никогда не модифицируются Nightly /dream дистиллирует логи в topic files и MEMORY.md Аналогия с WAL (Write-Ahead Log) в БД: полный audit trail + lean working memory Почему это важно? Потому что это решает фундаментальную проблему текущих подходов: что если dreaming удалит что-то важное? В KAIROS — не удалит никогда. Логи append-only, они не меняются. /dream создаёт новое представление (MEMORY.md), но оригинал остаётся. Как WAL в PostgreSQL: даже если checkpoint упал — логи на месте, можно восстановиться.\nЭто тот же принцип, что в событийном моделировании (event sourcing): состояние — это функция от истории событий. MEMORY.md — это projection, а логи — это event store. Projection можно пересоздать, event store — нельзя.\n4.4. OpenAI: Dreaming V3 (июнь 2026) Фоновый процесс синтеза, полностью заменивший ручные «saved memories» (OpenAI, 2026):\nПоколение Механизм Factual Recall 2024: Saved Memories Ручной список, явный «remember this» 41.5% 2025: Dreaming V0 Supplemental synthesis overlay поверх saved memories 67.9% 2026: Dreaming V3 Полностью автоматический синтез, заменяет saved memories 82.8% Эволюция впечатляет: с 41.5% до 82.8% за два года (OpenAI, 2026). Но ещё интереснее — как Dreaming V3 работает:\nЧитает всю историю диалогов пользователя Автоматически синтезирует профиль: факты, предпочтения, инструкции, неявные паттерны Self-updating: «пользователь едет в Сингапур в июле» → после поездки автоматически переписывается на «пользователь ездил в Сингапур в июле 2026» Запускается между диалогами, не внутри Self-updating — это киллер-фича. В нашем подходе MEMORY.md содержит «пользователь едет в Сингапур в июле». После июля — это устаревший факт. В Dreaming V3 — факт автоматически обновляется. Никакого ручного управления.\nНо есть и проблемы:\nПлатформа молча переписывает записи — нет лога ревизий (OpenAI, 2026; solaius, 2026). Вы не можете узнать, что было до перезаписи. Audit trail непрозрачен — критично для regulated workflows. В healthcare вы обязаны знать, почему агент принял решение, а если память молча изменилась — вы этого не узнаете. 5x сокращение compute позволило расширить на Free-тир — но ценой качества для сложных сценариев. 4.5. Сравнение подходов Аспект Auto Dream (Anthropic) Dreams API (Enterprise) KAIROS (unreleased) Dreaming V3 (OpenAI) Наш подход (inline) Когда capture После сессии После сессии Nightly После сессии Во время сессии Что анализирует Полные логи До 100 сессий Все логи Всю историю Только текущий контекст Консолидация Авто: мерж, противоречия, нормализация Авто + review gate Nightly distillation Авто: синтез + self-update Инструкция «сжимай MEMORY.md» Триггер 24ч + 5 сессий По запросу Nightly Между диалогами Нет триггера Аудит — immutable versions append-only логи (WAL) Нет ревизий Last-write-wins Стоимость Доп. LLM-вызов Доп. LLM-вызов Доп. LLM-вызов nightly Доп. compute (5x сокращено) Нет доп. стоимости Задержка обновления 24ч+ По запросу 1 ночь Между диалогами Мгновенно 4.6. Почему мы пока НЕ делаем dreaming Четыре причины:\nБазовый механизм достаточен — inline-запись через memory tools + инструкция «сжимай» покрывает 80% ценности. Память работает, факты сохраняются, агенты вспоминают контекст. Проблема «память превращается в помойку» — теоретическая, пока мы не увидели её в проде.\nИнфраструктура — dreaming требует worker, очередь задач (NATS stream или BullMQ), триггеры по завершению сессии, промпт консолидации. Этого нет, и это не «пара строк кода».\nНет данных — без накопленного опыта работы с MEMORY.md в проде неясно, насколько критична проблема. Возможно, инструкция «сжимай» достаточно. Возможно — нет. Нужно смотреть метрики: размер MEMORY.md, частота устаревания, количество дубликатов.\nСтоимость и риски — LLM может ошибочно мержнуть разные факты, удалить важное как «устаревшее». На первом этапе нужен review gate (не авто-применение), а это уже другой UX.\nРекомендуемый путь:\nТекущий эпик — реализовать inline-механизм Наблюдение — собрать метрики по MEMORY.md в проде Следующий эпик — если метрики покажут деградацию → реализовать Вариант B (batch-консолидация) с review gate Future — если потребуется audit trail → Вариант C (append-only + nightly distillation) Приблизительная стоимость batch-консолидации: ~5500 токенов за run (3000 на лог сессии + 1000 на MEMORY.md + 500 на промпт + 1000 на output). При частоте «раз в 24ч при 5+ сессиях» и дешёвой модели (OSS 20B) — ~$0.01–0.05 за агент/день. Это 1–3% от стоимости обслуживания агента. Копейки — но только если базовый механизм уже работает.\n5. Write-Manage-Read на практике В первой части я описал write-manage-read loop (Du et al., 2026) как абстрактную модель. Давайте посмотрим, как это выглядит в нашей реализации.\n5.1. Write: кто, когда, через что Файл Кто пишет Когда Через что SOUL.md Пользователь (UI) / Агент При создании агента, по запросу пользователя UI / memory tools ABOUT.md Пользователь (UI) / Агент При создании, при смене роли UI / memory tools TOOLS.md Пользователь (UI) / Агент Когда накопился практический опыт UI / memory tools USER.md Агент / Пользователь При первом взаимодействии, при обновлении предпочтений Memory tools / UI MEMORY.md Агент Когда выявлен важный факт Memory tools 5.2. Manage: сжатие, дедупликация, устаревание Текущий механизм — инструкция агенту:\nРегулярно сжимай MEMORY.md. Удаляй устаревшие факты. Мержни дубликаты. Если MEMORY.md \u0026gt; 5000 символов — сожми.\nЭто не автоматическая консолидация, а делегирование агенту. Работает? В большинстве случаев — да. LLM вполне способна сжать 5000 символов фактов до 3000, убрав дубли и устаревшее. Но не всегда корректно — может удалить важное или мержнуть разное. Поэтому soft limit, а не hard truncation.\nFuture: автоматическая дедупликация через LLM (как Auto Dream Phase 3). Но — после накопления метрик из прода.\nTemporal validity: у каждого файла памяти есть last_modified. Если факт не обновлялся дольше TTL (настраивается) — он помечается stale. Проще, чем Knowledge Graph с valid_from/valid_to у MemPalace, но для платформы с сотнями агентов — достаточно. Графовые запросы — территория Zep.\n5.3. Read: injection vs on-demand Механизм Файлы Когда Токены Bootstrap injection SOUL.md, ABOUT.md, TOOLS.md Каждый запрос ~1-2K Prompt Hint — Каждый запрос ~50 On-demand (memory tools) USER.md, MEMORY.md По решению агента ~0.5-5K Итого на boot: ~1-2K токенов. На первый запрос с загрузкой USER.md + MEMORY.md: ~2-4K. На каждый последующий: зависит от задачи — агент может не обращаться к памяти вообще.\n6. Итоги 6.1. Сводная таблица файлов памяти Файл Scope Загрузка Лимит chars Кто пишет Ключевое назначение SOUL.md agent Injection 2000 UI / agent Идентичность ABOUT.md agent Injection 2000 UI / agent Профиль TOOLS.md agent Injection 2000 UI / agent Практика инструментов USER.md agent_user On-demand 2000 agent / UI Профиль пользователя MEMORY.md agent_user On-demand 5000 (soft) agent only Долговременная память 6.2. Стратегии контекст-сборки Стратегия Boot tokens Предсказуемость Гибкость Масштабируемость Full Injection ~5-10K ✅ ❌ ❌ On-Demand 0 ❌ ✅ ✅ Hybrid v1 ~2-3K ✅ ⚠️ ⚠️ Hybrid v2 ~1-2K ⚠️ ✅ ✅ 6.3. Dreaming: не «если», а «когда» Anthropic и OpenAI уже реализовали фоновую консолидацию памяти. Результаты впечатляют: 82.8% factual recall у OpenAI, 6x task completion у Harvey (Anthropic). Но оба подхода имеют компромиссы: OpenAI молча переписывает без audit trail, Anthropic требует отдельного sub-agent и двойного гейта.\nНаш путь — сначала inline-механизм, потом метрики, потом dreaming. Не потому, что мы не верим в консолидацию, а потому что нужно понять что именно консолидировать. Без данных из прода — это стрельба вслепую.\nКогда будем реализовывать — скорее всего Вариант B (batch-консолидация с review gate), как у Anthropic. А если понадобится audit trail — Вариант C (append-only логи + nightly distillation), как в KAIROS. Но это уже совсем другая история.\nПредыдущая статья серии: Часть 5: Agent Memory Management Следующая статья серии: Часть 7: Cloud-Native Standards\nИсточники OpenAI. \u0026ldquo;Dreaming: Better memory for a more helpful ChatGPT.\u0026rdquo; 4 июня 2026. openai.com/index/chatgpt-memory-dreaming solaius (Red Hat Research). \u0026ldquo;Claude Memory \u0026amp; Dreaming Deep Dive.\u0026rdquo; Июнь 2026. github.com/solaius/ai-asset-registry ","permalink":"https://triumphpc.github.io/blog/ru/posts/ai-agent-design-patterns-6-memory-deep-dive/","summary":"Deep dive в механику памяти агента: пять файлов памяти, четыре стратегии контекст-сборки, TOOLS.md как два источника ответственности, и Dreaming — фоновая консолидация от Anthropic (Auto Dream, Dreams API, KAIROS) и OpenAI (Dreaming V3, 82.8% factual recall). Инженерные решения, которые принимаются за кулисами production-платформы.","title":"Шаблоны проектирования AI-агентов. Часть 6: Agent Memory in Practice"},{"content":"1. Введение В Части 1 я разобрал ReAct — агента, который думает и действует. В Части 2 — Plan-and-Execute, где планировщик строит N-шаговый план. В Части 3 — Reflexion, где агент учится на ошибках. Три паттерна, три одиночных агента.\nИ тут мы упираемся в потолок. Один LLM-агент не может быть экспертом во всём. Точнее, может — но плохо. Один system prompt — одна «роль». Writer ≠ Reviewer ≠ Researcher в одной голове. Конфликт интересов, галлюцинации, потеря фокуса. И даже Reflexion не поможет, если задача требует принципиально разных экспертиз — агент не «отрефлексирует» себе навык, которого у него нет.\nЗначит, нужна команда. Несколько агентов, каждый со своей ролью, координирующихся для решения общей задачи. Звучит просто, но тут начинается самое интересное: как именно они координируются? Кто принимает решения? Как передавать контекст? Как избежать бесконечных циклов handoff?\nЯ разберу пять архитектур multi-agent оркестрации — от централизованного супервизора до параллельного дебата — с Mermaid-схемами, кодом на Eino (Go) и честными ограничениями. Все примеры на одном сценарии: code review pipeline с Researcher, Coder и Reviewer — так будет проще сравнивать.\n2. Supervisor Pattern Мотивация Самый частый сценарий: у вас есть задача, требующая разных экспертиз, но нужен кто-то, кто решит, какому агенту что делегировать. Без централизованной координации агенты будут говорить одновременно, перебивать друг друга или, что ещё хуже, передавать задачу по кругу.\nSupervisor — это централизованная координация: один агент-маршрутизатор получает задачу, решает, кому делегировать, собирает результаты и принимает решение о следующем шаге.\nАрхитектура graph TB USER[\"👤 User\"] --\u003e|\"задача\"| SUP[\"🧑‍💼 Supervisorделегирует, агрегирует\"] SUP --\u003e|\"delegate: research\"| RES[\"🔍 Researcherсобирает контекст\"] SUP --\u003e|\"delegate: code\"| COD[\"💻 Coderпишет код\"] SUP --\u003e|\"delegate: review\"| REV[\"🔎 Reviewerпроверяет код\"] RES --\u003e|\"результат\"| SUP COD --\u003e|\"результат\"| SUP REV --\u003e|\"результат\"| SUP SUP --\u003e|\"финальный ответ\"| USER style SUP fill:#2e7d32,color:#fff style RES fill:#1565c0,color:#fff style COD fill:#e65100,color:#fff style REV fill:#6a1b9a,color:#fff Supervisor видит полную картину: каждый sub-agent возвращает результат супервизору, а не следующему агенту. Это исключает хаос, но создаёт bottleneck.\nРеализация на Eino Eino ADK предоставляет готовый supervisor.New:\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/cloudwego/eino/adk\u0026#34; \u0026#34;github.com/cloudwego/eino/adk/prebuilt/supervisor\u0026#34; \u0026#34;github.com/cloudwego/eino-ext/components/model/openai\u0026#34; ) func main() { ctx := context.Background() // Инициализируем ChatModel для всех агентов chatModel, err := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ APIKey: os.Getenv(\u0026#34;OPENAI_API_KEY\u0026#34;), Model: os.Getenv(\u0026#34;OPENAI_MODEL\u0026#34;), BaseURL: os.Getenv(\u0026#34;OPENAI_BASE_URL\u0026#34;), }) if err != nil { log.Fatal(err) } // Создаём sub-агентов researcher, _ := adk.NewChatModelAgent(ctx, \u0026amp;adk.ChatModelAgentConfig{ Name: \u0026#34;researcher\u0026#34;, Description: \u0026#34;Researches best practices and gathers context for coding tasks\u0026#34;, Instruction: \u0026#34;You are a research specialist. Find relevant information, API docs, and best practices.\u0026#34;, Model: chatModel, }) coder, _ := adk.NewChatModelAgent(ctx, \u0026amp;adk.ChatModelAgentConfig{ Name: \u0026#34;coder\u0026#34;, Description: \u0026#34;Writes Go code based on research findings\u0026#34;, Instruction: \u0026#34;You are a Go developer. Write clean, idiomatic Go code.\u0026#34;, Model: chatModel, }) reviewer, _ := adk.NewChatModelAgent(ctx, \u0026amp;adk.ChatModelAgentConfig{ Name: \u0026#34;reviewer\u0026#34;, Description: \u0026#34;Reviews code for bugs, style issues, and correctness\u0026#34;, Instruction: \u0026#34;You are a code reviewer. Check for bugs, edge cases, and style issues.\u0026#34;, Model: chatModel, }) // Supervisor-агент (координатор) coordinator, _ := adk.NewChatModelAgent(ctx, \u0026amp;adk.ChatModelAgentConfig{ Name: \u0026#34;coordinator\u0026#34;, Description: \u0026#34;Coordinates research, coding, and review sub-agents\u0026#34;, Instruction: \u0026#34;You are a project coordinator. Delegate tasks to the right specialist.\u0026#34;, Model: chatModel, }) // Собираем Supervisor pattern supervisorAgent, err := supervisor.New(ctx, \u0026amp;supervisor.Config{ Supervisor: coordinator, SubAgents: []adk.Agent{researcher, coder, reviewer}, }) if err != nil { log.Fatal(err) } // Запускаем runner := adk.NewRunner(ctx, adk.RunnerConfig{ Agent: supervisorAgent, }) result, _ := runner.Query(ctx, \u0026#34;Implement a thread-safe LRU cache in Go\u0026#34;) _ = result } Ключевой момент: supervisor.Config принимает Supervisor (координатор) и SubAgents (массив специалистов). Supervisor сам решает, кого вызвать и когда.\nОграничения Bottleneck: все результаты проходят через supervisor → при росте числа агентов задержка растёт линейно Контекстный взрыв: supervisor должен держать в контексте результаты всех sub-agents → токены расходуются быстро Single point of failure: если supervisor делегирует неправильно, весь процесс идёт не туда 3. Swarm / Handoff Pattern Мотивация А что если supervisor не нужен? Что если агенты сами знают, кому передать задачу? Это децентрализованная координация: агент выполнил свою часть и передал (handoff) следующему.\nЗвучит заманчиво — нет bottleneck, нет single point of failure. Но есть цена: кто решает, когда передавать? Агент должен понимать, когда его работа закончена и кому передавать эстафету.\nАрхитектура graph LR RES[\"🔍 Researcher\"] --\u003e|\"handoff:контекст готов\"| COD[\"💻 Coder\"] COD --\u003e|\"handoff:код готов\"| REV[\"🔎 Reviewer\"] REV --\u003e|\"handoff:нужен фикс\"| COD REV --\u003e|\"done\"| USER[\"👤 User\"] style RES fill:#1565c0,color:#fff style COD fill:#e65100,color:#fff style REV fill:#6a1b9a,color:#fff Обратите внимание: Reviewer может передать задачу обратно Coder (нашёл баг → исправь). Это цикл — в отличие от Assembly Line, где поток строго однонаправленный.\nРеализация на Eino Eino предоставляет host.NewMultiAgent — Host-паттерн, где один агент-хост маршрутизирует к специалистам:\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/cloudwego/eino/flow/agent/multiagent/host\u0026#34; \u0026#34;github.com/cloudwego/eino-ext/components/model/openai\u0026#34; ) func main() { ctx := context.Background() chatModel, _ := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ APIKey: os.Getenv(\u0026#34;OPENAI_API_KEY\u0026#34;), Model: os.Getenv(\u0026#34;OPENAI_MODEL\u0026#34;), BaseURL: os.Getenv(\u0026#34;OPENAI_BASE_URL\u0026#34;), }) // Host-агент: маршрутизатор hostAgent := \u0026amp;host.Host{ ChatModel: chatModel, SystemPrompt: \u0026#34;You manage a code review pipeline. Route requests to the right specialist.\u0026#34;, } // Специалисты (реализуют интерфейс host.Specialist) researcher := NewResearchSpecialist(chatModel) coder := NewCodeSpecialist(chatModel) reviewer := NewReviewSpecialist(chatModel) // Собираем multi-agent multiAgent, err := host.NewMultiAgent(ctx, \u0026amp;host.Config{ Host: hostAgent, Specialists: []host.Specialist{researcher, coder, reviewer}, }) if err != nil { log.Fatal(err) } // Запускаем result, _ := multiAgent.Run(ctx, \u0026#34;Implement a thread-safe LRU cache in Go\u0026#34;) _ = result } Host-паттерн в Eino — это гибрид: хост-агент маршрутизирует задачи, но специалисты могут быть вызваны по контексту, а не только по прямому приказу. Это ближе к swarm, чем к чистому supervisor.\nОграничения Deadlock: Agent A → B → A → B → \u0026hellip; бесконечный цикл handoff. Нет supervisor, который разорвёт петлю Качество маршрутизации: если агент неправильно оценивает, кому передавать, вся цепочка ломается Контекстная изоляция: каждый агент видит только свой контекст + handoff-сообщение. Нет глобальной картины 4. Debate Pattern Мотивация А если задача не имеет единственного правильного ответа? Или если критично проверить несколько гипотез параллельно? Du et al. показали, что мультиагентный дебат улучшает фактологическую точность на +8% на GSM8K через debate между N агентами (Du et al., 2023).\nИдея: несколько агентов независимо решают задачу, затем критикуют решения друг друга — и приходят к консенсусу. Как society of minds Мински, только на практике.\nАрхитектура graph TB TASK[\"📋 Task\"] --\u003e A1[\"🤖 Agent A\"] TASK --\u003e A2[\"🤖 Agent B\"] TASK --\u003e A3[\"🤖 Agent C\"] A1 \u003c--\u003e|\"critique\"| A2 A2 \u003c--\u003e|\"critique\"| A3 A1 \u003c--\u003e|\"critique\"| A3 A1 --\u003e J[\"⚖️ Judge / Consensus\"] A2 --\u003e J A3 --\u003e J J --\u003e RESULT[\"✅ Final Answer\"] style A1 fill:#2e7d32,color:#fff style A2 fill:#1565c0,color:#fff style A3 fill:#e65100,color:#fff style J fill:#c62828,color:#fff Три агента решают задачу параллельно, критикуют друг друга, а Judge (или majority vote) выбирает финальный ответ. Это не конвейер — это параллельный консенсус.\nРеализация на Eino Готового Debate-паттерна в Eino нет. Собираем через compose.Graph:\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/cloudwego/eino/compose\u0026#34; \u0026#34;github.com/cloudwego/eino/schema\u0026#34; \u0026#34;github.com/cloudwego/eino-ext/components/model/openai\u0026#34; ) func main() { ctx := context.Background() chatModel, _ := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ APIKey: os.Getenv(\u0026#34;OPENAI_API_KEY\u0026#34;), Model: os.Getenv(\u0026#34;OPENAI_MODEL\u0026#34;), BaseURL: os.Getenv(\u0026#34;OPENAI_BASE_URL\u0026#34;), }) g := compose.NewGraph[string, string]() // Три независимых агента — генерируют решения параллельно g.AddGraphNode(\u0026#34;agent_a\u0026#34;, func(ctx context.Context, task string) (string, error) { msgs := []*schema.Message{ schema.SystemMessage(\u0026#34;You are a senior Go developer. Solve the task.\u0026#34;), schema.UserMessage(task), } resp, _ := chatModel.Generate(ctx, msgs) return resp.Content, nil }) g.AddGraphNode(\u0026#34;agent_b\u0026#34;, func(ctx context.Context, task string) (string, error) { msgs := []*schema.Message{ schema.SystemMessage(\u0026#34;You are a careful code reviewer. Solve the task with emphasis on correctness.\u0026#34;), schema.UserMessage(task), } resp, _ := chatModel.Generate(ctx, msgs) return resp.Content, nil }) g.AddGraphNode(\u0026#34;agent_c\u0026#34;, func(ctx context.Context, task string) (string, error) { msgs := []*schema.Message{ schema.SystemMessage(\u0026#34;You are a performance expert. Solve the task with emphasis on efficiency.\u0026#34;), schema.UserMessage(task), } resp, _ := chatModel.Generate(ctx, msgs) return resp.Content, nil }) // Judge: агрегирует и выбирает лучший ответ g.AddGraphNode(\u0026#34;judge\u0026#34;, func(ctx context.Context, input string) (string, error) { // input содержит результаты всех трёх агентов msgs := []*schema.Message{ schema.SystemMessage(\u0026#34;You are a judge. Compare three solutions and pick the best one. Explain your choice.\u0026#34;), schema.UserMessage(input), } resp, _ := chatModel.Generate(ctx, msgs) return resp.Content, nil }) // Параллельная генерация → Judge g.AddEdge(compose.START, \u0026#34;agent_a\u0026#34;) g.AddEdge(compose.START, \u0026#34;agent_b\u0026#34;) g.AddEdge(compose.START, \u0026#34;agent_c\u0026#34;) g.AddEdge(\u0026#34;agent_a\u0026#34;, \u0026#34;judge\u0026#34;) g.AddEdge(\u0026#34;agent_b\u0026#34;, \u0026#34;judge\u0026#34;) g.AddEdge(\u0026#34;agent_c\u0026#34;, \u0026#34;judge\u0026#34;) g.AddEdge(\u0026#34;judge\u0026#34;, compose.END) r, _ := g.Compile(ctx) result, _ := r.Invoke(ctx, \u0026#34;Implement a thread-safe LRU cache in Go\u0026#34;) fmt.Println(result) } Ключевой момент: compose.START → три ноды параллельно → Judge агрегирует. Graph автоматически параллелит ноды, у которых все входы готовы.\nОграничения 3x cost: три LLM-вызова вместо одного (минимум). Judge — четвёртый Не работает без Judge: простое majority vote может совпасть на ошибке (коллективная галлюцинация) Задержка: параллельно — быстрее, чем последовательно, но всё равно минимум max(agent_a, agent_b, agent_c) + judge 5. Assembly Line Pattern Мотивация А что если задача строго последовательна? Research → Code → Review — каждый этап зависит от предыдущего. Никакой параллелизм, никакого дебата. Просто конвейер.\nЭто именно то, что реализовали ChatDev (Qian et al., 2023) и MetaGPT (Hong et al., 2023). MetaGPT добавил SOP: каждый агент получает чёткий формат входа/выхода, что снижает галлюцинации на 1.8x по сравнению с ChatDev.\nАрхитектура graph LR TASK[\"📋 Task\"] --\u003e RES[\"🔍 Researcherсобирает контекст\"] RES --\u003e|\"researchfindings\"| COD[\"💻 Coderпишет код\"] COD --\u003e|\"codedraft\"| REV[\"🔎 Reviewerпроверяет\"] REV --\u003e|\"approvedcode\"| DONE[\"✅ Result\"] style RES fill:#1565c0,color:#fff style COD fill:#e65100,color:#fff style REV fill:#6a1b9a,color:#fff Строгая последовательность. Никаких циклов, никаких параллельных веток. Каждый агент получает выход предыдущего — и только его.\nРеализация на Eino Assembly Line — это классический compose.Workflow (или Chain):\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/cloudwego/eino/compose\u0026#34; \u0026#34;github.com/cloudwego/eino/schema\u0026#34; \u0026#34;github.com/cloudwego/eino-ext/components/model/openai\u0026#34; ) func main() { ctx := context.Background() chatModel, _ := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ APIKey: os.Getenv(\u0026#34;OPENAI_API_KEY\u0026#34;), Model: os.Getenv(\u0026#34;OPENAI_MODEL\u0026#34;), BaseURL: os.Getenv(\u0026#34;OPENAI_BASE_URL\u0026#34;), }) // Assembly Line через Chain chain := compose.NewChain[string, string]() // Stage 1: Research chain.AppendChatTemplate( prompt.FromMessages(schema.FString, schema.SystemMessage(\u0026#34;You are a research specialist. Gather context and best practices for the task. Output: structured research findings.\u0026#34;), schema.UserMessage(\u0026#34;{input}\u0026#34;), ), ).AppendChatModel(chatModel) // Stage 2: Code chain.AppendChatTemplate( prompt.FromMessages(schema.FString, schema.SystemMessage(\u0026#34;You are a Go developer. Write code based on the research findings. Output: complete Go source code.\u0026#34;), schema.UserMessage(\u0026#34;{input}\u0026#34;), ), ).AppendChatModel(chatModel) // Stage 3: Review chain.AppendChatTemplate( prompt.FromMessages(schema.FString, schema.SystemMessage(\u0026#34;You are a code reviewer. Review the code for bugs, edge cases, and style issues. Output: approved code or list of fixes.\u0026#34;), schema.UserMessage(\u0026#34;{input}\u0026#34;), ), ).AppendChatModel(chatModel) r, _ := chain.Compile(ctx) result, _ := r.Invoke(ctx, \u0026#34;Implement a thread-safe LRU cache in Go\u0026#34;) fmt.Println(result) } compose.Chain — это Workflow под капотом. Каждая пара ChatTemplate + ChatModel — один этап конвейера. Выход предыдущего автоматически подаётся на вход следующего.\nОграничения Последовательность = медленно: нет параллелизма, каждый этап ждёт предыдущий Каскадные ошибки: если Researcher дал плохой контекст, Coder напишет плохой код, а Reviewer может не поймать корневую проблему Нет цикла: если Reviewer нашёл баг — весь конвейер нужно перезапускать (в отличие от Swarm, где возможен handoff назад) 6. Hierarchical Pattern Мотивация А если задача настолько сложная, что один уровень делегирования не справляется? Проект из 10 подзадач, каждая из которых сама разбивается на 3-4 микрозадачи. Один supervisor утонет в контексте.\nHierarchical — это многоуровневая декомпозиция. CEO делегирует Team Lead\u0026rsquo;ам, те — специалистам. Каждый уровень управляет только своим scope.\nАрхитектура graph TB USER[\"👤 User\"] --\u003e CEO[\"🎯 CEO Agentстратегия\"] CEO --\u003e|\"designresearch\"| TL1[\"📋 Team Lead: Research\"] CEO --\u003e|\"implement\"| TL2[\"📋 Team Lead: Dev\"] TL1 --\u003e R1[\"🔍 Researcher 1\"] TL1 --\u003e R2[\"🔍 Researcher 2\"] TL2 --\u003e D1[\"💻 Coder 1\"] TL2 --\u003e D2[\"💻 Coder 2\"] TL1 --\u003e CEO TL2 --\u003e CEO CEO --\u003e USER style CEO fill:#c62828,color:#fff style TL1 fill:#2e7d32,color:#fff style TL2 fill:#2e7d32,color:#fff style R1 fill:#1565c0,color:#fff style R2 fill:#1565c0,color:#fff style D1 fill:#e65100,color:#fff style D2 fill:#e65100,color:#fff Ключевое отличие от Supervisor: здесь два уровня делегирования. CEO не знает про Researcher 1 и Coder 2 — он работает только с Team Lead\u0026rsquo;ами. Это снижает контекстную нагрузку на каждом уровне.\nРеализация на Eino Eino ADK предоставляет deep.New — DeepAgent с встроенным task management:\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/cloudwego/eino/adk\u0026#34; \u0026#34;github.com/cloudwego/eino/adk/prebuilt/deep\u0026#34; \u0026#34;github.com/cloudwego/eino-ext/components/model/openai\u0026#34; ) func main() { ctx := context.Background() chatModel, _ := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ APIKey: os.Getenv(\u0026#34;OPENAI_API_KEY\u0026#34;), Model: os.Getenv(\u0026#34;OPENAI_MODEL\u0026#34;), BaseURL: os.Getenv(\u0026#34;OPENAI_BASE_URL\u0026#34;), }) // Создаём sub-агентов researcher, _ := adk.NewChatModelAgent(ctx, \u0026amp;adk.ChatModelAgentConfig{ Name: \u0026#34;researcher\u0026#34;, Description: \u0026#34;Researches best practices and gathers context\u0026#34;, Instruction: \u0026#34;You are a research specialist. Find relevant information and API docs.\u0026#34;, Model: chatModel, }) coder, _ := adk.NewChatModelAgent(ctx, \u0026amp;adk.ChatModelAgentConfig{ Name: \u0026#34;coder\u0026#34;, Description: \u0026#34;Writes Go code based on requirements\u0026#34;, Instruction: \u0026#34;You are a Go developer. Write clean, idiomatic Go code.\u0026#34;, Model: chatModel, }) reviewer, _ := adk.NewChatModelAgent(ctx, \u0026amp;adk.ChatModelAgentConfig{ Name: \u0026#34;reviewer\u0026#34;, Description: \u0026#34;Reviews code for correctness and quality\u0026#34;, Instruction: \u0026#34;You are a code reviewer. Check for bugs and style issues.\u0026#34;, Model: chatModel, }) // DeepAgent: автоматически разбивает задачу на подзадачи // и делегирует их sub-agent\u0026#39;ам deepAgent, err := deep.New(ctx, \u0026amp;deep.Config{ Name: \u0026#34;project_manager\u0026#34;, Description: \u0026#34;Manages complex coding projects by breaking them into subtasks\u0026#34;, ChatModel: chatModel, Instruction: \u0026#34;You are a project manager. Break down tasks and delegate to specialists.\u0026#34;, SubAgents: []adk.Agent{researcher, coder, reviewer}, }) if err != nil { log.Fatal(err) } runner := adk.NewRunner(ctx, adk.RunnerConfig{ Agent: deepAgent, }) result, _ := runner.Query(ctx, \u0026#34;Implement a thread-safe LRU cache in Go with tests\u0026#34;) _ = result } DeepAgent использует встроенный write_todos tool для планирования и task tool для делегирования sub-agent\u0026rsquo;ам. Контекст изолирован между основным агентом и sub-agent\u0026rsquo;ами — это предотвращает контекстное загрязнение.\nОграничения Overhead: два уровня координации = два уровня LLM-вызовов. CEO + Team Lead перед тем, как задача дойдёт до исполнителя Сложность отладки: ошибка на нижнем уровне может незаметно просочиться наверх Over-engineering для простых задач: если задача укладывается в одного supervisor — иерархия избыточна 7. Сравнение паттернов Паттерн Централизация Параллелизм Сложность координации Token overhead Когда использовать Supervisor Высокая Низкий Низкая Средний Чёткие роли, нужен контроль над порядком выполнения Swarm / Handoff Низкая Средний Высокая Средний Агенты знают, кому передавать; гибкие маршруты Debate Низкая (или Judge) Высокий Средняя Высокий Неоднозначные задачи, нужна проверка гипотез Assembly Line Низкая Нет Низкая Низкий Строгая последовательность этапов, SOP Hierarchical Высокая (многоуровневая) Средний Высокая Высокий Сложные проекты с декомпозицией на подзадачи Как читать таблицу: Централизация — сколько точек принятия решений. Параллелизм — могут ли агенты работать одновременно. Сложность координации — сколько усилий нужно на маршрутизацию. Token overhead — сколько дополнительных токенов тратится на координацию.\n8. Честные ограничения Коммуникационный overhead Каждый обмен между агентами = полный контекст в промпте. При 3 агентах и 2 раундах дебата — 3×2×context_length токенов. При длинном контексте это может стоить $0.50+ на один запрос. Mitigation: суммаризация промежуточных результатов перед передачей.\nКоординационные ошибки Supervisor может делегировать неправильно. Agent может передать задачу не тому специалисту. Judge может выбрать худший ответ. В исследовании Li et al. показано, что «more agents» улучшает результат через sampling-and-voting, но это работает только когда базовая модель уже достаточно хороша (Li et al., 2024). Если модель даёт \u0026lt;50% accuracy, больше агентов не помогут.\nDiminishing returns Больше агентов ≠ лучше результат. Li et al. показали, что sampling-and-voting масштабируется с числом агентов, но с убывающей отдачей: каждый следующий агент приносит меньше пользы, чем предыдущий. На практике 3-5 агентов — оптимум. После 5-7 стоимость растёт, а прирост минимален.\nDeadlock / Infinite loop В Swarm/Handoff агент A передаёт B, B — A, и так бесконечно. В Debate агенты не могут договориться. В Hierarchical Team Lead\u0026rsquo;ы пересылают задачу друг другу. Это не теоретическая проблема — исследование MAST проанализировало 1642 трассы в 7 фреймворках и показало, что координационные сбои составляют 36.9% всех отказов в multi-agent системах (Tran et al., 2025).\nКак это решают на практике?\n1. Hard turn limit — грубый, но надёжный потолок. LangGraph: recursion_limit (по умолчанию 25). CrewAI: max_iter + max_execution_time. AutoGen: max_consecutive_auto_reply + max_rounds. Eino: WithMaxRunSteps в compose.Graph. Достигли лимита — выполнение останавливается, даже если задача не решена. Это спасает от бесконечного расхода токенов, но не от потери качества.\n2. Termination signals — агент явно сигнализирует о завершении. Каждый агент получает в инструкцию: «Если задача выполнена — верни TASK_COMPLETED + результат. Если не можешь завершить после 3 попыток — верни TASK_FAILED. Если нужен человек — верни NEEDS_HUMAN». Оркестратор проверяет сигнал после каждого шага и разрывает цикл при COMPLETED или FAILED.\n3. Handoff history — отслеживание маршрута. Оркестратор хранит список (agent_name, task_hash). Если агент получает задачу, которую уже обрабатывал с тем же хэшем → цикл обнаружен, передать следующему агенту или завершить. Это Circuit breaker из распределённых систем, применённый к агентам.\nНи один метод не работает отдельно. Рабочая комбинация: termination signals (осознанная остановка) + handoff history (обнаружение циклов) + hard turn limit (страховка от зависания).\nКогда один агент лучше Если задача:\nНе требует разных экспертиз Имеет чёткий критерий успеха Укладывается в один system prompt \u0026hellip;то multi-agent — это лишняя сложность. ReAct с правильным промптом часто работает лучше, чем три агента, которые координируются через supervisor. Не добавляйте агентов ради агентов.\n9. Резюме Пять паттернов multi-agent оркестрации — не конкурирующие, а дополняющие:\nSupervisor — когда нужен контроль и предсказуемость Swarm — когда агенты знают свои границы и могут самоорганизоваться Debate — когда важна проверка гипотез и фактологическая точность Assembly Line — когда этапы строго последовательны и SOP важнее гибкости Hierarchical — когда сложность задачи требует многоуровневой декомпозиции На Eino: три паттерна из коробки (supervisor.New, host.NewMultiAgent, deep.New), два через compose.Graph. Выбор паттерна — это выбор trade-off между контролем и гибкостью, параллелизмом и стоимостью, простотой и масштабируемостью.\nИ главное: добавление агентов не решает проблему плохого промпта. Multi-agent — это инструмент для координации экспертиз, а не волшебная таблетка от галлюцинаций.\nСледующая статья серии: Часть 5: Agent Memory Management\n","permalink":"https://triumphpc.github.io/blog/ru/posts/ai-agent-design-patterns-4-multi-agent/","summary":"Пять архитектур multi-agent оркестрации — Supervisor, Swarm, Debate, Assembly Line, Hierarchical — с Mermaid-схемами, код-примерами на Eino (Go) и честным разбором ограничений. Когда один агент не справляется, а когда лучше им и остаться.","title":"Шаблоны проектирования AI-агентов. Часть 4: Multi-Agent Patterns"},{"content":"1. Проблема: Agent Amnesia В Части 1 я показал ReAct-агента. В Части 3 — Reflexion, где агент учится на ошибках через эпизодическую память. Четыре паттерна, четыре сессии — и ни один не решает фундаментальную проблему: контекстное окно — это не память.\nПредставьте: ваш дебаг-ассистент в пятницу нашёл, что баг в middleware — это race condition на shared cache. В понедельник вы спрашиваете того же агента про похожий баг — и он начинает с нуля. Не помнит ни вывод, ни контекст, ни даже то, что cache вообще существует. Каждый понедельник — Groundhog Day.\nИ это не экзотика. Это норма. LLM stateless по природе: обработала токены, вернула результат, забыла. Контекстное окно — 128K, 200K, даже 1M токенов — всё равно конечное. Сессия закончилась, окно очистилось, агент ничего не знает о вашем проекте.\nНо ведь человеку тоже не нужно помнить всю жизнь — он хранит важное и забывает мусор. Агенту нужна не бесконечная память, а правильная. Что сохранять? Как сжимать? Когда поднимать в контекст? Именно эти вопросы я разберу.\n2. Таксономия памяти агента Системный обзор Du et al. (Memory for Autonomous LLM Agents, 2026) формализует память агента как write-manage-read loop:\ngraph LR W[\"✏️ WRITE\nЧто и когда сохранять\"] --\u003e M[\"🔧 MANAGE\nСжатие, дедупликация,\nразрешение противоречий\"] M --\u003e R[\"📖 READ\nЧто и когда\nподнять в контекст\"] R --\u003e|\"новый опыт\"| W Три фазы, три принципиально разных инженерных решения:\nWrite: не всё стоит сохранять. «Пользователь спросил погоду» — мусор. «Пользователь предпочитает YAML вместо JSON» — факт. Нужна фильтрация. Manage: факты устаревают, дублируются, противоречат друг другу. «Боб — лид ML-команды» и «Алиса руководит ML-командой» — нужен механизм разрешения. Read: поднять нужное в нужный момент. Не весь архив, а релевантное. И уложить в токен-бюджет. Du выделяет три измерения для классификации: temporal scope (краткосрочная / долгосрочная), representation (текст / векторы / графы) и control policy (фиксированные правила / обучаемые). Из комбинаций рождаются пять семейств механизмов — от context-resident compression до policy-learned management. Но на практике инженеры выбирают не из академических семейств, а из готовых решений. И тут вариантов неожиданно много.\n3. Подход 1: File-First Brain (OpenClaw) OpenClaw — проект от команды mem0 (58K ⭐ на GitHub, YC S24), популярного управляемого слоя памяти для AI-агентов. Если mem0 выносит память в сервис, то OpenClaw уходит в противоположную крайность — радикальная идея: файловая система = мозг агента.\nАгент хранит всё в markdown-файлах:\nФайл Назначение Размер SOUL.md Идентичность: кто я, мои ценности ~2K токенов AGENTS.md Процедуры: как я работаю ~3K токенов MEMORY.md Курируемая долгосрочная память ~5K токенов memory/YYYY-MM-DD.md Сырые дневные логи Без лимита Каждая сессия начинается с boot sequence — агент читает SOUL.md, AGENTS.md, MEMORY.md и последние дневные логи. Это его «утренний кофе»: 4K–10K токенов только на старт, прежде чем он скажет первое слово.\ngraph TB START[\"🚀 Новая сессия\"] --\u003e SOUL[\"📖 SOUL.md\nидентичность\"] SOUL --\u003e AGENTS[\"📖 AGENTS.md\nпроцедуры\"] AGENTS --\u003e MEMORY[\"📖 MEMORY.md\nкурированные факты\"] MEMORY --\u003e LOGS[\"📖 дневные логи\nпоследние записи\"] LOGS --\u003e READY[\"✅ Агент готов\"] style START fill:#1565c0,color:#fff style READY fill:#2e7d32,color:#fff Зачем файлы, а не векторная база? Философский аргумент OpenClaw: RAG — для поиска информации, а агенту нужен мозг. Векторная база фрагментирована — семантический поиск возвращает куски без контекста. Файлы — цельные, читаемые агентом нативно, редактируемые человеком.\nНо есть цена. 10K токенов на boot — это 10K токенов, которые не используются для задачи. При 5M токенов/день (типичный расход OpenClaw) это серьёзный бюджет. И чем больше MEMORY.md, тем дороже каждая сессия.\n4. Подход 2: Structured Compression (MemPalace) MemPalace решает главную проблему OpenClaw — token crushing. Идея: не грузить всё сразу, а поднимать по требованию.\nАрхитектура — иерархия Memory Palace: Wing (проект/человек) → Room (подтема) → Hall (тип памяти: факты, события, открытия, предпочтения, советы). На каждом уровне — сжатое описание, которое LLM читает нативно. Сжатие — не магия AAAK, а банальная truncation: snippets обрезаются до 200–300 символов и группируются по room.\nЧетыре слоя памяти (layers.py):\nСлой Что хранит Как работает Токены L0 — Identity Кто я, мои принципы, ключевые люди Читает ~/.mempalace/identity.txt (пишет пользователь) ~100 L1 — Essential Story Топ-15 важных моментов из всего palace Сканирует до 2000 drawers, ранжирует по importance/emotional_weight/weight, группирует по room, обрезает до 3200 символов ~500–800 L2 — On-Demand Фильтрованная выборка по wing/room Metadata-фильтр в ChromaDB (не семантика!), до N drawers ~200–500 за запрос L3 — Deep Search Полный семантический поиск col.query(query_texts=...) по всему palace с similarity-ранжированием Без лимита Старт — только L0 + L1, ~600–900 токенов. L2 и L3 поднимаются через MCP-инструменты, когда агент сталкивается с задачей, требующей контекста.\ngraph TB BOOT[\"🚀 wake-up\n~600-900 tokens\"] --\u003e L0[\"L0: Identity\n~100 токенов\nфайл identity.txt\"] BOOT --\u003e L1[\"L1: Essential Story\n~500-800 токенов\nтоп-15 drawers по весу\"] L0 --\u003e L2[\"L2: On-Demand\n~200-500 токенов\nфильтр по wing/room\"] L1 --\u003e L2 L2 --\u003e L3[\"L3: Deep Search\nбез лимита\nсемантический поиск\"] L3 --\u003e WING[\"🏰 Wing\nпроект / человек\"] WING --\u003e ROOM[\"🏠 Room\nподтема\"] ROOM --\u003e HALL[\"🚪 Hall\nтип памяти\"] style BOOT fill:#2e7d32,color:#fff style L2 fill:#e65100,color:#fff style L3 fill:#c62828,color:#fff Плюс: Knowledge Graph с temporal validity — факты имеют срок годности. «Боб — лид ML-команды» действительно до определённой даты. Когда Алиса стала лидом — факт автоматически устаревает. Это решает проблему противоречий из write-manage-read loop.\nРезультат: 96.6% R@5 на LongMemEval (бенчмарк долгосрочной памяти) при ~600–900 токенах на wake-up (L0 + L1). Против 10K у OpenClaw. Разница в 10–15x — MemPalace вспоминает лучше, тратя на загрузку на порядок меньше контекста. И это без LLM на этапе поиска: чистый ChromaDB, чистый semantic search, ноль API-вызовов.\n5. Подход 3: Managed Memory Layer Третий подход — вынести память в отдельный сервис. Агент не хранит ничего, а запрашивает контекст у внешнего слоя.\nmem0 (GitHub, 58K ⭐) — «universal memory layer». Управляемый сервис (YC S24), который автоматически извлекает факты из диалогов, дедуплицирует и возвращает релевантное по запросу. Новый алгоритм (апрель 2026): 94.8% на LongMemEval при 6.8K токенов. Плюсы: не нужно думать о write/manage — сервис делает всё сам. Минусы: вендор-лок, зависимость от внешнего API.\nА вот Zep — совсем другой зверь. Это open-source memory server, и его стоит рассмотреть отдельно, потому что он решает проблему, которую mem0 не решает: структуру связей между фактами.\nПредставьте: агент знает, что «Боб работает в ML-команде» и «ML-команда использует PyTorch». Векторная база (как у mem0) найдёт каждый факт по отдельности — но не поймёт, что Боб, скорее всего, работает с PyTorch. Zep добавляет Knowledge Graph поверх векторов — через свой движок Graphiti. Сущности и связи извлекаются автоматически из диалогов, а namespace-based изоляция разделяет контекст разных пользователей.\nПочему мы его рассматриваем? Потому что в реальных проектах факты не живут в вакууме — они связаны. «Клиент X перешёл на план Y» и «план Y не поддерживает фичу Z» — агент должен сделать вывод, а не просто вернуть оба факта. Граф даёт такую возможность.\nНо есть цена. Zep требует инфраструктуру: векторная БД + граф + embedding service. Это не один сервис, а стек. Если у вас нет DevOps-ресурса — деплой будет болью.\nLangMem (документация, от LangChain) — SDK с тремя типами памяти: semantic (факты), episodic (прошлый опыт), procedural (эволюция поведения). Процедурная память — уникальная фича: агент обновляет свой промпт на основе обратной связи, «учится» вести себя лучше. Плюсы: нативная интеграция с LangGraph, namespace-изоляция для privacy. Минусы: привязка к экосистеме LangChain.\nЧто общего? Все три — middleware: сидят между агентом и LLM, перехватывают контекст, обогащают релевантными фактами. Различия — в хранилище (векторы vs граф vs промпт), управлении (авто vs курируемое) и деплойменте (SaaS vs self-hosted vs embedded).\n6. Как мы решаем А мы с командой как раз решаем это — проектируем память для AI-агентов на нашей платформе. Архитектура, к которой мы пришли, подозрительно похожа на гибрид OpenClaw и MemPalace.\nЧетыре слоя памяти:\ngraph TB SESSION[\"🔄 Session Layer\nтекущий диалог,\nавтоочистка\"] AGENT[\"🤖 Agent Layer\nSOUL.md, ABOUT.md,\nхарактер и профиль\"] USER[\"👤 User Layer\nпредпочтения,\nконтекст пользователя\"] PROJECT[\"📁 Project Layer\nAGENTS.md, TOOLS.md,\nworkspace + Virtual FS\"] SESSION --\u003e AGENT --\u003e USER --\u003e PROJECT style SESSION fill:#1565c0,color:#fff style AGENT fill:#6a1b9a,color:#fff style USER fill:#e65100,color:#fff style PROJECT fill:#2e7d32,color:#fff Runtime-сборка контекста: при каждом запросе система собирает контекст послойно — от session (самый дешёвый, всегда в окне) до project (самый дорогой, грузится по требованию). Это как MemPalace: старт дешёвый, детализация on-demand.\nФайловая структура агента — прямое вдохновение от OpenClaw:\nФайл Аналог OpenClaw Назначение SOUL.md SOUL.md Характер агента ABOUT.md — Профиль/описание AGENTS.md AGENTS.md Инструкции по созданию агента TOOLS.md — Авто-сборка из скиллов + MCP От MemPalace мы взяли идею layered loading: не грузим всё в контекст сразу, а поднимаем слои по мере необходимости. И концепцию temporal validity — у фактов есть срок годности, устаревшие не попадают в контекст. Но что это значит на практике?\nLayered loading: как это работает у нас В MemPalace четыре слоя (layers.py), и каждый решает свою задачу:\nL0 — Identity (~100 токенов). Plain-text файл ~/.mempalace/identity.txt, который пишет пользователь. «Я — Atlas, личный AI-ассистент Алисы. Черты: тёплый, прямой. Люди: Алиса (создатель), Боб (партнёр Алисы). Проект: приложение для дневников.» Это константа — не меняется между сессиями, не вычисляется, просто читается с диска.\nL1 — Essential Story (~500–800 токенов). Автоматически генерируется из palace: сканирует до 2000 drawers (чанков памяти), ранжирует по весу (importance, emotional_weight, weight), берёт топ-15, группирует по room для читаемости, обрезает до 3200 символов. Это «самое важное, что случилось» — не весь архив, а выжимка. Алгоритм не идеальный: например, drawer с высоким emotional_weight может вытеснить более полезный, но менее «эмоциональный» факт. Но для boot-задачи — дать агенту минимальный контекст — работает.\nL2 — On-Demand (~200–500 токенов за запрос). Фильтрованная выборка: «дай мне всё из wing=driftwood, room=auth-migration». Это metadata-фильтр в ChromaDB, не семантический поиск — просто WHERE wing = ? AND room = ?. Поднимается, когда в разговоре всплывает конкретный топик.\nL3 — Deep Search (без лимита). Полный семантический поиск: col.query(query_texts=[\u0026quot;why did we switch to GraphQL\u0026quot;]) по всему palace. Возвращает результаты с similarity-ранжированием. Это уже тяжёлая артиллерия — когда L1 и L2 не дали нужного контекста.\nСтарт — wake_up() → L0 + L1, ~600–900 токенов. L2 и L3 — через MCP-инструменты, когда агент сталкивается с задачей, требующей контекста.\nМы адаптировали эту модель, но с ключевым отличием: MemPalace — это внешний сервис, а у нас слои собираются внутри runtime агента. Не нужно ходить по MCP за памятью — контекст уже собран к моменту, когда LLM начинает генерировать ответ.\nКак выглядит сборка на каждом запросе:\nСлой Что грузится Когда Токены Session Текущий диалог Всегда 0 (уже в окне) Agent SOUL.md + ABOUT.md Всегда ~1K User Предпочтения, контекст Всегда (для текущего юзера) ~500 Project AGENTS.md + TOOLS.md + workspace По требованию (первый запрос к проекту) ~2–5K Итого на старте: ~1.5K токенов. Больше, чем MemPalace (~600–900), но на порядок меньше, чем OpenClaw (~10K). Компромисс.\nЗачем грузить Project по требованию? Представьте: пользователь зашёл в чат, поздоровался. Агент не знает, в какой проект пойдёт работа. Зачем тратить 5K токенов на загрузку AGENTS.md и TOOLS.md проекта, который может вообще не понадобиться? Поэтому: ждём, пока задача не потребует контекста проекта — и только тогда поднимаем слой.\nА как у нас работает аналог L1 — ранжирование «самого важного»? У нас его нет. SOUL.md и ABOUT.md — это и есть L0/L1, курируемые вручную. Нет автоматического ранжирования из 2000 drawers, зато нет и риска, что алгоритм с высоким emotional_weight вытолкнет полезный, но «скучный» факт. Для платформы с сотнями агентов — это осозненный выбор: предсказуемость важнее автоматизации.\nTemporal validity: факты с сроком годности Это вторая идея из MemPalace, которую мы перенесли. В MemPalace Knowledge Graph реализован на SQLite: каждый факт — это триплет (subject, predicate, object) с полями valid_from и valid_to (knowledge_graph.py). Когда факт устаревает, он не удаляется — вместо этого проставляется valid_to. Это позволяет отвечать на запросы вида «что было правдой на дату X?» фильтрацией: valid_from \u0026lt;= X AND (valid_to \u0026gt;= X OR valid_to IS NULL).\n# MemPalace: adding a temporal fact kg.add_triple(\u0026#34;Kai\u0026#34;, \u0026#34;works_on\u0026#34;, \u0026#34;Orion\u0026#34;, valid_from=\u0026#34;2025-06-01\u0026#34;) # Kai leaves Orion kg.invalidate(\u0026#34;Kai\u0026#34;, \u0026#34;works_on\u0026#34;, \u0026#34;Orion\u0026#34;, ended=\u0026#34;2026-03-01\u0026#34;) # Query: what\u0026#39;s true now? kg.query_entity(\u0026#34;Kai\u0026#34;) # → [Kai → works_on → Orion (ended), Kai → recommended → Clerk] # Query: what was true on Jan 20, 2026? kg.query_entity(\u0026#34;Kai\u0026#34;, as_of=\u0026#34;2026-01-20\u0026#34;) # → [Kai → works_on → Orion (active)] Зачем это нужно? Самая частая проблема с памятью агента — не отсутствие фактов, а устаревшие факты. Агент помнит, что «проект использует REST API», а команда уже три месяца как переехала на gRPC. И вместо правильного ответа агент тащит в контекст ложный факт. Temporal validity решает это: у каждого факта срок годности, истёкшие автоматически исключаются из контекста.\nУ нас реализация проще, чем в MemPalace: вместо отдельного SQLite-графа мы используем метаданные в Virtual FS. Каждый файл памяти агента (MEMORY.md, ABOUT.md) имеет last_modified — и если факт не обновлялся дольше TTL (настраивается), он помечается как stale и не попадает в контекст. Нет полноценного графового запроса с as_of, но для нашего кейса (платформа с сотнями агентов, не один coding-ассистент) этого достаточно. Графовые запросы — это территория Zep, и если нам понадобится логический вывод на графе фактов, мы знаем, куда смотреть.\nОтличие от OpenClaw: мы не храним сырые дневные логи. Вместо этого — курируемая память с автоматическим сжатием. Отличие от mem0: мы не зависим от внешнего сервиса — всё работает внутри платформы через Virtual FS и S3-маунт.\nRuntime-архитектура: от запроса до контекста Выше я описал абстрактные слои памяти. А вот как это выглядит в рантайме — на уровне кода.\nГлавный оркестратор — Chat UseCase. Каждый запрос пользователя проходит через конвейер из 8 этапов:\ngraph TB REQ[\"📥 User Request\"] --\u003e PREP[\"1️⃣ prepareSession\nLoad Session + AgentVersion\"] PREP --\u003e REG[\"2️⃣ RegisterInAgent\nBind tools to agent\"] REG --\u003e PRE[\"3️⃣ ExecutePreAgent\nRAG pre-search\"] PRE --\u003e ASM[\"4️⃣ Context Assembly\nSession[] + SystemPrompt + Tool Injections\"] ASM --\u003e LLM[\"5️⃣ LLM react-loop\neino/adk Runner\"] LLM --\u003e|\"needs context\"| TOOLS[\"6️⃣ Tool Execution\nWorkspace files / RAG / Skills\"] TOOLS --\u003e|\"loaded into context\"| LLM LLM --\u003e|\"done\"| POST[\"7️⃣ PostAgent + Hooks\ncleanup + side effects\"] POST --\u003e SAVE[\"8️⃣ Update Session\nContext[]\"] style REQ fill:#1565c0,color:#fff style TOOLS fill:#e65100,color:#fff style SAVE fill:#2e7d32,color:#fff Ключевой момент: контекст собирается до того, как LLM начинает генерировать. К моменту вызова runner.Run() всё уже собрано — сессия, системный промпт, инъекции от инструментов. LLM получает готовый контекст и работает с ним.\nНо это не значит, что вся память грузится сразу. SOUL.md и ABOUT.md уже в системном промпте — это «горячая» память. А вот файлы из Workspace агент запрашивает сам во время react-loop (этап 5→6 на схеме), когда понимает, что ему не хватает контекста. Это и есть on-demand loading из MemPalace — только вместо MCP-инструментов у нас eino tools.\nПять scope\u0026rsquo;ов Workspace и их маппинг на S3:\nScope Virtual Path S3 Prefix Кто использует session /storage/session/... projects/{pid}/sessions/{sid}/workspace/ Артефакты текущего диалога user /storage/user/... projects/{pid}/users/{uid}/workspace/ Файлы и предпочтения пользователя agent_user /storage/agent_user/... projects/{pid}/agents/{aid}/users/{uid}/workspace/ Память пары «агент ↔ юзер» skills /storage/skills/... projects/{pid}/skills/ SKILL.md, проектные документы hooks /storage/hooks/... projects/{pid}/hooks/ Hook-скрипты (post-agent) И в отличие от OpenClaw и MemPalace, нам не нужен внешний сервис памяти — всё работает внутри платформы через Workspace (Virtual FS → S3) и Session Context (PostgreSQL).\n7. Итоги Пять подходов к памяти агента — от файлового мозга до управляемого сервиса:\nПодход Проект Boot cost Хранилище Управление Best for File-First OpenClaw ~10K токенов Markdown-файлы Курируемое вручную Coding-агенты, полный контроль Structured Compression MemPalace ~600–900 токенов Иерархия + ChromaDB Авто (ранжирование + temporal) Много-проектные агенты, строгий бюджет Managed Layer mem0 ~6.8K токенов Векторная БД Авто (SaaS) Быстрый старт, не хочешь инфра Vector + Graph Zep Зависит от запроса Векторы + Knowledge Graph Авто (self-hosted) Структурированные связи, privacy Embedded SDK LangMem Зависит от типа LangGraph store Авто + процедурная память Экосистема LangChain Нет серебряной пули. OpenClaw даёт максимальный контроль, но сжигает токены. MemPalace элегантна, но требует дисциплины в структуре. mem0 прост в интеграции, но вендор-лок. Zep мощен для графовых связей, но тяжёл в деплое. LangMem идеален для LangChain, но бесполезен вне его.\nМы выбрали гибрид: слои памяти как у MemPalace, файловая структура как у OpenClaw, runtime-сборка вместо статического boot. Потому что наша задача — не coding-агент и не чат-бот, а платформа, где агенты разных типов живут и работают вместе. И память должна обслуживать это разнообразие.\nПредыдущая статья серии: Часть 4: Multi-Agent Patterns\n","permalink":"https://triumphpc.github.io/blog/ru/posts/ai-agent-design-patterns-5-memory/","summary":"Контекстное окно ≠ память. Разбираю проблему agent amnesia, академическую таксономию (Du et al., 2026), пять подходов к памяти — от файлового мозга OpenClaw до управляемого слоя mem0 — и как наша команда решает это на практике.","title":"Шаблоны проектирования AI-агентов. Часть 5: Agent Memory Management"},{"content":"1. Введение В Части 1 я разобрал ReAct — паттерн, где агент чередует рассуждение и действие. Проблема: ReAct планирует на один шаг вперёд. Для задач, где нужен глобальный план — анализ инцидента, многошаговый debug, оркестрация сервисов — myopic-подход ломается. Plan-and-Execute решает эту проблему разделением планирования и выполнения.\n2. Что такое Plan-and-Execute История В мае 2023 года Wang et al. опубликовали Plan-and-Solve Prompting — zero-shot стратегию, которая сначала составляет план разбиения задачи на подзадачи, а затем выполняет их по порядку. Это была прямая реакция на три ошибки Zero-shot-CoT: пропущенные шаги, арифметические ошибки, семантические недопонимания.\nВ марте 2023 года Shen et al. представили HuggingGPT — LLM как контроллер, который планирует задачу, выбирает модели из Hugging Face и выполняет каждую подзадачу. Это первая реализация паттерна в production-масштабе: ChatGPT декомпозирует запрос, подбирает AI-модели по описаниям, выполняет и суммаризирует результат.\nИтак. Паттерн оформился: отдельный Planner (стратег), отдельный Executor (исполнитель), опциональный Reviser (ревьюер). LangChain — Python-фреймворк для построения LLM-приложений, включающий агентные примитивы, RAG-пайплайны и инструменты оркестрации — популяризировал этот паттерн как Plan-and-Execute Agent в 2024 году, но идея — из 2023-го.\nАналогия с командой Представьте: техлид (Planner) декомпозирует задачу на тикеты, разработчик (Executor) берёт тикет и делает, QA-инженер (Reviser) проверяет — и если найдёт проблему, тикет возвращается на доработку. ReAct — это разработчик-одиночка, который сам ставит себе задачу на один шаг, делает, проверяет, ставит следующую. Для простых задач — ок. Для сложных — нужен менеджер.\nФормальное определение Plan-and-Execute работает в три фазы:\nPlan: Planner генерирует список шагов [S₁, S₂, ..., Sₙ] для решения задачи. Execute: Executor выполняет каждый шаг Sᵢ, вызывая инструменты и получая наблюдения. Revise: Reviser оценивает прогресс и при необходимости модифицирует оставшийся план. Ключевое отличие от ReAct: планирование глобальное, а не пошаговое. Planner видит задачу целиком и строит план на N шагов вперёд. Executor получает один шаг за раз — не весь план, не пользовательский ввод. Это создаёт естественную границу безопасности.\n3. Какую задачу решает Проблема ReAct: myopic планирование ReAct, как я показал в Части 1, планирует на один шаг. Каждый Thought — реакция на предыдущее Observation, а не на общую стратегию. Для задач с известной структурой (анализ логов → фильтрация → корреляция → диагноз) это неэффективно: агент «бродит» вместо того, чтобы следовать плану.\nПроблема ReAct: стоимость Каждый шаг ReAct — полноценный вызов LLM с полным контекстом (все предыдущие Thoughts + Actions + Observations). При 10 шагах — 10 вызовов дорогой модели. ReWOO (Xu et al., 2023) показал, что это избыточно: до 5x сокращение потребления токенов на HotpotQA при росте точности на 4.4% (Xu et al., 2023).\nПроблема ReAct: нет явного перепланирования ReAct не пересматривает план — его просто нет. Если шаг привёл в тупик, модель «имплицитно» корректируется через следующий Thought. Но это не перепланирование — это реактивное блуждание. Plan-and-Execute делает перепланирование явным через Reviser.\nЧто обеспечивает Plan-and-Execute Проблема Решение Plan-and-Execute Myopic планирование Planner видит задачу целиком, строит N-шаговый план Высокая стоимость Executor может использовать дешёвую модель — ему нужен только один шаг Нет перепланирования Reviser явно оценивает прогресс и модифицирует план Prompt injection Executor не видит пользовательский ввод и полный план Непрозрачность План — читаемый артефакт, который можно аудировать 4. Архитектура Plan → Execute → Revise sequenceDiagram participant U as User participant P as Planner participant E as Executor participant T as Tools participant R as Reviser U-\u003e\u003eP: Task P-\u003e\u003eP: Generate plan [S₁, S₂, ..., Sₙ] loop For each step P-\u003e\u003eE: Step Sᵢ E-\u003e\u003eT: Call tool T--\u003e\u003eE: Observation E--\u003e\u003eR: Step result R-\u003e\u003eR: Evaluate progress alt Plan needs revision R-\u003e\u003eP: Revised plan else Step OK R-\u003e\u003eE: Next step Sᵢ₊₁ end end R--\u003e\u003eU: Final answer Planner получает задачу и генерирует план. Executor выполняет шаг за шагом, вызывая инструменты. Reviser проверяет результат каждого шага и решает: продолжать план, пересмотреть или завершить.\nEino State Graph В Eino паттерн реализуется через compose.Graph — направленный граф с тремя узлами-агентами:\ngraph TB START((START)) --\u003e PL[PlannerBaseChatModel] PL --\u003e EX[ExecutorToolCallingChatModel+ ToolsNode] EX --\u003e RV{ReviserBaseChatModel} RV --\u003e|needs revision| PL RV --\u003e|step OK, more steps| EX RV --\u003e|all done| END((END)) State-структура хранит: сообщения (history), текущий план (steps), номер шага. Параметр maxStep ограничивает число итераций — защита от бесконечных циклов.\nЭволюция паттернов graph LR C[\"CoTWei 2022\"] --\u003e R[\"ReActYao 2022\"] R --\u003e PS[\"Plan-and-SolveWang 2023\"] PS --\u003e PA[\"Plan-and-ExecuteLangChain 2024\"] PA --\u003e RW[\"ReWOOXu 2023\"] PA --\u003e LC[\"LLMCompilerKim 2023\"] CoT (Chain-of-Thought, Wei et al., 2022) дал пошаговое рассуждение без действий. ReAct (Yao et al., 2022) добавил действия. Plan-and-Solve (Wang et al., 2023) ввёл явное планирование. Plan-and-Execute оформил это как архитектурный паттерн с отдельными агентами. ReWOO и LLMCompiler — оптимизации базового паттерна.\n5. Варианты ReWOO: Reasoning Without Observation ReWOO (Xu et al., 2023) убирает interleaved-вызовы LLM. Planner один раз генерирует план с переменными, Executor подставляет результаты — без вызова LLM на каждом шаге. Результат: 5x сокращение токенов и +4.4% точности на HotpotQA. Дополнительный бонус: можно дистиллировать Planner с 175B (GPT-3.5) на 7B (LLaMA) — и это работает.\nКак это выглядит на нашем сценарии анализа логов:\ngraph LR subgraph \"1. Planner (LLM, 1 вызов)\" P[\"План:#E1 = query_logs('error spike')#E2 = filter_by_time(#E1, 'last hour')#E3 = correlate_deployments(#E1)#E4 = suggest_fix(#E2, #E3)\"] end subgraph \"2. Executor (без LLM)\" E1[\"#E1 → query_logs→ результат в #E1\"] E2[\"#E2 → filter_by_time(#E1)→ результат в #E2\"] E3[\"#E3 → correlate(#E1)→ результат в #E3\"] E4[\"#E4 → suggest_fix(#E2,#E3)→ итоговый ответ\"] end P --\u003e E1 E1 --\u003e E2 E1 --\u003e E3 E2 --\u003e E4 E3 --\u003e E4 style P fill:#4a9eff,color:#fff style E1 fill:#2d8659,color:#fff style E2 fill:#2d8659,color:#fff style E3 fill:#2d8659,color:#fff style E4 fill:#2d8659,color:#fff Ключевой инсайт: Planner использует LLM один раз для генерации плана. Дальше Executor просто вызывает инструменты и подставляет результаты в переменные — как в шаблонизаторе. LLM нужна только для следующего Planner-вызова, если Reviser решил перепланировать.\nLLMCompiler: параллельное выполнение LLMCompiler (Kim et al., 2023) берёт идею классических компиляторов: Planner генерирует DAG (Directed Acyclic Graph, направленный ациклический граф) зависимостей между шагами, Task Fetching Unit диспетчеризирует независимые шаги параллельно, Executor выполняет их concurrently. Результат: 3.7x ускорение latency, 6.7x экономия стоимости, ~9% рост точности по сравнению с ReAct.\nНа нашем сценарии это выглядит так:\ngraph LR subgraph \"1. Planner → DAG\" P[\"План с зависимостями:#E1 = query_logs('error')#E2 = filter_by_time(#E1)#E3 = correlate_deployments(#E1)#E4 = query_metrics('cpu')#E5 = suggest_fix(#E2, #E3, #E4)\"] end subgraph \"2. Параллельное выполнение\" E1[\"#E1 query_logs⏱ 200ms\"] E2[\"#E2 filter_by_time⏱ 100ms\"] E3[\"#E3 correlate⏱ 150ms\"] E4[\"#E4 query_metrics⏱ 120ms\"] E5[\"#E5 suggest_fix⏱ 300ms\"] end P --\u003e E1 E1 --\u003e E2 E1 --\u003e E3 E1 --\u003e E4 E2 --\u003e E5 E3 --\u003e E5 E4 --\u003e E5 style P fill:#4a9eff,color:#fff style E1 fill:#2d8659,color:#fff style E2 fill:#e6a817,color:#333 style E3 fill:#e6a817,color:#333 style E4 fill:#e6a817,color:#333 style E5 fill:#2d8659,color:#fff Жёлтым выделены шаги, которые выполняются параллельно: после получения #E1 (query_logs) шаги #E2, #E3, #E4 запускаются одновременно — каждый зависит только от #E1, но не друг от друга. Итоговое время: 200ms + max(100, 150, 120) + 300ms = 650ms вместо секвенциальных 870ms. На реальных задачах с десятками шагов выигрыш растёт до 3.7x.\nHuggingGPT: LLM как контроллер HuggingGPT (Shen et al., 2023) — конкретная реализация паттерна: ChatGPT планирует задачу, выбирает AI-модели из Hugging Face по описаниям, выполняет каждую подзадачу выбранной моделью, суммаризирует результат. Валидация — через ручной контроль формата вывода. Не отдельный вариант, а иллюстрация применимости паттерна к реальным системам.\ngraph LR U[\"Пользователь:Сгенерируй изображениеи опиши его голосом\"] P[\"ChatGPT(Planner)\"] M1[\"Stable Diffusion(text-to-image)\"] M2[\"Whisper(speech-to-text)\"] M3[\"Bark(text-to-speech)\"] S[\"ChatGPT(Summarizer)\"] U --\u003e P P --\u003e|\"1. image generation\"| M1 P --\u003e|\"2. describe image\"| M2 P --\u003e|\"3. voice output\"| M3 M1 --\u003e S M2 --\u003e S M3 --\u003e S S --\u003e U style P fill:#4a9eff,color:#fff style M1 fill:#9b59b6,color:#fff style M2 fill:#9b59b6,color:#fff style M3 fill:#9b59b6,color:#fff style S fill:#4a9eff,color:#fff style U fill:#555,color:#fff Фиолетовым — специализированные AI-модели из Hugging Face. ChatGPT выступает как Planner: разбирает запрос, подбирает модели по описаниям их возможностей, передаёт результаты между ними и формирует финальный ответ. В отличие от ReWOO и LLMCompiler, Executor здесь — не LLM, а внешние модели. Паттерн тот же: планирование → выполнение → суммаризация.\nПолный разбор ReWOO и LLMCompiler — в Части 4 этого цикла.\n6. Безопасность: Control-Flow Integrity И тут начинается самое интересное. Разделение Planner и Executor — не просто архитектурный изыск. Это защита от prompt injection (атака, при которой злоумышленник внедряет вредоносную инструкцию в данные, которые LLM обрабатывает, — например, в вывод инструмента или пользовательский контент).\nDel Rosario et al. (2025) в «Architecting Resilient LLM Agents» формулируют принцип control-flow integrity: если Executor видит только один шаг плана (не весь план, не пользовательский ввод), то атакующий, контролирующий вывод инструмента, не может перенаправить весь workflow. Blast radius ограничен одним шагом.\nКонтраст с ReAct: в ReAct-агенте полный контекст (включая пользовательский ввод) доступен на каждом шаге. Если инструмент вернул вредоносный контент, модель может интерпретировать его как новую инструкцию — и изменить поведение. В Plan-and-Execute Executor получает изолированную задачу: «вызови инструмент X с параметрами Y».\nДополнительные меры защиты из Del Rosario 2025:\nPrinciple of Least Privilege: каждому шагу — минимальный набор инструментов Task-scoped tool access: Executor для шага «фильтрация логов» не должен иметь доступ к инструменту «удаление записей» Sandboxed execution: код, генерируемый Executor, выполняется в песочнице HITL (Human-in-the-Loop): критические шаги требуют подтверждения человеком Это не теоретические рекомендации. Del Rosario et al. приводят имплементационные чертежи для LangGraph, CrewAI и AutoGen — с рабочим кодом.\n7. Практика: Plan-and-Execute на Eino, Go Сценарий: анализ production-инцидента Дежурный инженер получает алерт: latency spike на сервисе orders. Нужно: запросить логи → отфильтровать по времени → скоррелировать с деплоями → запросить метрики → предложить фикс. Пять шагов, известная структура — идеальный кейс для Plan-and-Execute.\nУстановка go get github.com/cloudwego/eino@latest go get github.com/cloudwego/eino-ext/components/model/openai@latest Минимальный Plan-and-Execute агент package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/cloudwego/eino-ext/components/model/openai\u0026#34; \u0026#34;github.com/cloudwego/eino/components/tool\u0026#34; \u0026#34;github.com/cloudwego/eino/compose\u0026#34; \u0026#34;github.com/cloudwego/eino/flow/agent/multiagent/planexecute\u0026#34; \u0026#34;github.com/cloudwego/eino/schema\u0026#34; ) func main() { ctx := context.Background() plannerModel, _ := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ Model: \u0026#34;gpt-4o\u0026#34;, }) executorModel, _ := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ Model: \u0026#34;gpt-4o-mini\u0026#34;, }) reviserModel, _ := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ Model: \u0026#34;gpt-4o\u0026#34;, }) agent, _ := planexecute.NewAgent(ctx, \u0026amp;planexecute.AgentConfig{ Planner: plannerModel, Reviser: reviserModel, Executor: executorModel, ToolsConfig: compose.ToolsNodeConfig{ InvokableTools: []tool.InvokableTool{ queryLogsTool(), filterByTimeTool(), correlateDeploymentsTool(), queryMetricsTool(), suggestFixTool(), }, }, MaxStep: 10, }) msg, _ := agent.Generate(ctx, []*schema.Message{ schema.UserMessage( \u0026#34;Latency spike на сервисе orders. \u0026#34; + \u0026#34;Найди root cause и предложи фикс.\u0026#34;, ), }) fmt.Println(msg.Content) } Обратите внимание: Planner и Reviser используют gpt-4o (стратегические решения), а Executor — gpt-4o-mini (вызов одного инструмента). Это экономия: дешёвая модель для тактической работы, дорогая — для стратегической. В ReAct такой оптимизации нет — каждый шаг требует полноценной модели.\nИнструменты определяются через utils.NewTool — так же, как в Части 1 для ReAct. Полный пример с инструментами и промптами: eino-examples/flow/agent/multiagent/plan_execute.\nТрассировка выполнения Раскройте блоки, чтобы увидеть промежуточные выходы агентов:\nШаг 1: Planner — генерация плана Задача: Latency spike на сервисе orders. Найди root cause и предложи фикс. План: 1. query_logs — запросить логи сервиса orders за последний час 2. filter_by_time — отфильтровать логи по временному окну spike (14:00-14:30) 3. correlate_deployments — проверить деплои за то же окно 4. query_metrics — запросить CPU/memory метрики сервиса orders 5. suggest_fix — на основе данных предложить исправление Шаг 2: Executor — query_logs Вызов: query_logs(service=\u0026#34;orders\u0026#34;, level=\u0026#34;ERROR\u0026#34;, limit=100) Результат: 47 ERROR записей за последний час. Основные ошибки: - \u0026#34;timeout connecting to payments service\u0026#34; (23 occurrences) - \u0026#34;connection pool exhausted\u0026#34; (18 occurrences) - \u0026#34;slow query: SELECT * FROM orders WHERE...\u0026#34; (6 occurrences) Шаг 3: Executor — filter_by_time + correlate_deployments Вызов: filter_by_time(logs=\u0026lt;step2_result\u0026gt;, start=\u0026#34;14:00\u0026#34;, end=\u0026#34;14:30\u0026#34;) Результат: Spike начался в 14:12. До 14:12 — нормальный уровень ошибок. Вызов: correlate_deployments(service=\u0026#34;orders\u0026#34;, start=\u0026#34;14:00\u0026#34;, end=\u0026#34;14:30\u0026#34;) Результат: Деплой payments-service v2.3.1 в 14:10 (за 2 минуты до spike). Шаг 4: Executor — query_metrics Вызов: query_metrics(service=\u0026#34;orders\u0026#34;, metric=\u0026#34;cpu,memory\u0026#34;, start=\u0026#34;14:00\u0026#34;, end=\u0026#34;14:30\u0026#34;) Результат: CPU orders — норма (45%). Memory — норма (62%). Но: latency payments-service вырос с 50ms до 2000ms начиная с 14:10. Шаг 5: Reviser — валидация + suggest_fix Reviser: Root cause найден — деплой payments-service v2.3.1 в 14:10 вызвал рост latency в payments, что привело к timeout в orders. План выполнен, перепланирование не требуется. Вызов: suggest_fix(root_cause=\u0026#34;payments-service v2.3.1 latency regression\u0026#34;, affected_service=\u0026#34;orders\u0026#34;) Результат: 1. Немедленно: откатить payments-service до v2.3.0 2. Краткосрочно: увеличить timeout и connection pool для orders→payments 3. Долгосрочно: добавить circuit breaker между orders и payments Ключевые параметры Planner — модель для генерации плана (интерфейс BaseChatModel). Reviser — модель для оценки прогресса и перепланирования (BaseChatModel). Executor — модель для выполнения шагов (ToolCallingChatModel + ToolsNode). MaxStep — лимит итераций графа (default 12). Защита от бесконечных циклов. ToolsConfig — инструменты, доступные Executor. Ссылки для углубления Eino Plan-and-Execute Agent Manual — полное описание параметров eino-examples/flow/agent/multiagent/plan_execute — полный рабочий пример GitHub: cloudwego/eino — исходники 8. Сравнительные таблицы Архитектура: ReAct vs Plan-and-Execute Измерение ReAct Plan-and-Execute Горизонт планирования Myopic: 1 шаг Global: N шагов LLM-вызовы на шаг Полная модель каждый шаг Дешёвый Executor для шага, дорогой Planner один раз Управление контекстом Scratchpad: все Thoughts/Actions/Observations накапливаются State: план + результаты шагов Перепланирование Имплицитное: через следующий Thought Явное: Reviser модифицирует план Восстановление после ошибок Retry in-place: модель корректирует на следующем шаге Re-plan from checkpoint: Reviser перестраивает оставшийся план Архитектурная сложность Один цикл (ChatModel → ToolsNode) Три агента (Planner → Executor → Reviser) Устойчивость к prompt injection Низкая: полный контекст доступен на каждом шаге Выше: Executor видит только один шаг (Del Rosario et al., 2025) Производительность и стоимость Метрика ReAct Plan-and-Execute (naive) ReWOO LLMCompiler Потребление токенов (относительно ReAct) 1x ~1.2x (overhead от Planner/Reviser) 0.2x (Xu 2023) ~0.5x (параллельное выполнение) Число LLM-вызовов N шагов × полная модель 1 Planner + N × Executor + k × Reviser 1 Planner + N × tool calls (без LLM) 1 Planner + параллельные Executor Latency Секвенциальная: N × T_step Секвенциальная: T_plan + N × T_exec Секвенциальная, но без LLM на шаг Параллельная: ~T_plan + T_slowest_step (Kim 2023) Точность (HotpotQA) Baseline Сопоставимо +4.4% (Xu 2023) +~9% vs ReAct (Kim 2023) Ускорение latency 1x ~1x ~1x 3.7x (Kim 2023) Экономия стоимости 1x ~0.8x (дешёвый Executor) ~5x (Xu 2023) 6.7x (Kim 2023) Варианты Plan-and-Execute Вариант Токены vs ReAct Ускорение Точность (delta) Сложность Ключевая инновация Naive (LangChain-style) ~1.2x ~1x ~0% Низкая Планирование + секвенциальное выполнение ReWOO (Xu 2023) 0.2x (-5x) ~1x +4.4% Средняя Подстановка переменных, LLM только для Planner LLMCompiler (Kim 2023) ~0.5x 3.7x +~9% Высокая DAG зависимостей, параллельное выполнение Руководство по выбору паттерна Характеристика задачи Рекомендуемый паттерн Пример Простая, один API-вызов с парсингом ReAct «Какая погода в Париже?» → один вызов weather API Сложная, многошаговая, структура известна Plan-and-Execute Анализ production-инцидента: логи → фильтр → корреляция → диагноз Параллелизуемые подзадачи LLMCompiler «Сравни цены на iPhone в 5 магазинах» → 5 параллельных вызовов Чувствительность к стоимости, фиксированный набор инструментов ReWOO Регулярный мониторинг: тот же набор шагов, тот же набор инструментов Требуется учёсть прошлые ошибки Reflexion Код-ревью: агент проверяет, находит баг, перепроверяет исправление 9. Когда НЕ использовать Microsoft Azure Architecture Center в руководстве по оркестрации AI-агентов формулирует принцип: используй минимальный уровень сложности, который решает задачу.\nЕсли ваша задача\u0026hellip; \u0026hellip;рассмотрите альтернативу Решается прямым вызовом модели Direct model call — классификация, суммаризация Нужен один-два инструмента, шаги неизвестны заранее ReAct — как в Части 1 Фиксированный workflow без перепланирования ReWOO — дешевле, без Reviser-overhead Требуется деревья гипотез Tree of Thoughts (Yao et al., 2023) Несколько доменов, разные security boundaries Multi-agent — Part 5 этого цикла Plan-and-Execute — это средний уровень сложности между ReAct и multi-agent. Не прыгайте на него, если задача решается ReAct. Не оставайтесь на нём, если нужна координация нескольких агентов.\n10. Что дальше Часть 3: Reflexion Pattern — ReAct + самооценка: агент учится на собственных ошибках через вербальное подкрепление. Часть 4: ReWOO и LLMCompiler — глубокий разбор двух оптимизаций Plan-and-Execute: variable substitution и parallel DAG execution. Часть 5: Сравнение паттернов + Multi-Agent Orchestration — когда какой паттерн выбирать, и как несколько агентов координируются для решения сложных задач. Обсуждение приветствуется — комментарии на сайте или GitHub Issues.\n11. Ссылки Исследовательские статьи Wang et al., 2023 — Plan-and-Solve Prompting: Zero-shot декомпозиция задачи на подзадачи. Решение проблемы пропущенных шагов Zero-shot-CoT. arXiv:2305.04091 Shen et al., 2023 — HuggingGPT: LLM как контроллер для оркестрации AI-моделей из Hugging Face. Первая production-реализация паттерна. arXiv:2303.17580 Xu et al., 2023 — ReWOO: Отделение рассуждения от наблюдений. Variable substitution, 5x token efficiency, +4.4% точности на HotpotQA. Дистилляция 175B→7B. arXiv:2305.18323 Kim et al., 2023 — LLMCompiler: Параллельное выполнение function calls через DAG. 3.7x latency speedup, 6.7x cost savings, +~9% точности. arXiv:2312.04511 Del Rosario et al., 2025 — Plan-then-Execute Security: Control-flow integrity как защита от prompt injection. Least privilege, task-scoped tool access, sandboxed execution. arXiv:2509.08646 Yao et al., 2022 — ReAct: Базовый паттерн Reasoning + Acting, от которого отталкивается Plan-and-Execute. arXiv:2210.03629 Wei et al., 2022 — Chain-of-Thought: Пошаговое рассуждение — предшественник всех агентных паттернов. arXiv:2201.11903 Документация и примеры Eino Plan-and-Execute Agent Manual — все параметры и конфигурация Eino Open Source Announcement — обзор фреймворка GitHub: cloudwego/eino — исходники GitHub: eino-examples/flow/agent/multiagent/plan_execute — полный рабочий пример Архитектурные руководства AI agent design patterns — Microsoft Azure Architecture Center — иерархия сложности, паттерны оркестрации, production-рекомендации Следующая статья: Шаблоны проектирования AI-агентов. Часть 3: Reflexion Pattern — ReAct + самооценка: агент учится на собственных ошибках через вербальное подкрепление и эпизодическую память.\n","permalink":"https://triumphpc.github.io/blog/ru/posts/ai-agent-design-patterns-2-plan-execute/","summary":"Plan-and-Execute — паттерн, который разделяет стратегическое планирование и тактическое выполнение. Разбираю архитектуру Planner+Executor+Reviser, варианты ReWOO и LLMCompiler с цифрами из оригинальных работ, защиту от prompt injection через control-flow integrity, и минимальный рабочий код на Go через Eino.","title":"Шаблоны проектирования AI-агентов. Часть 2: Plan-and-Execute Pattern"},{"content":"1. Введение В Части 1 я разобрал ReAct — паттерн, где агент чередует рассуждение и действие. В Части 2 — Plan-and-Execute, где отдельный Planner строит N-шаговый план. Оба паттерна объединяет один недостаток: агент не учится на своих ошибках. Каждый запуск — с чистого листа. Reflexion-паттерн решает эту проблему: агент совершает попытку, оценивает результат, рефлексирует — и при следующей попытке использует накопленный опыт.\n2. Что такое Reflexion История В декабре 2022 года Anthropic опубликовала Constitutional AI — подход, где языковая модель критикует собственные ответы и переписывает их для снижения вредности. Это был первый масштабный пример self-critique-паттерна: generate → self-critique → revise. Но Constitutional AI использовала критику для обучения (finetuning через RLAIF), а не для инференса.\nВ марте 2023 года Madaan et al. опубликовали Self-Refine — итеративное улучшение через self-feedback. Та же LLM выступает в трёх ролях: генератор, критик и рефайнер. Результат: ~20% average improvement на 7 задачах (Madaan et al., 2023). Но есть нюанс — на задачах рассуждения (Math Reasoning) улучшение 0%: модель не способна надёжно определить, правильное у неё рассуждение или нет.\nИ тут начинается самое интересное. В том же марте 2023 года Shinn et al. опубликовали Reflexion — и решили главную проблему Self-Refine: добавили эпизодическую память и внешнюю оценку. Вместо одного цикла critique-refine — мульти-триальный процесс, где агент накапливает вербальные «уроки» и использует их при следующих попытках. Результат: 91% pass@1 на HumanEval (против 80% у GPT-4) и +22% absolute на AlfWorld (Shinn et al., 2023).\nАналогия с разработчиком Представьте: джун пишет код, запускает тесты — падают. Что он делает? Не переписывает с нуля — он читает ошибку, понимает причину, записывает «в следующий раз проверю edge-case с nil». Это и есть рефлексия: не просто исправить, а извлечь урок на будущее. ReAct — джун без памяти: каждый раз наступает на те же грабли. Plan-and-Execute — джун с планом, но без ретроспективы. Reflexion — джун, который ведёт дневник ошибок.\nФормальное определение Reflexion работает в три фазы, повторяющиеся циклически:\nAct: Actor (LLM) генерирует действия и получает наблюдения из среды. Evaluate: Evaluator оценивает результат — скалярной оценкой или свободным текстом. Reflect: Self-Reflection модель генерирует вербальную обратную связь — что пошло не так и как исправить. Рефлексия сохраняется в episodic memory. При следующей попытке Actor получает содержимое эпизодической памяти в контексте — и может избежать повторения ошибок.\n3. Какую задачу решает Проблема ReAct: нет обучения на ошибках ReAct, как я показал в Части 1, чередует Thought-Action-Observation в одном цикле. Если задача не решена — агент просто начинает заново. Прошлый опыт? Утрачен. Каждый запуск — первый и последний.\nПроблема Plan-and-Execute: нет ретроспективы Plan-and-Execute строит план и выполняет его. Reviser корректирует план при необходимости — но внутри одного запуска. Между запусками — чистый лист. На прошлых ошибках не учимся.\nПроблема Self-Refine: нет памяти между попытками Self-Refine делает critique-refine в одном вызове LLM. Улучшение есть на задачах генерации (стиль, формат) — но на задачах рассуждения 0%, потому что модель не может надёжно оценить правильность собственного рассуждения без внешнего арбитра (Madaan et al., 2023, Table 1: Math Reasoning).\nЧто обеспечивает Reflexion Проблема Решение Reflexion Нет обучения на ошибках Episodic memory хранит вербальные уроки между попытками Ненадёжная самооценка Evaluator — внешний арбитр (тесты, компилятор, среда) Нет ретроспективы Каждая попытка обогащается рефлексиями из прошлых Myopic исправления Рефлексия фокусируется на причине ошибки, а не на симптоме 4. Архитектура Actor → Evaluator → Reflector → Memory → retry sequenceDiagram participant U as User participant A as Actor participant E as Evaluator participant R as Reflector participant M as Episodic Memory U-\u003e\u003eA: Task + reflections from memory A-\u003e\u003eA: Generate actions (ReAct loop) A-\u003e\u003eE: Trajectory (actions + observations) alt Result is correct E--\u003e\u003eU: ✅ Success else Result has errors E-\u003e\u003eR: Trajectory + evaluation R-\u003e\u003eR: Generate verbal reflection\"I failed because...\" R-\u003e\u003eM: Store reflection M--\u003e\u003eA: Enriched context for next attempt Note over A,M: Retry with accumulated experience end Компоненты Actor — LLM, генерирующая действия. Может быть ReAct-агентом или простой ChatModel. Ключевое отличие от обычного ReAct: Actor получает содержимое эпизодической памяти в system prompt, что позволяет учитывать прошлые ошибки.\nEvaluator — оценивает результат Actor. Может быть:\nДетерминированным: unit tests, компилятор, game environment — даёт объективную оценку LLM-based: другая языковая модель оценивает качество — менее надёжно, но применимо для open-ended задач Почему это важно? Именно внешний верификатор решает проблему Self-Refine, где модель не способна оценить собственную правильность.\nSelf-Reflection model — LLM, генерирующая вербальную рефлексию. Получает: траекторию Actor (actions + observations), оценку Evaluator, прошлые рефлексии из памяти. Генерирует текст вида: «Я допустил ошибку в обработке edge-case с пустым списком. В следующий раз нужно проверить len() \u0026gt; 0 перед доступом к элементу».\nEpisodic Memory — хранилище рефлексий. Простая структура: список текстовых строк, которые инжектируются в контекст Actor при следующей попытке. Чем больше попыток — тем богаче память.\n5. Эволюция self-critique Три паттерна self-critique появились за 3 месяца — и каждый решал проблему предыдущего:\nflowchart TD CAI[\"🛡️ Constitutional AIDec 2022 • Bai/AnthropicSelf-critique → reviseдля обучения (finetuning)Цель: harmlessness\"] SRF[\"🔄 Self-RefineMar 2023 • Madaan/CMUSame LLM: generate → critique → refineдля инференса (1 вызов)~20% improvement на генерации\"] REF[\"🧠 ReflexionMar 2023 • Shinn/Princeton+NEUActor → Evaluator → Reflectorдля инференса (N попыток)+ Episodic Memory\"] HUANG[\"⚠️ Huang et al.Oct 2023 • ICLR 2024LLMs Cannot Self-CorrectReasoning YetБез external feedback — не работает\"] RRR[\"🚀 Reflect, Retry, RewardMay 2025 • Bensal/WriterRL-тренировка рефлексий1.5B-7B бьёт 10x модели\"] CAI --\u003e|\"добавилinference-timecritique\"| SRF SRF --\u003e|\"добавилepisodic memory+ external eval\"| REF REF --\u003e|\"показалограничениябез верификатора\"| HUANG HUANG --\u003e|\"RL-тренировкалучших рефлексий\"| RRR style CAI fill:#2e7d32,color:#fff style SRF fill:#1565c0,color:#fff style REF fill:#e65100,color:#fff style HUANG fill:#c62828,color:#fff style RRR fill:#6a1b9a,color:#fff Сравнение трёх паттернов Аспект Constitutional AI Self-Refine Reflexion Дата Dec 2022 Mar 2023 Mar 2023 Цель Harmlessness (safety) Качество вывода Качество вывода + обучение Критика Self-critique Self-feedback External evaluator + self-reflection Память Нет Нет Episodic memory Попытки 1 1 (итерации внутри) N (мульти-триальный) Применение Finetuning (offline) Инференс (online) Инференс (online) Результат Улучшение harmlessness +20% на генерации, 0% на рассуждении +11% HumanEval (91% vs 80% GPT-4), +22% AlfWorld Тип оценки RLAIF (RL от AI-судьи) Self-judge External verifier Закономерность очевидна: каждый следующий паттерн добавляет то, чего не хватало предыдущему. Constitutional AI не было памяти и мульти-триальности. Self-Refine добавил inference-time critique, но без памяти и без внешнего верификатора. Reflexion замкнул петлю: внешний верификатор решает проблему ненадёжной самооценки, а эпизодическая память позволяет учиться между попытками.\n6. Когда работает / когда нет Зачем отдельная секция про ограничения? Потому что в октябре 2023 года Huang et al. опубликовали «Large Language Models Cannot Self-Correct Reasoning Yet» — и доказали, что self-correction без внешней обратной связи ухудшает результат.\nflowchart TD START[Задача агента] --\u003e Q{Есть внешнийверификатор?} Q --\u003e|Да| Q2{Низкая начальнаяточность?} Q --\u003e|Нет| FAIL[\"❌ Reflexion не поможетSelf-correction ухудшит результат(Huang et al., 2023)\"] Q2 --\u003e|Да| WORKS[\"✅ Reflexion работает+11-22% improvement(HumanEval, AlfWorld)\"] Q2 --\u003e|Нет| WARN[\"⚠️ Может не окупитьсяРиск ухудшения на простых задачахDiminishing returns\"] WORKS --\u003e REC1[\"Рекомендация:max 3-5 попыток,оценивай cost/benefit\"] WARN --\u003e REC2[\"Рекомендация:не более 2 попыток,мониторь качество\"] style FAIL fill:#c62828,color:#fff style WORKS fill:#2e7d32,color:#fff style WARN fill:#e65100,color:#fff Цифры Huang et al. Без внешнего верификатора (intrinsic self-correction), качество падает на всех моделях:\nМодель GSM8K (было → стало) CommonSenseQA (было → стало) GPT-3.5 75.9 → 74.7 75.8 → 41.8 GPT-4 95.5 → 89.0 82.0 → 80.0 Llama-2-70b 62.0 → 36.5 64.0 → 36.5 Причины: LLM чаще меняет правильный ответ на неправильный, чем наоборот. Фундаментальная проблема — модель не способна надёжно оценить правильность собственного рассуждения (Huang et al., 2023, Figure 1).\nКогда Reflexion работает Условие Почему Внешний верификатор (тесты, компилятор, env) Объективная оценка → точная рефлексия Низкая начальная точность Есть пространство для улучшения Мульти-триальный сценарий Память накапливает уроки Задачи с объективным критерием успеха Чёткий сигнал об ошибке Когда Reflexion НЕ работает Условие Почему Нет внешнего верификатора Модель не может оценить свою правильность Высокая начальная точность Риск ухудшения (correct → incorrect) Open-ended задачи без критерия Нечего оценивать → неточная рефлексия Простые задачи Cost рефлексии не окупается 7. Варианты реализации на Eino Eino не предоставляет готового reflection.NewAgent() — в отличие от react.NewAgent() из Части 1 или planexecute.NewAgent() из Части 2. Но это и к лучшему: Reflexion — это не отдельный тип агента, а паттерн компоновки, который можно реализовать несколькими способами.\nВариант A: compose.Graph с циклом Eino Graph поддерживает циклы через AddEdge из узла в себя же + WithMaxRunSteps для ограничения итераций. Это наиболее естественная реализация Reflexion:\nflowchart LR START((\"START\")) --\u003e Actor[\"🎬 Actor(react.Agent)\"] Actor --\u003e Evaluator[\"🔍 Evaluator(Lambda: run tests)\"] Evaluator --\u003e Branch{\"Pass?\"} Branch --\u003e|Yes| END((\"END\")) Branch --\u003e|No| Reflector[\"🪞 Reflector(ChatModel)\"] Reflector --\u003e Memory[\"💾 Memory(State: []string)\"] Memory --\u003e Actor style START fill:#2e7d32,color:#fff style END fill:#2e7d32,color:#fff style Branch fill:#e65100,color:#fff style Actor fill:#1565c0,color:#fff style Evaluator fill:#1565c0,color:#fff style Reflector fill:#6a1b9a,color:#fff style Memory fill:#ad1457,color:#fff Плюсы: явный цикл retry, поддержка ветвления, checkpoint через WithCheckPointStore, можно встроить ReAct-агента через ExportGraph().\nМинусы: Graph API использует неявную передачу данных (весь output → весь input), нужно аккуратно управлять state.\nВариант B: compose.Workflow (линейный, без цикла) Workflow — декларативный граф с явным маппингом полей. Проблема: Workflow не поддерживает циклы — всегда AllPredecessor. Для Reflexion это фатально: нет retry-loop.\nНо если цикл реализовать снаружи (в Go-коде), а Workflow использовать для одной итерации Actor → Evaluator → Reflector — это работает:\n// Внешний retry-loop for attempt := 0; attempt \u0026lt; maxAttempts; attempt++ { result, err := workflow.Invoke(ctx, input) if result.Passed { break } memory = append(memory, result.Reflection) // Инжектируем память в следующий вызов input.Reflections = memory } Плюсы: явный field mapping, типобезопасность, проще тестировать одну итерацию.\nМинусы: нет встроенного цикла — пришлось реализовывать вручную, нет checkpoint между итерациями.\nВариант C: deer-go паттерн (State Graph) deer-go — Go-реализация ByteDance DEER-flow на Eino Graph. Использует Goto-маршрутизацию: каждый узел записывает следующий целевой узел в state.Goto, а agentHandOff-функция направляет выполнение.\nДля Reflexion можно добавить Critic-узел и ребро Critic → Actor (retry). Это расширяет существующую архитектуру, но deer-go не содержит готовых паттернов рефлексии — только re-planning loop.\nПлюсы: готовая архитектура state graph, checkpoint, human-in-the-loop через InterruptAndRerun.\nМинусы: сложнее, требует HTTP-сервер и MCP tools, избыточно для простых сценариев.\nСравнение вариантов Аспект Graph + цикл Workflow + внешний loop deer-go Цикл retry Встроенный Внешний (Go-код) Через Goto Checkpoint ✅ WithCheckPointStore ❌ Вручную ✅ Встроенный Сложность Средняя Низкая Высокая Field mapping Неявный Явный Через State Готовность к prod Высокая Средняя Высокая Мой выбор для примера: compose.Graph — нативная поддержка циклов, checkpoint, и прямое встраивание react.Agent через ExportGraph().\n8. Код-пример Реализую Reflexion на compose.Graph: Actor (ReAct-агент) генерирует Go-код, Evaluator запускает тесты, Reflector анализирует ошибки, Memory накапливает рефлексии.\nflowchart TD START((\"START\")) --\u003e Actor[\"🎬 Actorreact.Agent+ write_code tool\"] Actor --\u003e Evaluator[\"🔍 EvaluatorLambda: go test\"] Evaluator --\u003e Branch{\"Tests pass?\"} Branch --\u003e|Yes| END((\"END ✅\")) Branch --\u003e|No| Reflector[\"🪞 ReflectorChatModel: analyzetest failures\"] Reflector --\u003e Memory[\"💾 Append reflectionto episodic memory\"] Memory --\u003e Actor style START fill:#2e7d32,color:#fff style END fill:#2e7d32,color:#fff style Branch fill:#e65100,color:#fff package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/cloudwego/eino/compose\u0026#34; \u0026#34;github.com/cloudwego/eino/components/tool\u0026#34; \u0026#34;github.com/cloudwego/eino/components/tool/utils\u0026#34; \u0026#34;github.com/cloudwego/eino-ext/components/model/openai\u0026#34; \u0026#34;github.com/cloudwego/eino/flow/agent/react\u0026#34; \u0026#34;github.com/cloudwego/eino/schema\u0026#34; ) // reflexionState — разделяемое состояние для одного выполнения Reflexion. // Создаётся заново при каждом Invoke. type reflexionState struct { // Task — исходная задача (например, \u0026#34;write a function that sorts a list\u0026#34;) Task string // Reflections — накопленные вербальные рефлексии из прошлых попыток Reflections []string // Attempt — текущий номер попытки (1-based) Attempt int // MaxAttempts — максимальное число попыток MaxAttempts int // Code — сгенерированный код (выход Actor) Code string // TestResult — результат запуска тестов (выход Evaluator) TestResult string // Passed — флаг: тесты пройдены? Passed bool } // writeCodeTool — инструмент для Actor: \u0026#34;записывает\u0026#34; код в файл. // В реальном приложении здесь будет запись на диск. type writeCodeTool struct{} func (t *writeCodeTool) Info(ctx context.Context) (*schema.ToolInfo, error) { return \u0026amp;schema.ToolInfo{ Name: \u0026#34;write_code\u0026#34;, Desc: \u0026#34;Write Go code to solve the task. The code will be tested automatically.\u0026#34;, }, nil } func (t *writeCodeTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) { // В реальном приложении: записать args в .go файл return fmt.Sprintf(\u0026#34;Code written (%d bytes)\u0026#34;, len(args)), nil } func main() { ctx := context.Background() // 1. Создаём модель для Actor и Reflector chatModel, err := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ Model: \u0026#34;gpt-4o\u0026#34;, }) if err != nil { panic(err) } // 2. Создаём ReAct-агента как Actor // ExportGraph() позволяет встроить его в compose.Graph codeTool := utils.NewTool(\u0026amp;writeCodeTool{}, nil) actor, err := react.NewAgent(ctx, \u0026amp;react.AgentConfig{ ToolCallingModel: chatModel, ToolsConfig: compose.ToolsNodeConfig{ Tools: []tool.BaseTool{codeTool}, }, MaxStep: 5, }) if err != nil { panic(err) } // 3. Строим Reflexion Graph g := compose.NewGraph[string, string]( compose.WithGenLocalState(func(ctx context.Context) *reflexionState { return \u0026amp;reflexionState{ MaxAttempts: 3, } }), compose.WithMaxRunSteps(20), // ограничение на циклы ) // Actor: встроим ReAct-агента через ExportGraph actorGraph, actorOpts := actor.ExportGraph() g.AddGraphNode(\u0026#34;actor\u0026#34;, actorGraph, actorOpts...) g.AddEdge(compose.START, \u0026#34;actor\u0026#34;) // Evaluator: Lambda-узел, который \u0026#34;запускает тесты\u0026#34; g.AddLambdaNode(\u0026#34;evaluator\u0026#34;, compose.InvokableLambda(func(ctx context.Context, code string) (string, error) { // В реальном приложении: exec.Command(\u0026#34;go\u0026#34;, \u0026#34;test\u0026#34;, \u0026#34;./...\u0026#34;) // Здесь: симуляция if len(code) \u0026gt; 10 { return \u0026#34;PASS: all tests passed\u0026#34;, nil } return \u0026#34;FAIL: TestSortEmpty - expected [], got nil\u0026#34;, nil }), ) g.AddEdge(\u0026#34;actor\u0026#34;, \u0026#34;evaluator\u0026#34;) // Reflector: ChatModel анализирует ошибки g.AddChatModelNode(\u0026#34;reflector\u0026#34;, chatModel) g.AddEdge(\u0026#34;evaluator\u0026#34;, \u0026#34;reflector\u0026#34;) // Conditional Branch: pass → END, fail → back to Actor g.AddBranch(\u0026#34;evaluator\u0026#34;, compose.NewGraphBranch( func(ctx context.Context, testResult string) (string, error) { // Читаем state для проверки числа попыток if err := compose.ProcessState[*reflexionState](ctx, func(ctx context.Context, s *reflexionState) error { s.Attempt++ s.TestResult = testResult // Простой эвристический критерий s.Passed = len(testResult) \u0026gt; 4 \u0026amp;\u0026amp; testResult[:4] == \u0026#34;PASS\u0026#34; }, ); err != nil { return \u0026#34;\u0026#34;, err } var target string _ = compose.ProcessState[*reflexionState](ctx, func(ctx context.Context, s *reflexionState) error { if s.Passed || s.Attempt \u0026gt;= s.MaxAttempts { target = compose.END } else { target = \u0026#34;reflector\u0026#34; // сначала рефлексируем, потом retry } return nil }, ) return target, nil }, map[string]bool{compose.END: true, \u0026#34;reflector\u0026#34;: true}, )) // Reflector → Actor: retry с рефлексией в контексте g.AddEdge(\u0026#34;reflector\u0026#34;, \u0026#34;actor\u0026#34;) // END g.AddEdge(compose.END, compose.END) // 4. Компилируем и запускаем runnable, err := g.Compile(ctx) if err != nil { panic(fmt.Sprintf(\u0026#34;compile error: %v\u0026#34;, err)) } result, err := runnable.Invoke(ctx, \u0026#34;Write a function that sorts a slice of integers\u0026#34;) if err != nil { panic(fmt.Sprintf(\u0026#34;invoke error: %v\u0026#34;, err)) } fmt.Println(\u0026#34;Result:\u0026#34;, result) } Что здесь происходит Состояние — reflexionState хранит текущую попытку, рефлексии и результат оценки. Создаётся через WithGenLocalState при каждом запуске графа.\nActor — встроен через ExportGraph(). ReAct-агент с инструментом write_code. При повторном заходе (после рефлексии) получает обновлённый контекст.\nEvaluator — Lambda-узел, который «запускает тесты». В реальном приложении — exec.Command(\u0026quot;go\u0026quot;, \u0026quot;test\u0026quot;). В примере — симуляция: если код длиннее 10 байт — PASS.\nReflector — ChatModel, анализирующая ошибки. Получает результат тестов и генерирует вербальную рефлексию.\nBranch — условное ветвление после Evaluator: PASS → END, FAIL → Reflector → Actor (retry). Проверяет Attempt \u0026lt; MaxAttempts.\nЦикл — g.AddEdge(\u0026quot;reflector\u0026quot;, \u0026quot;actor\u0026quot;) замыкает петлю. WithMaxRunSteps(20) ограничивает общее число шагов (защита от бесконечного цикла).\n9. Инженерный сценарий Кодогенерация с тестами — идеальный сценарий для Reflexion. Почему? Потому что есть объективный внешний верификатор: компилятор и unit-тесты. Это не субъективная LLM-оценка, а бинарный PASS/FAIL.\nTDD для агента sequenceDiagram participant Dev as Developer participant A as Actor Agent participant T as go test participant R as Reflector participant M as Memory Dev-\u003e\u003eA: \"Write sort([]int)\" Note over A: Attempt 1 A-\u003e\u003eT: sort.go T--\u003e\u003eA: ❌ FAIL: TestSortEmptyexpected [], got nil A-\u003e\u003eR: Trajectory + test failure R-\u003e\u003eR: \"Forgot to handleempty slice\" R-\u003e\u003eM: Store reflection #1 Note over A: Attempt 2 (with reflection) M--\u003e\u003eA: \"Check empty slice first\" A-\u003e\u003eT: sort_v2.go T--\u003e\u003eA: ❌ FAIL: TestSortStableunstable sort on equal elements A-\u003e\u003eR: Trajectory + test failure R-\u003e\u003eR: \"Used unstable sort,need stable\" R-\u003e\u003eM: Store reflection #2 Note over A: Attempt 3 (with 2 reflections) M--\u003e\u003eA: \"Check empty slice firstUse stable sort\" A-\u003e\u003eT: sort_v3.go T--\u003e\u003eA: ✅ PASS: all tests passed A--\u003e\u003eDev: sort_v3.go ✅ Почему это работает лучше Self-Refine Self-Refine в том же сценарии дал бы 0% improvement на Math Reasoning (Madaan et al., 2023). Почему? Без тестов модель не отличает sort([]int{}) от sort([]int{1}) — ей не хватает объективного сигнала. Reflexion с go test как Evaluator решает эту проблему: тесты дают точную диагностику ошибки → рефлексия фокусируется на реальной проблеме → Actor исправляет именно то, что нужно.\nProduction-реализация В production сценарий расширяется:\nКомпонент Пример В production Actor react.NewAgent с write_code + read_file, search_docs, lint_code Evaluator go test ./... + go vet, golangci-lint, coverage ≥ 80% Reflector ChatModel «analyze failures» Промпт с конкретными паттернами ошибок Memory []string в state Redis / файл с reflection history Max attempts 3 5 (HumanEval: 91% достигнуто за 12 попыток, но 3-5 обычно достаточно) 10. Практические рекомендации Когда применять Reflexion Сценарий Применимость Обоснование Кодогенерация + тесты ✅ Отлично Объективный верификатор (компилятор/тесты) Game-агенты ✅ Отлично Среда даёт чёткий reward signal Data pipeline с валидацией ✅ Хорошо Schema validation как верификатор Code review автоматизация ⚠️ Осторожно LLM-оценка менее надёжна, чем тесты Креативное письмо ⚠️ Осторожно Нет объективного критерия успеха Математика / рассуждение ❌ Не рекомендуется Без внешнего верификатора — ухудшает результат Настройка гиперпараметров Max attempts: 3-5 для задач с быстрым верификатором (тесты). HumanEval достиг 91% за 12 попыток, но diminishing returns начинается после 3-5. Больше — дороже, но не лучше.\nEpisodic memory size: храните последние 5-10 рефлексий. Слишком много — контекст разрастается, модель теряет фокус. Слишком мало — не учитывает давние ошибки.\nEvaluator choice: детерминированный верификатор (тесты, компилятор) всегда лучше LLM-based. Если верификатор ненадёжный — Reflexion превращается в Self-Refine с его проблемами.\nReflector prompt: конкретный \u0026gt; абстрактный. Не «проанализируй ошибку», а «идентифицируй: (1) какой тест упал, (2) какой вход вызвал падение, (3) какое предположение было неверным, (4) что изменить в коде».\nCost management Каждая попытка Reflexion = полный цикл Actor + Evaluator + Reflector. При 3 попытках — 3x cost. Mitigation:\nИспользовать дешёвую модель для Evaluator (детерминированная проверка не требует GPT-4) Рано останавливаться: если 2 попытки не помогли — третья вряд ли поможет Кешировать рефлексии для похожих задач 11. Итоги Reflexion-паттерн — это ReAct + самооценка + эпизодическая память. Ключевые выводы:\nВнешний верификатор — обязателен. Без него self-correction ухудшает результат (Huang et al., 2023). С ним — Reflexion даёт +11% на HumanEval и +22% на AlfWorld.\nЭпизодическая память — ключевое отличие от Self-Refine. Не просто critique-refine, а накопление вербальных уроков между попытками. Это превращает одноразового агента в обучающегося.\nНе панацея. Reflexion не работает на задачах без объективного критерия успеха и может ухудшить результат при высокой начальной точности.\nВ Eino — compose.Graph с циклом. Workflow не подходит (нет циклов). Graph + AddEdge(\u0026quot;reflector\u0026quot;, \u0026quot;actor\u0026quot;) + WithMaxRunSteps — естественная реализация.\nЧто дальше? В Части 4 — Multi-Agent паттерны: когда один агент не справляется, и нужна команда. А тему памяти агентов — долгосрочной, эпизодической, семантической — я подробно разберу в Части 6.\nReflexion: Shinn, N., Cassano, F., Labash, A., Gopinath, A., Narasimhan, K., \u0026amp; Yao, S. (2023). Reflexion: Language Agents with Verbal Reinforcement Learning. NeurIPS 2023. Self-Refine: Madaan, A., Tandon, N., Gupta, P., Hallinan, S., Gao, L., Wiegreffe, S., \u0026hellip; \u0026amp; Yang, D. (2023). Self-Refine: Iterative Refinement with Self-Feedback. NeurIPS 2023. Constitutional AI: Bai, Y., Kadavath, S., Kundu, S., Askell, A., Kernion, J., Jones, A., \u0026hellip; \u0026amp; Kaplan, J. (2022). Constitutional AI: Harmlessness from AI Feedback. Anthropic. Huang et al.: Huang, J., Chen, X., Mishra, S., Zheng, H. S., Yu, A. W., Song, Q., \u0026amp; Zhou, D. (2023). Large Language Models Cannot Self-Correct Reasoning Yet. ICLR 2024. Reflect, Retry, Reward: Bensal, Y., Kim, S., Bosselut, A., \u0026amp; Guha, N. (2025). Reflect, Retry, Reward: Training LLM Agents to Reflect and Retry with Reward-Guided Self-Reflection. Eino: CloudWeGo. Eino: The ultimate LLM/AI application development framework in Go. deer-go: CloudWeGo. DEER-flow Go implementation. ","permalink":"https://triumphpc.github.io/blog/ru/posts/ai-agent-design-patterns-3-reflexion/","summary":"Reflexion — паттерн, в котором агент учится на собственных ошибках через вербальное подкрепление и эпизодическую память. Разбираю эволюцию self-critique от Constitutional AI до Reflexion, честно показываю ограничения (без внешнего верификатора не работает), и реализую рабочий пример на Go через Eino compose.Graph с циклом Actor → Evaluator → Reflector → retry.","title":"Шаблоны проектирования AI-агентов. Часть 3: Reflexion Pattern"},{"content":"1. Введение Я начинаю цикл о шаблонах проектирования AI-агентов. Цель — референс с оригинальными research papers, рабочим кодом на Go и честным анализом ограничений. Каждое утверждение — с отсылкой к первоисточнику, никаких пересказов. Аудитория — опытные инженеры, которым не нужны объяснения основ.\nReAct Pattern (Yao et al., 2022) — фундамент, на котором строятся Reflexion, Plan-and-Execute, ReWOO и все агентные архитектуры. Без него остальные паттерны не складываются.\n2. Что такое ReAct Pattern История В октябре 2022 года команда из Princeton University и Google Brain — Shunyu Yao, Jeffrey Zhao, Dian Yu, Nan Du, Izhak Shafran, Karthik Narasimhan, Yuan Cao — опубликовала работу «ReAct: Synergizing Reasoning and Acting in Language Models». Проектная страница: react-lm.github.io.\nИдея проста: LLM (Large Language Model) должен не только рассуждать (как в Chain-of-Thought), и не только действовать (как в tool-use агентах), но и чередовать рассуждение и действие, формируя замкнутый цикл обратной связи.\nАналогия с человеческим мышлением Когда вы решаете сложную задачу — скажем, планируете маршрут в незнакомом городе — вы не строите весь план в голове, а потом действуете. Вы: смотрите карту → думаете → идёте → видите знак → корректируете маршрут → идёте дальше. Именно этот паттерн — interleaved Reasoning + Acting — и формализует ReAct.\nКонтраст с предшественниками Подход Рассуждение Действие Grounding Self-correction CoT (Chain-of-Thought, Wei et al., 2022) Да Нет Нет Нет Act-only Нет Да Да Нет ReAct Да Да Да Да CoT (Chain-of-Thought) генерирует цепочку рассуждений, но не может проверить факты. Act-only вызывает инструменты, но не рефлексирует над результатами. ReAct объединяет оба мира.\nФормальное определение ReAct-агент работает в цикле:\ngraph LR Q((Question)) --\u003e T1[Thought] T1 --\u003e A1[Action] A1 --\u003e O1[Observation] O1 --\u003e T2[Thought] T2 --\u003e A2[Action] A2 --\u003e O2[Observation] O2 --\u003e TN[...] TN --\u003e Ans((Answer)) Пример трассы (задача: «Какая погода в Париже?»):\nThought: Нужно узнать текущую погоду в Париже Action: get_weather(city=\u0026#34;Paris\u0026#34;) Observation: 18°C, ясно, влажность 45% Thought: Данные получены, могу ответить Answer: В Париже сейчас 18°C, ясно, влажность 45% Каждый Thought — рассуждение модели о том, что делать дальше. Каждый Action — вызов инструмента. Каждая Observation — результат, который модель учитывает в следующем шаге.\n3. Какую задачу решает Проблема CoT: hallucination Chain-of-Thought (Wei et al., 2022) впечатляет на задачах, где ответ можно вывести из контекста. Но как только нужны внешние факты — модель галлюцинирует. Классический пример: CoT уверенно генерирует «президент Непала — Хари Бахадур Баснет» — имя звучит правдоподобно, но фактически неверно. Нет grounding — нет гарантии.\nПроблема Act-only: нет рефлексии Агент, который только действует, может вызвать инструмент, получить результат, но не способен синтезировать ответ. Он не «думает» над наблюдением — просто передаёт его дальше. Это работает для простых запросов, но ломается на многошаговых задачах.\nПроблема error propagation В длинных цепочках CoT ошибка на раннем шаге не замечается и каскадно размывается, делая финальный ответ бессмысленным. Без возможности перепроверить себя через внешнюю среду модель не может корректировать курс.\nЧто обеспечивает ReAct Проблема Механизм ReAct Hallucination Grounding через инструментальные вызовы (Observation = факт) Отсутствие рефлексии Thought после каждого Observation — модель анализирует результат Error propagation Self-correction: модель видит ошибочный результат и корректирует план Непрозрачность Interpretability: каждая Thought — читаемый след рассуждения Жёсткий план Dynamic planning: план пересматривается на каждом шаге Цифры из Yao et al., 2022 Ключевые результаты из оригинальной работы:\nHotpotQA + FEVER: ReAct преодолевает hallucination CoT через Wikipedia API — модель получает факты, а не выдумывает их (Yao et al., 2022, §4.1). ALFWorld: +34% success rate над imitation learning — в среде текстового взаимодействия с домом ReAct значительно превосходит базовый подход (Yao et al., 2022, §4.2). WebShop: +10% success rate над RL (Reinforcement Learning) — даже в сложной задаче онлайн-покупок ReAct демонстрирует преимущество (Yao et al., 2022, §4.2). Применимость паттерна Важно: паттерн проверен на широком классе моделей и задач. Конкретные числа из Yao et al. получены на PaLM-540B, но сам принцип interleaved reasoning + acting не зависит от конкретной модели — он работает и с GPT-4, и с Claude, и с открытые моделями, поддерживающими tool calling.\n4. Архитектура и диаграммы Цикл Thought → Action → Observation sequenceDiagram participant U as User participant A as ReAct Agent participant T as Tools U-\u003e\u003eA: Question loop ReAct Loop A-\u003e\u003eA: Thought (рассуждение) A-\u003e\u003eT: Action (вызов инструмента) T--\u003e\u003eA: Observation (результат) end A-\u003e\u003eA: Final Thought A--\u003e\u003eU: Answer На каждой итерации агент формирует Thought — внутреннее рассуждение о текущем состоянии и следующем шаге. Затем выполняет Action — вызов одного из доступных инструментов. Полученная Observation добавляется в контекст, и цикл повторяется. State (история сообщений) накапливается: все предыдущие Thoughts, Actions и Observations доступны на следующем шаге.\nState-Graph представление В фреймворках вроде Eino ReAct реализуется как направленный граф (State Graph):\ngraph TB START((START)) --\u003e CM[ChatModel] CM --\u003e|tool_calls| B{Branch} CM --\u003e|no tool_calls| END((END)) B --\u003e|has tool calls| TN[ToolsNode] B --\u003e|no tool calls| END TN --\u003e CM Graph-представление важно по трём причинам:\nState: все сообщения хранятся в едином состоянии графа — не нужно вручную управлять контекстом. Streaming: каждый узел графа может стримить результат — пользователь видит рассуждение агента в реальном времени. Callbacks: на каждый узел можно повесить обработчики — логирование, метрики, трейсинг. Эволюция подходов 2022–2025 graph LR A[\"Prompt-basedReAct (2022)\"] --\u003e B[\"Tool CallingAPI (2023)\"] B --\u003e C[\"AgentFrameworks (2024)\"] C --\u003e D[\"Multi-AgentOrchestration (2025)\"] Prompt-based ReAct (2022): оригинальная реализация — few-shot промпты, инструменты через текстовый интерфейс. Работало, но хрупко и не масштабировалось. Tool Calling API (2023): модели получили нативную поддержку function calling — инструменты стали структурированными и надёжными. Schick et al., 2023 (arXiv:2302.04761) показали, что LLM могут самостоятельно учиться вызывать инструменты. Agent Frameworks (2024): LangGraph, LlamaIndex, Eino — фреймворки, которые инкапсулируют ReAct в переиспользуемые компоненты с графовой архитектурой. Multi-Agent Orchestration (2025): несколько ReAct-агентов координируются для решения сложных задач — каждый специализируется на своей области. 5. Состояние рынка: фреймворки ReAct — универсальный паттерн, реализованный во всех major-фреймворках. Сводная таблица:\nФреймворк Язык Реализация Ключевая особенность LangGraph Python create_react_agent Де-факто стандарт Python-мира, графовая модель LlamaIndex Python ReActAgent Глубокая интеграция с RAG (Retrieval-Augmented Generation) и индексами OpenAI Agents SDK (Software Development Kit) Python Agent + tools Нативная интеграция с GPT-4/GPT-4o Anthropic Claude API Python/TS Tool use + system prompt Максимум рассуждения через extended thinking Google ADK Python Agent + tools Интеграция с Gemini и Google Cloud Eino Go react.NewAgent Go-нативный, production-tested в ByteDance LangGraph — де-факто стандарт LangGraph стал стандартом Python-мира для агентных систем. Функция create_react_agent создаёт готового ReAct-агента из модели и списка инструментов за несколько строк. Графовая архитектура позволяет добавлять условные переходы, циклы и человеко-в-контуре (human-in-the-loop). Если вы в Python — это первый кандидат.\nEino — почему Go Eino — фреймворк от CloudWeGo (ByteDance), production-tested в Doubao, TikTok и Coze. Выбран для практической секции по трём причинам:\nGo-нативный: типизированные инструменты, интерфейсы, отсутствие interface{}-ада. Production-tested: обслуживает сотни миллионов запросов в день внутри ByteDance. Графовая архитектура: compose.Graph под капотом ReAct-агента — та же модель, что в LangGraph, но на Go. Python-примеры кода в этой статье не приводятся — это намеренно. Фокус блога — Go.\n6. Практика: ReAct на Eino, Go Установка go get github.com/cloudwego/eino@latest go get github.com/cloudwego/eino-ext/components/model/openai@latest Документация API: pkg.go.dev/github.com/cloudwego/eino\nМинимальный ReAct-агент package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/cloudwego/eino-ext/components/model/openai\u0026#34; \u0026#34;github.com/cloudwego/eino/components/tool\u0026#34; \u0026#34;github.com/cloudwego/eino/compose\u0026#34; \u0026#34;github.com/cloudwego/eino/flow/agent/react\u0026#34; \u0026#34;github.com/cloudwego/eino/schema\u0026#34; ) func main() { ctx := context.Background() // Модель с поддержкой tool calling chatModel, _ := openai.NewChatModel(ctx, \u0026amp;openai.ChatModelConfig{ Model: \u0026#34;gpt-4o\u0026#34;, }) // Создаём агента с одним инструментом agent, _ := react.NewAgent(ctx, \u0026amp;react.AgentConfig{ ToolCallingModel: chatModel, ToolsConfig: compose.ToolsNodeConfig{ InvokableTools: []tool.InvokableTool{weatherTool()}, }, }) // Вызов агента msg, _ := agent.Generate(ctx, []*schema.Message{ schema.UserMessage(\u0026#34;Какая погода в Париже?\u0026#34;), }) fmt.Println(msg.Content) } Типизированный инструмент через utils.NewTool type WeatherRequest struct { City string `json:\u0026#34;city\u0026#34; jsonschema:\u0026#34;description=Город для запроса погоды\u0026#34;` } type WeatherResponse struct { City string `json:\u0026#34;city\u0026#34;` Temperature int `json:\u0026#34;temperature\u0026#34;` Condition string `json:\u0026#34;condition\u0026#34;` } func weatherTool() tool.InvokableTool { return utils.NewTool( \u0026amp;schema.ToolInfo{ Name: \u0026#34;get_weather\u0026#34;, Desc: \u0026#34;Получить текущую погоду в указанном городе\u0026#34;, ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ \u0026#34;city\u0026#34;: {Type: \u0026#34;string\u0026#34;, Desc: \u0026#34;Город\u0026#34;, Required: true}, }), }, func(ctx context.Context, input *WeatherRequest) (*WeatherResponse, error) { // Здесь — реальный вызов API погоды return \u0026amp;WeatherResponse{ City: input.City, Temperature: 18, Condition: \u0026#34;ясно\u0026#34;, }, nil }, ) } Ключевые параметры (без кода) ToolCallingModel — модель обязана поддерживать tool calling (интерфейс ToolCallingChatModel). ToolsConfig — конфигурация узла инструментов: InvokableTools и StreamableTools. MaxStep — лимит шагов графа (default 12 = до 6 полных циклов ChatModel + Tools). MessageModifier — функция для модификации сообщений перед вызовом модели (например, добавление system prompt). ToolReturnDirectly — инструменты, результат которых возвращается напрямую пользователю, минуя следующий вызов модели. Ссылки для углубления ReAct Agent Manual — полное описание всех параметров eino-examples/flow/agent/react — полный рабочий пример (Food Recommender demo) GitHub: cloudwego/eino — исходники 7. Когда ReAct НЕ подходит ReAct — не серебряная пуля. Каждый цикл Thought→Action→Observation — это отдельный LLM-вызов, а значит: стоимость растёт линейно с числом шагов, latency накапливается, а горизонт планирования ограничен контекстным окном модели.\nИерархия сложности Microsoft Azure Architecture Center в руководстве по оркестрации AI-агентов формулирует принцип: используй минимальный уровень сложности, который решает задачу.\nУровень Описание Когда достаточно Direct model call Один вызов LLM, без инструментов Классификация, суммаризация, перевод Single agent + tools (ReAct) Агент с рассуждением и инструментами Динамический выбор инструментов в рамках одного домена Multi-agent orchestration Несколько специализированных агентов Кросс-доменные задачи, разные security boundaries ReAct занимает средний уровень. Если задача решается прямым вызовом модели — не нужен агент. Если один агент не справляется из-за сложности промпта, перегрузки инструментами или требований безопасности — переходи к multi-agent. Но не раньше.\nЕсли ваша задача\u0026hellip; \u0026hellip;рассмотрите альтернативу Фиксированный workflow с известными шагами Plan-and-Solve — планирование без итеративного поиска Чувствительна к cost (много LLM-вызовов) ReWOO — план без interleaved вызовов модели (Xu et al., 2023) Требует учёта прошлых ошибок Reflexion (он же maker-checker, evaluator-optimizer) — ReAct + самооценка (Shinn et al., 2023) Нуждается в дереве гипотез Tree of Thoughts — ветвящееся рассуждение (Yao et al., 2023) Длинный горизонт планирования Plan-and-Execute — декомпозиция на подзадачи Подробное сравнение паттернов — в Части 5 этого цикла.\n8. Что дальше в цикле Часть 2: Plan-and-Execute Pattern — декомпозиция задачи на подзадачи с отдельным планировщиком и исполнителем. Часть 3: Reflexion Pattern — ReAct + самооценка: агент учится на собственных ошибках. Часть 4: ReWOO Pattern — планирование без interleaved вызовов модели: дешевле, быстрее, но без динамической корректировки. Часть 5: Сравнение паттернов + Multi-Agent Orchestration — когда какой паттерн выбирать, и как несколько агентов координируются для решения сложных задач. Обсуждение приветствуется — комментарии на сайте или GitHub Issues.\n9. Ссылки Исследовательские статьи Yao et al., 2022 — ReAct: Синергия рассуждения и действия в языковых моделях. Формализация цикла Thought→Action→Observation. arXiv:2210.03629 Wei et al., 2022 — Chain-of-Thought: Пошаговое рассуждение без взаимодействия с внешней средой. Предшественник ReAct. arXiv:2201.11903 Schick et al., 2023 — Toolformer: LLM обучаются самостоятельно вызывать инструменты. Мост между prompt-based и API-based подходами. arXiv:2302.04761 Shinn et al., 2023 — Reflexion: Расширение ReAct с самооценкой и вербальным подкреплением. arXiv:2303.11366 Yao et al., 2023 — Tree of Thoughts: Обобщение CoT на дерево гипотез с поиском. arXiv:2305.10601 Xu et al., 2023 — ReWOO: Планирование без interleaved вызовов модели — показывает ограничения ReAct по cost/latency. arXiv:2305.18323 Документация и примеры Eino ReAct Agent Manual — все параметры и конфигурация Eino Open Source Announcement — обзор фреймворка GitHub: cloudwego/eino — исходники GitHub: eino-examples/flow/agent/react — полный рабочий пример (Food Recommender) LangGraph ReAct Agent template — реализация на Python React Project Page — проектная страница оригинальной работы Архитектурные руководства AI agent design patterns — Microsoft Azure Architecture Center — иерархия сложности (direct call → single agent → multi-agent), паттерны оркестрации, production-рекомендации по reliability, security, cost optimization ","permalink":"https://triumphpc.github.io/blog/ru/posts/ai-agent-design-patterns-1-react/","summary":"ReAct (Reasoning + Acting) — фундаментальный паттерн проектирования AI-агентов, объединяющий рассуждение и действие в едином цикле. Разбираем архитектуру, цифры из Yao et al. 2022, обзор фреймворков и минимальный рабочий код на Go через Eino.","title":"Шаблоны проектирования AI-агентов. Часть 1: ReAct Pattern"},{"content":"Почему Hugo? После оценки кастомных SSG-вариантов и Go-генераторов, я выбрал Hugo по трём причинам:\nКритерий Hugo Кастомный SSG Likho Скорость сборки ~40ms Н/Д ~200ms Экосистема тем 400+ Нет Минимальная Возможности Markdown Полные DIY Базовые Поддержка Сообщество Вы Вы Hugo даёт скорость, зрелую экосистему и тему PaperMod, которая предоставляет всё необходимое из коробки.\nОбзор архитектуры Блог работает по простому конвейеру — пишешь Markdown, пушешь в GitHub, и всё остальное автоматизировано:\ngraph LR A[\"✏️ Markdown\"] --\u003e B[\"🔨 Hugo Build\"] B --\u003e C[\"⏱️ GitHub Actions\"] C --\u003e D[\"🚀 GitHub Pages\"] style A fill:#24283b,stroke:#7aa2f7,color:#c0caf5 style B fill:#24283b,stroke:#bb9af7,color:#c0caf5 style C fill:#24283b,stroke:#7dcfff,color:#c0caf5 style D fill:#24283b,stroke:#9ece6a,color:#c0caf5 Основные моменты настройки Hugo Modules вместо Git Submodules hugo mod init github.com/triumumphc/blog В hugo.yaml:\nmodule: imports: - path: github.com/adityatelange/hugo-PaperMod Никаких проблем с git submodule — только Go-модули, версионированные и воспроизводимые.\nТёмная тема PaperMod Тёмная тема по умолчанию с переключателем на светлую. Цветовая палитра вдохновлена Tokyo Night:\npie title Color Palette Distribution \"Primary (#7aa2f7)\" : 35 \"Accent (#bb9af7)\" : 25 \"Success (#9ece6a)\" : 20 \"Warning (#e0af68)\" : 10 \"Error (#f7768e)\" : 10 Кнопки копирования кода Каждый блок кода автоматически получает кнопку копирования. Go-код выглядит так:\nfunc main() { mux := http.NewServeMux() mux.HandleFunc(\u0026#34;/\u0026#34;, handler) srv := \u0026amp;http.Server{ Addr: \u0026#34;:8080\u0026#34;, Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } log.Fatal(srv.ListenAndServe()) } Диаграммы Mermaid Диаграммы рендерятся на клиенте через CDN, загружаются только при использовании шорткода mermaid:\nsequenceDiagram participant W as Автор participant G as Git participant CI as GitHub Actions participant P as GitHub Pages W-\u003e\u003eG: git push G-\u003e\u003eCI: webhook trigger CI-\u003e\u003eCI: hugo --minify CI-\u003e\u003eP: deploy artifact P--\u003e\u003eW: сайт доступен ✅ Архитектурный стиль C4:\ngraph TB subgraph \"Написание\" MD[Markdown-файлы] IMG[Изображения и ассеты] end subgraph \"Сборочный конвейер\" HUGO[Hugo v0.162] CSS[Кастомный CSS] SC[Шорткоды] end subgraph \"Хостинг\" GHA[GitHub Actions] GHP[GitHub Pages] end MD --\u003e HUGO IMG --\u003e HUGO CSS --\u003e HUGO SC --\u003e HUGO HUGO --\u003e GHA GHA --\u003e GHP Математика с KaTeX KaTeX рендерит математику на клиенте, загружается только при наличии шорткода katex.\nТождество Эйлера Самое красивое уравнение в математике:\n$$ e^{i\\pi} + 1 = 0 $$Интеграл Гаусса $$ \\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi} $$Нормальное распределение Функция плотности вероятности нормального распределения:\n$$ f(x) = \\frac{1}{\\sigma\\sqrt{2\\pi}} e^{-\\frac{1}{2}\\left(\\frac{x-\\mu}{\\sigma}\\right)^2} $$Строчная математика тоже работает: среднее — $\\mu$, стандартное отклонение — $\\sigma$.\nОперации с матрицами $$ A = \\begin{pmatrix} a_{11} \u0026 a_{12} \\\\ a_{21} \u0026 a_{22} \\end{pmatrix}, \\quad \\det(A) = a_{11}a_{22} - a_{12}a_{21} $$Справочник шорткодов Шорткод Назначение Пример {{\u0026lt; mermaid \u0026gt;}} Диаграммы Mermaid Блок-схемы, последовательности, C4 $$ ... $$ Блочная математика Центрированные уравнения $ ... $ Строчная математика $\\mu$ отображается как μ Кастомные стили Тёмная тема расширяет PaperMod следующими элементами:\nБлоки кода — кастомный фон + рамка с закруглёнными углами Ссылки — тонкое подчёркивание при наведении Цитаты — акцентная левая рамка с фоном surface Таблицы — с рамками и строкой заголовка surface CI/CD-конвейер GitHub Actions обрабатывает всё автоматически:\nname: Deploy Hugo Blog on: push: branches: [main] Конвейер:\nУстанавливает Hugo Extended Клонирует репозиторий Кэширует Hugo Modules Собирает с hugo --minify Деплоит на GitHub Pages Что дальше Паттерны конкурентности Go — горутины, каналы, errgroup Интеграция с LLM — RAG-пайплайны, промпт-инжиниринг Архитектура AI-агентов — агентная оркестрация, использование инструментов Следите за обновлениями!\n","permalink":"https://triumphpc.github.io/blog/ru/posts/getting-started/","summary":"Как родился этот блог — Hugo, тема PaperMod, диаграммы Mermaid, математика KaTeX, кастомная тёмная тема и полностью автоматизированный CI/CD через GitHub Actions.","title":"Начало работы с Hugo + PaperMod"},{"content":"Привет! Я Sebastian PE — фулстек-разработчик с 15+ годами опыта в создании веб-продуктов, бэкенд-систем и инструментов для разработчиков.\nЧем я занимаюсь Работаю на всех этапах жизненного цикла продукта — от архитектуры и прототипирования до запуска и эксплуатации. Мой текущий фокус:\nGo — высокопроизводительные бэкенд-сервисы, конкурентные системы, микросервисы AI \u0026amp; LLM-агенты — промпт-инжиниринг, RAG-пайплайны, агентная оркестрация PHP \u0026amp; JavaScript — фулстек-разработка, проектирование API DevOps — CI/CD, наблюдаемость, Infrastructure as Code Я создавал e-commerce платформы, веб-порталы, CRM- и ERP-системы, бизнес-инструменты. Также имею обширный опыт тимлида и руководителя разработки — управлял инженерными командами и проектировал программную архитектуру.\nПреподавание и сообщество Делюсь знаниями через курсы и контент:\nUdemy — Паттерны проектирования, Domain-Driven Design для джунов YouTube — канал (архитектура, методологии, инструменты разработки) Habr — публикации Контакты GitHub: triumphpc Udemy: профиль инструктора Этот блог построен на Hugo с темой PaperMod.\n","permalink":"https://triumphpc.github.io/blog/ru/pages/about/","summary":"\u003cp\u003eПривет! Я \u003cstrong\u003eSebastian PE\u003c/strong\u003e — фулстек-разработчик с \u003cstrong\u003e15+ годами\u003c/strong\u003e опыта в создании веб-продуктов, бэкенд-систем и инструментов для разработчиков.\u003c/p\u003e\n\u003ch2 id=\"чем-я-занимаюсь\"\u003eЧем я занимаюсь\u003c/h2\u003e\n\u003cp\u003eРаботаю на всех этапах жизненного цикла продукта — от архитектуры и прототипирования до запуска и эксплуатации. Мой текущий фокус:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGo\u003c/strong\u003e — высокопроизводительные бэкенд-сервисы, конкурентные системы, микросервисы\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAI \u0026amp; LLM-агенты\u003c/strong\u003e — промпт-инжиниринг, RAG-пайплайны, агентная оркестрация\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePHP \u0026amp; JavaScript\u003c/strong\u003e — фулстек-разработка, проектирование API\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDevOps\u003c/strong\u003e — CI/CD, наблюдаемость, Infrastructure as Code\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eЯ создавал e-commerce платформы, веб-порталы, CRM- и ERP-системы, бизнес-инструменты. Также имею обширный опыт тимлида и руководителя разработки — управлял инженерными командами и проектировал программную архитектуру.\u003c/p\u003e","title":"Обо мне"},{"content":"Open Source Проект Описание blog Этот блог — Hugo + PaperMod, автодеплой через GitHub Actions Преподавание Курсы на Udemy по программной инженерии — профиль\n","permalink":"https://triumphpc.github.io/blog/ru/pages/projects/","summary":"\u003ch2 id=\"open-source\"\u003eOpen Source\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eПроект\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eОписание\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://github.com/triumphpc/blog\"\u003eblog\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eЭтот блог — Hugo + PaperMod, автодеплой через GitHub Actions\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"преподавание\"\u003eПреподавание\u003c/h2\u003e\n\u003cp\u003eКурсы на Udemy по программной инженерии — \u003ca href=\"https://www.udemy.com/user/sergei-1146/\"\u003eпрофиль\u003c/a\u003e\u003c/p\u003e","title":"Проекты"}]