[{"data":1,"prerenderedAt":1461},["ShallowReactive",2],{"\u002F2026-06-27":3,"\u002F2026-06-27-rel":515},{"id":4,"title":5,"body":6,"column":498,"date":499,"description":12,"extension":500,"hero_image":501,"meta":502,"navigation":503,"path":504,"seo":505,"series_id":501,"severity":501,"stem":506,"summary":507,"tags":508,"__hash__":514},"posts\u002F2026-06-27-多桶钱包与资源计价.md","「赠送额度不能提现」——这句话得写进数据结构",{"type":7,"value":8,"toc":489},"minimark",[9,13,17,29,35,38,58,61,68,78,85,112,119,126,141,144,151,165,204,215,220,252,257,290,296,299,306,320,331,334,337,340,354,369,372,378,403,414,452,461,464,471,482,485],[10,11,12],"p",{},"平台上要计费的资源有完全不同的\"形状\"：文本对话按 token、网络搜索按次、OCR 按页数、视频按时长。用一个\"积分余额\"和一套计费逻辑没法准确体现这些成本，更没法承载营销活动。这篇讲两项基础改造：把钱包从单一永久池重做为多个桶（带有效期和可用范围），以及把计价从隐式约定改成后台可配的资源价表。",[14,15,16],"h2",{"id":16},"单一余额的局限性",[10,18,19,20,24,25,28],{},"现有账户模型是这样的：每个用户一条 ",[21,22,23],"code",{},"Account"," 记录，字段就一个 ",[21,26,27],{},"Balance","。所有充值、赠送、活动都加到这个数字上。问题在于这个模型无法表达——也无法强制——不同金钱的用途限制和有效期。",[10,30,31,32,34],{},"比如月卡产品要求：每月发放 1000 点，有效期 30 天，到期清零。如果这 1000 点也落在 ",[21,33,27],{}," 里，那么\"到期清零\"这个约束就只能靠业务代码到处判断：扣费时检查来源是否过期、统计报表时提醒用户余额即将失效。一旦业务代码有一处漏判或遗忘，就会有用户在余额本该清零的那一刻发现账户还能扣费，或者收不到过期提醒。",[10,36,37],{},"类似的规则还有：赠送额度不能提现（充值时不计入），活动额度只能用于特定功能（订阅用户才能用），订阅档位要按周期重置。这些规则堆到业务代码里，复杂度爆炸。",[10,39,40,41,45,46,49,50,53,54,57],{},"真正的解决方案是把规则",[42,43,44],"strong",{},"落在数据结构","上：改用\"多桶钱包\"。每一笔发放都是一个桶，带自己的 ",[21,47,48],{},"Remaining","（剩余点数）和 ",[21,51,52],{},"ExpiresAt","（过期时间），以及预留的 ",[21,55,56],{},"AllowedModels","（可用模型白名单，先不启用）。余额变成了多个桶的求和，扣费时按优先级顺序消耗——最早过期的先扣，确保快要失效的点不会被浪费。",[14,59,60],{"id":60},"多桶钱包的设计",[10,62,63,64,67],{},"核心数据结构是一张 ",[21,65,66],{},"PointBucket"," 表：",[69,70,75],"pre",{"className":71,"code":73,"language":74},[72],"language-text","ID          | UserID | Remaining | ExpiresAt      | Source       | AllowedModels\n-----------|--------|-----------|----------------|--------------|---------------\n1          | user-a | 500       | NULL           | topup        | (空，不限)\n2          | user-a | 100       | 2026-07-27     | membership   | (空)\n3          | user-a | 50        | NULL           | redeem       | (空)\n","text",[21,76,73],{"__ignoreMap":77},"",[10,79,80,81,84],{},"User-a 的总余额就是 ",[21,82,83],{},"500 + 100 + 50 = 650"," 点。当他发起一个消耗 200 点的回合时：",[86,87,88,99,102,105],"ol",{},[89,90,91,92,95,96,98],"li",{},"锁定该用户存活的桶（",[21,93,94],{},"ExpiresAt IS NULL OR ExpiresAt > now()","），按 ",[21,97,52],{}," 升序排列",[89,100,101],{},"第 2 行那个 100 点（7 天后过期）会先被消耗，变成 0；剩余还需 100 点",[89,103,104],{},"第 1 行那个 500 点（永久）消耗 100，变成 400",[89,106,107,108,111],{},"记录这次扣费从哪些桶各扣了多少，存在 ",[21,109,110],{},"UsageRecord.Allocations"," 里",[10,113,114,115,118],{},"为什么要记录分配？因为结算时可能多退：模型只发出了 150 个 token，实际扣费少于预期 200 点。退费时要按",[42,116,117],{},"逆序","原路退回——最后扣的（永久的）先退，保证临时点不会被复活。而且如果某个原桶已经过期了，那笔退款就跳过（点本就该没了，不用还）。",[10,120,121,122,125],{},"每个桶本身没有\"花费\"的概念，只有\"剩余\"。所有的花费都是透过 ",[21,123,124],{},"UsageRecord"," 记录的。这样的好处是：",[127,128,129,132,135,138],"ul",{},[89,130,131],{},"多退少补的逻辑精确而无歧义",[89,133,134],{},"每笔支出都能精确审计追溯到来源桶",[89,136,137],{},"临时点和永久点永远不串味",[89,139,140],{},"防止了\"用完临时点还能从永久点倒流\"这类漏洞",[14,142,143],{"id":143},"发放与消费的原语",[10,145,146,147,150],{},"平台所有入账路径（充值、兑换、活动赠送、订阅月度发放）都调同一个 ",[21,148,149],{},"GrantPoints"," 函数：",[69,152,156],{"className":153,"code":154,"language":155,"meta":77,"style":77},"language-go shiki shiki-themes github-light github-dark","GrantPoints(tx, userID, amount, expiresAt, source)\n","go",[21,157,158],{"__ignoreMap":77},[159,160,163],"span",{"class":161,"line":162},"line",1,[159,164,154],{},[127,166,167,177,183],{},[89,168,169,172,173,176],{},[21,170,171],{},"amount > 0"," 才会建桶；",[21,174,175],{},"amount \u003C= 0"," 无效",[89,178,179,182],{},[21,180,181],{},"expiresAt = nil"," 表示永久（所有现有充值、赠送都是永久）",[89,184,185,188,189,192,193,192,196,199,200,203],{},[21,186,187],{},"source"," 标记来源：",[21,190,191],{},"topup","、",[21,194,195],{},"redeem",[21,197,198],{},"adjust","（管理员调整）、",[21,201,202],{},"membership","（订阅）",[10,205,206,207,210,211,214],{},"消费侧是 ",[21,208,209],{},"Reserve"," 和 ",[21,212,213],{},"Settle"," 的一对原语：",[10,216,217,219],{},[42,218,209],{}," 是预扣。给定一个操作 ID、用户、估计消费点数，它会：",[86,221,222,229,232,246],{},[89,223,224,225,228],{},"查存活桶、按最早过期先排序、加行级锁（",[21,226,227],{},"FOR UPDATE","）",[89,230,231],{},"贪心从前往后扣，直到凑满估计值或余额不足",[89,233,234,235,237,238,241,242,245],{},"写 ",[21,236,124],{}," 记录 ",[21,239,240],{},"status=reserved","，包含 ",[21,243,244],{},"Allocations"," 分配",[89,247,248,249],{},"余额不足则报错，",[42,250,251],{},"整个事务回滚、不写记录、不建空桶",[10,253,254,256],{},[42,255,213],{}," 是结算。给定操作 ID 和真实消费，它会：",[86,258,259,262,268,278,284],{},[89,260,261],{},"幂等检查：如果记录已经 settled 或 refunded，直接返回",[89,263,264,265],{},"计算 ",[21,266,267],{},"delta = actual - reserved",[89,269,270,271,274,275,277],{},"如果 ",[21,272,273],{},"delta \u003C 0","（多退），按 ",[21,276,244],{}," 逆序精确退回原桶",[89,279,270,280,283],{},[21,281,282],{},"delta > 0","（少补），尽力从存活桶再扣；不足仍结算（不卡死）",[89,285,286,287],{},"标记记录为 ",[21,288,289],{},"settled",[10,291,292,293,295],{},"为什么 settle 时\"少补不足仍结算\"？因为 reserve 已经用保守估计（input + maxOutputTokens）去预扣了，",[21,294,282],{}," 的缺口通常微小。而且结算不能卡死——已经发生的消费是既成事实，不因为后续余额不足而回滚。",[14,297,298],{"id":298},"并发与幂等的保证",[10,300,301,302,305],{},"同一用户的并发请求怎么防止双扣？方案很简单：",[42,303,304],{},"事务内 FOR UPDATE 锁定存活桶","。Postgres 会按行级锁串行化对同一用户的并发扣费。",[10,307,308,309,312,313,315,316,319],{},"幂等性靠 ",[21,310,311],{},"OperationID","（对话回合 ID）作主键。同一操作重试 reserve，会查到已存的 ",[21,314,124],{},"，直接返回其 ",[21,317,318],{},"ReservedPoints","，不重复扣。Settle 同理——已结的记录不再改。",[10,321,322,323,326,327,330],{},"过期的预留怎么处理？有一个后台对账任务，每日扫一遍超过 10 分钟还没 settle 的 ",[21,324,325],{},"reserved"," 记录，调 ",[21,328,329],{},"Settle(opID, 0)"," 让它全额退回原桶。这个任务即使失败也无碍——正确性不依赖它，它只是账务清理。",[14,332,333],{"id":333},"资源计价的统一框架",[10,335,336],{},"现在回到另一个问题：不同资源的单位不一样。对话要精确到 token 计数，网络搜索只能按次（一次搜索多少点），OCR 要按页数。长期来看，还会有 embedding 按千 token、视频按秒等等。",[10,338,339],{},"如果每个功能都自己定义一套计费规则，就会出现：",[127,341,342,345,348,351],{},[89,343,344],{},"有些用\"PriceRule + token 累加\"的复杂方式（对话双倍率）",[89,346,347],{},"有些用\"固定点数\"（网搜 10 点\u002F次）",[89,349,350],{},"有些用\"浮点折算\"（embedding 每 1000 token 多少点）",[89,352,353],{},"改价时无法审计，历史账单对不上",[10,355,356,357,360,361,364,365,368],{},"正确的做法是建一个",[42,358,359],{},"通用资源价表"," ",[21,362,363],{},"ResourcePrice","，所有资源都必须先配价，消费时统一调 ",[21,366,367],{},"Charge()"," 接口。",[10,370,371],{},"表的结构是这样的：",[69,373,376],{"className":374,"code":375,"language":74},[72],"ResourceKey  | DisplayName  | PricingType | Rate | PerUnits\n-------------|--------------|-------------|------|----------\nwebsearch    | 网络搜索     | PER_CALL    | 10   | 1\nocr          | OCR识别      | PER_UNIT    | 5    | 1\nembedding    | 文本嵌入     | PER_UNIT    | 0.2  | 1000\nvideo.gen    | 视频生成     | PER_UNIT    | 100  | 60\n",[21,377,375],{"__ignoreMap":77},[127,379,380,390],{},[89,381,382,385,386,389],{},[21,383,384],{},"PER_CALL","：每次调用固定点数。比如网搜一次 10 点，参数 ",[21,387,388],{},"units"," 无关",[89,391,392,395,396,399,400,228],{},[21,393,394],{},"PER_UNIT","：按单位线性计费。比如 OCR 每页 5 点（",[21,397,398],{},"perUnits=1","），embedding 每 1000 token 0.2 点（",[21,401,402],{},"perUnits=1000",[10,404,405,406,409,410,413],{},"消费时调 ",[21,407,408],{},"Quote(resourceKey, units)"," 得到应扣点数，然后 ",[21,411,412],{},"Charge(opID, userID, resourceKey, units)"," 一次性扣费，包括：",[86,415,416,422,437,444],{},[89,417,418,419,421],{},"查 ",[21,420,363],{},"（必须 enabled）",[89,423,424,425,427,428,431,432,427,434],{},"按公式计费：",[21,426,384],{}," 时取 ",[21,429,430],{},"ceil(rate)","；",[21,433,394],{},[21,435,436],{},"ceil(rate * units \u002F perUnits)",[89,438,439,440,443],{},"用多桶钱包的 ",[21,441,442],{},"Reserve\u002FSettle"," 逻辑扣分配额",[89,445,234,446,448,449,228],{},[21,447,124],{},"（",[21,450,451],{},"status=settled",[10,453,454,455,457,458,460],{},"这样的好处是：改价只需改 ",[21,456,363],{}," 里一行。所有历史的 ",[21,459,124],{}," 还是记着原来的实际扣点，不会被改价影响。新的消费按新价表计算。审计时能看到每笔消费用的是什么价表。",[14,462,463],{"id":463},"为什么是现在",[10,465,466,467,470],{},"这两项改造之所以现在做，而不是等到实际需求出现，原因很直接：",[42,468,469],{},"项目未上线、没有真实用户或活钱","。这意味着：",[86,472,473,476,479],{},[89,474,475],{},"没有历史数据需要迁移——全量重写钱包表无成本",[89,477,478],{},"没有活跃用户会在改造期间遇到不一致的余额计算",[89,480,481],{},"如果后续发现设计有漏洞，改数据结构的成本最低",[10,483,484],{},"一旦上线、产生真实用户和消费记录，再想重做多桶就会牵扯数据迁移、兼容旧账户、处理部分迁移失败等等复杂性。现在这两项都是\"必须一次做对\"的基础设施，做对的成本远小于上线后被迫改的成本。",[486,487,488],"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 .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":77,"searchDepth":490,"depth":490,"links":491},2,[492,493,494,495,496,497],{"id":16,"depth":490,"text":16},{"id":60,"depth":490,"text":60},{"id":143,"depth":490,"text":143},{"id":298,"depth":490,"text":298},{"id":333,"depth":490,"text":333},{"id":463,"depth":490,"text":463},"Agent 平台","2026-06-27","md",null,{},true,"\u002F2026-06-27",{"title":5,"description":12},"2026-06-27-多桶钱包与资源计价","平台核心计费改造：余额从单一池拆成多个受限的桶，资源计价从各自为政统一为声明式框架。",[509,510,511,512,513],"计费系统","钱包设计","幂等性","并发控制","PostgreSQL","zdVJJHPsW7FogLIbgtLSbU0tygzyS7NEvHwyOQXihk8",[516,838,1135],{"id":517,"title":518,"body":519,"column":498,"date":824,"description":523,"extension":500,"hero_image":501,"meta":825,"navigation":503,"path":826,"seo":827,"series_id":501,"severity":501,"stem":828,"summary":829,"tags":830,"__hash__":837},"posts\u002F2026-07-26-峰值1987一个人维护AI平台的边界.md","一个人，能维护到多大规模？",{"type":7,"value":520,"toc":808},[521,524,527,530,533,565,568,571,576,579,586,601,604,611,615,618,621,624,631,638,642,645,652,656,659,662,673,684,687,691,694,697,704,708,711,714,717,721,724,727,753,756,759,762,768,782,788,802],[10,522,523],{},"从 2026-05-22 的 270 个用户到 07 下旬的峰值 1987 日活，这个平台的增长不是线性的。中间隔着四次明确的容量撞墙，每一次都留下了可复查的根因记录。这篇文章讲的是，什么条件下一个人能维护一个到达四位数日活规模的多租户平台。",[14,525,526],{"id":526},"成长曲线与撞墙的四个节点",[10,528,529],{},"架构的设计前提是\"1 用户 = 1 容器\"。这个决策确定了，用户数与容器数就是同一个口径。没有\"日活 X 万、同时在线 Y 万\"这种双指标的混淆。",[10,531,532],{},"实际的规模序列是这样的：",[127,534,535,541,547,553,559],{},[89,536,537,540],{},[42,538,539],{},"270 个 service","（05-22）：刚完成 ffmpeg 升级后的测量",[89,542,543,546],{},[42,544,545],{},"372 RUNNING","（05-24）：滚动升级前的容器计数",[89,548,549,552],{},[42,550,551],{},"379","（05-25）：五个 P0 patch 部署后稳定",[89,554,555,558],{},[42,556,557],{},"574 用户","（06-03）：迁移到 K8s 时的基线，539 个 RUNNING",[89,560,561,564],{},[42,562,563],{},"1987","（07 下旬）：最近测得的峰值",[10,566,567],{},"跨度是两个月，增速从 Swarm 期间的两周内 270→379（40% 增长）、到迁 K8s 后约八周 574→1987（约 3.5 倍）。这个加速度不是\"计划好的伸缩\"，而是每一次解决了瓶颈后，下一个瓶颈暴露出来。",[14,569,570],{"id":570},"四堵撞过的墙",[572,573,575],"h3",{"id":574},"第一堵容器网络-ip-池05-22fa-012","第一堵：容器网络 IP 池（05-22，FA-012）",[10,577,578],{},"用户报告\"AI 容器里没 ffmpeg\"。这本来是个 Dockerfile 一行 apt 的事。但打算上线这个修改时，意外发现 gwbridge（Docker Swarm 默认的容器网络）IP 地址段是 \u002F24，总共 253 个 IP，已经被 270 个用户的容器占满。13 个用户容器长期处于启动失败的循环中。",[10,580,581,582,585],{},"排查逻辑是这样的：一行 Dockerfile 改动不至于触发灰度风险，但灰度一个用户时，Swarm 需要给新容器分配 gwbridge 的 IP。如果池子满了，IP 分配失败，新容器启动失败，再加上老容器还没完全释放（endpoint 还在占位），就陷入了\"申请 → 失败 → retry\"的循环。这种症状看起来随机，用户看到的是\"我的容器启不来\"，系统看到的是\"又一个容器 cycling\"。直到看 ",[21,583,584],{},"docker info"," 的输出，才发现 gwbridge 已经 100% 满用。",[10,587,588,589,592,593,600],{},"根本原因在于一个不易察觉的配置陷阱：Docker Daemon 的配置文件里改了 ",[21,590,591],{},"default-address-pools","，但这个配置",[42,594,595,596,599],{},"只在 ",[21,597,598],{},"swarm init"," 那一刻消费一次","。已经创建的网络不会动。之前改过这个配置的人可能不知道这个行为，改了等于没改。",[10,602,603],{},"修复不能简单地改配置重启。我试过在 staging 环境用 dry-run 验证，写脚本探测不冲突的子网段，然后在 production 的 canary 验证阶段发现\"释放的 IP 立刻被其它 cycling 任务抢走\"这样的负反馈。所以流程变成：先 scale 0 所有失联的服务（停止它们 cycling，释放的位置不会被抢），然后手动删除旧的 gwbridge、用新 subnet 重建。最终把容量从 253 扩到了 4094，增长了 16 倍。13 个失联用户全部恢复。",[10,605,606,607,610],{},"这次的启示是",[42,608,609],{},"配置陷阱往往比代码 bug 更隐蔽","。因为配置改了看不出效果，维护者会觉得没改上去、会反复尝试，但每次尝试的假设都错了。",[572,612,614],{"id":613},"第二堵单容器资源限制与内核参数05-25fa-013","第二堵：单容器资源限制与内核参数（05-25，FA-013）",[10,616,617],{},"五天后，部署了五个 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,619,620],{},"但更关键的数据是这个：升级前，平台里有一个重灾户容器一天被 OOM kill 了 247 次。这不是偶发故障——是每一天都在重复。升级完成一小时后，这个数字变成了 0。",[10,622,623],{},"这次的根因是九个缺陷的叠加。B1 的根因是硬限制设得太低（原来 93 个用户只有 500MB 硬限，远低于实际需要），B2 是 mcp register exit code 的错误判断（非零 exit 自动当做失败，但有时是因为配置覆盖了），B3 是文件描述符不足导致新连接打开失败。单个缺陷可能不致命，但聚在一起就是 OOM 风暴。更严重的是，一个用户的 OOM 不只影响那个用户——每次 OOM 都会触发内核的 swap 操作，拖累整机的 I\u002FO，让 cpa-api 主进程的事件循环卡顿 18 秒。前端看到的是全站变慢，实际根因可能是某个用户容器在反复 OOM。",[10,625,626,627,630],{},"部署前做了充分的 staging 验证和 production canary，分阶段升级了重灾户、然后全量升级、最后等完全收敛。期间遇到的问题是单容器的 ",[21,628,629],{},"docker service update"," 实际耗时约 60 秒（含 Swarm scheduler 延迟），并发调度起来比预期慢 2 倍。",[10,632,633,634,637],{},"这次的启示是：",[42,635,636],{},"当多个独立缺陷同时叠加时，表象看起来是单一故障（OOM 风暴），但根因分散在配置、参数、逻辑判断的不同层","。修复必须从全景取证开始，先理清每个环节的缺陷，再按优先级有序修复。一次部署才能彻底收敛，局部修复反而会留下隐患。",[572,639,641],{"id":640},"第三堵编排层与自愈能力06-03迁-k8s","第三堵：编排层与自愈能力（06-03，迁 K8s）",[10,643,644],{},"到 06-03，用户已经涨到 574 个。继续打补丁的成本已经高于\"一次性迁移编排平台\"。Swarm 的问题不只是容量——还有调度自愈的缺失。任何故障都依赖人工介入，一个人无法 24 小时在线。决策很简单：从 Docker Swarm 迁到 Kubernetes。",[10,646,647,648,651],{},"这次迁移的复杂性不在于技术实现本身（用 COS 中转数据、转换 sub2api 密钥、把 124G 数据分批导入），而在于",[42,649,650],{},"理解新平台引入的新故障域","。K8s 有更细的控制粒度和自动调度，但同时也暴露了之前 Swarm 隐藏的问题。比如共享存储（NFS\u002FSFS）的客户端卡死，在 Swarm 时期可能因为容器分散在不同节点而被掩盖；但在 K8s 这样的细粒度编排下，如果一个节点的 NFS 客户端出问题，节点上的所有 pod 都会受影响。所以迁移不是终点，而是暴露新问题的起点。",[572,653,655],{"id":654},"第四堵节点级存储与可观测性06-05fa-015","第四堵：节点级存储与可观测性（06-05，FA-015）",[10,657,658],{},"迁移两天后，某个节点上的 NFS 客户端在某个时刻 hang 住了。Kubernetes 不知道发生了什么，kubelet 仍然报告节点 Ready（因为 kubelet 本身没卡），但这个节点上 52 个实例的网关全部 DOWN 或 HUNG。这 52 个实例对应的是本该分散部署的用户容器，因为某种原因堆在了同一个节点上。",[10,660,661],{},"故障的症状可以分成几类。单个实例网关的 DOWN（容器无法启动、schema 非法）、HUNG（反复重启导致 adapter 504）、节点级的卡死（整节点上的容器创建\u002F删除都阻塞）、数据库与实际状态割裂（DB 里是 ERROR、但 pod 健康了）。每种症状对应不同的根因，需要不同的检测和自愈手段。",[10,663,664,665,668,669,672],{},"最严重的是，",[42,666,667],{},"这个故障没有任何告警","。平台的监控指标全部正常，绿灯一片。直到用户反馈\"连不上面板\"，才被发现。这已经是故障发生几小时以后的事。故障本身是可逆的——节点冻死就人工 cordon、删除卡住的 pod、让其它节点重新调度。但",[42,670,671],{},"不可见的故障比故障本身更致命","。",[10,674,675,676,679,680,683],{},"这次事故直接导出了四层兜底的设计：L0 从配置和资源限制层面降低故障触发（比如 NFS 改 ",[21,677,678],{},"soft"," 参数而不是 ",[21,681,682],{},"hard","，这样网络抖动时容器报错退出而不是整个节点冻）；L1 加检测让故障可见（每 60 秒检测一遍\"节点上是否有 pod 卡 ContainerCreating、网关探测失败率多少\"）；L2 对低风险故障自动自愈（网关单次 DOWN 就重建 pod、DB 对账失败就修复状态）；L3 对高风险动作告警优先（节点冻死先推送告警、等人工确认再 drain）。",[14,685,686],{"id":686},"三件真正决定可行性的事",[572,688,690],{"id":689},"_1-文档即基础设施","1. 文档即基础设施",[10,692,693],{},"这个平台现在有 200+ 份设计文档与 80 份故障档案。这些不是为了\"好看\"存在的。",[10,695,696],{},"一个人无法对整个系统保持完整的心智模型。200+ 文档是唯一能让一个人记住系统全貌的方式。每当遇到新故障，能快速检索以前遇过的类似问题。每当需要做架构决策，能回溯当初为什么这样设计、放弃过哪些选项。",[10,698,699,700,703],{},"同时，",[42,701,702],{},"这些文档也是 AI 能有效介入的前提","。我可以把这些故障档案喂给模型，让它帮助排查新问题、验证修复方案、甚至生成监控规则。但前提是要把故障记录得清楚。空洞的\"修好了\"没有任何价值。",[572,705,707],{"id":706},"_2-故障必须被归档","2. 故障必须被归档",[10,709,710],{},"同一个症状反复出现，每次修复可能只治了表象的一个侧面，真正的收敛需要理解全部的根因。这只有在每一次都写清档案的情况下才可能。",[10,712,713],{},"如果没有归档制度，第二次遇到类似症状时，维护者根本不知道第一次修复做过什么、为什么还没有彻底解决。\"又来了\"和\"这个问题还有遗留\"的反应完全不同。前者是被动应对、逐次救火，后者是主动追踪、系统解决。",[10,715,716],{},"实际上，平台里有不少故障走过了四到五次的修复周期。每一次修复时，回看之前的档案，能快速理清\"这一层已经改过，那一层还没触及\"。这种\"有案可查、有据可循\"的状态，把修复从赌博变成了可重复的流程——每一次问题复发时，不是从零开始排查，而是从已知的检查点继续。",[572,718,720],{"id":719},"_3-自愈优先于告警告警优先于人工","3. 自愈优先于告警，告警优先于人工",[10,722,723],{},"在 FA-015 之前，平台大量依赖人工值守。任何问题都需要运维看到日志、理解现象、手动操作。一个人无法 24 小时在线。",[10,725,726],{},"FA-015 的教训直接导出了四层兜底的设计：",[127,728,729,735,741,747],{},[89,730,731,734],{},[42,732,733],{},"L0 预防","：从配置和资源限制的层面降低故障触发的概率。",[89,736,737,740],{},[42,738,739],{},"L1 检测","：让不可见的故障变成可见——节点卡死、实例网关异常、数据库与实际状态割裂，全部要有独立的检测逻辑。",[89,742,743,746],{},[42,744,745],{},"L2 自愈","：对于低风险的故障（单实例网关重启、DB 状态对账），直接自动修复。高风险的动作（节点 drain）先告警、等人工确认。",[89,748,749,752],{},[42,750,751],{},"L3 告警","：自愈失败时、检测到新的异常时，推送给人。",[10,754,755],{},"这样的设计下，一个人维护平台的上限大幅抬高。不是因为个人能力变强了，而是系统能自动处理大多数故障，只把人类的决策能力用在最关键的地方。",[14,757,758],{"id":758},"诚实的边界在哪",[10,760,761],{},"但这个设计也有明显的天花板：",[10,763,764,767],{},[42,765,766],{},"真正撑不住的","（需要六小时以上连续操作、需要跨时区响应、需要多人交叉验证）：",[127,769,770,773,776,779],{},[89,771,772],{},"数据库或存储层的重大故障。恢复涉及数据一致性检查，无法完全自动化。",[89,774,775],{},"涉及业务逻辑的错误。修复需要理解用户意图，不只是系统恢复。",[89,777,778],{},"密钥泄露或安全事件。需要立刻通知客户、协调应急处置、事后全面审计。",[89,780,781],{},"多个独立故障同时发生、相互放大的情况。需要多个人在不同维度分别操作。",[10,783,784,787],{},[42,785,786],{},"如果重来一次，优先级这样排","：",[86,789,790,793,796,799],{},[89,791,792],{},"最先做的是 L1 检测——让故障可见。这是一切自动化的前提。宁可产生虚报，也不能漏掉真实故障。",[89,794,795],{},"其次是 L0 预防——从配置、资源限制、网络参数这些基础设施层降低故障率。这些改动成本低、收益高。",[89,797,798],{},"然后才是 L2 自愈——只对低风险的故障做自动恢复。对于高风险操作，即使多花一个人工确认的时间，也要确保不会进一步破坏系统。",[89,800,801],{},"最后是文档和监控。这些不是\"最后的事情\"，而是贯穿全过程的——每个改动都要同步更新文档、每个故障都要写进档案。",[10,803,804,805,807],{},"那些回避的成本很高。曾经因为 NFS 挂载的 ",[21,806,682],{}," 参数导致节点冻死，这个参数的改动只需要改一行配置文件、然后滚动重启一次实例。但因为这个调整一直没做，就承受了几小时的无声故障。反过来说，那些看起来\"小\"的改动——改配置参数、改资源限制、加一个监控规则——才是最划算的投资。",{"title":77,"searchDepth":490,"depth":490,"links":809},[810,811,818,823],{"id":526,"depth":490,"text":526},{"id":570,"depth":490,"text":570,"children":812},[813,815,816,817],{"id":574,"depth":814,"text":575},3,{"id":613,"depth":814,"text":614},{"id":640,"depth":814,"text":641},{"id":654,"depth":814,"text":655},{"id":686,"depth":490,"text":686,"children":819},[820,821,822],{"id":689,"depth":814,"text":690},{"id":706,"depth":814,"text":707},{"id":719,"depth":814,"text":720},{"id":758,"depth":490,"text":758},"2026-07-26",{},"\u002F2026-07-26-1987ai",{"title":518,"description":523},"2026-07-26-峰值1987一个人维护AI平台的边界","从 270 到 1987 日活，每一次规模跃升前都先撞了一次墙。这条增长曲线记录的不是预设设计，而是每次故障都被完整归档后逐步演进出来的可行性边界。",[831,832,833,834,835,836],"Docker Swarm","Kubernetes","容器编排","运维自动化","故障自愈","规模扩展","D-mYcaOLOBT0PhwjmrqVVEwCE7hl8Tqi9I57HrRlt5U",{"id":839,"title":840,"body":841,"column":498,"date":1122,"description":1123,"extension":500,"hero_image":501,"meta":1124,"navigation":503,"path":1125,"seo":1126,"series_id":501,"severity":501,"stem":1127,"summary":1128,"tags":1129,"__hash__":1134},"posts\u002F2026-07-14-分销体系的账本设计.md","一笔充值，要拆成几条流水？",{"type":7,"value":842,"toc":1112},[843,850,853,857,860,863,870,881,888,892,895,901,904,907,910,921,924,932,939,943,946,957,960,971,978,982,985,991,994,1005,1008,1012,1015,1018,1021,1032,1035,1043,1046,1057,1060,1063,1068,1075,1080,1091,1094,1097,1100,1106,1109],[10,844,845,846,849],{},"单笔用户充值，背后是一次复杂的资金拆分：用户充值 100 元，既是平台的收入，也是分销代理的佣金来源，可能还有上级代理的层级提成。这些数字必须同时记录、互相平衡、永不重复。这不是数据流通的问题，是",[42,847,848],{},"现金流的问题","——差一分钱就是漏账，重复一次就是挪用。",[10,851,852],{},"分销账本设计的核心就四条铁律和一个恒等式。遵循它们，系统能撑到任何规模；跳过其中任何一条，早晚会在对账时翻车。",[14,854,856],{"id":855},"规则一佣金计算基数要先定死","规则一：佣金计算基数要先定死",[10,858,859],{},"从什么数字出发算佣金？这个问题比看起来复杂。",[10,861,862],{},"通常的选项有三个：订单总金额、用户实付金额、或者订单到账净额。乍看没区别，一旦遇上退款就完全不同。",[10,864,865,866,869],{},"采用的方案是",[42,867,868],{},"用户充值净额","（billing 系统中已确认到账的实付分）。理由很直白：",[86,871,872,875,878],{},[89,873,874],{},"退款处理天然免疫。用户充值后退款，billing 的该用户账户余额已经扣掉，充值净额自动反映了这笔冲销。分成计算只需聚合这个净额乘以比例，不用单独写退款冲正逻辑。",[89,876,877],{},"避免应收账款。如果以订单金额算，还没到账时代理已经看得到分成，这在 reporting-only 设计下容易造成认知错位（代理以为钱已经是他的，实际还在支付处理中）。",[89,879,880],{},"同源唯一。billing 是平台的权威账本，分成的基数来自这里，对账时只需验证\"代理分成之和 + 平台收入 = billing 总充值\"，一个公式搞定。",[10,882,883,884,887],{},"反过来说，如果公司后续引入退款主动冲补（而非被动扣减），这个基数设定会变得复杂。但在初期，",[42,885,886],{},"基数 = billing 已确认充值"," 是最简洁的切口。",[14,889,891],{"id":890},"规则二结算时点决定了数据流向","规则二：结算时点决定了数据流向",[10,893,894],{},"到底是在订单成交时计提佣金，还是账期结束时一次性结算？",[10,896,897,898,672],{},"这里的选择是 ",[42,899,900],{},"reporting-only 只读聚合，实时查询，不计提、不累积",[10,902,903],{},"具体含义是：代理看到的\"我的分成\"不是一条条流水记录，而是每次查询时现场计算出来的聚合数字。算法是\"我名下所有用户的充值净额总和 × 我的佣金比例\"。没有单独的\"分成计提\"操作，没有一条条的\"分成到账\"记录。",[10,905,906],{},"好处和代价是对偶的。",[10,908,909],{},"好处：",[127,911,912,915,918],{},[89,913,914],{},"免除计提时点的争议。不用决定在订单成交时、支付完成时、还是 T+1 时计提，因为根本不计提。",[89,916,917],{},"天然避免双扣。既然分成不落库不累积，就不存在\"发放一次、又重复发放一次\"的并发风险。同一笔充值无论被查询多少次，贡献的佣金永远相同。",[89,919,920],{},"简化对账。代理的分成数字永远等于\"最新充值净额 × 比例\"，无需追溯历史。",[10,922,923],{},"代价：",[127,925,926,929],{},[89,927,928],{},"代理无法看到\"分成流水\"。有些运营场景下，需要展示\"哪笔订单产生了多少佣金\"这样的明细，reporting-only 做不了（可以通过关联用户的充值明细变通，但那是用户维度的流水，不是分成维度的）。",[89,930,931],{},"退款时必须同步。如果用户退了 50 块钱，billing 系统立刻反映这笔扣减，代理的分成下一秒查询就会跌下来。这对代理来说是透明的（分成就是动态的），但运营沟通时需要提前说清楚。",[10,933,934,935,938],{},"选择 reporting-only 的核心原因是：",[42,936,937],{},"初期不出金、无提现","。既然分成只是一个数字展示、不涉及真金白银的打款，那就不用建立复杂的流水账体系。等到未来做提现时，可以在 reporting-only 的基础上加一层\"快照 + 冻结\"机制（即每个提现周期开始时拍一个快照，这个快照才是可提的分成额）。",[14,940,942],{"id":941},"规则三层级上限和循环检测","规则三：层级上限和循环检测",[10,944,945],{},"分销是分多少层级？",[10,947,948,949,952,953,956],{},"首期方案是",[42,950,951],{},"单层","。用户通过一个特定的渠道码注册（如 ",[21,954,955],{},"AB-48210377","），永久绑定到某个代理。代理无法有\"上级代理\"，也就无法有\"上级佣金\"这样的递归结构。",[10,958,959],{},"这一约束看起来很强，但在无提现的 reporting-only 下，是合理的。理由是：",[86,961,962,965,968],{},[89,963,964],{},"简化代理运维。总台只需管理一套代理的佣金比例（per-代理），不用维护代理之间的树形关系。",[89,966,967],{},"避免环形链。单层天然杜绝了\"A 的上级是 B，B 的上级是 A\"这类配置错误。多层结构下，环形检测本身又是一个故障点。",[89,969,970],{},"初期够用。大多数分销场景早期就是\"直销商（代理）→ 用户\"的二元关系，不需要分级。",[10,972,973,974,977],{},"但要注意，这个约束是",[42,975,976],{},"数据模型层的","，不是业务规则层的。如果未来需要升级到多层结构，数据模型需要改（Channel 表可能要加 parentResellerId 等），但已经发出去的单层记录无需回溯改造——它们天然是单层的。",[14,979,981],{"id":980},"规则四幂等键设计防重复计提","规则四：幂等键设计（防重复计提）",[10,983,984],{},"同一笔充值可能被多个系统调用、被回调多次。分成必须严格幂等：无论这笔充值被聚合几次，贡献给代理的佣金永远是\"充值额 × 比例\"这一个数字，不能是两倍、三倍。",[10,986,987,988,672],{},"因为采用 reporting-only + 只读聚合的设计，幂等性",[42,989,990],{},"自动满足",[10,992,993],{},"推理如下：",[86,995,996,999,1002],{},[89,997,998],{},"billing 侧的充值流水本身是幂等的。同一个订单号的充值，billing 确保只入账一次（通过订单号的唯一性约束）。",[89,1000,1001],{},"分成聚合是无状态的。每次查询时，服务端都是\"遍历该代理名下的用户 → 调用 billing 的 summaryByUsers 接口 → 汇总充值净额 → 乘以比例\"。这个聚合过程不依赖任何之前的计提记录。",[89,1003,1004],{},"结论：即使 billing 错误地返回了同一笔充值两次，聚合结果也只会包含一次（因为底层是\"用户ID → 总充值净额\"的映射，不是\"订单 → 充值\"的流水列表）。",[10,1006,1007],{},"相反，如果设计成\"订单成交时计提一条分成记录\"的模式，就需要在分成记录上加幂等键（如 orderId），确保同一订单的分成只计提一次。这个幂等键检查本身就是一个额外的故障点。",[14,1009,1011],{"id":1010},"一个恒等式对账的唯一标准","一个恒等式：对账的唯一标准",[10,1013,1014],{},"前面四条规则规范了流程，但最后的验证还是要靠一个简单的数学公式。",[10,1016,1017],{},"$$\n\\sum_^{n} \\text{Commission}_i + \\text{PlatformNetIncome} = \\text{TotalTopup}\n$$",[10,1019,1020],{},"其中：",[127,1022,1023,1026,1029],{},[89,1024,1025],{},"$\\text{Commission}_i$ 是第 $i$ 个代理的分成（所有名下用户的充值净额 × 佣金比例）",[89,1027,1028],{},"$\\text{PlatformNetIncome}$ 是平台的净收入（总充值 - 所有代理的分成总额）",[89,1030,1031],{},"$\\text{TotalTopup}$ 是 billing 系统的总充值额（所有用户的到账充值之和）",[10,1033,1034],{},"这个等式是唯一可靠的对账标尺。任何时刻，只要这个等式不成立，就说明某个环节出了问题：",[127,1036,1037,1040],{},[89,1038,1039],{},"等式左侧大于右侧 → 某个代理的分成算重了，或者平台收入算多了",[89,1041,1042],{},"等式左侧小于右侧 → 某个代理的分成算少了，或者某笔收入漏了",[10,1044,1045],{},"而且这个等式不需要建立任何新表。完全可以通过查询三个现成的数据源验证：",[86,1047,1048,1051,1054],{},[89,1049,1050],{},"billing 的 SummaryByUsers（每个用户的充值净额）",[89,1052,1053],{},"代理表的 commissionRate（每个代理的佣金比例）",[89,1055,1056],{},"一行 SQL 的聚合（sum 和乘法）",[14,1058,1059],{"id":1059},"技术实现的两个关键点",[10,1061,1062],{},"光有规则还不够，实现层要支撑这些规则。素材中的设计有两个细节值得指出。",[10,1064,1065,672],{},[42,1066,1067],{},"其一，billing 要暴露 summaryByUsers 接口",[10,1069,1070,1071,1074],{},"分成聚合依赖\"按用户ID汇总充值净额和消耗\"这个操作。如果 billing 侧没有这个批量接口，代理端就得自己拼接多个单用户查询，性能和一致性都会打折扣。实现计划里新增的 ",[21,1072,1073],{},"POST \u002Fanalytics\u002Fsummary-by-users"," 就是为了这个。",[10,1076,1077,672],{},[42,1078,1079],{},"其二，reseller 侧的所有查询要服务端注入 channelId",[10,1081,1082,1083,1086,1087,1090],{},"代理登录后调用 ",[21,1084,1085],{},"\u002Fapi\u002Freseller\u002Fsummary"," 或 ",[21,1088,1089],{},"\u002Fapi\u002Freseller\u002Fusers","，后端不能信任请求体里的 channelId 参数。而是从 token 解析出代理身份 → 查表得出该代理对应的 channelId → 强制注入到查询条件里。这是防代理 A 越权查看代理 B 渠道的唯一有效方式。",[10,1092,1093],{},"代码里体现为：从 Admin token 反查 Channel 表的 resellerId 字段，确保该 Admin 只能看自己那行 Channel。",[14,1095,1096],{"id":1096},"与既有分账设计的呼应",[10,1098,1099],{},"这套账本设计不是凭空造出来的。平台此前在另一个系统里实现过五类角色的分账体系（Platform、Agency、Escort、Distributor、Merchant），每个角色各维护一张 Ledger 流水表。那个设计的核心思想是\"分账入表\"——即每一笔影响各角色收入的交易，都要对应地在各自的 Ledger 表里落一条记录。",[10,1101,1102,1103,672],{},"当前这套分销设计采用了相反的思路：不建 Ledger 表，而是在查询时通过聚合来推导分成。这的背景是 reporting-only 属性（不出金、只展示），使得可以接受动态聚合的方案。但底层的思想是一致的——",[42,1104,1105],{},"通过对账恒等式来保证多角色之间的收支平衡",[14,1107,1108],{"id":1108},"结尾",[10,1110,1111],{},"账本设计的目标不是漂亮的表格或丰富的报表，而是一个简单的数学等式永远成立。一旦等式破裂，对账人员立刻能定位是哪个环节失守。这比事后扑火要高效得多。",{"title":77,"searchDepth":490,"depth":490,"links":1113},[1114,1115,1116,1117,1118,1119,1120,1121],{"id":855,"depth":490,"text":856},{"id":890,"depth":490,"text":891},{"id":941,"depth":490,"text":942},{"id":980,"depth":490,"text":981},{"id":1010,"depth":490,"text":1011},{"id":1059,"depth":490,"text":1059},{"id":1096,"depth":490,"text":1096},{"id":1108,"depth":490,"text":1108},"2026-07-14","单笔用户充值，背后是一次复杂的资金拆分：用户充值 100 元，既是平台的收入，也是分销代理的佣金来源，可能还有上级代理的层级提成。这些数字必须同时记录、互相平衡、永不重复。这不是数据流通的问题，是现金流的问题——差一分钱就是漏账，重复一次就是挪用。",{},"\u002F2026-07-14",{"title":840,"description":1123},"2026-07-14-分销体系的账本设计","分销系统最容易在财务对账出错。单笔充值需同时产生平台收入、代理佣金等多条记录，必须在同一事务内闭合。四条规则与一个恒等式是账本设计的全部。",[1130,1131,1132,511,1133],"分销","账本设计","对账","佣金结算","Q3NC9Z8JBJ7e2MLQFH10q68Igpbp2fMF5gT-do6Rxaw",{"id":1136,"title":1137,"body":1138,"column":498,"date":1448,"description":1142,"extension":500,"hero_image":501,"meta":1449,"navigation":503,"path":1450,"seo":1451,"series_id":501,"severity":501,"stem":1452,"summary":1453,"tags":1454,"__hash__":1460},"posts\u002F2026-06-29-记忆星系长期记忆可视化工作台.md","模型记错了，用户得能删掉",{"type":7,"value":1139,"toc":1438},[1140,1143,1146,1150,1156,1159,1162,1165,1168,1175,1178,1181,1213,1216,1219,1292,1299,1306,1309,1312,1356,1359,1362,1366,1369,1372,1375,1378,1381,1384,1387,1390,1393,1400,1407,1410,1413,1420,1423,1426,1429,1432,1435],[10,1141,1142],{},"长期记忆存起来容易，用户看不见也管不了。记忆一旦不可见，就会累积错误信息并持续污染后续对话——模型出错时没有纠正入口，错误就永久留存。",[10,1144,1145],{},"我在 yun-claude 的记忆设计中遇到的问题正是这个。聊天系统能自动从对话抽取持久要点并入库，但页面还是传统的卡片列表，看不出记忆的类型、重要性、使用频次。更严重的是，用户无法编辑或删除那些被错误标记的记忆。所以这次升级的核心不是加一个炫彩的可视化，而是把记忆管理变成一个真正可用的信息工作台。",[14,1147,1149],{"id":1148},"表格优先而不是星河优先","表格优先，而不是星河优先",[10,1151,1152,1153,672],{},"设计的第一个决策是：",[42,1154,1155],{},"主界面用表格承载记忆列表，不用星河画布作主体交互",[10,1157,1158],{},"这听起来反直觉。当初考虑过让星河画布成为核心——节点按重要性大小分布、按创建时间环形排列、搜索命中时高亮。视觉上是漂亮的，也更有\"知识宇宙沉淀\"的产品感。但约束改变了这个选择：",[10,1160,1161],{},"第一，信息密度。表格一屏可以显示 10-20 条记忆的核心属性（标题、类型、重要性、标签、创建时间），并支持排序和筛选。星河画布要展示相同的信息量，就必须让节点变小、缩放调整、甚至分屏展示——交互成本陡升。而用户——AI agent 的主人——需要快速浏览和定位记忆，不是浏览艺术装置。",[10,1163,1164],{},"第二，编辑成本。星河上的节点编辑通常要额外打开面板或弹窗。如果记忆管理的主要工作是\"检查是否有错记、删除重复、调整分类\"，那星河就不是最优方案。对比之下，表格 + 右侧详情面板的结构让用户可以同时看到列表和正在编辑的项目，没有上下文切换。",[10,1166,1167],{},"第三，用户规模。当前 yun-claude 没有真实用户，推断单个用户的记忆条数会比较少（几十到几百）。在这个规模下，表格就足够快了，不必依赖图形加速或向量索引的复杂优化。如果将来规模增大再考虑换方案。",[10,1169,1170,1171,1174],{},"所以最终设计是：",[42,1172,1173],{},"表格为主工作台，右侧详情面板负责编辑与整理，星系可视化只作为辅助视图（后续可选）","。这不是缺乏视觉想象力，而是对工作流的务实权衡。代价是产品感稍弱，但可用性强得多。",[14,1176,1177],{"id":1177},"五类语义记忆与设计令牌",[10,1179,1180],{},"为了让记忆可见可管，将长期记忆分成五类：",[127,1182,1183,1189,1195,1201,1207],{},[89,1184,1185,1188],{},[42,1186,1187],{},"核心记忆","（CORE）：与用户身份、核心项目、重要约束直接相关。模型在每轮对话发送前都应该检索。",[89,1190,1191,1194],{},[42,1192,1193],{},"常驻记忆","（PERMANENT）：用户的工作背景、技术栈偏好、团队结构等长期背景。",[89,1196,1197,1200],{},[42,1198,1199],{},"临时记忆","（TEMPORARY）：短期的任务进度、当前问题、临时约束。生命周期短。",[89,1202,1203,1206],{},[42,1204,1205],{},"知识星云","（KNOWLEDGE）：用户分享的文档要点、API 文档摘录、最佳实践。主要用于 RAG 增强。",[89,1208,1209,1212],{},[42,1210,1211],{},"其他","（OTHER）：模型无法明确分类或用户手动标记的记忆。",[10,1214,1215],{},"每一类的关键差异是生命周期和召回策略。核心记忆应该高频被注入 prompt，临时记忆应该自动清理，知识类应该被 RAG 系统共同使用。",[10,1217,1218],{},"为了在视觉上强化这个分类，为每类配置了一个独立的语义色：",[1220,1221,1222,1238],"table",{},[1223,1224,1225],"thead",{},[1226,1227,1228,1232,1235],"tr",{},[1229,1230,1231],"th",{},"类型",[1229,1233,1234],{},"颜色",[1229,1236,1237],{},"用途",[1239,1240,1241,1253,1263,1273,1283],"tbody",{},[1226,1242,1243,1247,1250],{},[1244,1245,1246],"td",{},"核心",[1244,1248,1249],{},"Amber-500",[1244,1251,1252],{},"节点、筛选按钮、卡片左边线",[1226,1254,1255,1258,1261],{},[1244,1256,1257],{},"常驻",[1244,1259,1260],{},"Sky-500",[1244,1262,1252],{},[1226,1264,1265,1268,1271],{},[1244,1266,1267],{},"临时",[1244,1269,1270],{},"Teal-500",[1244,1272,1252],{},[1226,1274,1275,1278,1281],{},[1244,1276,1277],{},"知识",[1244,1279,1280],{},"Violet-500",[1244,1282,1252],{},[1226,1284,1285,1287,1290],{},[1244,1286,1211],{},[1244,1288,1289],{},"Slate-500",[1244,1291,1252],{},[10,1293,1294,1295,1298],{},"关键的设计决策是：",[42,1296,1297],{},"颜色不只是装饰，而是结构的一部分","。在表格、筛选条、详情面板的边线上，用户都能看到同一个颜色，强化类型认知。这样的一致性是可信的信息工作台的标志——用户一眼知道哪条记忆是核心、哪条是临时。",[10,1300,1301,1302,1305],{},"颜色值不是散落在各个组件里的魔法数字，而是集中在一个 ",[21,1303,1304],{},"memoryStyles.ts"," 文件中管理。任何需要记忆类型颜色的地方都从这里引用。这样改一个颜色时不必跨多个文件搜索替换，也不会出现同类型在不同地方显示不同色的尴尬局面。",[14,1307,1308],{"id":1308},"记忆模型与后端约束",[10,1310,1311],{},"后端为每条记忆增加了结构化字段：",[127,1313,1314,1320,1326,1332,1338,1344,1350],{},[89,1315,1316,1319],{},[21,1317,1318],{},"title","：节点或列表行的标题，从正文前 18 个字符生成",[89,1321,1322,1325],{},[21,1323,1324],{},"type","：五类之一",[89,1327,1328,1331],{},[21,1329,1330],{},"importance","：1-100 的重要性分值，决定节点大小和列表排序",[89,1333,1334,1337],{},[21,1335,1336],{},"tags","：字符串数组，最多 8 个标签",[89,1339,1340,1343],{},[21,1341,1342],{},"lastUsedAt","：最近被召回的时间",[89,1345,1346,1349],{},[21,1347,1348],{},"usedCount","：被召回次数",[89,1351,1352,1355],{},[21,1353,1354],{},"metadata","：扩展字段，保存来源会话、模型、抽取动作等",[10,1357,1358],{},"当模型抽取记忆时，输出包含这些结构化字段。服务端必须做兜底和校验：type 非法时改为 OTHER、importance 超范围时裁剪、tags 去重去空白最多保留 8 个。如果抽取失败，仍然保存正文并用默认字段（importance=50, type=OTHER）。",[10,1360,1361],{},"这些默认值的设计是为了降级优雅。即便结构化抽取失败，记忆也不会丢失，只是分类不精准——这是可以接受的，因为用户随后可以手动调整。",[14,1363,1365],{"id":1364},"搜索筛选与编辑","搜索、筛选与编辑",[10,1367,1368],{},"用户在表格上方有一个搜索框和类型筛选条。搜索时调用后端的语义搜索接口，命中的记忆在表格中高亮，并自动选中最高相关性的一条，打开右侧详情面板。语义搜索基于向量数据库的余弦相似度匹配——记忆文本被嵌入为 4096 维向量，查询时也转换为向量并与库内存储按相似度排序召回，只返回分值达到阈值（≥0.3）的结果。这样的匹配比关键词搜索精准度高，也支持语义近似的记忆关联（比如\"TypeScript 后端\"和\"TS 服务端\"会被认为相关）。搜索失败时保留当前星河状态并 toast 提示，不中断工作流。",[10,1370,1371],{},"筛选可以按五类过滤，清除筛选则回到全量视图。如果当前选中的记忆被筛选隐藏了，系统会自动选中可见记忆中最高重要性的那一条；如果没有可见记忆，则关闭详情面板显示空状态。",[10,1373,1374],{},"详情面板里，用户可以编辑标题、正文、类型、标签和重要性。编辑表单使用产品内设计，不弹浏览器原生弹窗。保存后立即更新列表和节点（如果有星河视图的话）。这样用户修改一条记忆时，不必刷新页面或等待后台同步，改动立刻可见。",[10,1376,1377],{},"删除是另一个关键能力。必须让用户能删除被错误标记的记忆，否则错误就成了永久污染源。删除按钮放在详情面板的危险操作区，点击后需确认。删除成功后，该条记忆从表格和星河中消失，列表自动选中下一条（如果有的话）。",[14,1379,1380],{"id":1380},"星河与移动端降级",[10,1382,1383],{},"虽然主界面是表格，但在设计中保留了星河作为可视化补充。桌面端在表格下方或侧边可以显示一个小星图，让用户看到记忆的空间分布——核心记忆聚在中心，临时记忆分散在外围。星河上的节点与表格关联：点击表格行时，星河也高亮对应节点；点击星河节点时，表格定位到对应行。",[10,1385,1386],{},"但星河不是强制项。第一版的实现可能不包含完整星河，而是先确保表格工作台完整可用。星河可以作为后续的增强——用户如果觉得需要可视化辅助，才加上去。",[10,1388,1389],{},"移动端设计上，星河更是不现实（屏幕太小）。所以移动端完全降级为列表视图，点击列表项后通过底部抽屉显示详情和编辑表单。搜索和类型 tabs 放在顶部，保持核心交互可用。这样既避免了表格在手机上的横向溢出，也保证了可用性。",[14,1391,1392],{"id":1392},"节点布局与稳定性",[10,1394,1395,1396,1399],{},"如果星河要实现，一个重要的设计细节是：",[42,1397,1398],{},"节点位置必须稳定","。即便刷新页面或重新打开应用，同一批记忆应该保持相同的位置，这样用户才能形成空间记忆——\"核心记忆总在中心，临时的在右上角\"。",[10,1401,1402,1403,1406],{},"这意味着节点位置不能是随机的实时布局，也不能用物理模拟（那会每次都算不同的位置）。用 ",[21,1404,1405],{},"createdAt"," 字段参与布局计算，保证确定性：同一条记忆的创建时间固定了，它在环形排列中的角度也就固定了。结合 importance 决定距离中心的远近，位置就完全由数据驱动，刷新后毫厘不差。",[14,1408,1409],{"id":1409},"一致性与可维护性",[10,1411,1412],{},"这个设计的关键约束是一致性。如果记忆在表格中是 Amber-500（核心），那在星河、筛选、详情面板的边线上也必须是 Amber-500。任何拆散这个一致性的修改都会破坏用户的心智模型。",[10,1414,1415,1416,1419],{},"所以在设计系统里明确了这一点：",[42,1417,1418],{},"新增或调整记忆类型视觉时，先更新设计文档里的语义色板和 memoryStyles.ts，再在各组件中引用，不允许组件各自复制色值","。这样的约束看似严苛，但它保证了长期的可维护性——下次要改颜色时，只需改一个文件。",[14,1421,1422],{"id":1422},"代价与权衡",[10,1424,1425],{},"这个设计的代价是什么？",[10,1427,1428],{},"第一，视觉冲击力比不上星河优先。表格是务实的设计，不够\"黑科技\"感。如果产品定位是\"AI 记忆系统\"要卖视觉冲击，这个方案会显得保守。",[10,1430,1431],{},"第二，星河的潜力没有完全释放。放弃了复杂的关系推理、自由漫游、力导向布局这些\"高级\"可视化特性，原因就是表格工作台不需要它们，而加上去反而添加复杂度。",[10,1433,1434],{},"第三，设计系统的维护成本提高了。一致性的要求意味着每次改动都要考虑全局影响。但这其实是长期收益——减少了 bug 和不一致的可能。",[10,1436,1437],{},"这些代价对 yun-claude 是可以接受的，因为当前用户还不多，重点是让产品可用，而不是炫技。如果将来用户规模上升，数百条甚至千条记忆的管理场景出现，表格 + 星河的混合界面可能不够，那时再考虑更激进的可视化方案。现在，信息工作台的优先级明确高于视觉体验。",{"title":77,"searchDepth":490,"depth":490,"links":1439},[1440,1441,1442,1443,1444,1445,1446,1447],{"id":1148,"depth":490,"text":1149},{"id":1177,"depth":490,"text":1177},{"id":1308,"depth":490,"text":1308},{"id":1364,"depth":490,"text":1365},{"id":1380,"depth":490,"text":1380},{"id":1392,"depth":490,"text":1392},{"id":1409,"depth":490,"text":1409},{"id":1422,"depth":490,"text":1422},"2026-06-29",{},"\u002F2026-06-29",{"title":1137,"description":1142},"2026-06-29-记忆星系长期记忆可视化工作台","表格优先的记忆管理：用高信息密度工作台承载持久要点，可视化只作辅助，设计令牌贯穿全局。",[1455,1456,1457,1458,1459],"长期记忆","信息工作台","设计决策","语义搜索","设计令牌","gsJKxlNQ-FiADsw2ca6e5WJo-_uyYDFZ55iuiMr4jQE",1785406912236]