AI 应用生产级护栏实战:限流、日志、降级、缓存、审计、成本

一、AI 应用上线最小护栏清单

如果你的 AI 应用准备上线,至少要有这张表。

护栏必须做什么不做的后果
用户级限流限制单用户每分钟、每天、每月调用次数被单个用户刷爆额度
IP 级限流限制匿名 IP 或异常 IP 请求被脚本、爬虫、代理刷爆
高成本接口单独限流对深度分析、PDF、长上下文接口加更严限制一个按钮打穿账单
AI 任务幂等同一输入重复提交只生成一个任务重复扣点、重复调用模型
traceId每个请求贯穿前端、后端、AI、PDF、日志出错无法定位
AI 调用日志记录模型、耗时、token、状态、错误码无法审计和复盘
模型错误码分类区分限流、超时、鉴权、内容安全、JSON 失败全部变成“服务器异常”
JSON 解析失败兜底Schema 校验、修复、降级结果模型输出一乱前端就崩
Redis 缓存对相同输入、相同 Prompt 版本复用结果重复请求浪费成本
输入 hash不直接用原文做缓存 key泄露隐私、缓存不可控
promptVersionPrompt 改版后缓存自动失效新旧结果混用
token 成本记录记录 input/output/cached token 和估算成本账单不可解释
失败不乱扣点reserve、commit、refund 三段式扣点用户信任崩塌
PDF 任务队列导出走异步队列,限制并发Puppeteer/浏览器服务被打爆
审计记录记录谁在什么时候对什么数据做了什么 AI 操作出问题无法追责

这些不是“大厂才需要”的东西。只要你接了大模型 API,只要你的产品对外开放,哪怕只有几十个内测用户,也应该做最小版本。如果把这些护栏压缩成一张图,它大概是这样的:

在这里插入图片描述

很多 AI 应用 Demo 看起来都很简单:用户输入一段文本,后端拼一个 Prompt,请求大模型,再把结果展示出来。

但一旦真的上线,问题会马上暴露:

  • 用户连续点按钮,模型账单飙升;
  • 同一个请求重复提交,扣了多次额度;
  • 模型返回半截 JSON,前端直接崩;
  • 第三方模型限流,用户只看到“服务器异常”;
  • AI 生成失败了,系统却扣了点数;
  • 管理员想查一次调用为什么失败,结果没有 traceId、没有日志、没有成本记录;
  • PDF、AI、上传、分享这些高成本/高风险链路全部挤在同步接口里。

所以,AI 应用从 Demo 到可上线产品,中间真正差的不是“多接几个模型”,而是一套最小生产护栏。

为了不把这些护栏讲成散点,下面我会按一次 AI 请求的生命周期来拆:

入口阶段:先挡住异常用户、异常 IP 和高成本请求; 任务阶段:保证同一请求不会重复创建、重复调用、重复扣点; 调用阶段:通过 AI Gateway 统一记录模型、token、耗时和错误; 输出阶段:对 JSON、Schema、质量门禁和 fallback 做兜底; 追踪阶段:用 traceId、调用日志、审计日志把整条链路串起来。

也就是说,AI 应用上线不是给每个接口加几个 if,而是把一次 AI 调用当成一个可追踪、可补偿、可审计的任务。

这篇文章以我上线简喵的经验为例,拆一套 AI 应用上线后必须具备的工程护栏:限流、日志、降级、缓存、审计、成本、任务队列和失败兜底。

简喵是一个证据约束型 AI 求职助手,核心链路是:

text
复制代码
简历模板 / 编辑 → 目标岗位分析(JD诊断) → 岗位竞争力诊断 → 投递文案 → PDF 导出

这类项目的特点很典型:AI 调用有成本,用户输入包含隐私,模型输出必须结构化,失败不能乱扣额度,生成结果还要能追溯。

简喵项目在线体验地址:在线体验

部署上线及域名绑定全流程:****

