13-模块二-架构基础功 第13讲-系统设计实战 - 如何设计一个日处理 10 万 PR 的代码审核平台

2 阅读20分钟

模块二-架构基础功 | 第13讲:系统设计实战 - 如何设计一个日处理 10 万 PR 的代码审核平台

开场:把面试题变成交付物,而不是背八股

系统设计的价值不在于“画一张看起来很厉害的架构图”,而在于你能把 需求 → 约束 → 估算 → 权衡 → 演进路线 串成一条可执行的决策链。本讲以 CodeSentinel 为主线,用接近 系统设计面试 的叙述方式,演练如何在 日处理 10 万 PR 的量级下,仍然保持成本可控、延迟可接受、LLM 调用可治理。你会看到:功能需求与非功能需求如何分开写;如何做 背板估算 让团队对机器规模心里有数;为什么 队列 + 水平扩展 Worker 往往比同步堆 API 线程更稳;以及 限流、缓存、批处理 在 AI 审核场景里如何决定盈亏。

本讲假设你已经理解模块二前几讲的分层与一致性取舍:权威状态仍在关系型数据库,派生索引与报表允许滞后。这里我们把镜头拉远,讨论 多服务、多队列、多副本 下的吞吐与弹性。完成本讲后,你应能对外解释:我们的峰值 QPS 大概是多少、Worker 需要多少核、向量索引写入带宽来自哪里、LLM 预算如何不被突发流量击穿。下一讲我们将回到代码结构本身,讨论重构与坏味道;本讲先确保“系统能撑住”。你也可以把本讲当作 架构师面试自述:用 CodeSentinel 的真实约束讲清 trade-off,而不是背诵通用模板。

把“10 万 PR/天”当作训练基准,是因为它强迫你做 全链路乘法:不是只看 API 网关,而是把 diff 大小、检查项数量、嵌入维度、重试倍数 全部算进去。很多团队在引入大模型后出现的成本失控,本质上是把 同步链路 当成默认,而没有用队列做 负载整形(load leveling)。CodeSentinel 的架构师要把这一点写成显式原则:API 负责受理与查询,重计算进 Worker,观测贯穿两者。下面先用全局架构图把组件关系钉住,再进入估算与代码。

面试场景里,面试官常会追问“瓶颈在哪里”。对本讲架构,最常见的真实瓶颈依次是:LLM 供应商限额向量索引写入带宽Git/对象存储读取PostgreSQL 写入热点(例如全局计数器或单表索引不当)。你的回答不要泛泛说“可以加机器”,而要指出 哪一类资源不可线性扩展,以及你们如何用 分片、批处理、缓存、降级 缓解。CodeSentinel 若把“每次审核都全量 embedding 全仓库”,那成本曲线会离谱;更合理的通常是 增量变更 + 受影响文件子集 + 近邻检索,把计算聚焦在风险最高的差异上。


全局视角:CodeSentinel 大规模拓扑与数据流

第一张图给出 负载均衡 → 网关 → 服务 → 队列 → Worker → 存储 的教学版拓扑。实际生产会再拆分租户、多区域与专线,但主干逻辑一致。

flowchart TB
  LB["负载均衡 / Ingress"]
  GW["API Gateway<br/>(鉴权/配额/路由)"]
  API["FastAPI 控制面"]
  Q["Redis Streams<br/>或 Kafka"]
  W1["Review Worker"]
  W2["Review Worker"]
  WN["Review Worker ..."]

  PG[("PostgreSQL<br/>元数据权威")]
  RD[("Redis<br/>缓存/队列/限流")]
  VDB[("Vector DB<br/>代码嵌入索引")]
  OBJ[("对象存储<br/>补丁快照")]

  LB --> GW --> API
  API --> PG
  API --> RD
  API --> Q
  Q --> W1 & W2 & WN
  W1 & W2 & WN --> PG
  W1 & W2 & WN --> VDB
  W1 & W2 & WN --> OBJ
  W1 & W2 & WN --> RD

