[{"data":1,"prerenderedAt":2254},["ShallowReactive",2],{"\u002F2026-06-21-ultracodeagent":3,"\u002F2026-06-21-ultracodeagent-rel":1309},{"id":4,"title":5,"body":6,"column":1292,"date":1293,"description":1294,"extension":1295,"hero_image":1296,"meta":1297,"navigation":134,"path":1298,"seo":1299,"series_id":1296,"severity":1296,"stem":1300,"summary":1301,"tags":1302,"__hash__":1308},"posts\u002F2026-06-21-Ultracode多Agent编排引擎重写.md","一次扇出，就能把机器打满",{"type":7,"value":8,"toc":1275},"minimark",[9,18,22,25,33,40,47,50,53,59,65,71,74,81,271,285,288,291,298,345,352,355,362,459,476,493,497,504,507,525,540,553,556,559,565,579,589,592,595,598,605,608,611,618,633,640,643,646,681,695,698,701,704,1183,1186,1190,1193,1199,1206,1209,1219,1222,1225,1228,1231,1238,1244,1250,1253,1268,1271],[10,11,12,13,17],"p",{},"单 Agent 处理复杂任务时，上下文上限和职责混乱很难避免。当任务规模扩大、需要多个专职代理时，简单的顺序派发不够用——需要一个真正的执行引擎，而不是一串 ",[14,15,16],"code",{},"await"," 调用。Ultracode 的重写把这个执行引擎从写死的轮次评委模式升级为通用、分阶段、可观察的编排系统。",[19,20,21],"h2",{"id":21},"为什么需要重写",[10,23,24],{},"原有 Ultracode 的核心逻辑是循环评委投票：单个 worker 代理做任务 → 固定的 9 个 judge 代理投票 → 收敛检测 → autoFix 落盘。这个模型有三个硬伤。",[10,26,27,28,32],{},"其一，",[29,30,31],"strong",{},"职责混乱","。所有代理都用同一套 prompt 模板，judge 和 worker 没有差别的实现，评委们也不知道自己要对抗什么。结果是许多时间花在重复评判已稳定的地方，而不是聚焦新引入的变化。",[10,34,35,36,39],{},"其二，",[29,37,38],{},"并发失控","。原架构是串行的：worker 做完 → 再起 9 个 judge。如果拆成了 10 个子任务，那就是 10×（1 + 9）= 100 个 agent 跑，没有并发上限的制约，大任务轻易触发网关限流。",[10,41,42,43,46],{},"其三，",[29,44,45],{},"观察盲区","。系统只在所有 judge 投票完后汇报结果。长任务跑到一半，用户看不见进展、无法判断是真的在做还是卡住了，也没办法中途调整策略。",[19,48,49],{"id":49},"三个核心决策",[10,51,52],{},"重写围绕三个硬需求展开。",[10,54,55,58],{},[29,56,57],{},"第一，并发必须有上限。"," 没有信号量的话，一个大的代理分派会立刻把网关打满 429，然后不得不在代码里加退避——但那是被动的、出了问题才补。正确做法是从一开始就设定并发的快慢桶：默认 10 并发（可调 5 ~ 15），队列存 100 个任务，当网关响应 429 时指数退避（100ms → 5s 重试，最多 3 次）。这样既防止了失控的并发喷发，也保证了任务不会丢失。",[10,60,61,64],{},[29,62,63],{},"第二，失败必须能隔离。"," 一个子任务失败不应该拦截整个编排流程，但要能标记为不可用、向下游传播。比如拆解阶段有个 worker 崩了，应该照样让其他 worker 继续跑，最后的综合阶段在做决策时看到「该 worker 失败」的标记，转入降级策略。这要求每个 agent 的执行结果都被独立记录：成功、失败、超时等状态都是数据，而不是异常。",[10,66,67,70],{},[29,68,69],{},"第三，执行过程必须可观察。"," 不是只返回最终结果，而是全程发事件：某阶段开始 → 该阶段有 N 个代理排队 → 某代理开始跑 → 它完成了（消耗多少 token、多少成本、耗时几秒）。中间可以实时看树形结构，长任务跑到一半时能判断是否要停、是否有机会加速。",[19,72,73],{"id":73},"从编排计划到分阶段执行",[10,75,76,77,80],{},"重写后的架构围绕一个数据结构：",[29,78,79],{},"编排计划","（Plan）。",[82,83,88],"pre",{"className":84,"code":85,"language":86,"meta":87,"style":87},"language-ts shiki shiki-themes github-light github-dark","interface Plan {\n  phases: Phase[];\n}\n\ninterface Phase {\n  title: string;\n  agents: AgentTask[];\n}\n\ninterface AgentTask {\n  id: string;\n  label: string;\n  role: 'worker' | 'judge' | 'synth';\n  prompt: string;\n  inputsFrom?: string[];\n}\n","ts","",[14,89,90,107,123,129,136,145,160,173,178,183,192,204,216,241,253,266],{"__ignoreMap":87},[91,92,95,99,103],"span",{"class":93,"line":94},"line",1,[91,96,98],{"class":97},"szBVR","interface",[91,100,102],{"class":101},"sScJk"," Plan",[91,104,106],{"class":105},"sVt8B"," {\n",[91,108,110,114,117,120],{"class":93,"line":109},2,[91,111,113],{"class":112},"s4XuR","  phases",[91,115,116],{"class":97},":",[91,118,119],{"class":101}," Phase",[91,121,122],{"class":105},"[];\n",[91,124,126],{"class":93,"line":125},3,[91,127,128],{"class":105},"}\n",[91,130,132],{"class":93,"line":131},4,[91,133,135],{"emptyLinePlaceholder":134},true,"\n",[91,137,139,141,143],{"class":93,"line":138},5,[91,140,98],{"class":97},[91,142,119],{"class":101},[91,144,106],{"class":105},[91,146,148,151,153,157],{"class":93,"line":147},6,[91,149,150],{"class":112},"  title",[91,152,116],{"class":97},[91,154,156],{"class":155},"sj4cs"," string",[91,158,159],{"class":105},";\n",[91,161,163,166,168,171],{"class":93,"line":162},7,[91,164,165],{"class":112},"  agents",[91,167,116],{"class":97},[91,169,170],{"class":101}," AgentTask",[91,172,122],{"class":105},[91,174,176],{"class":93,"line":175},8,[91,177,128],{"class":105},[91,179,181],{"class":93,"line":180},9,[91,182,135],{"emptyLinePlaceholder":134},[91,184,186,188,190],{"class":93,"line":185},10,[91,187,98],{"class":97},[91,189,170],{"class":101},[91,191,106],{"class":105},[91,193,195,198,200,202],{"class":93,"line":194},11,[91,196,197],{"class":112},"  id",[91,199,116],{"class":97},[91,201,156],{"class":155},[91,203,159],{"class":105},[91,205,207,210,212,214],{"class":93,"line":206},12,[91,208,209],{"class":112},"  label",[91,211,116],{"class":97},[91,213,156],{"class":155},[91,215,159],{"class":105},[91,217,219,222,224,228,231,234,236,239],{"class":93,"line":218},13,[91,220,221],{"class":112},"  role",[91,223,116],{"class":97},[91,225,227],{"class":226},"sZZnC"," 'worker'",[91,229,230],{"class":97}," |",[91,232,233],{"class":226}," 'judge'",[91,235,230],{"class":97},[91,237,238],{"class":226}," 'synth'",[91,240,159],{"class":105},[91,242,244,247,249,251],{"class":93,"line":243},14,[91,245,246],{"class":112},"  prompt",[91,248,116],{"class":97},[91,250,156],{"class":155},[91,252,159],{"class":105},[91,254,256,259,262,264],{"class":93,"line":255},15,[91,257,258],{"class":112},"  inputsFrom",[91,260,261],{"class":97},"?:",[91,263,156],{"class":155},[91,265,122],{"class":105},[91,267,269],{"class":93,"line":268},16,[91,270,128],{"class":105},[10,272,273,274,277,278,280,281,284],{},"计划定义了多个阶段，每个阶段内有若干代理并行执行。key point 是 ",[14,275,276],{},"inputsFrom","——如果某个 judge 要对抗式检查前面三个 worker 的产出，它的 ",[14,279,276],{}," 就引用那三个 worker 的 id。引擎执行时会把前序输出自动拼接到 prompt 前，规则很简单：",[29,282,283],{},"只能引用更早阶段的输出","，否则计划非法。",[10,286,287],{},"最终产出规定为最后一个阶段所有代理的输出拼接。通常最后一个阶段是单个 synth（综合器），所以最终答案就是它的输出。",[19,289,290],{"id":290},"执行引擎的三层保证",[10,292,293,294,297],{},"新的执行引擎 ",[14,295,296],{},"runPlan"," 接收这份计划和一个回调函数，逐阶段推进：",[299,300,301,311,325],"ol",{},[302,303,304,307,308,310],"li",{},[29,305,306],{},"校验阶段","：计划的 id 要唯一、",[14,309,276],{}," 只能指向更早阶段的存在 id、每个阶段至少有一个代理。非法计划直接返回失败，不浪费 API 调用。",[302,312,313,316,317,320,321,324],{},[29,314,315],{},"并发执行","：每个阶段启动后，引擎用一个信号量控制并发。假设有 5 个代理要在某阶段跑，并发限是 10，那么 5 个直接入队并行。前面某个代理完成后，队列里的下一个立刻启动，永远不超过 10 个。每个代理在启动时发 ",[14,318,319],{},"agent_start"," 事件，完成时发 ",[14,322,323],{},"agent_end"," 事件（包含成功与否、消耗、耗时）。",[302,326,327,330,331,334,335,334,338,334,341,344],{},[29,328,329],{},"护栏检查","：每启动新代理前检查三条红线——已用总成本超预算、已跑过的代理总数超上限（200）、墙钟超 4 小时。任一触发就停，返回已累计的结果和停止原因（",[14,332,333],{},"done"," \u002F ",[14,336,337],{},"budget",[14,339,340],{},"max_agents",[14,342,343],{},"timeout","）。",[10,346,347,348,351],{},"这样设计的好处是，即便一个代理失败，也不卡管线——该代理的 ",[14,349,350],{},"ok:false"," 被记录，下一代理照样启动。最后发现有人失败时，综合器可以选择降级方案（比如忽略这个 worker 的产出、或者用备选逻辑）。",[19,353,354],{"id":354},"失败隔离与结果合并",[10,356,357,358,361],{},"每个代理的执行结果是一个 ",[14,359,360],{},"AgentRunResult"," 对象：",[82,363,365],{"className":84,"code":364,"language":86,"meta":87,"style":87},"interface AgentRunResult {\n  ok: boolean;\n  text: string;\n  costUsd: number;\n  tokensIn: number;\n  tokensOut: number;\n  turns: number;\n  elapsedMs: number;\n}\n",[14,366,367,376,388,399,411,422,433,444,455],{"__ignoreMap":87},[91,368,369,371,374],{"class":93,"line":94},[91,370,98],{"class":97},[91,372,373],{"class":101}," AgentRunResult",[91,375,106],{"class":105},[91,377,378,381,383,386],{"class":93,"line":109},[91,379,380],{"class":112},"  ok",[91,382,116],{"class":97},[91,384,385],{"class":155}," boolean",[91,387,159],{"class":105},[91,389,390,393,395,397],{"class":93,"line":125},[91,391,392],{"class":112},"  text",[91,394,116],{"class":97},[91,396,156],{"class":155},[91,398,159],{"class":105},[91,400,401,404,406,409],{"class":93,"line":131},[91,402,403],{"class":112},"  costUsd",[91,405,116],{"class":97},[91,407,408],{"class":155}," number",[91,410,159],{"class":105},[91,412,413,416,418,420],{"class":93,"line":138},[91,414,415],{"class":112},"  tokensIn",[91,417,116],{"class":97},[91,419,408],{"class":155},[91,421,159],{"class":105},[91,423,424,427,429,431],{"class":93,"line":147},[91,425,426],{"class":112},"  tokensOut",[91,428,116],{"class":97},[91,430,408],{"class":155},[91,432,159],{"class":105},[91,434,435,438,440,442],{"class":93,"line":162},[91,436,437],{"class":112},"  turns",[91,439,116],{"class":97},[91,441,408],{"class":155},[91,443,159],{"class":105},[91,445,446,449,451,453],{"class":93,"line":175},[91,447,448],{"class":112},"  elapsedMs",[91,450,116],{"class":97},[91,452,408],{"class":155},[91,454,159],{"class":105},[91,456,457],{"class":93,"line":180},[91,458,128],{"class":105},[10,460,461,462,465,466,468,469,471,472,475],{},"引擎在 ",[14,463,464],{},"outputs"," 字典里按代理 id 存下所有输出文本，无论成功失败。如果某个 worker 失败了（比如网络超时、模型拒绝），它的 ",[14,467,350],{}," 和错误信息文本被记录；下游的 judge 如果设了 ",[14,470,276],{}," 引它，拿到的是「",[91,473,474],{},"失败","」的标记而不是代码。synth 在综合时看到这个标记，决定是否落地降级逻辑。这样避免了一个环节的失败导致后续环节无法启动的僵局。",[10,477,478,479,482,483,334,486,334,489,492],{},"计量在引擎层统一累加：每个代理完成后，它的 token 消耗、成本、耗时都加进全局统计。最终 ",[14,480,481],{},"PlanRunResult"," 返回的 ",[14,484,485],{},"totalCostUsd",[14,487,488],{},"totalTokensIn",[14,490,491],{},"totalTokensOut"," 是所有代理的总和，这个数字在实时树的统计行里显示。用户能看到这一轮 Ultracode 到底花了多少钱、消耗了多少 token。",[19,494,496],{"id":495},"ai-编排器从任务到计划","AI 编排器：从任务到计划",[10,498,499,500,503],{},"光有执行引擎不够，还需要一个组件把用户的任务自动转成合法的计划——这就是 ",[29,501,502],{},"AI 编排器","。",[10,505,506],{},"编排器是一个简单的 LLM 调用：告诉模型「我需要编排一个多代理方案解决这个任务」，模型回复一份 JSON Plan。它决定什么时候拆多阶段、什么时候加 judge、什么时候单 worker 就够了。核心要点：",[508,509,510,513,516],"ul",{},[302,511,512],{},"简单任务（比如\"翻译这段文本\"）→ 单 phase、单 worker，不过度工程化。",[302,514,515],{},"复杂任务 → 典型三阶段：Phase 1 拆解并行（多 worker）→ Phase 2 对抗验证（judge 们各自检查）→ Phase 3 综合（synth 合并）。",[302,517,518,519,521,522,524],{},"每个 judge 的 ",[14,520,276],{}," 指向前面的 worker，每个 synth 的 ",[14,523,276],{}," 指向全部前序输出。",[10,526,527,528,531,532,535,536,539],{},"Role 在执行时有默认映射。",[14,529,530],{},"worker"," 代理给全工具、maxTurns 设 16，让它有充分的时间思考和调用工具。",[14,533,534],{},"judge"," 代理不给文件工具（纯推理，避免在主目录空转浪费时间），maxTurns 设 4 就够（只需审查、不需实现）。",[14,537,538],{},"synth"," 不给任何工具（纯思考与综合），maxTurns 也是 4。这个映射可以在配置里调整，但默认值经过真机验证，不浪费 token 又保证完成度。",[10,541,542,543,545,546,549,550,552],{},"解析时有三道防线。第一，如果 LLM 完全乱来（乱文本、无 JSON、字段缺失），fallback 到「单 phase 单 worker」。第二，如果 Plan 结构合法但逻辑不符（id 重复、",[14,544,276],{}," 引同阶段的输出、总代理数超 200），",[14,547,548],{},"validatePlan"," 拦下来，转 fallback。第三，编排器代理本身崩了（返回 ",[14,551,350],{},"），直接 fallback。永远不抛异常、永远返回一个合法 Plan——这是对聊天服务的基本承诺。",[10,554,555],{},"Fallback 计划取用户任务的首行作为标签（约 24 字截断），生成一个单 phase 单 worker 的方案。即便编排器完全失败，系统也能降到「让单个 worker 代理尽力而为」的状态，至少能产出一个结果。这个降级极端简单，但对容错性至关重要。",[19,557,558],{"id":558},"单代理执行器的文本捕获修复",[10,560,561,562,503],{},"升级执行引擎时顺手修了一个隐藏的 bug：",[29,563,564],{},"空文本",[10,566,567,568,571,572,575,576,578],{},"之前的架构里，SDK 的消息循环会从 ",[14,569,570],{},"result"," 类消息里只读成本和轮数，但丢掉了最终文本字段。在聊天里没事，因为文本通过 ",[14,573,574],{},"onText"," 事件流式积累。但在 Ultracode 的低 maxTurns 子代理里（比如只跑 3 轮的 judge），最终答案常常只在 SDK 返回的 ",[14,577,570],{}," 对象的 text 字段里，一旦被丢就变成空字符串，后续评委拿不到内容。",[10,580,581,582,585,586,588],{},"修法是扩展 SDK message 的消费方式：新增一个 ",[14,583,584],{},"finalText"," 字段（从 result 消息的 text 取出），新执行器的文本获取规则是「优先用流式累加的 assistant 文本（非空），否则用 ",[14,587,584],{},"」。两层保险，确保没有空文本掉落。",[19,590,591],{"id":591},"实时执行树与折叠视觉",[10,593,594],{},"执行过程中，每个事件都被渲染成一棵树：顶部脉动蓝点 + 「Ultracode」+ 实时统计行（已用时间 · 代理数 · tokens · 成本）；下面是若干折叠的阶段分组，每个阶段显示代理数和进度圆点；展开时是一张表，每行一个代理（状态图标 + 名字 + tokens + 工具调用数 + 成本 + 耗时）。",[10,596,597],{},"运行中，running 的行背景高亮蓝色，queued 的行显「排队」，done 的行打 ✓，failed 的行打 ✗。进度圆点动态着色，一眼看本阶段的完成度。任务跑完后，树自动折成一行摘要（「Ultracode · 7 代理 · 142.6k tokens · $0.21」），点击展开看完整细节。",[10,599,600,601,604],{},"这一层的数据从执行引擎的事件流 → 聊天的 IPC → 前端组件。组件本地维护计时（用 ",[14,602,603],{},"setInterval"," 每秒更新已用时间），不依赖轮询服务端。组件卸载时清理 interval，避免泄漏。",[19,606,607],{"id":607},"并发与失败的权衡",[10,609,610],{},"这套系统在三个坐标轴上做了权衡。",[10,612,613,614,617],{},"其一是",[29,615,616],{},"吞吐 vs 限流","。并发默认 10，可调到 5（保守） ~ 15（激进）。10 这个数字经验得来：足够快（百代理任务不会太慢），又不会轻易撞 429。网关如果返回限流，退避队列自动延缓重试（100ms 起，指数增长到 5 秒，最多 3 轮），不需要外层干涉。这样既防止了脉冲式的并发喷发把网关打崩，也保证了排队的任务不会丢失。",[10,619,620,621,624,625,628,629,632],{},"其二是",[29,622,623],{},"自治 vs 安全","。主代理可以派任意数量的子代理、动态赋予工具权限，但 ",[14,626,627],{},"danger-guard","（防 ",[14,630,631],{},"rm -rf"," 等灾难命令）对所有子代理强制生效，不可绕过、不可被赋权赋进去。这是不可协商的红线。某个 judge 即便被指示「删掉这个目录」，系统也会直接 deny，日志记录尝试，让主代理知道。",[10,634,635,636,639],{},"其三是",[29,637,638],{},"完美 vs 可用","。原架构追求收敛一致的答案（所有 judge 投票通过），这导致许多任务无限收敛。新架构则是分阶段推进：每个阶段内，所有并行的代理跑完后再进入下一个阶段，不需要等待它们\"一致\"。如果最后发现有代理失败，synth 在综合时看到失败标记，主动降级（比如跳过那个输入、用其他输出补充）。终止条件是「超轮数（500）\u002F 超成本 \u002F 超墙钟（240 分钟）」，保证任务总能在有限时间内完成。这三条护栏是硬边界，任一触发立刻停，无论是否\"完美\"。",[19,641,642],{"id":642},"测试验证与真机硬门槛",[10,644,645],{},"新引擎在单元测试里用 mock executor 验证：",[508,647,648,657,663,669,675],{},[302,649,650,653,654,656],{},[29,651,652],{},"编排计划的校验","：id 唯一性、",[14,655,276],{}," 只能引更早阶段、无环依赖等；非法计划直接 reject。",[302,658,659,662],{},[29,660,661],{},"并发与信号量","：手动注入延迟的 executor，验证同时跑的代理数永不超过并发上限；队列机制正确。",[302,664,665,668],{},[29,666,667],{},"护栏触发","：构造会超成本、超代理数、超超时的场景，验证在对的时刻停止、返回对应的 stopReason。",[302,670,671,674],{},[29,672,673],{},"失败隔离","：某代理 ok:false，后续代理照样启动；最终输出字典里该代理的值是错误信息而不是空。",[302,676,677,680],{},[29,678,679],{},"事件流","：逐代理验证 agent_start 和 agent_end 事件的顺序和内容。",[10,682,683,684,690,691,694],{},"但光有单测不够。真机验收要求：",[29,685,686,687,689],{},"手写一份单 phase 单 worker 的最小计划，喂给 ",[14,688,296],{}," 走真实的 SDK 调用","。断言返回的 ",[14,692,693],{},"finalOutput"," 非空（确认空文本 bug 已修）、事件链完整、成本与 token 数据透出。这是硬门槛——不过此测试，不算完成。",[19,696,697],{"id":697},"一份具体的编排例子",[10,699,700],{},"假设任务是「用 TypeScript 各写 3 个独立纯函数（isPalindrome \u002F isPrime \u002F fibonacci），对抗式验证后综合为一份带测试的最终代码」。",[10,702,703],{},"编排器生成的计划看起来像：",[82,705,709],{"className":706,"code":707,"language":708,"meta":87,"style":87},"language-json shiki shiki-themes github-light github-dark","{\n  \"objective\": \"实现三个算法函数并通过对抗验证\",\n  \"phases\": [\n    {\n      \"title\": \"拆解并行实现\",\n      \"agents\": [\n        {\"id\": \"w1\", \"label\": \"实现 isPalindrome\", \"role\": \"worker\", \"prompt\": \"...\"},\n        {\"id\": \"w2\", \"label\": \"实现 isPrime\", \"role\": \"worker\", \"prompt\": \"...\"},\n        {\"id\": \"w3\", \"label\": \"实现 fibonacci\", \"role\": \"worker\", \"prompt\": \"...\"}\n      ]\n    },\n    {\n      \"title\": \"对抗验证\",\n      \"agents\": [\n        {\"id\": \"j1\", \"label\": \"检查 isPalindrome\", \"role\": \"judge\", \"inputsFrom\": [\"w1\"], \"prompt\": \"...\"},\n        {\"id\": \"j2\", \"label\": \"检查 isPrime\", \"role\": \"judge\", \"inputsFrom\": [\"w2\"], \"prompt\": \"...\"},\n        {\"id\": \"j3\", \"label\": \"检查 fibonacci\", \"role\": \"judge\", \"inputsFrom\": [\"w3\"], \"prompt\": \"...\"}\n      ]\n    },\n    {\n      \"title\": \"综合\",\n      \"agents\": [\n        {\"id\": \"s1\", \"label\": \"整合最终代码与测试\", \"role\": \"synth\", \"inputsFrom\": [\"w1\", \"w2\", \"w3\", \"j1\", \"j2\", \"j3\"], \"prompt\": \"...\"}\n      ]\n    }\n  ]\n}\n","json",[14,710,711,716,730,738,743,755,762,809,847,885,890,895,899,910,916,966,1012,1059,1064,1069,1074,1086,1093,1161,1166,1172,1178],{"__ignoreMap":87},[91,712,713],{"class":93,"line":94},[91,714,715],{"class":105},"{\n",[91,717,718,721,724,727],{"class":93,"line":109},[91,719,720],{"class":155},"  \"objective\"",[91,722,723],{"class":105},": ",[91,725,726],{"class":226},"\"实现三个算法函数并通过对抗验证\"",[91,728,729],{"class":105},",\n",[91,731,732,735],{"class":93,"line":125},[91,733,734],{"class":155},"  \"phases\"",[91,736,737],{"class":105},": [\n",[91,739,740],{"class":93,"line":131},[91,741,742],{"class":105},"    {\n",[91,744,745,748,750,753],{"class":93,"line":138},[91,746,747],{"class":155},"      \"title\"",[91,749,723],{"class":105},[91,751,752],{"class":226},"\"拆解并行实现\"",[91,754,729],{"class":105},[91,756,757,760],{"class":93,"line":147},[91,758,759],{"class":155},"      \"agents\"",[91,761,737],{"class":105},[91,763,764,767,770,772,775,778,781,783,786,788,791,793,796,798,801,803,806],{"class":93,"line":162},[91,765,766],{"class":105},"        {",[91,768,769],{"class":155},"\"id\"",[91,771,723],{"class":105},[91,773,774],{"class":226},"\"w1\"",[91,776,777],{"class":105},", ",[91,779,780],{"class":155},"\"label\"",[91,782,723],{"class":105},[91,784,785],{"class":226},"\"实现 isPalindrome\"",[91,787,777],{"class":105},[91,789,790],{"class":155},"\"role\"",[91,792,723],{"class":105},[91,794,795],{"class":226},"\"worker\"",[91,797,777],{"class":105},[91,799,800],{"class":155},"\"prompt\"",[91,802,723],{"class":105},[91,804,805],{"class":226},"\"...\"",[91,807,808],{"class":105},"},\n",[91,810,811,813,815,817,820,822,824,826,829,831,833,835,837,839,841,843,845],{"class":93,"line":175},[91,812,766],{"class":105},[91,814,769],{"class":155},[91,816,723],{"class":105},[91,818,819],{"class":226},"\"w2\"",[91,821,777],{"class":105},[91,823,780],{"class":155},[91,825,723],{"class":105},[91,827,828],{"class":226},"\"实现 isPrime\"",[91,830,777],{"class":105},[91,832,790],{"class":155},[91,834,723],{"class":105},[91,836,795],{"class":226},[91,838,777],{"class":105},[91,840,800],{"class":155},[91,842,723],{"class":105},[91,844,805],{"class":226},[91,846,808],{"class":105},[91,848,849,851,853,855,858,860,862,864,867,869,871,873,875,877,879,881,883],{"class":93,"line":180},[91,850,766],{"class":105},[91,852,769],{"class":155},[91,854,723],{"class":105},[91,856,857],{"class":226},"\"w3\"",[91,859,777],{"class":105},[91,861,780],{"class":155},[91,863,723],{"class":105},[91,865,866],{"class":226},"\"实现 fibonacci\"",[91,868,777],{"class":105},[91,870,790],{"class":155},[91,872,723],{"class":105},[91,874,795],{"class":226},[91,876,777],{"class":105},[91,878,800],{"class":155},[91,880,723],{"class":105},[91,882,805],{"class":226},[91,884,128],{"class":105},[91,886,887],{"class":93,"line":185},[91,888,889],{"class":105},"      ]\n",[91,891,892],{"class":93,"line":194},[91,893,894],{"class":105},"    },\n",[91,896,897],{"class":93,"line":206},[91,898,742],{"class":105},[91,900,901,903,905,908],{"class":93,"line":218},[91,902,747],{"class":155},[91,904,723],{"class":105},[91,906,907],{"class":226},"\"对抗验证\"",[91,909,729],{"class":105},[91,911,912,914],{"class":93,"line":243},[91,913,759],{"class":155},[91,915,737],{"class":105},[91,917,918,920,922,924,927,929,931,933,936,938,940,942,945,947,950,953,955,958,960,962,964],{"class":93,"line":255},[91,919,766],{"class":105},[91,921,769],{"class":155},[91,923,723],{"class":105},[91,925,926],{"class":226},"\"j1\"",[91,928,777],{"class":105},[91,930,780],{"class":155},[91,932,723],{"class":105},[91,934,935],{"class":226},"\"检查 isPalindrome\"",[91,937,777],{"class":105},[91,939,790],{"class":155},[91,941,723],{"class":105},[91,943,944],{"class":226},"\"judge\"",[91,946,777],{"class":105},[91,948,949],{"class":155},"\"inputsFrom\"",[91,951,952],{"class":105},": [",[91,954,774],{"class":226},[91,956,957],{"class":105},"], ",[91,959,800],{"class":155},[91,961,723],{"class":105},[91,963,805],{"class":226},[91,965,808],{"class":105},[91,967,968,970,972,974,977,979,981,983,986,988,990,992,994,996,998,1000,1002,1004,1006,1008,1010],{"class":93,"line":268},[91,969,766],{"class":105},[91,971,769],{"class":155},[91,973,723],{"class":105},[91,975,976],{"class":226},"\"j2\"",[91,978,777],{"class":105},[91,980,780],{"class":155},[91,982,723],{"class":105},[91,984,985],{"class":226},"\"检查 isPrime\"",[91,987,777],{"class":105},[91,989,790],{"class":155},[91,991,723],{"class":105},[91,993,944],{"class":226},[91,995,777],{"class":105},[91,997,949],{"class":155},[91,999,952],{"class":105},[91,1001,819],{"class":226},[91,1003,957],{"class":105},[91,1005,800],{"class":155},[91,1007,723],{"class":105},[91,1009,805],{"class":226},[91,1011,808],{"class":105},[91,1013,1015,1017,1019,1021,1024,1026,1028,1030,1033,1035,1037,1039,1041,1043,1045,1047,1049,1051,1053,1055,1057],{"class":93,"line":1014},17,[91,1016,766],{"class":105},[91,1018,769],{"class":155},[91,1020,723],{"class":105},[91,1022,1023],{"class":226},"\"j3\"",[91,1025,777],{"class":105},[91,1027,780],{"class":155},[91,1029,723],{"class":105},[91,1031,1032],{"class":226},"\"检查 fibonacci\"",[91,1034,777],{"class":105},[91,1036,790],{"class":155},[91,1038,723],{"class":105},[91,1040,944],{"class":226},[91,1042,777],{"class":105},[91,1044,949],{"class":155},[91,1046,952],{"class":105},[91,1048,857],{"class":226},[91,1050,957],{"class":105},[91,1052,800],{"class":155},[91,1054,723],{"class":105},[91,1056,805],{"class":226},[91,1058,128],{"class":105},[91,1060,1062],{"class":93,"line":1061},18,[91,1063,889],{"class":105},[91,1065,1067],{"class":93,"line":1066},19,[91,1068,894],{"class":105},[91,1070,1072],{"class":93,"line":1071},20,[91,1073,742],{"class":105},[91,1075,1077,1079,1081,1084],{"class":93,"line":1076},21,[91,1078,747],{"class":155},[91,1080,723],{"class":105},[91,1082,1083],{"class":226},"\"综合\"",[91,1085,729],{"class":105},[91,1087,1089,1091],{"class":93,"line":1088},22,[91,1090,759],{"class":155},[91,1092,737],{"class":105},[91,1094,1096,1098,1100,1102,1105,1107,1109,1111,1114,1116,1118,1120,1123,1125,1127,1129,1131,1133,1135,1137,1139,1141,1143,1145,1147,1149,1151,1153,1155,1157,1159],{"class":93,"line":1095},23,[91,1097,766],{"class":105},[91,1099,769],{"class":155},[91,1101,723],{"class":105},[91,1103,1104],{"class":226},"\"s1\"",[91,1106,777],{"class":105},[91,1108,780],{"class":155},[91,1110,723],{"class":105},[91,1112,1113],{"class":226},"\"整合最终代码与测试\"",[91,1115,777],{"class":105},[91,1117,790],{"class":155},[91,1119,723],{"class":105},[91,1121,1122],{"class":226},"\"synth\"",[91,1124,777],{"class":105},[91,1126,949],{"class":155},[91,1128,952],{"class":105},[91,1130,774],{"class":226},[91,1132,777],{"class":105},[91,1134,819],{"class":226},[91,1136,777],{"class":105},[91,1138,857],{"class":226},[91,1140,777],{"class":105},[91,1142,926],{"class":226},[91,1144,777],{"class":105},[91,1146,976],{"class":226},[91,1148,777],{"class":105},[91,1150,1023],{"class":226},[91,1152,957],{"class":105},[91,1154,800],{"class":155},[91,1156,723],{"class":105},[91,1158,805],{"class":226},[91,1160,128],{"class":105},[91,1162,1164],{"class":93,"line":1163},24,[91,1165,889],{"class":105},[91,1167,1169],{"class":93,"line":1168},25,[91,1170,1171],{"class":105},"    }\n",[91,1173,1175],{"class":93,"line":1174},26,[91,1176,1177],{"class":105},"  ]\n",[91,1179,1181],{"class":93,"line":1180},27,[91,1182,128],{"class":105},[10,1184,1185],{},"执行时：Phase 1 三个 worker 并行跑（互相独立）→ Phase 2 三个 judge 并行验证（各自消费一个 worker 的产出）→ Phase 3 synth 综合所有输入产出最终答案。如果某个 worker 失败了（比如网络超时），judge 仍继续验证其他 worker，synth 在综合时看到「w1 失败」的标记，决定采用降级方案。",[19,1187,1189],{"id":1188},"外部集成思考档位与聊天路由","外部集成：思考档位与聊天路由",[10,1191,1192],{},"在聊天层，Ultracode 和普通回复一样，是一条消息的「思考档位」。用户在 composer 选普通 \u002F 深度 \u002F Ultra（对应 normal \u002F extended \u002F ultracode），送出消息时聊天 endpoint 判断档位，如果是 Ultra 就走编排管线。",[10,1194,1195,1196,1198],{},"管线是：接收任务 → 编排器生成 Plan → 执行引擎 ",[14,1197,296],{}," 按阶段跑 → 事件流实时推到前端树 → 完成时最后的 synth 输出成为聊天的消息内容，落库作为一条 message。整个过程对聊天是透明的——用户看到的只是一条消息，但这条消息的产生过程被完整记录了（每个 phase、每个 agent、每个事件），支持稍后点击树节点回放具体某个代理的输出。",[10,1200,1201,1202,1205],{},"这个设计的好处是，Ultracode 不再是一个独立屏幕，而是聊天的一个模式。消息历史、搜索、导出等聊天功能自动支持 Ultracode 的任务——不需要单独维护一套 ",[14,1203,1204],{},"\u002Fultracode"," 屏的数据模型。",[19,1207,1208],{"id":1208},"从原架构到新架构的演变",[10,1210,1211,1212,1215,1216,503],{},"回看原有的评委-轮次模式和新的分阶段编排，区别在于",[29,1213,1214],{},"谁决策编排拓扑","和",[29,1217,1218],{},"何时收敛",[10,1220,1221],{},"原架构是「一个 worker + 9 个评委 + 收敛循环」的固定管线。优点是逻辑简单、有明确的投票机制；缺点是不敏捷——即便 worker 搞定了，还要等 9 个 judge 都投票，任何一个 judge 的异议都可能推进新一轮，导致无限循环。",[10,1223,1224],{},"新架构则是「LLM 编排器规划拓扑、通用引擎执行、阶段内并发、失败不拦管线」。编排器看到复杂任务时自主拆多 phase（比如 10 个并行 worker），而不是冗长的轮次。即便某个 worker 失败，judge 照样验证其他成功的 worker，synth 在综合时主动决定降级。这样既不浪费时间在已稳定的输出上重复评判，也保证了管线的吞吐。",[10,1226,1227],{},"从成本角度，新架构潜在地消耗更少——同样的任务规模，原架构是 1 worker + 9 judge × N 轮，新架构是 N worker + N judge（对抗验证）+ 1 synth，通常少好几倍。真机上见过单个任务从原架构的「200+ 代理跑了 6 轮」降到新架构的「30 代理一轮过」。",[19,1229,1230],{"id":1230},"仍未解决的边界情况",[10,1232,1233,1234,1237],{},"设计中也有已知的限制。其一，",[29,1235,1236],{},"子代理递归不支持","——即子代理再派下一层子代理。SDK 的 Task 工具在子代理的上下文不可用，这是 SDK 层的约束，引擎无法绕过。实践中这个限制影响不大，因为 Ultracode 的典型用途是「任务 → 多 worker 并行 → 对抗验证」的三层，很少需要递归。",[10,1239,35,1240,1243],{},[29,1241,1242],{},"动态编排调整","。计划一旦生成后就是固定的，运行中不能动态插入新 phase 或新 agent。如果某个阶段的结果出乎预料，主代理不能中途改变后续阶段的代理配置。这是为了保证计划的确定性和可回放性——一份计划应该在相同的输入下产出相同的执行树。未来如果真需要这个能力，可以在编排器层面实现「多轮编排」（第一轮生成初步计划、看结果后再微调）。",[10,1245,42,1246,1249],{},[29,1247,1248],{},"编排器本身的稳定性","。LLM 产出畸形 Plan 的风险永远存在，fallback 机制可以兜底但不能杜绝。未来可以考虑对编排器的输出做更激进的验证、甚至让编排器自己生成测试 case 来验证拓扑设计。但这超出了 SP 子项目的范围。",[19,1251,1252],{"id":1252},"后续迭代方向",[10,1254,1255,1256,1259,1260,1263,1264,1267],{},"这套引擎的设计目标是「通用」，所以留了扩展点。首先是",[29,1257,1258],{},"编排策略","——现在编排器是简单的「LLM 一次调用」，未来可以升级为「判断复杂度 → 按复杂度选用编排模板 → 微调参数」，甚至「用历史成功案例做 few-shot 引导」。其次是",[29,1261,1262],{},"收敛检测的进阶","——现在的判据是「连续 N 轮无新问题」，太粗糙。可以升级为「按问题类型（语法 vs 逻辑 vs 性能）分别统计、不同类型不同收敛阈值」。第三是",[29,1265,1266],{},"成本可见与预算控制","——现在成本相加没有细颗粒度控制（只有绝对上限），可以做分阶段的预算、或者基于历史任务的成本预测。",[10,1269,1270],{},"但这些都是后话。眼下重要的是让核心引擎的三个决策——并发上限、失败隔离、实时可观察——都落地并验证。",[1272,1273,1274],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":87,"searchDepth":109,"depth":109,"links":1276},[1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291],{"id":21,"depth":109,"text":21},{"id":49,"depth":109,"text":49},{"id":73,"depth":109,"text":73},{"id":290,"depth":109,"text":290},{"id":354,"depth":109,"text":354},{"id":495,"depth":109,"text":496},{"id":558,"depth":109,"text":558},{"id":591,"depth":109,"text":591},{"id":607,"depth":109,"text":607},{"id":642,"depth":109,"text":642},{"id":697,"depth":109,"text":697},{"id":1188,"depth":109,"text":1189},{"id":1208,"depth":109,"text":1208},{"id":1230,"depth":109,"text":1230},{"id":1252,"depth":109,"text":1252},"Agent 平台","2026-06-21","单 Agent 处理复杂任务时，上下文上限和职责混乱很难避免。当任务规模扩大、需要多个专职代理时，简单的顺序派发不够用——需要一个真正的执行引擎，而不是一串 await 调用。Ultracode 的重写把这个执行引擎从写死的轮次评委模式升级为通用、分阶段、可观察的编排系统。","md",null,{},"\u002F2026-06-21-ultracodeagent",{"title":5,"description":1294},"2026-06-21-Ultracode多Agent编排引擎重写","从顺序调用升级为分阶段扇出的通用执行引擎，用 AI 规划编排拓扑、并发控制防爆、失败隔离传播。",[1303,1304,1305,1306,1307],"多代理编排","Agent 引擎","任务并发","实时可视化","Claude","AV6bLD0-HzCYqijQdQ1dyCaZGHEeRLnBt7idYgeJaDk",[1310,1630,1928],{"id":1311,"title":1312,"body":1313,"column":1292,"date":1616,"description":1317,"extension":1295,"hero_image":1296,"meta":1617,"navigation":134,"path":1618,"seo":1619,"series_id":1296,"severity":1296,"stem":1620,"summary":1621,"tags":1622,"__hash__":1629},"posts\u002F2026-07-26-峰值1987一个人维护AI平台的边界.md","一个人，能维护到多大规模？",{"type":7,"value":1314,"toc":1601},[1315,1318,1321,1324,1327,1359,1362,1365,1370,1373,1380,1395,1398,1405,1409,1412,1415,1418,1425,1432,1436,1439,1446,1450,1453,1456,1466,1477,1480,1484,1487,1490,1497,1501,1504,1507,1510,1514,1517,1520,1546,1549,1552,1555,1561,1575,1581,1595],[10,1316,1317],{},"从 2026-05-22 的 270 个用户到 07 下旬的峰值 1987 日活，这个平台的增长不是线性的。中间隔着四次明确的容量撞墙，每一次都留下了可复查的根因记录。这篇文章讲的是，什么条件下一个人能维护一个到达四位数日活规模的多租户平台。",[19,1319,1320],{"id":1320},"成长曲线与撞墙的四个节点",[10,1322,1323],{},"架构的设计前提是\"1 用户 = 1 容器\"。这个决策确定了，用户数与容器数就是同一个口径。没有\"日活 X 万、同时在线 Y 万\"这种双指标的混淆。",[10,1325,1326],{},"实际的规模序列是这样的：",[508,1328,1329,1335,1341,1347,1353],{},[302,1330,1331,1334],{},[29,1332,1333],{},"270 个 service","（05-22）：刚完成 ffmpeg 升级后的测量",[302,1336,1337,1340],{},[29,1338,1339],{},"372 RUNNING","（05-24）：滚动升级前的容器计数",[302,1342,1343,1346],{},[29,1344,1345],{},"379","（05-25）：五个 P0 patch 部署后稳定",[302,1348,1349,1352],{},[29,1350,1351],{},"574 用户","（06-03）：迁移到 K8s 时的基线，539 个 RUNNING",[302,1354,1355,1358],{},[29,1356,1357],{},"1987","（07 下旬）：最近测得的峰值",[10,1360,1361],{},"跨度是两个月，增速从 Swarm 期间的两周内 270→379（40% 增长）、到迁 K8s 后约八周 574→1987（约 3.5 倍）。这个加速度不是\"计划好的伸缩\"，而是每一次解决了瓶颈后，下一个瓶颈暴露出来。",[19,1363,1364],{"id":1364},"四堵撞过的墙",[1366,1367,1369],"h3",{"id":1368},"第一堵容器网络-ip-池05-22fa-012","第一堵：容器网络 IP 池（05-22，FA-012）",[10,1371,1372],{},"用户报告\"AI 容器里没 ffmpeg\"。这本来是个 Dockerfile 一行 apt 的事。但打算上线这个修改时，意外发现 gwbridge（Docker Swarm 默认的容器网络）IP 地址段是 \u002F24，总共 253 个 IP，已经被 270 个用户的容器占满。13 个用户容器长期处于启动失败的循环中。",[10,1374,1375,1376,1379],{},"排查逻辑是这样的：一行 Dockerfile 改动不至于触发灰度风险，但灰度一个用户时，Swarm 需要给新容器分配 gwbridge 的 IP。如果池子满了，IP 分配失败，新容器启动失败，再加上老容器还没完全释放（endpoint 还在占位），就陷入了\"申请 → 失败 → retry\"的循环。这种症状看起来随机，用户看到的是\"我的容器启不来\"，系统看到的是\"又一个容器 cycling\"。直到看 ",[14,1377,1378],{},"docker info"," 的输出，才发现 gwbridge 已经 100% 满用。",[10,1381,1382,1383,1386,1387,1394],{},"根本原因在于一个不易察觉的配置陷阱：Docker Daemon 的配置文件里改了 ",[14,1384,1385],{},"default-address-pools","，但这个配置",[29,1388,1389,1390,1393],{},"只在 ",[14,1391,1392],{},"swarm init"," 那一刻消费一次","。已经创建的网络不会动。之前改过这个配置的人可能不知道这个行为，改了等于没改。",[10,1396,1397],{},"修复不能简单地改配置重启。我试过在 staging 环境用 dry-run 验证，写脚本探测不冲突的子网段，然后在 production 的 canary 验证阶段发现\"释放的 IP 立刻被其它 cycling 任务抢走\"这样的负反馈。所以流程变成：先 scale 0 所有失联的服务（停止它们 cycling，释放的位置不会被抢），然后手动删除旧的 gwbridge、用新 subnet 重建。最终把容量从 253 扩到了 4094，增长了 16 倍。13 个失联用户全部恢复。",[10,1399,1400,1401,1404],{},"这次的启示是",[29,1402,1403],{},"配置陷阱往往比代码 bug 更隐蔽","。因为配置改了看不出效果，维护者会觉得没改上去、会反复尝试，但每次尝试的假设都错了。",[1366,1406,1408],{"id":1407},"第二堵单容器资源限制与内核参数05-25fa-013","第二堵：单容器资源限制与内核参数（05-25，FA-013）",[10,1410,1411],{},"五天后，部署了五个 P0 patch：B1 容器内存 burst factor 调整（硬限制改为内存 × 4）、B2 mcp register 的假失败救活逻辑、B3 ulimit nofile 扩大到 65536、B4 mcp cooldown 清理、B6 provision 接口幂等性。这一次的滚动升级从 05-25 凌晨 0:41 一直跑到 07:34，实际耗时 6 小时 42 分钟，原本预估只要 3 小时。",[10,1413,1414],{},"但更关键的数据是这个：升级前，平台里有一个重灾户容器一天被 OOM kill 了 247 次。这不是偶发故障——是每一天都在重复。升级完成一小时后，这个数字变成了 0。",[10,1416,1417],{},"这次的根因是九个缺陷的叠加。B1 的根因是硬限制设得太低（原来 93 个用户只有 500MB 硬限，远低于实际需要），B2 是 mcp register exit code 的错误判断（非零 exit 自动当做失败，但有时是因为配置覆盖了），B3 是文件描述符不足导致新连接打开失败。单个缺陷可能不致命，但聚在一起就是 OOM 风暴。更严重的是，一个用户的 OOM 不只影响那个用户——每次 OOM 都会触发内核的 swap 操作，拖累整机的 I\u002FO，让 cpa-api 主进程的事件循环卡顿 18 秒。前端看到的是全站变慢，实际根因可能是某个用户容器在反复 OOM。",[10,1419,1420,1421,1424],{},"部署前做了充分的 staging 验证和 production canary，分阶段升级了重灾户、然后全量升级、最后等完全收敛。期间遇到的问题是单容器的 ",[14,1422,1423],{},"docker service update"," 实际耗时约 60 秒（含 Swarm scheduler 延迟），并发调度起来比预期慢 2 倍。",[10,1426,1427,1428,1431],{},"这次的启示是：",[29,1429,1430],{},"当多个独立缺陷同时叠加时，表象看起来是单一故障（OOM 风暴），但根因分散在配置、参数、逻辑判断的不同层","。修复必须从全景取证开始，先理清每个环节的缺陷，再按优先级有序修复。一次部署才能彻底收敛，局部修复反而会留下隐患。",[1366,1433,1435],{"id":1434},"第三堵编排层与自愈能力06-03迁-k8s","第三堵：编排层与自愈能力（06-03，迁 K8s）",[10,1437,1438],{},"到 06-03，用户已经涨到 574 个。继续打补丁的成本已经高于\"一次性迁移编排平台\"。Swarm 的问题不只是容量——还有调度自愈的缺失。任何故障都依赖人工介入，一个人无法 24 小时在线。决策很简单：从 Docker Swarm 迁到 Kubernetes。",[10,1440,1441,1442,1445],{},"这次迁移的复杂性不在于技术实现本身（用 COS 中转数据、转换 sub2api 密钥、把 124G 数据分批导入），而在于",[29,1443,1444],{},"理解新平台引入的新故障域","。K8s 有更细的控制粒度和自动调度，但同时也暴露了之前 Swarm 隐藏的问题。比如共享存储（NFS\u002FSFS）的客户端卡死，在 Swarm 时期可能因为容器分散在不同节点而被掩盖；但在 K8s 这样的细粒度编排下，如果一个节点的 NFS 客户端出问题，节点上的所有 pod 都会受影响。所以迁移不是终点，而是暴露新问题的起点。",[1366,1447,1449],{"id":1448},"第四堵节点级存储与可观测性06-05fa-015","第四堵：节点级存储与可观测性（06-05，FA-015）",[10,1451,1452],{},"迁移两天后，某个节点上的 NFS 客户端在某个时刻 hang 住了。Kubernetes 不知道发生了什么，kubelet 仍然报告节点 Ready（因为 kubelet 本身没卡），但这个节点上 52 个实例的网关全部 DOWN 或 HUNG。这 52 个实例对应的是本该分散部署的用户容器，因为某种原因堆在了同一个节点上。",[10,1454,1455],{},"故障的症状可以分成几类。单个实例网关的 DOWN（容器无法启动、schema 非法）、HUNG（反复重启导致 adapter 504）、节点级的卡死（整节点上的容器创建\u002F删除都阻塞）、数据库与实际状态割裂（DB 里是 ERROR、但 pod 健康了）。每种症状对应不同的根因，需要不同的检测和自愈手段。",[10,1457,1458,1459,1462,1463,503],{},"最严重的是，",[29,1460,1461],{},"这个故障没有任何告警","。平台的监控指标全部正常，绿灯一片。直到用户反馈\"连不上面板\"，才被发现。这已经是故障发生几小时以后的事。故障本身是可逆的——节点冻死就人工 cordon、删除卡住的 pod、让其它节点重新调度。但",[29,1464,1465],{},"不可见的故障比故障本身更致命",[10,1467,1468,1469,1472,1473,1476],{},"这次事故直接导出了四层兜底的设计：L0 从配置和资源限制层面降低故障触发（比如 NFS 改 ",[14,1470,1471],{},"soft"," 参数而不是 ",[14,1474,1475],{},"hard","，这样网络抖动时容器报错退出而不是整个节点冻）；L1 加检测让故障可见（每 60 秒检测一遍\"节点上是否有 pod 卡 ContainerCreating、网关探测失败率多少\"）；L2 对低风险故障自动自愈（网关单次 DOWN 就重建 pod、DB 对账失败就修复状态）；L3 对高风险动作告警优先（节点冻死先推送告警、等人工确认再 drain）。",[19,1478,1479],{"id":1479},"三件真正决定可行性的事",[1366,1481,1483],{"id":1482},"_1-文档即基础设施","1. 文档即基础设施",[10,1485,1486],{},"这个平台现在有 200+ 份设计文档与 80 份故障档案。这些不是为了\"好看\"存在的。",[10,1488,1489],{},"一个人无法对整个系统保持完整的心智模型。200+ 文档是唯一能让一个人记住系统全貌的方式。每当遇到新故障，能快速检索以前遇过的类似问题。每当需要做架构决策，能回溯当初为什么这样设计、放弃过哪些选项。",[10,1491,1492,1493,1496],{},"同时，",[29,1494,1495],{},"这些文档也是 AI 能有效介入的前提","。我可以把这些故障档案喂给模型，让它帮助排查新问题、验证修复方案、甚至生成监控规则。但前提是要把故障记录得清楚。空洞的\"修好了\"没有任何价值。",[1366,1498,1500],{"id":1499},"_2-故障必须被归档","2. 故障必须被归档",[10,1502,1503],{},"同一个症状反复出现，每次修复可能只治了表象的一个侧面，真正的收敛需要理解全部的根因。这只有在每一次都写清档案的情况下才可能。",[10,1505,1506],{},"如果没有归档制度，第二次遇到类似症状时，维护者根本不知道第一次修复做过什么、为什么还没有彻底解决。\"又来了\"和\"这个问题还有遗留\"的反应完全不同。前者是被动应对、逐次救火，后者是主动追踪、系统解决。",[10,1508,1509],{},"实际上，平台里有不少故障走过了四到五次的修复周期。每一次修复时，回看之前的档案，能快速理清\"这一层已经改过，那一层还没触及\"。这种\"有案可查、有据可循\"的状态，把修复从赌博变成了可重复的流程——每一次问题复发时，不是从零开始排查，而是从已知的检查点继续。",[1366,1511,1513],{"id":1512},"_3-自愈优先于告警告警优先于人工","3. 自愈优先于告警，告警优先于人工",[10,1515,1516],{},"在 FA-015 之前，平台大量依赖人工值守。任何问题都需要运维看到日志、理解现象、手动操作。一个人无法 24 小时在线。",[10,1518,1519],{},"FA-015 的教训直接导出了四层兜底的设计：",[508,1521,1522,1528,1534,1540],{},[302,1523,1524,1527],{},[29,1525,1526],{},"L0 预防","：从配置和资源限制的层面降低故障触发的概率。",[302,1529,1530,1533],{},[29,1531,1532],{},"L1 检测","：让不可见的故障变成可见——节点卡死、实例网关异常、数据库与实际状态割裂，全部要有独立的检测逻辑。",[302,1535,1536,1539],{},[29,1537,1538],{},"L2 自愈","：对于低风险的故障（单实例网关重启、DB 状态对账），直接自动修复。高风险的动作（节点 drain）先告警、等人工确认。",[302,1541,1542,1545],{},[29,1543,1544],{},"L3 告警","：自愈失败时、检测到新的异常时，推送给人。",[10,1547,1548],{},"这样的设计下，一个人维护平台的上限大幅抬高。不是因为个人能力变强了，而是系统能自动处理大多数故障，只把人类的决策能力用在最关键的地方。",[19,1550,1551],{"id":1551},"诚实的边界在哪",[10,1553,1554],{},"但这个设计也有明显的天花板：",[10,1556,1557,1560],{},[29,1558,1559],{},"真正撑不住的","（需要六小时以上连续操作、需要跨时区响应、需要多人交叉验证）：",[508,1562,1563,1566,1569,1572],{},[302,1564,1565],{},"数据库或存储层的重大故障。恢复涉及数据一致性检查，无法完全自动化。",[302,1567,1568],{},"涉及业务逻辑的错误。修复需要理解用户意图，不只是系统恢复。",[302,1570,1571],{},"密钥泄露或安全事件。需要立刻通知客户、协调应急处置、事后全面审计。",[302,1573,1574],{},"多个独立故障同时发生、相互放大的情况。需要多个人在不同维度分别操作。",[10,1576,1577,1580],{},[29,1578,1579],{},"如果重来一次，优先级这样排","：",[299,1582,1583,1586,1589,1592],{},[302,1584,1585],{},"最先做的是 L1 检测——让故障可见。这是一切自动化的前提。宁可产生虚报，也不能漏掉真实故障。",[302,1587,1588],{},"其次是 L0 预防——从配置、资源限制、网络参数这些基础设施层降低故障率。这些改动成本低、收益高。",[302,1590,1591],{},"然后才是 L2 自愈——只对低风险的故障做自动恢复。对于高风险操作，即使多花一个人工确认的时间，也要确保不会进一步破坏系统。",[302,1593,1594],{},"最后是文档和监控。这些不是\"最后的事情\"，而是贯穿全过程的——每个改动都要同步更新文档、每个故障都要写进档案。",[10,1596,1597,1598,1600],{},"那些回避的成本很高。曾经因为 NFS 挂载的 ",[14,1599,1475],{}," 参数导致节点冻死，这个参数的改动只需要改一行配置文件、然后滚动重启一次实例。但因为这个调整一直没做，就承受了几小时的无声故障。反过来说，那些看起来\"小\"的改动——改配置参数、改资源限制、加一个监控规则——才是最划算的投资。",{"title":87,"searchDepth":109,"depth":109,"links":1602},[1603,1604,1610,1615],{"id":1320,"depth":109,"text":1320},{"id":1364,"depth":109,"text":1364,"children":1605},[1606,1607,1608,1609],{"id":1368,"depth":125,"text":1369},{"id":1407,"depth":125,"text":1408},{"id":1434,"depth":125,"text":1435},{"id":1448,"depth":125,"text":1449},{"id":1479,"depth":109,"text":1479,"children":1611},[1612,1613,1614],{"id":1482,"depth":125,"text":1483},{"id":1499,"depth":125,"text":1500},{"id":1512,"depth":125,"text":1513},{"id":1551,"depth":109,"text":1551},"2026-07-26",{},"\u002F2026-07-26-1987ai",{"title":1312,"description":1317},"2026-07-26-峰值1987一个人维护AI平台的边界","从 270 到 1987 日活，每一次规模跃升前都先撞了一次墙。这条增长曲线记录的不是预设设计，而是每次故障都被完整归档后逐步演进出来的可行性边界。",[1623,1624,1625,1626,1627,1628],"Docker Swarm","Kubernetes","容器编排","运维自动化","故障自愈","规模扩展","D-mYcaOLOBT0PhwjmrqVVEwCE7hl8Tqi9I57HrRlt5U",{"id":1631,"title":1632,"body":1633,"column":1292,"date":1914,"description":1915,"extension":1295,"hero_image":1296,"meta":1916,"navigation":134,"path":1917,"seo":1918,"series_id":1296,"severity":1296,"stem":1919,"summary":1920,"tags":1921,"__hash__":1927},"posts\u002F2026-07-14-分销体系的账本设计.md","一笔充值，要拆成几条流水？",{"type":7,"value":1634,"toc":1904},[1635,1642,1645,1649,1652,1655,1662,1673,1680,1684,1687,1693,1696,1699,1702,1713,1716,1724,1731,1735,1738,1749,1752,1763,1770,1774,1777,1783,1786,1797,1800,1804,1807,1810,1813,1824,1827,1835,1838,1849,1852,1855,1860,1867,1872,1883,1886,1889,1892,1898,1901],[10,1636,1637,1638,1641],{},"单笔用户充值，背后是一次复杂的资金拆分：用户充值 100 元，既是平台的收入，也是分销代理的佣金来源，可能还有上级代理的层级提成。这些数字必须同时记录、互相平衡、永不重复。这不是数据流通的问题，是",[29,1639,1640],{},"现金流的问题","——差一分钱就是漏账，重复一次就是挪用。",[10,1643,1644],{},"分销账本设计的核心就四条铁律和一个恒等式。遵循它们，系统能撑到任何规模；跳过其中任何一条，早晚会在对账时翻车。",[19,1646,1648],{"id":1647},"规则一佣金计算基数要先定死","规则一：佣金计算基数要先定死",[10,1650,1651],{},"从什么数字出发算佣金？这个问题比看起来复杂。",[10,1653,1654],{},"通常的选项有三个：订单总金额、用户实付金额、或者订单到账净额。乍看没区别，一旦遇上退款就完全不同。",[10,1656,1657,1658,1661],{},"采用的方案是",[29,1659,1660],{},"用户充值净额","（billing 系统中已确认到账的实付分）。理由很直白：",[299,1663,1664,1667,1670],{},[302,1665,1666],{},"退款处理天然免疫。用户充值后退款，billing 的该用户账户余额已经扣掉，充值净额自动反映了这笔冲销。分成计算只需聚合这个净额乘以比例，不用单独写退款冲正逻辑。",[302,1668,1669],{},"避免应收账款。如果以订单金额算，还没到账时代理已经看得到分成，这在 reporting-only 设计下容易造成认知错位（代理以为钱已经是他的，实际还在支付处理中）。",[302,1671,1672],{},"同源唯一。billing 是平台的权威账本，分成的基数来自这里，对账时只需验证\"代理分成之和 + 平台收入 = billing 总充值\"，一个公式搞定。",[10,1674,1675,1676,1679],{},"反过来说，如果公司后续引入退款主动冲补（而非被动扣减），这个基数设定会变得复杂。但在初期，",[29,1677,1678],{},"基数 = billing 已确认充值"," 是最简洁的切口。",[19,1681,1683],{"id":1682},"规则二结算时点决定了数据流向","规则二：结算时点决定了数据流向",[10,1685,1686],{},"到底是在订单成交时计提佣金，还是账期结束时一次性结算？",[10,1688,1689,1690,503],{},"这里的选择是 ",[29,1691,1692],{},"reporting-only 只读聚合，实时查询，不计提、不累积",[10,1694,1695],{},"具体含义是：代理看到的\"我的分成\"不是一条条流水记录，而是每次查询时现场计算出来的聚合数字。算法是\"我名下所有用户的充值净额总和 × 我的佣金比例\"。没有单独的\"分成计提\"操作，没有一条条的\"分成到账\"记录。",[10,1697,1698],{},"好处和代价是对偶的。",[10,1700,1701],{},"好处：",[508,1703,1704,1707,1710],{},[302,1705,1706],{},"免除计提时点的争议。不用决定在订单成交时、支付完成时、还是 T+1 时计提，因为根本不计提。",[302,1708,1709],{},"天然避免双扣。既然分成不落库不累积，就不存在\"发放一次、又重复发放一次\"的并发风险。同一笔充值无论被查询多少次，贡献的佣金永远相同。",[302,1711,1712],{},"简化对账。代理的分成数字永远等于\"最新充值净额 × 比例\"，无需追溯历史。",[10,1714,1715],{},"代价：",[508,1717,1718,1721],{},[302,1719,1720],{},"代理无法看到\"分成流水\"。有些运营场景下，需要展示\"哪笔订单产生了多少佣金\"这样的明细，reporting-only 做不了（可以通过关联用户的充值明细变通，但那是用户维度的流水，不是分成维度的）。",[302,1722,1723],{},"退款时必须同步。如果用户退了 50 块钱，billing 系统立刻反映这笔扣减，代理的分成下一秒查询就会跌下来。这对代理来说是透明的（分成就是动态的），但运营沟通时需要提前说清楚。",[10,1725,1726,1727,1730],{},"选择 reporting-only 的核心原因是：",[29,1728,1729],{},"初期不出金、无提现","。既然分成只是一个数字展示、不涉及真金白银的打款，那就不用建立复杂的流水账体系。等到未来做提现时，可以在 reporting-only 的基础上加一层\"快照 + 冻结\"机制（即每个提现周期开始时拍一个快照，这个快照才是可提的分成额）。",[19,1732,1734],{"id":1733},"规则三层级上限和循环检测","规则三：层级上限和循环检测",[10,1736,1737],{},"分销是分多少层级？",[10,1739,1740,1741,1744,1745,1748],{},"首期方案是",[29,1742,1743],{},"单层","。用户通过一个特定的渠道码注册（如 ",[14,1746,1747],{},"AB-48210377","），永久绑定到某个代理。代理无法有\"上级代理\"，也就无法有\"上级佣金\"这样的递归结构。",[10,1750,1751],{},"这一约束看起来很强，但在无提现的 reporting-only 下，是合理的。理由是：",[299,1753,1754,1757,1760],{},[302,1755,1756],{},"简化代理运维。总台只需管理一套代理的佣金比例（per-代理），不用维护代理之间的树形关系。",[302,1758,1759],{},"避免环形链。单层天然杜绝了\"A 的上级是 B，B 的上级是 A\"这类配置错误。多层结构下，环形检测本身又是一个故障点。",[302,1761,1762],{},"初期够用。大多数分销场景早期就是\"直销商（代理）→ 用户\"的二元关系，不需要分级。",[10,1764,1765,1766,1769],{},"但要注意，这个约束是",[29,1767,1768],{},"数据模型层的","，不是业务规则层的。如果未来需要升级到多层结构，数据模型需要改（Channel 表可能要加 parentResellerId 等），但已经发出去的单层记录无需回溯改造——它们天然是单层的。",[19,1771,1773],{"id":1772},"规则四幂等键设计防重复计提","规则四：幂等键设计（防重复计提）",[10,1775,1776],{},"同一笔充值可能被多个系统调用、被回调多次。分成必须严格幂等：无论这笔充值被聚合几次，贡献给代理的佣金永远是\"充值额 × 比例\"这一个数字，不能是两倍、三倍。",[10,1778,1779,1780,503],{},"因为采用 reporting-only + 只读聚合的设计，幂等性",[29,1781,1782],{},"自动满足",[10,1784,1785],{},"推理如下：",[299,1787,1788,1791,1794],{},[302,1789,1790],{},"billing 侧的充值流水本身是幂等的。同一个订单号的充值，billing 确保只入账一次（通过订单号的唯一性约束）。",[302,1792,1793],{},"分成聚合是无状态的。每次查询时，服务端都是\"遍历该代理名下的用户 → 调用 billing 的 summaryByUsers 接口 → 汇总充值净额 → 乘以比例\"。这个聚合过程不依赖任何之前的计提记录。",[302,1795,1796],{},"结论：即使 billing 错误地返回了同一笔充值两次，聚合结果也只会包含一次（因为底层是\"用户ID → 总充值净额\"的映射，不是\"订单 → 充值\"的流水列表）。",[10,1798,1799],{},"相反，如果设计成\"订单成交时计提一条分成记录\"的模式，就需要在分成记录上加幂等键（如 orderId），确保同一订单的分成只计提一次。这个幂等键检查本身就是一个额外的故障点。",[19,1801,1803],{"id":1802},"一个恒等式对账的唯一标准","一个恒等式：对账的唯一标准",[10,1805,1806],{},"前面四条规则规范了流程，但最后的验证还是要靠一个简单的数学公式。",[10,1808,1809],{},"$$\n\\sum_^{n} \\text{Commission}_i + \\text{PlatformNetIncome} = \\text{TotalTopup}\n$$",[10,1811,1812],{},"其中：",[508,1814,1815,1818,1821],{},[302,1816,1817],{},"$\\text{Commission}_i$ 是第 $i$ 个代理的分成（所有名下用户的充值净额 × 佣金比例）",[302,1819,1820],{},"$\\text{PlatformNetIncome}$ 是平台的净收入（总充值 - 所有代理的分成总额）",[302,1822,1823],{},"$\\text{TotalTopup}$ 是 billing 系统的总充值额（所有用户的到账充值之和）",[10,1825,1826],{},"这个等式是唯一可靠的对账标尺。任何时刻，只要这个等式不成立，就说明某个环节出了问题：",[508,1828,1829,1832],{},[302,1830,1831],{},"等式左侧大于右侧 → 某个代理的分成算重了，或者平台收入算多了",[302,1833,1834],{},"等式左侧小于右侧 → 某个代理的分成算少了，或者某笔收入漏了",[10,1836,1837],{},"而且这个等式不需要建立任何新表。完全可以通过查询三个现成的数据源验证：",[299,1839,1840,1843,1846],{},[302,1841,1842],{},"billing 的 SummaryByUsers（每个用户的充值净额）",[302,1844,1845],{},"代理表的 commissionRate（每个代理的佣金比例）",[302,1847,1848],{},"一行 SQL 的聚合（sum 和乘法）",[19,1850,1851],{"id":1851},"技术实现的两个关键点",[10,1853,1854],{},"光有规则还不够，实现层要支撑这些规则。素材中的设计有两个细节值得指出。",[10,1856,1857,503],{},[29,1858,1859],{},"其一，billing 要暴露 summaryByUsers 接口",[10,1861,1862,1863,1866],{},"分成聚合依赖\"按用户ID汇总充值净额和消耗\"这个操作。如果 billing 侧没有这个批量接口，代理端就得自己拼接多个单用户查询，性能和一致性都会打折扣。实现计划里新增的 ",[14,1864,1865],{},"POST \u002Fanalytics\u002Fsummary-by-users"," 就是为了这个。",[10,1868,1869,503],{},[29,1870,1871],{},"其二，reseller 侧的所有查询要服务端注入 channelId",[10,1873,1874,1875,1878,1879,1882],{},"代理登录后调用 ",[14,1876,1877],{},"\u002Fapi\u002Freseller\u002Fsummary"," 或 ",[14,1880,1881],{},"\u002Fapi\u002Freseller\u002Fusers","，后端不能信任请求体里的 channelId 参数。而是从 token 解析出代理身份 → 查表得出该代理对应的 channelId → 强制注入到查询条件里。这是防代理 A 越权查看代理 B 渠道的唯一有效方式。",[10,1884,1885],{},"代码里体现为：从 Admin token 反查 Channel 表的 resellerId 字段，确保该 Admin 只能看自己那行 Channel。",[19,1887,1888],{"id":1888},"与既有分账设计的呼应",[10,1890,1891],{},"这套账本设计不是凭空造出来的。平台此前在另一个系统里实现过五类角色的分账体系（Platform、Agency、Escort、Distributor、Merchant），每个角色各维护一张 Ledger 流水表。那个设计的核心思想是\"分账入表\"——即每一笔影响各角色收入的交易，都要对应地在各自的 Ledger 表里落一条记录。",[10,1893,1894,1895,503],{},"当前这套分销设计采用了相反的思路：不建 Ledger 表，而是在查询时通过聚合来推导分成。这的背景是 reporting-only 属性（不出金、只展示），使得可以接受动态聚合的方案。但底层的思想是一致的——",[29,1896,1897],{},"通过对账恒等式来保证多角色之间的收支平衡",[19,1899,1900],{"id":1900},"结尾",[10,1902,1903],{},"账本设计的目标不是漂亮的表格或丰富的报表，而是一个简单的数学等式永远成立。一旦等式破裂，对账人员立刻能定位是哪个环节失守。这比事后扑火要高效得多。",{"title":87,"searchDepth":109,"depth":109,"links":1905},[1906,1907,1908,1909,1910,1911,1912,1913],{"id":1647,"depth":109,"text":1648},{"id":1682,"depth":109,"text":1683},{"id":1733,"depth":109,"text":1734},{"id":1772,"depth":109,"text":1773},{"id":1802,"depth":109,"text":1803},{"id":1851,"depth":109,"text":1851},{"id":1888,"depth":109,"text":1888},{"id":1900,"depth":109,"text":1900},"2026-07-14","单笔用户充值，背后是一次复杂的资金拆分：用户充值 100 元，既是平台的收入，也是分销代理的佣金来源，可能还有上级代理的层级提成。这些数字必须同时记录、互相平衡、永不重复。这不是数据流通的问题，是现金流的问题——差一分钱就是漏账，重复一次就是挪用。",{},"\u002F2026-07-14",{"title":1632,"description":1915},"2026-07-14-分销体系的账本设计","分销系统最容易在财务对账出错。单笔充值需同时产生平台收入、代理佣金等多条记录，必须在同一事务内闭合。四条规则与一个恒等式是账本设计的全部。",[1922,1923,1924,1925,1926],"分销","账本设计","对账","幂等性","佣金结算","Q3NC9Z8JBJ7e2MLQFH10q68Igpbp2fMF5gT-do6Rxaw",{"id":1929,"title":1930,"body":1931,"column":1292,"date":2241,"description":1935,"extension":1295,"hero_image":1296,"meta":2242,"navigation":134,"path":2243,"seo":2244,"series_id":1296,"severity":1296,"stem":2245,"summary":2246,"tags":2247,"__hash__":2253},"posts\u002F2026-06-29-记忆星系长期记忆可视化工作台.md","模型记错了，用户得能删掉",{"type":7,"value":1932,"toc":2231},[1933,1936,1939,1943,1949,1952,1955,1958,1961,1968,1971,1974,2006,2009,2012,2085,2092,2099,2102,2105,2149,2152,2155,2159,2162,2165,2168,2171,2174,2177,2180,2183,2186,2193,2200,2203,2206,2213,2216,2219,2222,2225,2228],[10,1934,1935],{},"长期记忆存起来容易，用户看不见也管不了。记忆一旦不可见，就会累积错误信息并持续污染后续对话——模型出错时没有纠正入口，错误就永久留存。",[10,1937,1938],{},"我在 yun-claude 的记忆设计中遇到的问题正是这个。聊天系统能自动从对话抽取持久要点并入库，但页面还是传统的卡片列表，看不出记忆的类型、重要性、使用频次。更严重的是，用户无法编辑或删除那些被错误标记的记忆。所以这次升级的核心不是加一个炫彩的可视化，而是把记忆管理变成一个真正可用的信息工作台。",[19,1940,1942],{"id":1941},"表格优先而不是星河优先","表格优先，而不是星河优先",[10,1944,1945,1946,503],{},"设计的第一个决策是：",[29,1947,1948],{},"主界面用表格承载记忆列表，不用星河画布作主体交互",[10,1950,1951],{},"这听起来反直觉。当初考虑过让星河画布成为核心——节点按重要性大小分布、按创建时间环形排列、搜索命中时高亮。视觉上是漂亮的，也更有\"知识宇宙沉淀\"的产品感。但约束改变了这个选择：",[10,1953,1954],{},"第一，信息密度。表格一屏可以显示 10-20 条记忆的核心属性（标题、类型、重要性、标签、创建时间），并支持排序和筛选。星河画布要展示相同的信息量，就必须让节点变小、缩放调整、甚至分屏展示——交互成本陡升。而用户——AI agent 的主人——需要快速浏览和定位记忆，不是浏览艺术装置。",[10,1956,1957],{},"第二，编辑成本。星河上的节点编辑通常要额外打开面板或弹窗。如果记忆管理的主要工作是\"检查是否有错记、删除重复、调整分类\"，那星河就不是最优方案。对比之下，表格 + 右侧详情面板的结构让用户可以同时看到列表和正在编辑的项目，没有上下文切换。",[10,1959,1960],{},"第三，用户规模。当前 yun-claude 没有真实用户，推断单个用户的记忆条数会比较少（几十到几百）。在这个规模下，表格就足够快了，不必依赖图形加速或向量索引的复杂优化。如果将来规模增大再考虑换方案。",[10,1962,1963,1964,1967],{},"所以最终设计是：",[29,1965,1966],{},"表格为主工作台，右侧详情面板负责编辑与整理，星系可视化只作为辅助视图（后续可选）","。这不是缺乏视觉想象力，而是对工作流的务实权衡。代价是产品感稍弱，但可用性强得多。",[19,1969,1970],{"id":1970},"五类语义记忆与设计令牌",[10,1972,1973],{},"为了让记忆可见可管，将长期记忆分成五类：",[508,1975,1976,1982,1988,1994,2000],{},[302,1977,1978,1981],{},[29,1979,1980],{},"核心记忆","（CORE）：与用户身份、核心项目、重要约束直接相关。模型在每轮对话发送前都应该检索。",[302,1983,1984,1987],{},[29,1985,1986],{},"常驻记忆","（PERMANENT）：用户的工作背景、技术栈偏好、团队结构等长期背景。",[302,1989,1990,1993],{},[29,1991,1992],{},"临时记忆","（TEMPORARY）：短期的任务进度、当前问题、临时约束。生命周期短。",[302,1995,1996,1999],{},[29,1997,1998],{},"知识星云","（KNOWLEDGE）：用户分享的文档要点、API 文档摘录、最佳实践。主要用于 RAG 增强。",[302,2001,2002,2005],{},[29,2003,2004],{},"其他","（OTHER）：模型无法明确分类或用户手动标记的记忆。",[10,2007,2008],{},"每一类的关键差异是生命周期和召回策略。核心记忆应该高频被注入 prompt，临时记忆应该自动清理，知识类应该被 RAG 系统共同使用。",[10,2010,2011],{},"为了在视觉上强化这个分类，为每类配置了一个独立的语义色：",[2013,2014,2015,2031],"table",{},[2016,2017,2018],"thead",{},[2019,2020,2021,2025,2028],"tr",{},[2022,2023,2024],"th",{},"类型",[2022,2026,2027],{},"颜色",[2022,2029,2030],{},"用途",[2032,2033,2034,2046,2056,2066,2076],"tbody",{},[2019,2035,2036,2040,2043],{},[2037,2038,2039],"td",{},"核心",[2037,2041,2042],{},"Amber-500",[2037,2044,2045],{},"节点、筛选按钮、卡片左边线",[2019,2047,2048,2051,2054],{},[2037,2049,2050],{},"常驻",[2037,2052,2053],{},"Sky-500",[2037,2055,2045],{},[2019,2057,2058,2061,2064],{},[2037,2059,2060],{},"临时",[2037,2062,2063],{},"Teal-500",[2037,2065,2045],{},[2019,2067,2068,2071,2074],{},[2037,2069,2070],{},"知识",[2037,2072,2073],{},"Violet-500",[2037,2075,2045],{},[2019,2077,2078,2080,2083],{},[2037,2079,2004],{},[2037,2081,2082],{},"Slate-500",[2037,2084,2045],{},[10,2086,2087,2088,2091],{},"关键的设计决策是：",[29,2089,2090],{},"颜色不只是装饰，而是结构的一部分","。在表格、筛选条、详情面板的边线上，用户都能看到同一个颜色，强化类型认知。这样的一致性是可信的信息工作台的标志——用户一眼知道哪条记忆是核心、哪条是临时。",[10,2093,2094,2095,2098],{},"颜色值不是散落在各个组件里的魔法数字，而是集中在一个 ",[14,2096,2097],{},"memoryStyles.ts"," 文件中管理。任何需要记忆类型颜色的地方都从这里引用。这样改一个颜色时不必跨多个文件搜索替换，也不会出现同类型在不同地方显示不同色的尴尬局面。",[19,2100,2101],{"id":2101},"记忆模型与后端约束",[10,2103,2104],{},"后端为每条记忆增加了结构化字段：",[508,2106,2107,2113,2119,2125,2131,2137,2143],{},[302,2108,2109,2112],{},[14,2110,2111],{},"title","：节点或列表行的标题，从正文前 18 个字符生成",[302,2114,2115,2118],{},[14,2116,2117],{},"type","：五类之一",[302,2120,2121,2124],{},[14,2122,2123],{},"importance","：1-100 的重要性分值，决定节点大小和列表排序",[302,2126,2127,2130],{},[14,2128,2129],{},"tags","：字符串数组，最多 8 个标签",[302,2132,2133,2136],{},[14,2134,2135],{},"lastUsedAt","：最近被召回的时间",[302,2138,2139,2142],{},[14,2140,2141],{},"usedCount","：被召回次数",[302,2144,2145,2148],{},[14,2146,2147],{},"metadata","：扩展字段，保存来源会话、模型、抽取动作等",[10,2150,2151],{},"当模型抽取记忆时，输出包含这些结构化字段。服务端必须做兜底和校验：type 非法时改为 OTHER、importance 超范围时裁剪、tags 去重去空白最多保留 8 个。如果抽取失败，仍然保存正文并用默认字段（importance=50, type=OTHER）。",[10,2153,2154],{},"这些默认值的设计是为了降级优雅。即便结构化抽取失败，记忆也不会丢失，只是分类不精准——这是可以接受的，因为用户随后可以手动调整。",[19,2156,2158],{"id":2157},"搜索筛选与编辑","搜索、筛选与编辑",[10,2160,2161],{},"用户在表格上方有一个搜索框和类型筛选条。搜索时调用后端的语义搜索接口，命中的记忆在表格中高亮，并自动选中最高相关性的一条，打开右侧详情面板。语义搜索基于向量数据库的余弦相似度匹配——记忆文本被嵌入为 4096 维向量，查询时也转换为向量并与库内存储按相似度排序召回，只返回分值达到阈值（≥0.3）的结果。这样的匹配比关键词搜索精准度高，也支持语义近似的记忆关联（比如\"TypeScript 后端\"和\"TS 服务端\"会被认为相关）。搜索失败时保留当前星河状态并 toast 提示，不中断工作流。",[10,2163,2164],{},"筛选可以按五类过滤，清除筛选则回到全量视图。如果当前选中的记忆被筛选隐藏了，系统会自动选中可见记忆中最高重要性的那一条；如果没有可见记忆，则关闭详情面板显示空状态。",[10,2166,2167],{},"详情面板里，用户可以编辑标题、正文、类型、标签和重要性。编辑表单使用产品内设计，不弹浏览器原生弹窗。保存后立即更新列表和节点（如果有星河视图的话）。这样用户修改一条记忆时，不必刷新页面或等待后台同步，改动立刻可见。",[10,2169,2170],{},"删除是另一个关键能力。必须让用户能删除被错误标记的记忆，否则错误就成了永久污染源。删除按钮放在详情面板的危险操作区，点击后需确认。删除成功后，该条记忆从表格和星河中消失，列表自动选中下一条（如果有的话）。",[19,2172,2173],{"id":2173},"星河与移动端降级",[10,2175,2176],{},"虽然主界面是表格，但在设计中保留了星河作为可视化补充。桌面端在表格下方或侧边可以显示一个小星图，让用户看到记忆的空间分布——核心记忆聚在中心，临时记忆分散在外围。星河上的节点与表格关联：点击表格行时，星河也高亮对应节点；点击星河节点时，表格定位到对应行。",[10,2178,2179],{},"但星河不是强制项。第一版的实现可能不包含完整星河，而是先确保表格工作台完整可用。星河可以作为后续的增强——用户如果觉得需要可视化辅助，才加上去。",[10,2181,2182],{},"移动端设计上，星河更是不现实（屏幕太小）。所以移动端完全降级为列表视图，点击列表项后通过底部抽屉显示详情和编辑表单。搜索和类型 tabs 放在顶部，保持核心交互可用。这样既避免了表格在手机上的横向溢出，也保证了可用性。",[19,2184,2185],{"id":2185},"节点布局与稳定性",[10,2187,2188,2189,2192],{},"如果星河要实现，一个重要的设计细节是：",[29,2190,2191],{},"节点位置必须稳定","。即便刷新页面或重新打开应用，同一批记忆应该保持相同的位置，这样用户才能形成空间记忆——\"核心记忆总在中心，临时的在右上角\"。",[10,2194,2195,2196,2199],{},"这意味着节点位置不能是随机的实时布局，也不能用物理模拟（那会每次都算不同的位置）。用 ",[14,2197,2198],{},"createdAt"," 字段参与布局计算，保证确定性：同一条记忆的创建时间固定了，它在环形排列中的角度也就固定了。结合 importance 决定距离中心的远近，位置就完全由数据驱动，刷新后毫厘不差。",[19,2201,2202],{"id":2202},"一致性与可维护性",[10,2204,2205],{},"这个设计的关键约束是一致性。如果记忆在表格中是 Amber-500（核心），那在星河、筛选、详情面板的边线上也必须是 Amber-500。任何拆散这个一致性的修改都会破坏用户的心智模型。",[10,2207,2208,2209,2212],{},"所以在设计系统里明确了这一点：",[29,2210,2211],{},"新增或调整记忆类型视觉时，先更新设计文档里的语义色板和 memoryStyles.ts，再在各组件中引用，不允许组件各自复制色值","。这样的约束看似严苛，但它保证了长期的可维护性——下次要改颜色时，只需改一个文件。",[19,2214,2215],{"id":2215},"代价与权衡",[10,2217,2218],{},"这个设计的代价是什么？",[10,2220,2221],{},"第一，视觉冲击力比不上星河优先。表格是务实的设计，不够\"黑科技\"感。如果产品定位是\"AI 记忆系统\"要卖视觉冲击，这个方案会显得保守。",[10,2223,2224],{},"第二，星河的潜力没有完全释放。放弃了复杂的关系推理、自由漫游、力导向布局这些\"高级\"可视化特性，原因就是表格工作台不需要它们，而加上去反而添加复杂度。",[10,2226,2227],{},"第三，设计系统的维护成本提高了。一致性的要求意味着每次改动都要考虑全局影响。但这其实是长期收益——减少了 bug 和不一致的可能。",[10,2229,2230],{},"这些代价对 yun-claude 是可以接受的，因为当前用户还不多，重点是让产品可用，而不是炫技。如果将来用户规模上升，数百条甚至千条记忆的管理场景出现，表格 + 星河的混合界面可能不够，那时再考虑更激进的可视化方案。现在，信息工作台的优先级明确高于视觉体验。",{"title":87,"searchDepth":109,"depth":109,"links":2232},[2233,2234,2235,2236,2237,2238,2239,2240],{"id":1941,"depth":109,"text":1942},{"id":1970,"depth":109,"text":1970},{"id":2101,"depth":109,"text":2101},{"id":2157,"depth":109,"text":2158},{"id":2173,"depth":109,"text":2173},{"id":2185,"depth":109,"text":2185},{"id":2202,"depth":109,"text":2202},{"id":2215,"depth":109,"text":2215},"2026-06-29",{},"\u002F2026-06-29",{"title":1930,"description":1935},"2026-06-29-记忆星系长期记忆可视化工作台","表格优先的记忆管理：用高信息密度工作台承载持久要点，可视化只作辅助，设计令牌贯穿全局。",[2248,2249,2250,2251,2252],"长期记忆","信息工作台","设计决策","语义搜索","设计令牌","gsJKxlNQ-FiADsw2ca6e5WJo-_uyYDFZ55iuiMr4jQE",1785406912236]