AI功能测试见上一篇博客:****


二、总链路:不要让业务接口直接调用模型

先看整体链路。后面所有护栏,都是围绕这条链路展开的。

很多早期项目会这样写:

text
复制代码
Controller → 拼 Prompt → 调模型 → 返回结果

这在 Demo 阶段可以跑,但上线后非常危险。更合理的结构应该是:

text
复制代码
用户请求 → API Controller → 鉴权 / CSRF / 参数校验 → 限流 → 额度预占 → 幂等任务检查 → 异步任务队列 → AI Gateway → 模型供应商 → JSON 校验 / 降级 → 结果入库 → 扣点提交或退款 → 前端轮询任务结果

可以画成这样:

mermaid
复制代码
flowchart TD A["用户提交 AI 请求"] --> B["鉴权 / 参数校验"] B --> C["用户级 + IP 级限流"] C --> D["额度预占 reserve"] D --> E["生成 inputHash + idempotencyKey"] E --> F{"是否已有相同任务"} F -->|有| G["返回已有 taskId"] F -->|无| H["写入 async_task"] H --> I["AI Gateway"] I --> J["模型供应商"] J --> K["结构化输出校验"] K -->|成功| L["结果入库 + commit 扣点"] K -->|失败| M["降级 / fallback + refund"] L --> N["审计日志"] M --> N N --> O["前端轮询展示"]

这里的关键点是:AI 调用不是一个普通同步接口,而是一个有成本、有状态、有失败补偿的任务。

后面按五层护栏展开:防成本失控、防重复提交、防模型输出失控、防问题无法追踪、防高成本任务拖垮服务。


三、第一层护栏:防止成本被刷爆

第一层护栏解决的是成本和滥用问题:谁能调用、多久能调用一次、哪些高成本接口必须更严格限制。

AI 应用不能只做一个全局限流。不同风险要分层。

1. 用户级限流

用户级限流解决的是“单个登录用户重复点击或恶意刷调用”。

例如:

text
复制代码
目标岗位分析:每用户每分钟 2 次 岗位竞争力诊断:每用户每分钟 1 次 投递文案:每用户每分钟 3 次

Redis key 可以这样设计:

text
复制代码
rate:user:{userId}:{feature}:{window}

例如:

text
复制代码
rate:user:123:job_score:202607041530

如果使用滑动窗口,key 可以更细:

text
复制代码
rate:user:{userId}:{feature}:sliding

value 用 sorted set 存时间戳。

Redis 官方文档里也明确提到,滑动窗口比固定窗口更能平滑处理突发请求,避免固定窗口边界被打穿。

2. IP 级限流

IP 级限流解决的是匿名请求、注册接口、登录接口、验证码接口、分享访问、PDF 下载等入口被刷。

例如:

text
复制代码
rate:ip:{clientIp}:auth_login rate:ip:{clientIp}:pdf_export rate:ip:{clientIp}:ai_submit

注意:如果你用了 Cloudflare / Nginx 反代,后端不能直接信任 X-Forwarded-For。必须只信任来自 Cloudflare 或受控反代的真实 IP 头,否则攻击者可以伪造 IP 绕过限流。

3. 高成本接口单独限流

AI 应用里不是所有接口成本一样。

低成本:

text
复制代码
投递短文案 简历字段建议 标题润色

高成本:

text
复制代码
长简历 + 长 JD 深度诊断 岗位竞争力体检 面试追问包 PDF 服务端导出 OCR / 文档解析

高成本接口应该有单独阈值:

text
复制代码
rate:user:{userId}:heavy_ai rate:ip:{ip}:heavy_ai rate:global:heavy_ai

这样即使单个用户额度足够,也不能瞬间把队列打爆。

4. Redis 缓存:缓存的是“同一输入 + 同一 Prompt 版本”的结果

AI 缓存不是简单把用户问题当 key。

错误做法:

text
复制代码
cacheKey = userInput