第二张图描述 一次 PR 审核的数据流:从入队到落库、再到异步索引与缓存回填。

flowchart LR
  A["客户端提交 PR 审核"] --> B["API 写入 review 记录"]
  B --> C["投递队列消息"]
  C --> D["Worker 拉取"]
  D --> E["拉取 diff / 构建上下文"]
  E --> F["规则检查 + LLM 调用"]
  F --> G["持久化 findings"]
  G --> H["发布完成事件"]
  H --> I["索引 upsert"]
  I --> J["缓存失效/回填"]

第三张图强调 水平扩展与背压:Worker 数量随队列深度弹性伸缩,网关层用 令牌桶 限制 LLM 调用提交速率,避免把第三方 API 与内部预算同时打爆。这里刻意把“扩容”画在队列深度驱动上,是要强调:CPU 低但 lag 高 时,盲目加 API 副本往往无效;正确动作是加 Worker 或修消费逻辑。

flowchart TB
  subgraph Control["控制面弹性"]
    HPA["队列深度指标"] --> SCALE["扩容 Worker"]
  end

  subgraph Protect["保护面"]
    RL["全局限流 / 租户配额"]
    CB["熔断与半开探测"]
    DLQ["死信队列 + 人工介入"]
  end

  Q2["任务队列"] --> HPA
  API2["提交接口"] --> RL
  W["Worker 执行"] --> CB
  W --> DLQ

核心原理:需求、估算、存储与扩展策略

1. 需求分层:先写清“必须”和“最好”

功能需求(示例):创建审核、查询状态、拉取 findings、按仓库策略触发检查、对接 CI、生成报告导出。

非功能需求(示例):峰值 10 万 PR/天;P95 入队延迟 < 200ms;审核完成时间 P95 < N 分钟(由检查项与模型决定);可用性 99.9%;多租户隔离;审计可追溯;成本上限可配置。

2. 背板估算:把乘法写在白板上

估算的第一步不是拿计算器,而是 统一单位:PR/天、PR/秒、checks/PR、tokens/check、美元/百万 tokens。团队常在“千 tokens”和“百万 tokens”之间混用,导致预算差三个数量级。第二步是把 峰值 定义成可检验的:例如“工作日 10:00-12:00 的 P99 入站速率”,而不是拍脑袋“10 倍”。第三步是为每一项假设标注 置信度:来自日志的是高置信,来自访谈的是中置信,来自猜测的必须标红并在 PoC 里验证。

假设:

  • 10 万 PR/天 ≈ 1.16 PR/s 平均;峰值按 10× 估算 ≈ 12 PR/s 入站(仍可能更高,需按业务活动校准)。
  • 每 PR 平均 500 行变更(教学假设),每行 tokenizer 后按 1.2 token 粗算 ≈ 600 tokens;再加系统提示与上下文,可能到 8k~32k tokens/PR(取决于你是否全量塞入模型窗口)。
  • 每 PR 3 类检查(静态规则、相似度检索、LLM 深度评审各算一类),其中 LLM 检查可能是 1~3 次调用

则 LLM 调用量粗算:10 万 × 2(中位)≈ 20 万次/天;峰值再乘倍数。若每次平均 6k tokens 输入 + 1k 输出,则 token/天费用 可直接映射到预算。架构师必须把这个表给财务与产品看,否则“智能审核”会变成不可控支出。

3. 为什么队列是核心:同步链路的隐藏二次成本

同步执行审核会把 长尾延迟 直接暴露给 HTTP 客户端:Git 拉取慢、对象存储慢、模型慢,任何抖动都会放大为超时重试,进而放大为 重复任务。队列把计算从请求线程剥离,使 API 只做 受理 + 持久化 + 排队,用户体验改为 轮询/推送进度,系统获得 削峰填谷 能力。

