[{"data":1,"prerenderedAt":6010},["ShallowReactive",2],{"articles":3},[4,1801,2454,4653],{"id":5,"title":6,"body":7,"date":1784,"description":1785,"extension":1786,"meta":1787,"navigation":327,"path":1788,"readingTime":1789,"seo":1790,"stem":1791,"tags":1792,"__hash__":1800},"articles\u002Fblog\u002Fmulti-agent-frameworks-comparison.md","Multi-agent фреймворки: кто кого оркестрирует",{"type":8,"value":9,"toc":1759},"minimark",[10,15,19,22,25,29,154,157,161,164,169,172,179,185,191,195,198,206,209,213,216,219,223,226,229,232,236,239,242,246,249,252,256,259,263,267,270,273,276,279,282,515,526,532,535,538,541,810,813,816,821,824,827,830,1190,1193,1196,1199,1204,1206,1209,1212,1215,1220,1223,1226,1229,1232,1235,1240,1243,1246,1249,1253,1256,1259,1262,1265,1491,1494,1498,1696,1700,1706,1712,1718,1724,1730,1736,1740,1743,1746,1749,1752,1755],[11,12,14],"h2",{"id":13},"зачем-вообще-нужны-мультиагентные-системы","Зачем вообще нужны мультиагентные системы",[16,17,18],"p",{},"Представь, что ты поручил одному человеку написать статью, провести ресёрч, нарисовать иллюстрации и отредактировать текст. Он справится. Медленно, с переключением контекста, но справится. А теперь дай каждому из четырёх задачу своему специалисту — и параллельно. Результат будет быстрее и, скорее всего, качественнее.",[16,20,21],{},"Мультиагентные AI-системы работают по тому же принципу. Вместо одного LLM-агента, который делает всё подряд, ты запускаешь нескольких агентов с разными ролями. Один ищет информацию, другой пишет код, третий проверяет результат. Фреймворк берёт на себя оркестрацию — кто за что отвечает, как они общаются, что делать при ошибке.",[16,23,24],{},"В 2025–2026 эта идея вышла из стадии экспериментов в production. На GitHub десятки фреймворков, и у каждого — своя философия. Я выбрал шесть, которые реально используются, и разобрал их по косточкам.",[11,26,28],{"id":27},"что-мы-сравниваем","Что мы сравниваем",[30,31,32,54],"table",{},[33,34,35],"thead",{},[36,37,38,42,45,48,51],"tr",{},[39,40,41],"th",{},"Фреймворк",[39,43,44],{},"Автор",[39,46,47],{},"Звёзды",[39,49,50],{},"Лицензия",[39,52,53],{},"Последний релиз",[55,56,57,75,92,108,123,138],"tbody",{},[36,58,59,63,66,69,72],{},[60,61,62],"td",{},"Hermes Agent",[60,64,65],{},"NousResearch",[60,67,68],{},"164k",[60,70,71],{},"Apache 2.0",[60,73,74],{},"2026-05",[36,76,77,80,83,86,89],{},[60,78,79],{},"CrewAI",[60,81,82],{},"crewAI Inc",[60,84,85],{},"52k",[60,87,88],{},"MIT",[60,90,91],{},"2026-05-18",[36,93,94,97,100,103,105],{},[60,95,96],{},"LangGraph",[60,98,99],{},"LangChain",[60,101,102],{},"33k",[60,104,88],{},[60,106,107],{},"2026-05-22",[36,109,110,113,116,119,121],{},[60,111,112],{},"CAMEL-AI",[60,114,115],{},"camel-ai",[60,117,118],{},"17k",[60,120,71],{},[60,122,74],{},[36,124,125,128,131,134,136],{},[60,126,127],{},"AG2",[60,129,130],{},"ag2ai (ex-AutoGen)",[60,132,133],{},"4.6k",[60,135,71],{},[60,137,74],{},[36,139,140,143,146,149,151],{},[60,141,142],{},"OpenPlanter",[60,144,145],{},"ShinMegamiBoson",[60,147,148],{},"1.6k",[60,150,88],{},[60,152,153],{},"2026-03",[16,155,156],{},"Все — на Python. Все open source. Но на этом сходства заканчиваются.",[11,158,160],{"id":159},"какие-фичи-важны-и-что-они-значат","Какие фичи важны (и что они значат)",[16,162,163],{},"Прежде чем лезть в сравнение, разберёмся с терминологией. В таблицах и доках фреймворков часто фигурируют одни и те же слова, которые значат совершенно разные вещи.",[165,166,168],"h3",{"id":167},"оркестрация-single-multi-parallel","Оркестрация (single \u002F multi \u002F parallel)",[16,170,171],{},"Это сердце любого мультиагентного фреймворка — как агенты координируют работу.",[16,173,174,178],{},[175,176,177],"strong",{},"Single"," — один агент, одна задача. По сути, обычный LLM-вызов. OpenPlanter работает именно так: ты запускаешь цепочку, но внутри неё нет реально параллельных агентов.",[16,180,181,184],{},[175,182,183],{},"Multi"," — несколько агентов, но работают они по очереди или по схеме «ведущий ведомый». AG2, CrewAI и CAMEL-AI используют этот подход. Один агент может делегировать задачу другому, но они не тянут одновременно.",[16,186,187,190],{},[175,188,189],{},"Parallel"," — агенты реально работают параллельно, с чекпоинтами и условной маршрутизацией. LangGraph тут впереди: его DAG (Directed Acyclic Graph) позволяет ветвить и мержить потоки выполнения. Hermes Agent тоже поддерживает multi через Kanban-доску с зависимостями между задачами.",[165,192,194],{"id":193},"разделяемая-память","Разделяемая память",[16,196,197],{},"Могут ли агенты видеть контекст друг друга? Это критично для сложных пайплайнов, где результат одного агента — входные данные для другого.",[16,199,200,201,205],{},"AG2 реализует это через ",[202,203,204],"code",{},"context_variables"," — общий словарь, который читают и пишут все агенты в Group Chat. CrewAI и LangGraph тоже поддерживают shared state. CAMEL-AI имеет общую память через паттерн Workforce.",[16,207,208],{},"У Hermes Agent shared memory частичная — есть issue #377 с предложением scratchpad-паттерна, но оно пока открыто. Kanban-задачи обеспечивают обмен данными через метаданные и комментарии, но это не то же самое, что общий пул памяти в реальном времени.",[165,210,212],{"id":211},"коммуникация","Коммуникация",[16,214,215],{},"Как агенты разговаривают друг с другом? Все шесть фреймворков используют internal-коммуникацию — сообщения передаются внутри фреймворка, не через внешний API. Это нормально для большинства кейсов, но становится проблемой, когда нужно связать агентов из разных систем.",[16,217,218],{},"Тут на помощь приходит A2A-протокол (об этом ниже).",[165,220,222],{"id":221},"adversarial-debate-антагонистический-дебат","Adversarial debate (антагонистический дебат)",[16,224,225],{},"Один агент генерирует ответ, другой его критикует. Если критик находит проблемы — первый переделывает. Это мощный паттерн для повышения качества: по сути, code review для LLM-вывода.",[16,227,228],{},"AG2 реализует это через Group Chat с LLM-handoffs. CAMEL-AI идёт дальше — у него RolePlaying-паттерн изначально спроектирован для дебатов. Hermes Agent имеет partial-поддержку через PR #20158 (режим Adversarial Debate Mode).",[16,230,231],{},"CrewAI, LangGraph и OpenPlanter — без встроенного дебата. Можно эмулировать через дополнительные агенты с промптами, но из коробки не работает.",[165,233,235],{"id":234},"inception-prompting","Inception prompting",[16,237,238],{},"Самоулучшающиеся промпты — агент анализирует свой вывод и корректирует системный промпт для следующей попытки. Звучит как sci-fi, но CAMEL-AI реализует это через Inception Prompting из своей исследовательской работы (NeurIPS 2023).",[16,240,241],{},"У AG2 partial-поддержка через системные сообщения. У остальных — нет.",[165,243,245],{"id":244},"adaptive-retry","Adaptive retry",[16,247,248],{},"Что происходит, когда агент ошибается? Простой вариант — повторить тот же запрос. Умный вариант — проанализировать ошибку, сменить модель, скорректировать промпт, и только потом повторить.",[16,250,251],{},"Hermes Agent планирует Adaptive Retry с лестницей эскалации модели (issue #30587, PR #30620). CrewAI и LangGraph имеют retry-механизмы. AG2, CAMEL-AI и OpenPlanter — нет.",[165,253,255],{"id":254},"managed-runtime","Managed runtime",[16,257,258],{},"Нужно ли тебе управлять инфраструктурой, на которой работают агенты? CrewAI и LangGraph предлагают managed-решения — облако, где агенты уже запущены и настроены. AG2 и Hermes Agent — partial: можно запустить самому, но есть некоторые контракты для управления средой выполнения. CAMEL-AI и OpenPlanter — полностью self-hosted.",[11,260,262],{"id":261},"фреймворк-за-фреймворком","Фреймворк за фреймворком",[165,264,266],{"id":265},"ag2-ex-autogen","AG2 (ex-AutoGen)",[16,268,269],{},"AG2 — это переименованный AutoGen от Microsoft, который теперь живёт в организации ag2ai. Самый зрелый фреймворк в плане оркестрации.",[16,271,272],{},"У AG2 пять паттернов координации: AutoPattern (LLM выбирает следующего спикера), RoundRobin (по кругу), Random (случайно), Manual (человек выбирает), Default (явные handoffs с условиями). Это гибко — можно выстроить практически любую топологию.",[16,274,275],{},"Главный козырь AG2 — нативная поддержка A2A с версии 0.10. Ты exposing своего агента как A2A-сервер, и к нему могут подключаться агенты из других фреймворков. На сегодня AG2 единственный крупный фреймворк, у которого A2A работает «из коробки» и совместим с v1.0 спецификации.",[16,277,278],{},"Слабые стороны: нет quality gates (агент не проверяет качество вывода подчинённых), нет персистентных профилей, нет CLI-интерфейса — только Python-библиотека. Если тебе нужен «агент как сервис» с CLI и плагинами, AG2 не про это.",[16,280,281],{},"Вот так выглядит оркестрация в AG2:",[283,284,289],"pre",{"className":285,"code":286,"language":287,"meta":288,"style":288},"language-python shiki shiki-themes github-light catppuccin-mocha","from autogen import ConversableAgent, GroupChat, GroupChatManager\n\ncoder = ConversableAgent(\"coder\", system_message=\"Ты пишешь Python-код.\")\nreviewer = ConversableAgent(\"reviewer\", system_message=\"Ты проверяешь код и находишь баги.\")\nwriter = ConversableAgent(\"writer\", system_message=\"Ты пишешь документацию.\")\n\ngroupchat = GroupChat(\n    agents=[coder, reviewer, writer],\n    messages=[],\n    speaker_selection_method=\"auto\",  # LLM выбирает, кто говорит следующим\n)\nmanager = GroupChatManager(groupchat=groupchat)\n","python","",[202,290,291,322,329,363,389,415,420,433,460,471,488,493],{"__ignoreMap":288},[292,293,296,300,304,307,310,314,317,319],"span",{"class":294,"line":295},"line",1,[292,297,299],{"class":298},"saXKZ","from",[292,301,303],{"class":302},"slTIY"," autogen ",[292,305,306],{"class":298},"import",[292,308,309],{"class":302}," ConversableAgent",[292,311,313],{"class":312},"s_QEy",",",[292,315,316],{"class":302}," GroupChat",[292,318,313],{"class":312},[292,320,321],{"class":302}," GroupChatManager\n",[292,323,325],{"class":294,"line":324},2,[292,326,328],{"emptyLinePlaceholder":327},true,"\n",[292,330,332,335,339,342,345,349,351,355,357,360],{"class":294,"line":331},3,[292,333,334],{"class":302},"coder ",[292,336,338],{"class":337},"s_Q3D","=",[292,340,309],{"class":341},"sPNDc",[292,343,344],{"class":312},"(",[292,346,348],{"class":347},"sG7gF","\"coder\"",[292,350,313],{"class":312},[292,352,354],{"class":353},"s-dMd"," system_message",[292,356,338],{"class":337},[292,358,359],{"class":347},"\"Ты пишешь Python-код.\"",[292,361,362],{"class":312},")\n",[292,364,366,369,371,373,375,378,380,382,384,387],{"class":294,"line":365},4,[292,367,368],{"class":302},"reviewer ",[292,370,338],{"class":337},[292,372,309],{"class":341},[292,374,344],{"class":312},[292,376,377],{"class":347},"\"reviewer\"",[292,379,313],{"class":312},[292,381,354],{"class":353},[292,383,338],{"class":337},[292,385,386],{"class":347},"\"Ты проверяешь код и находишь баги.\"",[292,388,362],{"class":312},[292,390,392,395,397,399,401,404,406,408,410,413],{"class":294,"line":391},5,[292,393,394],{"class":302},"writer ",[292,396,338],{"class":337},[292,398,309],{"class":341},[292,400,344],{"class":312},[292,402,403],{"class":347},"\"writer\"",[292,405,313],{"class":312},[292,407,354],{"class":353},[292,409,338],{"class":337},[292,411,412],{"class":347},"\"Ты пишешь документацию.\"",[292,414,362],{"class":312},[292,416,418],{"class":294,"line":417},6,[292,419,328],{"emptyLinePlaceholder":327},[292,421,423,426,428,430],{"class":294,"line":422},7,[292,424,425],{"class":302},"groupchat ",[292,427,338],{"class":337},[292,429,316],{"class":341},[292,431,432],{"class":312},"(\n",[292,434,436,439,441,444,447,449,452,454,457],{"class":294,"line":435},8,[292,437,438],{"class":353},"    agents",[292,440,338],{"class":337},[292,442,443],{"class":312},"[",[292,445,446],{"class":302},"coder",[292,448,313],{"class":312},[292,450,451],{"class":302}," reviewer",[292,453,313],{"class":312},[292,455,456],{"class":302}," writer",[292,458,459],{"class":312},"],\n",[292,461,463,466,468],{"class":294,"line":462},9,[292,464,465],{"class":353},"    messages",[292,467,338],{"class":337},[292,469,470],{"class":312},"[],\n",[292,472,474,477,479,482,484],{"class":294,"line":473},10,[292,475,476],{"class":353},"    speaker_selection_method",[292,478,338],{"class":337},[292,480,481],{"class":347},"\"auto\"",[292,483,313],{"class":312},[292,485,487],{"class":486},"skkvY","  # LLM выбирает, кто говорит следующим\n",[292,489,491],{"class":294,"line":490},11,[292,492,362],{"class":312},[292,494,496,499,501,504,506,509,511,513],{"class":294,"line":495},12,[292,497,498],{"class":302},"manager ",[292,500,338],{"class":337},[292,502,503],{"class":341}," GroupChatManager",[292,505,344],{"class":312},[292,507,508],{"class":353},"groupchat",[292,510,338],{"class":337},[292,512,508],{"class":302},[292,514,362],{"class":312},[16,516,517,518,521,522,525],{},"DefaultPattern даёт ещё больше контроля — явные условия перехода между агентами с ",[202,519,520],{},"OnCondition"," и ",[202,523,524],{},"LLMCondition",". Это как state machine, только вместо состояний — агенты.",[16,527,528,531],{},[175,529,530],{},"Когда выбирать:"," нужна гибкая оркестрация с A2A-интеропом между разными фреймворками.",[165,533,79],{"id":534},"crewai",[16,536,537],{},"CrewAI — самый популярный фреймворк после Hermes. 52k звёзд, MIT-лицения, простой API. Философия — «ролевые команды»: ты определяешь агентов с ролями (researcher, writer, analyst), даёшь им задачи, и CrewAI оркестрирует выполнение.",[16,539,540],{},"CrewAI выглядит примерно так:",[283,542,544],{"className":285,"code":543,"language":287,"meta":288,"style":288},"from crewai import Agent, Task, Crew\n\nresearcher = Agent(\n    role=\"Research Analyst\",\n    goal=\"Find relevant data on the topic\",\n    backstory=\"You are an expert researcher with 10 years of experience.\",\n)\nwriter = Agent(\n    role=\"Content Writer\",\n    goal=\"Write a clear, engaging article\",\n    backstory=\"You are a skilled technical writer.\",\n)\n\nresearch_task = Task(description=\"Research the topic\", agent=researcher)\nwrite_task = Task(description=\"Write the article\", agent=writer)\n\ncrew = Crew(agents=[researcher, writer], tasks=[research_task, write_task])\nresult = crew.kickoff()\n",[202,545,546,568,572,583,596,608,620,624,634,645,656,667,671,676,708,738,743,790],{"__ignoreMap":288},[292,547,548,550,553,555,558,560,563,565],{"class":294,"line":295},[292,549,299],{"class":298},[292,551,552],{"class":302}," crewai ",[292,554,306],{"class":298},[292,556,557],{"class":302}," Agent",[292,559,313],{"class":312},[292,561,562],{"class":302}," Task",[292,564,313],{"class":312},[292,566,567],{"class":302}," Crew\n",[292,569,570],{"class":294,"line":324},[292,571,328],{"emptyLinePlaceholder":327},[292,573,574,577,579,581],{"class":294,"line":331},[292,575,576],{"class":302},"researcher ",[292,578,338],{"class":337},[292,580,557],{"class":341},[292,582,432],{"class":312},[292,584,585,588,590,593],{"class":294,"line":365},[292,586,587],{"class":353},"    role",[292,589,338],{"class":337},[292,591,592],{"class":347},"\"Research Analyst\"",[292,594,595],{"class":312},",\n",[292,597,598,601,603,606],{"class":294,"line":391},[292,599,600],{"class":353},"    goal",[292,602,338],{"class":337},[292,604,605],{"class":347},"\"Find relevant data on the topic\"",[292,607,595],{"class":312},[292,609,610,613,615,618],{"class":294,"line":417},[292,611,612],{"class":353},"    backstory",[292,614,338],{"class":337},[292,616,617],{"class":347},"\"You are an expert researcher with 10 years of experience.\"",[292,619,595],{"class":312},[292,621,622],{"class":294,"line":422},[292,623,362],{"class":312},[292,625,626,628,630,632],{"class":294,"line":435},[292,627,394],{"class":302},[292,629,338],{"class":337},[292,631,557],{"class":341},[292,633,432],{"class":312},[292,635,636,638,640,643],{"class":294,"line":462},[292,637,587],{"class":353},[292,639,338],{"class":337},[292,641,642],{"class":347},"\"Content Writer\"",[292,644,595],{"class":312},[292,646,647,649,651,654],{"class":294,"line":473},[292,648,600],{"class":353},[292,650,338],{"class":337},[292,652,653],{"class":347},"\"Write a clear, engaging article\"",[292,655,595],{"class":312},[292,657,658,660,662,665],{"class":294,"line":490},[292,659,612],{"class":353},[292,661,338],{"class":337},[292,663,664],{"class":347},"\"You are a skilled technical writer.\"",[292,666,595],{"class":312},[292,668,669],{"class":294,"line":495},[292,670,362],{"class":312},[292,672,674],{"class":294,"line":673},13,[292,675,328],{"emptyLinePlaceholder":327},[292,677,679,682,684,686,688,691,693,696,698,701,703,706],{"class":294,"line":678},14,[292,680,681],{"class":302},"research_task ",[292,683,338],{"class":337},[292,685,562],{"class":341},[292,687,344],{"class":312},[292,689,690],{"class":353},"description",[292,692,338],{"class":337},[292,694,695],{"class":347},"\"Research the topic\"",[292,697,313],{"class":312},[292,699,700],{"class":353}," agent",[292,702,338],{"class":337},[292,704,705],{"class":302},"researcher",[292,707,362],{"class":312},[292,709,711,714,716,718,720,722,724,727,729,731,733,736],{"class":294,"line":710},15,[292,712,713],{"class":302},"write_task ",[292,715,338],{"class":337},[292,717,562],{"class":341},[292,719,344],{"class":312},[292,721,690],{"class":353},[292,723,338],{"class":337},[292,725,726],{"class":347},"\"Write the article\"",[292,728,313],{"class":312},[292,730,700],{"class":353},[292,732,338],{"class":337},[292,734,735],{"class":302},"writer",[292,737,362],{"class":312},[292,739,741],{"class":294,"line":740},16,[292,742,328],{"emptyLinePlaceholder":327},[292,744,746,749,751,754,756,759,761,763,765,767,769,772,775,777,779,782,784,787],{"class":294,"line":745},17,[292,747,748],{"class":302},"crew ",[292,750,338],{"class":337},[292,752,753],{"class":341}," Crew",[292,755,344],{"class":312},[292,757,758],{"class":353},"agents",[292,760,338],{"class":337},[292,762,443],{"class":312},[292,764,705],{"class":302},[292,766,313],{"class":312},[292,768,456],{"class":302},[292,770,771],{"class":312},"],",[292,773,774],{"class":353}," tasks",[292,776,338],{"class":337},[292,778,443],{"class":312},[292,780,781],{"class":302},"research_task",[292,783,313],{"class":312},[292,785,786],{"class":302}," write_task",[292,788,789],{"class":312},"])\n",[292,791,793,796,798,801,804,807],{"class":294,"line":792},18,[292,794,795],{"class":302},"result ",[292,797,338],{"class":337},[292,799,800],{"class":302}," crew",[292,802,803],{"class":312},".",[292,805,806],{"class":341},"kickoff",[292,808,809],{"class":312},"()\n",[16,811,812],{},"Два режима: sequential (задачи по очереди) и hierarchical (менеджер делегирует работникам). Есть managed runtime — можно запустить в облаке CrewAI. Adaptive retry — да, встроен.",[16,814,815],{},"Слабые стороны: нет adversarial debate, слабая поддержка сложных DAG (это не LangGraph), state persistence ограничен. A2A — только community-адаптеры, нативной поддержки нет.",[16,817,818,820],{},[175,819,530],{}," нужен быстрый старт с понятной ролевой моделью. Типичный кейс: «researcher ищет информацию, writer пишет отчёт, reviewer проверяет».",[165,822,96],{"id":823},"langgraph",[16,825,826],{},"LangGraph — это надстройка над LangChain, которая превращает цепочки вызовов в граф. Каждый узел — шаг обработки, рёбра — условия перехода. Можно ветвить, мержить, делать checkpoint, вставлять human-in-the-loop.",[16,828,829],{},"Вот схематично, как выглядит граф:",[283,831,833],{"className":285,"code":832,"language":287,"meta":288,"style":288},"from langgraph.graph import StateGraph\n\ndef research(state):\n    return {\"data\": call_llm(\"Research: \" + state[\"topic\"])}\n\ndef write(state):\n    return {\"draft\": call_llm(\"Write about: \" + state[\"data\"])}\n\ndef review(state):\n    approved = call_llm(\"Is this good? \" + state[\"draft\"])\n    return {\"approved\": \"yes\" in approved.lower()}\n\ngraph = StateGraph(dict)\ngraph.add_node(\"research\", research)\ngraph.add_node(\"write\", write)\ngraph.add_node(\"review\", review)\ngraph.add_edge(\"research\", \"write\")\ngraph.add_conditional_edges(\"review\", lambda s: \"end\" if s[\"approved\"] else \"write\")\n",[202,834,835,852,856,874,916,920,933,966,970,983,1012,1040,1044,1061,1082,1101,1120,1140],{"__ignoreMap":288},[292,836,837,839,842,844,847,849],{"class":294,"line":295},[292,838,299],{"class":298},[292,840,841],{"class":302}," langgraph",[292,843,803],{"class":312},[292,845,846],{"class":302},"graph ",[292,848,306],{"class":298},[292,850,851],{"class":302}," StateGraph\n",[292,853,854],{"class":294,"line":324},[292,855,328],{"emptyLinePlaceholder":327},[292,857,858,861,865,867,871],{"class":294,"line":331},[292,859,860],{"class":298},"def",[292,862,864],{"class":863},"siMrf"," research",[292,866,344],{"class":312},[292,868,870],{"class":869},"sO2U0","state",[292,872,873],{"class":312},"):\n",[292,875,876,879,882,885,888,891,893,896,899,902,904,907,911,913],{"class":294,"line":365},[292,877,878],{"class":298},"    return",[292,880,881],{"class":312}," {",[292,883,884],{"class":347},"\"data\"",[292,886,887],{"class":312},":",[292,889,890],{"class":341}," call_llm",[292,892,344],{"class":312},[292,894,895],{"class":347},"\"Research: \"",[292,897,898],{"class":337}," +",[292,900,901],{"class":869}," state",[292,903,443],{"class":312},[292,905,906],{"class":347},"\"",[292,908,910],{"class":909},"sdETa","topic",[292,912,906],{"class":347},[292,914,915],{"class":312},"])}\n",[292,917,918],{"class":294,"line":391},[292,919,328],{"emptyLinePlaceholder":327},[292,921,922,924,927,929,931],{"class":294,"line":417},[292,923,860],{"class":298},[292,925,926],{"class":863}," write",[292,928,344],{"class":312},[292,930,870],{"class":869},[292,932,873],{"class":312},[292,934,935,937,939,942,944,946,948,951,953,955,957,959,962,964],{"class":294,"line":422},[292,936,878],{"class":298},[292,938,881],{"class":312},[292,940,941],{"class":347},"\"draft\"",[292,943,887],{"class":312},[292,945,890],{"class":341},[292,947,344],{"class":312},[292,949,950],{"class":347},"\"Write about: \"",[292,952,898],{"class":337},[292,954,901],{"class":869},[292,956,443],{"class":312},[292,958,906],{"class":347},[292,960,961],{"class":909},"data",[292,963,906],{"class":347},[292,965,915],{"class":312},[292,967,968],{"class":294,"line":435},[292,969,328],{"emptyLinePlaceholder":327},[292,971,972,974,977,979,981],{"class":294,"line":462},[292,973,860],{"class":298},[292,975,976],{"class":863}," review",[292,978,344],{"class":312},[292,980,870],{"class":869},[292,982,873],{"class":312},[292,984,985,988,990,992,994,997,999,1001,1003,1005,1008,1010],{"class":294,"line":473},[292,986,987],{"class":302},"    approved ",[292,989,338],{"class":337},[292,991,890],{"class":341},[292,993,344],{"class":312},[292,995,996],{"class":347},"\"Is this good? \"",[292,998,898],{"class":337},[292,1000,901],{"class":869},[292,1002,443],{"class":312},[292,1004,906],{"class":347},[292,1006,1007],{"class":909},"draft",[292,1009,906],{"class":347},[292,1011,789],{"class":312},[292,1013,1014,1016,1018,1021,1023,1026,1029,1032,1034,1037],{"class":294,"line":490},[292,1015,878],{"class":298},[292,1017,881],{"class":312},[292,1019,1020],{"class":347},"\"approved\"",[292,1022,887],{"class":312},[292,1024,1025],{"class":347}," \"yes\"",[292,1027,1028],{"class":298}," in",[292,1030,1031],{"class":302}," approved",[292,1033,803],{"class":312},[292,1035,1036],{"class":341},"lower",[292,1038,1039],{"class":312},"()}\n",[292,1041,1042],{"class":294,"line":495},[292,1043,328],{"emptyLinePlaceholder":327},[292,1045,1046,1048,1050,1053,1055,1059],{"class":294,"line":673},[292,1047,846],{"class":302},[292,1049,338],{"class":337},[292,1051,1052],{"class":341}," StateGraph",[292,1054,344],{"class":312},[292,1056,1058],{"class":1057},"smIoM","dict",[292,1060,362],{"class":312},[292,1062,1063,1066,1068,1071,1073,1076,1078,1080],{"class":294,"line":678},[292,1064,1065],{"class":302},"graph",[292,1067,803],{"class":312},[292,1069,1070],{"class":341},"add_node",[292,1072,344],{"class":312},[292,1074,1075],{"class":347},"\"research\"",[292,1077,313],{"class":312},[292,1079,864],{"class":302},[292,1081,362],{"class":312},[292,1083,1084,1086,1088,1090,1092,1095,1097,1099],{"class":294,"line":710},[292,1085,1065],{"class":302},[292,1087,803],{"class":312},[292,1089,1070],{"class":341},[292,1091,344],{"class":312},[292,1093,1094],{"class":347},"\"write\"",[292,1096,313],{"class":312},[292,1098,926],{"class":302},[292,1100,362],{"class":312},[292,1102,1103,1105,1107,1109,1111,1114,1116,1118],{"class":294,"line":740},[292,1104,1065],{"class":302},[292,1106,803],{"class":312},[292,1108,1070],{"class":341},[292,1110,344],{"class":312},[292,1112,1113],{"class":347},"\"review\"",[292,1115,313],{"class":312},[292,1117,976],{"class":302},[292,1119,362],{"class":312},[292,1121,1122,1124,1126,1129,1131,1133,1135,1138],{"class":294,"line":745},[292,1123,1065],{"class":302},[292,1125,803],{"class":312},[292,1127,1128],{"class":341},"add_edge",[292,1130,344],{"class":312},[292,1132,1075],{"class":347},[292,1134,313],{"class":312},[292,1136,1137],{"class":347}," \"write\"",[292,1139,362],{"class":312},[292,1141,1142,1144,1146,1149,1151,1153,1155,1158,1161,1163,1166,1169,1171,1173,1175,1178,1180,1183,1186,1188],{"class":294,"line":792},[292,1143,1065],{"class":302},[292,1145,803],{"class":312},[292,1147,1148],{"class":341},"add_conditional_edges",[292,1150,344],{"class":312},[292,1152,1113],{"class":347},[292,1154,313],{"class":312},[292,1156,1157],{"class":298}," lambda",[292,1159,1160],{"class":869}," s",[292,1162,887],{"class":312},[292,1164,1165],{"class":347}," \"end\"",[292,1167,1168],{"class":298}," if",[292,1170,1160],{"class":869},[292,1172,443],{"class":312},[292,1174,906],{"class":347},[292,1176,1177],{"class":909},"approved",[292,1179,906],{"class":347},[292,1181,1182],{"class":312},"]",[292,1184,1185],{"class":298}," else",[292,1187,1137],{"class":347},[292,1189,362],{"class":312},[16,1191,1192],{},"Сильная сторона — stateful workflows с чекпоинтами. Если агент упал на шаге 7 из 12, можно откатиться к шагу 6 и продолжить. Это редкость среди мультиагентных фреймворков. Human-in-the-loop тоже хорошо реализован — можно вставить «паузу» в граф, чтобы человек подтвердил переход.",[16,1194,1195],{},"33k звёзд, MIT, managed runtime через LangGraph Cloud. Adaptive retry есть.",[16,1197,1198],{},"Слабые стороны: нет встроенного adversarial debate, нет inception prompting, A2A только через community. LangGraph больше про workflow-оркестрацию, чем про «агенты, которые спорят друг с другом».",[16,1200,1201,1203],{},[175,1202,530],{}," сложные многошаговые пайплайны с ветвлениями, чекпоинтами и human-in-the-loop. Если тебе нужен «граф обработки с возможностью отката» — это LangGraph.",[165,1205,112],{"id":115},[16,1207,1208],{},"CAMEL-AI — самый исследовательский фреймворк. Родился как академический проект (NeurIPS 2023, arXiv:2303.17760) и до сих пор сохраняет research-DNA.",[16,1210,1211],{},"У CAMEL-AI два уникальных паттерна: RolePlaying (два агента играют роли и спорят, пока не придут к консенсусу) и Inception Prompting (агент улучшает свои промпты на основе предыдущих попыток). Workforce-паттерн реализует shared memory для команд агентов.",[16,1213,1214],{},"17k звёзд, Apache 2.0. Из минусов — нет managed runtime, нет adaptive retry, нет A2A-поддержки. Фреймворк больше заточен под эксперименты, чем под production.",[16,1216,1217,1219],{},[175,1218,530],{}," исследовательские задачи, где нужен adversarial debate и inception prompting. Если ты изучаешь, как LLM-агенты могут спорить и самообучаться — CAMEL-AI это место.",[165,1221,62],{"id":1222},"hermes-agent",[16,1224,1225],{},"Hermes Agent от NousResearch — не совсем фреймворк в классическом смысле. Это CLI-first система с Kanban-доской, профилями, навыками и плагинами. 164k звёзд — самый популярный в списке.",[16,1227,1228],{},"Философия другая: Hermes решает application layer (задачи, память, навыки, CLI), а не framework layer (Python SDK для написания агентов). Kanban-доска с зависимостями — это и есть оркестрация: задачи блокируются, разблокируются, передаются между профилями агентов.",[16,1230,1231],{},"Shared memory partial, adversarial debate partial (PR #20158), adaptive retry в разработке (PR #30620). A2A — реализация существует (PR #4135, +2831 строк, 71 тест), но не смержена.",[16,1233,1234],{},"Главная сила Hermes — экосистема. Persistent profiles (изолированная конфигурация, память, навыки для каждого агента), Skills system (процедурная память), Holographic Memory (гибридный поиск с FTS5 + семантика). Никакой другой фреймворк не даёт ничего подобного.",[16,1236,1237,1239],{},[175,1238,530],{}," нужна CLI-система с persistent агентами, задачами и навыками. Hermes — это не «библиотека для написания агентов», а «платформа для запуска агентов как сервисов».",[165,1241,142],{"id":1242},"openplanter",[16,1244,1245],{},"OpenPlanter — самый маленький фреймворк в подборке. 1.6k звёзд, single orchestration, без shared memory, без debate, без retry. По сути, это обёртка для последовательного вызова LLM с минимальной оркестрацией.",[16,1247,1248],{},"Не буду рекомендовать его для production. Но если тебе нужен простой каркас для экспериментов — OpenPlanter может быть отправной точкой. Иногда «less is more» работает.",[11,1250,1252],{"id":1251},"a2a-протокол-почему-это-важно","A2A-протокол: почему это важно",[16,1254,1255],{},"Google представил A2A (Agent-to-Agent) протокол в апреле 2025 года. Идея простая: MCP отвечает на вопрос «какие инструменты доступны?», а A2A — «кто может помочь?»",[16,1257,1258],{},"Ключевые концепции: Agent Card (JSON-описание возможностей агента, аналог MCP tool list), Task (единица работы), Message (коммуникация внутри задачи), Artifact (результат). Есть streaming через SSE.",[16,1260,1261],{},"Сейчас A2A поддерживают нативно: AG2 (с v0.10), Google ADK (с первого дня), Pydantic AI. Community-адаптеры есть у CrewAI и LangChain. Hermes Agent — полная реализация существует, но PR #4135 не смержен.",[16,1263,1264],{},"Почему это важно? Потому что мультиагентные фреймворки — это острова. Твой агент на CrewAI не может легко позвать агента на AG2. A2A решает эту проблему: expose агента как A2A-сервер, и любой фреймворк с A2A-клиентом может к нему обратиться.",[283,1266,1268],{"className":285,"code":1267,"language":287,"meta":288,"style":288},"# AG2: exposing агента как A2A-сервер\nfrom autogen import ConversableAgent, LLMConfig\nfrom autogen.a2a import A2aAgentServer\n\nagent = ConversableAgent(\n    name=\"coder\",\n    system_message=\"Expert Python developer\",\n    llm_config=LLMConfig({\"model\": \"gpt-4o-mini\"}),\n)\nserver = A2aAgentServer(agent).build()\n# uvicorn server:server --port 8000\n\n# Подключение из другого процесса\nfrom autogen.a2a import A2aRemoteAgent\nremote = A2aRemoteAgent(url=\"http:\u002F\u002Flocalhost:8000\", name=\"coder\")\nawait local_agent.a_initiate_chat(recipient=remote, message=\"Write a CSV parser\")\n",[202,1269,1270,1275,1290,1307,1311,1322,1333,1345,1369,1373,1396,1401,1405,1410,1425,1456],{"__ignoreMap":288},[292,1271,1272],{"class":294,"line":295},[292,1273,1274],{"class":486},"# AG2: exposing агента как A2A-сервер\n",[292,1276,1277,1279,1281,1283,1285,1287],{"class":294,"line":324},[292,1278,299],{"class":298},[292,1280,303],{"class":302},[292,1282,306],{"class":298},[292,1284,309],{"class":302},[292,1286,313],{"class":312},[292,1288,1289],{"class":302}," LLMConfig\n",[292,1291,1292,1294,1297,1299,1302,1304],{"class":294,"line":331},[292,1293,299],{"class":298},[292,1295,1296],{"class":302}," autogen",[292,1298,803],{"class":312},[292,1300,1301],{"class":302},"a2a ",[292,1303,306],{"class":298},[292,1305,1306],{"class":302}," A2aAgentServer\n",[292,1308,1309],{"class":294,"line":365},[292,1310,328],{"emptyLinePlaceholder":327},[292,1312,1313,1316,1318,1320],{"class":294,"line":391},[292,1314,1315],{"class":302},"agent ",[292,1317,338],{"class":337},[292,1319,309],{"class":341},[292,1321,432],{"class":312},[292,1323,1324,1327,1329,1331],{"class":294,"line":417},[292,1325,1326],{"class":353},"    name",[292,1328,338],{"class":337},[292,1330,348],{"class":347},[292,1332,595],{"class":312},[292,1334,1335,1338,1340,1343],{"class":294,"line":422},[292,1336,1337],{"class":353},"    system_message",[292,1339,338],{"class":337},[292,1341,1342],{"class":347},"\"Expert Python developer\"",[292,1344,595],{"class":312},[292,1346,1347,1350,1352,1355,1358,1361,1363,1366],{"class":294,"line":435},[292,1348,1349],{"class":353},"    llm_config",[292,1351,338],{"class":337},[292,1353,1354],{"class":341},"LLMConfig",[292,1356,1357],{"class":312},"({",[292,1359,1360],{"class":347},"\"model\"",[292,1362,887],{"class":312},[292,1364,1365],{"class":347}," \"gpt-4o-mini\"",[292,1367,1368],{"class":312},"}),\n",[292,1370,1371],{"class":294,"line":462},[292,1372,362],{"class":312},[292,1374,1375,1378,1380,1383,1385,1388,1391,1394],{"class":294,"line":473},[292,1376,1377],{"class":302},"server ",[292,1379,338],{"class":337},[292,1381,1382],{"class":341}," A2aAgentServer",[292,1384,344],{"class":312},[292,1386,1387],{"class":302},"agent",[292,1389,1390],{"class":312},").",[292,1392,1393],{"class":341},"build",[292,1395,809],{"class":312},[292,1397,1398],{"class":294,"line":490},[292,1399,1400],{"class":486},"# uvicorn server:server --port 8000\n",[292,1402,1403],{"class":294,"line":495},[292,1404,328],{"emptyLinePlaceholder":327},[292,1406,1407],{"class":294,"line":673},[292,1408,1409],{"class":486},"# Подключение из другого процесса\n",[292,1411,1412,1414,1416,1418,1420,1422],{"class":294,"line":678},[292,1413,299],{"class":298},[292,1415,1296],{"class":302},[292,1417,803],{"class":312},[292,1419,1301],{"class":302},[292,1421,306],{"class":298},[292,1423,1424],{"class":302}," A2aRemoteAgent\n",[292,1426,1427,1430,1432,1435,1437,1440,1442,1445,1447,1450,1452,1454],{"class":294,"line":710},[292,1428,1429],{"class":302},"remote ",[292,1431,338],{"class":337},[292,1433,1434],{"class":341}," A2aRemoteAgent",[292,1436,344],{"class":312},[292,1438,1439],{"class":353},"url",[292,1441,338],{"class":337},[292,1443,1444],{"class":347},"\"http:\u002F\u002Flocalhost:8000\"",[292,1446,313],{"class":312},[292,1448,1449],{"class":353}," name",[292,1451,338],{"class":337},[292,1453,348],{"class":347},[292,1455,362],{"class":312},[292,1457,1458,1461,1464,1466,1469,1471,1474,1476,1479,1481,1484,1486,1489],{"class":294,"line":740},[292,1459,1460],{"class":298},"await",[292,1462,1463],{"class":302}," local_agent",[292,1465,803],{"class":312},[292,1467,1468],{"class":341},"a_initiate_chat",[292,1470,344],{"class":312},[292,1472,1473],{"class":353},"recipient",[292,1475,338],{"class":337},[292,1477,1478],{"class":302},"remote",[292,1480,313],{"class":312},[292,1482,1483],{"class":353}," message",[292,1485,338],{"class":337},[292,1487,1488],{"class":347},"\"Write a CSV parser\"",[292,1490,362],{"class":312},[16,1492,1493],{},"A2A и MCP — не конкуренты, а комплементарные протоколы. MCP даёт агенту инструменты, A2A даёт агенту коллег. Вместе они формируют полный стек для мультиагентных систем.",[11,1495,1497],{"id":1496},"сводная-таблица","Сводная таблица",[30,1499,1500,1520],{},[33,1501,1502],{},[36,1503,1504,1507,1510,1512,1514,1516,1518],{},[39,1505,1506],{},"Фича",[39,1508,1509],{},"Hermes",[39,1511,127],{},[39,1513,79],{},[39,1515,96],{},[39,1517,112],{},[39,1519,142],{},[55,1521,1522,1542,1562,1579,1595,1611,1627,1645,1662,1679],{},[36,1523,1524,1527,1530,1532,1534,1537,1539],{},[60,1525,1526],{},"Оркестрация",[60,1528,1529],{},"multi",[60,1531,1529],{},[60,1533,1529],{},[60,1535,1536],{},"parallel",[60,1538,1529],{},[60,1540,1541],{},"single",[36,1543,1544,1547,1550,1553,1555,1557,1559],{},[60,1545,1546],{},"Shared memory",[60,1548,1549],{},"partial",[60,1551,1552],{},"✅",[60,1554,1552],{},[60,1556,1552],{},[60,1558,1552],{},[60,1560,1561],{},"❌",[36,1563,1564,1567,1569,1571,1573,1575,1577],{},[60,1565,1566],{},"Adversarial debate",[60,1568,1549],{},[60,1570,1552],{},[60,1572,1561],{},[60,1574,1561],{},[60,1576,1552],{},[60,1578,1561],{},[36,1580,1581,1583,1585,1587,1589,1591,1593],{},[60,1582,235],{},[60,1584,1561],{},[60,1586,1549],{},[60,1588,1561],{},[60,1590,1561],{},[60,1592,1552],{},[60,1594,1561],{},[36,1596,1597,1599,1601,1603,1605,1607,1609],{},[60,1598,245],{},[60,1600,1549],{},[60,1602,1561],{},[60,1604,1552],{},[60,1606,1552],{},[60,1608,1561],{},[60,1610,1561],{},[36,1612,1613,1615,1617,1619,1621,1623,1625],{},[60,1614,255],{},[60,1616,1549],{},[60,1618,1549],{},[60,1620,1552],{},[60,1622,1552],{},[60,1624,1561],{},[60,1626,1561],{},[36,1628,1629,1632,1634,1637,1639,1641,1643],{},[60,1630,1631],{},"A2A протокол",[60,1633,1549],{},[60,1635,1636],{},"✅ native",[60,1638,1549],{},[60,1640,1549],{},[60,1642,1561],{},[60,1644,1561],{},[36,1646,1647,1650,1652,1654,1656,1658,1660],{},[60,1648,1649],{},"CLI",[60,1651,1552],{},[60,1653,1561],{},[60,1655,1561],{},[60,1657,1561],{},[60,1659,1561],{},[60,1661,1561],{},[36,1663,1664,1667,1669,1671,1673,1675,1677],{},[60,1665,1666],{},"Persistent profiles",[60,1668,1552],{},[60,1670,1561],{},[60,1672,1561],{},[60,1674,1561],{},[60,1676,1561],{},[60,1678,1561],{},[36,1680,1681,1684,1686,1688,1690,1692,1694],{},[60,1682,1683],{},"Skills\u002Fпамять",[60,1685,1552],{},[60,1687,1561],{},[60,1689,1561],{},[60,1691,1561],{},[60,1693,1561],{},[60,1695,1561],{},[11,1697,1699],{"id":1698},"рекомендации","Рекомендации",[16,1701,1702,1705],{},[175,1703,1704],{},"Быстрый старт с ролевыми командами"," → CrewAI. Простой API, managed runtime, MIT-лицения. Для MVP и прототипов — идеально.",[16,1707,1708,1711],{},[175,1709,1710],{},"Сложные stateful-пайплайны"," → LangGraph. DAG с чекпоинтами, conditional routing, human-in-the-loop. Если твой workflow имеет ветвления и нужен откат — это оно.",[16,1713,1714,1717],{},[175,1715,1716],{},"A2A-интероп и гибкая оркестрация"," → AG2. Единственный крупный фреймворк с нативной A2A v1.0. Пять паттернов координации. Если строишь систему, где агенты из разных фреймворков должны общаться — AG2.",[16,1719,1720,1723],{},[175,1721,1722],{},"Исследование и эксперименты"," → CAMEL-AI. RolePlaying, Inception Prompting, академический подход. Для изучения того, как LLM-агенты спорят и обучаются — лучший выбор.",[16,1725,1726,1729],{},[175,1727,1728],{},"Платформа для persistent агентов"," → Hermes Agent. CLI, Kanban, profiles, skills, holographic memory. Не фреймворк для написания агентов, а операционная система для их запуска.",[16,1731,1732,1735],{},[175,1733,1734],{},"Простые эксперименты"," → OpenPlanter. Минимальный каркас, ничего лишнего.",[11,1737,1739],{"id":1738},"что-будет-дальше","Что будет дальше",[16,1741,1742],{},"Мультиагентные фреймворки движутся в сторону интероперабельности. A2A станет стандартом де-факто — слишком много крупных игроков уже его поддерживают. MCP + A2A дадут полный стек: инструменты для агента и агенты для агента.",[16,1744,1745],{},"Hermes Agent выделяется тем, что решает проблему на другом уровне. Пока другие фреймворки спорят о паттернах оркестрации, Hermes создаёт инфраструктуру для persistent агентов с памятью, навыками и задачами. Это как разница между «библиотекой для HTTP-запросов» и «веб-сервером с плагинами».",[16,1747,1748],{},"AG2 тихо стал самым зрелым фреймворком для production-мультиагентных систем. Пять паттернов координации, нативный A2A, context variables. Если Microsoft вернёт себе AutoGen-бренд или ag2ai получит серьёзное финансирование — CrewAI и LangGraph могут оказаться под давлением.",[16,1750,1751],{},"CAMEL-AI остаётся исследовательским проектом, и это нормально. Не всё должно быть production-ready. Inception Prompting и RolePlaying — это будущее, которое пока живёт в лаборатории.",[16,1753,1754],{},"Выбирай фреймворк под задачу, а не под хайп. И помни: мультиагентная система — это не серебряная пуля. Если один агент справляется с задачей, не добавляйте ещё четырёх «для красоты».",[1756,1757,1758],"style",{},"html pre.shiki code .saXKZ, html code.shiki .saXKZ{--shiki-light:#D73A49;--shiki-dark:#CBA6F7}html pre.shiki code .slTIY, html code.shiki .slTIY{--shiki-light:#24292E;--shiki-dark:#CDD6F4}html pre.shiki code .s_QEy, html code.shiki .s_QEy{--shiki-light:#24292E;--shiki-dark:#9399B2}html pre.shiki code .s_Q3D, html code.shiki .s_Q3D{--shiki-light:#D73A49;--shiki-dark:#94E2D5}html pre.shiki code .sPNDc, html code.shiki .sPNDc{--shiki-light:#24292E;--shiki-dark:#89B4FA}html pre.shiki code .sG7gF, html code.shiki .sG7gF{--shiki-light:#032F62;--shiki-dark:#A6E3A1}html pre.shiki code .s-dMd, html code.shiki .s-dMd{--shiki-light:#E36209;--shiki-light-font-style:inherit;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic}html pre.shiki code .skkvY, html code.shiki .skkvY{--shiki-light:#6A737D;--shiki-light-font-style:inherit;--shiki-dark:#9399B2;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siMrf, html code.shiki .siMrf{--shiki-light:#6F42C1;--shiki-light-font-style:inherit;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic}html pre.shiki code .sO2U0, html code.shiki .sO2U0{--shiki-light:#24292E;--shiki-light-font-style:inherit;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic}html pre.shiki code .sdETa, html code.shiki .sdETa{--shiki-light:#032F62;--shiki-light-font-style:inherit;--shiki-dark:#A6E3A1;--shiki-dark-font-style:italic}html pre.shiki code .smIoM, html code.shiki .smIoM{--shiki-light:#005CC5;--shiki-light-font-style:inherit;--shiki-dark:#CBA6F7;--shiki-dark-font-style:italic}",{"title":288,"searchDepth":324,"depth":324,"links":1760},[1761,1762,1763,1772,1780,1781,1782,1783],{"id":13,"depth":324,"text":14},{"id":27,"depth":324,"text":28},{"id":159,"depth":324,"text":160,"children":1764},[1765,1766,1767,1768,1769,1770,1771],{"id":167,"depth":331,"text":168},{"id":193,"depth":331,"text":194},{"id":211,"depth":331,"text":212},{"id":221,"depth":331,"text":222},{"id":234,"depth":331,"text":235},{"id":244,"depth":331,"text":245},{"id":254,"depth":331,"text":255},{"id":261,"depth":324,"text":262,"children":1773},[1774,1775,1776,1777,1778,1779],{"id":265,"depth":331,"text":266},{"id":534,"depth":331,"text":79},{"id":823,"depth":331,"text":96},{"id":115,"depth":331,"text":112},{"id":1222,"depth":331,"text":62},{"id":1242,"depth":331,"text":142},{"id":1251,"depth":324,"text":1252},{"id":1496,"depth":324,"text":1497},{"id":1698,"depth":324,"text":1699},{"id":1738,"depth":324,"text":1739},"2026-06-01","Сравнение шести фреймворков для мультиагентных AI-систем — AG2, CrewAI, LangGraph, CAMEL-AI, Hermes Agent и OpenPlanter. Архитектура, фичи, реальные отличия.","md",{},"\u002Fblog\u002Fmulti-agent-frameworks-comparison",null,{"title":6,"description":1785},"blog\u002Fmulti-agent-frameworks-comparison",[1793,1794,1795,1796,1797,1798,534,823,1799,115,1222],"multi-agent","ai","frameworks","comparison","a2a","orchestration","ag2","4gARHD1CmsDy91fDFcNlEe7dcnEqxf7izD9IPiyjj8I",{"id":1802,"title":1803,"body":1804,"date":2440,"description":2441,"extension":1786,"meta":2442,"navigation":327,"path":2443,"readingTime":1789,"seo":2444,"stem":2445,"tags":2446,"__hash__":2453},"articles\u002Fblog\u002Fcloudflare-hermes-integration.md","Hermes + Cloudflare: автоматизация DNS и деплоя из терминала",{"type":8,"value":1805,"toc":2424},[1806,1810,1813,1816,1820,1823,1842,1845,1849,1859,1877,1880,1910,1913,1917,1921,1924,1974,1977,2015,2022,2026,2068,2072,2096,2100,2103,2163,2166,2232,2242,2246,2252,2258,2261,2265,2268,2273,2276,2290,2293,2297,2300,2323,2326,2330,2336,2342,2391,2397,2411,2415,2418,2421],[1807,1808,1803],"h1",{"id":1809},"hermes-cloudflare-автоматизация-dns-и-деплоя-из-терминала",[16,1811,1812],{},"У меня десятки субдоменов: блог, лендинги, API, нотификационные сервисы. Управлять всем этим через Cloudflare Dashboard — больно. Каждый раз логиниться, искать нужную запись, кликать, ждать...",[16,1814,1815],{},"Решение — дать это дело AI-агенту. Hermes Agent умеет работать с Cloudflare через CLI и API. Вот как я настроил этот pipeline.",[11,1817,1819],{"id":1818},"зачем-агенту-cloudflare","Зачем агенту Cloudflare?",[16,1821,1822],{},"Типичный сценарий: я ставлю новый сервис на VPS и хочу, чтобы он был доступен по HTTPS на своём субдомене. Раньше это было:",[1824,1825,1826,1830,1833,1836,1839],"ol",{},[1827,1828,1829],"li",{},"Зайти в Cloudflare Dashboard",[1827,1831,1832],{},"Создать DNS-запись",[1827,1834,1835],{},"Дождаться пропагации",[1827,1837,1838],{},"Проверить SSL",[1827,1840,1841],{},"Настроить Caddy\u002Fnginx",[16,1843,1844],{},"Теперь я говорю Hermes: «Подними ntfy на субдомене notify и проксируй через Cloudflare». Агент делает всё сам.",[11,1846,1848],{"id":1847},"инструмент-flarectl","Инструмент: flarectl",[16,1850,1851,1858],{},[1852,1853,1857],"a",{"href":1854,"rel":1855},"https:\u002F\u002Fgithub.com\u002Fcloudflare\u002Fcloudflare-go\u002Ftree\u002Fmain\u002Fcmd\u002Fflarectl",[1856],"nofollow","flarectl"," — официальная CLI-утилита от Cloudflare. Установка:",[283,1860,1864],{"className":1861,"code":1862,"language":1863,"meta":288,"style":288},"language-bash shiki shiki-themes github-light catppuccin-mocha","go install github.com\u002Fcloudflare\u002Fcloudflare-go\u002Fcmd\u002Fflarectl@latest\n","bash",[202,1865,1866],{"__ignoreMap":288},[292,1867,1868,1871,1874],{"class":294,"line":295},[292,1869,1870],{"class":863},"go",[292,1872,1873],{"class":347}," install",[292,1875,1876],{"class":347}," github.com\u002Fcloudflare\u002Fcloudflare-go\u002Fcmd\u002Fflarectl@latest\n",[16,1878,1879],{},"Авторизация через переменные окружения:",[283,1881,1883],{"className":1861,"code":1882,"language":1863,"meta":288,"style":288},"export CF_API_TOKEN=your-api-token\nexport CF_ACCOUNT_ID=your-account-id\n",[202,1884,1885,1898],{"__ignoreMap":288},[292,1886,1887,1890,1893,1895],{"class":294,"line":295},[292,1888,1889],{"class":298},"export",[292,1891,1892],{"class":302}," CF_API_TOKEN",[292,1894,338],{"class":337},[292,1896,1897],{"class":302},"your-api-token\n",[292,1899,1900,1902,1905,1907],{"class":294,"line":324},[292,1901,1889],{"class":298},[292,1903,1904],{"class":302}," CF_ACCOUNT_ID",[292,1906,338],{"class":337},[292,1908,1909],{"class":302},"your-account-id\n",[16,1911,1912],{},"Токен создаётся в Dashboard → My Profile → API Tokens. Нужны permissions: Zone:DNS:Edit, Zone:Zone:Read.",[11,1914,1916],{"id":1915},"базовые-операции","Базовые операции",[165,1918,1920],{"id":1919},"создание-dns-записи","Создание DNS-записи",[16,1922,1923],{},"A-запись (субдомен → IP сервера):",[283,1925,1927],{"className":1861,"code":1926,"language":1863,"meta":288,"style":288},"flarectl dns create --zone example.com \\\n  --type A --name myservice --content 203.0.113.1 --proxied\n",[202,1928,1929,1950],{"__ignoreMap":288},[292,1930,1931,1933,1936,1939,1943,1946],{"class":294,"line":295},[292,1932,1857],{"class":863},[292,1934,1935],{"class":347}," dns",[292,1937,1938],{"class":347}," create",[292,1940,1942],{"class":1941},"soLUO"," --zone",[292,1944,1945],{"class":347}," example.com",[292,1947,1949],{"class":1948},"s_VIv"," \\\n",[292,1951,1952,1955,1958,1961,1964,1967,1971],{"class":294,"line":324},[292,1953,1954],{"class":1941},"  --type",[292,1956,1957],{"class":347}," A",[292,1959,1960],{"class":1941}," --name",[292,1962,1963],{"class":347}," myservice",[292,1965,1966],{"class":1941}," --content",[292,1968,1970],{"class":1969},"sNSVI"," 203.0.113.1",[292,1972,1973],{"class":1941}," --proxied\n",[16,1975,1976],{},"CNAME (субдомен → Cloudflare Pages проект):",[283,1978,1980],{"className":1861,"code":1979,"language":1863,"meta":288,"style":288},"flarectl dns create --zone example.com \\\n  --type CNAME --name blog --content my-project.pages.dev --proxied\n",[202,1981,1982,1996],{"__ignoreMap":288},[292,1983,1984,1986,1988,1990,1992,1994],{"class":294,"line":295},[292,1985,1857],{"class":863},[292,1987,1935],{"class":347},[292,1989,1938],{"class":347},[292,1991,1942],{"class":1941},[292,1993,1945],{"class":347},[292,1995,1949],{"class":1948},[292,1997,1998,2000,2003,2005,2008,2010,2013],{"class":294,"line":324},[292,1999,1954],{"class":1941},[292,2001,2002],{"class":347}," CNAME",[292,2004,1960],{"class":1941},[292,2006,2007],{"class":347}," blog",[292,2009,1966],{"class":1941},[292,2011,2012],{"class":347}," my-project.pages.dev",[292,2014,1973],{"class":1941},[16,2016,2017,2018,2021],{},"Флаг ",[202,2019,2020],{},"--proxied"," включает оранжевый облако — Cloudflare кеширует, защищает от DDoS, terminates SSL.",[165,2023,2025],{"id":2024},"обновление-записей","Обновление записей",[283,2027,2029],{"className":1861,"code":2028,"language":1863,"meta":288,"style":288},"flarectl dns update --zone example.com \\\n  --id RECORD_ID --type A --name myservice --content 203.0.113.2\n",[202,2030,2031,2046],{"__ignoreMap":288},[292,2032,2033,2035,2037,2040,2042,2044],{"class":294,"line":295},[292,2034,1857],{"class":863},[292,2036,1935],{"class":347},[292,2038,2039],{"class":347}," update",[292,2041,1942],{"class":1941},[292,2043,1945],{"class":347},[292,2045,1949],{"class":1948},[292,2047,2048,2051,2054,2057,2059,2061,2063,2065],{"class":294,"line":324},[292,2049,2050],{"class":1941},"  --id",[292,2052,2053],{"class":347}," RECORD_ID",[292,2055,2056],{"class":1941}," --type",[292,2058,1957],{"class":347},[292,2060,1960],{"class":1941},[292,2062,1963],{"class":347},[292,2064,1966],{"class":1941},[292,2066,2067],{"class":1969}," 203.0.113.2\n",[165,2069,2071],{"id":2070},"удаление","Удаление",[283,2073,2075],{"className":1861,"code":2074,"language":1863,"meta":288,"style":288},"flarectl dns delete --zone example.com --id RECORD_ID\n",[202,2076,2077],{"__ignoreMap":288},[292,2078,2079,2081,2083,2086,2088,2090,2093],{"class":294,"line":295},[292,2080,1857],{"class":863},[292,2082,1935],{"class":347},[292,2084,2085],{"class":347}," delete",[292,2087,1942],{"class":1941},[292,2089,1945],{"class":347},[292,2091,2092],{"class":1941}," --id",[292,2094,2095],{"class":347}," RECORD_ID\n",[11,2097,2099],{"id":2098},"cloudflare-pages-деплой-статики","Cloudflare Pages: деплой статики",[16,2101,2102],{},"Для Nuxt, Astro, Next.js и других SSG\u002FSSR фреймворков Cloudflare Pages — бесплатный хостинг с CDN. Деплой через wrangler CLI:",[283,2104,2106],{"className":1861,"code":2105,"language":1863,"meta":288,"style":288},"npm install -g wrangler\n\n# Авторизация\nwrangler login\n\n# Деплой из папки dist\u002F\nwrangler pages deploy dist --project-name=my-blog\n",[202,2107,2108,2121,2125,2130,2138,2142,2147],{"__ignoreMap":288},[292,2109,2110,2113,2115,2118],{"class":294,"line":295},[292,2111,2112],{"class":863},"npm",[292,2114,1873],{"class":347},[292,2116,2117],{"class":1941}," -g",[292,2119,2120],{"class":347}," wrangler\n",[292,2122,2123],{"class":294,"line":324},[292,2124,328],{"emptyLinePlaceholder":327},[292,2126,2127],{"class":294,"line":331},[292,2128,2129],{"class":486},"# Авторизация\n",[292,2131,2132,2135],{"class":294,"line":365},[292,2133,2134],{"class":863},"wrangler",[292,2136,2137],{"class":347}," login\n",[292,2139,2140],{"class":294,"line":391},[292,2141,328],{"emptyLinePlaceholder":327},[292,2143,2144],{"class":294,"line":417},[292,2145,2146],{"class":486},"# Деплой из папки dist\u002F\n",[292,2148,2149,2151,2154,2157,2160],{"class":294,"line":422},[292,2150,2134],{"class":863},[292,2152,2153],{"class":347}," pages",[292,2155,2156],{"class":347}," deploy",[292,2158,2159],{"class":347}," dist",[292,2161,2162],{"class":1941}," --project-name=my-blog\n",[16,2164,2165],{},"Кастомный домен привязывается через API:",[283,2167,2169],{"className":1861,"code":2168,"language":1863,"meta":288,"style":288},"curl -s -X POST \\\n  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F$CF_ACCOUNT_ID\u002Fpages\u002Fprojects\u002Fmy-blog\u002Fdomains\" \\\n  -H \"Authorization: Bearer $CF_API_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  --data '{\"name\": \"blog.example.com\"}'\n",[202,2170,2171,2187,2200,2215,2224],{"__ignoreMap":288},[292,2172,2173,2176,2179,2182,2185],{"class":294,"line":295},[292,2174,2175],{"class":863},"curl",[292,2177,2178],{"class":1941}," -s",[292,2180,2181],{"class":1941}," -X",[292,2183,2184],{"class":347}," POST",[292,2186,1949],{"class":1948},[292,2188,2189,2192,2195,2198],{"class":294,"line":324},[292,2190,2191],{"class":347},"  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F",[292,2193,2194],{"class":302},"$CF_ACCOUNT_ID",[292,2196,2197],{"class":347},"\u002Fpages\u002Fprojects\u002Fmy-blog\u002Fdomains\"",[292,2199,1949],{"class":1948},[292,2201,2202,2205,2208,2211,2213],{"class":294,"line":331},[292,2203,2204],{"class":1941},"  -H",[292,2206,2207],{"class":347}," \"Authorization: Bearer ",[292,2209,2210],{"class":302},"$CF_API_TOKEN",[292,2212,906],{"class":347},[292,2214,1949],{"class":1948},[292,2216,2217,2219,2222],{"class":294,"line":365},[292,2218,2204],{"class":1941},[292,2220,2221],{"class":347}," \"Content-Type: application\u002Fjson\"",[292,2223,1949],{"class":1948},[292,2225,2226,2229],{"class":294,"line":391},[292,2227,2228],{"class":1941},"  --data",[292,2230,2231],{"class":347}," '{\"name\": \"blog.example.com\"}'\n",[2233,2234,2235],"blockquote",{},[16,2236,2237,2238,2241],{},"Wrangler v4.94+ может не поддерживать ",[202,2239,2240],{},"wrangler pages domain add",". Используйте API напрямую.",[11,2243,2245],{"id":2244},"ssl-flexible-vs-full","SSL: Flexible vs Full",[16,2247,2248,2251],{},[175,2249,2250],{},"SSL:Flexible"," — Cloudflare terminates SSL на своём edge, до вашего origin идёт plain HTTP. Не нужен сертификат на сервере. Идеально для сервисов, которые не работают с HTTPS нативно.",[16,2253,2254,2257],{},[175,2255,2256],{},"SSL:Full"," — Cloudflare terminates SSL на edge, но до origin тоже идёт SSL. Нужен сертификат на сервере (можно Cloudflare Origin CA — бесплатный).",[16,2259,2260],{},"Для self-hosted сервисов на одном VPS обычно хватает Flexible. Но если сервис передаёт чувствительные данные — ставьте Full.",[11,2262,2264],{"id":2263},"автоматизация-через-hermes","Автоматизация через Hermes",[16,2266,2267],{},"Вот как это работает в реальной жизни. Hermes Agent имеет доступ к терминалу и может выполнять команды. Я говорю:",[2233,2269,2270],{},[16,2271,2272],{},"«Поставь Grafana на монитор.example.com»",[16,2274,2275],{},"Агент:",[1824,2277,2278,2281,2284,2287],{},[1827,2279,2280],{},"Проверяет, что Grafana установлена",[1827,2282,2283],{},"Создаёт DNS A-запись через flarectl",[1827,2285,2286],{},"Настраивает Caddy reverse proxy",[1827,2288,2289],{},"Проверяет HTTPS",[16,2291,2292],{},"Всё в одном диалоге, без переключения в Dashboard.",[165,2294,2296],{"id":2295},"cron-мониторинг","Cron-мониторинг",[16,2298,2299],{},"Hermes может проверять DNS-записи по расписанию:",[283,2301,2303],{"className":1861,"code":2302,"language":1863,"meta":288,"style":288},"flarectl dns list --zone example.com --type A\n",[202,2304,2305],{"__ignoreMap":288},[292,2306,2307,2309,2311,2314,2316,2318,2320],{"class":294,"line":295},[292,2308,1857],{"class":863},[292,2310,1935],{"class":347},[292,2312,2313],{"class":347}," list",[292,2315,1942],{"class":1941},[292,2317,1945],{"class":347},[292,2319,2056],{"class":1941},[292,2321,2322],{"class":347}," A\n",[16,2324,2325],{},"Если запись изменилась или пропала — агент отправляет уведомление.",[11,2327,2329],{"id":2328},"подводные-камни","Подводные камни",[16,2331,2332,2335],{},[175,2333,2334],{},"Proxy status."," Если запись не proxied (серое облако), Cloudflare не terminates SSL и не кеширует. Для сервисов, которые должны быть за CDN — всегда proxied.",[16,2337,2338,2341],{},[175,2339,2340],{},"Cache purge."," После деплоя нового билда Cloudflare может отдавать старый кеш. Очистка:",[283,2343,2345],{"className":1861,"code":2344,"language":1863,"meta":288,"style":288},"curl -s -X POST \\\n  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F$ZONE_ID\u002Fpurge_cache\" \\\n  -H \"Authorization: Bearer $CF_API_TOKEN\" \\\n  --data '{\"purge_everything\":true}'\n",[202,2346,2347,2359,2372,2384],{"__ignoreMap":288},[292,2348,2349,2351,2353,2355,2357],{"class":294,"line":295},[292,2350,2175],{"class":863},[292,2352,2178],{"class":1941},[292,2354,2181],{"class":1941},[292,2356,2184],{"class":347},[292,2358,1949],{"class":1948},[292,2360,2361,2364,2367,2370],{"class":294,"line":324},[292,2362,2363],{"class":347},"  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F",[292,2365,2366],{"class":302},"$ZONE_ID",[292,2368,2369],{"class":347},"\u002Fpurge_cache\"",[292,2371,1949],{"class":1948},[292,2373,2374,2376,2378,2380,2382],{"class":294,"line":331},[292,2375,2204],{"class":1941},[292,2377,2207],{"class":347},[292,2379,2210],{"class":302},[292,2381,906],{"class":347},[292,2383,1949],{"class":1948},[292,2385,2386,2388],{"class":294,"line":365},[292,2387,2228],{"class":1941},[292,2389,2390],{"class":347}," '{\"purge_everything\":true}'\n",[16,2392,2393,2396],{},[175,2394,2395],{},"Rate limits."," Cloudflare Free план имеет лимиты на API-вызовы. Для автоматизации это редко проблема, но если агент делает десятки запросов в минуту — может словить 429.",[16,2398,2399,2402,2403,2406,2407,2410],{},[175,2400,2401],{},"DNS пропагация."," После создания записи DNS пропагируется за 1-5 минут. Hermes проверяет через ",[202,2404,2405],{},"dig"," или ",[202,2408,2409],{},"flarectl dns list"," перед тем, как считать задачу выполненной.",[11,2412,2414],{"id":2413},"итог","Итог",[16,2416,2417],{},"Cloudflare Free + Hermes Agent = полная автоматизация инфраструктуры из терминала. Никакого Dashboard, никаких ручных кликов. Агент управляет DNS, деплоит статику, настраивает SSL, мониторит записи.",[16,2419,2420],{},"Для self-hosted проектов это идеальный стек: бесплатно, надёжно, и всё через CLI.",[1756,2422,2423],{},"html pre.shiki code .siMrf, html code.shiki .siMrf{--shiki-light:#6F42C1;--shiki-light-font-style:inherit;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic}html pre.shiki code .sG7gF, html code.shiki .sG7gF{--shiki-light:#032F62;--shiki-dark:#A6E3A1}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .saXKZ, html code.shiki .saXKZ{--shiki-light:#D73A49;--shiki-dark:#CBA6F7}html pre.shiki code .slTIY, html code.shiki .slTIY{--shiki-light:#24292E;--shiki-dark:#CDD6F4}html pre.shiki code .s_Q3D, html code.shiki .s_Q3D{--shiki-light:#D73A49;--shiki-dark:#94E2D5}html pre.shiki code .soLUO, html code.shiki .soLUO{--shiki-light:#005CC5;--shiki-dark:#A6E3A1}html pre.shiki code .s_VIv, html code.shiki .s_VIv{--shiki-light:#005CC5;--shiki-dark:#F5C2E7}html pre.shiki code .sNSVI, html code.shiki .sNSVI{--shiki-light:#005CC5;--shiki-dark:#FAB387}html pre.shiki code .skkvY, html code.shiki .skkvY{--shiki-light:#6A737D;--shiki-light-font-style:inherit;--shiki-dark:#9399B2;--shiki-dark-font-style:italic}",{"title":288,"searchDepth":324,"depth":324,"links":2425},[2426,2427,2428,2433,2434,2435,2438,2439],{"id":1818,"depth":324,"text":1819},{"id":1847,"depth":324,"text":1848},{"id":1915,"depth":324,"text":1916,"children":2429},[2430,2431,2432],{"id":1919,"depth":331,"text":1920},{"id":2024,"depth":331,"text":2025},{"id":2070,"depth":331,"text":2071},{"id":2098,"depth":324,"text":2099},{"id":2244,"depth":324,"text":2245},{"id":2263,"depth":324,"text":2264,"children":2436},[2437],{"id":2295,"depth":331,"text":2296},{"id":2328,"depth":324,"text":2329},{"id":2413,"depth":324,"text":2414},"2026-05-24","Как AI-агент Hermes управляет DNS-записями, деплоит статику на Cloudflare Pages и настраивает SSL — всё через CLI, без Dashboard.",{},"\u002Fblog\u002Fcloudflare-hermes-integration",{"title":1803,"description":2441},"blog\u002Fcloudflare-hermes-integration",[2447,2448,2449,2450,2451,2452],"hermes","cloudflare","dns","devops","automation","cli","zBwXmjqPSypVC4vi_B8eEFZcexsExVqR5xCE6q_VDDA",{"id":2455,"title":2456,"body":2457,"date":2440,"description":4630,"extension":1786,"meta":4631,"navigation":327,"path":4632,"readingTime":1789,"seo":4633,"stem":4634,"tags":4635,"__hash__":4652},"articles\u002Fblog\u002Fholographic-memory-potato-vps.md","Голографическая память для AI-агента на Potato VPS",{"type":8,"value":2458,"toc":4601},[2459,2462,2465,2468,2475,2479,2482,2556,2566,2569,2573,2581,2584,2588,2592,2595,2599,2602,2606,2609,2613,2616,2620,2623,2716,2719,2722,2732,2736,3074,3081,3085,3150,3153,3157,3160,3320,3326,3332,3335,3339,3342,3721,3728,3755,3758,3762,3765,3864,3867,3871,3874,3958,3961,3965,3968,4022,4025,4032,4036,4039,4204,4207,4211,4215,4218,4224,4228,4235,4274,4277,4450,4454,4457,4461,4466,4470,4476,4567,4571,4574,4577,4580,4583,4586,4589,4598],[1807,2460,2456],{"id":2461},"голографическая-память-для-ai-агента-на-potato-vps",[16,2463,2464],{},"У меня есть так называемый Potato VPS — дешёвый сервер с минимумом оперативки. На нём крутится AI-агент — Hermes — который должен помнить контекст между сессиями: факты о пользователях, конфигурации проектов, предпочтения, решения. Не просто «записать в файл и grep'нуть», а смысловой поиск с пониманием перефразировок и мультиязычности.",[16,2466,2467],{},"Проблема: ChromaDB жрёт 400 МБ просто на старте. Pinecone — это SaaS, а я хочу локально. FAISS — без индексации по ключевым словам. Мне нужен гибрид: полнотекстовый поиск + векторная семантика + композиционная алгебра. И всё это на скромном сервере, включая саму модель эмбеддингов.",[16,2469,2470,2471,2474],{},"Решение — плагин ",[202,2472,2473],{},"holographic-memory",", четыре стратегии поиска в одном SQLite-файле. Вот как это устроено и почему.",[11,2476,2478],{"id":2477},"архитектура-четыре-стратегии-один-запрос","Архитектура: четыре стратегии, один запрос",[16,2480,2481],{},"Гибридный скоринг — не «векторный поиск с fallback на FTS», а четыре независимых канала, результаты которых складываются с весами:",[30,2483,2484,2500],{},[33,2485,2486],{},[36,2487,2488,2491,2494,2497],{},[39,2489,2490],{},"Стратегия",[39,2492,2493],{},"Вес",[39,2495,2496],{},"Что делает",[39,2498,2499],{},"Технология",[55,2501,2502,2516,2530,2543],{},[36,2503,2504,2507,2510,2513],{},[60,2505,2506],{},"FTS5",[60,2508,2509],{},"0.3",[60,2511,2512],{},"Кандидаты по ключевым словам (BM25)",[60,2514,2515],{},"SQLite FTS5",[36,2517,2518,2521,2524,2527],{},[60,2519,2520],{},"Jaccard",[60,2522,2523],{},"0.2",[60,2525,2526],{},"Пересечение токенов",[60,2528,2529],{},"Python sets",[36,2531,2532,2535,2537,2540],{},[60,2533,2534],{},"HRR",[60,2536,2523],{},[60,2538,2539],{},"Композиционная алгебра (probe\u002Frelated\u002Freason)",[60,2541,2542],{},"SHA-256 phase vectors, 1024d",[36,2544,2545,2548,2550,2553],{},[60,2546,2547],{},"Semantic",[60,2549,2509],{},[60,2551,2552],{},"Смысловое сходство",[60,2554,2555],{},"fastembed MiniLM-L12-v2, 384d",[16,2557,2558,2559,2562,2563,803],{},"Финальный скор: ",[202,2560,2561],{},"relevance × trust_score × temporal_decay",", где ",[202,2564,2565],{},"relevance = fts×0.3 + jaccard×0.2 + hrr×0.2 + semantic×0.3",[16,2567,2568],{},"Зачем четыре, если можно одним векторным поиском обойтись? Потому что векторный поиск плохо работает на коротких точных запросах («порядок деплоя nginx»), а FTS5 не понимает перефразировок («как выкатить nginx на прод»). HRR даёт алгебраические операции — probe по сущности, связь между фактами, мульти-сущностные JOIN'ы. Jaccard — дешёвый фильтр мусора.",[11,2570,2572],{"id":2571},"запуск-поиска-пайплайн","Запуск поиска: пайплайн",[283,2574,2579],{"className":2575,"code":2577,"language":2578},[2576],"language-text","1. FTS5 MATCH → limit×3 кандидатов (AND-семантика: все термы обязательны)\n2. Если FTS5 пустой + semantic доступен → _semantic_candidates() (полный cosine scan)\n3. Предвычисление эмбеддинга запроса (~46 мс)\n4. Реранжирование: relevance = fts×0.3 + jaccard×0.2 + hrr×0.2 + semantic×0.3\n5. Финал: score = relevance × trust × temporal_decay\n","text",[202,2580,2577],{"__ignoreMap":288},[16,2582,2583],{},"Ключевой момент: если FTS5 возвращает 0 (запрос на русском, а факты записаны по-английски, или перефразировка), пайплайн автоматически переключается на чистый семантический поиск. Semantic — не роскошь, а load-bearing компонент для мультиязычности.",[11,2585,2587],{"id":2586},"почему-не-готовые-решения","Почему не готовые решения",[165,2589,2591],{"id":2590},"chromadb","ChromaDB",[16,2593,2594],{},"Два процесса (Chroma + Hermes), ~800 МБ до того, как агент хоть что-то запомнил. На Potato VPS — это половина RAM. Плюс Chroma тянет за собой HNSW, который строит индекс в памяти. Для фактов (сотни, может тысячи записей) — это стрельба из пушки по воробьям.",[165,2596,2598],{"id":2597},"pinecone","Pinecone",[16,2600,2601],{},"SaaS. Требует API-ключ, интернет, и доверие к третьей стороне с данными агента. Для хобби-проекта на дешёвом VPS — overkill и vendor lock-in.",[165,2603,2605],{"id":2604},"faiss","FAISS",[16,2607,2608],{},"Отличная библиотека для векторного поиска, но без полнотекстовой индексации. Придётся поверх городить FTS5 отдельно. А если уж использовать SQLite и для текста, и для векторов — зачем FAISS?",[165,2610,2612],{"id":2611},"своё-решение","Своё решение",[16,2614,2615],{},"SQLite + FTS5 + WAL — это уже есть в Python stdlib (sqlite3). Добавляем fastembed для эмбеддингов и numpy для HRR-алгебры. Один файл базы, один процесс, zero infrastructure.",[11,2617,2619],{"id":2618},"выбор-модели-mpnet-vs-minilm","Выбор модели: mpnet vs MiniLM",[16,2621,2622],{},"Я тестировал две модели multilingual fastembed:",[30,2624,2625,2637],{},[33,2626,2627],{},[36,2628,2629,2631,2634],{},[39,2630],{},[39,2632,2633],{},"mpnet-base-v2",[39,2635,2636],{},"MiniLM-L12-v2",[55,2638,2639,2650,2661,2672,2683,2694,2705],{},[36,2640,2641,2644,2647],{},[60,2642,2643],{},"Размерность",[60,2645,2646],{},"768",[60,2648,2649],{},"384",[36,2651,2652,2655,2658],{},[60,2653,2654],{},"RSS (загружена)",[60,2656,2657],{},"1440 МБ",[60,2659,2660],{},"680 МБ",[36,2662,2663,2666,2669],{},[60,2664,2665],{},"RSS (residual после выгрузки)",[60,2667,2668],{},"693 МБ",[60,2670,2671],{},"481 МБ",[36,2673,2674,2677,2680],{},[60,2675,2676],{},"Время эмбеддинга",[60,2678,2679],{},"65 мс",[60,2681,2682],{},"46 мс",[36,2684,2685,2688,2691],{},[60,2686,2687],{},"Перезагрузка после выгрузки",[60,2689,2690],{},"20–25 с",[60,2692,2693],{},"1.33 с",[36,2695,2696,2699,2702],{},[60,2697,2698],{},"sim(\"компактный формат\" ↔ \"лаконичные сообщения\")",[60,2700,2701],{},"0.625",[60,2703,2704],{},"0.511",[36,2706,2707,2710,2713],{},[60,2708,2709],{},"sim(\"компактный формат\" ↔ \"погода для прогулки\")",[60,2711,2712],{},"0.225",[60,2714,2715],{},"0.008",[16,2717,2718],{},"MiniLM: 680 МБ RSS, 481 МБ residual. mpnet: 1440 МБ RSS, 693 МБ residual. На Potato VPS mpnet не влезает — после загрузки модели + Hermes + системы остаётся ~300 МБ, и OOM-killer стучится.",[16,2720,2721],{},"MiniLM даёт 0.511 для семантически схожих фраз и 0.008 для несвязанных — separation достаточная. Не идеальная (mpnet лучше на ~20%), но работоспособная.",[16,2723,2724,2727,2728,2731],{},[175,2725,2726],{},"Residual"," — это ONNX Runtime, который не освобождает пуллы памяти даже после ",[202,2729,2730],{},"del model + gc.collect()",". 481 МБ — плата за один вызов fastembed за жизнь процесса. Поэтому стратегия: lazy-load при первом запросе, держать в памяти до shutdown.",[11,2733,2735],{"id":2734},"lazy-loading-и-lifecycle-модели","Lazy-loading и lifecycle модели",[283,2737,2739],{"className":285,"code":2738,"language":287,"meta":288,"style":288},"_model = None\n_available: Optional[bool] = None\n\ndef is_available() -> bool:\n    \"\"\"Проверка без загрузки модели.\"\"\"\n    if _available is not None:\n        return _available\n    try:\n        import fastembed\n        _available = True\n    except ImportError:\n        _available = False\n    return _available\n\ndef _get_model():\n    \"\"\"Lazy-load при первом вызове embed_text().\"\"\"\n    global _model\n    if _model is None:\n        from fastembed import TextEmbedding\n        _model = TextEmbedding(\"sentence-transformers\u002Fparaphrase-multilingual-MiniLM-L12-v2\")\n    return _model\n\ndef embed_text(text: str) -> np.ndarray:\n    model = _get_model()\n    vec = list(model.embed([text]))[0]\n    return vec \u002F np.linalg.norm(vec)  # normalize → unit vector\n",[202,2740,2741,2751,2773,2777,2796,2801,2820,2828,2835,2843,2853,2864,2873,2879,2883,2893,2898,2906,2919,2933,2951,2958,2963,2995,3007,3041],{"__ignoreMap":288},[292,2742,2743,2746,2748],{"class":294,"line":295},[292,2744,2745],{"class":302},"_model ",[292,2747,338],{"class":337},[292,2749,2750],{"class":1969}," None\n",[292,2752,2753,2756,2758,2761,2763,2766,2768,2771],{"class":294,"line":324},[292,2754,2755],{"class":302},"_available",[292,2757,887],{"class":312},[292,2759,2760],{"class":869}," Optional",[292,2762,443],{"class":312},[292,2764,2765],{"class":1057},"bool",[292,2767,1182],{"class":312},[292,2769,2770],{"class":337}," =",[292,2772,2750],{"class":1969},[292,2774,2775],{"class":294,"line":331},[292,2776,328],{"emptyLinePlaceholder":327},[292,2778,2779,2781,2784,2787,2790,2793],{"class":294,"line":365},[292,2780,860],{"class":298},[292,2782,2783],{"class":863}," is_available",[292,2785,2786],{"class":312},"()",[292,2788,2789],{"class":312}," ->",[292,2791,2792],{"class":1057}," bool",[292,2794,2795],{"class":312},":\n",[292,2797,2798],{"class":294,"line":391},[292,2799,2800],{"class":347},"    \"\"\"Проверка без загрузки модели.\"\"\"\n",[292,2802,2803,2806,2809,2812,2815,2818],{"class":294,"line":417},[292,2804,2805],{"class":298},"    if",[292,2807,2808],{"class":302}," _available ",[292,2810,2811],{"class":298},"is",[292,2813,2814],{"class":298}," not",[292,2816,2817],{"class":1969}," None",[292,2819,2795],{"class":312},[292,2821,2822,2825],{"class":294,"line":422},[292,2823,2824],{"class":298},"        return",[292,2826,2827],{"class":302}," _available\n",[292,2829,2830,2833],{"class":294,"line":435},[292,2831,2832],{"class":298},"    try",[292,2834,2795],{"class":312},[292,2836,2837,2840],{"class":294,"line":462},[292,2838,2839],{"class":298},"        import",[292,2841,2842],{"class":302}," fastembed\n",[292,2844,2845,2848,2850],{"class":294,"line":473},[292,2846,2847],{"class":302},"        _available ",[292,2849,338],{"class":337},[292,2851,2852],{"class":1969}," True\n",[292,2854,2855,2858,2862],{"class":294,"line":490},[292,2856,2857],{"class":298},"    except",[292,2859,2861],{"class":2860},"sPY-v"," ImportError",[292,2863,2795],{"class":312},[292,2865,2866,2868,2870],{"class":294,"line":495},[292,2867,2847],{"class":302},[292,2869,338],{"class":337},[292,2871,2872],{"class":1969}," False\n",[292,2874,2875,2877],{"class":294,"line":673},[292,2876,878],{"class":298},[292,2878,2827],{"class":302},[292,2880,2881],{"class":294,"line":678},[292,2882,328],{"emptyLinePlaceholder":327},[292,2884,2885,2887,2890],{"class":294,"line":710},[292,2886,860],{"class":298},[292,2888,2889],{"class":863}," _get_model",[292,2891,2892],{"class":312},"():\n",[292,2894,2895],{"class":294,"line":740},[292,2896,2897],{"class":347},"    \"\"\"Lazy-load при первом вызове embed_text().\"\"\"\n",[292,2899,2900,2903],{"class":294,"line":745},[292,2901,2902],{"class":298},"    global",[292,2904,2905],{"class":302}," _model\n",[292,2907,2908,2910,2913,2915,2917],{"class":294,"line":792},[292,2909,2805],{"class":298},[292,2911,2912],{"class":302}," _model ",[292,2914,2811],{"class":298},[292,2916,2817],{"class":1969},[292,2918,2795],{"class":312},[292,2920,2922,2925,2928,2930],{"class":294,"line":2921},19,[292,2923,2924],{"class":298},"        from",[292,2926,2927],{"class":302}," fastembed ",[292,2929,306],{"class":298},[292,2931,2932],{"class":302}," TextEmbedding\n",[292,2934,2936,2939,2941,2944,2946,2949],{"class":294,"line":2935},20,[292,2937,2938],{"class":302},"        _model ",[292,2940,338],{"class":337},[292,2942,2943],{"class":341}," TextEmbedding",[292,2945,344],{"class":312},[292,2947,2948],{"class":347},"\"sentence-transformers\u002Fparaphrase-multilingual-MiniLM-L12-v2\"",[292,2950,362],{"class":312},[292,2952,2954,2956],{"class":294,"line":2953},21,[292,2955,878],{"class":298},[292,2957,2905],{"class":302},[292,2959,2961],{"class":294,"line":2960},22,[292,2962,328],{"emptyLinePlaceholder":327},[292,2964,2966,2968,2971,2973,2975,2977,2980,2983,2985,2988,2990,2993],{"class":294,"line":2965},23,[292,2967,860],{"class":298},[292,2969,2970],{"class":863}," embed_text",[292,2972,344],{"class":312},[292,2974,2578],{"class":869},[292,2976,887],{"class":312},[292,2978,2979],{"class":1057}," str",[292,2981,2982],{"class":312},")",[292,2984,2789],{"class":312},[292,2986,2987],{"class":302}," np",[292,2989,803],{"class":312},[292,2991,2992],{"class":302},"ndarray",[292,2994,2795],{"class":312},[292,2996,2998,3001,3003,3005],{"class":294,"line":2997},24,[292,2999,3000],{"class":302},"    model ",[292,3002,338],{"class":337},[292,3004,2889],{"class":341},[292,3006,809],{"class":312},[292,3008,3010,3013,3015,3017,3019,3022,3024,3027,3030,3032,3035,3038],{"class":294,"line":3009},25,[292,3011,3012],{"class":302},"    vec ",[292,3014,338],{"class":337},[292,3016,2313],{"class":1057},[292,3018,344],{"class":312},[292,3020,3021],{"class":302},"model",[292,3023,803],{"class":312},[292,3025,3026],{"class":341},"embed",[292,3028,3029],{"class":312},"([",[292,3031,2578],{"class":302},[292,3033,3034],{"class":312},"]))[",[292,3036,3037],{"class":1969},"0",[292,3039,3040],{"class":312},"]\n",[292,3042,3044,3046,3049,3052,3054,3056,3059,3061,3064,3066,3069,3071],{"class":294,"line":3043},26,[292,3045,878],{"class":298},[292,3047,3048],{"class":302}," vec ",[292,3050,3051],{"class":337},"\u002F",[292,3053,2987],{"class":302},[292,3055,803],{"class":312},[292,3057,3058],{"class":302},"linalg",[292,3060,803],{"class":312},[292,3062,3063],{"class":341},"norm",[292,3065,344],{"class":312},[292,3067,3068],{"class":302},"vec",[292,3070,2982],{"class":312},[292,3072,3073],{"class":486},"  # normalize → unit vector\n",[16,3075,3076,3077,3080],{},"Первый вызов ",[202,3078,3079],{},"embed_text()"," — ~1.3 с (загрузка ONNX-модели). Все последующие — ~46 мс. Модель живёт в памяти до shutdown.",[165,3082,3084],{"id":3083},"выгрузка-при-shutdown","Выгрузка при shutdown",[283,3086,3088],{"className":285,"code":3087,"language":287,"meta":288,"style":288},"# В __init__.py плагина, на shutdown hook:\nimport embedder, gc\n\ndef on_shutdown():\n    embedder._model = None\n    gc.collect()\n    # Освобождает ~200 МБ (веса модели), но ONNX residual (481 МБ) остаётся\n",[202,3089,3090,3095,3107,3111,3120,3133,3145],{"__ignoreMap":288},[292,3091,3092],{"class":294,"line":295},[292,3093,3094],{"class":486},"# В __init__.py плагина, на shutdown hook:\n",[292,3096,3097,3099,3102,3104],{"class":294,"line":324},[292,3098,306],{"class":298},[292,3100,3101],{"class":302}," embedder",[292,3103,313],{"class":312},[292,3105,3106],{"class":302}," gc\n",[292,3108,3109],{"class":294,"line":331},[292,3110,328],{"emptyLinePlaceholder":327},[292,3112,3113,3115,3118],{"class":294,"line":365},[292,3114,860],{"class":298},[292,3116,3117],{"class":863}," on_shutdown",[292,3119,2892],{"class":312},[292,3121,3122,3125,3127,3129,3131],{"class":294,"line":391},[292,3123,3124],{"class":302},"    embedder",[292,3126,803],{"class":312},[292,3128,2745],{"class":302},[292,3130,338],{"class":337},[292,3132,2750],{"class":1969},[292,3134,3135,3138,3140,3143],{"class":294,"line":417},[292,3136,3137],{"class":302},"    gc",[292,3139,803],{"class":312},[292,3141,3142],{"class":341},"collect",[292,3144,809],{"class":312},[292,3146,3147],{"class":294,"line":422},[292,3148,3149],{"class":486},"    # Освобождает ~200 МБ (веса модели), но ONNX residual (481 МБ) остаётся\n",[16,3151,3152],{},"На практике: после shutdown RSS падает с ~1.1 ГБ до ~620 МБ. ONNX Runtime не отдаёт память — это известная особенность. 620 МБ — рабочий baseline для Potato VPS.",[11,3154,3156],{"id":3155},"хранение-два-вектора-на-факт","Хранение: два вектора на факт",[16,3158,3159],{},"Каждый факт в SQLite хранит два вектора:",[283,3161,3165],{"className":3162,"code":3163,"language":3164,"meta":288,"style":288},"language-sql shiki shiki-themes github-light catppuccin-mocha","CREATE TABLE facts (\n    fact_id INTEGER PRIMARY KEY,\n    content TEXT NOT NULL,\n    category TEXT DEFAULT 'general',\n    tags TEXT DEFAULT '',\n    trust_score REAL DEFAULT 0.5,\n    retrieval_count INTEGER DEFAULT 0,\n    helpful_count INTEGER DEFAULT 0,\n    hrr_vector BLOB,        -- 1024 × float64 = 8192 bytes\n    semantic_vector BLOB,   -- 384 × float32 = 1536 bytes\n    created_at TEXT,\n    updated_at TEXT\n);\n","sql",[202,3166,3167,3181,3194,3207,3222,3236,3256,3269,3282,3290,3298,3307,3315],{"__ignoreMap":288},[292,3168,3169,3172,3175,3178],{"class":294,"line":295},[292,3170,3171],{"class":298},"CREATE",[292,3173,3174],{"class":298}," TABLE",[292,3176,3177],{"class":863}," facts",[292,3179,3180],{"class":302}," (\n",[292,3182,3183,3186,3189,3192],{"class":294,"line":324},[292,3184,3185],{"class":302},"    fact_id ",[292,3187,3188],{"class":298},"INTEGER",[292,3190,3191],{"class":298}," PRIMARY KEY",[292,3193,595],{"class":302},[292,3195,3196,3199,3202,3205],{"class":294,"line":331},[292,3197,3198],{"class":302},"    content ",[292,3200,3201],{"class":298},"TEXT",[292,3203,3204],{"class":298}," NOT NULL",[292,3206,595],{"class":302},[292,3208,3209,3212,3214,3217,3220],{"class":294,"line":365},[292,3210,3211],{"class":302},"    category ",[292,3213,3201],{"class":298},[292,3215,3216],{"class":298}," DEFAULT",[292,3218,3219],{"class":347}," 'general'",[292,3221,595],{"class":302},[292,3223,3224,3227,3229,3231,3234],{"class":294,"line":391},[292,3225,3226],{"class":302},"    tags ",[292,3228,3201],{"class":298},[292,3230,3216],{"class":298},[292,3232,3233],{"class":347}," ''",[292,3235,595],{"class":302},[292,3237,3238,3241,3244,3246,3249,3251,3254],{"class":294,"line":417},[292,3239,3240],{"class":302},"    trust_score ",[292,3242,3243],{"class":298},"REAL",[292,3245,3216],{"class":298},[292,3247,3248],{"class":1969}," 0",[292,3250,803],{"class":302},[292,3252,3253],{"class":1969},"5",[292,3255,595],{"class":302},[292,3257,3258,3261,3263,3265,3267],{"class":294,"line":422},[292,3259,3260],{"class":302},"    retrieval_count ",[292,3262,3188],{"class":298},[292,3264,3216],{"class":298},[292,3266,3248],{"class":1969},[292,3268,595],{"class":302},[292,3270,3271,3274,3276,3278,3280],{"class":294,"line":435},[292,3272,3273],{"class":302},"    helpful_count ",[292,3275,3188],{"class":298},[292,3277,3216],{"class":298},[292,3279,3248],{"class":1969},[292,3281,595],{"class":302},[292,3283,3284,3287],{"class":294,"line":462},[292,3285,3286],{"class":302},"    hrr_vector BLOB,        ",[292,3288,3289],{"class":486},"-- 1024 × float64 = 8192 bytes\n",[292,3291,3292,3295],{"class":294,"line":473},[292,3293,3294],{"class":302},"    semantic_vector BLOB,   ",[292,3296,3297],{"class":486},"-- 384 × float32 = 1536 bytes\n",[292,3299,3300,3303,3305],{"class":294,"line":490},[292,3301,3302],{"class":302},"    created_at ",[292,3304,3201],{"class":298},[292,3306,595],{"class":302},[292,3308,3309,3312],{"class":294,"line":495},[292,3310,3311],{"class":302},"    updated_at ",[292,3313,3314],{"class":298},"TEXT\n",[292,3316,3317],{"class":294,"line":673},[292,3318,3319],{"class":302},");\n",[16,3321,3322,3325],{},[175,3323,3324],{},"HRR-вектор"," (8 КБ на факт) — SHA-256 phase encoding. Токены контента → бандл атомов → bind с ROLE_CONTENT и ROLE_ENTITY. Используется для алгебраических операций: probe («все факты про X»), related («что связано с X»), reason («что общего у X, Y, Z»). Работает на numpy, без модели.",[16,3327,3328,3331],{},[175,3329,3330],{},"Semantic-вектор"," (1.5 КБ на факт) — выход fastembed. Нормализованный unit vector для cosine similarity. Используется в гибридном скоринге.",[16,3333,3334],{},"Итого: ~9.7 КБ на факт. Для 1000 фактов — ~10 МБ. Пустяк.",[11,3336,3338],{"id":3337},"hrr-композиционная-алгебра-без-нейросетей","HRR: композиционная алгебра без нейросетей",[16,3340,3341],{},"HRR (Holographic Reduced Representations) — это способ кодировать структуру в фиксированный вектор. Вместо обучения — SHA-256 хеши от токенов, преобразованные в phase vectors.",[283,3343,3345],{"className":285,"code":3344,"language":287,"meta":288,"style":288},"def encode_atom(token: str, dim: int = 1024) -> np.ndarray:\n    \"\"\"Токен → единичный вектор в phase space.\"\"\"\n    h = hashlib.sha256(token.encode()).digest()\n    rng = np.frombuffer(h, dtype=np.uint8).astype(np.float64)\n    # Фазовый код: cos + j*sin → unit vector в комплексном пространстве\n    phases = rng[:dim] \u002F 255.0 * 2 * np.pi\n    return np.cos(phases) + 1j * np.sin(phases)\n\ndef bind(a, b):\n    \"\"\"Связывание: circular convolution в frequency domain.\"\"\"\n    return np.fft.ifft(np.fft.fft(a) * np.fft.fft(b))\n\ndef bundle(vectors):\n    \"\"\"Бандлинг: поэлементная сумма + нормализация.\"\"\"\n    result = np.sum(vectors, axis=0)\n    return result \u002F np.linalg.norm(result)\n",[202,3346,3347,3390,3395,3427,3477,3482,3521,3562,3566,3584,3589,3643,3647,3661,3666,3695],{"__ignoreMap":288},[292,3348,3349,3351,3354,3356,3359,3361,3363,3365,3368,3370,3373,3375,3378,3380,3382,3384,3386,3388],{"class":294,"line":295},[292,3350,860],{"class":298},[292,3352,3353],{"class":863}," encode_atom",[292,3355,344],{"class":312},[292,3357,3358],{"class":869},"token",[292,3360,887],{"class":312},[292,3362,2979],{"class":1057},[292,3364,313],{"class":312},[292,3366,3367],{"class":869}," dim",[292,3369,887],{"class":312},[292,3371,3372],{"class":1057}," int",[292,3374,2770],{"class":337},[292,3376,3377],{"class":1969}," 1024",[292,3379,2982],{"class":312},[292,3381,2789],{"class":312},[292,3383,2987],{"class":302},[292,3385,803],{"class":312},[292,3387,2992],{"class":302},[292,3389,2795],{"class":312},[292,3391,3392],{"class":294,"line":324},[292,3393,3394],{"class":347},"    \"\"\"Токен → единичный вектор в phase space.\"\"\"\n",[292,3396,3397,3400,3402,3405,3407,3410,3412,3414,3416,3419,3422,3425],{"class":294,"line":331},[292,3398,3399],{"class":302},"    h ",[292,3401,338],{"class":337},[292,3403,3404],{"class":302}," hashlib",[292,3406,803],{"class":312},[292,3408,3409],{"class":341},"sha256",[292,3411,344],{"class":312},[292,3413,3358],{"class":302},[292,3415,803],{"class":312},[292,3417,3418],{"class":341},"encode",[292,3420,3421],{"class":312},"()).",[292,3423,3424],{"class":341},"digest",[292,3426,809],{"class":312},[292,3428,3429,3432,3434,3436,3438,3441,3443,3446,3448,3451,3453,3456,3458,3461,3463,3466,3468,3470,3472,3475],{"class":294,"line":365},[292,3430,3431],{"class":302},"    rng ",[292,3433,338],{"class":337},[292,3435,2987],{"class":302},[292,3437,803],{"class":312},[292,3439,3440],{"class":341},"frombuffer",[292,3442,344],{"class":312},[292,3444,3445],{"class":302},"h",[292,3447,313],{"class":312},[292,3449,3450],{"class":353}," dtype",[292,3452,338],{"class":337},[292,3454,3455],{"class":302},"np",[292,3457,803],{"class":312},[292,3459,3460],{"class":302},"uint8",[292,3462,1390],{"class":312},[292,3464,3465],{"class":341},"astype",[292,3467,344],{"class":312},[292,3469,3455],{"class":302},[292,3471,803],{"class":312},[292,3473,3474],{"class":302},"float64",[292,3476,362],{"class":312},[292,3478,3479],{"class":294,"line":391},[292,3480,3481],{"class":486},"    # Фазовый код: cos + j*sin → unit vector в комплексном пространстве\n",[292,3483,3484,3487,3489,3492,3495,3498,3500,3503,3506,3509,3512,3514,3516,3518],{"class":294,"line":417},[292,3485,3486],{"class":302},"    phases ",[292,3488,338],{"class":337},[292,3490,3491],{"class":869}," rng",[292,3493,3494],{"class":312},"[:",[292,3496,3497],{"class":869},"dim",[292,3499,1182],{"class":312},[292,3501,3502],{"class":337}," \u002F",[292,3504,3505],{"class":1969}," 255.0",[292,3507,3508],{"class":337}," *",[292,3510,3511],{"class":1969}," 2",[292,3513,3508],{"class":337},[292,3515,2987],{"class":302},[292,3517,803],{"class":312},[292,3519,3520],{"class":302},"pi\n",[292,3522,3523,3525,3527,3529,3532,3534,3537,3539,3541,3544,3547,3549,3551,3553,3556,3558,3560],{"class":294,"line":422},[292,3524,878],{"class":298},[292,3526,2987],{"class":302},[292,3528,803],{"class":312},[292,3530,3531],{"class":341},"cos",[292,3533,344],{"class":312},[292,3535,3536],{"class":302},"phases",[292,3538,2982],{"class":312},[292,3540,898],{"class":337},[292,3542,3543],{"class":1969}," 1",[292,3545,3546],{"class":298},"j",[292,3548,3508],{"class":337},[292,3550,2987],{"class":302},[292,3552,803],{"class":312},[292,3554,3555],{"class":341},"sin",[292,3557,344],{"class":312},[292,3559,3536],{"class":302},[292,3561,362],{"class":312},[292,3563,3564],{"class":294,"line":435},[292,3565,328],{"emptyLinePlaceholder":327},[292,3567,3568,3570,3573,3575,3577,3579,3582],{"class":294,"line":462},[292,3569,860],{"class":298},[292,3571,3572],{"class":863}," bind",[292,3574,344],{"class":312},[292,3576,1852],{"class":869},[292,3578,313],{"class":312},[292,3580,3581],{"class":869}," b",[292,3583,873],{"class":312},[292,3585,3586],{"class":294,"line":473},[292,3587,3588],{"class":347},"    \"\"\"Связывание: circular convolution в frequency domain.\"\"\"\n",[292,3590,3591,3593,3595,3597,3600,3602,3605,3607,3609,3611,3613,3615,3617,3619,3621,3623,3625,3627,3629,3631,3633,3635,3637,3640],{"class":294,"line":490},[292,3592,878],{"class":298},[292,3594,2987],{"class":302},[292,3596,803],{"class":312},[292,3598,3599],{"class":302},"fft",[292,3601,803],{"class":312},[292,3603,3604],{"class":341},"ifft",[292,3606,344],{"class":312},[292,3608,3455],{"class":302},[292,3610,803],{"class":312},[292,3612,3599],{"class":302},[292,3614,803],{"class":312},[292,3616,3599],{"class":341},[292,3618,344],{"class":312},[292,3620,1852],{"class":302},[292,3622,2982],{"class":312},[292,3624,3508],{"class":337},[292,3626,2987],{"class":302},[292,3628,803],{"class":312},[292,3630,3599],{"class":302},[292,3632,803],{"class":312},[292,3634,3599],{"class":341},[292,3636,344],{"class":312},[292,3638,3639],{"class":302},"b",[292,3641,3642],{"class":312},"))\n",[292,3644,3645],{"class":294,"line":495},[292,3646,328],{"emptyLinePlaceholder":327},[292,3648,3649,3651,3654,3656,3659],{"class":294,"line":673},[292,3650,860],{"class":298},[292,3652,3653],{"class":863}," bundle",[292,3655,344],{"class":312},[292,3657,3658],{"class":869},"vectors",[292,3660,873],{"class":312},[292,3662,3663],{"class":294,"line":678},[292,3664,3665],{"class":347},"    \"\"\"Бандлинг: поэлементная сумма + нормализация.\"\"\"\n",[292,3667,3668,3671,3673,3675,3677,3680,3682,3684,3686,3689,3691,3693],{"class":294,"line":710},[292,3669,3670],{"class":302},"    result ",[292,3672,338],{"class":337},[292,3674,2987],{"class":302},[292,3676,803],{"class":312},[292,3678,3679],{"class":341},"sum",[292,3681,344],{"class":312},[292,3683,3658],{"class":302},[292,3685,313],{"class":312},[292,3687,3688],{"class":353}," axis",[292,3690,338],{"class":337},[292,3692,3037],{"class":1969},[292,3694,362],{"class":312},[292,3696,3697,3699,3702,3704,3706,3708,3710,3712,3714,3716,3719],{"class":294,"line":740},[292,3698,878],{"class":298},[292,3700,3701],{"class":302}," result ",[292,3703,3051],{"class":337},[292,3705,2987],{"class":302},[292,3707,803],{"class":312},[292,3709,3058],{"class":302},[292,3711,803],{"class":312},[292,3713,3063],{"class":341},[292,3715,344],{"class":312},[292,3717,3718],{"class":302},"result",[292,3720,362],{"class":312},[16,3722,3723,3724,3727],{},"Зачем это нужно, если есть fastembed? Потому что HRR поддерживает ",[175,3725,3726],{},"алгебраические операции",", которые векторные модели не умеют:",[3729,3730,3731,3737,3743],"ul",{},[1827,3732,3733,3736],{},[175,3734,3735],{},"probe(entity)"," — «дай все факты, привязанные к сущности X». Bind\u002Funbind с entity-атомом.",[1827,3738,3739,3742],{},[175,3740,3741],{},"related(entity)"," — «что структурно соседствует с X». Через HRR similarity.",[1827,3744,3745,3751,3752,803],{},[175,3746,3747,3748,2982],{},"reason(",[292,3749,3750],{},"e1, e2, e3"," — «что общего у нескольких сущностей». Multi-entity JOIN, ",[202,3753,3754],{},"min(scores)",[16,3756,3757],{},"Semantic-модель даёт «похожесть по смыслу», HRR — «связанность по структуре». Это разные оси.",[11,3759,3761],{"id":3760},"jaccard-дешёвый-фильтр","Jaccard: дешёвый фильтр",[16,3763,3764],{},"Jaccard — пересечение токенов запроса и факта, поделённое на объединение. Стоимость: O(n) по токенам, ноль выделений памяти. Работает как грубый фильтр: если запрос и факт не пересекаются ни одним токеном — штраф.",[283,3766,3768],{"className":285,"code":3767,"language":287,"meta":288,"style":288},"def jaccard(query_tokens: set, fact_tokens: set) -> float:\n    if not query_tokens or not fact_tokens:\n        return 0.0\n    return len(query_tokens & fact_tokens) \u002F len(query_tokens | fact_tokens)\n",[202,3769,3770,3805,3823,3830],{"__ignoreMap":288},[292,3771,3772,3774,3777,3779,3782,3784,3787,3789,3792,3794,3796,3798,3800,3803],{"class":294,"line":295},[292,3773,860],{"class":298},[292,3775,3776],{"class":863}," jaccard",[292,3778,344],{"class":312},[292,3780,3781],{"class":869},"query_tokens",[292,3783,887],{"class":312},[292,3785,3786],{"class":1057}," set",[292,3788,313],{"class":312},[292,3790,3791],{"class":869}," fact_tokens",[292,3793,887],{"class":312},[292,3795,3786],{"class":1057},[292,3797,2982],{"class":312},[292,3799,2789],{"class":312},[292,3801,3802],{"class":1057}," float",[292,3804,2795],{"class":312},[292,3806,3807,3809,3811,3814,3817,3819,3821],{"class":294,"line":324},[292,3808,2805],{"class":298},[292,3810,2814],{"class":298},[292,3812,3813],{"class":302}," query_tokens ",[292,3815,3816],{"class":298},"or",[292,3818,2814],{"class":298},[292,3820,3791],{"class":302},[292,3822,2795],{"class":312},[292,3824,3825,3827],{"class":294,"line":331},[292,3826,2824],{"class":298},[292,3828,3829],{"class":1969}," 0.0\n",[292,3831,3832,3834,3837,3839,3842,3845,3847,3849,3851,3853,3855,3857,3860,3862],{"class":294,"line":365},[292,3833,878],{"class":298},[292,3835,3836],{"class":2860}," len",[292,3838,344],{"class":312},[292,3840,3841],{"class":302},"query_tokens ",[292,3843,3844],{"class":337},"&",[292,3846,3791],{"class":302},[292,3848,2982],{"class":312},[292,3850,3502],{"class":337},[292,3852,3836],{"class":2860},[292,3854,344],{"class":312},[292,3856,3841],{"class":302},[292,3858,3859],{"class":337},"|",[292,3861,3791],{"class":302},[292,3863,362],{"class":312},[16,3865,3866],{},"Вес 0.2 — не главный канал, но отсекает шум. На практике: запрос «деплой nginx» и факт «настройка DNS» получают jaccard=0, и это правильно.",[11,3868,3870],{"id":3869},"fts5-полнотекстовая-индексация","FTS5: полнотекстовая индексация",[16,3872,3873],{},"SQLite FTS5 — встроенная полнотекстовая индексация. AND-семантика: все слова запроса должны присутствовать в факте. Это жёстко, но предсказуемо.",[283,3875,3877],{"className":3162,"code":3876,"language":3164,"meta":288,"style":288},"CREATE VIRTUAL TABLE facts_fts USING fts5(content, content=facts, content_rowid=fact_id);\n\n-- Поиск:\nSELECT fact_id, rank FROM facts_fts WHERE facts_fts MATCH 'деплой nginx'\nORDER BY rank LIMIT 30;\n",[202,3878,3879,3908,3912,3917,3941],{"__ignoreMap":288},[292,3880,3881,3883,3886,3889,3892,3895,3898,3900,3903,3905],{"class":294,"line":295},[292,3882,3171],{"class":298},[292,3884,3885],{"class":302}," VIRTUAL ",[292,3887,3888],{"class":298},"TABLE",[292,3890,3891],{"class":302}," facts_fts ",[292,3893,3894],{"class":298},"USING",[292,3896,3897],{"class":302}," fts5(content, content",[292,3899,338],{"class":337},[292,3901,3902],{"class":302},"facts, content_rowid",[292,3904,338],{"class":337},[292,3906,3907],{"class":302},"fact_id);\n",[292,3909,3910],{"class":294,"line":324},[292,3911,328],{"emptyLinePlaceholder":327},[292,3913,3914],{"class":294,"line":331},[292,3915,3916],{"class":486},"-- Поиск:\n",[292,3918,3919,3922,3925,3928,3930,3933,3935,3938],{"class":294,"line":365},[292,3920,3921],{"class":298},"SELECT",[292,3923,3924],{"class":302}," fact_id, rank ",[292,3926,3927],{"class":298},"FROM",[292,3929,3891],{"class":302},[292,3931,3932],{"class":298},"WHERE",[292,3934,3891],{"class":302},[292,3936,3937],{"class":298},"MATCH",[292,3939,3940],{"class":347}," 'деплой nginx'\n",[292,3942,3943,3946,3949,3952,3955],{"class":294,"line":391},[292,3944,3945],{"class":298},"ORDER BY",[292,3947,3948],{"class":302}," rank ",[292,3950,3951],{"class":298},"LIMIT",[292,3953,3954],{"class":1969}," 30",[292,3956,3957],{"class":302},";\n",[16,3959,3960],{},"Проблема FTS5: не понимает перефразировок. «Как выкатить nginx на прод» не матчит «деплой nginx через CI\u002FCD». Поэтому fallback на semantic search при пустом FTS5 — критичен.",[11,3962,3964],{"id":3963},"стоимость-на-potato-vps","Стоимость на Potato VPS",[16,3966,3967],{},"Типичный memory footprint при работающем агенте:",[30,3969,3970,3980],{},[33,3971,3972],{},[36,3973,3974,3977],{},[39,3975,3976],{},"Компонент",[39,3978,3979],{},"RSS",[55,3981,3982,3990,3998,4006,4014],{},[36,3983,3984,3987],{},[60,3985,3986],{},"Hermes core (Python)",[60,3988,3989],{},"~380 МБ",[36,3991,3992,3995],{},[60,3993,3994],{},"fastembed (загружена)",[60,3996,3997],{},"+300 МБ",[36,3999,4000,4003],{},[60,4001,4002],{},"ONNX Runtime residual",[60,4004,4005],{},"(включено выше)",[36,4007,4008,4011],{},[60,4009,4010],{},"SQLite + FTS5 index",[60,4012,4013],{},"~5 МБ",[36,4015,4016,4019],{},[60,4017,4018],{},"Итого",[60,4020,4021],{},"~680 МБ",[16,4023,4024],{},"Остаётся достаточно для системы, swap, и других процессов. Комфортно.",[16,4026,4027,4028,4031],{},"При shutdown модель выгружается, RSS падает до ~620 МБ. ONNX residual не освобождается — это цена одного ",[202,4029,4030],{},"import fastembed"," за жизнь процесса.",[11,4033,4035],{"id":4034},"weight-auto-redistribution","Weight auto-redistribution",[16,4037,4038],{},"Если fastembed недоступна (не установлена, или numpy отсутствует), веса перераспределяются автоматически:",[283,4040,4042],{"className":285,"code":4041,"language":287,"meta":288,"style":288},"def _redistribute_weights(self):\n    if not embedder.is_available():\n        # Semantic недоступен: 0.3 → FTS +0.15, Jaccard +0.1, HRR +0.05\n        self.fts_weight = 0.45\n        self.jaccard_weight = 0.30\n        self.hrr_weight = 0.25\n        self.semantic_weight = 0.0\n    elif not _HAS_NUMPY:\n        # HRR недоступен: 0.2 → FTS +0.1, Semantic +0.1\n        self.fts_weight = 0.40\n        self.jaccard_weight = 0.20\n        self.hrr_weight = 0.0\n        self.semantic_weight = 0.40\n",[202,4043,4044,4059,4074,4079,4095,4109,4123,4136,4149,4154,4167,4180,4192],{"__ignoreMap":288},[292,4045,4046,4048,4051,4053,4057],{"class":294,"line":295},[292,4047,860],{"class":298},[292,4049,4050],{"class":863}," _redistribute_weights",[292,4052,344],{"class":312},[292,4054,4056],{"class":4055},"s7pD5","self",[292,4058,873],{"class":312},[292,4060,4061,4063,4065,4067,4069,4072],{"class":294,"line":324},[292,4062,2805],{"class":298},[292,4064,2814],{"class":298},[292,4066,3101],{"class":302},[292,4068,803],{"class":312},[292,4070,4071],{"class":341},"is_available",[292,4073,2892],{"class":312},[292,4075,4076],{"class":294,"line":331},[292,4077,4078],{"class":486},"        # Semantic недоступен: 0.3 → FTS +0.15, Jaccard +0.1, HRR +0.05\n",[292,4080,4081,4085,4087,4090,4092],{"class":294,"line":365},[292,4082,4084],{"class":4083},"sG_o1","        self",[292,4086,803],{"class":312},[292,4088,4089],{"class":302},"fts_weight ",[292,4091,338],{"class":337},[292,4093,4094],{"class":1969}," 0.45\n",[292,4096,4097,4099,4101,4104,4106],{"class":294,"line":391},[292,4098,4084],{"class":4083},[292,4100,803],{"class":312},[292,4102,4103],{"class":302},"jaccard_weight ",[292,4105,338],{"class":337},[292,4107,4108],{"class":1969}," 0.30\n",[292,4110,4111,4113,4115,4118,4120],{"class":294,"line":417},[292,4112,4084],{"class":4083},[292,4114,803],{"class":312},[292,4116,4117],{"class":302},"hrr_weight ",[292,4119,338],{"class":337},[292,4121,4122],{"class":1969}," 0.25\n",[292,4124,4125,4127,4129,4132,4134],{"class":294,"line":422},[292,4126,4084],{"class":4083},[292,4128,803],{"class":312},[292,4130,4131],{"class":302},"semantic_weight ",[292,4133,338],{"class":337},[292,4135,3829],{"class":1969},[292,4137,4138,4141,4143,4147],{"class":294,"line":435},[292,4139,4140],{"class":298},"    elif",[292,4142,2814],{"class":298},[292,4144,4146],{"class":4145},"sbIxs"," _HAS_NUMPY",[292,4148,2795],{"class":312},[292,4150,4151],{"class":294,"line":462},[292,4152,4153],{"class":486},"        # HRR недоступен: 0.2 → FTS +0.1, Semantic +0.1\n",[292,4155,4156,4158,4160,4162,4164],{"class":294,"line":473},[292,4157,4084],{"class":4083},[292,4159,803],{"class":312},[292,4161,4089],{"class":302},[292,4163,338],{"class":337},[292,4165,4166],{"class":1969}," 0.40\n",[292,4168,4169,4171,4173,4175,4177],{"class":294,"line":490},[292,4170,4084],{"class":4083},[292,4172,803],{"class":312},[292,4174,4103],{"class":302},[292,4176,338],{"class":337},[292,4178,4179],{"class":1969}," 0.20\n",[292,4181,4182,4184,4186,4188,4190],{"class":294,"line":495},[292,4183,4084],{"class":4083},[292,4185,803],{"class":312},[292,4187,4117],{"class":302},[292,4189,338],{"class":337},[292,4191,3829],{"class":1969},[292,4193,4194,4196,4198,4200,4202],{"class":294,"line":673},[292,4195,4084],{"class":4083},[292,4197,803],{"class":312},[292,4199,4131],{"class":302},[292,4201,338],{"class":337},[292,4203,4166],{"class":1969},[16,4205,4206],{},"Graceful degradation: система работает и без эмбеддингов (FTS + Jaccard), и без HRR (FTS + Jaccard + Semantic). Но полная четвёрка — оптимальный режим.",[11,4208,4210],{"id":4209},"практические-грабли","Практические грабли",[165,4212,4214],{"id":4213},"_1-fts5-and-слишком-строго","1. FTS5 AND — слишком строго",[16,4216,4217],{},"Запрос «компактный формат сообщений» требует наличия всех трёх слов. Если факт записан как «лаконичные ответы» — FTS5 молчит. Semantic спасает, но только если модель загружена.",[16,4219,4220,4223],{},[175,4221,4222],{},"Решение:"," не полагаться на FTS5 как единственный канал. Всегда держать semantic включённым.",[165,4225,4227],{"id":4226},"_2-missing-semantic-vectors-после-обновления","2. Missing semantic vectors после обновления",[16,4229,4230,4231,4234],{},"Агент обновился, но работает старый процесс. Новые факты записываются без ",[202,4232,4233],{},"semantic_vector",". Симптом: русские запросы возвращают пустой результат.",[283,4236,4238],{"className":3162,"code":4237,"language":3164,"meta":288,"style":288},"SELECT COUNT(*) FROM facts WHERE semantic_vector IS NULL;\n",[202,4239,4240],{"__ignoreMap":288},[292,4241,4242,4244,4248,4250,4253,4256,4258,4261,4263,4266,4269,4272],{"class":294,"line":295},[292,4243,3921],{"class":298},[292,4245,4247],{"class":4246},"sRaXx"," COUNT",[292,4249,344],{"class":302},[292,4251,4252],{"class":337},"*",[292,4254,4255],{"class":302},") ",[292,4257,3927],{"class":298},[292,4259,4260],{"class":302}," facts ",[292,4262,3932],{"class":298},[292,4264,4265],{"class":302}," semantic_vector ",[292,4267,4268],{"class":298},"IS",[292,4270,4271],{"class":298}," NULL",[292,4273,3957],{"class":302},[16,4275,4276],{},"Если > 0 — запустить backfill:",[283,4278,4280],{"className":285,"code":4279,"language":287,"meta":288,"style":288},"import embedder, sqlite3\n\nconn = sqlite3.connect(db_path)  # путь к вашей базе\nrows = conn.execute('SELECT fact_id, content FROM facts WHERE semantic_vector IS NULL').fetchall()\n\nfor fid, content in rows:\n    vec = embedder.embed_text(content)\n    conn.execute('UPDATE facts SET semantic_vector = ? WHERE fact_id = ?',\n                 (embedder.vector_to_bytes(vec), fid))\n\nconn.commit()\n",[202,4281,4282,4293,4297,4322,4349,4353,4374,4394,4410,4434,4438],{"__ignoreMap":288},[292,4283,4284,4286,4288,4290],{"class":294,"line":295},[292,4285,306],{"class":298},[292,4287,3101],{"class":302},[292,4289,313],{"class":312},[292,4291,4292],{"class":302}," sqlite3\n",[292,4294,4295],{"class":294,"line":324},[292,4296,328],{"emptyLinePlaceholder":327},[292,4298,4299,4302,4304,4307,4309,4312,4314,4317,4319],{"class":294,"line":331},[292,4300,4301],{"class":302},"conn ",[292,4303,338],{"class":337},[292,4305,4306],{"class":302}," sqlite3",[292,4308,803],{"class":312},[292,4310,4311],{"class":341},"connect",[292,4313,344],{"class":312},[292,4315,4316],{"class":302},"db_path",[292,4318,2982],{"class":312},[292,4320,4321],{"class":486},"  # путь к вашей базе\n",[292,4323,4324,4327,4329,4332,4334,4337,4339,4342,4344,4347],{"class":294,"line":365},[292,4325,4326],{"class":302},"rows ",[292,4328,338],{"class":337},[292,4330,4331],{"class":302}," conn",[292,4333,803],{"class":312},[292,4335,4336],{"class":341},"execute",[292,4338,344],{"class":312},[292,4340,4341],{"class":347},"'SELECT fact_id, content FROM facts WHERE semantic_vector IS NULL'",[292,4343,1390],{"class":312},[292,4345,4346],{"class":341},"fetchall",[292,4348,809],{"class":312},[292,4350,4351],{"class":294,"line":391},[292,4352,328],{"emptyLinePlaceholder":327},[292,4354,4355,4358,4361,4363,4366,4369,4372],{"class":294,"line":417},[292,4356,4357],{"class":298},"for",[292,4359,4360],{"class":302}," fid",[292,4362,313],{"class":312},[292,4364,4365],{"class":302}," content ",[292,4367,4368],{"class":298},"in",[292,4370,4371],{"class":302}," rows",[292,4373,2795],{"class":312},[292,4375,4376,4378,4380,4382,4384,4387,4389,4392],{"class":294,"line":422},[292,4377,3012],{"class":302},[292,4379,338],{"class":337},[292,4381,3101],{"class":302},[292,4383,803],{"class":312},[292,4385,4386],{"class":341},"embed_text",[292,4388,344],{"class":312},[292,4390,4391],{"class":302},"content",[292,4393,362],{"class":312},[292,4395,4396,4399,4401,4403,4405,4408],{"class":294,"line":435},[292,4397,4398],{"class":302},"    conn",[292,4400,803],{"class":312},[292,4402,4336],{"class":341},[292,4404,344],{"class":312},[292,4406,4407],{"class":347},"'UPDATE facts SET semantic_vector = ? WHERE fact_id = ?'",[292,4409,595],{"class":312},[292,4411,4412,4415,4418,4420,4423,4425,4427,4430,4432],{"class":294,"line":462},[292,4413,4414],{"class":312},"                 (",[292,4416,4417],{"class":302},"embedder",[292,4419,803],{"class":312},[292,4421,4422],{"class":341},"vector_to_bytes",[292,4424,344],{"class":312},[292,4426,3068],{"class":302},[292,4428,4429],{"class":312},"),",[292,4431,4360],{"class":302},[292,4433,3642],{"class":312},[292,4435,4436],{"class":294,"line":473},[292,4437,328],{"emptyLinePlaceholder":327},[292,4439,4440,4443,4445,4448],{"class":294,"line":490},[292,4441,4442],{"class":302},"conn",[292,4444,803],{"class":312},[292,4446,4447],{"class":341},"commit",[292,4449,809],{"class":312},[165,4451,4453],{"id":4452},"_3-fastembed-pooling-change","3. fastembed pooling change",[16,4455,4456],{},"Версия 0.8.0+ меняет pooling с CLS на mean. Старые векторы несовместимы с новыми. Решение: полный re-embed всех фактов после обновления fastembed.",[165,4458,4460],{"id":4459},"_4-onnx-memory-leak-это-не-leak","4. ONNX memory leak (это не leak)",[16,4462,4463,4465],{},[202,4464,2730],{}," освобождает веса, но не ONNX Runtime pools. Это не утечка — так устроен ONNX. 481 МБ residual — норма. Не пытайтесь «починить».",[165,4467,4469],{"id":4468},"_5-rss-used-memory","5. RSS ≠ used memory",[16,4471,4472,4475],{},[202,4473,4474],{},"ru_maxrss"," показывает пик, а не текущее потребление. Для точного измерения:",[283,4477,4479],{"className":285,"code":4478,"language":287,"meta":288,"style":288},"def current_rss_mb():\n    with open('\u002Fproc\u002Fself\u002Fstatm') as f:\n        pages = int(f.read().split()[1])\n    return pages * 4096 \u002F 1024 \u002F 1024\n",[202,4480,4481,4490,4513,4546],{"__ignoreMap":288},[292,4482,4483,4485,4488],{"class":294,"line":295},[292,4484,860],{"class":298},[292,4486,4487],{"class":863}," current_rss_mb",[292,4489,2892],{"class":312},[292,4491,4492,4495,4498,4500,4503,4505,4508,4511],{"class":294,"line":324},[292,4493,4494],{"class":298},"    with",[292,4496,4497],{"class":2860}," open",[292,4499,344],{"class":312},[292,4501,4502],{"class":347},"'\u002Fproc\u002Fself\u002Fstatm'",[292,4504,2982],{"class":312},[292,4506,4507],{"class":298}," as",[292,4509,4510],{"class":302}," f",[292,4512,2795],{"class":312},[292,4514,4515,4518,4520,4522,4524,4527,4529,4532,4535,4538,4541,4544],{"class":294,"line":331},[292,4516,4517],{"class":302},"        pages ",[292,4519,338],{"class":337},[292,4521,3372],{"class":1057},[292,4523,344],{"class":312},[292,4525,4526],{"class":302},"f",[292,4528,803],{"class":312},[292,4530,4531],{"class":341},"read",[292,4533,4534],{"class":312},"().",[292,4536,4537],{"class":341},"split",[292,4539,4540],{"class":312},"()[",[292,4542,4543],{"class":1969},"1",[292,4545,789],{"class":312},[292,4547,4548,4550,4553,4555,4558,4560,4562,4564],{"class":294,"line":365},[292,4549,878],{"class":298},[292,4551,4552],{"class":302}," pages ",[292,4554,4252],{"class":337},[292,4556,4557],{"class":1969}," 4096",[292,4559,3502],{"class":337},[292,4561,3377],{"class":1969},[292,4563,3502],{"class":337},[292,4565,4566],{"class":1969}," 1024\n",[11,4568,4570],{"id":4569},"web-интерфейс-для-просмотра-фактов","Web-интерфейс для просмотра фактов",[16,4572,4573],{},"Для отладки я сделал standalone htmx-приложение на stdlib http.server. Тёмная тема, моноширинный шрифт, FTS5-поиск, inline-редактирование, кнопки feedback, цветовые бейджи по категориям. Zero dependencies — только Python stdlib. Запускается одной командой, слушает на локальном порту.",[11,4575,4018],{"id":4576},"итого",[16,4578,4579],{},"Четыре стратегии поиска в одном SQLite-файле. 680 МБ RSS при работающей модели. Lazy-load, graceful degradation, выгрузка при shutdown. Никаких внешних сервисов, никаких docker-compose, никаких Pinecone API-ключей.",[16,4581,4582],{},"Для хобби-проекта на Potato VPS — это единственный адекватный вариант. Не потому что «лучше ChromaDB», а потому что ChromaDB не влезает в скромный объём RAM, а Pinecone — это чужой компьютер.",[16,4584,4585],{},"Свой плагин на SQLite — это контроль. Контроль над памятью, над индексацией, над lifecycle модели. И когда в 3 часа ночи OOM-killer стучится — ты точно знаешь, кто виноват и что делать.",[4587,4588],"hr",{},[16,4590,4591],{},[4592,4593,4594,4595,4597],"em",{},"Плагин ",[202,4596,2473],{}," — часть проекта Hermes Agent.",[1756,4599,4600],{},"html pre.shiki code .slTIY, html code.shiki .slTIY{--shiki-light:#24292E;--shiki-dark:#CDD6F4}html pre.shiki code .s_Q3D, html code.shiki .s_Q3D{--shiki-light:#D73A49;--shiki-dark:#94E2D5}html pre.shiki code .sNSVI, html code.shiki .sNSVI{--shiki-light:#005CC5;--shiki-dark:#FAB387}html pre.shiki code .s_QEy, html code.shiki .s_QEy{--shiki-light:#24292E;--shiki-dark:#9399B2}html pre.shiki code .sO2U0, html code.shiki .sO2U0{--shiki-light:#24292E;--shiki-light-font-style:inherit;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic}html pre.shiki code .smIoM, html code.shiki .smIoM{--shiki-light:#005CC5;--shiki-light-font-style:inherit;--shiki-dark:#CBA6F7;--shiki-dark-font-style:italic}html pre.shiki code .saXKZ, html code.shiki .saXKZ{--shiki-light:#D73A49;--shiki-dark:#CBA6F7}html pre.shiki code .siMrf, html code.shiki .siMrf{--shiki-light:#6F42C1;--shiki-light-font-style:inherit;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic}html pre.shiki code .sG7gF, html code.shiki .sG7gF{--shiki-light:#032F62;--shiki-dark:#A6E3A1}html pre.shiki code .sPY-v, html code.shiki .sPY-v{--shiki-light:#005CC5;--shiki-light-font-style:inherit;--shiki-dark:#FAB387;--shiki-dark-font-style:italic}html pre.shiki code .sPNDc, html code.shiki .sPNDc{--shiki-light:#24292E;--shiki-dark:#89B4FA}html pre.shiki code .skkvY, html code.shiki .skkvY{--shiki-light:#6A737D;--shiki-light-font-style:inherit;--shiki-dark:#9399B2;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-dMd, html code.shiki .s-dMd{--shiki-light:#E36209;--shiki-light-font-style:inherit;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic}html pre.shiki code .s7pD5, html code.shiki .s7pD5{--shiki-light:#24292E;--shiki-light-font-style:inherit;--shiki-dark:#F38BA8;--shiki-dark-font-style:italic}html pre.shiki code .sG_o1, html code.shiki .sG_o1{--shiki-light:#005CC5;--shiki-light-font-style:inherit;--shiki-dark:#F38BA8;--shiki-dark-font-style:italic}html pre.shiki code .sbIxs, html code.shiki .sbIxs{--shiki-light:#005CC5;--shiki-dark:#CDD6F4}html pre.shiki code .sRaXx, html code.shiki .sRaXx{--shiki-light:#005CC5;--shiki-light-font-style:inherit;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic}",{"title":288,"searchDepth":324,"depth":324,"links":4602},[4603,4604,4605,4611,4612,4615,4616,4617,4618,4619,4620,4621,4628,4629],{"id":2477,"depth":324,"text":2478},{"id":2571,"depth":324,"text":2572},{"id":2586,"depth":324,"text":2587,"children":4606},[4607,4608,4609,4610],{"id":2590,"depth":331,"text":2591},{"id":2597,"depth":331,"text":2598},{"id":2604,"depth":331,"text":2605},{"id":2611,"depth":331,"text":2612},{"id":2618,"depth":324,"text":2619},{"id":2734,"depth":324,"text":2735,"children":4613},[4614],{"id":3083,"depth":331,"text":3084},{"id":3155,"depth":324,"text":3156},{"id":3337,"depth":324,"text":3338},{"id":3760,"depth":324,"text":3761},{"id":3869,"depth":324,"text":3870},{"id":3963,"depth":324,"text":3964},{"id":4034,"depth":324,"text":4035},{"id":4209,"depth":324,"text":4210,"children":4622},[4623,4624,4625,4626,4627],{"id":4213,"depth":331,"text":4214},{"id":4226,"depth":331,"text":4227},{"id":4452,"depth":331,"text":4453},{"id":4459,"depth":331,"text":4460},{"id":4468,"depth":331,"text":4469},{"id":4569,"depth":324,"text":4570},{"id":4576,"depth":324,"text":4018},"Как запустить гибридный поиск (FTS5 + Jaccard + HRR + fastembed MiniLM-L12-v2) на дешёвом VPS. Lazy-loading модели, ~680 MB RSS, выгрузка при shutdown. Почему не ChromaDB и не Pinecone — а свой плагин на SQLite.",{},"\u002Fblog\u002Fholographic-memory-potato-vps",{"title":2456,"description":4630},"blog\u002Fholographic-memory-potato-vps",[4636,4637,4638,4639,4640,4641,4642,4643,4644,4645,4646,4647,4648,4649,4650,4651],"ai-memory","vector-search","embeddings","sqlite","fts5","fastembed","semantic-search","self-hosted","potato-vps","rag","hybrid-search","nlp","mini-lm","hrr","jaccard","performance-optimization","7QeOSdwwmzCmc7K1aKbRemQMoO0eS3ZCz91ujdION3M",{"id":4654,"title":4655,"body":4656,"date":2440,"description":6000,"extension":1786,"meta":6001,"navigation":327,"path":6002,"readingTime":1789,"seo":6003,"stem":6004,"tags":6005,"__hash__":6009},"articles\u002Fblog\u002Fself-hosted-ntfy-notifications.md","Самостоятельный ntfy: свой push-сервер уведомлений",{"type":8,"value":4657,"toc":5964},[4658,4661,4664,4673,4677,4680,4698,4709,4713,4719,4808,4811,4823,4835,4843,4853,4892,4896,4908,4939,4946,4950,4956,4962,4965,4993,4997,5000,5021,5027,5030,5046,5056,5060,5063,5095,5098,5128,5131,5186,5190,5193,5208,5211,5225,5228,5312,5319,5323,5326,5331,5448,5454,5467,5471,5475,5488,5492,5495,5501,5504,5508,5522,5526,5529,5561,5564,5568,5572,5585,5591,5651,5655,5658,5663,5698,5702,5705,5728,5731,5735,5738,5752,5755,5767,5771,5775,5778,5828,5832,5842,5846,5849,5862,5866,5869,5903,5907,5911,5914,5918,5921,5925,5928,5932,5935,5939,5946,5949,5951,5958,5961],[1807,4659,4655],{"id":4660},"самостоятельный-ntfy-свой-push-сервер-уведомлений",[16,4662,4663],{},"На моём сервере крутится AI-агент (Hermes), мониторинг, блог, и куча мелких сервисов. Мне нужно было получать уведомления от всего этого хозяйства на телефон — не через Telegram Bot API (с его rate limit и зависимостью от серверов Telegram), а через свой канал, который я контролирую.",[16,4665,4666,4667,4672],{},"Решение — ",[1852,4668,4671],{"href":4669,"rel":4670},"https:\u002F\u002Fntfy.sh",[1856],"ntfy",": лёгкий Go-сервер, HTTP pub-sub, Android\u002FiOS приложения, и zero vendor lock-in. Вот как я его поднял и к чему пришёл.",[11,4674,4676],{"id":4675},"установка","Установка",[16,4678,4679],{},"На Debian\u002FUbuntu всё предельно просто:",[283,4681,4683],{"className":1861,"code":4682,"language":1863,"meta":288,"style":288},"apt-get install -y ntfy\n",[202,4684,4685],{"__ignoreMap":288},[292,4686,4687,4690,4692,4695],{"class":294,"line":295},[292,4688,4689],{"class":863},"apt-get",[292,4691,1873],{"class":347},[292,4693,4694],{"class":1941}," -y",[292,4696,4697],{"class":347}," ntfy\n",[16,4699,4700,4701,4704,4705,4708],{},"Пакет создаёт пользователя ",[202,4702,4703],{},"_ntfy",", systemd-сервис и конфиг по умолчанию. Но «по умолчанию» — это listen на ",[202,4706,4707],{},":80"," и без авторизации. Для production нужно поменять.",[11,4710,4712],{"id":4711},"конфигурация","Конфигурация",[16,4714,4715,4716,887],{},"Файл ",[202,4717,4718],{},"\u002Fetc\u002Fntfy\u002Fserver.yml",[283,4720,4724],{"className":4721,"code":4722,"language":4723,"meta":288,"style":288},"language-yaml shiki shiki-themes github-light catppuccin-mocha","base-url: \"https:\u002F\u002Fntfy.example.com\"\nlisten-http: \"127.0.0.1:8080\"\nbehind-proxy: true\ncache-file: \u002Fvar\u002Fcache\u002Fntfy\u002Fcache.db\ncache-duration: \"24h\"\nauth-file: \u002Fvar\u002Flib\u002Fntfy\u002Fuser.db\nauth-default-access: \"deny-all\"\nlog-level: info\n","yaml",[202,4725,4726,4738,4748,4758,4768,4778,4788,4798],{"__ignoreMap":288},[292,4727,4728,4732,4735],{"class":294,"line":295},[292,4729,4731],{"class":4730},"sEb-F","base-url",[292,4733,887],{"class":4734},"sgPNX",[292,4736,4737],{"class":347}," \"https:\u002F\u002Fntfy.example.com\"\n",[292,4739,4740,4743,4745],{"class":294,"line":324},[292,4741,4742],{"class":4730},"listen-http",[292,4744,887],{"class":4734},[292,4746,4747],{"class":347}," \"127.0.0.1:8080\"\n",[292,4749,4750,4753,4755],{"class":294,"line":331},[292,4751,4752],{"class":4730},"behind-proxy",[292,4754,887],{"class":4734},[292,4756,4757],{"class":1969}," true\n",[292,4759,4760,4763,4765],{"class":294,"line":365},[292,4761,4762],{"class":4730},"cache-file",[292,4764,887],{"class":4734},[292,4766,4767],{"class":347}," \u002Fvar\u002Fcache\u002Fntfy\u002Fcache.db\n",[292,4769,4770,4773,4775],{"class":294,"line":391},[292,4771,4772],{"class":4730},"cache-duration",[292,4774,887],{"class":4734},[292,4776,4777],{"class":347}," \"24h\"\n",[292,4779,4780,4783,4785],{"class":294,"line":417},[292,4781,4782],{"class":4730},"auth-file",[292,4784,887],{"class":4734},[292,4786,4787],{"class":347}," \u002Fvar\u002Flib\u002Fntfy\u002Fuser.db\n",[292,4789,4790,4793,4795],{"class":294,"line":422},[292,4791,4792],{"class":4730},"auth-default-access",[292,4794,887],{"class":4734},[292,4796,4797],{"class":347}," \"deny-all\"\n",[292,4799,4800,4803,4805],{"class":294,"line":435},[292,4801,4802],{"class":4730},"log-level",[292,4804,887],{"class":4734},[292,4806,4807],{"class":347}," info\n",[16,4809,4810],{},"Разберу ключевые моменты:",[16,4812,4813,4818,4819,4822],{},[175,4814,4815],{},[202,4816,4817],{},"listen-http: \"127.0.0.1:8080\""," — слушаем только на localhost. Весь внешний трафик идёт через reverse proxy. Если поставить ",[202,4820,4821],{},"0.0.0.0:80",", ntfy будет доступен напрямую, минуя SSL и rate limiting.",[16,4824,4825,4830,4831,4834],{},[175,4826,4827],{},[202,4828,4829],{},"behind-proxy: true"," — обязательно, если вы за Caddy\u002Fnginx. Без этого ntfy не увидит реальные IP-адреса подписчиков (будет видеть ",[202,4832,4833],{},"127.0.0.1","), и rate limiting не будет работать корректно.",[16,4836,4837,4842],{},[175,4838,4839],{},[202,4840,4841],{},"auth-default-access: \"deny-all\""," — по умолчанию всё закрыто. Без этого любой, кто угадает имя топика, может читать и писать в него. Для публичного ntfy.sh это нормально, для self-hosted — нет.",[16,4844,4845,4848,4849,4852],{},[175,4846,4847],{},"Питфолл",": не пишите конфиг через ",[202,4850,4851],{},"cat > \u002Fetc\u002Fntfy\u002Fserver.yml"," из терминала — некоторые security-сканеры блокируют heredoc в bash. Используйте Python:",[283,4854,4856],{"className":285,"code":4855,"language":287,"meta":288,"style":288},"import pathlib\npathlib.Path('\u002Fetc\u002Fntfy\u002Fserver.yml').write_text(config_yaml)\n",[202,4857,4858,4865],{"__ignoreMap":288},[292,4859,4860,4862],{"class":294,"line":295},[292,4861,306],{"class":298},[292,4863,4864],{"class":302}," pathlib\n",[292,4866,4867,4870,4872,4875,4877,4880,4882,4885,4887,4890],{"class":294,"line":324},[292,4868,4869],{"class":302},"pathlib",[292,4871,803],{"class":312},[292,4873,4874],{"class":341},"Path",[292,4876,344],{"class":312},[292,4878,4879],{"class":347},"'\u002Fetc\u002Fntfy\u002Fserver.yml'",[292,4881,1390],{"class":312},[292,4883,4884],{"class":341},"write_text",[292,4886,344],{"class":312},[292,4888,4889],{"class":302},"config_yaml",[292,4891,362],{"class":312},[11,4893,4895],{"id":4894},"права-на-директории","Права на директории",[16,4897,4898,4899,4901,4902,521,4905,887],{},"Пакетный ",[202,4900,4703],{}," пользователь должен иметь права на запись в ",[202,4903,4904],{},"\u002Fvar\u002Fcache\u002Fntfy",[202,4906,4907],{},"\u002Fvar\u002Flib\u002Fntfy",[283,4909,4911],{"className":1861,"code":4910,"language":1863,"meta":288,"style":288},"mkdir -p \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\nchown _ntfy:_ntfy \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\n",[202,4912,4913,4927],{"__ignoreMap":288},[292,4914,4915,4918,4921,4924],{"class":294,"line":295},[292,4916,4917],{"class":863},"mkdir",[292,4919,4920],{"class":1941}," -p",[292,4922,4923],{"class":347}," \u002Fvar\u002Fcache\u002Fntfy",[292,4925,4926],{"class":347}," \u002Fvar\u002Flib\u002Fntfy\n",[292,4928,4929,4932,4935,4937],{"class":294,"line":324},[292,4930,4931],{"class":863},"chown",[292,4933,4934],{"class":347}," _ntfy:_ntfy",[292,4936,4923],{"class":347},[292,4938,4926],{"class":347},[16,4940,4941,4942,4945],{},"Без этого сервис упадёт с ",[202,4943,4944],{},"\"unable to open database file\"",". Классика.",[11,4947,4949],{"id":4948},"caddy-reverse-proxy","Caddy reverse proxy",[16,4951,4952,4953,4955],{},"У меня Caddy слушает на ",[202,4954,4707],{}," (Cloudflare handles SSL termination снаружи). Конфиг для ntfy:",[283,4957,4960],{"className":4958,"code":4959,"language":2578},[2576],"@ntfy host ntfy.example.com\nhandle @ntfy {\n    reverse_proxy localhost:8080\n}\n",[202,4961,4959],{"__ignoreMap":288},[16,4963,4964],{},"Два нюанса:",[1824,4966,4967,4980],{},[1827,4968,4969,4972,4973,521,4976,4979],{},[175,4970,4971],{},"WebSocket"," — ntfy использует WebSocket для real-time подписок. Caddy проксирует его автоматически, но если вы за nginx, нужно явно прописать ",[202,4974,4975],{},"Upgrade",[202,4977,4978],{},"Connection"," заголовки.",[1827,4981,4982,4985,4986,4988,4989,4992],{},[175,4983,4984],{},"Cloudflare SSL:Flexible"," — Cloudflare terminates SSL на своём edge, а до origin идёт plain HTTP. Поэтому Caddy слушает на ",[202,4987,4707],{},", а не ",[202,4990,4991],{},":443",". Если поставить SSL:Full, нужно ещё и сертификат на origin настраивать — для self-hosted хобби-проекта это overkill.",[11,4994,4996],{"id":4995},"пользователи-и-токены","Пользователи и токены",[16,4998,4999],{},"Создаём первого пользователя:",[283,5001,5003],{"className":1861,"code":5002,"language":1863,"meta":288,"style":288},"ntfy user add --role=admin admin\n",[202,5004,5005],{"__ignoreMap":288},[292,5006,5007,5009,5012,5015,5018],{"class":294,"line":295},[292,5008,4671],{"class":863},[292,5010,5011],{"class":347}," user",[292,5013,5014],{"class":347}," add",[292,5016,5017],{"class":1941}," --role=admin",[292,5019,5020],{"class":347}," admin\n",[16,5022,5023,5024,1390],{},"Пароль запрашивается интерактивно. После этого все запросы требуют авторизации (из-за ",[202,5025,5026],{},"deny-all",[16,5028,5029],{},"Для программного доступа (скрипты, AI-агент) лучше использовать токены, а не логин\u002Fпароль:",[283,5031,5033],{"className":1861,"code":5032,"language":1863,"meta":288,"style":288},"ntfy token add admin\n",[202,5034,5035],{"__ignoreMap":288},[292,5036,5037,5039,5042,5044],{"class":294,"line":295},[292,5038,4671],{"class":863},[292,5040,5041],{"class":347}," token",[292,5043,5014],{"class":347},[292,5045,5020],{"class":347},[16,5047,5048,5049,5052,5053,803],{},"Токен выглядит как ",[202,5050,5051],{},"tk_abc123...",". Передаётся в заголовке ",[202,5054,5055],{},"Authorization: Bearer tk_abc123...",[11,5057,5059],{"id":5058},"публикация-уведомлений","Публикация уведомлений",[16,5061,5062],{},"Отправить сообщение — один HTTP POST:",[283,5064,5066],{"className":1861,"code":5065,"language":1863,"meta":288,"style":288},"curl -u admin:password \\\n  -d \"Сервер перегрелся! CPU 95%\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[202,5067,5068,5080,5090],{"__ignoreMap":288},[292,5069,5070,5072,5075,5078],{"class":294,"line":295},[292,5071,2175],{"class":863},[292,5073,5074],{"class":1941}," -u",[292,5076,5077],{"class":347}," admin:password",[292,5079,1949],{"class":1948},[292,5081,5082,5085,5088],{"class":294,"line":324},[292,5083,5084],{"class":1941},"  -d",[292,5086,5087],{"class":347}," \"Сервер перегрелся! CPU 95%\"",[292,5089,1949],{"class":1948},[292,5091,5092],{"class":294,"line":331},[292,5093,5094],{"class":347},"  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[16,5096,5097],{},"Или с токеном:",[283,5099,5101],{"className":1861,"code":5100,"language":1863,"meta":288,"style":288},"curl -H \"Authorization: Bearer tk_abc123...\" \\\n  -d \"Сервер перегрелся!\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[202,5102,5103,5115,5124],{"__ignoreMap":288},[292,5104,5105,5107,5110,5113],{"class":294,"line":295},[292,5106,2175],{"class":863},[292,5108,5109],{"class":1941}," -H",[292,5111,5112],{"class":347}," \"Authorization: Bearer tk_abc123...\"",[292,5114,1949],{"class":1948},[292,5116,5117,5119,5122],{"class":294,"line":324},[292,5118,5084],{"class":1941},[292,5120,5121],{"class":347}," \"Сервер перегрелся!\"",[292,5123,1949],{"class":1948},[292,5125,5126],{"class":294,"line":331},[292,5127,5094],{"class":347},[16,5129,5130],{},"С заголовками для кастомизации:",[283,5132,5134],{"className":1861,"code":5133,"language":1863,"meta":288,"style":288},"curl -H \"Authorization: Bearer tk_abc123...\" \\\n  -H \"Title: Мониторинг\" \\\n  -H \"Priority: high\" \\\n  -H \"Tags: warning,fire\" \\\n  -d \"CPU 95%, RAM 90%\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[202,5135,5136,5146,5155,5164,5173,5182],{"__ignoreMap":288},[292,5137,5138,5140,5142,5144],{"class":294,"line":295},[292,5139,2175],{"class":863},[292,5141,5109],{"class":1941},[292,5143,5112],{"class":347},[292,5145,1949],{"class":1948},[292,5147,5148,5150,5153],{"class":294,"line":324},[292,5149,2204],{"class":1941},[292,5151,5152],{"class":347}," \"Title: Мониторинг\"",[292,5154,1949],{"class":1948},[292,5156,5157,5159,5162],{"class":294,"line":331},[292,5158,2204],{"class":1941},[292,5160,5161],{"class":347}," \"Priority: high\"",[292,5163,1949],{"class":1948},[292,5165,5166,5168,5171],{"class":294,"line":365},[292,5167,2204],{"class":1941},[292,5169,5170],{"class":347}," \"Tags: warning,fire\"",[292,5172,1949],{"class":1948},[292,5174,5175,5177,5180],{"class":294,"line":391},[292,5176,5084],{"class":1941},[292,5178,5179],{"class":347}," \"CPU 95%, RAM 90%\"",[292,5181,1949],{"class":1948},[292,5183,5184],{"class":294,"line":417},[292,5185,5094],{"class":347},[11,5187,5189],{"id":5188},"подписка-на-уведомления","Подписка на уведомления",[16,5191,5192],{},"CLI:",[283,5194,5196],{"className":1861,"code":5195,"language":1863,"meta":288,"style":288},"ntfy subscribe ntfy.example.com\u002Fmonitoring\n",[202,5197,5198],{"__ignoreMap":288},[292,5199,5200,5202,5205],{"class":294,"line":295},[292,5201,4671],{"class":863},[292,5203,5204],{"class":347}," subscribe",[292,5206,5207],{"class":347}," ntfy.example.com\u002Fmonitoring\n",[16,5209,5210],{},"HTTP (long-polling):",[283,5212,5214],{"className":1861,"code":5213,"language":1863,"meta":288,"style":288},"curl -s ntfy.example.com\u002Fmonitoring\u002Fjson\n",[202,5215,5216],{"__ignoreMap":288},[292,5217,5218,5220,5222],{"class":294,"line":295},[292,5219,2175],{"class":863},[292,5221,2178],{"class":1941},[292,5223,5224],{"class":347}," ntfy.example.com\u002Fmonitoring\u002Fjson\n",[16,5226,5227],{},"WebSocket (для интеграций):",[283,5229,5233],{"className":5230,"code":5231,"language":5232,"meta":288,"style":288},"language-javascript shiki shiki-themes github-light catppuccin-mocha","const ws = new WebSocket('wss:\u002F\u002Fntfy.example.com\u002Fmonitoring\u002Fws');\nws.onmessage = (e) => console.log(JSON.parse(e.data));\n","javascript",[202,5234,5235,5261],{"__ignoreMap":288},[292,5236,5237,5240,5243,5245,5249,5252,5254,5257,5259],{"class":294,"line":295},[292,5238,5239],{"class":298},"const",[292,5241,5242],{"class":4145}," ws",[292,5244,2770],{"class":337},[292,5246,5248],{"class":5247},"sf7P5"," new",[292,5250,5251],{"class":863}," WebSocket",[292,5253,344],{"class":302},[292,5255,5256],{"class":347},"'wss:\u002F\u002Fntfy.example.com\u002Fmonitoring\u002Fws'",[292,5258,2982],{"class":302},[292,5260,3957],{"class":312},[292,5262,5263,5266,5268,5271,5273,5276,5279,5281,5284,5287,5289,5292,5294,5297,5299,5302,5305,5307,5310],{"class":294,"line":324},[292,5264,5265],{"class":302},"ws",[292,5267,803],{"class":4734},[292,5269,5270],{"class":863},"onmessage",[292,5272,2770],{"class":337},[292,5274,5275],{"class":312}," (",[292,5277,5278],{"class":353},"e",[292,5280,2982],{"class":312},[292,5282,5283],{"class":298}," =>",[292,5285,5286],{"class":302}," console",[292,5288,803],{"class":4734},[292,5290,5291],{"class":863},"log",[292,5293,344],{"class":302},[292,5295,5296],{"class":1969},"JSON",[292,5298,803],{"class":4734},[292,5300,5301],{"class":863},"parse",[292,5303,5304],{"class":302},"(e",[292,5306,803],{"class":4734},[292,5308,5309],{"class":302},"data))",[292,5311,3957],{"class":312},[16,5313,5314,5315,5318],{},"Android\u002FiOS приложение — просто добавляете сервер ",[202,5316,5317],{},"https:\u002F\u002Fntfy.example.com"," и подписываетесь на топики.",[11,5320,5322],{"id":5321},"интеграция-с-hermes-webhooks","Интеграция с Hermes: webhooks",[16,5324,5325],{},"Самое интересное — подключить ntfy к AI-агенту. У Hermes есть webhook-система, и nfy поддерживает actions — автоматические HTTP-запросы при получении сообщения.",[16,5327,5328,5329,887],{},"В ",[202,5330,4718],{},[283,5332,5334],{"className":4721,"code":5333,"language":4723,"meta":288,"style":288},"actions:\n  - action: \"webhook\"\n    label: \"Forward to Hermes\"\n    url: \"http:\u002F\u002Flocalhost:8644\u002Fwebhooks\u002Fntfy\"\n    headers:\n      Authorization: \"Bearer my-hermes-secret\"\n    body: |\n      {\n        \"topic\": \"{{ .Topic }}\",\n        \"message\": \"{{ .Message }}\",\n        \"title\": \"{{ .Title }}\",\n        \"sender\": \"{{ .Sender }}\",\n        \"time\": \"{{ .Time }}\"\n      }\n    topic: \"hermes\"\n",[202,5335,5336,5343,5356,5366,5376,5383,5393,5403,5408,5413,5418,5423,5428,5433,5438],{"__ignoreMap":288},[292,5337,5338,5341],{"class":294,"line":295},[292,5339,5340],{"class":4730},"actions",[292,5342,2795],{"class":4734},[292,5344,5345,5348,5351,5353],{"class":294,"line":324},[292,5346,5347],{"class":312},"  -",[292,5349,5350],{"class":4730}," action",[292,5352,887],{"class":4734},[292,5354,5355],{"class":347}," \"webhook\"\n",[292,5357,5358,5361,5363],{"class":294,"line":331},[292,5359,5360],{"class":4730},"    label",[292,5362,887],{"class":4734},[292,5364,5365],{"class":347}," \"Forward to Hermes\"\n",[292,5367,5368,5371,5373],{"class":294,"line":365},[292,5369,5370],{"class":4730},"    url",[292,5372,887],{"class":4734},[292,5374,5375],{"class":347}," \"http:\u002F\u002Flocalhost:8644\u002Fwebhooks\u002Fntfy\"\n",[292,5377,5378,5381],{"class":294,"line":391},[292,5379,5380],{"class":4730},"    headers",[292,5382,2795],{"class":4734},[292,5384,5385,5388,5390],{"class":294,"line":417},[292,5386,5387],{"class":4730},"      Authorization",[292,5389,887],{"class":4734},[292,5391,5392],{"class":347}," \"Bearer my-hermes-secret\"\n",[292,5394,5395,5398,5400],{"class":294,"line":422},[292,5396,5397],{"class":4730},"    body",[292,5399,887],{"class":4734},[292,5401,5402],{"class":298}," |\n",[292,5404,5405],{"class":294,"line":435},[292,5406,5407],{"class":347},"      {\n",[292,5409,5410],{"class":294,"line":462},[292,5411,5412],{"class":347},"        \"topic\": \"{{ .Topic }}\",\n",[292,5414,5415],{"class":294,"line":473},[292,5416,5417],{"class":347},"        \"message\": \"{{ .Message }}\",\n",[292,5419,5420],{"class":294,"line":490},[292,5421,5422],{"class":347},"        \"title\": \"{{ .Title }}\",\n",[292,5424,5425],{"class":294,"line":495},[292,5426,5427],{"class":347},"        \"sender\": \"{{ .Sender }}\",\n",[292,5429,5430],{"class":294,"line":673},[292,5431,5432],{"class":347},"        \"time\": \"{{ .Time }}\"\n",[292,5434,5435],{"class":294,"line":678},[292,5436,5437],{"class":347},"      }\n",[292,5439,5440,5443,5445],{"class":294,"line":710},[292,5441,5442],{"class":4730},"    topic",[292,5444,887],{"class":4734},[292,5446,5447],{"class":347}," \"hermes\"\n",[16,5449,5450,5451,5453],{},"Теперь каждое сообщение в топик ",[202,5452,2447],{}," автоматически пересылается в Hermes API. Агент может обработать уведомление и ответить.",[16,5455,5456,5459,5460,5462,5463,5466],{},[175,5457,5458],{},"Ловушка",": если Hermes отвечает в тот же топик ",[202,5461,2447],{},", ntfy снова вызывает webhook, Hermes снова отвечает — бесконечный цикл. Решение: отвечать в другой топик (например, ",[202,5464,5465],{},"hermes-responses","), или фильтровать по sender\u002Fheaders в обработчике.",[11,5468,5470],{"id":5469},"реальные-кейсы","Реальные кейсы",[165,5472,5474],{"id":5473},"мониторинг-сервера","Мониторинг сервера",[16,5476,5477,5478,5481,5482,5485,5486,803],{},"Cron-скрипт каждые 5 минут проверяет CPU, RAM, диск. Если значение порог — отправляем HTTP POST в ntfy. Приоритет ",[202,5479,5480],{},"high",", тег ",[202,5483,5484],{},"warning"," — и на телефоне сразу видно, что проблема. Скрипт занимает 5 строк bash, инициализация — один ",[202,5487,2175],{},[165,5489,5491],{"id":5490},"алёрты-от-ai-агента","Алёрты от AI-агента",[16,5493,5494],{},"Hermes запускает cron-задачу (ежедневный брифинг), и результат шлёт в ntfy:",[283,5496,5499],{"className":5497,"code":5498,"language":2578},[2576],"hermes cron job → Hermes API → POST ntfy.example.com\u002Fhermes\n",[202,5500,5498],{"__ignoreMap":288},[16,5502,5503],{},"Пользователь видит уведомление на телефоне, открывает — там саммари новостей, задач, статус сервисов.",[165,5505,5507],{"id":5506},"уведомления-о-деплоях","Уведомления о деплоях",[16,5509,5510,5511,5514,5515,5518,5519,5521],{},"CI\u002FCD pipeline (или простой скрипт) шлёт статус деплоя: тег ",[202,5512,5513],{},"white_check_mark"," для успеха, ",[202,5516,5517],{},"x"," для провала, приоритет ",[202,5520,5480],{}," для критичных ошибок. Один POST-запрос — и вы видите результат на телефоне, не открывая CI.",[11,5523,5525],{"id":5524},"почему-не-telegram","Почему не Telegram",[16,5527,5528],{},"Telegram Bot API — отличный вариант, и я его тоже использую. Но у self-hosted ntfy есть преимущества:",[3729,5530,5531,5537,5543,5549,5555],{},[1827,5532,5533,5536],{},[175,5534,5535],{},"Нет rate limit"," от Telegram (30 сообщений\u002Fсек на чат)",[1827,5538,5539,5542],{},[175,5540,5541],{},"Нет зависимости"," от серверов Telegram (они иногда падают)",[1827,5544,5545,5548],{},[175,5546,5547],{},"Полный контроль"," над данными (уведомления не проходят через Telegram)",[1827,5550,5551,5554],{},[175,5552,5553],{},"Нет необходимости"," в Bot Token и chat ID — просто HTTP POST",[1827,5556,5557,5560],{},[175,5558,5559],{},"WebSocket подписки"," встроены, без polling",[16,5562,5563],{},"Минусы: нет rich-контента (кнопки, inline keyboard), нет групповых чатов с историей, нет E2E шифрования. Для мониторинга и алертов — идеально. Для мессенджера — нет.",[11,5565,5567],{"id":5566},"безопасность-что-может-пойти-не-так","Безопасность: что может пойти не так",[165,5569,5571],{"id":5570},"публичные-топики","Публичные топики",[16,5573,5574,5575,5577,5578,5580,5581,5584],{},"Если ",[202,5576,4792],{}," не ",[202,5579,5026],{},", любой, кто угадает имя топика, может читать из него. Топик-имена — не секреты. ",[202,5582,5583],{},"ntfy subscribe ntfy.example.com\u002Fmy-secret-topic"," — это не защита, это security through obscurity.",[16,5586,5587,5588,5590],{},"Всегда ставьте ",[202,5589,4841],{}," и создавайте пользователей с явными правами на конкретные топики:",[283,5592,5594],{"className":1861,"code":5593,"language":1863,"meta":288,"style":288},"ntfy access admin monitoring rw\nntfy access admin hermes rw\nntfy access bot-hermes hermes rw\nntfy access bot-hermes monitoring ro\n",[202,5595,5596,5612,5625,5638],{"__ignoreMap":288},[292,5597,5598,5600,5603,5606,5609],{"class":294,"line":295},[292,5599,4671],{"class":863},[292,5601,5602],{"class":347}," access",[292,5604,5605],{"class":347}," admin",[292,5607,5608],{"class":347}," monitoring",[292,5610,5611],{"class":347}," rw\n",[292,5613,5614,5616,5618,5620,5623],{"class":294,"line":324},[292,5615,4671],{"class":863},[292,5617,5602],{"class":347},[292,5619,5605],{"class":347},[292,5621,5622],{"class":347}," hermes",[292,5624,5611],{"class":347},[292,5626,5627,5629,5631,5634,5636],{"class":294,"line":331},[292,5628,4671],{"class":863},[292,5630,5602],{"class":347},[292,5632,5633],{"class":347}," bot-hermes",[292,5635,5622],{"class":347},[292,5637,5611],{"class":347},[292,5639,5640,5642,5644,5646,5648],{"class":294,"line":365},[292,5641,4671],{"class":863},[292,5643,5602],{"class":347},[292,5645,5633],{"class":347},[292,5647,5608],{"class":347},[292,5649,5650],{"class":347}," ro\n",[165,5652,5654],{"id":5653},"rate-limiting","Rate limiting",[16,5656,5657],{},"ntfy имеет встроенный rate limiting (по умолчанию 250 сообщений в день на visitor). Для self-hosted с 1-2 пользователями — более чем достаточно. Но если вы шлете алерты каждые 10 секунд, можете упереться лимит.",[16,5659,5660,5661,887],{},"Настройка в ",[202,5662,4718],{},[283,5664,5666],{"className":4721,"code":5665,"language":4723,"meta":288,"style":288},"visitor-subscription-limit: 30\nvisitor-request-limit-burst: 60\nvisitor-request-limit-replenish: 5s\n",[202,5667,5668,5678,5688],{"__ignoreMap":288},[292,5669,5670,5673,5675],{"class":294,"line":295},[292,5671,5672],{"class":4730},"visitor-subscription-limit",[292,5674,887],{"class":4734},[292,5676,5677],{"class":1969}," 30\n",[292,5679,5680,5683,5685],{"class":294,"line":324},[292,5681,5682],{"class":4730},"visitor-request-limit-burst",[292,5684,887],{"class":4734},[292,5686,5687],{"class":1969}," 60\n",[292,5689,5690,5693,5695],{"class":294,"line":331},[292,5691,5692],{"class":4730},"visitor-request-limit-replenish",[292,5694,887],{"class":4734},[292,5696,5697],{"class":347}," 5s\n",[165,5699,5701],{"id":5700},"логирование","Логирование",[16,5703,5704],{},"ntfy пишет логи в stdout (systemd journal). Для продакшена стоит поднять логирование:",[283,5706,5708],{"className":4721,"code":5707,"language":4723,"meta":288,"style":288},"log-level: info\nlog-format: json\n",[202,5709,5710,5718],{"__ignoreMap":288},[292,5711,5712,5714,5716],{"class":294,"line":295},[292,5713,4802],{"class":4730},[292,5715,887],{"class":4734},[292,5717,4807],{"class":347},[292,5719,5720,5723,5725],{"class":294,"line":324},[292,5721,5722],{"class":4730},"log-format",[292,5724,887],{"class":4734},[292,5726,5727],{"class":347}," json\n",[16,5729,5730],{},"JSON-формат удобнее для парсинга в Loki\u002FELK, но для хобби-проекта достаточно text.",[11,5732,5734],{"id":5733},"миграция-и-бэкапы","Миграция и бэкапы",[16,5736,5737],{},"ntfy хранит всё в двух файлах:",[3729,5739,5740,5746],{},[1827,5741,5742,5745],{},[202,5743,5744],{},"\u002Fvar\u002Fcache\u002Fntfy\u002Fcache.db"," — кеш сообщений (SQLite)",[1827,5747,5748,5751],{},[202,5749,5750],{},"\u002Fvar\u002Flib\u002Fntfy\u002Fuser.db"," — пользователи и токены (SQLite)",[16,5753,5754],{},"Для бэкапа — просто копируйте эти файлы. Для миграции на другой сервер — перенесите файлы и конфиг.",[16,5756,5757,5759,5760,5762,5763,5766],{},[175,5758,4847],{},": при обновлении ntfy через apt, конфиг ",[202,5761,4718],{}," может быть перезаписан. Используйте ",[202,5764,5765],{},"dpkg --force-confold"," или бэкапите конфиг отдельно.",[11,5768,5770],{"id":5769},"траблшутинг","Траблшутинг",[165,5772,5774],{"id":5773},"ntfy-не-стартует-unable-to-open-database-file","ntfy не стартует: \"unable to open database file\"",[16,5776,5777],{},"Самая частая проблема. Проверьте:",[283,5779,5781],{"className":1861,"code":5780,"language":1863,"meta":288,"style":288},"ls -la \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\n# Должны принадлежать _ntfy:_ntfy\n\njournalctl -u ntfy --no-pager -n 20\n# Смотрим последние логи\n",[202,5782,5783,5795,5800,5804,5823],{"__ignoreMap":288},[292,5784,5785,5788,5791,5793],{"class":294,"line":295},[292,5786,5787],{"class":863},"ls",[292,5789,5790],{"class":1941}," -la",[292,5792,4923],{"class":347},[292,5794,4926],{"class":347},[292,5796,5797],{"class":294,"line":324},[292,5798,5799],{"class":486},"# Должны принадлежать _ntfy:_ntfy\n",[292,5801,5802],{"class":294,"line":331},[292,5803,328],{"emptyLinePlaceholder":327},[292,5805,5806,5809,5811,5814,5817,5820],{"class":294,"line":365},[292,5807,5808],{"class":863},"journalctl",[292,5810,5074],{"class":1941},[292,5812,5813],{"class":347}," ntfy",[292,5815,5816],{"class":1941}," --no-pager",[292,5818,5819],{"class":1941}," -n",[292,5821,5822],{"class":1969}," 20\n",[292,5824,5825],{"class":294,"line":391},[292,5826,5827],{"class":486},"# Смотрим последние логи\n",[165,5829,5831],{"id":5830},"уведомления-не-доходят-через-cloudflare","Уведомления не доходят через Cloudflare",[16,5833,5834,5835,5838,5839,803],{},"Проверьте, что Cloudflare не кеширует API-ответы. ntfy API endpoints (",[202,5836,5837],{},"\u002Fv1\u002F...",") не должны кешироваться. В Dashboard: Caching → Configuration → Cache Level: Bypass для ",[202,5840,5841],{},"ntfy.example.com\u002Fv1\u002F*",[165,5843,5845],{"id":5844},"websocket-не-подключается","WebSocket не подключается",[16,5847,5848],{},"За Cloudflare WebSocket работает, но нужно убедиться, что:",[1824,5850,5851,5856,5859],{},[1827,5852,5853,5855],{},[202,5854,4829],{}," в конфиге ntfy",[1827,5857,5858],{},"Cloudflare не блокирует WebSocket (на Free плане не блокирует)",[1827,5860,5861],{},"Caddy\u002Fnginx проксирует Upgrade заголовки",[165,5863,5865],{"id":5864},"_403-forbidden-при-публикации","\"403 Forbidden\" при публикации",[16,5867,5868],{},"Пользователь не имеет доступа к топику. Проверьте:",[283,5870,5872],{"className":1861,"code":5871,"language":1863,"meta":288,"style":288},"ntfy access LIST  # Кто имеет доступ к чему\nntfy access admin my-topic rw  # Дать доступ\n",[202,5873,5874,5886],{"__ignoreMap":288},[292,5875,5876,5878,5880,5883],{"class":294,"line":295},[292,5877,4671],{"class":863},[292,5879,5602],{"class":347},[292,5881,5882],{"class":347}," LIST",[292,5884,5885],{"class":486},"  # Кто имеет доступ к чему\n",[292,5887,5888,5890,5892,5894,5897,5900],{"class":294,"line":324},[292,5889,4671],{"class":863},[292,5891,5602],{"class":347},[292,5893,5605],{"class":347},[292,5895,5896],{"class":347}," my-topic",[292,5898,5899],{"class":347}," rw",[292,5901,5902],{"class":486},"  # Дать доступ\n",[11,5904,5906],{"id":5905},"альтернативы","Альтернативы",[165,5908,5910],{"id":5909},"gotify","Gotify",[16,5912,5913],{},"Похож на ntfy, но написан на Go с другим API. Меньше комьюнити, меньше приложений. Если уже выбрали ntfy — нет смысла менять.",[165,5915,5917],{"id":5916},"apprise","Apprise",[16,5919,5920],{},"Python-библиотека для отправки уведомлений в 80+ сервисов (Telegram, Slack, Discord, ntfy, и т.д.). Хороша как unified sender, но не как сервер. Можно комбинировать: Apprise → ntfy → телефон.",[165,5922,5924],{"id":5923},"shoutrrr","Shoutrrr",[16,5926,5927],{},"Go-альтернатива Apprise. Тот же подход — единый интерфейс для разных notification backends.",[165,5929,5931],{"id":5930},"telegram-bot-api","Telegram Bot API",[16,5933,5934],{},"Самый очевидный вариант, и я его тоже использую. Но ntfy выигрывает для self-hosted: нет rate limit от Telegram, нет зависимости от серверов Telegram, полный контроль над данными. Telegram — для мессенджерного опыта. ntfy — для infrastructure alerts.",[11,5936,5938],{"id":5937},"ресурсы","Ресурсы",[16,5940,5941,5942,5945],{},"На типичном VPS ntfy потребляет ",[175,5943,5944],{},"~18 МБ RAM"," и практически не грузит CPU. За 4 часа работы — 51 сообщение опубликовано, 3 подписчика, 4 активных топика. Это капля в море.",[16,5947,5948],{},"Для сравнения: Prometheus + Alertmanager жрут ~200 МБ. Grafana — ещё ~150 МБ. ntfy с webhook-интеграцией покрывает 80% кейсов мониторинга для хобби-проекта без всей этой инфраструктуры.",[11,5950,4018],{"id":4576},[16,5952,5953,5954,5957],{},"ntfy — это быстро: от ",[202,5955,5956],{},"apt install"," до работающих push-уведомлений — минимум настроек. Go-бинарник, systemd-сервис, HTTP API. Ставите, настраиваете Caddy, создаёте пользователя — и у вас свой notification backend, который не зависит ни от кого.",[16,5959,5960],{},"Для self-hosted AI-агента — это must-have. Мониторинг, алерты, уведомления о задачах — всё через один простой протокол.",[1756,5962,5963],{},"html pre.shiki code .siMrf, html code.shiki .siMrf{--shiki-light:#6F42C1;--shiki-light-font-style:inherit;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic}html pre.shiki code .sG7gF, html code.shiki .sG7gF{--shiki-light:#032F62;--shiki-dark:#A6E3A1}html pre.shiki code .soLUO, html code.shiki .soLUO{--shiki-light:#005CC5;--shiki-dark:#A6E3A1}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sEb-F, html code.shiki .sEb-F{--shiki-light:#22863A;--shiki-dark:#89B4FA}html pre.shiki code .sgPNX, html code.shiki .sgPNX{--shiki-light:#24292E;--shiki-dark:#94E2D5}html pre.shiki code .sNSVI, html code.shiki .sNSVI{--shiki-light:#005CC5;--shiki-dark:#FAB387}html pre.shiki code .saXKZ, html code.shiki .saXKZ{--shiki-light:#D73A49;--shiki-dark:#CBA6F7}html pre.shiki code .slTIY, html code.shiki .slTIY{--shiki-light:#24292E;--shiki-dark:#CDD6F4}html pre.shiki code .s_QEy, html code.shiki .s_QEy{--shiki-light:#24292E;--shiki-dark:#9399B2}html pre.shiki code .sPNDc, html code.shiki .sPNDc{--shiki-light:#24292E;--shiki-dark:#89B4FA}html pre.shiki code .s_VIv, html code.shiki .s_VIv{--shiki-light:#005CC5;--shiki-dark:#F5C2E7}html pre.shiki code .sbIxs, html code.shiki .sbIxs{--shiki-light:#005CC5;--shiki-dark:#CDD6F4}html pre.shiki code .s_Q3D, html code.shiki .s_Q3D{--shiki-light:#D73A49;--shiki-dark:#94E2D5}html pre.shiki code .sf7P5, html code.shiki .sf7P5{--shiki-light:#D73A49;--shiki-light-font-weight:inherit;--shiki-dark:#CBA6F7;--shiki-dark-font-weight:bold}html pre.shiki code .s-dMd, html code.shiki .s-dMd{--shiki-light:#E36209;--shiki-light-font-style:inherit;--shiki-dark:#EBA0AC;--shiki-dark-font-style:italic}html pre.shiki code .skkvY, html code.shiki .skkvY{--shiki-light:#6A737D;--shiki-light-font-style:inherit;--shiki-dark:#9399B2;--shiki-dark-font-style:italic}",{"title":288,"searchDepth":324,"depth":324,"links":5965},[5966,5967,5968,5969,5970,5971,5972,5973,5974,5979,5980,5985,5986,5992,5998,5999],{"id":4675,"depth":324,"text":4676},{"id":4711,"depth":324,"text":4712},{"id":4894,"depth":324,"text":4895},{"id":4948,"depth":324,"text":4949},{"id":4995,"depth":324,"text":4996},{"id":5058,"depth":324,"text":5059},{"id":5188,"depth":324,"text":5189},{"id":5321,"depth":324,"text":5322},{"id":5469,"depth":324,"text":5470,"children":5975},[5976,5977,5978],{"id":5473,"depth":331,"text":5474},{"id":5490,"depth":331,"text":5491},{"id":5506,"depth":331,"text":5507},{"id":5524,"depth":324,"text":5525},{"id":5566,"depth":324,"text":5567,"children":5981},[5982,5983,5984],{"id":5570,"depth":331,"text":5571},{"id":5653,"depth":331,"text":5654},{"id":5700,"depth":331,"text":5701},{"id":5733,"depth":324,"text":5734},{"id":5769,"depth":324,"text":5770,"children":5987},[5988,5989,5990,5991],{"id":5773,"depth":331,"text":5774},{"id":5830,"depth":331,"text":5831},{"id":5844,"depth":331,"text":5845},{"id":5864,"depth":331,"text":5865},{"id":5905,"depth":324,"text":5906,"children":5993},[5994,5995,5996,5997],{"id":5909,"depth":331,"text":5910},{"id":5916,"depth":331,"text":5917},{"id":5923,"depth":331,"text":5924},{"id":5930,"depth":331,"text":5931},{"id":5937,"depth":324,"text":5938},{"id":4576,"depth":324,"text":4018},"Как поднять свой ntfy-сервер для push-уведомлений: Caddy reverse proxy, токены доступа, интеграция с AI-агентом через webhooks. Мониторинг, алерты и ловушка с бесконечным циклом.",{},"\u002Fblog\u002Fself-hosted-ntfy-notifications",{"title":4655,"description":6000},"blog\u002Fself-hosted-ntfy-notifications",[4671,4643,6006,6007,6008,2447],"notifications","vps","caddy","nVgsVh3Yj-JM6u9Vp0nbm4h2SG5rxy-kOsA8I1D8rJc",1780763805927]