问题:

  • 泄露隐私;
  • 文本太长;
  • Prompt 改了以后还命中旧结果;
  • 不同模型结果混用;
  • 不同用户场景串污染。

正确做法:

text
复制代码
cacheKey = ai:cache:{feature}:{model}:{promptVersion}:{inputHash}

其中 inputHash 可以这样算:

text
复制代码
sha256( normalizedResumeVersion + normalizedJD + scenario + promptVersion )

缓存值里记录:

json
复制代码
{ "result": {}, "promptVersion": "job-score-v3", "model": "glm-5.2", "createdAt": "2026-07-04T12:00:00", "ttl": 86400 }

缓存 TTL 建议:

text
复制代码
投递文案:几小时到 1 天 JD 诊断:1 到 7 天 岗位竞争力诊断:1 到 7 天 高风险写入类建议:短 TTL 或不缓存

重复 Prompt 前缀可以降低延迟和成本,但这是模型供应商层面的缓存;业务侧仍需要自己的结果缓存和幂等。

5. token 成本记录:AI 成本不是月底看账单才知道

AI 成本必须在每次调用时记录。

至少记录:

text
复制代码
inputTokens outputTokens cachedTokens modelName unitPrice estimatedCost actualCostStatus

如果供应商返回 usage,就用供应商 usage。

如果不返回,就用本地 tokenizer 或估算逻辑,标记为 estimated。

一条成本记录可以长这样:

json
复制代码
{ "feature": "JOB_SCORE", "model": "GLM-5.2", "inputTokens": 6200, "outputTokens": 1800, "cachedTokens": 0, "estimatedCost": 0.018, "currency": "CNY", "traceId": "01JZ..." }

如果是免费产品,也不能没有成本记录。免费不等于没有成本,只是你暂时替用户承担了成本。

这一层护栏的目标很简单:不要等到账单爆了才发现问题。限流负责挡住异常调用,缓存负责减少重复调用,token 成本记录负责解释每一次调用到底花在哪里。


四、第二层护栏:防止重复提交和乱扣点

第二层护栏解决的是任务一致性问题:同一个输入不能重复创建任务,不能重复调用模型,更不能重复扣点。

1. AI 任务幂等:同一请求不要重复扣费

这是很多 AI 产品最容易忽略的问题。

用户点击“生成岗位竞争力诊断”,如果网络慢,他可能连续点三次。没有幂等的话,后端会创建三个任务,调用三次模型,扣三次点数。

正确做法是生成一个幂等 key:

text
复制代码
idempotencyKey = userId + feature + resumeVersionId + jobSessionId + inputHash + promptVersion + model

其中:

  • inputHash:用户输入、简历版本、JD 文本等核心输入的 hash;
  • promptVersion:Prompt 模板版本;
  • feature:功能名,例如 JD_DIAGNOSISJOB_SCOREDELIVERY_NOTE
  • resumeVersionId / jobSessionId:避免不同会话串结果。

示例代码:

java
复制代码
String key = buildIdempotencyKey(userId, feature, versionId, sessionId, inputHash, promptVersion); AiTask existing = taskRepository.findRunningOrRecentByKey(key); if (existing != null) { return existing.taskId(); } AiTask task = taskRepository.createQueuedTask(key, userId, feature); queue.submit(task); return task.taskId();

状态机建议至少包含:

text
复制代码
queued running succeeded failed canceled refunded

不要只用 success=true/false。AI 任务是长链路,状态必须可解释。

2. 失败不乱扣点:reserve、commit、refund 三段式

这是用户信任的底线。

错误做法:

text
复制代码
用户点击按钮 → 直接扣点 → 模型失败 → 用户没结果也没点数

正确做法:

text
复制代码
1. reserve:创建任务时预占额度 2. commit:任务成功并通过质量校验后正式扣点 3. refund:任务失败、超时、JSON 不合格、质量门禁拒绝时退回

状态流:

