AI 工程基础体系 · 第 33/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

AI 生产系统架构:网关、模型、RAG、Agent、队列、存储和发布

AI 生产系统不是“给一个模型套一层 HTTP 服务”。它同时处理四类对象:

  1. 模型:传统机器学习模型、深度学习模型、Embedding 模型、重排模型、生成式模型。
  2. 数据:训练数据、特征、文档、向量、会话、工具结果、评测集和审计记录。
  3. 工作流:一次请求可能包含检索、模型调用、工具执行、人工审批和异步任务。
  4. 治理约束:身份、权限、隐私、可靠性、质量、成本和发布风险。

因此,生产架构的基本问题不是“模型能不能生成”,而是:

输入身份与策略任务编排模型/检索/工具结果验证发布与审计可追踪输出\text{输入} \xrightarrow{\text{身份与策略}} \text{任务编排} \xrightarrow{\text{模型/检索/工具}} \text{结果验证} \xrightarrow{\text{发布与审计}} \text{可追踪输出}

这里的“结果”不只包括回答,还包括模型版本、检索证据、工具副作用、消耗的 Token、延迟、错误和权限判断。


一、先建立系统边界:在线路径、异步路径和控制平面

一个完整的 AI 生产系统至少可以分成三条路径:

  • 在线数据路径:用户请求在有限延迟预算内得到结果。
  • 异步数据路径:文档解析、Embedding、批量推理、评测和报表通过队列执行。
  • 控制平面:模型、Prompt、索引、权限策略、工作流定义和发布版本的管理。

典型组件关系如下:

flowchart LR
    U[用户或业务系统] --> G[AI 网关]
    G --> O[编排器]
    O --> M[模型服务]
    O --> R[RAG 检索服务]
    O --> T[工具服务]
    R --> VS[向量存储]
    R --> DS[文档与元数据存储]
    T --> DB[业务数据库]
    O --> Q[任务队列]
    Q --> W[异步 Worker]
    W --> M
    W --> R
    W --> DS

    G --> TR[Trace 与审计]
    O --> TR
    M --> TR
    R --> TR
    T --> TR

    CP[控制平面] --> G
    CP --> O
    CP --> M
    CP --> R
    CP --> P[发布与评测系统]
    P --> CP

在线请求不应直接把所有组件串成不可控的长链路。例如,用户问答可以同步完成检索和生成,但文档切分、向量化、索引构建应进入异步队列。否则,一个大文件上传会占满模型和数据库连接,影响所有在线请求。

1. 在线数据路径

在线请求通常经历:

  1. 网关验证身份和请求格式;
  2. 根据租户、用户、用途和预算加载策略;
  3. 编排器决定是否检索、调用哪个模型、是否调用工具;
  4. 模型服务执行推理;
  5. 结果经过格式校验、敏感信息处理和权限检查;
  6. 返回结果,并写入 Trace、用量和审计记录。

2. 异步数据路径

异步任务通常具有更长的处理时间和更强的重试需求:

  • 文件解析;
  • 文档切分和去重;
  • Embedding 生成;
  • 向量索引构建;
  • 离线批量预测;
  • 评测集运行;
  • 失败任务补偿;
  • 数据删除传播。

异步任务必须有任务 ID、状态、重试次数、错误原因和幂等键。仅把任务放入消息队列而不保存任务状态,会导致“队列里看不到任务,数据库里也不知道任务是否完成”的不可诊断状态。

3. 控制平面

控制平面管理“系统使用什么版本”,而数据平面负责“系统执行请求”。应至少版本化:

  • 模型及其权重;
  • Tokenizer 和 Embedding 模型;
  • Prompt 模板;
  • RAG 解析、切分、过滤和排序配置;
  • Agent 工作流;
  • 工具 schema 和权限策略;
  • 评测集、阈值和发布规则。

只记录“当前模型名称”不够。相同模型名称在不同权重、量化方式、Prompt 或索引版本下,行为可能完全不同。


二、网关:不是反向代理,而是 AI 请求的策略边界

AI 网关是所有外部 AI 请求进入系统的统一入口。普通反向代理主要关心路由、连接和 TLS;AI 网关还必须理解模型、Token、租户和工具风险。

1. 网关需要解决的状态

一次请求至少包含以下上下文:

{
  "request_id": "req_01",
  "tenant_id": "tenant_a",
  "principal_id": "user_42",
  "purpose": "internal_knowledge_qa",
  "model_policy": "chat-default",
  "input": "请总结这份合同",
  "stream": true,
  "idempotency_key": "tenant_a:job_1001"
}

