AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

容量、并发、缓存与 FinOps

我的学习 →

正在读取学习记录…

资料助手每秒收到10个问题,每次模型调用平均占用2秒。容量表示系统在约定质量和响应时间下能承担多少业务;并发表示某一时刻正在处理多少请求;缓存保存可复用结果,让部分请求直接取得答案。FinOps是把资源费用与业务使用关联起来,让技术、财务和业务一起决定投入与收益。

先看正常输入:缓存命中率50%,10次/秒中有5次需要模型,每次占用2秒,平均有10次模型调用在进行。模型入口提供12个并发槽位,这组平均输入可装入;如果缓存完全不命中,平均在途增到20,就超过12。

把请求率、耗时和成本放在同一张纸上

λ 表示每秒需要模型的请求数,W 表示平均占用秒数,L 表示平均在途调用数。在稳定、统计窗口一致的条件下,用 Little 定律 L = λ × W 估算平均并发。

就地代入:总请求率10次/秒,命中率0.50,模型率 λ = 10 × (1 − 0.50) = 5次/秒;L = 5 × 2 = 10。若结果为10.2,槽位数向上取整为11。平均值用于初步规划,突发流量和尾部耗时仍需预留空间并压测。

容量、并发、缓存与 FinOps机制图
每格表示一个并发槽位。50%缓存命中时需要10格,无缓存时需要20格;超过12个槽位的部分标红。

图按这组固定输入绘制:12个容量格中10格被平均在途占用;无缓存时需要20格,其中8格超出限制。颜色表达计算关系,数字来自输入公式。

命中率模型请求率2秒平均耗时下在途数1000请求的假设费用
0%10次/秒2010.00元
50%5次/秒105.00元
100%0次/秒00.00元

费用列假设每次未命中调用0.01元:1000 × (1 − 0.50) × 0.01 = 5元。真实模型按输入、输出、缓存等项目计价时,应改成各项数量乘对应单价;存储、服务器和日志费用再分别加入。

缓存命中之前先完成授权

缓存键是查找结果的标识。只用问题文本做键,会让“报销上限”在不同租户间共用答案。示例键包括租户、文档版本、模型、提示词版本和问题。权限改变后应失效缓存或加入权限版本;查缓存之前仍要核验当前身份和资源访问权限。

Google SRE 的过载处理讨论了在容量不足时控制进入系统的工作量。缓存之外还需要并发限制、有界队列、超时、降级和公平分配。否则一次模型拥堵可能通过无限重试扩散到整个服务。

建立新文件夹 l8-capacity,保存 capacity_plan.py 和 test_capacity_plan.py。程序用十进制数计算费用,用向上取整计算槽位,不进行真实模型调用或计时。

先在 PowerShell 创建并进入目录,再保存下方两份文件:

PowerShell
New-Item -ItemType Directory -Force l8-capacity
cd l8-capacity

macOS 终端:

Bash
mkdir -p l8-capacity
cd l8-capacity
Python
"""用固定业务输入计算并发与费用;没有计时或真实模型调用。"""
from decimal import Decimal
from math import ceil

HIT_RATE = Decimal("0.50")

def plan(rate, latency, hit_rate, slot_limit):
    if rate < 0 or latency < 0 or not Decimal("0") <= hit_rate <= Decimal("1"):
        raise ValueError("速率、耗时非负,命中率在0到1之间")
    if slot_limit <= 0:
        raise ValueError("并发上限必须为正")
    # 仅未命中流量消耗模型并发,平均在途数为速率乘耗时。
    model_rate = rate * (1 - hit_rate)
    in_flight = model_rate * latency
    slots = ceil(in_flight)
    return model_rate, in_flight, slots, slots <= slot_limit

def cost(requests, hit_rate, call_price):
    # 单价是教学假设,每次未命中固定一次调用。
    return Decimal(requests) * (1 - hit_rate) * call_price

def cache_key(tenant, document_version, model, prompt_version, question):
    # 租户与版本参与键,授权仍须在查缓存之前完成。
    return tenant, document_version, model, prompt_version, question

def main():
    rate, in_flight, slots, fits = plan(Decimal("10"), Decimal("2"), HIT_RATE, 12)
    print(f"模型请求: {rate:.2f}次/秒")
    print(f"平均在途: {in_flight:.2f} / 向上取整{slots}个并发")
    print(f"12个槽位: {'容纳' if fits else '不足'}")
    print(f"1000次请求假设费用: {cost(1000, HIT_RATE, Decimal('0.01')):.2f}元")
    print("租户隔离:",
          cache_key("A", "v1", "m1", "p1", "报销") !=
          cache_key("B", "v1", "m1", "p1", "报销"))