mermaid
复制代码
stateDiagram-v2 [*] --> RESERVED RESERVED --> COMMITTED: AI成功且结果可展示 RESERVED --> REFUNDED: 模型失败/超时/解析失败 RESERVED --> EXPIRED: 长时间未执行 COMMITTED --> [*] REFUNDED --> [*] EXPIRED --> [*]

账本表建议:

sql
复制代码
CREATE TABLE ai_credit_ledger ( id BIGINT PRIMARY KEY, user_id BIGINT, task_id BIGINT, feature VARCHAR(64), change_amount INT, balance_after INT, type VARCHAR(32), -- RESERVE / COMMIT / REFUND reason VARCHAR(255), trace_id VARCHAR(64), created_at DATETIME );

用户看到的文案也要讲人话:

text
复制代码
AI 服务暂时不可用,本次未消耗额度。

不要写:

text
复制代码
MODEL_EMPTY_OUTPUT reserve credits failed JSON parse error

内部错误码留给日志,不要泄漏给普通用户。

这一层护栏的目标不是提升模型效果,而是保护用户信任:网络慢、模型失败、JSON 解析失败,都不能让用户承担系统的不确定性。


五、第三层护栏:防止模型输出失控

第三层护栏解决的是模型输出不稳定问题:模型可以生成内容,但不能直接决定业务结果。所有输出都必须经过错误分类、Schema 校验、质量门禁和前端可理解的提示。

1. 模型错误码分类:不要全都叫服务器异常

模型调用失败不等于系统崩了。至少要分这些类型:

错误码含义用户提示
MODEL_TIMEOUT模型超时AI 服务响应较慢,请稍后重试
MODEL_RATE_LIMITED供应商限流当前 AI 服务繁忙,已保留你的内容
MODEL_AUTH_FAILEDAPI Key / 鉴权失败服务配置异常,请联系管理员
MODEL_SERVER_ERROR模型供应商 5xxAI 服务暂不可用,请稍后再试
MODEL_CONTENT_BLOCKED内容安全拦截当前内容无法生成,请调整输入
MODEL_EMPTY_OUTPUT返回为空已生成保守兜底版本或未消耗额度
JSON_PARSE_FAILEDJSON 解析失败AI 返回格式异常,系统已降级处理
SCHEMA_VALIDATION_FAILEDSchema 校验失败结果结构不完整,未写入最终结果
QUALITY_GATE_REJECTED质量门禁拒绝本次结果未达到展示标准,未扣点或已退回

2. JSON 解析失败兜底:结构化输出也不能完全裸奔

AI 应用经常要求模型返回 JSON:

json
复制代码
{ "score": 82, "summary": "...", "risks": [], "fixTasks": [] }

但真实线上会遇到:

text
复制代码
- JSON 前后带 Markdown - 少一个右括号 - enum 返回了不存在的值 - 数组变成字符串 - 空字段过多 - 模型把解释写到 JSON 外面

如果直接 JSON.parse(),前端就会炸。

生产级做法:

text
复制代码
模型输出 → 提取 JSON 块 → JSON parse → Schema 校验 → 一次修复 → 再校验 → 失败则 fallback

如果模型支持结构化输出,优先使用 Schema 约束。但即使使用结构化输出,仍建议保留后端 Schema 校验,因为你不应该把模型输出直接写入数据库或 HTML。

伪代码:

java
复制代码
try { ParsedResult result = parser.parse(rawOutput); validator.validate(result); return result; } catch (JsonParseException e) { String repaired = jsonRepair.repair(rawOutput); ParsedResult repairedResult = parser.parse(repaired); validator.validate(repairedResult); return repairedResult; } catch (Exception e) { return fallbackResult(input); }

fallback 不是假结果,而是保守结果:

text
复制代码
AI 返回格式异常,本次未写入高风险建议。 系统基于已有简历和 JD 生成保守提示: 1. 哪些经历可直接强调 2. 哪些要求需要补证据 3. 哪些内容不能硬写