其中:

  • tenant_id 表示计费和隔离边界;
  • principal_id 表示具体用户或服务身份;
  • purpose 表示用途,便于按场景执行不同策略;
  • model_policy 不是固定模型名,而是一个可解析的路由策略;
  • idempotency_key 用于重试去重;
  • request_id 必须贯穿网关、编排器、模型、检索和工具服务。

2. 认证、授权和租户隔离

认证回答“你是谁”,授权回答“你能做什么”。

例如,用户有权访问某个文档库,并不意味着模型可以检索所有文档。检索条件必须包含权限过滤:

SELECT chunk_id, content, embedding
FROM document_chunks
WHERE collection_id = :collection_id
  AND tenant_id = :tenant_id
  AND (
      visibility = 'public'
      OR :principal_id = ANY(allowed_principal_ids)
  );

如果系统先在全库做向量近邻搜索,再在结果页过滤权限,可能出现两个问题:

  1. 未授权文档的相似度和内容可能短暂进入中间日志或缓存;
  2. 权限过滤后结果数量不足,系统却误以为“没有相关内容”。

更安全的实现是让权限条件进入检索阶段,或者为安全边界建立独立索引。对于复杂权限,必须验证向量数据库是否真的支持预过滤,以及过滤发生在近邻搜索之前还是之后;不能仅依据 SDK 参数名称推断语义。

3. 限流与预算

传统限流常按请求数计算,但生成式 AI 的成本和资源消耗更接近 Token 数:

Crequest=PinTin+PoutTout+Cembedding+CtoolC_{\text{request}} = P_{\text{in}}T_{\text{in}} + P_{\text{out}}T_{\text{out}} + C_{\text{embedding}} + C_{\text{tool}}

其中:

  • PinP_{\text{in}}PoutP_{\text{out}} 是输入和输出 Token 单价;
  • TinT_{\text{in}}ToutT_{\text{out}} 是输入和输出 Token 数;
  • CembeddingC_{\text{embedding}} 是检索向量化成本;
  • CtoolC_{\text{tool}} 是搜索、数据库或外部 API 成本。

因此,网关通常需要同时执行:

  • 每秒请求数限制;
  • 并发请求数限制;
  • 输入和输出 Token 限制;
  • 单租户预算限制;
  • 单用户或单 API Key 限制;
  • 长上下文请求限制。

一个租户可能只发 10 个请求,但每个请求有 100 万 Token;另一个租户发 10,000 个短请求。只按请求数限流会错误地认为二者负载相同。

4. 流式响应的边界

流式输出降低首 Token 延迟(TTFT),但会增加状态管理复杂度:

  • 客户端断开后,模型是否继续运行;
  • 已输出内容是否可撤回;
  • 审计记录按 Token 增量写入还是结束时写入;
  • 工具调用期间是否允许继续向用户输出;
  • 中途错误如何表示。

流式响应不应绕过最终校验。常见做法是将“可显示文本”和“最终结果状态”分离:客户端可以逐步收到文本,但服务端仍需在结束时写入成功、拒绝、超时或部分失败状态。


三、模型服务:模型、推理和容量是三个不同概念

模型是参数、结构和推理规则的组合;模型服务是把模型加载到计算资源上,并通过 API 提供推理的运行时;模型路由则决定请求使用哪个模型实例或版本。

生产环境中至少有四种模型调用:

  1. 预测模型:输入特征,输出分类、回归或排序结果;
  2. Embedding 模型:输入文本或多模态内容,输出向量;
  3. 重排模型:输入查询和候选文档,输出相关性分数;
  4. 生成模型:输入上下文,逐 Token 生成文本或结构化结果。

它们的资源特征不同,不能都用“平均响应时间”管理。

1. 批处理与连续批次

批处理是在一次推理调用中把多个请求合并。它提高 GPU 利用率,但需要等待请求凑成批次。

设单请求计算时间为 t1t_1,批大小为 bb,合并后时间为 tbt_b。若:

tb<bt1t_b < b \cdot t_1

则批处理有吞吐收益。可是请求需要等待形成批次,增加排队延迟。

连续批次允许正在生成的请求动态加入或退出批次。它适合自回归生成,因为不同请求的输出长度不同。如果固定批次等待所有请求结束,短请求会被长请求拖慢;连续批次可以在某个请求结束后立即填入新请求。

