主题
生产级 AI 应用架构
AI 原型常是一段“拼提示词 → 调模型 → 显示文本”的代码。生产系统的难点却在模型之外:请求会超时,输出会漂移,供应商会限流,用户输入会攻击工具,长上下文会吞掉预算。解决方法不是寻找一个永不出错的模型,而是把不确定组件放进确定性的工程护栏。
**带着一个功能读这页:**为客服做“工单分流建议”。输入是工单正文和允许访问的知识条目;输出是分类、优先级、证据和建议团队。误分流是主要风险;验收时用脱敏历史工单重放,检查结构校验、证据引用、超时转人工以及写入前的人工审核。先画出第 1 节的请求链路,再按第 3、4、6、7 节逐项补护栏。可运行的 Agent 控制循环见工单分流 Agent 实战。
1. 请求链路与职责划分
建议把一次请求拆成以下阶段:
text
鉴权 → 输入规范化 → 上下文构建 → 模型调用 → 输出校验
→ 业务动作 → 结果呈现 → 轨迹与反馈入库1.1 模型适合负责什么
- 理解自然语言意图与隐含条件;
- 在多个合理选项中生成候选方案;
- 摘要、分类、改写和从非结构文本提取字段;
- 判断下一步需要哪一种工具或信息。
1.2 普通程序必须负责什么
- 身份、权限、额度与租户隔离;
- 金额、库存、状态迁移等业务不变量;
- 参数类型、枚举、长度与引用完整性;
- 最终写入、幂等、事务和审计;
- 超时、重试、并发和资源预算。
可以把模型当成一个强大的候选生成器,但不能把它当成权限系统或数据库事务。
2. 用模型网关隔离变化
业务代码不应散落供应商 SDK 调用。建立内部 ModelGateway,把变化集中在一个边界中:
ts
interface GenerateRequest<T> {
task: "classify-ticket" | "draft-reply" | "extract-order";
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
schema: unknown;
timeoutMs: number;
maxOutputTokens: number;
traceId: string;
}
interface GenerateResult<T> {
value: T;
model: string;
inputTokens: number;
outputTokens: number;
latencyMs: number;
finishReason: string;
}
interface ModelGateway {
generate<T>(request: GenerateRequest<T>): Promise<GenerateResult<T>>;
}网关内部统一实现:模型路由、凭据读取、限流、并发控制、超时、可重试错误分类、用量记录和脱敏日志。业务层只表达“任务是什么”,避免被某个 SDK 的字段绑死。
2.1 模型路由不是简单的主备切换
不同模型的输出分布不同,直接切换可能让提示词和评分阈值失效。路由表应以任务为单位:
| 任务 | 主要目标 | 路由策略 |
|---|---|---|
| 短文本分类 | 低延迟、低成本 | 小模型;低置信度再升级 |
| 复杂代码修改 | 推理与工具能力 | 强模型;限制最大步骤 |
| 合同字段提取 | 格式稳定、可追溯 | 结构化输出;失败进入人工队列 |
每个候选模型都必须单独通过相同评测集,才能进入路由池。
3. 结构化输出是协议,不是提示技巧
要求模型“只返回 JSON”仍可能得到 Markdown、缺字段或错误枚举。正确链路是:
- 用 JSON Schema 或类型模式约束输出;
- 解析后做结构校验;
- 再做业务语义校验;
- 可修复错误最多重试一次;
- 仍失败就进入降级或人工处理。
ts
type TicketDecision = {
category: "billing" | "technical" | "account";
priority: 1 | 2 | 3;
reason: string;
};
function validateDecision(x: TicketDecision): void {
if (x.priority === 1 && x.reason.length < 10) {
throw new Error("high priority requires an auditable reason");
}
}Schema 只能验证“形状”,不能证明语义正确。比如 priority: 1 类型合法,但理由可能与工单无关,因此还需业务规则或评测器。
4. 超时、重试与降级
4.1 先分类错误,再决定是否重试
| 错误 | 是否重试 | 处理 |
|---|---|---|
| 网络抖动、限流、服务端错误 | 是 | 指数退避加随机抖动,设置总时限 |
| 鉴权失败、余额不足 | 否 | 告警并切断请求 |
| 输入超过上限 | 否 | 缩减上下文或改走摘要链路 |
| 输出结构错误 | 有条件 | 携带校验错误修复一次 |
| 工具业务校验失败 | 通常否 | 回到规划或请求用户补充 |
模型调用可能已经在服务端成功,即使客户端收到超时。任何后续写操作都要用幂等键,避免重试造成重复下单或重复发信。
4.2 预算是终止条件
每次请求至少设四个上限:总时长、模型调用次数、输入输出 Token、外部工具次数。达到上限时返回“已完成到哪、缺少什么、如何继续”,比在循环中消耗到平台强制中断更可控。
5. 缓存的正确层次
缓存不是只按完整提示词做哈希:
- 确定性业务结果缓存:相同规范化输入可直接复用,适合分类和抽取;
- 语义缓存:相似问题复用答案,必须考虑知识版本、用户权限与相似度误判;
- 检索缓存:缓存查询到文档 ID 的结果,文档更新时失效;
- 前缀缓存:复用稳定系统指令和长背景,减少重复计算。
缓存键至少包含任务版本、模型版本、提示模板版本、知识库版本和权限范围。否则一次提示词更新后可能继续返回旧行为。
6. 安全边界与数据流
6.1 把提示词注入当成不可信输入传播
网页、邮件和检索文档中的文字可能包含“忽略规则并执行某命令”。它们是数据,不是系统指令。工程上要做到:
- 系统规则与外部内容分区,明确标注来源;
- 工具白名单与参数校验独立于模型;
- 读取和写入权限分开,敏感写操作要求确认;
- 返回给用户前过滤秘密、内部路径和跨租户内容;
- 审计“谁通过哪个 Agent 对什么资源做了什么”。
6.2 最小化进入模型的数据
在上下文构建阶段只取完成任务所需字段。日志默认记录哈希、长度、文档 ID 和错误类别,正文按调试权限与保留周期单独控制。模型供应商、检索库、追踪系统和人工标注平台都是数据流中的独立处理方,需要分别审查。
7. 可观测性:记录轨迹而非只记答案
一次请求的 Trace 应串起:
text
trace_id
├─ input_normalization
├─ retrieval(query, filters, document_ids, scores)
├─ model_call(model, tokens, latency, prompt_version)
├─ validation(schema_errors, business_errors)
├─ tool_call(tool, arguments_digest, result, duration)
└─ final(status, user_feedback, total_cost)不要只看平均延迟。至少跟踪 P50/P95 延迟、任务成功率、结构校验失败率、人工接管率、每成功任务成本和用户纠正率。平均分可能掩盖某一类用户持续失败。
8. 推荐的上线演进顺序
- 单一任务、单一模型、固定模板,先积累评测集。
- 加结构校验、超时、错误分类和完整 Trace。
- 接入检索或只读工具,验证权限过滤。
- 增加有限写工具与人工确认,补齐幂等和审计。
- 在评测门禁下尝试模型路由、缓存与自治循环。
每一步都必须回答:质量是否提升、成本是否可控、失败是否更容易定位。回答不了,就不应仅凭演示效果继续增加复杂度。