3. Prompt Injection 和输出处理:不能相信模型,也不能相信用户输入

AI 应用不是传统后端接口。Prompt 里既有系统规则,也有用户输入,还有外部文档。模型不天然区分“指令”和“数据”。

生产上至少要做:

text
复制代码
用户输入作为数据,不作为指令 Prompt 明确边界 模型输出必须 Schema 校验 模型输出进入 HTML 前必须净化 模型输出进入数据库前必须校验 模型输出进入命令、SQL、配置前必须禁止或人工确认

以简历/JD 场景为例,最重要的规则是:

text
复制代码
JD 是岗位要求,不是候选人事实。 简历是候选人事实来源。 没有证据的内容只能写成缺口、建议、待补充,不能写成经历。

这就是“证据约束”。

4. 用户界面也要有护栏:别把内部错误丢给用户

后端做了很多护栏,如果前端直接展示内部错误,用户体验还是很差。

错误示例:

text
复制代码
JSON_PARSE_FAILED reserve credits failed same AI task creating model output empty

应该翻译成:

text
复制代码
AI 服务暂时没有返回可用结果,本次未消耗额度。 当前已有相同任务正在处理中,请稍后查看结果。 当前请求较复杂,已为你保留输入内容,可以稍后重试。

用户只需要知道三件事:

text
复制代码
发生了什么 有没有扣额度 下一步能做什么

六、第四层护栏:防止问题无法追踪

第四层护栏解决的是线上排查问题:出错时,必须知道是哪次请求、哪个用户、哪个模型、哪个 Prompt 版本、哪一步失败、有没有扣点、有没有退款。

1. traceId:所有日志必须能串起来

没有 traceId 的系统,线上问题基本靠猜。

一次 AI 调用至少跨这些层:

text
复制代码
浏览器 → Nginx → Backend API → Async Task → AI Gateway → Model Provider → JSON Parser → DB → 前端轮询

必须在入口生成 traceId:

text
复制代码
X-Trace-Id: 01JZ...

如果前端已经传了可信 traceId,可以复用;否则后端生成。

日志中至少带:

json
复制代码
{ "traceId": "01JZ...", "userId": 123, "feature": "JOB_SCORE", "taskId": "2072...", "sessionId": "2072...", "resumeVersionId": "2072...", "promptVersion": "job-score-v3", "model": "glm-4-flash", "status": "FAILED", "errorCode": "MODEL_TIMEOUT" }

trace 的核心定义是把一次请求拆成多个 span,并用上下文把它们关联起来。OpenTelemetry Traces
日志也应该带 trace 信息,方便从错误日志跳到调用链。OpenTelemetry Logs

2. AI 调用日志:不要只记“调用成功”

AI 调用日志不是为了好看,而是为了回答这些问题:

  • 谁调用了?
  • 调用了哪个功能?
  • 输入多长?
  • 用了哪个 Prompt 版本?
  • 用了哪个模型?
  • 花了多少 token?
  • 成功还是失败?
  • 失败原因是什么?
  • 有没有扣点?
  • 有没有退款?
  • 结果是否写入数据库?
  • 是否命中缓存?
  • 是否触发降级?

建议建一张调用日志表:

sql
复制代码
CREATE TABLE ai_call_log ( id BIGINT PRIMARY KEY, trace_id VARCHAR(64), task_id BIGINT, user_id BIGINT, feature VARCHAR(64), model_provider VARCHAR(64), model_name VARCHAR(128), prompt_version VARCHAR(64), input_hash VARCHAR(128), request_tokens INT, response_tokens INT, cached_tokens INT, estimated_cost DECIMAL(10, 6), status VARCHAR(32), error_code VARCHAR(64), latency_ms BIGINT, cache_hit BOOLEAN, created_at DATETIME );

