在SaaS中接入LLM时,从请求体取tenantId不可靠,须从认证态解析;模型本身不是鉴权层;发送字段要最小化;JSON模式只管格式不管值正确性,需用zod schema验证输出;日志和重试系统可能保留请求,需注意敏感数据顺序。
在现有的 SaaS 产品中添加一次 LLM 调用,看起来可能微不足道:获取一份文档、发送给模型、返回摘要。在多租户系统中,这个端点会同时跨越多条信任边界。一个在演示中运行良好的功能,仍然可能泄露错误租户的数据、接受未授权的文档 ID,或者将输出保存到错误的账户下。
我在将 AI 端点视为生产就绪之前,会使用这份检查清单。
永远不要将 tenantId 从请求体中接受为作用域的证明。应当从已认证的用户或服务身份中解析出它,然后将这个可信的作用域携带到每一次查询中。
const document = await prisma.document.findFirst({
where: {
id: input.documentId,
tenantId: auth.tenantId,
},
});
if (!document) {
throw new NotFoundException();
}
对缺失文档和未授权文档都返回 404,也能避免确认某条记录存在于另一个租户中。
模型不是授权层。它只应该接收调用者已被允许访问的数据。在构建 prompt 之前,检查租户作用域、用户角色、文档状态和字段级限制。
这个顺序很重要,因为日志和重试系统可能会保留模型请求。如果受限数据曾经到达提供商一次,在此之后拒绝响应就太晚了。
发送任务所需的字段,而不是整条数据库记录。排除内部备注、凭证、不相关的客户数据,以及不可能影响回答的元数据。
小的 prompt 更便宜,也更容易审计。它还能减少提供商日志或调试追踪泄露时造成的损害。
JSON 模式改进了格式。但它不会让值变得正确。
用 schema 验证响应,强制执行长度和枚举限制,并拒绝未知字段。
const SummarySchema = z.object({
summary: z.string().min(1).max(2_000),
category: z.enum(["billing", "support", "sales", "other"]),
confidence: z.number().min(0).max(1),
});
const result = SummarySchema.parse(modelResponse);
如果输出将触发另一个操作,在验证后添加一个确定性策略检查。
AI 结果不应该成为一个自由漂浮的制品。请将租户、源记录、模型、prompt 版本和请求标识与它一起持久化。这使得后续读取是安全的,并为支持团队提供了足够的上下文来调查一个错误的结果。
最有价值的测试不是快乐路径的摘要。它们证明的是:
对于行级安全性,请至少通过一条与生产环境中使用的相同的数据库角色运行一次集成测试。模拟的仓库无法证明策略是正确的。
用租户安全的标签追踪延迟、提供商错误、验证失败、token 使用量和成本。避免将客户文本放入指标或错误消息中。运维人员应该能够回答:"这是只有一个租户在失败,还是所有人都在失败?"而不需要打开原始 prompt。
AI 端点终究还是一个应用端点。认证、租户作用域、授权、数据最小化、验证、持久化、测试和可观测性,都应该先于信任模型的回答。
模型调用可能是架构中最新的那一行,但最古老的安全规则仍然适用。