[{"data":1,"prerenderedAt":6378},["ShallowReactive",2],{"posts-list":3},[4,2047,2133,2537,3912,4243,4536,5005,6310],{"id":5,"title":6,"body":7,"cover":2034,"date":2035,"description":54,"draft":2036,"extension":2037,"meta":2038,"navigation":1339,"path":2039,"seo":2040,"stem":2041,"summary":2042,"tags":2043,"__hash__":2046},"posts\u002Fposts\u002F26_06_09\u002Fmain.md","以 QQ 机器人为梳理解异步编程",{"type":8,"value":9,"toc":1996},"minimark",[10,15,22,25,30,33,45,48,78,85,95,98,146,157,159,163,166,175,182,186,252,258,264,294,316,320,361,363,367,378,382,389,424,430,438,462,466,471,500,503,509,512,585,597,599,603,606,610,615,621,627,631,638,658,662,669,673,680,700,710,717,719,723,726,730,741,791,804,819,826,831,835,866,872,879,890,899,944,951,1017,1022,1026,1032,1038,1064,1075,1078,1175,1201,1205,1225,1255,1265,1300,1304,1311,1420,1423,1429,1439,1443,1449,1458,1461,1479,1481,1485,1488,1492,1497,1500,1571,1578,1601,1613,1616,1624,1659,1667,1715,1807,1823,1827,1833,1853,1856,1866,1872,1898,1903,1909,1912,1931,1933,1937,1985,1992],[11,12,14],"h1",{"id":13},"以-qq-机器人为例讲解异步编程","以 QQ 机器人为例讲解异步编程",[16,17,18],"blockquote",{},[19,20,21],"p",{},"从「异步是什么」到「事件循环怎么转」，再到一个真实框架（NcatBot）里的应用，最后用一个真实的定时任务 bug 复盘，把抽象的概念落到能跑的代码上。",[23,24],"hr",{},[26,27,29],"h2",{"id":28},"一为什么需要异步","一、为什么需要异步",[19,31,32],{},"先想一个最朴素的问题：一个 QQ 机器人在干什么？",[19,34,35,36,40,41,44],{},"它绝大部分时间在",[37,38,39],"strong",{},"等","——等 WebSocket 收到下一条消息、等 LLM API 返回、等沙箱里的命令跑完、等一张图片下载完。这些操作的共同点是 ",[37,42,43],{},"I\u002FO 密集","：CPU 几乎不工作，只是干等外部资源。",[19,46,47],{},"如果用同步阻塞的写法：",[49,50,55],"pre",{"className":51,"code":52,"language":53,"meta":54,"style":54},"language-python shiki shiki-themes github-dark github-light","msg = ws.recv()              # 阻塞，等消息（可能等几秒，也可能几分钟）\nreply = llm.chat(msg)        # 阻塞，等 LLM 返回（好几秒）\nws.send(reply)               # 阻塞，等发送完成\n","python","",[56,57,58,66,72],"code",{"__ignoreMap":54},[59,60,63],"span",{"class":61,"line":62},"line",1,[59,64,65],{},"msg = ws.recv()              # 阻塞，等消息（可能等几秒，也可能几分钟）\n",[59,67,69],{"class":61,"line":68},2,[59,70,71],{},"reply = llm.chat(msg)        # 阻塞，等 LLM 返回（好几秒）\n",[59,73,75],{"class":61,"line":74},3,[59,76,77],{},"ws.send(reply)               # 阻塞，等发送完成\n",[19,79,80,81,84],{},"在 ",[56,82,83],{},"ws.recv()"," 等消息的那几分钟里，整个程序什么都做不了——来了第二个群的消息也只能排队干瞪眼。要并发处理多个会话，传统办法是「一个连接一个线程」，但线程有真实的内存和上下文切换开销，几千个会话就会把机器拖垮。",[19,86,87,90,91,94],{},[37,88,89],{},"异步（async）的核心思想","：既然大部分时间都在等 I\u002FO，那就在「等」的时候让出 CPU，去干别的活，等 I\u002FO 好了再回来接着干。这样",[37,92,93],{},"单个线程","就能同时推进成百上千个「等待中」的任务。",[19,96,97],{},"关键区分：",[99,100,101,116],"table",{},[102,103,104],"thead",{},[105,106,107,110,113],"tr",{},[108,109],"th",{},[108,111,112],{},"含义",[108,114,115],{},"适合场景",[117,118,119,133],"tbody",{},[105,120,121,127,130],{},[122,123,124],"td",{},[37,125,126],{},"并发（concurrency）",[122,128,129],{},"多个任务交替推进，宏观上「同时」进行",[122,131,132],{},"I\u002FO 密集（机器人就是典型）",[105,134,135,140,143],{},[122,136,137],{},[37,138,139],{},"并行（parallelism）",[122,141,142],{},"多个任务在多核上真正同时执行",[122,144,145],{},"CPU 密集（大量计算）",[19,147,148,149,152,153,156],{},"Python 的 ",[56,150,151],{},"asyncio"," 提供的是",[37,154,155],{},"单线程并发","：不靠多核，靠在一个线程里飞快地切换任务来「假装同时」。对 I\u002FO 密集的机器人来说，这恰好是最划算的模型。",[23,158],{},[26,160,162],{"id":161},"二事件循环event-loop异步的心脏","二、事件循环（Event Loop）：异步的心脏",[19,164,165],{},"异步代码不会自己跑。背后必须有一个调度器在不停地做这件事：",[16,167,168],{},[19,169,170,171,174],{},"「谁的 I\u002FO 好了 \u002F 谁的定时器到点了 → 让谁继续往下执行；谁遇到 ",[56,172,173],{},"await"," 要等待了 → 把它挂起，切去跑别的。」",[19,176,177,178,181],{},"这个调度器就是 ",[37,179,180],{},"event loop（事件循环）","。",[183,184,185],"h3",{"id":185},"三个核心概念",[99,187,188,201],{},[102,189,190],{},[105,191,192,195,198],{},[108,193,194],{},"概念",[108,196,197],{},"是什么",[108,199,200],{},"类比",[117,202,203,219,239],{},[105,204,205,210,216],{},[122,206,207],{},[37,208,209],{},"coroutine（协程）",[122,211,212,215],{},[56,213,214],{},"async def"," 函数，调用后返回一个「待执行的计划」，本身不会自动运行",[122,217,218],{},"一份写好的菜谱",[105,220,221,226,236],{},[122,222,223],{},[37,224,225],{},"Task（任务）",[122,227,228,231,232,235],{},[56,229,230],{},"asyncio.create_task(coro)"," 把协程",[37,233,234],{},"注册到当前 loop","，loop 才会调度它",[122,237,238],{},"把菜谱交给厨师排进待办单",[105,240,241,246,249],{},[122,242,243],{},[37,244,245],{},"event loop",[122,247,248],{},"真正的调度者，运行期间不断切换、推进所有 Task",[122,250,251],{},"厨房里那个不停切换炉灶的厨师",[183,253,255,257],{"id":254},"await-到底发生了什么",[56,256,173],{}," 到底发生了什么",[19,259,260,263],{},[56,261,262],{},"await some_io()"," 这行代码的语义是：",[265,266,267,271,278,284,287],"ol",{},[268,269,270],"li",{},"「我要等这个 I\u002FO，现在还没好。」",[268,272,273,274,277],{},"把当前协程的执行",[37,275,276],{},"挂起","，记下它停在哪一行。",[268,279,280,281,181],{},"把控制权",[37,282,283],{},"交还给 event loop",[268,285,286],{},"loop 转去推进其他就绪的任务。",[268,288,289,290,293],{},"等这个 I\u002FO 完成，loop 再把这个协程从挂起处",[37,291,292],{},"恢复","执行。",[19,295,296,297,299,300,303,304,307,308,311,312,315],{},"这套「挂起—交还—恢复」就是单线程能并发的全部秘密。注意：",[56,298,173],{}," 让出的前提是它等的东西",[37,301,302],{},"本身是异步的","（如 ",[56,305,306],{},"asyncio.sleep","、异步网络库）。如果你在协程里写了同步阻塞的 ",[56,309,310],{},"time.sleep(5)","，loop 会被这一行",[37,313,314],{},"整个卡死"," 5 秒，所有其他任务一起停摆——这是异步编程最常见的坑。",[183,317,319],{"id":318},"三个必须记住的认知后面-bug-复盘的关键","三个必须记住的认知（后面 bug 复盘的关键）",[265,321,322,335,348],{},[268,323,324,181,327,330,331,334],{},[37,325,326],{},"协程不会自己跑",[56,328,329],{},"f()"," 只是创建协程对象，必须有一个",[37,332,333],{},"正在运行的 loop"," 去驱动。",[268,336,337,343,344,347],{},[37,338,339,342],{},[56,340,341],{},"create_task"," 绑定到「当前正在运行的那个 loop」","——调用那一刻 ",[56,345,346],{},"asyncio.get_running_loop()"," 返回的 loop。",[268,349,350,353,354,357,358,360],{},[37,351,352],{},"loop 一旦停止\u002F关闭，挂在它上面的所有 Task 都不再被调度","。哪怕协程是个 ",[56,355,356],{},"while True","，loop 不转，它就永远卡在某个 ",[56,359,173],{}," 上不动。",[23,362],{},[26,364,366],{"id":365},"三原理对比typescript-浏览器-与-python-的事件循环","三、原理对比：TypeScript \u002F 浏览器 与 Python 的事件循环",[19,368,369,370,373,374,377],{},"很多人是先在前端接触异步的。JS\u002FTS 和 Python 的 ",[56,371,372],{},"async\u002Fawait"," ",[37,375,376],{},"语法几乎一样","，但底层调度模型有一个值得一提的区别。",[183,379,381],{"id":380},"js-浏览器宏任务-微任务两个队列","JS \u002F 浏览器：宏任务 + 微任务两个队列",[19,383,384,385,388],{},"浏览器的事件循环把待执行的回调分成",[37,386,387],{},"两类队列","：",[390,391,392,405],"ul",{},[268,393,394,388,397,400,401,404],{},[37,395,396],{},"宏任务（macrotask）队列",[56,398,399],{},"setTimeout","、",[56,402,403],{},"setInterval","、I\u002FO、UI 渲染事件等。",[268,406,407,388,410,413,414,416,417,400,420,423],{},[37,408,409],{},"微任务（microtask）队列",[56,411,412],{},"Promise.then"," 回调、",[56,415,173],{}," 之后的续体、",[56,418,419],{},"queueMicrotask",[56,421,422],{},"MutationObserver"," 等。",[19,425,426,427],{},"调度规则是：",[37,428,429],{},"每执行完一个宏任务，就把当前微任务队列里的任务全部清空，然后才考虑渲染、再取下一个宏任务。",[49,431,436],{"className":432,"code":434,"language":435,"meta":54},[433],"language-text","取一个宏任务执行\n  └─ 执行过程中产生的微任务进微任务队列\n执行完这个宏任务\n  └─ 把微任务队列「一次性全部排空」（期间新产生的微任务也一起处理掉）\n  └─ （浏览器）此时可能进行一次渲染\n取下一个宏任务……\n","text",[56,437,434],{"__ignoreMap":54},[19,439,440,441,444,445,448,449,451,452,455,456,458,459,461],{},"为什么要分两个队列？很大程度上正是",[37,442,443],{},"为了浏览器渲染","。浏览器希望「一组逻辑上相关的、原子的更新」在同一帧内一起完成，不要被渲染打断到中间状态。微任务队列保证了：一个宏任务触发的所有连锁 Promise 回调，会在",[37,446,447],{},"下一次渲染之前一次性跑完","，从而把这些更新「绑定到一起」原子地呈现，避免画面闪烁或撕裂。",[56,450,173],{}," 的续体走的是微任务，所以它的优先级高于 ",[56,453,454],{},"setTimeout(0)"," 这种宏任务——这就是经典面试题「",[56,457,412],{}," 比 ",[56,460,399],{}," 先执行」的根源。",[183,463,465],{"id":464},"python-asyncio单一就绪队列-定时器堆","Python asyncio：单一就绪队列 + 定时器堆",[19,467,148,468,470],{},[56,469,151],{}," 没有「宏\u002F微任务」这种二分。它的模型更直接：",[390,472,473,480,494],{},[268,474,475,476,479],{},"一个 ",[37,477,478],{},"ready 队列","：所有当前就绪、可以立刻继续跑的回调\u002FTask。",[268,481,482,483,486,487,490,491,493],{},"一个按时间排序的",[37,484,485],{},"定时器结构","（最小堆）：",[56,488,489],{},"call_later"," \u002F ",[56,492,306],{}," 注册的「到点才就绪」的回调。",[268,495,475,496,499],{},[37,497,498],{},"selector","：通过操作系统的 I\u002FO 多路复用（epoll\u002Fkqueue\u002FIOCP）监听 fd 是否就绪。",[19,501,502],{},"每一轮循环（一次 \"tick\"）大致是：",[49,504,507],{"className":505,"code":506,"language":435,"meta":54},[433],"1. 算出最近的定时器还有多久到点 → 作为本轮 select 的超时时间\n2. select(timeout)：等 I\u002FO 事件，或等到最近的定时器到点\n3. 把「I\u002FO 就绪的回调」和「已到点的定时器回调」放进 ready 队列\n4. 把 ready 队列里当前的回调「逐个执行完」\n5. 回到第 1 步\n",[56,508,506],{"__ignoreMap":54},[19,510,511],{},"和 JS 的根本区别：",[99,513,514,527],{},[102,515,516],{},[105,517,518,521,524],{},[108,519,520],{},"维度",[108,522,523],{},"JS \u002F 浏览器",[108,525,526],{},"Python asyncio",[117,528,529,540,555,574],{},[105,530,531,534,537],{},[122,532,533],{},"队列结构",[122,535,536],{},"宏任务队列 + 微任务队列（二分）",[122,538,539],{},"单一 ready 队列 + 定时器堆",[105,541,542,545,552],{},[122,543,544],{},"设计动机",[122,546,547,548,551],{},"与",[37,549,550],{},"渲染","协调，把原子更新绑定在一帧内",[122,553,554],{},"纯后端调度，无渲染概念，模型更扁平",[105,556,557,562,571],{},[122,558,559,561],{},[56,560,173],{}," 续体优先级",[122,563,564,565,373,568,570],{},"走微任务，",[37,566,567],{},"高于",[56,569,399],{}," 等宏任务",[122,572,573],{},"就是普通就绪回调，进同一个 ready 队列",[105,575,576,579,582],{},[122,577,578],{},"「下一个 tick」语义",[122,580,581],{},"微任务排空 → （渲染）→ 下个宏任务",[122,583,584],{},"一轮 select + 执行 ready 回调",[19,586,587,590,591,593,594,596],{},[37,588,589],{},"JS 的两队列模型是被浏览器渲染需求「逼」出来的；Python 没有渲染负担，所以用了一个更扁平的单队列 + 定时器堆模型。"," 但两者对应用层开发者暴露的 ",[56,592,372],{}," 心智模型是高度一致的——遇到 ",[56,595,173],{}," 让出、I\u002FO 好了恢复。",[23,598],{},[26,600,602],{"id":601},"四异步的应用一个真实的-qq-机器人框架","四、异步的应用：一个真实的 QQ 机器人框架",[19,604,605],{},"来看异步在真实项目里怎么用。下面以我正在开发的基于 NcatBot 的 QQ AI Agent 框架（AEsirClaw）为例。",[183,607,609],{"id":608},"_41-整条链路都是异步的","4.1 整条链路都是异步的",[19,611,612,613,388],{},"一条 QQ 消息从进来到回复，几乎每一环都在 ",[56,614,173],{},[49,616,619],{"className":617,"code":618,"language":435,"meta":54},[433],"QQ 消息 ──→ WebSocket 收包（await recv）\n                │\n                ▼\n           事件分发 → 防抖调度（await sleep 等窗口）\n                │\n                ▼\n           组装上下文 → Agent Loop ◄──► LLM API（await chat）\n                │\n                ├── execute_task → Docker 沙箱（await 执行）\n                └── send_group_msg → QQ（await 发送 + await 打字延迟）\n",[56,620,618],{"__ignoreMap":54},[19,622,623,624,626],{},"每个 ",[56,625,173],{}," 都是一次「让出 CPU 去处理别的会话」的机会。正因如此，单线程就能同时服务多个群和私聊：A 群在等 LLM 返回时，loop 顺手就把 B 群刚到的消息推进了。",[183,628,630],{"id":629},"_42-异步让拟人化变得自然","4.2 异步让「拟人化」变得自然",[19,632,633,634,637],{},"机器人为了模拟真人打字节奏，会分段发送、每段之间停顿。同步写法里「停顿」意味着卡死整个程序；异步里它只是一次 ",[56,635,636],{},"await asyncio.sleep","，停顿期间 loop 照样服务别人：",[49,639,641],{"className":51,"code":640,"language":53,"meta":54,"style":54},"for seg in messages:\n    await send(seg)\n    await asyncio.sleep(字数 * delay + 随机波动)   # 停顿，但不阻塞其他会话\n",[56,642,643,648,653],{"__ignoreMap":54},[59,644,645],{"class":61,"line":62},[59,646,647],{},"for seg in messages:\n",[59,649,650],{"class":61,"line":68},[59,651,652],{},"    await send(seg)\n",[59,654,655],{"class":61,"line":74},[59,656,657],{},"    await asyncio.sleep(字数 * delay + 随机波动)   # 停顿，但不阻塞其他会话\n",[183,659,661],{"id":660},"_43-防抖用异步实现听完再说","4.3 防抖：用异步实现「听完再说」",[19,663,664,665,668],{},"人聊天习惯是连发好几条，机器人不该逐条机械回复。框架用一个异步防抖器：首条消息到达后启动一个 5 秒窗口（",[56,666,667],{},"await asyncio.sleep(5)","），窗口期间的新消息只刷新「候选」，窗口结束才取最新候选统一处理。整个等待逻辑就是一个挂在 loop 上的协程，等待期间完全不占用 CPU。",[183,670,672],{"id":671},"_44-框架如何托管-event-loop","4.4 框架如何托管 event loop",[19,674,675,676,679],{},"应用层开发者通常不自己写 ",[56,677,678],{},"asyncio.run","，而是框架替你把 loop 跑起来。NcatBot 的入口极简：",[49,681,683],{"className":51,"code":682,"language":53,"meta":54,"style":54},"from ncatbot.core import BotClient\nbot = BotClient()\nbot.run_frontend()   # 一切从这里开始\n",[56,684,685,690,695],{"__ignoreMap":54},[59,686,687],{"class":61,"line":62},[59,688,689],{},"from ncatbot.core import BotClient\n",[59,691,692],{"class":61,"line":68},[59,693,694],{},"bot = BotClient()\n",[59,696,697],{"class":61,"line":74},[59,698,699],{},"bot.run_frontend()   # 一切从这里开始\n",[19,701,702,705,706,709],{},[56,703,704],{},"run_frontend()"," 内部最终会进入一个长驻的事件循环，由它统一驱动所有异步任务。",[37,707,708],{},"理解「框架在哪里、用哪个 loop 跑你的代码」，是用好异步框架的关键","——下一节的 bug 正是栽在这上面。",[19,711,712,713,716],{},"这里先埋一个伏笔：NcatBot 在启动过程中其实会创建",[37,714,715],{},"不止一个"," event loop，它们各司其职、互相独立。记住这一点，往下看。",[23,718],{},[26,720,722],{"id":721},"五实践定时任务的实现与一次真实-bug-复盘","五、实践：定时任务的实现与一次真实 bug 复盘",[19,724,725],{},"需求很常见：让 QQ 机器人能自主设定定时任务（「每天早上 8 点拉一遍新闻」「每隔一小时报时」「10 分钟后提醒我」），到点后机器人主动在原会话里发言。",[183,727,729],{"id":728},"_51-调度器的设计","5.1 调度器的设计",[19,731,732,733,736,737,740],{},"核心是一个 ",[56,734,735],{},"TaskScheduler","：用 JSON 文件持久化任务，挂一个",[37,738,739],{},"常驻的异步扫描循环","，每隔 N 秒扫一遍，把到期的任务触发掉。扫描循环长这样：",[49,742,744],{"className":51,"code":743,"language":53,"meta":54,"style":54},"async def _scan_loop(self) -> None:\n    \"\"\"常驻循环：周期性扫描到期任务并触发。\"\"\"\n    while True:\n        try:\n            await asyncio.sleep(self.scan_interval)\n            await self._tick()                 # 扫描 + 触发到期任务\n        except asyncio.CancelledError:\n            break\n",[56,745,746,751,756,761,767,773,779,785],{"__ignoreMap":54},[59,747,748],{"class":61,"line":62},[59,749,750],{},"async def _scan_loop(self) -> None:\n",[59,752,753],{"class":61,"line":68},[59,754,755],{},"    \"\"\"常驻循环：周期性扫描到期任务并触发。\"\"\"\n",[59,757,758],{"class":61,"line":74},[59,759,760],{},"    while True:\n",[59,762,764],{"class":61,"line":763},4,[59,765,766],{},"        try:\n",[59,768,770],{"class":61,"line":769},5,[59,771,772],{},"            await asyncio.sleep(self.scan_interval)\n",[59,774,776],{"class":61,"line":775},6,[59,777,778],{},"            await self._tick()                 # 扫描 + 触发到期任务\n",[59,780,782],{"class":61,"line":781},7,[59,783,784],{},"        except asyncio.CancelledError:\n",[59,786,788],{"class":61,"line":787},8,[59,789,790],{},"            break\n",[19,792,793,794,796,797,799,800,803],{},"这是个标准的异步后台循环：",[56,795,356],{}," + ",[56,798,636],{},"。它必须被某个",[37,801,802],{},"持续运行的 loop"," 调度，才能一轮一轮地转下去。启动它的代码也很直觉：",[49,805,807],{"className":51,"code":806,"language":53,"meta":54,"style":54},"def start(self) -> None:\n    self._loop_task = asyncio.create_task(self._scan_loop())\n",[56,808,809,814],{"__ignoreMap":54},[59,810,811],{"class":61,"line":62},[59,812,813],{},"def start(self) -> None:\n",[59,815,816],{"class":61,"line":68},[59,817,818],{},"    self._loop_task = asyncio.create_task(self._scan_loop())\n",[19,820,821,822,825],{},"到点触发时，它调用一个回调，往会话里注入一条「",[59,823,824],{},"定时任务触发","」系统消息，再唤醒一次 Agent Loop，让机器人自己决定怎么发言。整个机制干净利落——逻辑测试全过，任务也能正常写进 JSON 文件。",[19,827,828],{},[37,829,830],{},"然后部署上去，到点了……什么都没发生。",[183,832,834],{"id":833},"_52-现象","5.2 现象",[390,836,837,848,855],{},[268,838,839,840,843,844,847],{},"任务确实被创建了：",[56,841,842],{},"schedules.json"," 里躺着两条 ",[56,845,846],{},"daily"," 任务。",[268,849,850,851,854],{},"时间早就过了，",[56,852,853],{},"next_run"," 时间戳算得也对。",[268,856,857,858,865],{},"但日志里",[37,859,860,861,864],{},"没有任何 ",[56,862,863],{},"[Scheduler] 触发任务"," 的记录","，连「扫描器已启动」之后的活动都看不到。",[19,867,868,869,181],{},"任务能存、时间能算，就是不触发。问题不在逻辑，而在",[37,870,871],{},"那个扫描循环根本没在转",[183,873,875,876,878],{"id":874},"_53-根因create_task-挂错了-loop","5.3 根因：",[56,877,341],{}," 挂错了 loop",[19,880,881,882,885,886,889],{},"我最初把 ",[56,883,884],{},"scheduler.start()"," 放在了插件的 ",[56,887,888],{},"on_load","（插件加载时的初始化钩子）里。看起来天经地义——加载时启动调度器，对吧？",[19,891,892,893,898],{},"错。关键在于框架",[37,894,895,896],{},"怎么调用 ",[56,897,888],{},"。扒开 NcatBot 的插件加载器：",[49,900,902],{"className":51,"code":901,"language":53,"meta":54,"style":54},"# ncatbot\u002Fplugin_system\u002Floader.py\ndef _run_init():\n    loop = asyncio.new_event_loop()              # ← 新建一个临时 loop\n    asyncio.set_event_loop(loop)\n    try:\n        loop.run_until_complete(plugin.__onload__())   # ← 跑你的 on_load\n    finally:\n        loop.close()                             # ← 跑完立刻关闭这个 loop！\n",[56,903,904,909,914,919,924,929,934,939],{"__ignoreMap":54},[59,905,906],{"class":61,"line":62},[59,907,908],{},"# ncatbot\u002Fplugin_system\u002Floader.py\n",[59,910,911],{"class":61,"line":68},[59,912,913],{},"def _run_init():\n",[59,915,916],{"class":61,"line":74},[59,917,918],{},"    loop = asyncio.new_event_loop()              # ← 新建一个临时 loop\n",[59,920,921],{"class":61,"line":763},[59,922,923],{},"    asyncio.set_event_loop(loop)\n",[59,925,926],{"class":61,"line":769},[59,927,928],{},"    try:\n",[59,930,931],{"class":61,"line":775},[59,932,933],{},"        loop.run_until_complete(plugin.__onload__())   # ← 跑你的 on_load\n",[59,935,936],{"class":61,"line":781},[59,937,938],{},"    finally:\n",[59,940,941],{"class":61,"line":787},[59,942,943],{},"        loop.close()                             # ← 跑完立刻关闭这个 loop！\n",[19,945,946,947,950],{},"而且这一切还被丢进",[37,948,949],{},"一个独立线程","里执行。问题链条于是浮现：",[265,952,953,962,976,992,1003],{},[268,954,955,957,958,961],{},[56,956,888],{}," 运行在这个",[37,959,960],{},"临时 loop","（Loop ①）上。",[268,963,964,967,968,971,972,975],{},[56,965,966],{},"start()"," 里的 ",[56,969,970],{},"asyncio.create_task(_scan_loop())"," 把扫描循环",[37,973,974],{},"绑定到了 Loop ①","（回忆第二节的认知 2：create_task 绑定当前正在跑的 loop）。",[268,977,978,980,981,984,985,988,989,181],{},[56,979,888],{}," 一返回，",[56,982,983],{},"run_until_complete"," 结束，紧接着 ",[56,986,987],{},"loop.close()"," —— ",[37,990,991],{},"Loop ① 死亡",[268,993,994,995,998,999,1002],{},"扫描循环卡在第一个 ",[56,996,997],{},"await asyncio.sleep(...)"," 上，",[37,1000,1001],{},"再也没有 loop 去唤醒它","（认知 3）。",[268,1004,1005,1006,1009,1010,1013,1014,181],{},"而真正长驻的业务主 loop（Loop ②，由 ",[56,1007,1008],{},"run_frontend"," 内部的 ",[56,1011,1012],{},"asyncio.run(connect_websocket())"," 驱动）上，",[37,1015,1016],{},"压根没有这个扫描任务",[19,1018,1019],{},[37,1020,1021],{},"任务被挂在了一个用完即弃的 loop 上，那个 loop 关了之后，任务就成了无人调度的僵尸。",[183,1023,1025],{"id":1024},"_54-框架里到底有几个-loop","5.4 框架里到底有几个 loop",[19,1027,1028,1029,1031],{},"把 ",[56,1030,704],{}," 的链路摊开看，就能数清楚：",[49,1033,1036],{"className":1034,"code":1035,"language":435,"meta":54},[433],"run_frontend()\n  └─ start()\n       ├─ 加载插件\n       │    └─ 独立线程\n       │         └─ Loop ① = asyncio.new_event_loop()    ← 临时\n       │              └─ run_until_complete(on_load)       ← ❌ 我最初在这里 create_task\n       │         └─ loop.close()                           ← Loop ① 死亡，扫描循环冻结\n       │\n       └─ asyncio.run(connect_websocket())\n            └─ Loop ② = 长驻主循环                          ← ✅ 应该挂在这里\n                 ├─ while True: recv() → 分发 QQ 消息\n                 └─ 触发 STARTUP 事件 → _on_bot_ready       ← 这里 start() 才对\n",[56,1037,1035],{"__ignoreMap":54},[390,1039,1040,1055],{},[268,1041,1042,1045,1046,1048,1049,1051,1052,181],{},[37,1043,1044],{},"Loop ①（临时）","：只为跑一次性的 ",[56,1047,888],{}," 初始化，",[56,1050,983],{}," 完就 ",[56,1053,1054],{},"close",[268,1056,1057,1060,1061,1063],{},[37,1058,1059],{},"Loop ②（长驻）","：bot 的命脉，跑 ",[56,1062,356],{}," 收消息，所有 QQ 事件、启动回调都在它身上。",[183,1065,1067,1068,1071,1072,1074],{"id":1066},"_55-looprun_until_complete-vs-run_frontend-的本质区别","5.5 ",[56,1069,1070],{},"loop.run_until_complete"," vs ",[56,1073,704],{}," 的本质区别",[19,1076,1077],{},"这两者处在完全不同的抽象层级，正好对照理解：",[99,1079,1080,1095],{},[102,1081,1082],{},[105,1083,1084,1086,1091],{},[108,1085,520],{},[108,1087,1088],{},[56,1089,1090],{},"loop.run_until_complete(coro)",[108,1092,1093],{},[56,1094,704],{},[117,1096,1097,1116,1136,1150,1165],{},[105,1098,1099,1102,1109],{},[122,1100,1101],{},"本质",[122,1103,1104,1105,1108],{},"event loop 的",[37,1106,1107],{},"底层方法","：跑到指定协程完成就停",[122,1110,1111,1112,1115],{},"框架的",[37,1113,1114],{},"顶层入口","：启动整个 bot 并阻塞到退出",[105,1117,1118,1121,1127],{},[122,1119,1120],{},"生命周期",[122,1122,1123,1126],{},[37,1124,1125],{},"短暂","，跑完一个协程就返回（随后被 close）",[122,1128,1129,1132,1133,1135],{},[37,1130,1131],{},"长驻","，内部进 ",[56,1134,356],{}," 收消息，正常永不返回",[105,1137,1138,1141,1144],{},[122,1139,1140],{},"用的 loop",[122,1142,1143],{},"临时新建的 Loop ①，用完即弃",[122,1145,1146,1147,1149],{},"内部 ",[56,1148,678],{}," 独占运行的 Loop ②",[105,1151,1152,1155,1158],{},[122,1153,1154],{},"适合放什么",[122,1156,1157],{},"一次性、跑完就结束的初始化",[122,1159,1160,1161,1164],{},"需要",[37,1162,1163],{},"长期存活","的后台任务应挂在它的 loop 上",[105,1166,1167,1169,1172],{},[122,1168,200],{},[122,1170,1171],{},"临时点火烧开一壶水就熄火",[122,1173,1174],{},"常明的灶火，整晚不灭",[19,1176,1177,1178,1181,1182,1185,1186,1189,1190,1193,1194,1197,1198,1200],{},"补一个常被忽略的关系：",[56,1179,1180],{},"asyncio.run(coro)"," ≈ 「新建 loop + ",[56,1183,1184],{},"run_until_complete(coro)"," + 最后 ",[56,1187,1188],{},"close()","」。所以二者底层是同一类操作，区别只在跑的协程",[37,1191,1192],{},"会不会很快结束","——",[56,1195,1196],{},"connect_websocket"," 是死循环（loop 长驻），",[56,1199,888],{}," 很快结束（loop 短命）。",[183,1202,1204],{"id":1203},"_56-修复","5.6 修复",[19,1206,1028,1207,1209,1210,1212,1213,1216,1217,1220,1221,1224],{},[56,1208,966],{}," 从 ",[56,1211,888],{}," 挪到 ",[56,1214,1215],{},"_on_bot_ready","（",[56,1218,1219],{},"OFFICIAL_STARTUP_EVENT"," 的回调，",[37,1222,1223],{},"运行在长驻主 loop Loop ② 上","）：",[49,1226,1228],{"className":51,"code":1227,"language":53,"meta":54,"style":54},"async def _on_bot_ready(self, event):\n    \"\"\"Bot 连接成功后加载历史消息，并在主 loop 上启动定时任务调度器。\"\"\"\n    await self._load_recent_messages()\n    # 在 bot 主 event loop 上启动调度器（此回调即运行于该 loop）\n    self.scheduler.start()\n",[56,1229,1230,1235,1240,1245,1250],{"__ignoreMap":54},[59,1231,1232],{"class":61,"line":62},[59,1233,1234],{},"async def _on_bot_ready(self, event):\n",[59,1236,1237],{"class":61,"line":68},[59,1238,1239],{},"    \"\"\"Bot 连接成功后加载历史消息，并在主 loop 上启动定时任务调度器。\"\"\"\n",[59,1241,1242],{"class":61,"line":74},[59,1243,1244],{},"    await self._load_recent_messages()\n",[59,1246,1247],{"class":61,"line":763},[59,1248,1249],{},"    # 在 bot 主 event loop 上启动调度器（此回调即运行于该 loop）\n",[59,1251,1252],{"class":61,"line":769},[59,1253,1254],{},"    self.scheduler.start()\n",[19,1256,1257,1258,1260,1261,1264],{},"而 ",[56,1259,888],{}," 里只保留「构造对象、注册回调」这类",[37,1262,1263],{},"不依赖 loop 存活","的同步初始化，并留下注释警示后人：",[49,1266,1268],{"className":51,"code":1267,"language":53,"meta":54,"style":54},"# 注意：on_load 由框架在独立线程的临时 event loop 上执行\n# （loader.py: loop.run_until_complete(...) 后 loop.close()），\n# 因此这里只构造调度器，绝不能在此 start()——否则扫描协程会被挂到\n# 那个用完即弃的 loop 上，永远不会被调度。真正的启动放在 _on_bot_ready 中。\nself.scheduler = TaskScheduler(...)\nself.scheduler.set_callback(self._on_scheduled_trigger)\n",[56,1269,1270,1275,1280,1285,1290,1295],{"__ignoreMap":54},[59,1271,1272],{"class":61,"line":62},[59,1273,1274],{},"# 注意：on_load 由框架在独立线程的临时 event loop 上执行\n",[59,1276,1277],{"class":61,"line":68},[59,1278,1279],{},"# （loader.py: loop.run_until_complete(...) 后 loop.close()），\n",[59,1281,1282],{"class":61,"line":74},[59,1283,1284],{},"# 因此这里只构造调度器，绝不能在此 start()——否则扫描协程会被挂到\n",[59,1286,1287],{"class":61,"line":763},[59,1288,1289],{},"# 那个用完即弃的 loop 上，永远不会被调度。真正的启动放在 _on_bot_ready 中。\n",[59,1291,1292],{"class":61,"line":769},[59,1293,1294],{},"self.scheduler = TaskScheduler(...)\n",[59,1296,1297],{"class":61,"line":775},[59,1298,1299],{},"self.scheduler.set_callback(self._on_scheduled_trigger)\n",[183,1301,1303],{"id":1302},"_57-最小复现一眼看穿","5.7 最小复现：一眼看穿",[19,1305,1306,1307,1310],{},"为了把这个机制钉死，写一个不依赖框架的十几行 demo（项目里 ",[56,1308,1309],{},"test\u002Ftest_async.py","），对比两种 loop：",[49,1312,1314],{"className":51,"code":1313,"language":53,"meta":54,"style":54},"async def background_loop(tag):\n    while True:\n        await asyncio.sleep(0.2)\n        fired.append(tag)\n\n# 场景 A：临时 loop —— 等价于 on_load\ndef scenario_temp_loop():\n    loop = asyncio.new_event_loop()\n    asyncio.set_event_loop(loop)\n    loop.run_until_complete(fake_on_load())   # 内部 create_task(background_loop)\n    loop.close()                              # ← loop 关闭\n    time.sleep(1.0)                           # 等一秒，看后台循环跑没跑\n\n# 场景 B：长驻 loop —— 等价于主 loop\ndef scenario_long_running_loop():\n    async def main():\n        asyncio.create_task(background_loop(\"B\"))\n        await asyncio.sleep(1.0)              # loop 持续运转 1 秒\n    asyncio.run(main())\n",[56,1315,1316,1321,1325,1330,1335,1341,1346,1351,1356,1361,1367,1373,1379,1384,1390,1396,1402,1408,1414],{"__ignoreMap":54},[59,1317,1318],{"class":61,"line":62},[59,1319,1320],{},"async def background_loop(tag):\n",[59,1322,1323],{"class":61,"line":68},[59,1324,760],{},[59,1326,1327],{"class":61,"line":74},[59,1328,1329],{},"        await asyncio.sleep(0.2)\n",[59,1331,1332],{"class":61,"line":763},[59,1333,1334],{},"        fired.append(tag)\n",[59,1336,1337],{"class":61,"line":769},[59,1338,1340],{"emptyLinePlaceholder":1339},true,"\n",[59,1342,1343],{"class":61,"line":775},[59,1344,1345],{},"# 场景 A：临时 loop —— 等价于 on_load\n",[59,1347,1348],{"class":61,"line":781},[59,1349,1350],{},"def scenario_temp_loop():\n",[59,1352,1353],{"class":61,"line":787},[59,1354,1355],{},"    loop = asyncio.new_event_loop()\n",[59,1357,1359],{"class":61,"line":1358},9,[59,1360,923],{},[59,1362,1364],{"class":61,"line":1363},10,[59,1365,1366],{},"    loop.run_until_complete(fake_on_load())   # 内部 create_task(background_loop)\n",[59,1368,1370],{"class":61,"line":1369},11,[59,1371,1372],{},"    loop.close()                              # ← loop 关闭\n",[59,1374,1376],{"class":61,"line":1375},12,[59,1377,1378],{},"    time.sleep(1.0)                           # 等一秒，看后台循环跑没跑\n",[59,1380,1382],{"class":61,"line":1381},13,[59,1383,1340],{"emptyLinePlaceholder":1339},[59,1385,1387],{"class":61,"line":1386},14,[59,1388,1389],{},"# 场景 B：长驻 loop —— 等价于主 loop\n",[59,1391,1393],{"class":61,"line":1392},15,[59,1394,1395],{},"def scenario_long_running_loop():\n",[59,1397,1399],{"class":61,"line":1398},16,[59,1400,1401],{},"    async def main():\n",[59,1403,1405],{"class":61,"line":1404},17,[59,1406,1407],{},"        asyncio.create_task(background_loop(\"B\"))\n",[59,1409,1411],{"class":61,"line":1410},18,[59,1412,1413],{},"        await asyncio.sleep(1.0)              # loop 持续运转 1 秒\n",[59,1415,1417],{"class":61,"line":1416},19,[59,1418,1419],{},"    asyncio.run(main())\n",[19,1421,1422],{},"运行结果：",[49,1424,1427],{"className":1425,"code":1426,"language":435,"meta":54},[433],"【场景 A：临时 loop（用完即弃）】\n  >>> 结果：后台循环执行了 0 次（loop 已关闭，无人调度）\n\n【场景 B：长驻 loop】\n  [B-长驻loop] 后台循环执行了一次 ...   (×4)\n  >>> 结果：后台循环执行了 4 次（loop 持续运转，正常调度）\n",[56,1428,1426],{"__ignoreMap":54},[19,1430,1431,1438],{},[37,1432,1433,1434,1437],{},"完全相同的 ",[56,1435,1436],{},"create_task(background_loop)","，差别只在「它绑定的 loop 是否持续运行」。"," 场景 A 的 0 次，就是线上「定时任务永不触发」的精确缩影；场景 B 的 4 次，就是修复后扫描循环每 20 秒正常扫一遍的样子。",[183,1440,1442],{"id":1441},"_58-经验法则","5.8 经验法则",[19,1444,1445,1446,1448],{},"在「框架托管 event loop」的场景下，判断「后台任务该在哪 ",[56,1447,341],{},"」只需一条：",[16,1450,1451],{},[19,1452,1453],{},[37,1454,1455,1457],{},[56,1456,341],{}," 必须在「那个会长期活着的 loop」正在运行时调用。",[19,1459,1460],{},"具体到这类机器人框架：",[390,1462,1463,1473,1476],{},[268,1464,1465,1466,1468,1469,1472],{},"❌ 不要在 ",[56,1467,888],{},"\u002F",[56,1470,1471],{},"__init__"," 这类「初始化完就结束」的地方起长驻后台任务。",[268,1474,1475],{},"✅ 在「bot 就绪 \u002F 连接成功」的事件回调里起——那才是长驻主 loop。",[268,1477,1478],{},"快速自检：如果你的任务需要「一直跑」，但它所在的函数是「跑完就返回」的，那它大概率挂错了 loop。",[23,1480],{},[26,1482,1484],{"id":1483},"六延伸思考一个程序能有几个-event-loop","六、延伸思考：一个程序能有几个 event loop？",[19,1486,1487],{},"bug 复盘里反复出现「临时 loop」「长驻 loop」，自然引出三个递进的问题。",[183,1489,1491],{"id":1490},"_61-异步只能有一个-event-loop-吗","6.1 异步只能有一个 event loop 吗？",[19,1493,1494],{},[37,1495,1496],{},"不是。但有一条铁律：每个线程在同一时刻，最多只能有一个「正在运行」的 event loop。",[19,1498,1499],{},"「能创建几个」和「能同时运行几个」是两回事：",[99,1501,1502,1511],{},[102,1503,1504],{},[105,1505,1506,1508],{},[108,1507,520],{},[108,1509,1510],{},"答案",[117,1512,1513,1529,1544,1556],{},[105,1514,1515,1522],{},[122,1516,1517,1518,1521],{},"一个程序能",[37,1519,1520],{},"创建","几个 loop？",[122,1523,1524,1525,1528],{},"任意多个，",[56,1526,1527],{},"asyncio.new_event_loop()"," 想建几个建几个",[105,1530,1531,1537],{},[122,1532,1533,1534,1521],{},"一个线程能同时",[37,1535,1536],{},"运行",[122,1538,1539,1540,1543],{},"只能 1 个，loop 是阻塞式运行的，",[56,1541,1542],{},"loop.run_xxx()"," 没返回前线程被它占着",[105,1545,1546,1553],{},[122,1547,1548,1549,1552],{},"多个 loop 能在",[37,1550,1551],{},"不同线程","里同时跑吗？",[122,1554,1555],{},"可以，每个线程跑自己的 loop，互不干扰",[105,1557,1558,1565],{},[122,1559,1560,1561,1564],{},"loop 能",[37,1562,1563],{},"先后","在同一线程跑吗？",[122,1566,1567,1568,1570],{},"可以，跑完一个 ",[56,1569,1054],{}," 掉，再建一个跑——这正是 NcatBot 干的事",[19,1572,1573,1574,1577],{},"关键限制来自一个",[37,1575,1576],{},"线程局部","的概念「当前线程的 event loop」：",[390,1579,1580,1589],{},[268,1581,1582,1584,1585,1588],{},[56,1583,346],{}," 返回",[37,1586,1587],{},"此刻正在跑","的那个 loop。",[268,1590,1591,1593,1594,1597,1598,1600],{},[56,1592,230],{}," 把任务绑定到它返回的那个 loop——",[37,1595,1596],{},"这就是定时任务 bug 的命门","：你在哪个 loop 运行时调 ",[56,1599,341],{},"，任务就绑给哪个 loop。",[183,1602,1604,1605,1608,1609,1612],{"id":1603},"_62-new_event_loop-和-asynciorunmain-的区别","6.2 ",[56,1606,1607],{},"new_event_loop()"," 和 ",[56,1610,1611],{},"asyncio.run(main())"," 的区别",[19,1614,1615],{},"这俩不在一个层级——一个是「底层手动挡」，一个是「高层自动挡」。",[19,1617,1618,1623],{},[37,1619,1620,1622],{},[56,1621,1527],{},"：只「造」一个 loop","，不运行任何东西，生命周期全靠你：",[49,1625,1627],{"className":51,"code":1626,"language":53,"meta":54,"style":54},"loop = asyncio.new_event_loop()        # 1. 造\nasyncio.set_event_loop(loop)           # 2. 设为本线程当前 loop\ntry:\n    loop.run_until_complete(main())    # 3. 手动驱动\nfinally:\n    loop.close()                       # 4. 手动关（不关就泄漏）\n",[56,1628,1629,1634,1639,1644,1649,1654],{"__ignoreMap":54},[59,1630,1631],{"class":61,"line":62},[59,1632,1633],{},"loop = asyncio.new_event_loop()        # 1. 造\n",[59,1635,1636],{"class":61,"line":68},[59,1637,1638],{},"asyncio.set_event_loop(loop)           # 2. 设为本线程当前 loop\n",[59,1640,1641],{"class":61,"line":74},[59,1642,1643],{},"try:\n",[59,1645,1646],{"class":61,"line":763},[59,1647,1648],{},"    loop.run_until_complete(main())    # 3. 手动驱动\n",[59,1650,1651],{"class":61,"line":769},[59,1652,1653],{},"finally:\n",[59,1655,1656],{"class":61,"line":775},[59,1657,1658],{},"    loop.close()                       # 4. 手动关（不关就泄漏）\n",[19,1660,1661,1666],{},[37,1662,1663,1665],{},[56,1664,1611],{},"：造 + 跑 + 清理 + 关，一条龙","。等价于（简化）：",[49,1668,1670],{"className":51,"code":1669,"language":53,"meta":54,"style":54},"def run(main):\n    loop = asyncio.new_event_loop()                    # 造\n    try:\n        asyncio.set_event_loop(loop)\n        return loop.run_until_complete(main)           # 跑到完成\n    finally:\n        loop.run_until_complete(loop.shutdown_asyncgens())  # 清理残留 task \u002F async gen\n        asyncio.set_event_loop(None)\n        loop.close()                                   # 关\n",[56,1671,1672,1677,1682,1686,1691,1696,1700,1705,1710],{"__ignoreMap":54},[59,1673,1674],{"class":61,"line":62},[59,1675,1676],{},"def run(main):\n",[59,1678,1679],{"class":61,"line":68},[59,1680,1681],{},"    loop = asyncio.new_event_loop()                    # 造\n",[59,1683,1684],{"class":61,"line":74},[59,1685,928],{},[59,1687,1688],{"class":61,"line":763},[59,1689,1690],{},"        asyncio.set_event_loop(loop)\n",[59,1692,1693],{"class":61,"line":769},[59,1694,1695],{},"        return loop.run_until_complete(main)           # 跑到完成\n",[59,1697,1698],{"class":61,"line":775},[59,1699,938],{},[59,1701,1702],{"class":61,"line":781},[59,1703,1704],{},"        loop.run_until_complete(loop.shutdown_asyncgens())  # 清理残留 task \u002F async gen\n",[59,1706,1707],{"class":61,"line":787},[59,1708,1709],{},"        asyncio.set_event_loop(None)\n",[59,1711,1712],{"class":61,"line":1358},[59,1713,1714],{},"        loop.close()                                   # 关\n",[99,1716,1717,1731],{},[102,1718,1719],{},[105,1720,1721,1723,1727],{},[108,1722],{},[108,1724,1725],{},[56,1726,1607],{},[108,1728,1729],{},[56,1730,1611],{},[117,1732,1733,1744,1755,1768,1779,1796],{},[105,1734,1735,1738,1741],{},[122,1736,1737],{},"抽象层级",[122,1739,1740],{},"底层，手动挡",[122,1742,1743],{},"高层，自动挡",[105,1745,1746,1749,1752],{},[122,1747,1748],{},"做什么",[122,1750,1751],{},"只创建 loop 对象",[122,1753,1754],{},"创建 + 运行 + 清理 + 关闭",[105,1756,1757,1760,1765],{},[122,1758,1759],{},"谁负责 close",[122,1761,1762],{},[37,1763,1764],{},"你自己",[122,1766,1767],{},"自动",[105,1769,1770,1773,1776],{},[122,1771,1772],{},"能否复用 loop",[122,1774,1775],{},"能（建一次跑多次）",[122,1777,1778],{},"不能（每次新建新关）",[105,1780,1781,1784,1787],{},[122,1782,1783],{},"能否嵌套",[122,1785,1786],{},"—（你自己管）",[122,1788,1789,1792,1793],{},[37,1790,1791],{},"不能","，运行中的 loop 里再调会 ",[56,1794,1795],{},"RuntimeError",[105,1797,1798,1801,1804],{},[122,1799,1800],{},"典型用途",[122,1802,1803],{},"框架\u002F库内部精细控制、子线程建 loop",[122,1805,1806],{},"应用程序入口，跑一个顶层协程",[19,1808,1809,1810],{},"一句话：",[37,1811,1812,1814,1815,796,1818,796,1820,1822],{},[56,1813,678],{}," ≈ ",[56,1816,1817],{},"new_event_loop",[56,1819,983],{},[56,1821,1054],{}," 的安全封装。",[183,1824,1826],{"id":1825},"_63-机器人框架设计两个-loop是否合理","6.3 机器人框架设计「两个 loop」是否合理？",[19,1828,1829,1830,1832],{},"回看 NcatBot 的 ",[56,1831,966],{},"，它确实先后用了两个：",[49,1834,1836],{"className":51,"code":1835,"language":53,"meta":54,"style":54},"run_coroutine(self.plugin_loader.load_plugins)    # 临时 loop ①：插件加载，跑完即弃\n...\nasyncio.run(self.adapter.connect_websocket())     # 长驻 loop ②：业务主循环\n",[56,1837,1838,1843,1848],{"__ignoreMap":54},[59,1839,1840],{"class":61,"line":62},[59,1841,1842],{},"run_coroutine(self.plugin_loader.load_plugins)    # 临时 loop ①：插件加载，跑完即弃\n",[59,1844,1845],{"class":61,"line":68},[59,1846,1847],{},"...\n",[59,1849,1850],{"class":61,"line":74},[59,1851,1852],{},"asyncio.run(self.adapter.connect_websocket())     # 长驻 loop ②：业务主循环\n",[19,1854,1855],{},"我的判断分两层：",[19,1857,1858,1861,1862,1865],{},[37,1859,1860],{},"✅ 合理的部分","：生命周期不同的阶段用不同 loop 是正当的。「一次性初始化」和「长期运行」本就是两个阶段，只要两个 loop ",[37,1863,1864],{},"不同时运行、而是先后接力","，工程上完全站得住脚。",[19,1867,1868,1871],{},[37,1869,1870],{},"⚠️ 不优雅的部分","：NcatBot 这个具体实现是历史包袱，而非精心设计。两个味道：",[265,1873,1874,1887],{},[268,1875,1876,1882,1883,1886],{},[37,1877,1878,1881],{},[56,1879,1880],{},"run_coroutine"," 自己都标了「已废弃」","——其源码注释直言「NcatBot 已重构为纯异步架构，此函数使用 ",[56,1884,1885],{},"asyncio.run()"," 会阻塞线程，违背异步并发原则」。即作者自己都认为「插件加载另起一个线程 + 临时 loop」不该保留。",[268,1888,1889,388,1892,1894,1895,1897],{},[37,1890,1891],{},"正是这个设计埋了定时任务的坑",[56,1893,888],{}," 跑在临时 loop ① 上，",[56,1896,341],{}," 起的扫描循环随 ① 关闭而冻结，而长驻的是 ②。统一一个 loop，这坑根本不存在。",[19,1899,1900,388],{},[37,1901,1902],{},"更优雅的设计是「单一长驻 loop」",[49,1904,1907],{"className":1905,"code":1906,"language":435,"meta":54},[433],"asyncio.run(main())\n  └─ loop（唯一且长驻）\n       ├─ await 初始化（加载插件、连数据库……）   # 同一个 loop\n       ├─ create_task(后台任务们)                  # 绑定到这个长驻 loop ✅\n       └─ await 主循环（while True 收消息）         # 同一个 loop\n",[56,1908,1906],{"__ignoreMap":54},[19,1910,1911],{},"一个 loop 从头跑到尾，所有初始化、后台任务、业务循环都挂在它身上，就不会「挂错 loop」。",[19,1913,1914,1915,1918,1919,1922,1923,1926,1927,1930],{},"那「多 loop」什么时候才是真正的合理设计、而非妥协？——",[37,1916,1917],{},"多线程隔离","（阻塞\u002FCPU 密集活丢子线程跑独立 loop）、",[37,1920,1921],{},"库与宿主集成","（某库内部要跑自己的 loop，靠 ",[56,1924,1925],{},"run_coroutine_threadsafe"," 跨 loop 通信）、",[37,1928,1929],{},"多进程隔离","（每进程一个 loop，靠 IPC）。这些是目的明确的架构选择；而 NcatBot 的两个 loop 更像「重构没做完的残留」。",[23,1932],{},[26,1934,1936],{"id":1935},"七小结","七、小结",[390,1938,1939,1945,1951,1961,1968],{},[268,1940,1941,1944],{},[37,1942,1943],{},"异步","用「等 I\u002FO 时让出 CPU」换来单线程的高并发，天生契合 QQ 机器人这种 I\u002FO 密集场景。",[268,1946,1947,1950],{},[37,1948,1949],{},"事件循环","是异步的心脏；协程靠它驱动，Task 绑定它调度，loop 一停任务即冻结。",[268,1952,1953,1956,1957,1960],{},[37,1954,1955],{},"JS\u002FTS"," 因渲染需求采用宏\u002F微任务双队列，",[37,1958,1959],{},"Python"," 用更扁平的单就绪队列 + 定时器堆，但上层心智一致。",[268,1962,1963,1964,1967],{},"真实框架往往",[37,1965,1966],{},"托管多个 loop","，分清「临时初始化 loop」与「长驻业务 loop」，是把长驻后台任务挂对地方的前提——这次定时任务 bug 就是活生生的教训。",[268,1969,1970,1971,1974,1975,1977,1978,1981,1982,181],{},"一个程序",[37,1972,1973],{},"可以有多个 loop","，但同线程同一刻只跑一个；",[56,1976,678],{}," 是 ",[56,1979,1980],{},"new_event_loop + run + close"," 的安全封装；",[37,1983,1984],{},"「单一长驻 loop」通常是比多 loop 更省心的设计",[19,1986,1987,1988,1991],{},"异步不难，难的是搞清楚「",[37,1989,1990],{},"此刻我的代码，跑在哪个 loop 上，那个 loop 还活着吗","」。",[1993,1994,1995],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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.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);}",{"title":54,"searchDepth":68,"depth":68,"links":1997},[1998,1999,2005,2009,2015,2027,2033],{"id":28,"depth":68,"text":29},{"id":161,"depth":68,"text":162,"children":2000},[2001,2002,2004],{"id":185,"depth":74,"text":185},{"id":254,"depth":74,"text":2003},"await 到底发生了什么",{"id":318,"depth":74,"text":319},{"id":365,"depth":68,"text":366,"children":2006},[2007,2008],{"id":380,"depth":74,"text":381},{"id":464,"depth":74,"text":465},{"id":601,"depth":68,"text":602,"children":2010},[2011,2012,2013,2014],{"id":608,"depth":74,"text":609},{"id":629,"depth":74,"text":630},{"id":660,"depth":74,"text":661},{"id":671,"depth":74,"text":672},{"id":721,"depth":68,"text":722,"children":2016},[2017,2018,2019,2021,2022,2024,2025,2026],{"id":728,"depth":74,"text":729},{"id":833,"depth":74,"text":834},{"id":874,"depth":74,"text":2020},"5.3 根因：create_task 挂错了 loop",{"id":1024,"depth":74,"text":1025},{"id":1066,"depth":74,"text":2023},"5.5 loop.run_until_complete vs run_frontend() 的本质区别",{"id":1203,"depth":74,"text":1204},{"id":1302,"depth":74,"text":1303},{"id":1441,"depth":74,"text":1442},{"id":1483,"depth":68,"text":1484,"children":2028},[2029,2030,2032],{"id":1490,"depth":74,"text":1491},{"id":1603,"depth":74,"text":2031},"6.2 new_event_loop() 和 asyncio.run(main()) 的区别",{"id":1825,"depth":74,"text":1826},{"id":1935,"depth":68,"text":1936},"\u002Fimages\u002Fposts\u002F26_06_09\u002Fcover.jpg","2026-06-09",false,"md",{},"\u002Fposts\u002F26_06_09\u002Fmain",{"title":6,"description":54},"posts\u002F26_06_09\u002Fmain","巩固对异步编程的理解，记录在AI Agent QQ机器人中定时任务的实现过程",[2044,2045],"notes","开发","nofOd1E3dwhXmWsl9CQr9zkrEOQWm3tQNJm5SsnS39o",{"id":2048,"title":2049,"body":2050,"cover":2122,"date":2123,"description":2054,"draft":2036,"extension":2037,"meta":2124,"navigation":1339,"path":2125,"seo":2126,"stem":2127,"summary":2128,"tags":2129,"__hash__":2132},"posts\u002Fposts\u002Flearning_with_ai\u002Fmain.md","关于AI时代的学习",{"type":8,"value":2051,"toc":2116},[2052,2055,2058,2061,2064,2067,2070,2073,2076,2082,2085,2088,2091,2094,2097,2100,2103,2106,2113],[19,2053,2054],{},"进入 AI 时代之后，很多原本需要上网狂暴搜索半天才能解决的问题，现在可能只要简单描述一下，十几秒就能得到一个看起来还不错的答案。比如报错信息看不懂、某个 API 不会用、想快速了解一个概念的背景，AI 往往都能给出比搜索引擎更直接的反馈。",[19,2056,2057],{},"但这并不意味着学习效率也会以同样夸张的幅度提升。信息获取变快了，不代表理解、内化和实践也会自动变快。很多时候，AI 能帮我们跨过“查资料”的门槛，却不能替我们完成“建立知识结构”和“形成经验”的过程。实际学习中，我们可能还会遇到下面这些困难。",[26,2059,2060],{"id":2060},"不知道自己不知道什么",[19,2062,2063],{},"学习一个新领域时，最难的往往不是某个具体问题，而是不知道这个领域到底由哪些问题组成。如果对整体体系没有概念，就很难向 AI 提出有效的问题。",[19,2065,2066],{},"例如刚开始学 TypeScript 时，可能只会问“TypeScript 怎么学？”或者“interface 和 type 有什么区别？”。这些问题当然能得到答案，但很容易停留在零散知识点上。真正影响长期理解的内容可能包括类型推导、泛型、结构化类型系统、类型收窄、工程配置、和 JavaScript 运行时之间的边界等。如果一开始完全不知道这些主题的存在，就很难主动问到它们。",[19,2068,2069],{},"所以 AI 更像是一个很强的问答对象，而不是天然可靠的学习地图。我们仍然需要先让它帮忙梳理领域轮廓，比如让它列出学习路线、核心概念、常见误区，再基于这些内容逐步深入。",[26,2071,2072],{"id":2072},"缺少实践",[19,2074,2075],{},"有些技能的掌握离不开实践，尤其是写代码。编程能力并不只是“知道某个语法怎么写”，还包括拆解问题、设计结构、定位 bug、理解报错、权衡取舍等一整套经验。",[19,2077,2078,2079,2081],{},"如果过于追求效率，把实现过程全部交给 AI，短期内确实可以更快得到一个能跑的结果，但自己的经验积累可能会被跳过。比如一个异步请求失败了，AI 可以直接告诉你要加 ",[56,2080,173],{},"、要处理异常、要检查接口返回值；但如果每次都只是复制修复方案，而没有自己观察现象、猜测原因、验证假设，那么下次遇到类似问题时，仍然很难独立判断。",[19,2083,2084],{},"更好的方式也许是把 AI 当成陪练：先自己尝试实现，遇到卡点时让 AI 提示思路；修好 bug 之后，再让它解释为什么这样改有效、还有没有其他写法。这样 AI 提供的是反馈和复盘，而不是直接替代练习。",[26,2086,2087],{"id":2087},"知识的诅咒",[19,2089,2090],{},"AI 也可能会有某种“知识的诅咒”：它默认自己知道的东西你也大概知道，于是解释时跳过了关键前提，或是抛出大量“越级”概念；或者反过来，它不知道你的真实基础，又花很多篇幅讲解你已经熟悉的内容。",[19,2092,2093],{},"比如学习 TypeScript 时，如果我已经有一些 Python 异步开发经验，那么“异步函数会返回 Promise”“await 会等待异步结果”这类解释可能就不需要太多篇幅。真正需要补足的，可能是 TypeScript 如何描述 Promise 的类型、泛型在异步函数中的作用、前端框架里异步状态该如何建模。可是如果没有主动说明这些背景，AI 很可能按通用初学者教程来讲，导致信息密度不合适。",[19,2095,2096],{},"因此，和 AI 学习时，描述自己的背景很重要。比起直接问“给我讲讲 TypeScript”，更好的提问可能是：“我有 Python 基础，了解 async\u002Fawait，但不熟悉前端工程和 TypeScript 类型系统。请跳过基础编程概念，重点讲 TypeScript 相比 Python 类型标注最不一样的地方。”",[26,2098,2099],{"id":2099},"个人尝试",[19,2101,2102],{},"目前我也在尝试探索一些更适合自己的 AI 学习方法。受 AI coding 的启发，我发现开发复杂任务时，通常不会一上来就让 AI 写代码，而是先讨论需求、澄清边界、拆分步骤，再进入具体实现。学习复杂知识时，也许可以采用类似的流程。",[19,2104,2105],{},"比如在开始学习一个新领域前，可以先与 AI 讨论，根据自己的背景制定学习计划：我已经会什么、不熟悉什么、目标是能读懂项目还是能独立开发、每天大概能投入多少时间。计划确定之后，再把学习过程拆成一个个小任务，每完成一部分就让 AI 帮忙检查理解是否准确，或者根据练习结果调整后续安排。",[19,2107,2108,2109,2112],{},"在这个过程中，还可以像维护 ",[56,2110,2111],{},"CLAUDE.md"," 一样维护一份“个人学习画像”：已有基础、常见误区、喜欢的解释方式、正在学习的主题、已经完成的练习等。这样 AI 不需要每次从零理解我是谁，也能更快进入合适的讲解深度。",[19,2114,2115],{},"当前正在尝试用这种方法快速学习网站前后端开发、工业级coding agent搭建，也作为该方法的一种实验看看效果如何（体验好的话感觉可以整个skill🥰）",{"title":54,"searchDepth":68,"depth":68,"links":2117},[2118,2119,2120,2121],{"id":2060,"depth":68,"text":2060},{"id":2072,"depth":68,"text":2072},{"id":2087,"depth":68,"text":2087},{"id":2099,"depth":68,"text":2099},"\u002Fimages\u002Fposts\u002Flearning_with_ai\u002Fcover.png","2026-05-27",{},"\u002Fposts\u002Flearning_with_ai\u002Fmain",{"title":2049,"description":2054},"posts\u002Flearning_with_ai\u002Fmain","AI时代如何更高效地学习？",[2130,2131],"杂谈","AI","JqXiYbhXLQh2poEKGXuVZ8IlImulklqOFSkFfW-bSQ0",{"id":2134,"title":2135,"body":2136,"cover":2527,"date":2528,"description":2140,"draft":2036,"extension":2037,"meta":2529,"navigation":1339,"path":2530,"seo":2531,"stem":2532,"summary":2533,"tags":2534,"__hash__":2536},"posts\u002Fposts\u002FFSDP\u002Fmain.md","FSDP-Zero123分片简介",{"type":8,"value":2137,"toc":2517},[2138,2141,2144,2151,2154,2157,2241,2251,2254,2257,2260,2263,2266,2274,2277,2281,2294,2300,2303,2306,2309,2312,2320,2324,2327,2333,2336,2345,2348,2351,2354,2358,2366,2372,2375,2383,2386,2389,2392,2395,2406,2410,2420,2426,2429,2435,2438,2442,2445,2514],[19,2139,2140],{},"随着模型越来越大，单卡显存已经无法满足训练需求，人们使用多种手段来扩大训练规模，如使用混合精度减少参数内存占用，或是使用多卡通信的方法进行并行训练。ZeRO 是常见的数据并行模型状态分片方案，思路为将模型权重、梯度、优化器状态等训练状态分片到多张卡上。",[19,2142,2143],{},"这里需要先区分 ZeRO 分片和 tensor parallel。ZeRO-3 也会分片模型权重，但它分片的是模型状态的常驻存储：每张卡平时只保存一部分权重，真正计算某一层时再临时 AllGather 出这一层需要的完整参数。tensor parallel 则是把一层里的矩阵乘、attention head、MLP hidden dim 等计算本身切开，多张卡共同完成同一个 batch 的同一层计算。ZeRO 的核心问题是“状态放不下，怎么存”，tensor parallel 的核心问题是“单层算不动，怎么切计算”。",[19,2145,2146],{},[2147,2148],"img",{"alt":2149,"src":2150},"全量微调一个7.5B模型时，显存中的参数组成，以及zero并行方案示意图","\u002Fimages\u002Fposts\u002FFSDP\u002Ffsdp_preview.png",[26,2152,2153],{"id":2153},"参数组成",[19,2155,2156],{},"假设使用AdamW + BF16 进行混合精度训练，则在训练过程中，需要存储的参数组成如下：",[99,2158,2159,2175],{},[102,2160,2161],{},[105,2162,2163,2166,2169,2172],{},[108,2164,2165],{},"组件",[108,2167,2168],{},"精度",[108,2170,2171],{},"每参数字节数",[108,2173,2174],{},"用途",[117,2176,2177,2191,2203,2217,2229],{},[105,2178,2179,2182,2185,2188],{},[122,2180,2181],{},"模型权重（计算用）",[122,2183,2184],{},"BF16",[122,2186,2187],{},"2B",[122,2189,2190],{},"前向\u002F反向计算",[105,2192,2193,2196,2198,2200],{},[122,2194,2195],{},"梯度",[122,2197,2184],{},[122,2199,2187],{},[122,2201,2202],{},"反向传播产出，供优化器更新参数",[105,2204,2205,2208,2211,2214],{},[122,2206,2207],{},"FP32 主权重（优化器）",[122,2209,2210],{},"FP32",[122,2212,2213],{},"4B",[122,2215,2216],{},"保存更高精度的参数副本，用于稳定更新",[105,2218,2219,2222,2224,2226],{},[122,2220,2221],{},"一阶动量 m_t",[122,2223,2210],{},[122,2225,2213],{},[122,2227,2228],{},"AdamW 对梯度均值的指数滑动平均",[105,2230,2231,2234,2236,2238],{},[122,2232,2233],{},"二阶动量 v_t",[122,2235,2210],{},[122,2237,2213],{},[122,2239,2240],{},"AdamW 对梯度平方的指数滑动平均",[19,2242,2243,2244,400,2247,2250],{},"BF16 模型权重主要服务于矩阵乘等实际计算，可以显著降低显存和带宽压力；梯度是反向传播阶段产生的临时训练信号，优化器会读取它来决定参数更新方向；FP32 主权重、",[56,2245,2246],{},"m_t",[56,2248,2249],{},"v_t"," 则属于 AdamW 的优化器状态，通常用更高精度保存，以减少长时间训练中的数值误差。",[19,2252,2253],{},"可见训练情况下，参数占用的大头在优化器状态上",[19,2255,2256],{},"不分片时：每张卡都持有完整的一份，即 N × (2+2+4+4+4) = 16N 字节（不算激活值）。",[26,2258,2259],{"id":2259},"训练流程梳理",[19,2261,2262],{},"先不考虑 ZeRO 分片，只看普通多卡 DDP 的训练流程。DDP 中每张 GPU 都持有一份完整的模型权重和优化器状态，但每张卡读取的数据不同。例如 8 卡训练时，一个 global batch 会被切成 8 份 micro-batch，每张卡只负责其中一份。前向传播时，每张卡用自己本地的完整 BF16 模型权重计算输出，并保存反向传播需要的激活值。",[19,2264,2265],{},"反向传播时，每张卡根据自己的 micro-batch 计算本地梯度。此时不同 GPU 上的梯度并不相同，因为它们看到的数据不同。如果直接用各自的梯度更新参数，8 张卡的模型副本会从这一步开始分叉，后续训练就不再等价于单个大 batch 的训练。因此 DDP 会在反向传播过程中对梯度做 AllReduce，把所有卡的梯度求和或平均，并把同步后的梯度写回每张卡本地。",[19,2267,2268,2269,1608,2271,2273],{},"梯度同步完成后，每张卡都拥有相同的模型权重、相同的平均梯度，以及相同的 AdamW 优化器状态。随后每张卡各自在本地执行 optimizer step，读取 BF16 梯度、FP32 主权重、",[56,2270,2246],{},[56,2272,2249],{},"，计算出完全相同的参数更新。因为更新前的状态相同、梯度也已经同步，所以更新后的模型副本仍然保持一致。",[19,2275,2276],{},"这个流程的好处是实现简单、计算并行度高，每张卡都可以独立完成完整模型的前向和反向。代价是显存冗余很大：模型权重、梯度和优化器状态在每张卡上都完整复制了一份。ZeRO 后续要优化的正是这部分冗余存储。",[26,2278,2280],{"id":2279},"zero-stage-1分片优化器状态","ZeRO Stage 1：分片优化器状态",[19,2282,2283,2284,400,2286,2288,2289,400,2291,2293],{},"ZeRO-1 先处理最大的一块冗余：优化器状态。在普通 DDP 里，每张卡不仅保存完整的 BF16 计算权重和 BF16 梯度，还保存完整的 FP32 主权重、",[56,2285,2246],{},[56,2287,2249],{},"。如果使用 AdamW，这三项合起来是 12N 字节，比 BF16 模型权重本身大很多。ZeRO-1 的做法是：BF16 模型权重和 BF16 梯度仍然完整复制在每张卡上，但把 FP32 主权重、",[56,2290,2246],{},[56,2292,2249],{}," 按参数范围切成 8 份，每张卡只负责其中 1 份。",[49,2295,2298],{"className":2296,"code":2297,"language":435},[433],"┌──────────────────────────────────────────────────┐\n│              8 张 GPU 各自持有                      │\n│                                                    │\n│  完整 BF16 模型权重           N × 2B              │\n│  完整 BF16 梯度               N × 2B              │\n│                                                    │\n│  分片优化器状态                                    │\n│     • 1\u002F8 的 FP32 主权重       N\u002F8 × 4B            │\n│     • 1\u002F8 的 m_t              N\u002F8 × 4B             │\n│     • 1\u002F8 的 v_t              N\u002F8 × 4B             │\n└──────────────────────────────────────────────────┘\n",[56,2299,2297],{"__ignoreMap":54},[19,2301,2302],{},"训练时，ZeRO-1 的前向和反向基本仍然像 DDP 一样执行。每张卡都有完整 BF16 权重，所以可以独立完成自己 micro-batch 的 forward\u002Fbackward；反向传播得到的梯度也会像普通 DDP 一样做 AllReduce，使每张卡都拿到完整的平均梯度。",[19,2304,2305],{},"区别发生在 optimizer step。由于 GPU0 只保存第 0 片参数的 FP32 主权重和 AdamW 动量，GPU1 只保存第 1 片，以此类推，所以每张卡只更新自己负责的那 1\u002F8 参数。更新完成后，各卡会把自己更新好的参数分片同步给其他卡，所有卡再把这些分片拼回本地完整的 BF16 模型权重。这样下一轮 forward 时，每张卡仍然能看到完整且一致的模型。",[19,2307,2308],{},"从通信角度看，ZeRO-1 仍然保留 DDP 的梯度 AllReduce；额外需要在参数更新后同步各自更新的参数分片。它的优点是对计算流程改动较小，因为 forward\u002Fbackward 期间每张卡仍有完整模型；缺点是梯度和 BF16 权重还没有省，显存优化只作用在优化器状态上。",[19,2310,2311],{},"每卡显存：2N + 2N + (4N+4N+4N)\u002F8 = 5.5N 字节（vs 原来 16N）",[19,2313,2314,2315,400,2317,2319],{},"节省的是：FP32 主权重、",[56,2316,2246],{},[56,2318,2249],{}," 这 12N 优化器状态从完整保存变成按卡分片。",[26,2321,2323],{"id":2322},"zero-stage-2分片优化器状态-梯度","ZeRO Stage 2：分片优化器状态 + 梯度",[19,2325,2326],{},"ZeRO-2 在 ZeRO-1 的基础上继续观察一个事实：既然每张卡只负责更新 1\u002F8 的参数，那么它其实也只需要这 1\u002F8 参数对应的平均梯度。Stage 1 中每张卡保留完整梯度，是为了和普通 DDP 的流程保持一致；Stage 2 则把这部分也分片掉。",[49,2328,2331],{"className":2329,"code":2330,"language":435},[433],"┌──────────────────────────────────────────────────┐\n│              8 张 GPU 各自持有                      │\n│                                                    │\n│  完整 BF16 模型权重           N × 2B              │\n│                                                    │\n│  分片梯度                                          │\n│     • 1\u002F8 的 BF16 梯度        N\u002F8 × 2B             │\n│                                                    │\n│  分片优化器状态                                    │\n│     • 1\u002F8 的 FP32 主权重       N\u002F8 × 4B            │\n│     • 1\u002F8 的 m_t              N\u002F8 × 4B             │\n│     • 1\u002F8 的 v_t              N\u002F8 × 4B             │\n└──────────────────────────────────────────────────┘\n",[56,2332,2330],{"__ignoreMap":54},[19,2334,2335],{},"普通 DDP 的梯度同步可以理解为“先把所有卡的梯度求和，再把完整结果发回每张卡”。ZeRO-2 把这个过程改成 Reduce-Scatter：仍然对所有卡的梯度求和或平均，但最终结果不是完整复制给每张卡，而是按参数范围 scatter 出去。GPU0 只拿到第 0 片平均梯度，GPU1 只拿到第 1 片平均梯度，刚好和各自保存的优化器状态对应。",[19,2337,2338,2339,2341,2342,2344],{},"这样做在数学上仍然等价于 DDP 的全局平均梯度更新，只是每张卡不再保存自己不负责的梯度部分。随后 optimizer step 和 ZeRO-1 类似：每张卡读取自己的梯度分片、FP32 主权重分片、",[56,2340,2246],{}," 分片和 ",[56,2343,2249],{}," 分片，更新自己负责的参数。因为 BF16 模型权重在 Stage 2 里仍然完整复制在每张卡上，所以更新后的参数分片仍需要同步给所有卡，让每张卡本地的完整 BF16 权重保持一致。",[19,2346,2347],{},"实际实现通常会按 bucket 或按 layer 边反向、边 Reduce-Scatter、边释放梯度缓存，而不是等整个模型的梯度都算完再统一处理。因此下面的公式更接近稳定状态下的模型状态占用；真实峰值还会受到 bucket 大小、梯度累积、通信重叠策略影响。",[19,2349,2350],{},"每卡显存：2N + (2N+4N+4N+4N)\u002F8 = 3.75N 字节",[19,2352,2353],{},"节省的是：在 Stage 1 基础上，梯度也从 2N → 0.25N。",[26,2355,2357],{"id":2356},"zero-stage-3分片一切优化器状态-梯度-模型权重","ZeRO Stage 3：分片一切（优化器状态 + 梯度 + 模型权重）",[19,2359,2360,2361,2341,2363,2365],{},"ZeRO-3 再进一步，把计算用的 BF16 模型权重也分片保存。到这一步，每张卡常驻的模型状态只剩自己负责的 1\u002F8：BF16 参数分片、BF16 梯度分片、FP32 主权重分片、",[56,2362,2246],{},[56,2364,2249],{}," 分片。它和 tensor parallel 的关键区别是：ZeRO-3 并不是把某个矩阵乘的计算永久切给不同 GPU，而是把参数平时分片存放；真正执行某个 module 的计算前，再临时收集出这个 module 需要的完整参数。",[49,2367,2370],{"className":2368,"code":2369,"language":435},[433],"┌──────────────────────────────────────────────────┐\n│              8 张 GPU 各自持有                      │\n│                                                    │\n│  分片 BF16 模型权重           N\u002F8 × 2B            │\n│  分片 BF16 梯度               N\u002F8 × 2B            │\n│  分片 FP32 主权重             N\u002F8 × 4B            │\n│  分片 m_t                     N\u002F8 × 4B            │\n│  分片 v_t                     N\u002F8 × 4B            │\n└──────────────────────────────────────────────────┘\n",[56,2371,2369],{"__ignoreMap":54},[19,2373,2374],{},"前向传播时，框架会在进入某一层或某个 FSDP unit 前，对这一段参数做 AllGather。AllGather 之后，每张卡临时拥有这一层的完整 BF16 权重，于是可以像普通数据并行一样，用自己的 micro-batch 完成这一层计算。计算结束后，非本卡负责的参数分片可以释放，只保留本卡原本负责的 shard。",[19,2376,2377,2378,400,2380,2382],{},"反向传播时也类似。计算某一层的 backward 需要对应的权重，因此可能再次 AllGather 这一层参数；得到梯度后，再用 Reduce-Scatter 把梯度规约并切回各个 rank。最终每张卡只保留自己负责的梯度分片，并用它更新本地的 FP32 主权重、",[56,2379,2246],{},[56,2381,2249],{}," 和 BF16 参数分片。",[19,2384,2385],{},"和 ZeRO-1\u002F2 不同，ZeRO-3 更新完参数后不需要把完整模型权重常驻同步到每张卡，因为每张卡本来就不保存完整权重。下一次 forward 需要某一层参数时，再从各卡当前持有的参数分片 AllGather 出来即可。这个设计把常驻显存压到最低，但把参数通信放进了 forward\u002Fbackward 的关键路径。",[19,2387,2388],{},"每卡显存：(2N+2N+4N+4N+4N)\u002F8 = 2N 字节",[19,2390,2391],{},"这个 2N 指的是理想情况下每卡常驻的模型状态。真实训练峰值会更高，因为某些时刻会临时持有当前层 AllGather 出来的完整参数、通信 bucket、prefetch 参数以及激活值。ZeRO-3 的核心取舍就是：用更多、更频繁的参数 AllGather，换取模型状态显存近似按数据并行卡数线性下降。",[26,2393,2394],{"id":2394},"通信代价的直觉",[390,2396,2397,2400,2403],{},[268,2398,2399],{},"ZeRO-1：梯度同步仍然接近普通 DDP，额外多了参数更新后的同步，整体通信压力通常可控。",[268,2401,2402],{},"ZeRO-2：用 Reduce-Scatter 替代 AllReduce 梯度同步，通信总量和 DDP 同阶，但每张卡只保留梯度分片。",[268,2404,2405],{},"ZeRO-3：前向和反向会按层 AllGather 参数，反向后再 Reduce-Scatter 梯度。它的显存收益最大，但速度更依赖网络带宽、分片粒度、通信计算重叠和框架实现。",[26,2407,2409],{"id":2408},"zero分片与张量并行组合使用","Zero分片与张量并行组合使用",[19,2411,2412,2413,400,2416,2419],{},"假设有 8 张 GPU，设置 ",[56,2414,2415],{},"tp_size=2",[56,2417,2418],{},"dp_size=4","，那么可以把相邻两张卡组成一个 TP group，共同切分同一个模型副本中的张量计算：",[49,2421,2424],{"className":2422,"code":2423,"language":435,"meta":54},[433],"TP groups:\n  [GPU0, GPU1], [GPU2, GPU3], [GPU4, GPU5], [GPU6, GPU7]\n",[56,2425,2423],{"__ignoreMap":54},[19,2427,2428],{},"这 4 个 TP group 之间再构成数据并行关系，每个 group 处理不同的 mini-batch。为了让同一个 tensor-parallel 分片位置之间同步梯度或分片优化器状态，通常会按 TP rank 位置组成 DP group：",[49,2430,2433],{"className":2431,"code":2432,"language":435,"meta":54},[433],"DP groups:\n  [GPU0, GPU2, GPU4, GPU6]  # 每个模型副本里的第 0 个 TP 分片\n  [GPU1, GPU3, GPU5, GPU7]  # 每个模型副本里的第 1 个 TP 分片\n",[56,2434,2432],{"__ignoreMap":54},[19,2436,2437],{},"这样，一层内部的计算由 TP group 内的两张卡协作完成；不同数据 batch 之间的梯度同步、优化器状态分片，则发生在 DP group 内。ZeRO\u002FFSDP 通常作用在 DP group 维度上，而不是替代 tensor parallel 的算子切分。",[26,2439,2441],{"id":2440},"fsdp-和-zero-的关系","FSDP 和 ZeRO 的关系",[19,2443,2444],{},"PyTorch FSDP 可以理解为 ZeRO 思路在 PyTorch 里的实现之一。常见对应关系是：",[99,2446,2447,2460],{},[102,2448,2449],{},[105,2450,2451,2454,2457],{},[108,2452,2453],{},"FSDP 策略",[108,2455,2456],{},"近似对应",[108,2458,2459],{},"说明",[117,2461,2462,2475,2488,2501],{},[105,2463,2464,2469,2472],{},[122,2465,2466],{},[56,2467,2468],{},"NO_SHARD",[122,2470,2471],{},"DDP",[122,2473,2474],{},"不分片参数、梯度和优化器状态",[105,2476,2477,2482,2485],{},[122,2478,2479],{},[56,2480,2481],{},"SHARD_GRAD_OP",[122,2483,2484],{},"ZeRO-2",[122,2486,2487],{},"分片梯度和优化器状态，参数在计算时保持完整",[105,2489,2490,2495,2498],{},[122,2491,2492],{},[56,2493,2494],{},"FULL_SHARD",[122,2496,2497],{},"ZeRO-3",[122,2499,2500],{},"分片参数、梯度和优化器状态",[105,2502,2503,2508,2511],{},[122,2504,2505],{},[56,2506,2507],{},"HYBRID_SHARD",[122,2509,2510],{},"ZeRO-3 + 层级分组",[122,2512,2513],{},"组内分片，组间复制，常用于多机多卡",[19,2515,2516],{},"实际训练时还需要额外考虑激活值、通信 bucket、临时 AllGather 出来的完整参数、CUDA allocator 碎片以及 checkpoint 保存方式，所以这些公式更适合用来建立显存组成的直觉，而不是直接等同于最终峰值显存。",{"title":54,"searchDepth":68,"depth":68,"links":2518},[2519,2520,2521,2522,2523,2524,2525,2526],{"id":2153,"depth":68,"text":2153},{"id":2259,"depth":68,"text":2259},{"id":2279,"depth":68,"text":2280},{"id":2322,"depth":68,"text":2323},{"id":2356,"depth":68,"text":2357},{"id":2394,"depth":68,"text":2394},{"id":2408,"depth":68,"text":2409},{"id":2440,"depth":68,"text":2441},"\u002Fimages\u002Fposts\u002FFSDP\u002Fcover.png","2026-04-16",{},"\u002Fposts\u002Ffsdp\u002Fmain",{"title":2135,"description":2140},"posts\u002FFSDP\u002Fmain","zero并行训练方案简介",[2044,2535],"后训练","LXnMqZsxJNTbq709BfEfelc-WUxUk4q9kvBXhrGS_GQ",{"id":2538,"title":2539,"body":2540,"cover":3905,"date":2528,"description":2544,"draft":2036,"extension":2037,"meta":3906,"navigation":1339,"path":3907,"seo":3908,"stem":3909,"summary":2539,"tags":3910,"__hash__":3911},"posts\u002Fposts\u002Fmix_precision\u002Fmain.md","混合精度训练简介",{"type":8,"value":2541,"toc":3899},[2542,2545,2548,2551,2843,2846,2849,2856,2859,2866,2876,2879,2882,2888,2891,3280,3283,3434,3437,3441,3587,3590,3698,3712,3715,3729,3733,3736,3739,3883,3886,3889,3896],[19,2543,2544],{},"传统的模型训练一般使用FP32精度进行训练。近年来，由于模型越来越大，我们希望在有限的显存中训练更大的模型，也希望能够提高训练速度，因此，人们开始研究如何在混合使用低精度的参数进行模型训练时，保持模型的性能。",[26,2546,2547],{"id":2547},"浮点数据类型介绍",[19,2549,2550],{},"要理解混合精度训练，最好先把“浮点数”这件事想清楚。计算机里存一个浮点数，并不是直接把十进制小数塞进去，而是把它拆成三块：符号位决定正负，指数位决定这个数大概落在哪个数量级，尾数位决定这个数量级里还能分得多细。粗略写出来就是：",[59,2552,2555,2657],{"className":2553},[2554],"katex",[59,2556,2559],{"className":2557},[2558],"katex-mathml",[2560,2561,2563],"math",{"xmlns":2562},"http:\u002F\u002Fwww.w3.org\u002F1998\u002FMath\u002FMathML",[2564,2565,2566,2652],"semantics",{},[2567,2568,2569,2574,2577,2581,2602,2605,2608,2611,2613,2616,2618,2620,2622,2624,2626],"mrow",{},[2570,2571,2573],"mo",{"stretchy":2572},"false","(",[2570,2575,2576],{},"−",[2578,2579,2580],"mn",{},"1",[2582,2583,2584,2587],"msup",{},[2570,2585,2586],{"stretchy":2572},")",[2567,2588,2589,2593,2596,2599],{},[2590,2591,2592],"mi",{},"s",[2590,2594,2595],{},"i",[2590,2597,2598],{},"g",[2590,2600,2601],{},"n",[2570,2603,2604],{},"×",[2590,2606,2607],{},"m",[2590,2609,2610],{},"a",[2590,2612,2601],{},[2590,2614,2615],{},"t",[2590,2617,2595],{},[2590,2619,2592],{},[2590,2621,2592],{},[2590,2623,2610],{},[2570,2625,2604],{},[2582,2627,2628,2631],{},[2578,2629,2630],{},"2",[2567,2632,2633,2636,2639,2641,2644,2646,2648,2650],{},[2590,2634,2635],{},"e",[2590,2637,2638],{},"x",[2590,2640,19],{},[2590,2642,2643],{},"o",[2590,2645,2601],{},[2590,2647,2635],{},[2590,2649,2601],{},[2590,2651,2615],{},[2653,2654,2656],"annotation",{"encoding":2655},"application\u002Fx-tex","(-1)^{sign} \\times mantissa \\times 2^{exponent}\n",[59,2658,2662,2750,2783],{"className":2659,"ariaHidden":2661},[2660],"katex-html","true",[59,2663,2666,2671,2675,2679,2682,2738,2743,2747],{"className":2664},[2665],"base",[59,2667],{"className":2668,"style":2670},[2669],"strut","height:1.0747em;vertical-align:-0.25em;",[59,2672,2573],{"className":2673},[2674],"mopen",[59,2676,2576],{"className":2677},[2678],"mord",[59,2680,2580],{"className":2681},[2678],[59,2683,2686,2689],{"className":2684},[2685],"mclose",[59,2687,2586],{"className":2688},[2685],[59,2690,2693],{"className":2691},[2692],"msupsub",[59,2694,2697],{"className":2695},[2696],"vlist-t",[59,2698,2701],{"className":2699},[2700],"vlist-r",[59,2702,2706],{"className":2703,"style":2705},[2704],"vlist","height:0.8247em;",[59,2707,2709,2714],{"style":2708},"top:-3.063em;margin-right:0.05em;",[59,2710],{"className":2711,"style":2713},[2712],"pstrut","height:2.7em;",[59,2715,2721],{"className":2716},[2717,2718,2719,2720],"sizing","reset-size6","size3","mtight",[59,2722,2724,2728,2731,2735],{"className":2723},[2678,2720],[59,2725,2592],{"className":2726},[2678,2727,2720],"mathnormal",[59,2729,2595],{"className":2730},[2678,2727,2720],[59,2732,2598],{"className":2733,"style":2734},[2678,2727,2720],"margin-right:0.0359em;",[59,2736,2601],{"className":2737},[2678,2727,2720],[59,2739],{"className":2740,"style":2742},[2741],"mspace","margin-right:0.2222em;",[59,2744,2604],{"className":2745},[2746],"mbin",[59,2748],{"className":2749,"style":2742},[2741],[59,2751,2753,2757,2761,2764,2767,2771,2774,2777,2780],{"className":2752},[2665],[59,2754],{"className":2755,"style":2756},[2669],"height:0.7429em;vertical-align:-0.0833em;",[59,2758,2760],{"className":2759},[2678,2727],"man",[59,2762,2615],{"className":2763},[2678,2727],[59,2765,2595],{"className":2766},[2678,2727],[59,2768,2770],{"className":2769},[2678,2727],"ss",[59,2772,2610],{"className":2773},[2678,2727],[59,2775],{"className":2776,"style":2742},[2741],[59,2778,2604],{"className":2779},[2746],[59,2781],{"className":2782,"style":2742},[2741],[59,2784,2786,2790],{"className":2785},[2665],[59,2787],{"className":2788,"style":2789},[2669],"height:0.7936em;",[59,2791,2793,2796],{"className":2792},[2678],[59,2794,2630],{"className":2795},[2678],[59,2797,2799],{"className":2798},[2692],[59,2800,2802],{"className":2801},[2696],[59,2803,2805],{"className":2804},[2700],[59,2806,2808],{"className":2807,"style":2789},[2704],[59,2809,2810,2813],{"style":2708},[59,2811],{"className":2812,"style":2713},[2712],[59,2814,2816],{"className":2815},[2717,2718,2719,2720],[59,2817,2819,2822,2825,2828,2831,2834,2837,2840],{"className":2818},[2678,2720],[59,2820,2635],{"className":2821},[2678,2727,2720],[59,2823,2638],{"className":2824},[2678,2727,2720],[59,2826,19],{"className":2827},[2678,2727,2720],[59,2829,2643],{"className":2830},[2678,2727,2720],[59,2832,2601],{"className":2833},[2678,2727,2720],[59,2835,2635],{"className":2836},[2678,2727,2720],[59,2838,2601],{"className":2839},[2678,2727,2720],[59,2841,2615],{"className":2842},[2678,2727,2720],[19,2844,2845],{},"所以同样是一个 16 bit 或 32 bit 的数字，指数位多一点，能表示的范围就更大；尾数位多一点，相邻两个可表示数字之间就更密，数值就更精细。混合精度训练里的很多选择，本质上都是在这两个方向之间做取舍：我们到底更怕数值溢出，还是更怕舍入误差。",[19,2847,2848],{},"FP32 是最传统的训练精度，它用 1 bit 存符号、8 bit 存指数、23 bit 存尾数，一共 4 bytes。它的好处是范围够大、精度也够细，所以不管是前向计算、反向梯度，还是优化器里长期累积的小更新，都比较稳。问题也很直接：每个参数 4 bytes，模型一大，权重、梯度、优化器状态很快就把显存吃完；同时 GPU 做矩阵乘时，低精度 Tensor Core 往往能给出更高吞吐，继续全程 FP32 就显得有点浪费。",[19,2850,2851,2852,2855],{},"FP16 看起来像是最自然的压缩版本：它只有 2 bytes，显存直接减半，而且 10 bit 尾数比 BF16 更细。但 FP16 把指数位压到了 5 bit，动态范围小很多；按照 IEEE FP16 的编码规则，它的最大有限值是 65504。训练里一旦激活值、loss scale 或梯度中间值稍微大一点，就容易上溢成 ",[56,2853,2854],{},"inf","；梯度太小的时候又容易下溢成 0。因此早期 FP16 训练通常要配合 loss scaling，把 loss 先放大再反传，尽量让梯度落在 FP16 能表示的区间里。",[19,2857,2858],{},"BF16 的设计思路刚好不一样。它同样是 2 bytes，但保留了和 FP32 一样的 8 bit 指数，只把尾数缩到了 7 bit。也就是说，BF16 的动态范围几乎和 FP32 一样，训练中不太容易因为量级问题直接爆掉；代价是同一个数量级里的刻度更粗，精度比 FP16 还低。这个取舍非常适合深度学习里的矩阵乘：神经网络本身对一点舍入噪声比较耐受，而大范围可以显著减少上溢、下溢问题。所以现在做大模型训练或微调时，BF16 往往比 FP16 更省心。",[19,2860,2861,2862,2865],{},"这也解释了为什么后文会把 BF16 放在前向和反向传播里，却仍然让优化器保存 FP32 主权重。前向\u002F反向主要是大批量矩阵乘和激活函数，低精度误差通常会被模型和 batch 噪声“吃掉”；优化器更新则是在做长期累加，很多时候是把一个很小的 ",[56,2863,2864],{},"lr * grad"," 加到一个已经存在的权重上。如果权重只保存在 BF16 里，这个微小更新可能连最小刻度都碰不到，直接被舍入掉。于是计算可以低精度，状态累积仍然要高精度，这就是混合精度训练里最核心的分工。",[19,2867,2868,2869,1608,2872,2875],{},"再往下压就是 FP8。PyTorch 里常见的两个 FP8 类型是 ",[56,2870,2871],{},"torch.float8_e4m3fn",[56,2873,2874],{},"torch.float8_e5m2","。前者可以理解成 4 bit 指数、3 bit 尾数，精度稍好但范围更小；后者是 5 bit 指数、2 bit 尾数，范围更大但刻度更粗。实际训练里，FP8 通常不会像 BF16 那样直接替换所有计算，而是要配合 scaling，把张量按块或按通道缩放到合适区间，再交给硬件做低精度矩阵乘。直觉上可以把它看成比 BF16 更激进的带宽和算力优化：收益更大，但对数值缩放、算子支持和训练框架的要求也更高。",[26,2877,2878],{"id":2878},"混合精度训练的完整工作流",[19,2880,2881],{},"下面以 AdamW + BF16 计算为例。先说明一下，这里讨论的是大模型训练里很常见的一种实现方式：前向、反向使用 BF16 权重和激活来降低显存与带宽压力，优化器内部仍然维护 FP32 主权重和 FP32 动量状态。不同框架的细节会有差异，比如 PyTorch AMP 可能让参数本体保持 FP32，只在算子执行时临时 autocast，但背后的数值逻辑是一样的：计算可以低精度，长期累积的训练状态最好保留高精度。",[19,2883,2884],{},[2147,2885],{"alt":2886,"src":2887},"混合精度训练流程（BF16计算，FP32优化器）","\u002Fimages\u002Fposts\u002Fmix_precision\u002Fmix_precision.png",[19,2889,2890],{},"如果只看模型状态，不考虑激活值，一个参数在 AdamW + BF16 混合精度全量微调中大致会占：",[59,2892,2894,2989],{"className":2893},[2554],[59,2895,2897],{"className":2896},[2558],[2560,2898,2899],{"xmlns":2562},[2564,2900,2901,2986],{},[2567,2902,2903,2905,2909,2911,2914,2916,2919,2921,2923,2925,2928,2930,2932,2935,2937,2939,2942,2944,2946,2948,2950,2952,2959,2961,2963,2965,2967,2969,2976,2978,2981,2984],{},[2578,2904,2630],{},[2906,2907,2908],"mtext",{},"B",[2570,2910,2573],{"stretchy":2572},[2906,2912,2913],{},"BF16 权重",[2570,2915,2586],{"stretchy":2572},[2570,2917,2918],{},"+",[2578,2920,2630],{},[2906,2922,2908],{},[2570,2924,2573],{"stretchy":2572},[2906,2926,2927],{},"BF16 梯度",[2570,2929,2586],{"stretchy":2572},[2570,2931,2918],{},[2578,2933,2934],{},"4",[2906,2936,2908],{},[2570,2938,2573],{"stretchy":2572},[2906,2940,2941],{},"FP32 主权重",[2570,2943,2586],{"stretchy":2572},[2570,2945,2918],{},[2578,2947,2934],{},[2906,2949,2908],{},[2570,2951,2573],{"stretchy":2572},[2953,2954,2955,2957],"msub",{},[2590,2956,2607],{},[2590,2958,2615],{},[2570,2960,2586],{"stretchy":2572},[2570,2962,2918],{},[2578,2964,2934],{},[2906,2966,2908],{},[2570,2968,2573],{"stretchy":2572},[2953,2970,2971,2974],{},[2590,2972,2973],{},"v",[2590,2975,2615],{},[2570,2977,2586],{"stretchy":2572},[2570,2979,2980],{},"=",[2578,2982,2983],{},"16",[2906,2985,2908],{},[2653,2987,2988],{"encoding":2655},"2\\text{B}(\\text{BF16 权重}) + 2\\text{B}(\\text{BF16 梯度}) + 4\\text{B}(\\text{FP32 主权重}) + 4\\text{B}(m_t) + 4\\text{B}(v_t) = 16\\text{B}\n",[59,2990,2992,3035,3074,3115,3191,3264],{"className":2991,"ariaHidden":2661},[2660],[59,2993,2995,2999,3002,3008,3011,3023,3026,3029,3032],{"className":2994},[2665],[59,2996],{"className":2997,"style":2998},[2669],"height:1em;vertical-align:-0.25em;",[59,3000,2630],{"className":3001},[2678],[59,3003,3005],{"className":3004},[2678,435],[59,3006,2908],{"className":3007},[2678],[59,3009,2573],{"className":3010},[2674],[59,3012,3014,3018],{"className":3013},[2678,435],[59,3015,3017],{"className":3016},[2678],"BF16 ",[59,3019,3022],{"className":3020},[2678,3021],"cjk_fallback","权重",[59,3024,2586],{"className":3025},[2685],[59,3027],{"className":3028,"style":2742},[2741],[59,3030,2918],{"className":3031},[2746],[59,3033],{"className":3034,"style":2742},[2741],[59,3036,3038,3041,3044,3050,3053,3062,3065,3068,3071],{"className":3037},[2665],[59,3039],{"className":3040,"style":2998},[2669],[59,3042,2630],{"className":3043},[2678],[59,3045,3047],{"className":3046},[2678,435],[59,3048,2908],{"className":3049},[2678],[59,3051,2573],{"className":3052},[2674],[59,3054,3056,3059],{"className":3055},[2678,435],[59,3057,3017],{"className":3058},[2678],[59,3060,2195],{"className":3061},[2678,3021],[59,3063,2586],{"className":3064},[2685],[59,3066],{"className":3067,"style":2742},[2741],[59,3069,2918],{"className":3070},[2746],[59,3072],{"className":3073,"style":2742},[2741],[59,3075,3077,3080,3083,3089,3092,3103,3106,3109,3112],{"className":3076},[2665],[59,3078],{"className":3079,"style":2998},[2669],[59,3081,2934],{"className":3082},[2678],[59,3084,3086],{"className":3085},[2678,435],[59,3087,2908],{"className":3088},[2678],[59,3090,2573],{"className":3091},[2674],[59,3093,3095,3099],{"className":3094},[2678,435],[59,3096,3098],{"className":3097},[2678],"FP32 ",[59,3100,3102],{"className":3101},[2678,3021],"主权重",[59,3104,2586],{"className":3105},[2685],[59,3107],{"className":3108,"style":2742},[2741],[59,3110,2918],{"className":3111},[2746],[59,3113],{"className":3114,"style":2742},[2741],[59,3116,3118,3121,3124,3130,3133,3179,3182,3185,3188],{"className":3117},[2665],[59,3119],{"className":3120,"style":2998},[2669],[59,3122,2934],{"className":3123},[2678],[59,3125,3127],{"className":3126},[2678,435],[59,3128,2908],{"className":3129},[2678],[59,3131,2573],{"className":3132},[2674],[59,3134,3136,3139],{"className":3135},[2678],[59,3137,2607],{"className":3138},[2678,2727],[59,3140,3142],{"className":3141},[2692],[59,3143,3146,3170],{"className":3144},[2696,3145],"vlist-t2",[59,3147,3149,3165],{"className":3148},[2700],[59,3150,3153],{"className":3151,"style":3152},[2704],"height:0.2806em;",[59,3154,3156,3159],{"style":3155},"top:-2.55em;margin-left:0em;margin-right:0.05em;",[59,3157],{"className":3158,"style":2713},[2712],[59,3160,3162],{"className":3161},[2717,2718,2719,2720],[59,3163,2615],{"className":3164},[2678,2727,2720],[59,3166,3169],{"className":3167},[3168],"vlist-s","​",[59,3171,3173],{"className":3172},[2700],[59,3174,3177],{"className":3175,"style":3176},[2704],"height:0.15em;",[59,3178],{},[59,3180,2586],{"className":3181},[2685],[59,3183],{"className":3184,"style":2742},[2741],[59,3186,2918],{"className":3187},[2746],[59,3189],{"className":3190,"style":2742},[2741],[59,3192,3194,3197,3200,3206,3209,3250,3253,3257,3261],{"className":3193},[2665],[59,3195],{"className":3196,"style":2998},[2669],[59,3198,2934],{"className":3199},[2678],[59,3201,3203],{"className":3202},[2678,435],[59,3204,2908],{"className":3205},[2678],[59,3207,2573],{"className":3208},[2674],[59,3210,3212,3215],{"className":3211},[2678],[59,3213,2973],{"className":3214,"style":2734},[2678,2727],[59,3216,3218],{"className":3217},[2692],[59,3219,3221,3242],{"className":3220},[2696,3145],[59,3222,3224,3239],{"className":3223},[2700],[59,3225,3227],{"className":3226,"style":3152},[2704],[59,3228,3230,3233],{"style":3229},"top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;",[59,3231],{"className":3232,"style":2713},[2712],[59,3234,3236],{"className":3235},[2717,2718,2719,2720],[59,3237,2615],{"className":3238},[2678,2727,2720],[59,3240,3169],{"className":3241},[3168],[59,3243,3245],{"className":3244},[2700],[59,3246,3248],{"className":3247,"style":3176},[2704],[59,3249],{},[59,3251,2586],{"className":3252},[2685],[59,3254],{"className":3255,"style":3256},[2741],"margin-right:0.2778em;",[59,3258,2980],{"className":3259},[3260],"mrel",[59,3262],{"className":3263,"style":3256},[2741],[59,3265,3267,3271,3274],{"className":3266},[2665],[59,3268],{"className":3269,"style":3270},[2669],"height:0.6833em;",[59,3272,2983],{"className":3273},[2678],[59,3275,3277],{"className":3276},[2678,435],[59,3278,2908],{"className":3279},[2678],[19,3281,3282],{},"所以一个常用的粗估是：",[59,3284,3286,3324],{"className":3285},[2554],[59,3287,3289],{"className":3288},[2558],[2560,3290,3291],{"xmlns":2562},[2564,3292,3293,3321],{},[2567,3294,3295,3298,3301,3304,3306,3308,3311,3313,3316,3318],{},[2906,3296,3297],{},"显存",[2570,3299,3300],{},"≈",[2906,3302,3303],{},"参数量",[2570,3305,2604],{},[2578,3307,2983],{},[2906,3309,3310],{}," bytes",[2570,3312,2918],{},[2906,3314,3315],{},"激活值",[2570,3317,2918],{},[2906,3319,3320],{},"临时 buffer",[2653,3322,3323],{"encoding":2655},"\\text{显存} \\approx \\text{参数量} \\times 16\\text{ bytes} + \\text{激活值} + \\text{临时 buffer}\n",[59,3325,3327,3348,3370,3395,3416],{"className":3326,"ariaHidden":2661},[2660],[59,3328,3330,3333,3339,3342,3345],{"className":3329},[2665],[59,3331],{"className":3332,"style":3270},[2669],[59,3334,3336],{"className":3335},[2678,435],[59,3337,3297],{"className":3338},[2678,3021],[59,3340],{"className":3341,"style":3256},[2741],[59,3343,3300],{"className":3344},[3260],[59,3346],{"className":3347,"style":3256},[2741],[59,3349,3351,3355,3361,3364,3367],{"className":3350},[2665],[59,3352],{"className":3353,"style":3354},[2669],"height:0.7667em;vertical-align:-0.0833em;",[59,3356,3358],{"className":3357},[2678,435],[59,3359,3303],{"className":3360},[2678,3021],[59,3362],{"className":3363,"style":2742},[2741],[59,3365,2604],{"className":3366},[2746],[59,3368],{"className":3369,"style":2742},[2741],[59,3371,3373,3377,3380,3386,3389,3392],{"className":3372},[2665],[59,3374],{"className":3375,"style":3376},[2669],"height:0.8889em;vertical-align:-0.1944em;",[59,3378,2983],{"className":3379},[2678],[59,3381,3383],{"className":3382},[2678,435],[59,3384,3310],{"className":3385},[2678],[59,3387],{"className":3388,"style":2742},[2741],[59,3390,2918],{"className":3391},[2746],[59,3393],{"className":3394,"style":2742},[2741],[59,3396,3398,3401,3407,3410,3413],{"className":3397},[2665],[59,3399],{"className":3400,"style":3354},[2669],[59,3402,3404],{"className":3403},[2678,435],[59,3405,3315],{"className":3406},[2678,3021],[59,3408],{"className":3409,"style":2742},[2741],[59,3411,2918],{"className":3412},[2746],[59,3414],{"className":3415,"style":2742},[2741],[59,3417,3419,3423],{"className":3418},[2665],[59,3420],{"className":3421,"style":3422},[2669],"height:0.6944em;",[59,3424,3426,3430],{"className":3425},[2678,435],[59,3427,3429],{"className":3428},[2678,3021],"临时",[59,3431,3433],{"className":3432},[2678]," buffer",[19,3435,3436],{},"实际估算时我一般会把模型状态按 16 到 18 bytes\u002Fparam 留余量，因为不同实现里还会有参数对齐、通信 bucket、梯度临时副本、fused optimizer workspace 等额外开销。真正容易被低估的往往是激活值，它和 sequence length、micro batch size、是否开启 activation checkpointing 强相关；有时候模型状态算得很准，最后还是爆在激活值上。",[26,3438,3440],{"id":3439},"为什么优化器必须保存-fp32-主权重","为什么优化器必须保存 FP32 主权重？",[19,3442,3443,3444,3515,3516,3586],{},"最核心的原因是权重更新通常非常小，而 BF16 在某个数值附近能分辨的刻度又比较粗。BF16 只有 7 bit 显式尾数，算上隐含的最高位，大约可以理解成 8 bit 有效精度；在 1.0 附近，相邻两个 BF16 数之间的间隔约为 ",[59,3445,3447,3470],{"className":3446},[2554],[59,3448,3450],{"className":3449},[2558],[2560,3451,3452],{"xmlns":2562},[2564,3453,3454,3467],{},[2567,3455,3456],{},[2582,3457,3458,3460],{},[2578,3459,2630],{},[2567,3461,3462,3464],{},[2570,3463,2576],{},[2578,3465,3466],{},"7",[2653,3468,3469],{"encoding":2655},"2^{-7}",[59,3471,3473],{"className":3472,"ariaHidden":2661},[2660],[59,3474,3476,3480],{"className":3475},[2665],[59,3477],{"className":3478,"style":3479},[2669],"height:0.8141em;",[59,3481,3483,3486],{"className":3482},[2678],[59,3484,2630],{"className":3485},[2678],[59,3487,3489],{"className":3488},[2692],[59,3490,3492],{"className":3491},[2696],[59,3493,3495],{"className":3494},[2700],[59,3496,3498],{"className":3497,"style":3479},[2704],[59,3499,3500,3503],{"style":2708},[59,3501],{"className":3502,"style":2713},[2712],[59,3504,3506],{"className":3505},[2717,2718,2719,2720],[59,3507,3509,3512],{"className":3508},[2678,2720],[59,3510,2576],{"className":3511},[2678,2720],[59,3513,3466],{"className":3514},[2678,2720],"，也就是 0.0078125。FP32 的尾数细得多，在 1.0 附近的间隔约为 ",[59,3517,3519,3542],{"className":3518},[2554],[59,3520,3522],{"className":3521},[2558],[2560,3523,3524],{"xmlns":2562},[2564,3525,3526,3539],{},[2567,3527,3528],{},[2582,3529,3530,3532],{},[2578,3531,2630],{},[2567,3533,3534,3536],{},[2570,3535,2576],{},[2578,3537,3538],{},"23",[2653,3540,3541],{"encoding":2655},"2^{-23}",[59,3543,3545],{"className":3544,"ariaHidden":2661},[2660],[59,3546,3548,3551],{"className":3547},[2665],[59,3549],{"className":3550,"style":3479},[2669],[59,3552,3554,3557],{"className":3553},[2678],[59,3555,2630],{"className":3556},[2678],[59,3558,3560],{"className":3559},[2692],[59,3561,3563],{"className":3562},[2696],[59,3564,3566],{"className":3565},[2700],[59,3567,3569],{"className":3568,"style":3479},[2704],[59,3570,3571,3574],{"style":2708},[59,3572],{"className":3573,"style":2713},[2712],[59,3575,3577],{"className":3576},[2717,2718,2719,2720],[59,3578,3580,3583],{"className":3579},[2678,2720],[59,3581,2576],{"className":3582},[2678,2720],[59,3584,3538],{"className":3585},[2678,2720],"，也就是 1.19e-7。",[19,3588,3589],{},"训练里一次参数更新大概长这样：",[59,3591,3593,3631],{"className":3592},[2554],[59,3594,3596],{"className":3595},[2558],[2560,3597,3598],{"xmlns":2562},[2564,3599,3600,3628],{},[2567,3601,3602,3606,3609,3611,3614,3617,3619,3621,3623,3625],{},[2590,3603,3605],{"mathvariant":3604},"normal","Δ",[2590,3607,3608],{},"w",[2570,3610,2980],{},[2590,3612,3613],{},"l",[2590,3615,3616],{},"r",[2570,3618,2604],{},[2590,3620,2598],{},[2590,3622,3616],{},[2590,3624,2610],{},[2590,3626,3627],{},"d",[2653,3629,3630],{"encoding":2655},"\\Delta w = lr \\times grad\n",[59,3632,3634,3656,3680],{"className":3633,"ariaHidden":2661},[2660],[59,3635,3637,3640,3643,3647,3650,3653],{"className":3636},[2665],[59,3638],{"className":3639,"style":3270},[2669],[59,3641,3605],{"className":3642},[2678],[59,3644,3608],{"className":3645,"style":3646},[2678,2727],"margin-right:0.0269em;",[59,3648],{"className":3649,"style":3256},[2741],[59,3651,2980],{"className":3652},[3260],[59,3654],{"className":3655,"style":3256},[2741],[59,3657,3659,3663,3667,3671,3674,3677],{"className":3658},[2665],[59,3660],{"className":3661,"style":3662},[2669],"height:0.7778em;vertical-align:-0.0833em;",[59,3664,3613],{"className":3665,"style":3666},[2678,2727],"margin-right:0.0197em;",[59,3668,3616],{"className":3669,"style":3670},[2678,2727],"margin-right:0.0278em;",[59,3672],{"className":3673,"style":2742},[2741],[59,3675,2604],{"className":3676},[2746],[59,3678],{"className":3679,"style":2742},[2741],[59,3681,3683,3686,3689,3692,3695],{"className":3682},[2665],[59,3684],{"className":3685,"style":3376},[2669],[59,3687,2598],{"className":3688,"style":2734},[2678,2727],[59,3690,3616],{"className":3691,"style":3670},[2678,2727],[59,3693,2610],{"className":3694},[2678,2727],[59,3696,3627],{"className":3697},[2678,2727],[19,3699,3700,3701,3704,3705,3708,3709,181],{},"如果学习率是 1e-5，梯度量级又差不多是 1，那么这一步更新就是 1e-5。对于 FP32 来说，这个变化明显大于 1.0 附近的最小刻度，所以权重会真实发生变化；虽然十进制的 ",[56,3702,3703],{},"1.00001"," 本身不一定能被二进制浮点数精确表示，但这个更新不会被整个抹掉。对于 BF16 来说就不同了，1e-5 远小于 0.0078125，",[56,3706,3707],{},"1.0 + 0.00001"," 四舍五入之后仍然很可能回到 ",[56,3710,3711],{},"1.0",[19,3713,3714],{},"这就是所谓的 swamping problem：大数加小数时，小数被低精度格式的刻度吞掉。它麻烦的地方不在于某一步误差稍微大一点，而在于训练本来就是几千步、几万步的小更新累积。如果每一步都在 BF16 权重上直接更新，大量微小变化会反复消失，模型看起来在反传，实际上参数没有按预期移动。",[19,3716,3717,3718,1608,3720,3722,3723,3725,3726,3728],{},"AdamW 里的 ",[56,3719,2246],{},[56,3721,2249],{}," 也有类似问题。它们是梯度的一阶、二阶指数滑动平均，本质上也是长期状态。如果这些状态本身就用低精度保存，早期的细小变化和后期的微调信号都会更容易被量化误差污染。所以常见做法是：计算权重可以是 BF16，优化器读到梯度后，仍然在 FP32 主权重、FP32 ",[56,3724,2246],{}," 和 FP32 ",[56,3727,2249],{}," 上完成更新，再把新的权重同步回低精度计算副本。",[26,3730,3732],{"id":3731},"为什么前向反向传播可以用-bf16","为什么前向\u002F反向传播可以用 BF16？",[19,3734,3735],{},"看起来这好像有点矛盾：既然优化器更新这么怕 BF16，为什么前向和反向又能用 BF16？关键在于，这两类计算对误差的敏感方式不一样。",[19,3737,3738],{},"前向传播主要是一层层矩阵乘、归一化、激活函数和残差连接：",[59,3740,3742,3794],{"className":3741},[2554],[59,3743,3745],{"className":3744},[2558],[2560,3746,3747],{"xmlns":2562},[2564,3748,3749,3791],{},[2567,3750,3751,3754,3756,3758,3761,3763,3765,3767,3769,3771,3773,3775,3777,3779,3782,3784,3786,3789],{},[2590,3752,3753],{},"y",[2570,3755,2980],{},[2590,3757,2610],{},[2590,3759,3760],{},"c",[2590,3762,2615],{},[2590,3764,2595],{},[2590,3766,2973],{},[2590,3768,2610],{},[2590,3770,2615],{},[2590,3772,2595],{},[2590,3774,2643],{},[2590,3776,2601],{},[2570,3778,2573],{"stretchy":2572},[2590,3780,3781],{},"W",[2590,3783,2638],{},[2570,3785,2918],{},[2590,3787,3788],{},"b",[2570,3790,2586],{"stretchy":2572},[2653,3792,3793],{"encoding":2655},"y = activation(Wx + b)\n",[59,3795,3797,3816,3871],{"className":3796,"ariaHidden":2661},[2660],[59,3798,3800,3804,3807,3810,3813],{"className":3799},[2665],[59,3801],{"className":3802,"style":3803},[2669],"height:0.625em;vertical-align:-0.1944em;",[59,3805,3753],{"className":3806,"style":2734},[2678,2727],[59,3808],{"className":3809,"style":3256},[2741],[59,3811,2980],{"className":3812},[3260],[59,3814],{"className":3815,"style":3256},[2741],[59,3817,3819,3822,3825,3828,3831,3834,3837,3840,3843,3846,3849,3852,3855,3859,3862,3865,3868],{"className":3818},[2665],[59,3820],{"className":3821,"style":2998},[2669],[59,3823,2610],{"className":3824},[2678,2727],[59,3826,3760],{"className":3827},[2678,2727],[59,3829,2615],{"className":3830},[2678,2727],[59,3832,2595],{"className":3833},[2678,2727],[59,3835,2973],{"className":3836,"style":2734},[2678,2727],[59,3838,2610],{"className":3839},[2678,2727],[59,3841,2615],{"className":3842},[2678,2727],[59,3844,2595],{"className":3845},[2678,2727],[59,3847,2643],{"className":3848},[2678,2727],[59,3850,2601],{"className":3851},[2678,2727],[59,3853,2573],{"className":3854},[2674],[59,3856,3781],{"className":3857,"style":3858},[2678,2727],"margin-right:0.1389em;",[59,3860,2638],{"className":3861},[2678,2727],[59,3863],{"className":3864,"style":2742},[2741],[59,3866,2918],{"className":3867},[2746],[59,3869],{"className":3870,"style":2742},[2741],[59,3872,3874,3877,3880],{"className":3873},[2665],[59,3875],{"className":3876,"style":2998},[2669],[59,3878,3788],{"className":3879},[2678,2727],[59,3881,2586],{"className":3882},[2685],[19,3884,3885],{},"BF16 和 FP32 一样有 8 bit exponent，动态范围接近 FP32，因此相比 FP16 更不容易因为量级问题上溢或下溢。这里不能说“不会溢出”，极端输入、异常 loss、错误的初始化照样会把数值打爆；更准确的说法是，BF16 把 FP16 最头疼的动态范围问题缓和了很多。尾数少带来的舍入误差依然存在，但神经网络训练本身就带有 mini-batch 采样噪声、dropout、数据增强等随机性，很多局部舍入误差不会像优化器状态那样长期原封不动地累积。",[19,3887,3888],{},"反向传播也类似。梯度当然不是越粗越好，但它本身就是从一个 mini-batch 估计出来的随机量，我们通常更关心整体方向和统计趋势，而不是每个元素最后几位小数完全准确。更重要的是，现代 GPU 上很多低精度矩阵乘并不是“从头到尾都 BF16”：常见路径是 BF16 输入，内部用更高精度做累加，再输出 BF16 或 FP32 结果。框架也经常会让 softmax、LayerNorm、loss 计算这类敏感算子保留更高精度。",[19,3890,3891,3892,3895],{},"所以这里不能简单概括成“乘法可以低精度，累加必须高精度”。矩阵乘本身就包含大量累加，只是这些累加通常发生在受控的算子内部，并且硬件和框架会用更稳的 accumulate 精度处理。优化器更新的问题更特殊：它是在同一个持久化权重上反复加一个很小的增量，",[56,3893,3894],{},"w = w + lr * grad"," 这种“大数吃小数”的场景正好是低尾数精度最容易翻车的地方。",[19,3897,3898],{},"因此混合精度训练的直觉可以总结成一句话：把吞吐量最大的前向\u002F反向计算交给 BF16，把最怕长期量化误差的优化器状态留在 FP32。这样既吃到低精度硬件的速度和显存收益，又尽量不破坏训练过程中真正需要细粒度累积的部分。",{"title":54,"searchDepth":68,"depth":68,"links":3900},[3901,3902,3903,3904],{"id":2547,"depth":68,"text":2547},{"id":2878,"depth":68,"text":2878},{"id":3439,"depth":68,"text":3440},{"id":3731,"depth":68,"text":3732},"\u002Fimages\u002Fposts\u002Fmix_precision\u002Fcover.png",{},"\u002Fposts\u002Fmix_precision\u002Fmain",{"title":2539,"description":2544},"posts\u002Fmix_precision\u002Fmain",[2044,2535],"S-4aoM7SUoMsqd7d8Gp3rk4W3X4J9vb1xRMS7Nnztfw",{"id":3913,"title":3914,"body":3915,"cover":4234,"date":4235,"description":54,"draft":2036,"extension":2037,"meta":4236,"navigation":1339,"path":4237,"seo":4238,"stem":4239,"summary":4240,"tags":4241,"__hash__":4242},"posts\u002Fposts\u002Fverl_0\u002Fmain.md","verl调参指南",{"type":8,"value":3916,"toc":4221},[3917,3921,3924,3935,3945,3951,3954,3960,3968,3980,3989,3998,4000,4004,4013,4017,4024,4033,4048,4052,4059,4069,4086,4092,4094,4098,4105,4109,4114,4128,4137,4141,4147,4168,4175,4179,4189],[11,3918,3920],{"id":3919},"grpo-训练与调参梳理","GRPO 训练与调参梳理",[26,3922,3923],{"id":3923},"前言",[19,3925,3926,3927,3930,3931,3934],{},"不久前尝试复现了一个 GRPO 训练 Qwen2.5-VL-7B 的工作，当时面对 ",[56,3928,3929],{},"verl"," 繁多的参数，看个文档也是一知半解，简单粗暴地改小了 ",[56,3932,3933],{},"train_batch_size"," 试图增加 logging 频率，结果没练几步就爆了。",[19,3936,3937,3941],{},[2147,3938],{"alt":3939,"src":3940},"github issue 0","\u002Fimages\u002Fposts\u002Fverl_0\u002Fgrpo_0.png",[2147,3942],{"alt":3943,"src":3944},"github issue 1","\u002Fimages\u002Fposts\u002Fverl_0\u002Fgrpo_1.png",[19,3946,3947,3948,3950],{},"这次重回 ",[56,3949,3929],{}," 进行训练，决定先把这些参数背后的逻辑彻底理顺。",[26,3952,3953],{"id":3953},"概念梳理",[19,3955,3956,3957,3959],{},"在聊流程之前，我们得先搞清楚 ",[56,3958,3929],{}," 里这几个容易打架的 Batch Size 概念。这几个参数如果不区分清楚，后面的逻辑很难盘得通：",[19,3961,3962,3963,3967],{},"首先是 ",[37,3964,3965],{},[56,3966,3933],{},"，这是宏观层面的一次“采样大部队”。它决定了在一次采样阶段（Rollout），模型总共要处理多少个不同的 Prompt。",[19,3969,3970,3971,3976,3977,181],{},"其次是 ",[37,3972,3973],{},[56,3974,3975],{},"rollout.n","，也就是 Group Size。GRPO 的特点就是对同一个 Prompt 生成多个回复（Group），然后组内比较优势。所以，实际上模型在采样阶段产生的数据总量是 ",[56,3978,3979],{},"train_batch_size * rollout.n",[19,3981,3982,3983,3988],{},"有了数据就要训练，这就轮到 ",[37,3984,3985],{},[56,3986,3987],{},"ppo_mini_batch_size"," 了。这才是优化器（Optimizer）真正关心的参数——每次更新权重时“吃”多少数据。",[19,3990,3991,3992,3997],{},"至于 ",[37,3993,3994],{},[56,3995,3996],{},"micro_batch_size","，它纯粹是为了照顾显存的。如果显卡塞不下 mini batch，就切得更碎一点用梯度累加（Gradient Accumulation）来凑，它不影响训练逻辑。",[23,3999],{},[26,4001,4003],{"id":4002},"grpo-的两阶段循环","GRPO 的两阶段循环",[19,4005,4006,4007,1608,4010,181],{},"GRPO 的训练本质上就是两个阶段的无限循环：",[37,4008,4009],{},"造数据（Rollout）",[37,4011,4012],{},"吃数据（Update）",[183,4014,4016],{"id":4015},"第一阶段造数据-rollout","第一阶段：造数据 (Rollout)",[19,4018,4019,4020,4023],{},"这个时候模型是冻结的（",[56,4021,4022],{},"eval"," 模式），它的任务就是根据当前的策略去“见世面”。",[19,4025,4026,4027,4029,4030,4032],{},"系统会从数据集中捞出 ",[56,4028,3933],{}," 个 Prompt，然后让模型对着每个 Prompt 生成 ",[56,4031,3975],{}," 个回答。",[19,4034,4035,4036,4039,4040,4043,4044,4047],{},"生成完之后，最关键的一步来了：模型会立刻计算这些回答的 ",[56,4037,4038],{},"old_log_prob","。你可以把这个值看作是一个 ",[37,4041,4042],{},"“锚点”","。在接下来的训练中，无论模型参数怎么变，这个锚点数值是",[37,4045,4046],{},"绝对不会变","的。它就像是一个参照系，用来时刻提醒模型：“你现在的策略和刚开始采样时的策略差了多远”。",[183,4049,4051],{"id":4050},"第二阶段吃数据-ppo-update","第二阶段：吃数据 (PPO Update)",[19,4053,4054,4055,4058],{},"数据造好了，Reward Model 也打完分了，现在模型切换到训练模式（",[56,4056,4057],{},"train","），开始参数更新。",[19,4060,4061,4062,4065,4066,4068],{},"通常我们会把这批造好的数据重复利用几次（",[56,4063,4064],{},"ppo_epochs","），在每一个 Epoch 里，数据被切分成小块（",[56,4067,3987],{},"）喂给优化器。",[19,4070,4071,4074,4075,4078,4079,4082,4083,4085],{},[37,4072,4073],{},"这里有个非常重要的细节：","\n我们在 PPO Update 中会进行多次 ",[56,4076,4077],{},"optimizer.step()","。虽然每一次更新后，模型的参数都变了，新计算出的概率（",[56,4080,4081],{},"log_prob","）也变了，但我们在第一阶段算好的那个“锚点”（",[56,4084,4038],{},"）是定死的。",[19,4087,4088,4089,181],{},"这意味着，",[37,4090,4091],{},"随着更新步数越来越多，现在的模型策略会逐渐偏离采样时的策略",[23,4093],{},[26,4095,4097],{"id":4096},"为什么会练崩调参背后的逻辑","为什么会练崩？调参背后的逻辑",[19,4099,4100,4101,4104],{},"理解了上面的流程，之前的“崩盘”原因就呼之欲出了。调参的核心，其实就是在平衡**“外循环的广度”",[37,4102,4103],{},"和","“内循环的深度”**。",[183,4106,4108],{"id":4107},"_1-外循环为什么-train_batch_size-越大越稳","1. 外循环：为什么 train_batch_size 越大越稳？",[19,4110,4111,4113],{},[56,4112,3933],{}," 决定了模型在改变策略之前，到底看了多少“案例”。",[390,4115,4116,4122],{},[268,4117,4118,4121],{},[37,4119,4120],{},"如果 Batch 很小（比如 256）","：\n这就好比一个学生，做了一道题就急着对答案、改思路。这道题可能比较偏，学生改完思路后，再做下一道题发现又不对，于是又改。模型就会陷入这种“做一题改一次”的震荡中，非常容易过拟合当前的局部数据，导致策略剧烈抖动。",[268,4123,4124,4127],{},[37,4125,4126],{},"如果 Batch 很大（比如 10240）","：\n这就像学生做完了一整套模拟卷（100题），综合了所有题目的得失，才总结出一套改进方案。这种基于大样本量的梯度方向是非常稳健的，因为它看到了数据的“全貌”。",[19,4129,4130,4131],{},"所以结论很简单：",[37,4132,4133,4134,4136],{},"只要显存和时间允许，",[56,4135,3933],{}," 越大越好。",[183,4138,4140],{"id":4139},"_2-内循环ppo_mini_batch_size-的黄金比例","2. 内循环：ppo_mini_batch_size 的黄金比例",[19,4142,4143,4144,4146],{},"确定了要采多少数据（Train Batch），接下来就是决定“怎么吃掉这批数据”。这时候 ",[56,4145,3987],{}," 就很关键了。",[390,4148,4149,4159],{},[268,4150,4151,4154,4155,4158],{},[37,4152,4153],{},"吃得太慢（Mini Batch 太小）","：\n这意味着你需要更新很多次参数才能吃完这批数据。就像上面说的，更新次数越多，模型策略偏离“锚点”就越远。\n这就导致到了后面几步，模型现在的想法和采样时的想法已经天差地别了。此时计算出的 Ratio (",[56,4156,4157],{},"log_prob \u002F old_log_prob",") 会变得极不稳定，PPO 的裁剪机制（Clip）会强制把梯度切零。结果就是：你后面算的这些步数，基本都是无效计算，甚至是有害的。",[268,4160,4161,4164,4165,4167],{},[37,4162,4163],{},"吃得太快（Mini Batch 太大）","：\n假如你直接一口吞（Mini Batch = Train Batch），那这一轮 Rollout 辛辛苦苦生成的几万条数据，只换来了一次 ",[56,4166,4077],{},"。这就太奢侈了，数据利用率极低，训练效率慢得令人发指。",[19,4169,4170,4171,4174],{},"所以，这里需要一个**“黄金比例”**。经验法则通常是：",[37,4172,4173],{},"调节 Mini Batch 的大小，让每一轮 Rollout 的总更新步数保持在 4-8 步左右。"," 既保证了数据被重复利用，又不会让策略偏离太远。",[183,4176,4178],{"id":4177},"_3-实战避坑指南","3. 实战避坑指南",[19,4180,4181,4182,1608,4185,4188],{},"最后总结一下，我们在看 Log 的时候，主要盯着 ",[56,4183,4184],{},"Clip Ratio",[56,4186,4187],{},"KL Divergence"," 这两个指标看就行：",[390,4190,4191,4207],{},[268,4192,4193,4199,4200,4203,4204,4206],{},[37,4194,4195,4196,4198],{},"如果你发现 ",[56,4197,4184],{}," 特别低 (\u003C 1%)","，而且 Loss 下降得很慢。那说明你太保守了，数据还没被榨干。\n👉 ",[37,4201,4202],{},"尝试","：减小 ",[56,4205,3987],{},"（多更几步），或者干脆多跑一个 Epoch。",[268,4208,4209,4214,4215,4217,4218,4220],{},[37,4210,4195,4211,4213],{},[56,4212,4184],{}," 飙升 (> 15%)","，或者 KL 散度突然爆炸。那说明你太激进，模型已经“学歪了”。\n👉 ",[37,4216,4202],{},"：增大 ",[56,4219,3987],{},"（少更几步），让模型“少吃多餐”变为“多吃少餐”，稳住心态。",{"title":54,"searchDepth":68,"depth":68,"links":4222},[4223,4224,4225,4229],{"id":3923,"depth":68,"text":3923},{"id":3953,"depth":68,"text":3953},{"id":4002,"depth":68,"text":4003,"children":4226},[4227,4228],{"id":4015,"depth":74,"text":4016},{"id":4050,"depth":74,"text":4051},{"id":4096,"depth":68,"text":4097,"children":4230},[4231,4232,4233],{"id":4107,"depth":74,"text":4108},{"id":4139,"depth":74,"text":4140},{"id":4177,"depth":74,"text":4178},"\u002Fimages\u002Fposts\u002Fverl_0\u002Fcover.png","2026-01-08",{},"\u002Fposts\u002Fverl_0\u002Fmain",{"title":3914,"description":54},"posts\u002Fverl_0\u002Fmain","梳理GRPO流程以及参数",[2044,2535],"_oKO3pjhfCDakSPQBUJpkFye5oOiGAhgShYOmxmJquI",{"id":4244,"title":4245,"body":4246,"cover":4527,"date":4528,"description":54,"draft":2036,"extension":2037,"meta":4529,"navigation":1339,"path":4530,"seo":4531,"stem":4532,"summary":4533,"tags":4534,"__hash__":4535},"posts\u002Fposts\u002Fvideoagent_note0\u002Fmain.md","VideoAgent 开发笔记0: 基于 Mem0 的实时视觉问答 Agent",{"type":8,"value":4247,"toc":4513},[4248,4252,4255,4269,4273,4277,4300,4304,4311,4361,4365,4368,4398,4402,4406,4420,4424,4436,4440,4495,4499],[26,4249,4251],{"id":4250},"_1-项目目标-project-objective","1. 项目目标 (Project Objective)",[19,4253,4254],{},"构建一个能够实时处理视频流输入的智能 Agent。该 Agent 需具备以下能力：",[390,4256,4257,4263],{},[268,4258,4259,4262],{},[37,4260,4261],{},"实时感知","：理解当前的视觉（视频）信息。",[268,4264,4265,4268],{},[37,4266,4267],{},"长期记忆","：通过检索历史交互和视频内容，结合当前上下文进行连贯问答。",[26,4270,4272],{"id":4271},"_2-核心架构-core-architecture","2. 核心架构 (Core Architecture)",[183,4274,4276],{"id":4275},"_21-技术栈-tech-stack","2.1 技术栈 (Tech Stack)",[390,4278,4279,4285,4294],{},[268,4280,4281,4284],{},[37,4282,4283],{},"Captioning Model & Main Inference Model",": 豆包 (Doubao) API。",[268,4286,4287,4290,4291,181],{},[37,4288,4289],{},"Embedding Model",": OpenAI ",[56,4292,4293],{},"text-embedding-3-small",[268,4295,4296,4299],{},[37,4297,4298],{},"Memory Framework",": Mem0。",[183,4301,4303],{"id":4302},"_22-记忆模块-memory-module","2.2 记忆模块 (Memory Module)",[19,4305,4306,4307,4310],{},"采用 ",[37,4308,4309],{},"mem0"," 作为记忆管理框架，负责视频内容的语义化存储。",[390,4312,4313,4327],{},[268,4314,4315,4318,4319,4322,4323,4326],{},[37,4316,4317],{},"写入机制","：采用",[37,4320,4321],{},"固定窗口","策略（0-10s, 10-20s...），每 ",[37,4324,4325],{},"10秒"," 截取一个视频片段，通过多模态模型转换为文本描述 (Caption)，写入记忆库。",[268,4328,4329,4332,4333,4336,4337,4340,4341,4344,4345,4348,4349],{},[37,4330,4331],{},"存储痛点","：mem0 设计了 ",[56,4334,4335],{},"add"," (新增), ",[56,4338,4339],{},"update"," (更新), ",[56,4342,4343],{},"delete"," (删除), ",[56,4346,4347],{},"noop"," (无操作) 四种状态。\n",[390,4350,4351],{},[268,4352,4353,4357,4358,4360],{},[4354,4355,4356],"em",{},"现状","：由于视频 Caption 内容随时间变化大，且带有时间戳信息，导致系统几乎总是触发 ",[56,4359,4335],{}," 操作，造成记忆库快速膨胀。",[183,4362,4364],{"id":4363},"_23-推理管线-inference-pipeline","2.3 推理管线 (Inference Pipeline)",[19,4366,4367],{},"当接收到用户 Query 时，构建如下 Context 输入给大模型：",[265,4369,4370,4376,4386,4392],{},[268,4371,4372,4375],{},[37,4373,4374],{},"用户指令"," (User Prompt \u002F System Prompt)",[268,4377,4378,4381,4382,4385],{},[37,4379,4380],{},"当前视觉信息","：最近的 ",[37,4383,4384],{},"4帧"," 图像（作为实时视觉感知的锚点）。",[268,4387,4388,4391],{},[37,4389,4390],{},"相关记忆 (RAG)","：根据 Query 从 mem0 中检索出的 Top-3 历史视频 Caption。",[268,4393,4394,4397],{},[37,4395,4396],{},"当前问题"," (Current Query)。",[26,4399,4401],{"id":4400},"_3-当前挑战与局限-challenges-limitations","3. 当前挑战与局限 (Challenges & Limitations)",[183,4403,4405],{"id":4404},"_31-上下文覆盖不足-context-coverage","3.1 上下文覆盖不足 (Context Coverage)",[390,4407,4408,4414],{},[268,4409,4410,4413],{},[37,4411,4412],{},"问题","：固定的 Top-3 RAG 检索仅能覆盖约 30秒 (3 x 10s) 的历史跨度。",[268,4415,4416,4419],{},[37,4417,4418],{},"后果","：对于需要长跨度信息的“总结性问题”无能为力。单纯增加检索数量 (Top-K) 会导致上下文过长，引起注意力稀疏 (Sparse Attention)，反而降低回答性能。",[183,4421,4423],{"id":4422},"_32-记忆干扰与噪声-memory-interference","3.2 记忆干扰与噪声 (Memory Interference)",[390,4425,4426,4431],{},[268,4427,4428,4430],{},[37,4429,4412],{},"：对于仅需“所见即所得”的实时问题（如“现在画面里有什么？”），引入历史记忆反而造成干扰。",[268,4432,4433,4435],{},[37,4434,4418],{},"：模型可能混淆“当前画面”与“记忆中的过时信息”，导致回答错误。",[183,4437,4439],{"id":4438},"_33-语义推导与记忆路由失效-failure-in-reasoning-based-memory-routing","3.3 语义推导与记忆路由失效 (Failure in Reasoning-based Memory Routing)",[390,4441,4442,4455,4461,4489],{},[268,4443,4444,4446,4447,4450,4451,4454],{},[37,4445,4412],{},"：当前的 RAG 机制仅依赖",[37,4448,4449],{},"直接语义相似度","，无法处理任何需要",[37,4452,4453],{},"逻辑推导","才能定位目标记忆的问题（不仅限于时间维度）。",[268,4456,4457,4460],{},[37,4458,4459],{},"核心缺陷","：系统缺失“先分析意图，再制定检索策略”的能力。",[268,4462,4463,4466,4467],{},[37,4464,4465],{},"案例","：\n",[390,4468,4469,4479],{},[268,4470,4471,4474,4475,4478],{},[37,4472,4473],{},"隐式时间约束","：用户问“刚刚那本书叫什么”，系统无法理解“刚刚”意味着需要检索",[4354,4476,4477],{},"最新","的记录，只能机械地检索与“书”语义最接近的片段（可能是很久以前的《哈利波特》）。",[268,4480,4481,4484,4485,4488],{},[37,4482,4483],{},"逻辑\u002F因果链条","：若问“他做完饭后干了什么？”，系统倾向于检索包含“做饭”画面的记忆，而非“做饭”",[4354,4486,4487],{},"之后","的时间片段。",[268,4490,4491,4494],{},[37,4492,4493],{},"结论","：单纯的 Semantic Search 无法承担复杂的记忆路由 (Memory Routing) 任务。",[183,4496,4498],{"id":4497},"_34-混合推理策略失效-strategic-failure","3.4 混合推理策略失效 (Strategic Failure)",[390,4500,4501,4507],{},[268,4502,4503,4506],{},[37,4504,4505],{},"尝试方案","：设计两阶段推理 —— 先让模型看最近几帧尝试回答；若无法回答，再请求访问记忆。",[268,4508,4509,4512],{},[37,4510,4511],{},"失败原因","：模型倾向于“偷懒”或自信地产生幻觉 (Hallucination)，不愿承认“不知道”并调用记忆工具。Prompt 优化目前尚未奏效。",{"title":54,"searchDepth":68,"depth":68,"links":4514},[4515,4516,4521],{"id":4250,"depth":68,"text":4251},{"id":4271,"depth":68,"text":4272,"children":4517},[4518,4519,4520],{"id":4275,"depth":74,"text":4276},{"id":4302,"depth":74,"text":4303},{"id":4363,"depth":74,"text":4364},{"id":4400,"depth":68,"text":4401,"children":4522},[4523,4524,4525,4526],{"id":4404,"depth":74,"text":4405},{"id":4422,"depth":74,"text":4423},{"id":4438,"depth":74,"text":4439},{"id":4497,"depth":74,"text":4498},"\u002Fimages\u002Fposts\u002Fvideoagent_note0\u002Fcover.jpg","2026-01-04",{},"\u002Fposts\u002Fvideoagent_note0\u002Fmain",{"title":4245,"description":54},"posts\u002Fvideoagent_note0\u002Fmain","构建一个具备长期记忆能力的实时视觉问答 Agent，探索基于 mem0 的视频流语义化存储与检索方案。",[2044,2045],"z52pJfNjljIo2XQ9eAOU950mK5sQ4n7hOh1Ozsm_xVk",{"id":4537,"title":4538,"body":4539,"cover":4995,"date":4996,"description":4543,"draft":2036,"extension":2037,"meta":4997,"navigation":1339,"path":4998,"seo":4999,"stem":5000,"summary":5001,"tags":5002,"__hash__":5004},"posts\u002Fposts\u002FMemory_note0\u002Fmemory-note-0.md","Memory调研笔记0",{"type":8,"value":4540,"toc":4978},[4541,4544,4547,4559,4562,4573,4577,4580,4583,4587,4591,4594,4597,4617,4621,4624,4627,4647,4650,4658,4661,4669,4672,4677,4681,4684,4687,4690,4701,4704,4712,4715,4717,4725,4728,4736,4740,4746,4749,4771,4774,4782,4784,4792,4794,4799,4803,4807,4810,4813,4816,4820,4823,4826,4840,4843,4847,4850,4853,4864,4867,4878,4881,4885,4888,4892,4895,4900,4908,4913,4921,4926,4934,4939,4947,4951,4956,4959,4964],[19,4542,4543],{},"现在最落地的 memory，多半是外部存储 + 人工规则\u002F工具 API，llm agent也可能参与memory的管理和查询（mem0 \u002F MIRIX \u002F DVD \u002F WorldMM）。",[19,4545,4546],{},"新趋势是“让模型学会管记忆”，可以分两派：",[390,4548,4549,4552],{},[268,4550,4551],{},"固定操作集下学策略（Memory-R1：ADD\u002FUPDATE\u002FDELETE\u002FNOOP）",[268,4553,4554,4555,4558],{},"直接把记忆揉进推理过程（MEM1：内部状态 ",[56,4556,4557],{},"\u003CIS>"," 迭代压缩）",[19,4560,4561],{},"视频领域基本在复刻这两派：",[390,4563,4564,4567,4570],{},[268,4565,4566],{},"结构化外存 + 自动回忆工具链（DeepVideoDiscovery）",[268,4568,4569],{},"层级记忆 + 先粗后细 + 多 agent（VideoLucy）",[268,4571,4572],{},"端到端带记忆看视频，即MEM1用到视频上（VideoMem：SFT+RL，但训练很贵，3200 A100 hours）",[26,4574,4576],{"id":4575},"_1memory-reseach的背景","1.Memory Reseach的背景",[19,4578,4579],{},"LLM的长上下文限制已经是总所周知的问题了，不仅是上下文窗口塞不下，attention计算量增加，还会存在注意力稀疏关注不到重点的问题（个人试过用kimi-k2、deepseek等api做qq聊天记录总结，会发现超长上下文情况会有很大幻觉。也尝试过用Qwen3-VL-8B搭流视频理解，发现长视频token下提问容易被过时事件干扰）。比较古典的解决方法是用RAG，直接暴力用问题检索向量数据库，但这方法从数据库搭建（碎片化）到检索内容（因果推理，数字等语意理解极度low level）哪哪都是问题，噪声很大。",[19,4581,4582],{},"所以自然需要研究如何从数据库的组织方法、内容的检索策略上做优化，现在我们一般用memory这个概念来称呼这类研究（",[26,4584,4586],{"id":4585},"_2memory代表工作介绍","2.memory代表工作介绍",[183,4588,4590],{"id":4589},"mem0arxiv-250419413","mem0（arxiv 2504.19413）",[19,4592,4593],{},"目前最热门memory开源项目，方法为 Agent 用固定 API 操作向量数据库，维护一堆碎片记忆条目。",[19,4595,4596],{},"核心做法：",[390,4598,4599,4602,4605,4608,4611,4614],{},[268,4600,4601],{},"LLM 先从对话里识别关键信息（facts）",[268,4603,4604],{},"再通过固定 API 做 增删改查（ADD\u002FUPDATE\u002FDELETE\u002F…）",[268,4606,4607],{},"记忆形态偏零散、轻量、工程可落地\n优点：简单、容易开源落地、可控性强。\n缺点：",[268,4609,4610],{},"“识别什么该记”很吃 prompt\u002F启发式",[268,4612,4613],{},"本质只是个用llm赋能优化了向量数据库维护的RAG系统，碎片多了以后，检索质量和一致性很容易漂",[268,4615,4616],{},"长期连贯性不靠“理解”，更像“记笔记的草稿本”",[183,4618,4620],{"id":4619},"mirixarxiv-250707957","MIRIX（arxiv 2507.07957）",[19,4622,4623],{},"也是个很热门的开源项目，把记忆做成“操作系统”，设计结构化记忆用multi-agent管理，自动路由。",[19,4625,4626],{},"共设计了六类记忆：",[390,4628,4629,4632,4635,4638,4641,4644],{},[268,4630,4631],{},"核心记忆：user profile（最高优先级）",[268,4633,4634],{},"情景记忆：日记式",[268,4636,4637],{},"语义记忆：概念\u002F实体\u002F关系抽象",[268,4639,4640],{},"程序记忆：流程\u002F技能",[268,4642,4643],{},"资源记忆：长文档\u002F大文件",[268,4645,4646],{},"敏感记忆：密码\u002FAPI key（注意合规与隔离）",[19,4648,4649],{},"管理方式：Multi-agent",[390,4651,4652,4655],{},[268,4653,4654],{},"存储：Meta Memory Manager 从上下文识别要存的东西，分发给 6 个 sub-agent",[268,4656,4657],{},"检索：Chat Agent 先看 6 类 summary → 决定查哪类 → 用向量（会自己重写query）\u002FBM25\u002F精确匹配等查",[19,4659,4660],{},"优点：",[390,4662,4663,4666],{},[268,4664,4665],{},"结构化强，工程可控，适合复杂任务\u002F企业场景",[268,4667,4668],{},"“记忆类型”本身就是一种 inductive bias（归纳偏置）",[19,4670,4671],{},"缺点：",[390,4673,4674],{},[268,4675,4676],{},"设计重、维护成本高",[183,4678,4680],{"id":4679},"memory-r1arxiv-250819828","Memory-R1（arxiv 2508.19828）",[19,4682,4683],{},"llm没有训练过如何管理记忆，导致有时“存\u002F更新\u002F删\u002F不管”的行为不合理，那就考虑把 mem0 的记忆 API 操作，变成可以用 RL 学出来的策略，使得记忆维护的更精确。",[19,4685,4686],{},"模块 1：Memory Manager",[19,4688,4689],{},"对每条新 fact，从 {ADD, UPDATE, DELETE, NOOP} 里选一个\nRL 流程：",[390,4691,4692,4695,4698],{},[268,4693,4694],{},"管理器操作 → 新记忆库状态",[268,4696,4697],{},"冻结的 Answer Agent 用新记忆回答问题",[268,4699,4700],{},"与 GT 比较（EM）→ 给奖励",[19,4702,4703],{},"模块 2：Answer Agent",[390,4705,4706,4709],{},[268,4707,4708],{},"从 RAG top-k（很多，60条）候选里，CoT推理出一小部分最有用的，然后回答",[268,4710,4711],{},"按答案给奖励",[19,4713,4714],{},"（Answer Agent RL有点丑陋，本质像在训一个更会做过滤的模型）",[19,4716,4660],{},[390,4718,4719,4722],{},[268,4720,4721],{},"把“记忆管理”从 prompt 手艺活，推进到可学习策略",[268,4723,4724],{},"操作集小，训练更稳",[19,4726,4727],{},"优化的方向：",[390,4729,4730,4733],{},[268,4731,4732],{},"固定操作集限制表达力（“记忆结构”很难涌现）",[268,4734,4735],{},"奖励信号似乎比较粗粒度",[183,4737,4739],{"id":4738},"mem1iclr26-score8666","MEM1（ICLR26 score8666）",[19,4741,4742,4743,4745],{},"不搞外部库，直接让 Agent 学会把过去压缩进一个新的总结文本 ",[56,4744,4557],{},"，迭代更新。",[19,4747,4748],{},"核心机制：",[390,4750,4751,4757,4764],{},[268,4752,4753,4754,4756],{},"用文本 ",[56,4755,4557],{}," 维护“内部状态\u002F记忆”",[268,4758,4759,4760,4763],{},"每轮 t：生成新的 ",[56,4761,4762],{},"\u003CIS_t>","，总结旧记忆 + 新观察 + 推理",[268,4765,4766,4767,4770],{},"更新完得到 ",[56,4768,4769],{},"\u003CIS_(t+1)>"," 后，丢弃旧标签，防止 prompt 变长，逼模型学压缩与整合",[19,4772,4773],{},"RL 设计：",[390,4775,4776,4779],{},[268,4777,4778],{},"PPO 端到端，只用任务成功做奖励",[268,4780,4781],{},"通过改mask限制注意力：每个 token 只能看当时仍保留的 token，逼它学会在当前memory下推理",[19,4783,4660],{},[390,4785,4786,4789],{},[268,4787,4788],{},"自由度最高：理论上“记忆结构”可能自己长出来",[268,4790,4791],{},"内存使用可控（cache 类的极致）",[19,4793,4671],{},[390,4795,4796],{},[268,4797,4798],{},"少量的memory状态在超长上下文下最终也得丢失信息",[26,4800,4802],{"id":4801},"_3视频理解方向的memory工作","3.视频理解方向的memory工作",[183,4804,4806],{"id":4805},"videomemarxiv-251204540","VideoMem（arxiv 2512.04540）",[19,4808,4809],{},"MEM1 + Video工作",[19,4811,4812],{},"让 agent 带着问题一段段看视频，边看边更记忆，SFT + RL。由于长视频数据每个视频就一个问题，RL监督力度不够，改了下GRPO，每看一段视频片段都尝试回答一下问题然后RL。",[19,4814,4815],{},"训练很烧（3200 A100 hours），没开源。\n定位：cache 类（精简记忆进上下文）在视频任务上的版本。\n隐含代价：视频领域的“观察”太大太噪，端到端学压缩更贵。",[183,4817,4819],{"id":4818},"videolucyneurips25","VideoLucy（NeurIPS25）",[19,4821,4822],{},"固定三层层级记忆 + 先粗看后细看的策略 + 回溯 loop（multi agent，为了用强推理模型）",[19,4824,4825],{},"四个 agent：",[390,4827,4828,4831,4834,4837],{},[268,4829,4830],{},"Captioning Agent：看视频片段 → 产文本描述（系统的眼睛）",[268,4832,4833],{},"Localization Agent：根据问题 + 当前记忆 → 找最相关时间段（导航与过滤）",[268,4835,4836],{},"Instruction Agent：判断还缺啥信息 → 生成更具体的指令给 Captioning（告诉眼睛看哪里）",[268,4838,4839],{},"Answering Agent：判断是否足够回答；不够就触发下一轮回溯",[19,4841,4842],{},"经验结论：\n这种“自动回忆\u002F回溯”很吃主控推理引擎质量（Deepseek-R1 \u002F o3 ），所以才整了个VL看视频转述给llm推理的pipeline，实验也提到换弱一点开源模型性能会明显掉。",[183,4844,4846],{"id":4845},"deepvideodiscoveryneurips25结构化记忆-工具链式回忆globalclipframe-三层","DeepVideoDiscovery（NeurIPS25）——“结构化记忆 + 工具链式回忆（Global\u002FClip\u002FFrame 三层）”",[19,4848,4849],{},"把视频记忆做成三层索引体系，然后让强主 agent 通过工具一步步查。",[19,4851,4852],{},"三层记忆结构：",[390,4854,4855,4858,4861],{},[268,4856,4857],{},"Global：主体注册表（Subject Registry）——谁出现、啥特征、在哪些时间段",[268,4859,4860],{},"Clip（5 秒一段）：caption + embedding，便于语义检索",[268,4862,4863],{},"Frame（比如 2fps）：需要细节时做 VQA\u002F检查（车牌号、物体交接等）\n记忆最后大概组织成：{Global, {Clip, Frame}_i}",[19,4865,4866],{},"工具（由主 agent 调）：",[390,4868,4869,4872,4875],{},[268,4870,4871],{},"Global Browse：宏观概览",[268,4873,4874],{},"Clip Search：按 query 检索相关 clip caption",[268,4876,4877],{},"Frame Inspect：指定 query 或 time_range，对帧做 VQA",[19,4879,4880],{},"定位：磁盘类 + 自动回忆（工具链），工程味很浓，也很实用，也是VL看视频转述给llm推理，依赖强推理引擎",[183,4882,4884],{"id":4883},"worldmmarxiv-251202425多模态世界记忆情景语义图-视觉向量库","WorldMM（arxiv 2512.02425）——“多模态世界记忆：情景\u002F语义（图）+ 视觉（向量库）”",[19,4886,4887],{},"把记忆拆成三块：情景记忆图、语义记忆图、视觉记忆向量库。跟DVD比基本就是记忆结构设计不同，加强版",[26,4889,4891],{"id":4890},"_4memory分类坐标系","4.memory分类坐标系",[19,4893,4894],{},"按目前调研来看，可以用4个纬度来分类memory：",[265,4896,4897],{},[268,4898,4899],{},"存哪里？",[390,4901,4902,4905],{},[268,4903,4904],{},"外部“磁盘类”：向量库\u002F文档库\u002F图数据库，推理时按需检索（mem0、MIRIX、Memory-R1、DVD、WorldMM、DeepVideoDiscovery）",[268,4906,4907],{},"内部“缓存类”：直接维护一个精简 state ，推理时全放进上下文（MEM1、VideoMem）",[265,4909,4910],{"start":68},[268,4911,4912],{},"长什么样？",[390,4914,4915,4918],{},[268,4916,4917],{},"零碎化（碎片 facts）",[268,4919,4920],{},"结构化（profile \u002F 层级 \u002F 图 \u002F registry）",[265,4922,4923],{"start":74},[268,4924,4925],{},"谁来管？",[390,4927,4928,4931],{},[268,4929,4930],{},"人工策略\u002F固定路由（成熟落地多）",[268,4932,4933],{},"可学习管理（RL \u002F 端到端）",[265,4935,4936],{"start":763},[268,4937,4938],{},"怎么回忆？",[390,4940,4941,4944],{},[268,4942,4943],{},"固定检索（top-k 向量相似度 + BM25 + 精确匹配）",[268,4945,4946],{},"自动回忆（automatic agent，先粗看再细看、工具调用、回溯循环、强推理模型主控）",[26,4948,4950],{"id":4949},"_5个人一些启发性的insight","5.个人一些启发性的insight",[265,4952,4953],{},[268,4954,4955],{},"记忆是感知的先验，人看视频可能会根据先前情节决定后面更关注什么内容，但“用问题当先验”可能太强\nVideoMem 用 question 引导记录什么——有效但强条件：真实应用里问题不一定先给你。而且导致拿到问题后才能开始构架记忆，延时高，特别是流式场景下",[19,4957,4958],{},"现实更像：实时被动观察 → 形成可复用记忆 → 遇到问题马上就可以回忆\u002F检索。\n所以可考虑从“无问题条件下的记忆组织”入手",[265,4960,4961],{"start":68},[268,4962,4963],{},"从调研看记忆可分为磁盘形记忆（记很多，按需检索一部分进上下文）和缓存型记忆（记很少，但每次推理都全带着），那感觉可以结合两者提出混合型：",[390,4965,4966,4969,4972,4975],{},[268,4967,4968],{},"cache：高频、近期、强相关的“工作集（working set）”",[268,4970,4971],{},"disk：长尾细节、可回溯证据、资源文档",[268,4973,4974],{},"是否可以让模型学习记忆要放在“缓存”还是“磁盘”，使得大部分情况可以直接回答，小部分情况进一步检索也能回答？",[268,4976,4977],{},"核心难点：让模型学会“该放缓存还是该落盘”（并且能自我纠错）",{"title":54,"searchDepth":68,"depth":68,"links":4979},[4980,4981,4987,4993,4994],{"id":4575,"depth":68,"text":4576},{"id":4585,"depth":68,"text":4586,"children":4982},[4983,4984,4985,4986],{"id":4589,"depth":74,"text":4590},{"id":4619,"depth":74,"text":4620},{"id":4679,"depth":74,"text":4680},{"id":4738,"depth":74,"text":4739},{"id":4801,"depth":68,"text":4802,"children":4988},[4989,4990,4991,4992],{"id":4805,"depth":74,"text":4806},{"id":4818,"depth":74,"text":4819},{"id":4845,"depth":74,"text":4846},{"id":4883,"depth":74,"text":4884},{"id":4890,"depth":68,"text":4891},{"id":4949,"depth":68,"text":4950},"\u002Fimages\u002Fposts\u002FMemory_note0\u002Fcover.jpg","2025-12-30",{},"\u002Fposts\u002Fmemory_note0\u002Fmemory-note-0",{"title":4538,"description":4543},"posts\u002FMemory_note0\u002Fmemory-note-0","近期memory工作调研，记忆系统到底在帮 Agent 干什么？",[2044,5003],"调研","qgfh4opOh8czZIxDnmxyW3S3FSVDM9gAJyKRgAnLi8w",{"id":5006,"title":5007,"body":5008,"cover":6301,"date":6302,"description":54,"draft":2036,"extension":2037,"meta":6303,"navigation":1339,"path":6304,"seo":6305,"stem":6306,"summary":5012,"tags":6307,"__hash__":6309},"posts\u002Fposts\u002Fdotfile_config\u002Fmain.md","Linux Server Configuration",{"type":8,"value":5009,"toc":6295},[5010,5013,5017,5083,5086,5090,5154,5157,5225,5228,5405,5408,5887,5894,5898,5901,5996,5999,6261,6264,6270,6274,6292],[11,5011,5012],{"id":5012},"个人服务器配置流程",[26,5014,5016],{"id":5015},"_1-添加用户并赋予-sudo-权限","1. 添加用户并赋予 sudo 权限",[49,5018,5022],{"className":5019,"code":5020,"language":5021,"meta":54,"style":54},"language-bash shiki shiki-themes github-dark github-light","adduser --disabled-password \u003Cyour_username>\nusermod -aG sudo \u003Cyour_username>\nsu - \u003Cyour_username>\n","bash",[56,5023,5024,5048,5067],{"__ignoreMap":54},[59,5025,5026,5030,5034,5038,5042,5045],{"class":61,"line":62},[59,5027,5029],{"class":5028},"sqoU-","adduser",[59,5031,5033],{"class":5032},"sTU5a"," --disabled-password",[59,5035,5037],{"class":5036},"spKkM"," \u003C",[59,5039,5041],{"class":5040},"skb7c","your_usernam",[59,5043,2635],{"class":5044},"shWlK",[59,5046,5047],{"class":5036},">\n",[59,5049,5050,5053,5056,5059,5061,5063,5065],{"class":61,"line":68},[59,5051,5052],{"class":5028},"usermod",[59,5054,5055],{"class":5032}," -aG",[59,5057,5058],{"class":5040}," sudo",[59,5060,5037],{"class":5036},[59,5062,5041],{"class":5040},[59,5064,2635],{"class":5044},[59,5066,5047],{"class":5036},[59,5068,5069,5072,5075,5077,5079,5081],{"class":61,"line":74},[59,5070,5071],{"class":5028},"su",[59,5073,5074],{"class":5040}," -",[59,5076,5037],{"class":5036},[59,5078,5041],{"class":5040},[59,5080,2635],{"class":5044},[59,5082,5047],{"class":5036},[19,5084,5085],{},"然后添加.ssh\u002Fauthorized_keys文件。",[26,5087,5089],{"id":5088},"_2-安装-zshoh-my-zsh","2. 安装 zsh、oh-my-zsh",[49,5091,5093],{"className":5019,"code":5092,"language":5021,"meta":54,"style":54},"sudo apt install -y zsh\nsudo chsh -s $(which zsh) $USER\nsh -c \"$(curl -fsSL https:\u002F\u002Fraw.githubusercontent.com\u002Fohmyzsh\u002Fohmyzsh\u002Fmaster\u002Ftools\u002Finstall.sh)\"\n",[56,5094,5095,5112,5134],{"__ignoreMap":54},[59,5096,5097,5100,5103,5106,5109],{"class":61,"line":62},[59,5098,5099],{"class":5028},"sudo",[59,5101,5102],{"class":5040}," apt",[59,5104,5105],{"class":5040}," install",[59,5107,5108],{"class":5032}," -y",[59,5110,5111],{"class":5040}," zsh\n",[59,5113,5114,5116,5119,5122,5125,5128,5131],{"class":61,"line":68},[59,5115,5099],{"class":5028},[59,5117,5118],{"class":5040}," chsh",[59,5120,5121],{"class":5032}," -s",[59,5123,5124],{"class":5044}," $(",[59,5126,5127],{"class":5032},"which",[59,5129,5130],{"class":5040}," zsh",[59,5132,5133],{"class":5044},") $USER\n",[59,5135,5136,5139,5142,5145,5148,5151],{"class":61,"line":74},[59,5137,5138],{"class":5028},"sh",[59,5140,5141],{"class":5032}," -c",[59,5143,5144],{"class":5040}," \"$(",[59,5146,5147],{"class":5028},"curl",[59,5149,5150],{"class":5032}," -fsSL",[59,5152,5153],{"class":5040}," https:\u002F\u002Fraw.githubusercontent.com\u002Fohmyzsh\u002Fohmyzsh\u002Fmaster\u002Ftools\u002Finstall.sh)\"\n",[19,5155,5156],{},"安装oh-my-zsh插件（命令高亮，自动补全）：",[49,5158,5160],{"className":5019,"code":5159,"language":5021,"meta":54,"style":54},"git clone https:\u002F\u002Fgithub.com\u002Fzsh-users\u002Fzsh-syntax-highlighting.git ${ZSH_CUSTOM:-~\u002F.oh-my-zsh\u002Fcustom}\u002Fplugins\u002Fzsh-syntax-highlighting\n\ngit clone https:\u002F\u002Fgithub.com\u002Fzsh-users\u002Fzsh-autosuggestions ${ZSH_CUSTOM:-~\u002F.oh-my-zsh\u002Fcustom}\u002Fplugins\u002Fzsh-autosuggestions\n",[56,5161,5162,5195,5199],{"__ignoreMap":54},[59,5163,5164,5167,5170,5173,5176,5179,5182,5184,5187,5189,5192],{"class":61,"line":62},[59,5165,5166],{"class":5028},"git",[59,5168,5169],{"class":5040}," clone",[59,5171,5172],{"class":5040}," https:\u002F\u002Fgithub.com\u002Fzsh-users\u002Fzsh-syntax-highlighting.git",[59,5174,5175],{"class":5044}," ${ZSH_CUSTOM",[59,5177,5178],{"class":5036},":-",[59,5180,5181],{"class":5044},"~",[59,5183,1468],{"class":5036},[59,5185,5186],{"class":5044},".oh-my-zsh",[59,5188,1468],{"class":5036},[59,5190,5191],{"class":5044},"custom}",[59,5193,5194],{"class":5040},"\u002Fplugins\u002Fzsh-syntax-highlighting\n",[59,5196,5197],{"class":61,"line":68},[59,5198,1340],{"emptyLinePlaceholder":1339},[59,5200,5201,5203,5205,5208,5210,5212,5214,5216,5218,5220,5222],{"class":61,"line":74},[59,5202,5166],{"class":5028},[59,5204,5169],{"class":5040},[59,5206,5207],{"class":5040}," https:\u002F\u002Fgithub.com\u002Fzsh-users\u002Fzsh-autosuggestions",[59,5209,5175],{"class":5044},[59,5211,5178],{"class":5036},[59,5213,5181],{"class":5044},[59,5215,1468],{"class":5036},[59,5217,5186],{"class":5044},[59,5219,1468],{"class":5036},[59,5221,5191],{"class":5044},[59,5223,5224],{"class":5040},"\u002Fplugins\u002Fzsh-autosuggestions\n",[19,5226,5227],{},"配置~\u002F.zshrc：",[49,5229,5231],{"className":5019,"code":5230,"language":5021,"meta":54,"style":54},"export ZSH=\"$HOME\u002F.oh-my-zsh\"\n\nZSH_THEME=\"AEsir\"\nexport VIRTUAL_ENV_DISABLE_PROMPT=1\nplugins=(git web-search jsontools z zsh-autosuggestions zsh-syntax-highlighting)\n\nsource $ZSH\u002Foh-my-zsh.sh\nunset PROMPT_DIRTRIM\n\nalias zshconfig=\"mate ~\u002F.zshrc\"\nalias ohmyzsh=\"mate ~\u002F.oh-my-zsh\"\n\n# some more ls aliases\nalias ll='ls -alF'\nalias la='ls -A'\nalias l='ls -CF'\n",[56,5232,5233,5252,5256,5266,5278,5307,5311,5322,5330,5334,5347,5359,5363,5369,5381,5393],{"__ignoreMap":54},[59,5234,5235,5238,5241,5243,5246,5249],{"class":61,"line":62},[59,5236,5237],{"class":5036},"export",[59,5239,5240],{"class":5044}," ZSH",[59,5242,2980],{"class":5036},[59,5244,5245],{"class":5040},"\"",[59,5247,5248],{"class":5044},"$HOME",[59,5250,5251],{"class":5040},"\u002F.oh-my-zsh\"\n",[59,5253,5254],{"class":61,"line":68},[59,5255,1340],{"emptyLinePlaceholder":1339},[59,5257,5258,5261,5263],{"class":61,"line":74},[59,5259,5260],{"class":5044},"ZSH_THEME",[59,5262,2980],{"class":5036},[59,5264,5265],{"class":5040},"\"AEsir\"\n",[59,5267,5268,5270,5273,5275],{"class":61,"line":763},[59,5269,5237],{"class":5036},[59,5271,5272],{"class":5044}," VIRTUAL_ENV_DISABLE_PROMPT",[59,5274,2980],{"class":5036},[59,5276,5277],{"class":5032},"1\n",[59,5279,5280,5283,5285,5287,5289,5292,5295,5298,5301,5304],{"class":61,"line":769},[59,5281,5282],{"class":5044},"plugins",[59,5284,2980],{"class":5036},[59,5286,2573],{"class":5044},[59,5288,5166],{"class":5040},[59,5290,5291],{"class":5040}," web-search",[59,5293,5294],{"class":5040}," jsontools",[59,5296,5297],{"class":5040}," z",[59,5299,5300],{"class":5040}," zsh-autosuggestions",[59,5302,5303],{"class":5040}," zsh-syntax-highlighting",[59,5305,5306],{"class":5044},")\n",[59,5308,5309],{"class":61,"line":775},[59,5310,1340],{"emptyLinePlaceholder":1339},[59,5312,5313,5316,5319],{"class":61,"line":781},[59,5314,5315],{"class":5032},"source",[59,5317,5318],{"class":5044}," $ZSH",[59,5320,5321],{"class":5040},"\u002Foh-my-zsh.sh\n",[59,5323,5324,5327],{"class":61,"line":787},[59,5325,5326],{"class":5032},"unset",[59,5328,5329],{"class":5040}," PROMPT_DIRTRIM\n",[59,5331,5332],{"class":61,"line":1358},[59,5333,1340],{"emptyLinePlaceholder":1339},[59,5335,5336,5339,5342,5344],{"class":61,"line":1363},[59,5337,5338],{"class":5036},"alias",[59,5340,5341],{"class":5044}," zshconfig",[59,5343,2980],{"class":5036},[59,5345,5346],{"class":5040},"\"mate ~\u002F.zshrc\"\n",[59,5348,5349,5351,5354,5356],{"class":61,"line":1369},[59,5350,5338],{"class":5036},[59,5352,5353],{"class":5044}," ohmyzsh",[59,5355,2980],{"class":5036},[59,5357,5358],{"class":5040},"\"mate ~\u002F.oh-my-zsh\"\n",[59,5360,5361],{"class":61,"line":1375},[59,5362,1340],{"emptyLinePlaceholder":1339},[59,5364,5365],{"class":61,"line":1381},[59,5366,5368],{"class":5367},"si27w","# some more ls aliases\n",[59,5370,5371,5373,5376,5378],{"class":61,"line":1386},[59,5372,5338],{"class":5036},[59,5374,5375],{"class":5044}," ll",[59,5377,2980],{"class":5036},[59,5379,5380],{"class":5040},"'ls -alF'\n",[59,5382,5383,5385,5388,5390],{"class":61,"line":1392},[59,5384,5338],{"class":5036},[59,5386,5387],{"class":5044}," la",[59,5389,2980],{"class":5036},[59,5391,5392],{"class":5040},"'ls -A'\n",[59,5394,5395,5397,5400,5402],{"class":61,"line":1398},[59,5396,5338],{"class":5036},[59,5398,5399],{"class":5044}," l",[59,5401,2980],{"class":5036},[59,5403,5404],{"class":5040},"'ls -CF'\n",[19,5406,5407],{},"个人主题：",[49,5409,5411],{"className":5019,"code":5410,"language":5021,"meta":54,"style":54},"short_path() {\n  local p=\"${PWD\u002F#$HOME\u002F~}\"          # ~\u002F 开头的友好路径\n  local -a segs=(\"${(s:\u002F:)p}\")       # 按 \u002F 拆分；zsh 支持负索引\n  local count=${#segs[@]}\n\n  # 只有 ~ 或 \u002F 的场合\n  (( count \u003C= 2 )) && print -r -- \"$p\" && return\n\n  # 从 ~ 开头：始终保留 ~ 与最后两段\n  if [[ ${segs[1]} == \"~\" || ${segs[1]} == \"\" && ${segs[2]} == \"~\" ]]; then\n    print -r -- \"…\u002F${segs[-2]}\u002F${segs[-1]}\"\n  else\n    # 普通绝对路径：用省略号开头\n    print -r -- \"…\u002F${segs[-2]}\u002F${segs[-1]}\"\n  fi\n}\n\n\nenv_prompt() {\n  if [[ -n \"$CONDA_DEFAULT_ENV\" ]]; then\n    print -r -- \"%{$fg[cyan]%}(${CONDA_DEFAULT_ENV})%{$reset_color%}\"\n  elif [[ -n \"$VIRTUAL_ENV\" ]]; then\n    print -r -- \"%{$fg[cyan]%}(${CONDA_DEFAULT_ENV})%{$reset_color%}\"\n  fi\n}\n\nPROMPT=$'%{$fg[red]%}┌[%{$fg_bold[white]%}$(env_prompt)%n%{$reset_color%}%{$fg[red]%}@%{$fg_bold[white]%}%m%{$reset_color%}%{$fg[red]%}] [%{$fg_bold[white]%}\u002Fdev\u002F%y%{$reset_color%}%{$fg[red]%}] %{$(git_prompt_info)%}%(?,,%{$fg[red]%}[%{$fg_bold[white]%}%?%{$reset_color%}%{$fg[red]%}])\n%{$fg[red]%}└[%{$fg_bold[white]%}$(short_path)%{$reset_color%}%{$fg[red]%}]>%{$reset_color%} '\nPS2=$' %{$fg[red]%}|>%{$reset_color%} '\n\nZSH_THEME_GIT_PROMPT_PREFIX=\"%{$fg[red]%}[%{$fg_bold[white]%}\"\nZSH_THEME_GIT_PROMPT_SUFFIX=\"%{$reset_color%}%{$fg[red]%}] \"\nZSH_THEME_GIT_PROMPT_DIRTY=\" %{$fg[red]%}⚡%{$reset_color%}\"\n",[56,5412,5413,5421,5450,5485,5509,5513,5518,5555,5559,5564,5605,5628,5633,5638,5656,5661,5666,5670,5674,5681,5703,5733,5754,5777,5782,5787,5792,5803,5809,5820,5825,5847,5867],{"__ignoreMap":54},[59,5414,5415,5418],{"class":61,"line":62},[59,5416,5417],{"class":5028},"short_path",[59,5419,5420],{"class":5044},"() {\n",[59,5422,5423,5426,5429,5431,5434,5437,5440,5442,5444,5447],{"class":61,"line":68},[59,5424,5425],{"class":5036},"  local",[59,5427,5428],{"class":5044}," p",[59,5430,2980],{"class":5036},[59,5432,5433],{"class":5040},"\"${",[59,5435,5436],{"class":5044},"PWD",[59,5438,5439],{"class":5036},"\u002F#",[59,5441,5248],{"class":5044},[59,5443,1468],{"class":5036},[59,5445,5446],{"class":5040},"~}\"",[59,5448,5449],{"class":5367},"          # ~\u002F 开头的友好路径\n",[59,5451,5452,5454,5457,5460,5462,5464,5467,5469,5472,5474,5476,5479,5482],{"class":61,"line":74},[59,5453,5425],{"class":5036},[59,5455,5456],{"class":5032}," -a",[59,5458,5459],{"class":5044}," segs",[59,5461,2980],{"class":5036},[59,5463,2573],{"class":5044},[59,5465,5466],{"class":5040},"\"${(",[59,5468,2592],{"class":5044},[59,5470,5471],{"class":5036},":\u002F:",[59,5473,2586],{"class":5040},[59,5475,19],{"class":5044},[59,5477,5478],{"class":5040},"}\"",[59,5480,5481],{"class":5044},")       ",[59,5483,5484],{"class":5367},"# 按 \u002F 拆分；zsh 支持负索引\n",[59,5486,5487,5489,5492,5494,5497,5500,5503,5506],{"class":61,"line":763},[59,5488,5425],{"class":5036},[59,5490,5491],{"class":5044}," count",[59,5493,2980],{"class":5036},[59,5495,5496],{"class":5044},"${",[59,5498,5499],{"class":5036},"#",[59,5501,5502],{"class":5044},"segs[",[59,5504,5505],{"class":5036},"@",[59,5507,5508],{"class":5044},"]}\n",[59,5510,5511],{"class":61,"line":769},[59,5512,1340],{"emptyLinePlaceholder":1339},[59,5514,5515],{"class":61,"line":775},[59,5516,5517],{"class":5367},"  # 只有 ~ 或 \u002F 的场合\n",[59,5519,5520,5523,5526,5529,5532,5535,5538,5541,5544,5547,5549,5552],{"class":61,"line":781},[59,5521,5522],{"class":5044},"  (( count ",[59,5524,5525],{"class":5036},"\u003C=",[59,5527,5528],{"class":5032}," 2",[59,5530,5531],{"class":5044}," )) && ",[59,5533,5534],{"class":5032},"print",[59,5536,5537],{"class":5032}," -r",[59,5539,5540],{"class":5032}," --",[59,5542,5543],{"class":5040}," \"",[59,5545,5546],{"class":5044},"$p",[59,5548,5245],{"class":5040},[59,5550,5551],{"class":5044}," && ",[59,5553,5554],{"class":5036},"return\n",[59,5556,5557],{"class":61,"line":787},[59,5558,1340],{"emptyLinePlaceholder":1339},[59,5560,5561],{"class":61,"line":1358},[59,5562,5563],{"class":5367},"  # 从 ~ 开头：始终保留 ~ 与最后两段\n",[59,5565,5566,5569,5572,5575,5578,5581,5584,5586,5589,5592,5595,5597,5599,5602],{"class":61,"line":1363},[59,5567,5568],{"class":5036},"  if",[59,5570,5571],{"class":5044}," [[ ${segs[1]} ",[59,5573,5574],{"class":5036},"==",[59,5576,5577],{"class":5040}," \"~\"",[59,5579,5580],{"class":5036}," ||",[59,5582,5583],{"class":5044}," ${segs[1]} ",[59,5585,5574],{"class":5036},[59,5587,5588],{"class":5040}," \"\"",[59,5590,5591],{"class":5036}," &&",[59,5593,5594],{"class":5044}," ${segs[2]} ",[59,5596,5574],{"class":5036},[59,5598,5577],{"class":5040},[59,5600,5601],{"class":5044}," ]]; ",[59,5603,5604],{"class":5036},"then\n",[59,5606,5607,5610,5612,5614,5617,5620,5623,5625],{"class":61,"line":1369},[59,5608,5609],{"class":5032},"    print",[59,5611,5537],{"class":5032},[59,5613,5540],{"class":5032},[59,5615,5616],{"class":5040}," \"…\u002F${",[59,5618,5619],{"class":5044},"segs",[59,5621,5622],{"class":5040},"[-2]}\u002F${",[59,5624,5619],{"class":5044},[59,5626,5627],{"class":5040},"[-1]}\"\n",[59,5629,5630],{"class":61,"line":1375},[59,5631,5632],{"class":5036},"  else\n",[59,5634,5635],{"class":61,"line":1381},[59,5636,5637],{"class":5367},"    # 普通绝对路径：用省略号开头\n",[59,5639,5640,5642,5644,5646,5648,5650,5652,5654],{"class":61,"line":1386},[59,5641,5609],{"class":5032},[59,5643,5537],{"class":5032},[59,5645,5540],{"class":5032},[59,5647,5616],{"class":5040},[59,5649,5619],{"class":5044},[59,5651,5622],{"class":5040},[59,5653,5619],{"class":5044},[59,5655,5627],{"class":5040},[59,5657,5658],{"class":61,"line":1392},[59,5659,5660],{"class":5036},"  fi\n",[59,5662,5663],{"class":61,"line":1398},[59,5664,5665],{"class":5044},"}\n",[59,5667,5668],{"class":61,"line":1404},[59,5669,1340],{"emptyLinePlaceholder":1339},[59,5671,5672],{"class":61,"line":1410},[59,5673,1340],{"emptyLinePlaceholder":1339},[59,5675,5676,5679],{"class":61,"line":1416},[59,5677,5678],{"class":5028},"env_prompt",[59,5680,5420],{"class":5044},[59,5682,5684,5686,5689,5692,5694,5697,5699,5701],{"class":61,"line":5683},20,[59,5685,5568],{"class":5036},[59,5687,5688],{"class":5044}," [[ ",[59,5690,5691],{"class":5036},"-n",[59,5693,5543],{"class":5040},[59,5695,5696],{"class":5044},"$CONDA_DEFAULT_ENV",[59,5698,5245],{"class":5040},[59,5700,5601],{"class":5044},[59,5702,5604],{"class":5036},[59,5704,5706,5708,5710,5712,5715,5718,5721,5724,5727,5730],{"class":61,"line":5705},21,[59,5707,5609],{"class":5032},[59,5709,5537],{"class":5032},[59,5711,5540],{"class":5032},[59,5713,5714],{"class":5040}," \"%{",[59,5716,5717],{"class":5044},"$fg",[59,5719,5720],{"class":5040},"[cyan]%}(${",[59,5722,5723],{"class":5044},"CONDA_DEFAULT_ENV",[59,5725,5726],{"class":5040},"})%{",[59,5728,5729],{"class":5044},"$reset_color",[59,5731,5732],{"class":5040},"%}\"\n",[59,5734,5736,5739,5741,5743,5745,5748,5750,5752],{"class":61,"line":5735},22,[59,5737,5738],{"class":5036},"  elif",[59,5740,5688],{"class":5044},[59,5742,5691],{"class":5036},[59,5744,5543],{"class":5040},[59,5746,5747],{"class":5044},"$VIRTUAL_ENV",[59,5749,5245],{"class":5040},[59,5751,5601],{"class":5044},[59,5753,5604],{"class":5036},[59,5755,5757,5759,5761,5763,5765,5767,5769,5771,5773,5775],{"class":61,"line":5756},23,[59,5758,5609],{"class":5032},[59,5760,5537],{"class":5032},[59,5762,5540],{"class":5032},[59,5764,5714],{"class":5040},[59,5766,5717],{"class":5044},[59,5768,5720],{"class":5040},[59,5770,5723],{"class":5044},[59,5772,5726],{"class":5040},[59,5774,5729],{"class":5044},[59,5776,5732],{"class":5040},[59,5778,5780],{"class":61,"line":5779},24,[59,5781,5660],{"class":5036},[59,5783,5785],{"class":61,"line":5784},25,[59,5786,5665],{"class":5044},[59,5788,5790],{"class":61,"line":5789},26,[59,5791,1340],{"emptyLinePlaceholder":1339},[59,5793,5795,5798,5800],{"class":61,"line":5794},27,[59,5796,5797],{"class":5044},"PROMPT",[59,5799,2980],{"class":5036},[59,5801,5802],{"class":5040},"$'%{$fg[red]%}┌[%{$fg_bold[white]%}$(env_prompt)%n%{$reset_color%}%{$fg[red]%}@%{$fg_bold[white]%}%m%{$reset_color%}%{$fg[red]%}] [%{$fg_bold[white]%}\u002Fdev\u002F%y%{$reset_color%}%{$fg[red]%}] %{$(git_prompt_info)%}%(?,,%{$fg[red]%}[%{$fg_bold[white]%}%?%{$reset_color%}%{$fg[red]%}])\n",[59,5804,5806],{"class":61,"line":5805},28,[59,5807,5808],{"class":5040},"%{$fg[red]%}└[%{$fg_bold[white]%}$(short_path)%{$reset_color%}%{$fg[red]%}]>%{$reset_color%} '\n",[59,5810,5812,5815,5817],{"class":61,"line":5811},29,[59,5813,5814],{"class":5044},"PS2",[59,5816,2980],{"class":5036},[59,5818,5819],{"class":5040},"$' %{$fg[red]%}|>%{$reset_color%} '\n",[59,5821,5823],{"class":61,"line":5822},30,[59,5824,1340],{"emptyLinePlaceholder":1339},[59,5826,5828,5831,5833,5836,5838,5841,5844],{"class":61,"line":5827},31,[59,5829,5830],{"class":5044},"ZSH_THEME_GIT_PROMPT_PREFIX",[59,5832,2980],{"class":5036},[59,5834,5835],{"class":5040},"\"%{",[59,5837,5717],{"class":5044},[59,5839,5840],{"class":5040},"[red]%}[%{",[59,5842,5843],{"class":5044},"$fg_bold",[59,5845,5846],{"class":5040},"[white]%}\"\n",[59,5848,5850,5853,5855,5857,5859,5862,5864],{"class":61,"line":5849},32,[59,5851,5852],{"class":5044},"ZSH_THEME_GIT_PROMPT_SUFFIX",[59,5854,2980],{"class":5036},[59,5856,5835],{"class":5040},[59,5858,5729],{"class":5044},[59,5860,5861],{"class":5040},"%}%{",[59,5863,5717],{"class":5044},[59,5865,5866],{"class":5040},"[red]%}] \"\n",[59,5868,5870,5873,5875,5878,5880,5883,5885],{"class":61,"line":5869},33,[59,5871,5872],{"class":5044},"ZSH_THEME_GIT_PROMPT_DIRTY",[59,5874,2980],{"class":5036},[59,5876,5877],{"class":5040},"\" %{",[59,5879,5717],{"class":5044},[59,5881,5882],{"class":5040},"[red]%}⚡%{",[59,5884,5729],{"class":5044},[59,5886,5732],{"class":5040},[19,5888,5889,5890],{},"写入到~\u002F.oh-my-zsh\u002Fthemes\u002FAEsir.zsh-theme，主要优化了路径显示：\n",[2147,5891],{"alt":5892,"src":5893},"AEsir.zsh-theme 预览","\u002Fimages\u002Fposts\u002Fdotfile_config\u002FAEsir_theme.png",[26,5895,5897],{"id":5896},"_3-配置vim","3. 配置vim",[19,5899,5900],{},"安装vim以及插件管理器、插件:",[49,5902,5904],{"className":5019,"code":5903,"language":5021,"meta":54,"style":54},"sudo apt install -y vim\n\nmkdir -p ~\u002F.vim\u002Fplugged && cd ~\u002F.vim\u002Fplugged\ngit clone git:\u002F\u002Fgithub.com\u002Fjiangmiao\u002Fauto-pairs.git \ngit clone https:\u002F\u002Fgithub.com\u002Fvim-airline\u002Fvim-airline \ngit clone https:\u002F\u002Fgithub.com\u002Fpreservim\u002Fnerdtree.git \n\ncurl -fLo ~\u002F.vim\u002Fautoload\u002Fplug.vim --create-dirs https:\u002F\u002Fraw.githubusercontent.com\u002Fjunegunn\u002Fvim-plug\u002Fmaster\u002Fplug.vim\n\n",[56,5905,5906,5919,5923,5942,5954,5965,5976,5980],{"__ignoreMap":54},[59,5907,5908,5910,5912,5914,5916],{"class":61,"line":62},[59,5909,5099],{"class":5028},[59,5911,5102],{"class":5040},[59,5913,5105],{"class":5040},[59,5915,5108],{"class":5032},[59,5917,5918],{"class":5040}," vim\n",[59,5920,5921],{"class":61,"line":68},[59,5922,1340],{"emptyLinePlaceholder":1339},[59,5924,5925,5928,5931,5934,5936,5939],{"class":61,"line":74},[59,5926,5927],{"class":5028},"mkdir",[59,5929,5930],{"class":5032}," -p",[59,5932,5933],{"class":5040}," ~\u002F.vim\u002Fplugged",[59,5935,5551],{"class":5044},[59,5937,5938],{"class":5032},"cd",[59,5940,5941],{"class":5040}," ~\u002F.vim\u002Fplugged\n",[59,5943,5944,5946,5948,5951],{"class":61,"line":763},[59,5945,5166],{"class":5028},[59,5947,5169],{"class":5040},[59,5949,5950],{"class":5040}," git:\u002F\u002Fgithub.com\u002Fjiangmiao\u002Fauto-pairs.git",[59,5952,5953],{"class":5044}," \n",[59,5955,5956,5958,5960,5963],{"class":61,"line":769},[59,5957,5166],{"class":5028},[59,5959,5169],{"class":5040},[59,5961,5962],{"class":5040}," https:\u002F\u002Fgithub.com\u002Fvim-airline\u002Fvim-airline",[59,5964,5953],{"class":5044},[59,5966,5967,5969,5971,5974],{"class":61,"line":775},[59,5968,5166],{"class":5028},[59,5970,5169],{"class":5040},[59,5972,5973],{"class":5040}," https:\u002F\u002Fgithub.com\u002Fpreservim\u002Fnerdtree.git",[59,5975,5953],{"class":5044},[59,5977,5978],{"class":61,"line":781},[59,5979,1340],{"emptyLinePlaceholder":1339},[59,5981,5982,5984,5987,5990,5993],{"class":61,"line":787},[59,5983,5147],{"class":5028},[59,5985,5986],{"class":5032}," -fLo",[59,5988,5989],{"class":5040}," ~\u002F.vim\u002Fautoload\u002Fplug.vim",[59,5991,5992],{"class":5032}," --create-dirs",[59,5994,5995],{"class":5040}," https:\u002F\u002Fraw.githubusercontent.com\u002Fjunegunn\u002Fvim-plug\u002Fmaster\u002Fplug.vim\n",[19,5997,5998],{},"配置 ~\u002F.vimrc",[49,6000,6002],{"className":5019,"code":6001,"language":5021,"meta":54,"style":54},"syntax on\n\n\" 设置编码为utf-8\nset encoding=utf-8\n\n\" 设置tab宽度为4个空格\nset tabstop=4\nset shiftwidth=4\nset expandtab\n\n\" 自动缩进\nset autoindent\n\n\" 显示行号和列号\nset number\nset ruler\n\n\" 显示当前行\nset cursorline\n\n\" 设置行号颜色为灰色\nhighlight LineNr ctermfg=gray\n\n\" 让vim可以用鼠标滚轮\nset mouse=a\n\ncall plug#begin('~\u002F.vim\u002Fplugged')\n\" Shorthand notation for plugin\nPlug 'jiangmiao\u002Fauto-pairs'\nPlug 'vim-airline\u002Fvim-airline'\nPlug 'preservim\u002Fnerdtree'\ncall plug#end()\n\nset laststatus=2  \"永远显示状态栏\nlet g:airline_powerline_fonts = 1  \" 支持 powerline 字体\nlet g:airline#extensions#tabline#enabled = 1 \" 显示窗口tab和buffer\n",[56,6003,6004,6012,6016,6021,6026,6030,6037,6048,6057,6064,6068,6073,6078,6082,6089,6096,6103,6107,6112,6117,6121,6128,6139,6143,6148,6153,6157,6162,6178,6186,6193,6200,6211,6215,6228,6243],{"__ignoreMap":54},[59,6005,6006,6009],{"class":61,"line":62},[59,6007,6008],{"class":5028},"syntax",[59,6010,6011],{"class":5040}," on\n",[59,6013,6014],{"class":61,"line":68},[59,6015,1340],{"emptyLinePlaceholder":1339},[59,6017,6018],{"class":61,"line":74},[59,6019,6020],{"class":5028},"\" 设置编码为utf-8\n",[59,6022,6023],{"class":61,"line":763},[59,6024,6025],{"class":5028},"set encoding=utf-8\n",[59,6027,6028],{"class":61,"line":769},[59,6029,1340],{"emptyLinePlaceholder":1339},[59,6031,6032,6034],{"class":61,"line":775},[59,6033,5245],{"class":5028},[59,6035,6036],{"class":5040}," 设置tab宽度为4个空格\n",[59,6038,6039,6042,6045],{"class":61,"line":781},[59,6040,6041],{"class":5032},"set",[59,6043,6044],{"class":5040}," tabstop=",[59,6046,6047],{"class":5032},"4\n",[59,6049,6050,6052,6055],{"class":61,"line":787},[59,6051,6041],{"class":5032},[59,6053,6054],{"class":5040}," shiftwidth=",[59,6056,6047],{"class":5032},[59,6058,6059,6061],{"class":61,"line":1358},[59,6060,6041],{"class":5032},[59,6062,6063],{"class":5040}," expandtab\n",[59,6065,6066],{"class":61,"line":1363},[59,6067,1340],{"emptyLinePlaceholder":1339},[59,6069,6070],{"class":61,"line":1369},[59,6071,6072],{"class":5028},"\" 自动缩进\n",[59,6074,6075],{"class":61,"line":1375},[59,6076,6077],{"class":5028},"set autoindent\n",[59,6079,6080],{"class":61,"line":1381},[59,6081,1340],{"emptyLinePlaceholder":1339},[59,6083,6084,6086],{"class":61,"line":1386},[59,6085,5245],{"class":5028},[59,6087,6088],{"class":5040}," 显示行号和列号\n",[59,6090,6091,6093],{"class":61,"line":1392},[59,6092,6041],{"class":5032},[59,6094,6095],{"class":5040}," number\n",[59,6097,6098,6100],{"class":61,"line":1398},[59,6099,6041],{"class":5032},[59,6101,6102],{"class":5040}," ruler\n",[59,6104,6105],{"class":61,"line":1404},[59,6106,1340],{"emptyLinePlaceholder":1339},[59,6108,6109],{"class":61,"line":1410},[59,6110,6111],{"class":5028},"\" 显示当前行\n",[59,6113,6114],{"class":61,"line":1416},[59,6115,6116],{"class":5028},"set cursorline\n",[59,6118,6119],{"class":61,"line":5683},[59,6120,1340],{"emptyLinePlaceholder":1339},[59,6122,6123,6125],{"class":61,"line":5705},[59,6124,5245],{"class":5028},[59,6126,6127],{"class":5040}," 设置行号颜色为灰色\n",[59,6129,6130,6133,6136],{"class":61,"line":5735},[59,6131,6132],{"class":5028},"highlight",[59,6134,6135],{"class":5040}," LineNr",[59,6137,6138],{"class":5040}," ctermfg=gray\n",[59,6140,6141],{"class":61,"line":5756},[59,6142,1340],{"emptyLinePlaceholder":1339},[59,6144,6145],{"class":61,"line":5779},[59,6146,6147],{"class":5028},"\" 让vim可以用鼠标滚轮\n",[59,6149,6150],{"class":61,"line":5784},[59,6151,6152],{"class":5028},"set mouse=a\n",[59,6154,6155],{"class":61,"line":5789},[59,6156,1340],{"emptyLinePlaceholder":1339},[59,6158,6159],{"class":61,"line":5794},[59,6160,6161],{"class":5028},"call plug#begin('~\u002F.vim\u002Fplugged')\n",[59,6163,6164,6166,6169,6172,6175],{"class":61,"line":5805},[59,6165,5245],{"class":5028},[59,6167,6168],{"class":5040}," Shorthand",[59,6170,6171],{"class":5040}," notation",[59,6173,6174],{"class":5040}," for",[59,6176,6177],{"class":5040}," plugin\n",[59,6179,6180,6183],{"class":61,"line":5811},[59,6181,6182],{"class":5028},"Plug",[59,6184,6185],{"class":5040}," 'jiangmiao\u002Fauto-pairs'\n",[59,6187,6188,6190],{"class":61,"line":5822},[59,6189,6182],{"class":5028},[59,6191,6192],{"class":5040}," 'vim-airline\u002Fvim-airline'\n",[59,6194,6195,6197],{"class":61,"line":5827},[59,6196,6182],{"class":5028},[59,6198,6199],{"class":5040}," 'preservim\u002Fnerdtree'\n",[59,6201,6202,6205,6208],{"class":61,"line":5849},[59,6203,6204],{"class":5028},"call",[59,6206,6207],{"class":5040}," plug#end",[59,6209,6210],{"class":5044},"()\n",[59,6212,6213],{"class":61,"line":5869},[59,6214,1340],{"emptyLinePlaceholder":1339},[59,6216,6218,6220,6223,6225],{"class":61,"line":6217},34,[59,6219,6041],{"class":5032},[59,6221,6222],{"class":5040}," laststatus=",[59,6224,2630],{"class":5032},[59,6226,6227],{"class":5040},"  \"永远显示状态栏\n",[59,6229,6231,6234,6237,6240],{"class":61,"line":6230},35,[59,6232,6233],{"class":5040},"let g:airline_powerline_fonts = 1  \"",[59,6235,6236],{"class":5040}," 支持",[59,6238,6239],{"class":5040}," powerline",[59,6241,6242],{"class":5040}," 字体\n",[59,6244,6246,6249,6252,6255,6258],{"class":61,"line":6245},36,[59,6247,6248],{"class":5032},"let",[59,6250,6251],{"class":5040}," g:airline#extensions#tabline#enabled",[59,6253,6254],{"class":5040}," =",[59,6256,6257],{"class":5032}," 1",[59,6259,6260],{"class":5040}," \" 显示窗口tab和buffer\n",[19,6262,6263],{},"效果如下：",[19,6265,6266],{},[2147,6267],{"alt":6268,"src":6269},"vim 预览","\u002Fimages\u002Fposts\u002Fdotfile_config\u002Fvim.png",[26,6271,6273],{"id":6272},"_4-安装tmux","4. 安装tmux",[49,6275,6277],{"className":5019,"code":6276,"language":5021,"meta":54,"style":54},"sudo apt install -y tmux\n",[56,6278,6279],{"__ignoreMap":54},[59,6280,6281,6283,6285,6287,6289],{"class":61,"line":62},[59,6282,5099],{"class":5028},[59,6284,5102],{"class":5040},[59,6286,5105],{"class":5040},[59,6288,5108],{"class":5032},[59,6290,6291],{"class":5040}," tmux\n",[1993,6293,6294],{},"html pre.shiki code .sqoU-, html code.shiki .sqoU-{--shiki-default:#B392F0;--shiki-light:#6F42C1}html pre.shiki code .sTU5a, html code.shiki .sTU5a{--shiki-default:#79B8FF;--shiki-light:#005CC5}html pre.shiki code .spKkM, html code.shiki .spKkM{--shiki-default:#F97583;--shiki-light:#D73A49}html pre.shiki code .skb7c, html code.shiki .skb7c{--shiki-default:#9ECBFF;--shiki-light:#032F62}html pre.shiki code .shWlK, html code.shiki .shWlK{--shiki-default:#E1E4E8;--shiki-light:#24292E}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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.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 pre.shiki code .si27w, html code.shiki .si27w{--shiki-default:#6A737D;--shiki-light:#6A737D}",{"title":54,"searchDepth":68,"depth":68,"links":6296},[6297,6298,6299,6300],{"id":5015,"depth":68,"text":5016},{"id":5088,"depth":68,"text":5089},{"id":5896,"depth":68,"text":5897},{"id":6272,"depth":68,"text":6273},"\u002Fimages\u002Fposts\u002Fdotfile_config\u002Fcover.jpg","2025-12-28",{},"\u002Fposts\u002Fdotfile_config\u002Fmain",{"title":5007,"description":54},"posts\u002Fdotfile_config\u002Fmain",[6308],"运维","9_-k5Sv2WradtJ4UETT_alxmxqhzTdGZf5PzTK9TOUc",{"id":6311,"title":6312,"body":6313,"cover":6369,"date":6370,"description":6317,"draft":2036,"extension":2037,"meta":6371,"navigation":1339,"path":6372,"seo":6373,"stem":6374,"summary":6375,"tags":6376,"__hash__":6377},"posts\u002Fposts\u002Ftest\u002Fmain.md","Æsir Home (v0)",{"type":8,"value":6314,"toc":6365},[6315,6318,6321,6325,6339,6342],[19,6316,6317],{},"Welcome 👋",[19,6319,6320],{},"实现了Æsir Home的最小化的v0版本，用于初步熟悉前后端的架构，以及Vue3 + Go的开发流程。\n目标在于快速发布并最小化未来的技术债",[26,6322,6324],{"id":6323},"v0-版本功能","v0 版本功能",[390,6326,6327,6330,6333,6336],{},[268,6328,6329],{},"文章列表（posts list）",[268,6331,6332],{},"文章详情（Markdown渲染，支持图片加载，代码高亮）",[268,6334,6335],{},"标签页面（tags page，支持标签筛选）",[268,6337,6338],{},"客户端搜索（标题 + 摘要）",[26,6340,6341],{"id":6341},"后续考虑升级的方向",[390,6343,6344,6347,6350,6353,6356,6359,6362],{},[268,6345,6346],{},"增加更多内容类型（视频，音频，或各种资源等）",[268,6348,6349],{},"资源访问权限（密码保护，或访问控制等）",[268,6351,6352],{},"服务端搜索（SQLite FTS5）",[268,6354,6355],{},"RSS + sitemap（用于订阅和SEO）",[268,6357,6358],{},"美化布局、交互等",[268,6360,6361],{},"Dark mode + themes（主题切换）",[268,6363,6364],{},"后台管理（可选，低优先级）",{"title":54,"searchDepth":68,"depth":68,"links":6366},[6367,6368],{"id":6323,"depth":68,"text":6324},{"id":6341,"depth":68,"text":6341},"\u002Fimages\u002Fposts\u002Ftest\u002Fcover.jpg","2025-12-27",{},"\u002Fposts\u002Ftest\u002Fmain",{"title":6312,"description":6317},"posts\u002Ftest\u002Fmain","Hello Æsir Home!",[2044],"K9cWZA1S3KLMn09iVIyZ5D3dscjmnIPytKqUOrzZbvU",1782672216592]