注意:不要把完整简历、JD、身份证、手机号、邮箱原文直接写进日志。
日志应该存 hash、长度、版本、状态、错误码。真正的用户内容要有更严格的权限和保留策略。

3. promptVersion:没有版本号,缓存和审计都会失真

Prompt 是 AI 应用里的业务逻辑。改 Prompt 就等于改规则。

所以每个功能都应该有版本号:

text
复制代码
jd-diagnosis-v1 jd-diagnosis-v2 job-score-v3 delivery-note-v4

日志、缓存、任务、结果都要带 promptVersion

为什么?

方便回滚

如果新 Prompt 质量变差,可以快速定位:

text
复制代码
job-score-v4 之后投诉增加

避免缓存污染

Prompt 改了,旧缓存不能继续命中。

text
复制代码
cacheKey = feature + inputHash + promptVersion

方便解释结果

用户问:“为什么这次结果和上次不一样?”

至少能回答:

text
复制代码
模型版本不同 / Prompt 版本不同 / 输入版本不同

4. 审计记录:不是为了“企业级”,是为了出事能查

AI 应用的审计至少覆盖这些动作:

text
复制代码
用户提交 AI 请求 模型调用开始 模型调用结束 结果解析 质量门禁 额度扣减 / 退款 结果写入数据库 PDF 导出 分享链接创建 管理员查看

审计记录建议包含:

json
复制代码
{ "actorUserId": 123, "action": "AI_JOB_SCORE_CREATE", "targetType": "JOB_SESSION", "targetId": "2072...", "traceId": "01JZ...", "ip": "masked", "userAgent": "masked", "result": "SUCCEEDED", "createdAt": "2026-07-04T12:00:00" }

注意两点:

1. 审计日志不等于业务日志

业务日志偏调试:

text
复制代码
模型耗时、错误栈、状态流

审计日志偏追责:

text
复制代码
谁对什么资源做了什么操作,结果如何

2. 审计日志不能存太多敏感内容

不要把完整简历、JD、Prompt 原文都塞进审计日志。
应该存资源 ID、hash、版本号、traceId。


七、第五层护栏:防止高成本任务拖垮服务

第五层护栏解决的是非模型资源问题。AI 应用里的高成本任务不只有模型调用,PDF、OCR、图片生成、文档解析这些任务也会吃 CPU、内存、浏览器进程和队列资源。

1. PDF 任务队列:PDF 导出不是普通下载接口

如果你的产品有 PDF 导出,尤其是服务端 Puppeteer/Chromium 渲染,必须做队列。

因为 PDF 导出会吃:

  • CPU
  • 内存
  • 浏览器进程
  • 字体渲染
  • 页面截图/分页
  • 文件写入

不能让用户直接同步打 Puppeteer。

推荐结构:

text
复制代码
用户点击导出 → 创建 pdf_task → 入队 → worker 拉取 → Puppeteer 渲染 → 写文件 → 更新状态 → 用户轮询下载

PDF 任务状态:

text
复制代码
queued rendering succeeded failed expired

并发限制:

text
复制代码
PDF_TASK_QUEUE_LIMIT=8 PUPPETEER_MAX_CONCURRENT=1 PUPPETEER_MAX_QUEUE_SIZE=8

单机小服务器建议从 1 个 Puppeteer 并发开始。不要一上来开 5 个。

还要做三层限制:

text
复制代码
用户级:每用户每分钟最多 N 次导出 IP 级:每 IP 每分钟最多 N 次导出 全局级:Puppeteer 同时最多 N 个渲染

失败兜底:

text
复制代码
服务端 PDF 导出失败 → 返回 taskId + 错误摘要 → 前端提示用户 → 提供浏览器打印/导出兜底

不要让用户只看到:

text
复制代码
服务器异常

八、最小数据库设计参考

如果把上面的护栏落到数据库,一个够用的 AI 任务系统,可以从这几张表开始:

