多租户 Prompt 隔离:不同业务的提示词不能互相污染

📅 2026/7/16 18:53:55
多租户 Prompt 隔离:不同业务的提示词不能互相污染
多租户 Prompt 隔离不同业务的提示词不能互相污染一、个性化深度引言凌晨三点排查一个线上诡异现象电商推荐接口正常返回结果但那句系统提示词里夹杂了一段医疗问诊的模板前缀。排查了两小时链路追踪定位到共享 Prompt 模板缓存。同一个模型实例同时服务三个不同业务线。Prompt 模板在内存中拼接时因为并发没有做数据隔断B业务的系统指令被拼入了A业务的用户输入前。这个问题在单租户场景下不会出现。但当你把一个 SaaS 平台的模型服务同时开放给多个业务方租户隔离就不再是可选项。见证奇迹的时刻不在于修好了这个 Bug。而在于意识到多租户 Prompt 隔离本质上是一个分布式系统的一致性问题。它需要的不是小心一点而是架构层的隔离机制。二、个性化原理剖析Prompt 隔离的核心问题不是简单的字符串拼接。在分布式推理架构中多个租户共享一个模型实例池每个推理请求的完整上下文由三部分组成租户级系统 Prompt定义角色、规则、输出格式会话级历史消息当前对话的上下文窗口请求级用户输入单次请求的 query隔离失效的典型场景发生在以下环节缓存层Prompt 模板被多线程共享时引用传递导致覆盖批处理层Continuous Batching 中不同租户的请求被打包到同一批次中间件层请求拦截器在异步环境下没有正确绑定租户上下文正确的隔离方案需要在缓存、拼接和上下文绑定三个环节同时建立边界。隔离层的核心策略命名空间隔离每个租户用独立 Key 前缀存储模板如tenant:{id}:prompt:v1避免并发读写冲突。深拷贝拼接每次请求从缓存取出模板后执行deepcopy再与用户输入拼接。隔离层的核心约束是模板本身是不可变对象。上下文绑定使用 Pythoncontextvars在异步环境中传递租户标识确保同一个请求链路中租户 ID 不丢失。见证奇迹的时刻——当你把这三种机制组合到同一个推理网关时所有跨租户污染的问题同时消失了。不是概率降低是架构上的杜绝。三、个性化代码实践import copy import contextvars from typing import Dict, Optional from dataclasses import dataclass, field # 设计原因使用 contextvars 确保异步环境下租户上下文不丢失 # 避免 threading.local 在 asyncio 中的变量混乱问题。 tenant_id_ctx: contextvars.ContextVar[str] contextvars.ContextVar( tenant_id, defaultdefault ) dataclass(frozenTrue) class PromptTemplate: 设计原因使用 frozenTrue 保证模板不可变。 任何拼接操作都返回新对象杜绝引用覆盖导致的跨租户污染。 system_prompt: str version: str tenant_id: str metadata: Dict field(default_factorydict) class PromptIsolationManager: 设计原因按租户分离模板存储使用 namespace 隔离。 所有读取操作先做租户ID校验防止越权访问。 def __init__(self): # 设计原因内存缓存使用 dict 嵌套外层 Key 为租户 ID # 确保不同租户的模板在存储层就完成了物理隔离。 self._templates: Dict[str, Dict[str, PromptTemplate]] {} def register_template(self, template: PromptTemplate) - None: 注册租户专用Prompt模板 if template.tenant_id not in self._templates: self._templates[template.tenant_id] {} self._templates[template.tenant_id][template.version] template def resolve_prompt( self, tenant_id: str, version: str, user_input: str ) - Optional[str]: 设计原因 1. 先从租户专用缓存获取模板 2. 使用 copy.deepcopy 做深拷贝避免修改缓存中的原始模板 3. cross-check 租户 ID防止内部调用传错参数 4. 拼接后返回不可变的字符串结果 tenant_templates self._templates.get(tenant_id) if not tenant_templates: return None template tenant_templates.get(version) if not template: return None # 设计原因深拷贝确保即使下游代码意外修改 # 也不会影响缓存的原始模板。 safe_template copy.deepcopy(template) # 设计原因双重校验即使传参错误也能在运行时捕获。 if safe_template.tenant_id ! tenant_id: raise PermissionError( fTenant mismatch: {safe_template.tenant_id} ! {tenant_id} ) return f{safe_template.system_prompt}\n\n{user_input} async def handle_inference_request(user_input: str): 设计原因从 contextvars 获取当前租户 ID 确保异步调度下也能正确隔离租户上下文。 tenant_id tenant_id_ctx.get() manager PromptIsolationManager() prompt manager.resolve_prompt( tenant_idtenant_id, versionv1, user_inputuser_input, ) if prompt is None: raise ValueError(fNo template found for tenant: {tenant_id}) # 此处调用模型推理 response await model_inference(prompt) return response四、个性化边界权衡1. 隔离粒度租户级 vs 用户级租户级隔离足够覆盖多业务线场景维护成本低。如果细化到用户级隔离每个用户独立 Prompt 模板会导致内存膨胀 N 倍。折中方案是租户级模板 用户级参数注入。模板本身租户共享用户个性化部分通过运行时变量替换完成。2. 缓存策略内存 vs 外部存储内存缓存Redis/本地响应延迟低1ms但模板更新需要主动失效机制。外部存储数据库保证一致性但每次请求查询增加 5-10ms 延迟。建议内存热缓存 定期同步 版本号比对机制。3. 深拷贝 vs Copy-on-Write每次深拷贝有内存分配开销约 0.1-1ms。如果 Prompt 模板长达数千 Token高频请求下 GC 压力显著。Copy-on-Write 策略可减少拷贝但要求下游所有消费方都遵循不可变约束——这在工程实践中很难保证。4. 租户校验网关层 vs 服务层在 API 网关做租户校验可以最早拦截非法请求。但 Prompt 拼接发生在服务内部网关校验和实际使用的租户 ID 可能不一致。最安全的做法是双重校验网关做第一次过滤服务内部在拼接前做第二次验证。5. 隔离开销 vs 业务灵活性最安全的隔离方案是每个租户独占模型实例。但成本极高GPU 利用率可能低于 10%。共享实例 软件隔离是成本与安全的平衡点。额外的隔离代码开销在推理总延迟中占比 1%可以接受。五、总结多租户 Prompt 隔离的架构设计核心在于三个维度的保障存储层的命名空间分离、运行时的不变性保护、调用链的上下文传递。通过contextvars 深拷贝 租户校验的三层机制可以在共享模型实例的前提下将跨租户污染的概率从偶发降低至零。工程实践中需要考虑隔离粒度与性能开销的平衡建议采用租户级模板 用户级参数注入的折中方案。最终目标不是增加安全层数而是在最少的代码路径上实现确定的隔离保障。