4. 数据库与存储选型:PostgreSQL 仍然是元数据真相源

  • PostgreSQLreviewsfindingsjobstenantspolicies 等强一致实体。
  • Redis:队列(Streams)、去重键、分布式锁、热点缓存、限流计数器。
  • Vector DB:代码块嵌入与相似缺陷检索;与上讲一致,属于 最终一致 派生视图。
  • 对象存储:大 diff、附件、报告快照,配合预签名 URL。

5. Worker 扩展:无状态、可抢占、可重入

Worker 进程应尽量 无本地状态:从队列读出 job_id,加载任务描述,执行,写回结果,ack 消息。执行过程必须 幂等(见上一讲)。Kubernetes 上按 队列 lagCPU 利用率 组合扩容,避免单指标误判。

6. LLM 限流与节流:保护预算与保护供应商

全局限流 + 租户配额 + 关键字成本标签(不同模型不同桶)。对可延迟任务使用 延迟队列,在预算耗尽时降级到规则-only 模式并标记报告“未启用深度模型”。

7. 缓存策略:结果缓存与嵌入缓存

  • 审核结果缓存:键可设为 (repo_id, commit_sha, policy_pack_version, checks_version),任一变化即失效。
  • 嵌入缓存:键为 (file_hash, chunk_span, embedding_model_version),命中率高时能显著降本。

8. 观测与 SLA:没有指标就没有“可扩展”

至少监控:入队速率、队列深度、消费延迟、Worker 错误率、LLM 429/5xx、token 用量、索引滞后、缓存命中率。把 SLO burn rate 接入告警,比单纯“CPU 高”更有效。

9. 多租户公平性:避免“大租户吃掉队列”

公平调度策略包括:每租户并发上限、加权轮询、单独队列与 Worker pool(贵客隔离)。CodeSentinel 若服务开源社区与企业客户,隔离是商业化底线。

10. 演进路线:从单区域到多区域

第一阶段单区域 + 多副本;第二阶段只读副本与就近缓存;第三阶段事件跨区复制与冲突解决(成本高)。架构评审要把阶段边界写清,避免一开始就做全球强一致幻想。

11. 安全与合规:大规模下的放大效应

10 万 PR/天意味着密钥轮换、审计日志、供应链检查都必须是 自动化 的。API Gateway 统一 OAuth、mTLS、内部服务 JWT;敏感代码片段进入模型前要执行 脱敏策略(与数据治理模块联动)。

12. 与 Clean Architecture 的对齐:扩展的是部署,不是领域规则

领域规则仍应在内核;扩展 Worker 只是增加 基础设施适配器 的实例数。不要在 Worker 里复制一份“临时业务规则”,否则多实例部署会变成 规则漂移灾难

13. 背板估算进阶:把“平均”拆成分布,而不是一句口号

真实 PR 的 diff 体积往往呈 长尾:大多数很小,但少数巨型 PR 会拖垮 Worker。架构上要对 pr_linespatch_bytes分层路由:小 PR 走快路径(更便宜的模型与更少的检索),大 PR 走慢路径(分块、MapReduce 式汇总、甚至人工确认)。否则你的估算是按平均值成立的,但线上是按 P99 崩的。CodeSentinel 可以在入队消息里携带 size_tier 字段,由网关根据统计阈值计算。

14. API 形态:同步、异步与 Webhook 的组合拳

企业客户常要求 CI 阻塞式 结果(同步),而平台内部仍应 异步执行:做法是网关接受同步请求后立即创建任务,客户端以 长轮询 / Webhook / SSE 获取最终结果。把“外部协议”和“内部执行模型”解耦,是规模化的关键。否则你会为了兼容 Jenkins 插件而把重计算塞回 HTTP 线程池。

15. 数据一致性与搜索体验:大规模下的产品策略

当索引滞后从秒级变分钟级,产品必须提供 可解释的进度:例如“深度检索同步中(预计 2 分钟)”。这与第12讲的读写分层一致,但在 10 万 PR/天时会变成 客服工单结构问题,需要提前设计 FAQ 与状态机文案。