text
复制代码
async_task -- AI 异步任务表,记录一次 AI/PDF 等任务的生命周期 ai_call_log -- AI 调用日志表,记录模型调用、耗时、token、错误码等信息 ai_credit_ledger -- AI 额度账本表,记录 reserve / commit / refund 等扣点流水 ai_prompt_version -- Prompt 版本表,记录每个功能使用的 Prompt 版本 ai_result_snapshot -- AI 结果快照表,保存一次任务生成后的结构化结果 audit_log -- 审计日志表,记录谁在什么时候对什么资源做了什么操作

async_task

text
复制代码
id -- 任务主键 ID user_id -- 发起任务的用户 ID feature -- 功能类型,例如 JD_DIAGNOSIS、JOB_SCORE、DELIVERY_NOTE、PDF_EXPORT idempotency_key -- 幂等 key,用来避免同一输入重复创建任务 status -- 任务状态,例如 queued、running、succeeded、failed、canceled、refunded progress -- 任务进度,例如 0 到 100,方便前端展示处理进度 trace_id -- 链路追踪 ID,用来串联前端、后端、AI 调用、日志和审计记录 error_code -- 错误码,例如 MODEL_TIMEOUT、JSON_PARSE_FAILED、PDF_RENDER_FAILED error_message -- 错误摘要,记录可排查的失败原因,不建议存敏感原文 created_at -- 任务创建时间 updated_at -- 任务最后更新时间

ai_call_log

text
复制代码
id -- AI 调用日志主键 ID task_id -- 关联的 async_task 任务 ID trace_id -- 链路追踪 ID,用来和任务、额度账本、审计日志串联 provider -- 模型供应商,例如 OpenAI、DeepSeek、智谱、通义等 model -- 实际调用的模型名称 prompt_version -- 本次调用使用的 Prompt 版本 input_hash -- 输入内容 hash,用来标识相同输入,避免直接记录简历、JD 等敏感原文 request_tokens -- 输入 token 数 response_tokens -- 输出 token 数 estimated_cost -- 本次调用的估算成本 status -- 调用状态,例如 succeeded、failed、fallback、cached error_code -- 调用失败时的错误码,例如 MODEL_RATE_LIMITED、MODEL_TIMEOUT latency_ms -- 模型调用耗时,单位毫秒

ai_credit_ledger

text
复制代码
id -- 额度流水主键 ID user_id -- 用户 ID task_id -- 关联的 async_task 任务 ID feature -- 消耗额度的功能类型 type -- 流水类型,例如 RESERVE、COMMIT、REFUND change_amount -- 额度变化数量,预占/扣减通常为负数,退款通常为正数 balance_after -- 本次流水发生后的用户额度余额 reason -- 额度变化原因,例如 AI 任务成功、模型超时退款、JSON 解析失败退款 trace_id -- 链路追踪 ID,用来和任务、AI 调用日志、审计日志串联

ai_prompt_version

text
复制代码
id -- Prompt 版本主键 ID feature -- 功能类型,例如 JD_DIAGNOSIS、JOB_SCORE、DELIVERY_NOTE prompt_version -- Prompt 版本号,例如 job-score-v3、delivery-note-v4 model -- 默认绑定或推荐使用的模型 status -- 版本状态,例如 active、deprecated、testing description -- 版本说明,例如本次 Prompt 改动解决了什么问题 created_at -- 版本创建时间 updated_at -- 版本最后更新时间

ai_result_snapshot

text
复制代码
id -- 结果快照主键 ID task_id -- 关联的 async_task 任务 ID user_id -- 用户 ID feature -- 功能类型 prompt_version -- 生成该结果时使用的 Prompt 版本 model -- 生成该结果时使用的模型 input_hash -- 输入内容 hash,用来标识结果对应的输入版本 result_json -- 结构化结果 JSON,例如评分、风险、建议、修复任务等 status -- 结果状态,例如 valid、fallback、rejected created_at -- 结果生成时间

audit_log

