Part 5: Operating an LLM system: observability, cost, routing, and the platform underneath
TL;DR · AI 摘要
运营LLM系统需关注可观测性、成本控制、路由及平台支撑,确保系统稳定与可控,避免错误决策和预算超支。
核心要点
- 引入决策层指标(如agent_decisions_total)可追踪模型决策质量
- 成本控制需结合自动执行率与预算阈值,避免超支
- 统一决策ID和域名设计能提升系统可维护性与故障隔离能力
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- LLM系统运营
- 可观测性
- RED指标扩展
- 决策层监控
- 成本控制
- 自动执行率阈值
- 预算绑定
- 平台架构
- 统一决策ID
- 多模型路由
金句 / Highlights
值得收藏与分享的关键句。
标准可观测性无法反映代理决策质量,需添加决策层指标(agent_decisions_total)
自动执行率超过阈值时需触发人工审核,防止错误决策扩散
统一域名抽象层可实现跨供应商故障转移,避免单点失效
第5部分:运营LLM系统:可观测性、成本、路由与底层平台 - Stack Overflow
2026年10月8日
第5部分:运营LLM系统:可观测性、成本、路由与底层平台
您的服务可能100%正常运行,但仍可能在暗中批准错误内容、消耗预算或切换到未经验证的质量模式。第五级是让您能够观察决策、控制支出、在模型间路由和故障转移、在数秒内终止不良行为的基础设施——以及使这一切成为可能的平台。
[
这是运行生产级LLM系统的六级成熟度模型的第五级。更早的级别让系统正常运行并正确工作。第五级关注的是可操作性:您是否真的能够日常运行这个系统?是否能看到它的决策?是否能控制成本?是否能在供应商故障时存活?是否能在系统行为异常时立即停止?底层平台是否具备支持所有这些能力?
传统服务出错时会返回500错误。而LLM系统出错时可能在大规模上快速采取错误行动。这种不对称性使得可操作性在这里不是可有可无的选项,而是区别于您能运行的系统和只能寄希望于的系统的分水岭。本文是可操作性层级的具体规范:可观测性信号、成本控制、路由与故障转移、终止开关、身份模型,以及将它们整合在一起的基础设施主干。这是系列文章中最长的一篇,因为这一层级涉及最多的组件。但统一的核心思想很简单:所有模型流量必须经过一个瓶颈,所有内容必须通过一个decision_id串联,而领域层永远不知道哪个供应商做出了响应。
观察决策本身,而不仅仅是服务
标准的可观测性——请求速率、延迟、错误率、CPU——只能告诉您服务是否在线。它无法说明代理是否良好地履行了职责。您需要第二层专门针对AI的信号,以及使这些信号可操作的模式和阈值。
模型:RED + 决策层
保留您原有的RED指标(速率、错误、持续时间)用于服务。添加一个决策层,将每个代理的决策视为首要的、可衡量的事件。以下名称遵循Prometheus惯例(_total计数器、_bucket直方图),每个序列都携带tenant_id和capability作为标签(表格中为简洁起见省略,但需假设它们无处不在)。一个注意事项:直方图上的每个tenant_id标签在超过数百个租户时会导致基数爆炸——请保持租户数量可控,或在超过该数量时将每个租户的汇总数据转移到记录规则/示例中。
指标名称 类型 关键标签 说明
--------------------------------- --------- ---------------------------------------------------------------------- -------------------------------------------------
`agent_decisions_total` counter `outcome` (auto / hitl_recommended / hitl_required / reject / abstain) 总量及自动/人工处理的分布
`agent_auto_execution_ratio` gauge — 无需人工处理的百分比
`agent_decision_confidence` histogram — 组合置信度的分布情况
`agent_decision_duration_seconds` histogram `node` 每个图节点的延迟
`guardrail_blocks_total` counter `layer` (input / output / pii), `rule` 被阻止的内容及位置
`judge_invocations_total` counter — 判定模块运行次数(用于计算比率的分母)
`judge_disagreements_total` counter — 判定模块推翻主决策的次数
`llm_tokens_total` counter `direction` (in / out), `model` 令牌消耗量
`llm_cost_usd_total` counter `model` 按租户/能力分类的总成本
`llm_call_duration_seconds` histogram `model`, `outcome` 模型延迟(与服务延迟分离)
`shadow_eval_pass_ratio` gauge `slice` 生产流量抽样中的实时质量
`human_override_rate` gauge —
``四个分类在此基础上形成:决策(总量、分布、置信度、延迟)、安全(防护规则拦截、判定冲突)、成本/性能(令牌消耗、美元成本、模型延迟)、质量(影子评估、人工覆盖)。如果只能监控四项指标,建议选择 auto_execution_ratio、guardrail_blocks_total、llm_cost_usd_total 和 human_override_rate — 它们能覆盖行为、安全、成本和质量四个维度。
结构化日志:三个层级
不要将所有内容都放入单一日志流 — 按敏感度分级,因为其中部分属于监管数据,而大多数告警只需访问第一层级。
- 层级1 — 运维(可安全存储于任何位置,高吞吐量):时间戳、级别、decision_id、tenant_id、能力、node、duration_ms、outcome。这是告警查询的主要数据源。
- 层级2 — 决策元数据(在信任边界内;接近审计记录):model、prompt_version、composed_confidence、路由决策、判定结果、防护规则动作、inputs_hash 以及脱敏后的非PII摘要。
- 层级3 — 受监管/原始数据(加密存储、访问控制、不在常规日志库中):如果保留敏感负载数据 — 通常在层级2中存储哈希值+脱敏摘要,并完全跳过层级3。
规则:层级1和2可被工程师查询;层级3是保险库。decision_id 贯穿三个层级 — 并与下方的追踪信息关联 — 使您能端到端重建任意单个决策。
追踪决策图
/think
常规追踪显示跨服务的HTTP请求。代理追踪应覆盖内部流程图,以便查看哪一步骤失败及各步骤耗时:
span: agent.decision attrs: decision_id, tenant_id, capability, outcome, confidence
├─ span: entry attrs: identity, auth_type
├─ span: context_load attrs: slices_loaded, cache_hit
├─ span: llm_decision attrs: model, prompt_version, tokens_in, tokens_out, cost_usd
├─ span: output_guardrail attrs: blocks[], pii_actions[]
├─ span: judge attrs: ran, model, agreed (only when sampled in)
├─ span: routing attrs: decision=auto|hitl|reject, threshold
└─ span: exit attrs: ledger_entry_id现在"为什么决策X耗时4秒/被拒绝?"可通过单次追踪查询快速定位。
基于症状而非根本原因设置阈值告警
监控影响用户体验或业务的指标,而非原因本身。基础告警集如下:
alert condition (example) severity
--------------------- ------------------------------------------------------------------------------ ------------
AutoExecutionSwing `auto_execution_ratio` moves >15% vs 7-day baseline warning
GuardrailBlockSpike `rate(guardrail_blocks_total[5m])` > 3× trailing hr critical
JudgeDisagreementHigh rate(judge_disagreements_total[1h]) / rate(judge_invocations_total[1h]) > 0.15 critical
CostCeiling `increase(llm_cost_usd_total[1h])` > budget/24 warning→page
ShadowEvalDrop `shadow_eval_pass_ratio` < 0.95 critical
ModelLatencyP99 `llm_call_duration_seconds` p99 > 8s for 10m对安全性和质量相关告警进行页面通知;其余告警触发工单。为决策系统设置有意义的SLO指标:决策可用性(≥99.9%的决策返回方案——无法决策才是真正的故障,而非5xx错误),质量(影子评估≥基准线-2%,滚动7天),延迟(排除人工时间后p95在交互预算内),成本(每千次决策成本控制在计划值±20%内)。
三个值得特别指出的可观测性反模式:所有日志使用单一日志流(三级日志会泄露到可搜索日志——合规问题),将模型置信度作为指标(存在校准偏差;应跟踪合成置信度并验证结果),以及缺少decision_id(每次调查都变成考古)。
在瓶颈处控制成本
大语言模型成本具有隐蔽特性:直到收到账单才会显现,且会随着未监控因素增长——每次调用的token数、请求次数、重试次数、某人添加的冗长提示等。团队往往在产品上线后才发现单位经济模型完全倒置。解决方案不是更换更便宜的模型,而是通过几个结构性控制措施使成本可观察且可控——所有措施都围绕同一个瓶颈展开。
在控制点进行测量
看不见就无法控制,当模型调用分散在代码库各处时更无法看清支出。将所有调用路由通过单一网关。该组件就是测量和控制的地点:
def complete(self, req: ChatRequest) -> ChatResponse:
rate_limiter.check(req.tenant_id, est_tokens(req)) # enforce (estimate now; settle actuals below)
resp = self._adapter.complete(req)
metrics.incr("llm_tokens_total", resp.tokens_in, direction="in", model=req.model, tenant_id=req.tenant_id)
metrics.incr("llm_tokens_total", resp.tokens_out, direction="out", model=req.model, tenant_id=req.tenant_id)
metrics.incr("llm_cost_usd_total", cost(req.model, resp), model=req.model, tenant_id=req.tenant_id, capability=req.capability)
return resp现在成本可以按租户 × 能力 × 模型进行归因——这正是你定位资金消耗位置的方式(通常是某个过度使用能力或过大提示词),而不是模糊地表述为“减少AI使用”。
按收益排序的控制手段
lever mechanism typical impact
------------------------ ------------------------------------------------------------ ----------------------------------------------------------------------
**不要调用模型** 将简单/确定性场景通过代码处理 往往收益最大 —— 大量“AI成本”其实是模型在执行规则的工作
**批量处理** 一次调用处理多个项目而不是N次调用 减少往返次数,单个项目的成本更低
**缓存** 缓存确定性结果(嵌入向量、重复查询) 最便宜的调用是完全跳过的调用
**合理选择模型规模** 简单决策使用廉价模型,复杂任务使用高性能模型 收益显著 —— 大部分流量是简单任务,大部分成本来自高端模型
**精简提示词** 仅加载必要上下文;删除“以防万一”的前缀 每次调用都会产生的固定成本明确合理选择模型规模非常重要,因为它直接关联到路由策略(下一节):如果廉价模型通过了某个切片的评估,就将请求路由到该模型——每次调用成本降低5-20倍是常见的。让数学公式可见(成本 = 输入token/1000 × 输入单价 + 输出token/1000 × 输出单价)而不是猜测。
预算、速率限制与隐性乘数
按租户设置花费上限和速率限制,状态需在副本间共享——无状态工作池无法通过本地内存强制执行租户级限制,因为每个副本都会允许其自身的完整配额:
RATE = { "default": { "tokens_per_min": 200_000, "usd_per_day": 50 } } # 根据实际价格推导
def check(tenant_id, est_tokens):
limits = RATE_FOR(tenant_id)
# 增加预估的token数量,而不是1 —— 用token预算统计调用次数永远不会触发限制
if redis.incr_window(f"tok:{tenant_id}", by=est_tokens, ttl=60) > limits["tokens_per_min"]:
raise RateLimited(tenant_id)
spent = redis.get_float(f"usd:{tenant_id}:{today()}") # 由网关的调用后成本路径写入
if spent > limits["usd_per_day"]: raise BudgetExceeded(tenant_id) # 100%: 硬性停止
if spent > 0.8 * limits["usd_per_day"]: alert(tenant_id, "80% of daily budget") # 80%: 警告同时设置平台级上限——否则N个租户 × 每租户上限将没有总限制。注意两个会静默使账单增加10倍的因素:重试风暴(验证失败导致模型重复调用)和无限制工具循环(代理持续执行)。对两者都设置上限,并记录llm_retries_total{reason}指标,使风暴可见。LLM成本本身并不高;真正的问题是其未被监控。在关键节点进行监控,账单将始终与价值保持比例关系。
让它变得高效 —— 不牺牲质量
成本和延迟的根源相同:一个耗时数分钟的流水线通常不是因为模型本身而变慢,而是因为存在 O(n²) 循环、逐条记录的往返操作或串行阶段 —— 这些一直是数据流水线的性能缺陷,现在只是被包裹在了大语言模型(LLM)周围。
规则0:在触碰任何内容之前,先锁定一个质量基线。性能优化具有危险性,因为最快版本往往在准确性上存在细微缺陷。记录一个基线 —— 代表性数据集、当前输出、关键质量指标 —— 并在每次修改后重新运行。任何导致基线退化的优化都不被接受。规则1:进行性能分析,而非猜测。一个匹配 10,000 × 10,000 项的流水线“感觉”像是模型限制,但性能分析显示 90% 的时间都花在嵌套循环中进行 100,000,000 次比较。模型从来就不是问题所在。
按收益排序的常见优化方向:
- O(n²) 匹配循环 → 一次性索引到哈希/键连接,然后查找:约 1 亿次比较变为约 2 万次操作。
- 逐条记录的模型/网络往返 → 批量处理,或对简单情况完全跳过模型(
partition(rows, is_deterministic),简单情况使用规则,复杂情况使用一次批量模型调用)。这与成本优化中“不调用模型”的杠杆相同,但收益更大。 - 可并行但串行的阶段 → 有界并发,按实际约束(提供方速率限制、CPU、内存)调整规模 —— 而不是无限制并发,后者只会转移瓶颈并突破限制。
- 重复计算 → 缓存确定性工作(嵌入向量、解析输入、参考查找)。
每次修改后,重新运行两个维度 —— 延迟基准测试和与基线的评估对比 —— 并回滚任何导致质量退化的修改,无论它有多快:
修改 延迟 与基线的质量对比
哈希连接(原为O(n²)) 180s → 12s = 基线 ✓
批量模型调用 12s → 6s = 基线 ✓
并行阶段(×8) 6s → 1.8s = 基线 ✓模型很少是瓶颈 —— “高效”永远不应是关于模型是否仍然正确的猜测。
路由、降级和紧急开关
你拥有的不是“一个模型”,而是一个模型舰队 —— 便宜且强大、主模型与仲裁模型、这个提供方与那个提供方 —— 你需要一个中间层来选择合适的模型,在某个模型宕机时存活,并在模型行为异常时能在几秒内停止。这三个功能都通过同一个网关实现,且只有当领域不关心哪个模型响应时,这三个功能才能正常工作。
按决策需求进行路由
条件 路由到 原因
------------------------------- --------------------------------- ----------------------------------
`is_judge` 与主模型不同的家族模型 独立的盲点
`stakes == low` / 高吞吐量 便宜/小模型 最大流量;最大的成本杠杆
模糊 / 高风险 强大模型 关键位置的准确性
长上下文 / 提取密集型 表现最佳的模型 任务适配(仅在你已测量过时)def route(d) -> str:
if d.is_judge: return JUDGE_MODEL # 与主模型独立
if d.stakes == "low": return CHEAP_MODEL
if d.needs_long_context: return LONG_CTX_MODEL
return CAPABLE_MODEL
# 保持路由规则在单一位置(网关),可读且可测试 —— 而非每个调用点的字符串降级:在提供方宕机时存活
def route_chain(req) -> list[str]: # 有序的回退链
if req.is_judge: return [JUDGE_MODEL] # 对于法官模型不进行静默回退
if req.stakes == "low": return [CHEAP_MODEL, CAPABLE_MODEL] # 失败时向上回退
return [CAPABLE_MODEL, FALLBACK_MODEL]
def complete_with_fallback(req, deadline):
for model in route_chain(req):
if breaker[model].is_open(): # 调用前检查 —— 跳过已知失效的提供者
continue
remaining = deadline - now() # 每次尝试的预算与整体截止时间
if remaining <= 0:
break
try:
resp = call(model, req, timeout=remaining, idem_key=req.idem_key)
breaker[model].record_success()
return resp
except (Timeout, ProviderError):
breaker[model].record_failure() # 每次捕获错误都记录失败 → 断路器可以打开
return route_to_human(req, reason="all_models_unavailable") # 有意识降级,不报错使此实现正确的非显而易见之处:在调用前检查断路器,并在每次捕获错误时记录失败(常见错误是将两者都放在except块中,导致断路器无法主动跳过已失效的提供者)。使用整体截止时间配合每次尝试的超时 —— 粗心的替代方案是每个模型重复使用固定超时,这会使总延迟达到N × 超时并超出上游预算。幂等性是强制要求:一个超时但实际完成(并写入账本条目)的主模型必须不能被重复处理。有意识降级 —— 根据能力决定是否接受较弱回退模型的答案,或者应该路由给人类。
将路由和回退绑定在一起的一个原则:评估路径上的每个模型。被路由到或回退到的模型是不同模型,因此可能质量不同。你的黄金测试集应在使用它们的能力上通过廉价模型和回退模型的测试 —— 一个静默降低质量的回退比它覆盖的故障更糟糕。记录哪个模型做出决策(在账本中),这样结果分析可以看到廉价模型或回退模型是否表现不佳。
熔断开关:一个可在数秒内生效的关闭机制
部署可能需要你没有的数分钟时间。该开关必须是每个节点都检查的运行时状态:
class Mode(Enum):
LIVE = "live" # 正常模式
HUMAN_ONLY = "human_only" # 停止自动执行;仍可提交给人类
HALTED = "halted" # 完全停止决策
def get_mode(switch, tenant_id, capability) -> Mode:
try:
raw = (switch.read(f"killswitch:{tenant_id}:{capability}") # 最具体的规则
or switch.read(f"killswitch:{tenant_id}") # 整个租户的规则
or switch.read("killswitch:global")) # 全局规则适用于所有地方
return Mode(raw) if raw else Mode.LIVE
except SwitchUnavailable:
return Mode.HUMAN_ONLY # 向安全方向失败 —— 不是live,也不是halted(短暂故障不应自我拒绝服务)四个设计选择使其值得信赖:快速传播(通过共享状态/发布-订阅实现,使切换操作能在数秒内同步到所有实例——30秒的本地TTL并不是“秒”级);细粒度控制(按租户和能力划分,使“关闭”操作的影响范围与问题本身的影响范围匹配);中间档位(HUMAN_ONLY模式保持提议能力但停止自动执行——很多时候你不需要“关闭”,而是需要让人类重新介入流程);以及失败时趋向安全(如果节点无法读取切换状态,系统将默认进入保守模式)。
按设计降级并验证其有效性
提前决定系统在故障时的表现方式,避免级联故障。熔断器机制能阻止对失效依赖的持续请求——在毫秒级失败而非60秒超时后才失败,这正是依赖故障演变为线程耗尽故障的典型方式。在过载时主动卸载负载:减速或转由人工处理,而非直接崩溃。一个能降级为“由人工处理”的系统,仍然实现了其核心目标。
如果这些机制仅在理论上有效,那它们毫无价值。需对代理执行混沌测试:
混沌测试 验证条件
--------------------------- -----------------------------------------------------
终止模型提供方 决策流程转向人工处理,而非报错
切换关闭开关 自动执行立即停止,所有实例同步生效
过载/突发流量 主动卸载负载/降级,不发生崩溃
决策过程中重启节点 运行中的决策恢复或安全失败(幂等性)
依赖方延迟突增 熔断器触发;无线程堆积如果你从未在真实故障场景中测试过关闭开关和降级机制,就无法确认它们真的有效——你只是在祈祷。关闭开关正是让你能安心入睡的保障。
底层平台支撑
以上所有设计都依赖于一个基础平台:一个允许模型切换而无需重构的架构,一个能追踪每个操作归属的身份模型,以及一套规模适配的基础设施。
六边形架构:将供应商隔离在领域之外
你部署的模型可能与最初选择的模型完全不同。供应商每几个月就会推出新版本;定价策略会变化;地区或合规要求可能强制切换模型。如果业务逻辑中充斥着供应商SDK的导入和特定提供方的请求对象,每次变更都需要重构。解决方案是将经典理念应用于新问题:端口与适配器模式。你的领域逻辑仅依赖于你定义的端口(接口)。外部复杂性(模型提供方、数据存储、队列等)则封装在实现这些端口的适配器中。领域逻辑从不直接导入供应商SDK。
定义一个统一的网关作为所有模型流量的唯一入口,使用你的术语体系:
@dataclass(frozen=True)
class ChatRequest: # 你的术语体系——注意你刻意不暴露的内容
messages: list[dict]
schema: dict | None = None # 结构化输出(你的概念,而非供应商的response_format)
max_tokens: int = 1024
# 不包含供应商特定参数(logit_bias等)——这会将供应商逻辑渗透到领域中
class LLMGateway(Protocol): # 为清晰度展示同步版本;实际网关为异步/流式处理
def complete(self, request: ChatRequest) -> ChatResponse: ... # 使用你的类型定义,而非供应商类型这是同一个网关,它负责成本计量、限流、路由、降级和熔断开关——所有横切关注点都集中在这里统一处理,而不是分散在各处。这并非巧合:使领域可移植的咽喉点,也正是使系统可操作的咽喉点。由于领域依赖于协议,测试时会注入一个伪造对象——无需网络、无需消耗、无需故障模拟——而适配器则会针对真实对象进行集成测试。
不要依赖人工警觉来保持边界清晰;应通过代码检查工具强制执行。一个导入契约规则("领域代码不得引入厂商SDK")会在有人将厂商导入语句写入领域代码的瞬间就让构建失败。结合命名规范——所有在adapters/目录下或按提供商后缀命名的代码可以引入对应提供商的依赖,其他任何代码都不允许。大多数生态系统都有类似机制(JS/TS的模块边界检查、Java的ArchUnit、Go的depguard)。这需要额外增加一层翻译层和前期接口设计;但换来的是单文件提供商替换、可测试的领域代码,以及构建系统永远维护的边界。这样"我们能切换模型吗?"就不再是需要立项的工程,而变成一个下午就能完成的变更。
身份:每个操作携带的是谁的权限?
代表用户执行操作的代理存在一个身份问题,这是无状态API所不需要面对的。请求以用户身份到达,代理进行推理、调用下游服务,可能持续运行一段时间,甚至在用户离开后仍执行操作。在每个跳转环节:当前操作使用的是谁的权限?是否允许该操作在该用户、该租户下执行?处理不当就会导致经典的"困惑代理"问题——代理使用自己的广泛权限执行用户无法执行的操作。
两种流程,两种策略。短时同步流程(< ~60秒):传播用户的凭证——入站令牌随下游调用传递,确保每个操作都严格使用用户的权限(仅在每个下游都执行操作级鉴权时安全)。长时/延迟流程:无法持有用户的令牌,因此在入口节点生成一个短期委托凭证——一个非对称签名的令牌,限定于当前工作流,代表用户执行,绑定单一受众和租户,具有精确匹配的最小权限范围、可撤销的ID,以及小时级的硬性TTL上限。
在每个跳转环节最重要的检查是:身份的租户必须与请求的租户匹配:
def authorize(token: str, request):
# 首先认证:非对称签名(下游可验证但无法生成),拒绝无效签名
# alg=none,检查exp/iat,aud == 本服务,jti不在黑名单。永远不要信任解析后的身份
identity = verify_grant(token, expected_aud=THIS_SERVICE, leeway_s=30)
if identity.tenant_id != request.tenant_id:
audit.violation("cross_tenant", identity=identity, request=request)
raise Forbidden() # 永远不要继续执行
if request.action not in identity.scope: # 精确匹配——不允许"write:*"通配符
raise Forbidden(f"out of scope: {request.action}")
return identity这是防止一个租户的代理访问其他租户数据的检查机制——即使存在漏洞或提示注入也是如此。使用非对称签名(共享的HMAC密钥允许任何持有者生成授权);遵循最小权限原则并设置短生存时间(TTL)(泄露的短期授权影响有限,长期授权则构成安全漏洞);在每个传输节点进行验证(注入攻击发生在边缘之后);将跨租户拒绝操作记录为审计违规——这些拒绝信号是攻击特征,绝不能静默返回403错误。所有设计的核心目标:代理执行的每个操作都必须可追溯、权限范围明确,并严格限定在对应的人员和租户范围内。
基础设施:构建骨干网络,抵制盲目复制
搭建代理平台时需在两种失败模式间取得平衡:资源不足(无审计存储、无密钥管理、仅使用共享的超级凭证)或过度配置(为少量无状态服务配置队列、三个数据库和一个服务网格)。实际核心组件清单如下:
组件 原因 建议起点
--------------- ------------------------------------------------------------- -------------------
代理服务 能力实现 几个无状态服务,自动扩展
模型网关 唯一的控制点;仅持有提供方凭证 1个
审计数据存储 仅追加的决策账本(权威记录) 关系型数据库
缓存 会话/工作状态;限流和熔断开关的后备 1个(如Redis)
密钥存储 模型密钥、防护规则配置、签名密钥 管理型密钥管理系统
对象存储 大型输入/工件 + 分层(受监管)日志,加密存储 1个存储桶代理端点本质上是无状态的HTTP服务。如果它们仅提出决策且不执行长时间内部循环,通常无需队列或事件总线——同步请求/响应即可满足需求。注意刻意省略的组件:队列、向量存储、第二个数据库。只有在具体工作负载明确需要时才添加这些组件。
有两个原则比组件列表更重要。最小权限IAM:不要为每个服务分配宽泛凭证——网关仅获得模型调用权限和模型密钥;代理服务仅获得审计存储访问权限和自身对象存储路径,但无模型凭证(它们通过网关调用);审计数据库用户权限限定在其模式范围内。使用按服务划分的工作负载身份,而非共享静态密钥,这样即使发生泄露,影响范围仅限于单个组件的狭窄权限,而非整个平台。加密方面:从第一天起使用托管密钥保护静态数据,如果账本需要加密签名则使用专用签名密钥,即使从单租户模式起步也要设计按租户划分的密钥。这项原则不是添加异想天开的基础设施,而是从第一天起就不添加冗余组件,严格限定每个凭证的权限范围,并确保审计存储和网关控制点始终就位。
核心要点
可操作性是将一个能运行的LLM系统转变为真正可用系统的关键。不仅要对服务进行监控,更要对决策过程进行追踪——建立按租户和能力分类的指标目录、分层日志、通过decision_id串联指标、日志、追踪和账本,并对具有实际阈值的症状设置告警。将所有模型流量通过单一网关进行路由,使成本变得可视化且可控,然后按顺序采取措施:跳过模型、进行批处理、启用缓存、合理配置资源、精简流程——在优化前先进行性能分析,并在每一步建立质量基线。将模型视为路由决策而非固定不变的实体:从低成本到低成本,从困难到具备能力,对独立模型进行判断,实现故障转移以避免单个提供商的故障影响自身——配备即时且细粒度的停止开关,并在混沌测试中实际验证过的降级方案。同时,所有这些功能都应建立在这样一个平台上:将供应商隐藏在端口之后,使租户验证的身份在每一步都得到传递,并在不依赖盲目复制(cargo cult)的情况下提供基础设施支持。
贯穿所有章节的核心思想是相同的:一个关键瓶颈。使你的领域可移植的网关,正是你测量成本、路由模型、实现故障转移和切换停止开关的地方。如果能妥善构建这个关键连接点,可操作性将不再成为事故处理时的混乱因素,而是系统本身固有的属性。
系列文章:在生产环境中运行LLM系统 —— 第5级/共6级:可操作性
这些文章采用知识共享署名-相同方式共享4.0国际许可协议进行授权。
creativecommons.org/licenses/by-sa/4.0/deed.en