16. 费用归因:让每一次 LLM 调用都能追溯到租户与仓库

tenant_idrepo_idreview_idcheck_namemodeltoken_in/out 写入 不可变计费流水(可存 PostgreSQL 分区表或低成本列存)。没有归因,你无法做公平限流,也无法向大客户解释账单。架构师要在第一版就预留字段,而不是等财务介入再改表。

17. 弹性与冷启动:队列瞬间堆积时 Worker 扩容的上限

云厂商扩容不是瞬时完成的,且镜像拉取、JIT、连接池预热都会吃时间。策略包括:最小副本数 > 0提前按日历扩容(大型活动前)、队列随机抖动入队(避免同一秒爆炸)。CodeSentinel 若对接开源活动(Hackathon),没有日历扩容几乎必炸。

18. 测试数据与合成流量:用“可控洪水”验证限流

在生产影子环境用真实大小的 patch 回放,验证令牌桶参数、Worker 并发、数据库连接池是否匹配。别只在开发机用 10 行 diff 测功能,那测不出规模。

19. 与 LangChain 的关系:框架是工具链,不是性能承诺

LangChain 适合编排与工具调用,但大规模下你要关注 每步阻塞点:串行工具调用会把吞吐压扁。应把可并行部分(多个文件 embedding)并行化,同时用信号量限制并行度,避免把向量库与 LLM 同时打到极限。

20. 设计评审清单(可直接贴到 Confluence)

需求是否区分峰值与平均?是否定义完成口径(落库 vs 可搜)?是否定义降级模式?是否定义租户隔离?是否定义计费归因?是否定义死信处理?是否定义密钥轮换?只要有一项空白,就不该进入“拍脑袋买机器”的阶段。


代码实战:基于 Redis Streams 的审核 Worker 与令牌桶限流

下面给出 可在本机运行 的精简示例:TokenBucketLimiter 用于保护 LLM 调用;ReviewWorker 从 Redis Stream 消费任务并模拟审核;enqueue_review_job 模拟 API 入队。你需要本地 Redis,并安装 redis 包。

文件:requirements.txt

redis>=5.0.0

文件:rate_limiter.py

from __future__ import annotations

import time
from dataclasses import dataclass

import redis


@dataclass(frozen=True)
class RateLimitResult:
    allowed: bool
    retry_after_ms: int


class TokenBucketLimiter:
    """
    分布式令牌桶(Redis + Lua):
    - key: rl:llm:global
    - 每秒 refill rate,burst 容量 cap
    生产环境应分租户分模型分桶。
    """

    def __init__(self, client: redis.Redis, key: str, rate_per_sec: float, burst: int) -> None:
        self._r = client
        self._key = key
        self._rate = float(rate_per_sec)
        self._burst = int(burst)

    _lua = """
    local key = KEYS[1]
    local now = tonumber(ARGV[1])
    local rate = tonumber(ARGV[2])
    local burst = tonumber(ARGV[3])
    local requested = tonumber(ARGV[4])

    local data = redis.call("HMGET", key, "tokens", "ts")
    local tokens = tonumber(data[1])
    local ts = tonumber(data[2])

    if tokens == nil then
      tokens = burst
      ts = now
    end

    local delta = math.max(0.0, now - ts)
    tokens = math.min(burst, tokens + delta * rate)

    if tokens < requested then
      local missing = requested - tokens
      local retry_after_ms = math.ceil((missing / rate) * 1000)
      redis.call("HSET", key, "tokens", tokens, "ts", now)
      return {0, retry_after_ms}
    end

    tokens = tokens - requested
    redis.call("HSET", key, "tokens", tokens, "ts", now)
    return {1, 0}
    """

    def consume(self, tokens: int = 1) -> RateLimitResult:
        now = time.time()
        allowed, retry_after_ms = self._r.eval(self._lua, 1, self._key, now, self._rate, self._burst, tokens)
        return RateLimitResult(bool(allowed), int(retry_after_ms))