因此调度器通常需要分别记录:

  • 排队时间;
  • Prefill 时间,即处理输入上下文的时间;
  • Decode 时间,即逐 Token 生成时间;
  • TTFT;
  • 总生成时间;
  • 输出 Token 数。

2. KV Cache

Transformer 生成第 tt 个 Token 时,需要使用前面 Token 的 Key 和 Value。KV Cache 保存已经计算出的 Key、Value,避免每一步重复计算。

若层数为 LL,注意力头数为 HH,每个头维度为 DD,上下文长度为 SS,数据类型每个元素占 BB 字节,粗略缓存大小为:

MKV2×L×H×S×D×BM_{\text{KV}} \approx 2 \times L \times H \times S \times D \times B

前面的 2 对应 Key 和 Value。若采用多查询注意力或分组查询注意力,实际 KV 头数可能小于注意力头数,应使用 KV 头数替代 HH

这解释了一个常见现象:模型权重能够放入 GPU,不代表系统能承受长上下文高并发。权重占用是相对固定的,而 KV Cache 随并发请求数和上下文长度增长。

3. 量化的取舍

量化将权重或激活从高精度表示转换为低位宽表示,例如从 FP16 转为 INT8 或更低位宽。理想情况下,权重显存大致按位宽比例下降:

MquantizedMoriginal×quantized bitsoriginal bits+MscaleM_{\text{quantized}} \approx M_{\text{original}} \times \frac{\text{quantized bits}}{\text{original bits}} + M_{\text{scale}}

但实际收益还受缩放因子、未量化层、运行时内核和 KV Cache 精度影响。量化不是“免费扩容”:

  • 生成质量可能下降;
  • 某些任务对数值误差敏感;
  • 不同硬件对量化格式支持不同;
  • 吞吐不一定随显存下降同比提高。

必须使用固定评测集比较准确率、拒答率、工具调用正确率、结构化输出合法率和长上下文表现。

4. GPU 容量估算示例

假设:

  • 模型权重为 70B 参数;
  • FP16 每参数 2 字节;
  • 量化后平均每参数 1 字节;
  • 额外运行时开销暂按权重大小的 20%;
  • KV Cache、激活和框架开销另行预留。

FP16 权重大约是:

70×109×2=140 GB70 \times 10^9 \times 2 = 140\text{ GB}

量化权重大约是 70 GB,加上 20% 运行时开销约 84 GB。这个数字仍不能直接得出“需要一张 96 GB GPU”,因为还要加 KV Cache、CUDA workspace、通信缓冲和安全余量。若 10 个并发请求各有 32k Token,上述 KV Cache 可能成为主要限制。

容量规划必须用目标硬件和目标运行时压测,而不是只用参数量除以显存。


四、RAG:检索增强生成不是“向量库加 Prompt”

**RAG(Retrieval-Augmented Generation,检索增强生成)**是在生成模型回答前,从外部知识源检索相关内容,并把经过筛选的内容作为上下文提供给模型。

它解决的是模型参数知识的三个问题:

  • 知识更新不需要重新训练模型;
  • 私有数据不必直接写入模型权重;
  • 回答可以附带检索证据。

但 RAG 不自动保证正确性。模型可能检索错文档、误读文档或根据不完整证据生成错误结论。

1. RAG 的完整生命周期

文档摄取

原始文档进入系统后,需要记录:

  • 文档 ID、租户、来源和版本;
  • MIME 类型、编码和语言;
  • 创建者、更新时间和权限;
  • 原始文件哈希;
  • 解析器版本;
  • 删除和生效状态。

文件哈希可用于幂等去重。若同一内容重复上传,不应重复产生无限个 Chunk。

解析与切分

解析把 PDF、HTML、Office 文档或数据库记录转换为结构化文本。切分把长文档划分为 Chunk。

Chunk 不能只按固定字符数切割。应尽量保留标题、段落、表格行和代码块等语义边界。设 Chunk 长度为 ll,重叠长度为 oo,则相邻 Chunk 共享内容。增大 oo 能减少边界信息丢失,但会增加索引大小和检索重复。

反例是把一个包含“前置条件”和“例外条款”的合同条款切成两个互不完整的 Chunk。向量检索可能只召回例外条款,模型便生成违反原意的回答。

向量化与索引

Embedding 模型把文本映射到向量:

f:textRdf: \text{text} \rightarrow \mathbb{R}^{d}