if __name__ == "__main__":
    main()

测试文件 test_capacity_plan.py:

Python
"""覆盖全命中、无命中、容量边界、版本隔离与费用计算。"""
from decimal import Decimal as D
import unittest
from capacity_plan import plan, cost, cache_key

class CapacityTests(unittest.TestCase):
    def test_baseline(self):
        self.assertEqual(plan(D(10), D(2), D("0.5"), 12), (D(5), D(10), 10, True))

    def test_zero_and_full_hit(self):
        self.assertEqual(plan(D(10), D(2), D(0), 12)[2:], (20, False))
        self.assertEqual(plan(D(10), D(2), D(1), 12)[2:], (0, True))

    def test_capacity_boundary(self):
        self.assertTrue(plan(D(6), D(2), D(0), 12)[3])
        self.assertFalse(plan(D("6.1"), D(2), D(0), 12)[3])

    def test_invalid_inputs(self):
        for hit in (D("-0.1"), D("1.1")):
            with self.assertRaises(ValueError):
                plan(D(10), D(2), hit, 12)
        with self.assertRaises(ValueError):
            plan(D(10), D(2), D(0), 0)

    def test_cost_and_version_key(self):
        self.assertEqual(cost(1000, D("0.5"), D("0.01")), D("5"))
        self.assertNotEqual(cache_key("A", "v1", "m1", "p1", "q"),
                            cache_key("A", "v2", "m1", "p1", "q"))

if __name__ == "__main__":
    unittest.main()

文件保存后,在同一窗口核对文件名并运行:

Bash
ls
python -X utf8 capacity_plan.py
python -X utf8 -m unittest -v test_capacity_plan.py

dir应显示这两个文件。运行程序后得到下面输出;测试末尾应显示 OK。

程序输出:

TXT
模型请求: 5.00次/秒
平均在途: 10.00 / 向上取整10个并发
12个槽位: 容纳
1000次请求假设费用: 5.00元
租户隔离: True

将缓存命中率改成0

在 capacity_plan.py 找到 HIT_RATE = Decimal("0.50"),把这一行改成 HIT_RATE = Decimal("0.00")。保存并运行:

Bash
python -X utf8 capacity_plan.py

模型请求变成10.00次/秒,平均在途20.00,需要20个槽位,12个槽位显示“不足”;1000次请求的假设费用变成10.00元。租户隔离仍为True。恢复 HIT_RATE = Decimal("0.50"),再次运行回到10个并发与5元。

这也说明缓存失效会同时改变容量和成本。部署新文档版本时,大量旧结果可能不能复用;应把低命中率作为发布前的规划场景。

从平均计算走向运营选择

并发限制要覆盖调用全生命周期,包括流式响应结束之前。请求率限制控制单位时间内进入多少工作,并发限制控制多少工作同时占据资源,二者不能互相替代。队列可以吸收短突发,队列长度与等待期限也要限制。

按租户记录请求量、缓存命中、模型令牌与总费用,再计算每个成功任务成本。一个失败后重试三次的任务可能比一次成功任务花费更多,所以归因应关联同一个任务ID。设置预算前,先预留可能费用,完成后对账;预算用尽时拒绝新调用或进入明确的低成本模式。

观察到的业务变化优先检查可尝试的动作
请求率不变,平均耗时从2秒升到4秒模型耗时与在途数将计划并发从10重新算到20
缓存命中骤降文档、权限、提示版本变化提前预热可公开且有权限的数据
队列等待越来越长进入速率与处理速率限流并缩短过期请求的排队
费用涨而成功任务数不涨重试、无效生成与缓存键减少重复调用,按任务统计

12个槽位容纳平均10个调用只是初步规划结论。压测需要测进入速率、成功率、延迟分位数、队列和资源占用,选择能达到服务目标的工作点。下一篇将这些目标写成可计算的SLO。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.1

选择题 · 先判断边界

关于“容量、并发、缓存与 FinOps”,哪种做法更稳妥?

对应知识点:L8-capacity

选择一个答案
练习 2 / 4 · v1.0.0

简答题 · 解释失败

写出一个失败输入并说明记录字段。

对应知识点:L8-capacity

练习 3 / 4 · v1.0.0

代码题 · 边界测试

补充正常、边界、失败三组输入和预期结果。

对应知识点:L8-capacity

练习 4 / 4 · v1.0.0

场景题 · 结果未知

证据不足时如何设计状态和人工接管?

对应知识点:L8-capacity

继续阅读SLO、SLA与错误预算。

图片查看器
100%

文章配图 · 按 Esc 关闭