def mock_llm_call() -> str:
    return "finding: possible N+1 query in repository layer"

文件:worker.py(消费循环 + 简易限流等待)

from __future__ import annotations

import json
import time
import uuid
from dataclasses import dataclass
from typing import Any, Dict, Optional

import redis

from rate_limiter import RateLimitResult, TokenBucketLimiter, mock_llm_call

STREAM_KEY = "codesentinel:review:jobs"
GROUP = "review-workers"
CONSUMER = f"worker-{uuid.uuid4().hex[:8]}"


@dataclass(frozen=True)
class ReviewJob:
    review_id: str
    tenant_id: str
    pr_lines: int
    checks: int


class ReviewWorker:
    def __init__(self, client: redis.Redis) -> None:
        self._r = client
        self._limiter = TokenBucketLimiter(
            client,
            key="rl:llm:global",
            rate_per_sec=5.0,  # 教学:每秒 5 次 LLM(全局)
            burst=10,
        )

    def ensure_group(self) -> None:
        try:
            self._r.xgroup_create(STREAM_KEY, GROUP, id="0-0", mkstream=True)
        except redis.ResponseError as exc:
            if "BUSYGROUP" not in str(exc):
                raise

    def enqueue(self, job: ReviewJob) -> str:
        payload: Dict[str, Any] = {
            "review_id": job.review_id,
            "tenant_id": job.tenant_id,
            "pr_lines": job.pr_lines,
            "checks": job.checks,
        }
        msg_id = self._r.xadd(STREAM_KEY, {"payload": json.dumps(payload, ensure_ascii=False)})
        return msg_id.decode("utf-8") if isinstance(msg_id, (bytes, bytearray)) else str(msg_id)

    def _handle_job(self, job: ReviewJob) -> None:
        # 每个 PR 假设触发 checks 次 LLM(教学)
        for i in range(job.checks):
            while True:
                res = self._limiter.consume(1)
                if res.allowed:
                    break
                time.sleep(max(res.retry_after_ms, 1) / 1000.0)
            _ = mock_llm_call()
        # 这里应写入 PostgreSQL;示例用 Redis 哈希演示落库效果
        self._r.hset(f"review:{job.review_id}", mapping={"status": "completed", "tenant": job.tenant_id})

    def run_forever(self, block_ms: int = 5000) -> None:
        self.ensure_group()
        while True:
            resp = self._r.xreadgroup(
                GROUP,
                CONSUMER,
                streams={STREAM_KEY: ">"},
                count=5,
                block=block_ms,
            )
            if not resp:
                continue
            for _stream, messages in resp:
                for msg_id, fields in messages:
                    raw = fields.get(b"payload") or fields.get("payload")
                    if isinstance(raw, bytes):
                        raw_s = raw.decode("utf-8")
                    else:
                        raw_s = str(raw)
                    data = json.loads(raw_s)
                    job = ReviewJob(
                        review_id=data["review_id"],
                        tenant_id=data["tenant_id"],
                        pr_lines=int(data["pr_lines"]),
                        checks=int(data["checks"]),
                    )
                    try:
                        self._handle_job(job)
                        self._r.xack(STREAM_KEY, GROUP, msg_id)
                    except Exception:
                        # 生产:进入 DLQ / 重试计数 / 告警
                        raise


def demo() -> None:
    r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=False)
    w = ReviewWorker(r)
    w.ensure_group()
    for i in range(20):
        jid = w.enqueue(
            ReviewJob(
                review_id=f"rev-{i}",
                tenant_id="tenant-a" if i % 2 == 0 else "tenant-b",
                pr_lines=500,
                checks=3,
            )
        )
        print("enqueued", jid)
    # 注意:demo 只入队;消费请运行 `python worker.py consume` 或调用 run_forever