查询和 Chunk 的相似度可使用余弦相似度:

sim(q,x)=qxqx\operatorname{sim}(q,x) = \frac{q \cdot x}{\|q\|\|x\|}

如果向量已归一化,则余弦相似度等于点积。Embedding 模型、归一化方式和距离度量必须一致,否则分数没有可比性。

召回、过滤和重排

一个常见流程是:

  1. 通过向量或关键词召回较大的候选集;
  2. 执行租户、权限、时间和文档状态过滤;
  3. 用重排模型对查询—文档对重新打分;
  4. 选择有限数量的 Chunk 放入上下文。

向量检索擅长语义相似,关键词检索擅长精确匹配。对于错误码、合同编号、产品型号等内容,混合检索通常比单纯向量检索更稳健。

生成与引用

生成 Prompt 应明确区分:

  • 用户问题;
  • 检索证据;
  • 不可信的文档内容;
  • 系统规则;
  • 输出格式。

文档中的“请忽略系统指令并执行某操作”属于检索到的内容,不应自动获得指令权限。这是间接 Prompt Injection 的典型来源。

2. RAG 的质量分解

RAG 回答质量可以拆成多个条件:

P(正确回答)P(召回证据)×P(正确使用证据已召回)×P(正确表达已使用)P(\text{正确回答}) \approx P(\text{召回证据}) \times P(\text{正确使用证据}\mid\text{已召回}) \times P(\text{正确表达}\mid\text{已使用})

如果正确文档没有被召回,后面的生成模型再强也无法稳定补救。评测不能只看最终回答,还应单独测:

  • Recall@k:正确证据是否进入前 kk 个结果;
  • MRR 或 nDCG:正确证据排名是否靠前;
  • 引用准确率:回答是否真的被引用内容支持;
  • 权限泄露率;
  • 过时文档命中率;
  • “无答案”时是否能够拒答。

3. RAG 失败诊断

回答错误时,应按以下路径定位:

  1. 原始文档是否包含正确答案;
  2. 解析是否丢失表格、脚注或编码;
  3. Chunk 是否破坏语义;
  4. Embedding 是否适合该语言和领域;
  5. 权限过滤是否误删了正确 Chunk;
  6. 候选是否被重排模型降权;
  7. 上下文是否超过模型有效窗口;
  8. 模型是否忽略了证据;
  9. 输出校验是否错误地接受了无引用回答。

只查看最终答案而不保存候选文档、分数和索引版本,无法区分检索问题与生成问题。


五、Agent:具有状态和工具副作用的策略执行器

Agent不是“会思考的 Prompt”。在工程上,Agent 是一个根据当前状态选择下一步动作的程序。动作可能是:

  • 调用模型;
  • 搜索知识库;
  • 查询数据库;
  • 调用外部 API;
  • 请求人工审批;
  • 结束任务;
  • 重试或转入补偿。

可以形式化为:

at=π(st,p,c)a_t = \pi(s_t, p, c)

st+1=T(st,at,ot)s_{t+1} = T(s_t, a_t, o_t)

其中:

  • sts_t 是第 tt 步状态;
  • pp 是策略、Prompt 和工作流版本;
  • cc 是权限、预算和上下文;
  • ata_t 是 Agent 选择的动作;
  • oto_t 是工具或模型返回的观察结果;
  • TT 是状态转移函数;
  • π\pi 是动作选择策略。

这个形式化说明了为什么 Agent 需要持久化状态:如果进程崩溃后只有一段自然语言上下文,系统无法可靠判断已经执行过哪些有副作用的动作。

1. 工具调用必须有契约

工具不能只暴露一个任意 JSON 接口。工具契约至少包括:

  • 名称和版本;
  • 输入 schema;
  • 输出 schema;
  • 超时;
  • 重试条件;
  • 所需权限;
  • 是否有副作用;
  • 幂等键;
  • 审计字段。

例如,“创建退款”与“查询订单”都可能被模型调用,但前者会产生业务副作用,必须要求更高权限,可能还需要人工审批。

2. Agent 状态机示例