text
复制代码
id -- 审计日志主键 ID actor_user_id -- 操作者用户 ID,例如普通用户或管理员 action -- 操作类型,例如 AI_JOB_SCORE_CREATE、PDF_EXPORT、ADMIN_VIEW_RESULT target_type -- 被操作资源类型,例如 JOB_SESSION、RESUME_VERSION、AI_TASK、PDF_TASK target_id -- 被操作资源 ID trace_id -- 链路追踪 ID,用来追踪这次操作关联的完整链路 result -- 操作结果,例如 SUCCEEDED、FAILED、DENIED created_at -- 审计记录创建时间

这套表不复杂,但足够支撑上线后的定位、退款、审计和成本复盘。


九、上线验收:不是能跑就算完成

AI 应用上线前,我建议至少跑这些场景:

限流

text
复制代码
同一用户连续点击 10 次 同一 IP 连续请求 50 次 高成本接口并发请求

验收:

text
复制代码
不会重复调用模型 不会重复扣点 用户看到明确提示

幂等

text
复制代码
同一简历 + 同一 JD + 同一功能重复提交

验收:

text
复制代码
返回同一个 taskId 或接管已有任务

JSON 失败

text
复制代码
模拟模型返回: - 半截 JSON - Markdown 包裹 JSON - 缺字段 - enum 错误

验收:

text
复制代码
系统降级,不崩溃,不写入危险结果

成本

text
复制代码
跑 10 次不同功能

验收:

text
复制代码
能看到 token、模型、功能、估算成本、是否命中缓存

额度

text
复制代码
成功 失败 超时 质量门禁拒绝

验收:

text
复制代码
成功才 commit 失败 refund 账本可解释

PDF

text
复制代码
并发导出 长简历导出 Puppeteer 超时

验收:

text
复制代码
队列可控 失败可查 taskId 前端有兜底

十、我认为 AI 应用最小生产标准是什么?

如果只能保留 10 条,我会选这些:

text
复制代码
1. 用户级限流 2. IP 级限流 3. 高成本接口单独限流 4. AI 任务幂等 5. traceId 6. AI 调用日志 7. JSON Schema 校验 8. 失败退款 9. token 成本记录 10. 审计日志

如果你的应用有 PDF、图片生成、OCR、RAG、Agent,再额外加:

text
复制代码
PDF / 图片 / OCR 队列 外部 URL 白名单 工具权限边界 RAG 引用校验 Agent 操作确认

十一、简喵里我实际做了哪些最小护栏?

已做:

text
复制代码
用户级限流 / Redis 缓存 / inputHash / promptVersion / AI 调用日志 / 失败不乱扣点 / PDF 导出限制 / traceId

进行中:

text
复制代码
更细粒度错误码 / 后台审计面板 / 成本看板 / PDF 异步队列优化

后续补齐:

text
复制代码
OpenTelemetry trace / 更完整的模型降级策略 / 管理员审计查询 / 高成本接口全局熔断

十二、结尾:AI 应用上线,拼的不是“接了哪个模型”

模型 API 越来越容易接。真正难的是:

text
复制代码
怎么限制成本 怎么处理失败 怎么保证输出可控 怎么让用户信任 怎么让出事时能追踪 怎么让免费能力不被滥用

一个能上线的 AI 应用,不应该只是:

text
复制代码
Prompt + API Key + 前端展示

而应该至少有:

text
复制代码
限流 幂等 队列 日志 缓存 降级 审计 成本 权限 兜底

这些东西看起来不酷,但决定了产品能不能真的给用户用。

Demo 阶段可以靠一次成功截图。
生产阶段必须靠失败时也不崩的系统设计。


参考资料

我是 Ryan,一个专注于可信 AI 应用工程的开发者,我的个人技术博客,相比让 AI 生成更多内容,我更关心它的回答是否有证据,过程是否可追溯,结果是否经得起验证。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
Ryan
下载 APP