if __name__ == "__main__":
    import sys

    r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=False)
    worker = ReviewWorker(r)
    if len(sys.argv) > 1 and sys.argv[1] == "consume":
        worker.run_forever()
    else:
        demo()

如何把示例映射到 CodeSentinel 真系统

  • API 层POST /reviews 写入 PostgreSQL 后 XADD,返回 202review_id
  • Worker 层:拉取后执行 规则引擎端口LLM 端口;限流键细化为 rl:llm:{tenant}:{model}
  • 缓存层:对 (repo, sha, policy_version) 做结果短路;miss 才进入完整链路。
  • 观测层:在 consume 前后打点,记录等待令牌耗时与执行耗时。

代码阅读提示:别忽略 ACK 与重试的边界

XREADGROUP 未 ACK 的消息会进入 Pending Entries List,需要 XPENDING/XCLAIM 治理。教学示例为短代码直接 raise,生产必须引入 最大投递次数死信流。否则队列会在“半成功”状态堆积,表现为 lag 高但 CPU 低 的经典症状。

与上一讲幂等的关系

同一条 review_id 可能被重复入队(客户端重试、网关重发)。Worker 应在落库 findings 时使用 唯一约束幂等键,把重复执行变成 可证明的无害

本地运行步骤(建议照做一遍)

  1. 启动 Redis:docker run --rm -p 6379:6379 redis:7
  2. rate_limiter.pyworker.py 放在同一目录,创建虚拟环境并 pip install redis
  3. 先运行 python worker.py 入队 20 条任务;再开第二个终端运行 python worker.py consume,观察令牌桶如何把 LLM 调用限制在每秒 5 次附近(可用日志或打印验证)。
  4. checks 调到 10 并快速入队 200 条,观察队列深度上升;思考若增加 Worker 副本,是否会线性提升吞吐直到触及 LLM 限额。

从 Redis Streams 迁移到 Kafka 的触发条件

当需要 更强保留策略更复杂重放跨团队多消费者生态 时,Kafka 更常见。代价是运维复杂度上升。教学示例用 Streams 是为降低门槛;架构评审要写的是 触发条件 而不是“Kafka 更酷”。

网关限流 vs Worker 限流:两层缺一不可

网关限流保护 入口 与被刷接口风险;Worker 限流保护 下游 与预算。只做一个会在另一侧失守。CodeSentinel 建议网关按租户做 并发连接 + QPS,Worker 侧按模型做 token/分钟 估算限流(可把 token 近似为调用次数乘以经验系数)。


生产环境实战:从演示到可运维平台

  1. 容量规划表:把 PR/天、峰值倍数、每 PR checks、token 单价写入表格,每月复盘偏差。

  2. 配额策略:企业租户购买“每日 token 包”,耗尽后自动降级并通知负责人。

  3. 隔离策略:大客户独立队列与 Worker Deployment,避免噪声邻居。

  4. 数据局部性:Worker 与 Git 镜像、对象存储同区域,降低拉取延迟。

  5. 回归测试:对关键规则集做 金丝雀 PR 集,每日跑一遍验证引擎版本升级无回归。

  6. 故障演练:随机杀 Worker Pod,验证 PEL 回收与任务最终完成率。

  7. 成本告警:token 用量 15 分钟环比异常 + 队列深度异常 → 可能是重试风暴或遭刷接口。

  8. API 防刷:网关层人机验证、签名、IP 信誉;内部服务 mTLS。

  9. 模型路由:小模型做初筛,大模型做深度;把路由策略配置化,避免改代码热修复。

  10. 合规:代码出境、模型托管区域、客户合同约束要在架构图上有明确边界。

  11. 备份与恢复:PostgreSQL PITR、对象存储版本控制、向量索引重建流程要写进 Runbook;否则“元数据回来了,但向量全丢”会让搜索侧长期不可用。

  12. 配置治理policy_pack_version 与规则集发布要走 CI,不允许 Worker 本地硬编码;否则多副本会出现 不同 Worker 不同规则 的隐性分区。

  13. 发布策略:Worker 与 API 版本握手(兼容旧消息格式),用 双写/双读 过渡期处理 schema 演进;禁止“停全世界升级”。

  14. 客户可见 SLA:把“完成”定义成数据库状态还是报告生成完成,要写进合同;否则销售与工程会对同一指标各自解释。

  15. 绿色节能与成本:非高峰自动缩容到最小副本,配合 Cron 把批处理任务放到低价时段(若合规允许)。

  16. 连接池与文件描述符:1 万个 Worker 副本并不等于更快;过多并发会把 PostgreSQL 连接打满。要用 池化、批处理写入、异步提交,并在架构层明确“最大安全并发”。

  17. 灰度与特性开关:新检查项默认影子模式(只记录不阻断),通过采样比例逐步放大,避免新规则导致队列全局堆积。

  18. 客户集成节奏:Webhook 投递失败重试要指数退避并设置上限,避免对客户系统造成 DDoS 观感;同时提供签名与时间戳防重放。

  19. 供应链安全:依赖包扫描、SBOM、镜像漏洞扫描在大规模平台里是基础项;否则一次供应链事件会放大成大规模数据外泄风险。

  20. 组织配套:SRE、平台、算法、数据、合规的 RACI 表要与架构图同步更新;系统能扩展但组织推诿会让平台停在纸面。