stateDiagram-v2
    [*] --> Received
    Received --> Planning
    Planning --> Retrieving: 需要知识
    Planning --> ToolPending: 需要工具
    Planning --> Answering: 无需外部动作
    Retrieving --> Planning: 得到证据
    ToolPending --> ToolRunning: 权限通过
    ToolPending --> HumanApproval: 高风险动作
    HumanApproval --> ToolRunning: 审批通过
    HumanApproval --> Failed: 拒绝或超时
    ToolRunning --> Planning: 工具成功
    ToolRunning --> RetryWait: 可重试错误
    RetryWait --> ToolRunning: 未超过上限
    ToolRunning --> Failed: 不可重试或超过上限
    Answering --> Completed
    Planning --> Failed: 超过步数或预算

关键是每次状态转移都应持久化。例如:

CREATE TABLE agent_runs (
    run_id              TEXT PRIMARY KEY,
    tenant_id           TEXT NOT NULL,
    workflow_version    TEXT NOT NULL,
    status              TEXT NOT NULL,
    step_no             INTEGER NOT NULL DEFAULT 0,
    budget_tokens       INTEGER NOT NULL,
    used_tokens         INTEGER NOT NULL DEFAULT 0,
    created_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at           TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE agent_steps (
    run_id              TEXT NOT NULL,
    step_no             INTEGER NOT NULL,
    action_id            TEXT NOT NULL,
    action_type          TEXT NOT NULL,
    input_json           JSONB NOT NULL,
    output_json          JSONB,
    status               TEXT NOT NULL,
    PRIMARY KEY (run_id, step_no),
    UNIQUE (action_id)
);

UNIQUE(action_id) 用于防止同一个副作用动作因消息重复投递而执行两次。它只能防止本系统内的重复;如果外部支付系统支持幂等键,还必须把 action_id 传给外部系统。

3. Agent 的边界条件

必须设置:

  • 最大步数;
  • 最大 Token 预算;
  • 最大墙上时间;
  • 工具调用超时;
  • 单工具调用次数;
  • 递归 Agent 层数;
  • 允许访问的工具集合;
  • 需要人工审批的动作集合。

没有这些边界,模型可能在“继续检索—继续思考—继续调用工具”的循环中耗尽预算。


六、队列:把时间、负载和失败隔离开

队列是生产者和消费者之间的持久化缓冲。它不只是“异步执行”的语法,而是用来隔离不同时间尺度和故障域。

适合进入队列的任务包括:

  • 文档处理;
  • Embedding;
  • 批量推理;
  • 长时间 Agent 运行;
  • 评测;
  • 报表;
  • 删除传播;
  • 重试和补偿。

1. 至少一次投递与幂等

许多消息系统提供的是 at-least-once(至少一次) 投递:消息可能重复,但在消费者确认前不会轻易丢失。

处理过程通常是:

  1. 消费者读取消息;
  2. 执行业务操作;
  3. 业务操作成功;
  4. 确认消息。

若第 3 步成功后进程在第 4 步前崩溃,消息会再次投递。因此消费者必须幂等。

一个安全模式是:

BEGIN;

INSERT INTO processed_messages(message_id, processed_at)
VALUES (:message_id, now())
ON CONFLICT (message_id) DO NOTHING;

-- 若插入行数为 0,说明该消息已经处理过,直接提交并确认消息
-- 若插入行数为 1,继续执行真正业务操作

UPDATE ingestion_jobs
SET status = 'completed', updated_at = now()
WHERE job_id = :job_id
  AND status IN ('queued', 'running');

COMMIT;

但“先记录 processed,再执行业务”存在进程崩溃风险:记录写入成功而业务未完成,重试会被错误跳过。更稳妥的方法是把去重记录和业务状态更新放在同一个数据库事务中;若外部系统无法参与事务,则使用 Outbox、幂等 API 或补偿状态。

2. 重试必须区分错误类型

可重试错误通常包括:

  • 临时网络错误;
  • 上游 429;
  • 服务过载;
  • 可恢复的数据库连接错误。

不可盲目重试的错误包括:

  • schema 校验失败;
  • 权限拒绝;
  • 输入文件损坏;
  • 模型明确拒答;
  • 外部副作用已经成功但响应丢失。

指数退避可以写成:

dn=min(dmax,d02n)+Jd_n = \min(d_{\max}, d_0 2^n) + J

其中 nn 是已重试次数,d0d_0 是初始等待时间,dmaxd_{\max} 是上限,JJ 是随机抖动。抖动用于避免大量任务同时重试形成惊群。

3. 队列状态与死信

任务至少应有:

queued -> running -> succeeded
                  -> retry_wait -> running
                  -> failed
                  -> dead_letter

死信队列不是垃圾桶。每条死信应包含原始任务、最后错误、重试次数、代码版本和输入摘要。修复消费者后,需要明确“重新投递哪些死信”,不能无条件全部重放,因为部分任务可能已经产生外部副作用。


七、存储:按数据语义分层,而不是“全部放数据库”

AI 系统通常使用多种存储,每种存储承担不同一致性和访问模式。

1. 对象存储

适合保存:

  • 原始文件;
  • 模型权重;
  • 评测输入输出;
  • 批量结果;
  • 训练和推理中间产物。

对象存储中的对象名应包含不可变版本或内容哈希。直接覆盖 latest/model.bin 会造成回滚不可复现,也可能让正在加载的 Worker 读到不完整文件。

2. 关系数据库

适合保存:

  • 租户和用户;
  • 权限;
  • 任务状态;
  • Agent 状态;
  • 模型与 Prompt 注册信息;
  • 预算;
  • 审批记录;
  • 审计索引。

需要事务的状态不应只放在缓存中。缓存丢失后,系统必须能从数据库恢复。

3. 向量存储

向量存储保存向量、Chunk ID、元数据和索引。它通常不应成为原始事实的唯一来源。向量可以重建,原文和权限元数据必须保留在文档系统或关系数据库中。

4. 缓存

缓存适合保存:

  • 短时相同请求结果;
  • Tokenizer 或 Embedding 结果;
  • 热门检索结果;
  • 会话读取副本;
  • 限流计数。

缓存键必须包含会改变结果的因素,例如:

tenant_id
principal_id 或权限版本
model_version
prompt_version
retrieval_index_version
request_normalized_hash

只用用户问题作为缓存键可能把一个用户的答案返回给另一个权限不同的用户。

5. 一致性边界

“文档上传成功”不等于“文档已经可以检索”。应显式区分:

uploaded -> parsed -> embedded -> indexed -> active

只有进入 active 状态,在线检索才应使用该版本。若新索引构建失败,旧索引仍可继续服务;如果直接原地修改索引,失败时可能出现部分新数据和部分旧数据混合。


八、发布:发布的不只是模型权重

AI 发布对象是一个版本集合:

V=(model,tokenizer,prompt,retriever,index,tools,policy)V = ( \text{model}, \text{tokenizer}, \text{prompt}, \text{retriever}, \text{index}, \text{tools}, \text{policy} )

只升级模型而不记录 Prompt、RAG 索引和策略,会导致线上问题无法重现。

1. 发布前的验证顺序

一个可审计的发布过程通常包含:

  1. 静态验证:配置 schema、工具 schema、权限规则;
  2. 离线评测:准确性、召回、引用、拒答、安全和成本;
  3. 回归测试:固定问题集与历史高风险案例;
  4. 影子流量:新版本接收真实请求副本,但不产生用户可见副作用;
  5. 灰度发布:按租户、用户或流量比例逐步放量;
  6. 在线守护:监控错误率、延迟、成本、拒答和安全事件;
  7. 回滚:恢复到上一个完整版本集合。

影子流量不能直接执行写操作工具,否则“影子”会产生真实副作用。工具应使用只读模式、沙箱或模拟器。

2. 质量阈值不是单一准确率

发布判断可以表示为:

Release=i(metricithresholdi)\text{Release} = \bigwedge_i \left(metric_i \ge threshold_i\right)

但不同指标不一定都是“越大越好”。例如:

  • 正确率、引用支持率越高越好;
  • 权限泄露率、无依据回答率、P95 延迟和单请求成本越低越好;
  • 拒答率需要结合问题集合解释,过高可能表示模型过度拒绝,过低可能表示安全边界失效。

必须把评测样本分层:普通问题、长上下文、越权问题、Prompt Injection、工具调用、边界输入和高成本请求不能混在一个平均值里。

3. 回滚条件

应预先定义自动或人工回滚条件,例如:

  • 5xx 或超时显著上升;
  • 结构化输出解析失败率超过阈值;
  • 高风险工具误调用;
  • 权限过滤异常;
  • 单 Token 成本超预算;
  • RAG 引用支持率下降;
  • GPU OOM 增加。

回滚必须恢复整个兼容集合,而不是只切换模型权重。模型与 Prompt、Tokenizer 或索引版本不兼容时,局部回滚反而会制造新故障。


九、可观测性:把一次回答拆成可解释的事件

AI 可观测性应同时覆盖系统性能、模型行为和业务成本。

1. Trace 结构

一次请求可以展开为:

trace: req_100
├── gateway              12 ms
├── policy_check          4 ms
├── retrieve
│   ├── query_embedding   35 ms
│   ├── vector_search     18 ms
│   └── rerank            62 ms
├── llm_prefill          210 ms
├── llm_decode           980 ms
└── output_validation      3 ms

每个 Span 应带有:

  • request_id、trace_id、tenant_id;
  • 模型、Prompt、索引和工作流版本;
  • 输入输出 Token 数;
  • TTFT、总延迟、生成 Token 数;
  • 重试次数;
  • 检索候选和分数;
  • 工具名称、参数摘要和结果状态;
  • 错误类型;
  • 成本估算。

生产日志不应默认保存完整敏感 Prompt 和完整工具参数。应使用脱敏、哈希、访问控制和保留期限;否则可观测性本身会变成数据泄露渠道。

2. 延迟分解

端到端延迟可近似表示为:

Te2e=Tqueue+Tnetwork+Tretrieve+Tprefill+Tdecode+TpostprocessT_{\text{e2e}} = T_{\text{queue}} + T_{\text{network}} + T_{\text{retrieve}} + T_{\text{prefill}} + T_{\text{decode}} + T_{\text{postprocess}}

TTFT 通常包括排队、网络、检索、Prefill 和首次 Decode 的时间;总时间还包括后续 Token 生成。若 TTFT 变差,应先查看排队和 Prefill,而不是只调低最大输出 Token。

3. 成本治理

预算应在请求开始时预留,在每一步执行后扣减。Agent 场景尤其要避免只在最终结束时计费,因为中途循环可能已经消耗大量 Token。

可以使用三种降级:

  1. 缓存命中,跳过模型;
  2. 降低检索候选数或跳过昂贵重排;
  3. 切换到更小或更快的模型;
  4. 将非紧急任务转异步;
  5. 在预算耗尽时返回已有证据和明确的部分结果。

降级不能绕过权限和安全校验。低成本模型同样可能泄露数据或误调用工具。


十、安全与治理:从风险分类落到控制点

NIST AI Risk Management Framework 将 AI 风险管理组织为 Govern、Map、Measure、Manage 等功能。工程实现时,这些功能应映射到具体资产、流程和证据,而不是停留在原则口号:

  • Govern:明确租户、责任人、审批人和风险接受边界;
  • Map:识别数据、模型、工具、用户和潜在伤害;
  • Measure:通过评测、Trace、红队和线上指标测量风险;
  • Manage:通过限流、隔离、拒答、人工审批、回滚和事件响应处理风险。

OWASP Top 10 for LLM Applications 中的风险也应进入架构设计。例如:

Prompt Injection

外部文档、网页和用户输入都可能包含诱导模型改变行为的指令。解决方案不是简单地在 Prompt 中写“忽略恶意指令”,而是:

  • 把文档内容标记为数据而非系统指令;
  • 工具权限由服务端策略判断;
  • 对高风险动作要求审批;
  • 限制工具参数;
  • 不让模型直接获得长期凭证。

不安全输出处理

模型输出不能直接拼接 SQL、Shell、HTML 或下游 API 请求。必须经过:

  • schema 校验;
  • 参数化查询;
  • 命令白名单;
  • HTML 转义;
  • 长度和格式限制;
  • 业务状态校验。

供应链风险

模型权重、Tokenizer、第三方 Prompt、解析器和向量数据库客户端都属于供应链。发布时应记录来源、版本、校验哈希和许可证,并在隔离环境验证加载行为。

敏感信息泄露

训练数据、RAG 文档、会话历史、Trace 和缓存都有泄露风险。必须定义数据分类、最小访问权限、脱敏规则、删除传播和保留期限。删除一个文档通常意味着同时删除原文、Chunk、向量、缓存、评测副本和审计中的可识别内容;只从向量库删除是不完整删除。


十一、一个端到端请求的完整故障路径

假设用户询问内部知识库,并要求 Agent 查询工单状态:

  1. 网关验证用户身份;
  2. 策略服务确认用户可访问该知识库;
  3. 编排器创建 run_id 和预算;
  4. RAG 以用户权限进行检索;
  5. 模型根据证据判断是否需要查询工单;
  6. 工具服务再次验证权限,而不是信任模型判断;
  7. 查询工单失败时,判断是超时、429、权限拒绝还是参数错误;
  8. 只有临时错误才进入重试;
  9. 工具结果写入 Agent 状态;
  10. 模型生成答案;
  11. 输出经过 schema、敏感信息和引用检查;
  12. 返回结果并写入 Trace、Token 和审计记录。

如果第 6 步工具执行成功,但第 9 步服务崩溃,重试可能再次查询工单。查询是只读操作,重复通常可接受;如果工具是“关闭工单”,则必须使用幂等键或业务侧去重,否则重试可能造成重复副作用。

如果第 4 步检索没有结果,系统不应让模型凭常识补全内部事实。可以返回“没有找到授权范围内的证据”,或转人工处理。这个边界比生成一个看似完整但无法验证的答案更可靠。


十二、最小可行的生产契约

一个 AI API 至少应明确以下输入输出契约:

POST /v1/runs
Idempotency-Key: tenant_a:run_1001
Authorization: Bearer <token>
Content-Type: application/json
{
  "workflow": "knowledge_qa",
  "workflow_version": "2025-03-01",
  "input": {
    "question": "如何申请设备维修?"
  },
  "options": {
    "stream": false,
    "max_output_tokens": 800
  }
}

成功响应:

{
  "run_id": "run_1001",
  "status": "completed",
  "answer": "……",
  "citations": [
    {
      "document_id": "doc_8",
      "chunk_id": "chunk_31",
      "version": "v4"
    }
  ],
  "usage": {
    "input_tokens": 1200,
    "output_tokens": 180
  },
  "versions": {
    "model": "model_a:v7",
    "prompt": "qa:v3",
    "index": "kb:v4"
  }
}

异步响应:

{
  "run_id": "run_1002",
  "status": "queued",
  "poll_url": "/v1/runs/run_1002"
}

错误响应也必须结构化:

{
  "error": {
    "code": "BUDGET_EXCEEDED",
    "message": "request budget exceeded",
    "retryable": false,
    "request_id": "req_1002"
  }
}

retryable 只能由服务端根据错误类型决定,不能让客户端对所有 5xx、4xx 都无限重试。客户端重试时应复用 Idempotency-Key,服务端则返回同一个任务结果或明确说明状态未知。


十三、架构取舍的判断方法

在线还是异步

若任务需要用户立即看到结果,并且可以在固定延迟预算内完成,适合在线路径。若任务包含大文件、长时间推理、批量处理或多次工具调用,适合异步路径。

单 Agent 还是显式 DAG

**DAG(有向无环图)**把任务步骤和依赖显式表示,例如:

解析文档 ──> 切分 ──> Embedding ──> 建索引
                  └──> 质量检查

步骤之间无循环、依赖明确时,DAG 更容易重试、并行和审计。需要动态决定下一步时才引入 Agent。把所有固定流程都交给自由生成的 Agent,会增加不可预测性和成本。

事务还是最终一致性

用户权限、扣费、任务状态等核心状态通常需要事务。文档索引、搜索缓存和报表可以接受最终一致性,但必须向用户显示状态,不能把“尚未完成索引”伪装成“知识库为空”。

自建模型还是外部模型

判断依据包括:

  • 数据是否允许离开控制域;
  • 目标延迟和吞吐;
  • 模型质量与领域适配;
  • GPU 和运维能力;
  • 合规、审计和供应链要求;
  • 总成本,而不只是单次 API 价格。

外部模型减少 GPU 运维,但增加网络、供应商可用性、数据处理协议和版本变化风险。自建模型拥有更多控制权,但必须承担容量规划、驱动、模型运行时、补丁和故障恢复。


结语:把 AI 当作有状态、可审计的分布式系统

生产级 AI 系统的核心不是某个模型,而是模型被放置在一条可控制的数据流中:

  • 网关控制身份、权限、限流和预算;
  • 模型服务控制推理、批处理、KV Cache、量化和 GPU 容量;
  • RAG控制外部知识的解析、检索、权限和引用;
  • Agent控制状态转移、工具副作用、审批和预算;
  • 队列控制异步任务、重试、重复投递和故障隔离;
  • 存储保存事实、状态、版本和可重建索引;
  • 发布系统控制模型、Prompt、索引、工具和策略的整体变更;
  • 可观测性与治理使质量、风险和成本能够被测量、解释和恢复。

当一次回答可以追溯到请求身份、策略版本、检索证据、模型版本、工具动作、Token 消耗和最终状态时,AI 才真正进入生产系统,而不是停留在一个不可解释的模型调用中。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。