Tag: 架构

被否掉的、没底的、还没做的

被否掉的、没底的、还没做的

《ops-agent 建造笔记》第 12 篇(终) 代码基线:v0.1.0-rc.1,提交 d897d2c95e70(2026-10-25 发布的候选版) 对照实现:core.py、execution.py、agent/tools.py、agent/schedule.py 与发布/验收文档

同一个内核,四个壳

同一个内核,四个壳

《ops-agent 建造笔记》第 10 篇 代码基线:v0.1.0-rc.1,提交 d897d2c95e70(2026-10-25 发布的候选版) 对照实现:opsagent、agent/loop.py、agent/session.py、agent/rpc.py、mcp_server.py

两轮系统性审查:哪些债我修了,哪些我明确决定不修

0. 我给自己做了一次代码审查,然后否决了它的大部分方案

有一阵子我觉得代码里积了不少问题——文件越来越大、有几处逻辑我自己都记不清了。于是我让一个模型对 develop 做了一轮系统性审查,产出一份问题清单。

订阅账务:续费、退款、冲正,以及「已售出的套餐被改了」怎么办

摘要

一个看起来很简单的问题,答案不简单:用户买了一个月套餐、用了 10 天、要求退款,退多少钱?而这只是开始——订阅怎么处理、已经消耗的额度怎么算、续过费先退哪个、账本上怎么写。这篇讲我怎么处理这三个操作,以及一个跨所有操作的机制:购买时的套餐快照。

透传:为什么 embeddings 不该被「翻译」,以及 29 个明确不支持的接口

摘要

做网关时我一开始想"给每个上游接口写一个适配器",列完清单就停住了:embeddings、moderations、images、audio 这些接口的内容我根本不需要理解,原样转发就行。对它们写适配器等于为每个接口维护一套可能与上游产生分歧的结构体。这篇讲这条路——代码只有几十行,但有几个我认为值得说的决定。

两轮系统性审查:哪些债我修了,哪些我明确决定不修

摘要

我给自己做了一次代码审查,然后否决了它的大部分方案。因为那份清单是在没有读全部代码的情况下写的——比如它说我 domain/ 目录"名不副实",但那条不符合我在仓库规范里定义的准入标准。这篇讲逐条复核的过程:哪些我认了、哪些我否了、哪些我决定什么都不做。

对账:七类核对、五种容差,以及“只报告不修复”为什么是对的

摘要

我第一版的对账逻辑很简单:把钱包余额和账本净额比一下,不一致就报出来。跑起来之后几乎每个用户都进了异常列表——因为我没想清楚"余额"到底是什么。这篇讲七类核对分别在查什么、为什么它们的容差不一样,以及我为什么坚持让它只报告不修复。

订阅账号治理:多副本同时跑定时任务,怎么保证不出错

摘要

订阅账号会遇到各种"暂时不能用":被限流、额度用完、凭据过期。这些状态都要有退出机制。自然的做法是定时扫一遍把该恢复的恢复——但 channel-service 有多个副本,于是四个副本会同时扫。这篇讲我怎么把"多副本并发"当成默认前提来设计这两个 sweeper。

不用消息队列也能做事件驱动:路由失效的 Outbox 实践

摘要

第 12 篇讲了三层缓存却留了个尾巴:缓存什么时候该失效。纯 TTL 的答案是"一个被禁用的 token 最多还能用 5 分钟",我不太能接受。这篇讲我最后怎么做的——没有引入消息队列,而是用了一张表加 Redis Streams,以及这条路上踩到的几个坑。

订阅额度:为什么我要同时维护两套完全不同的账

摘要

我最初以为"额度"只有一个概念:用户买了套餐,每月能用多少,超了就停。写下去才发现上游也在算额度,而且它算的跟我不一样——它报百分比,我记美元;它按自己的 5h/7d 窗口,我按用户的日/周/月。这篇讲我最后怎么同时维护这两套账,以及它们各自最容易出错的地方。

一个适配器怎么覆盖二十家 provider:上游接入的三种形态和两条边界

摘要

接上游 provider 这件事,我一开始以为是"一家一个适配器"。写到第十家才发现,真正需要单独写协议转换逻辑的只有四家,其余二十多家全是同一个适配器的不同配置。这篇讲这个拆分怎么形成的,以及我在接外部地址时踩到的两条边界——SSRF 防护和流式超时,两条我都做错过。

熔断、超时和降级:跨服务调用的那些保护,以及我接错了的地方

摘要

有一批请求要的模型在所有渠道上都没配置,channel-service 全部返回"没有可用渠道"。这个错误跨 gRPC 回来是 codes.Unknown,而我的熔断器把它算作失败——于是用户传错一个模型名,整个网关的流量都被熔断器拒了。这篇讲这次之后我怎么重排错误分类,以及跨服务降级的其他几处取舍。

订阅账号池:把一个账号“用满”和“用坏”分开处理

摘要

并发一上来,上游返回 429,而我当时的逻辑把 429 当失败,重试几次就把一个完全健康的账号标记成不健康、从候选里摘掉。这就是"用满"和"用坏"没分开的后果。这篇讲我最后怎么在限流器、冷却时长、重试策略、健康记录、指标标签每一处把它们分开。

一次请求该走哪个渠道:我的选路与重试是怎么长出来的

摘要

选路这件事比我想的难。优先级相同的渠道怎么分流量、重试该换谁、换的时候会不会误伤别的渠道、授权要不要再验一次、一个渠道坏到什么程度该被摘掉——这篇按我实际动手的顺序讲,中间有两次返工,还有一个后来 review 时自己揪出来的、后果比较严重的 bug。

一次 /v1/chat/completions 从进来到落账,中间发生了什么

摘要

从一次我自己撞的墙讲起:网关把上游的 4xx 改写成了 502,而终止分支缺日志,我完全看不到发生了什么。顺着这个坑,我把 /v1/chat/completions 这条链路从头到尾捋了一遍,这篇记录其中的选择——哪些是当初顺手写下的、哪些是返工回来的、哪些我到现在也还没底。

一个 2400 行的 http.go,是怎么被我拆成 9 个服务的

摘要

这个项目还没上线。我在开发过程中把结构翻了一遍,而且方向还相反:第一次是把一个 2391 行的 internal/relay/server/http.go 拆开,第二次是把拆出去的九个服务收进 Kratos 大仓结构、抽出共享层,并写脚本卡住服务之间互相 import。这篇讲哪些线是我真的按需要切的,哪些只是抄了微服务的目录形状。