本讲小结:大规模审核平台思维导图

mindmap
  root((10万PR天))
    需求
      功能
      非功能SLO
    估算
      PR速率
      token乘法
      峰值倍数
    架构
      网关保护
      队列削峰
      Worker水平扩展
    数据
      PostgreSQL权威
      Redis队列缓存限流
      VectorDB派生索引
    治理
      租户隔离
      成本配额
      观测告警

延伸阅读:把本讲输出成团队对齐文档

建议你课后整理一份 一页纸架构(One-pager):左侧写用户故事与 SLO,中间画数据流,右侧写风险与降级。再附一张 估算表:峰值 PR/s、每 PR token、每日费用区间、Worker 副本区间。文档的价值在于让产品、工程、财务对同一套数字讨论;否则“10 万 PR”只会变成口号。另建议把 队列深度LLM 429 比例 绑定为发布门禁:新版本若让重试放大,应在预发环境就被拦住,而不是靠线上用户投诉发现。

如果你需要向管理层解释“为什么必须上队列”,可以用一句非常朴素的话:同步系统把不确定性留给用户超时,异步系统把不确定性转化为可观测的队列深度。CodeSentinel 的用户可以接受“审核需要几分钟”,但很难接受“网关随机 504”。因此本讲的工程结论往往比具体中间件更重要:先把执行模型做对,再争论 Redis 还是 Kafka。最后提醒一句:任何估算都要标注假设来源(客户访谈、历史日志、PoC 压测),否则数字会在复盘时被一票否决。把假设写出来,不是示弱,而是让团队能一起把误差缩小;这也是专业工程沟通的一部分。


思考题

  1. 若 LLM 供应商突发限流(429),你会优先牺牲吞吐还是牺牲延迟?如何让用户可感知?

  2. 结果缓存键包含哪些版本字段才能避免“错缓存导致误判”?

  3. 单队列 vs 多队列(按租户/优先级)各有什么运维代价?

  4. 如果把审核结果缓存从 Redis 换成 CDN 边缘缓存,会带来哪些一致性与安全问题?


下一讲预告

下一讲我们进入 代码重构的艺术:面对 AI 生成代码常见的“上帝类、长方法、霰弹式修改”等坏味道,如何用 安全重构手法CodeSentinel 重构建议引擎 把质量拉回可控区,并用测试网兜住风险。那里我们会更多讨论“如何让机器提出改动、让人类负责合并”,与本讲的“如何让系统扛住流量”形成互补:性能与成本解决能不能跑,重构与评审解决跑得好不好