发布时间:2026/9/6 19:20:05
Agent 多租户隔离设计:不同客户的任务别混在同一个上下文里 Agent 多租户隔离设计不同客户的任务别混在同一个上下文里一、客户 A 的 Agent 调出了客户 B 的数据SaaS 平台的 AI 助手上线后客户 A 的售后客服问 Agent查看最近订单Agent 返回了客户 B 的订单明细。排查发现 Agent 的知识库检索范围是全库而非租户级别的——不同客户的数据混在了同一个检索空间里。Agent 多租户的本质不只是数据隔离还有上下文隔离、任务隔离和成本隔离。租户 A 的对话上下文不能污染租户 B、租户 A 的任务失败不影响租户 B、租户 A 的成本需要独立核算。二、多租户隔离的四个维度flowchart TD subgraph Data[数据隔离] D1[向量库: 按 TenantID 分区] D2[数据库: Row-Level Security] D3[缓存: 按 TenantID 做 Key 前缀] end subgraph Context[上下文隔离] C1[会话上下文绑定 TenantID] C2[Agent 记忆按租户隔离] C3[工具调用权限按租户配置] end subgraph Task[任务隔离] T1[任务队列: 按租户的独立队列?] T2[并发限制: 每租户最大并发数] T3[失败重试: 租户粒度而非全局] end subgraph Cost[成本隔离] CO1[Token 使用量按租户归集] CO2[模型调用次数独立统计] CO3[成本账单按租户生成] end API[API 请求] -- Auth[认证: 提取 TenantID] Auth -- C1 Auth -- D1 Auth -- CO1多租户隔离的核心原则所有数据操作必须携带 TenantID所有任务调度都要考虑租户的资源配额所有成本统计都要归属到具体租户。三、Go 实现多租户 Agent 中间件package multitenant import ( context fmt sync time ) // 租户上下文 type contextKey string const ( KeyTenantID contextKey tenant_id KeyTenantTier contextKey tenant_tier // free, pro, enterprise ) // TenantInfo 租户信息 type TenantInfo struct { ID string Name string Tier string // free, pro, enterprise MaxConcurrent int // 最大并发任务数 MaxTokensPerDay int64 // 每日 Token 配额 AllowedTools []string // 允许使用的工具列表 RateLimitQPS int // 租户级别 QPS 限制 } // WithTenant 给 context 注入租户信息 func WithTenant(ctx context.Context, info *TenantInfo) context.Context { ctx context.WithValue(ctx, KeyTenantID, info.ID) ctx context.WithValue(ctx, KeyTenantTier, info.Tier) return ctx } // TenantIDFromContext 从 context 中获取租户 ID func TenantIDFromContext(ctx context.Context) (string, bool) { id, ok : ctx.Value(KeyTenantID).(string) return id, ok } // 租户管理器 // TenantManager 租户管理器 type TenantManager struct { tenants map[string]*TenantInfo mu sync.RWMutex } func NewTenantManager() *TenantManager { return TenantManager{ tenants: make(map[string]*TenantInfo), } } func (tm *TenantManager) Register(info *TenantInfo) { tm.mu.Lock() defer tm.mu.Unlock() tm.tenants[info.ID] info } // 数据隔离中间件 // TenantDataFilter 数据层租户过滤器 type TenantDataFilter struct { db *mockDB // 实际项目中是真实的数据库连接 } // QueryDocuments 查询文档时自动注入 TenantID 过滤 func (tf *TenantDataFilter) QueryDocuments(ctx context.Context, query string) ([]string, error) { tenantID, ok : TenantIDFromContext(ctx) if !ok { return nil, fmt.Errorf(未找到租户 ID拒绝查询) } // 构造带租户过滤的查询 filter : map[string]string{ tenant_id: tenantID, query: query, } // 实际查询 _ filter return nil, nil } // 上下文隔离 // TenantContextStore 租户级别的上下文存储 type TenantContextStore struct { sessions map[string]map[string]interface{} // tenantID → sessionID → context mu sync.RWMutex } func NewTenantContextStore() *TenantContextStore { return TenantContextStore{ sessions: make(map[string]map[string]interface{}), } } // GetContext 获取租户的会话上下文 func (cs *TenantContextStore) GetContext( ctx context.Context, sessionID string, ) (map[string]interface{}, error) { tenantID, ok : TenantIDFromContext(ctx) if !ok { return nil, fmt.Errorf(未找到租户 ID) } cs.mu.RLock() defer cs.mu.RUnlock() if sessions, ok : cs.sessions[tenantID]; ok { if sessionData, ok : sessions[sessionID]; ok { return sessionData.(map[string]interface{}), nil } } return make(map[string]interface{}), nil } // SaveContext 保存租户的会话上下文 func (cs *TenantContextStore) SaveContext( ctx context.Context, sessionID string, data map[string]interface{}, ) error { tenantID, ok : TenantIDFromContext(ctx) if !ok { return fmt.Errorf(未找到租户 ID) } cs.mu.Lock() defer cs.mu.Unlock() if _, ok : cs.sessions[tenantID]; !ok { cs.sessions[tenantID] make(map[string]interface{}) } cs.sessions[tenantID][sessionID] data return nil } // 并发限制中间件 // TenantRateLimiter 租户级别并发限制 type TenantRateLimiter struct { tokens map[string]chan struct{} // 令牌桶 mu sync.Mutex } func NewTenantRateLimiter() *TenantRateLimiter { return TenantRateLimiter{ tokens: make(map[string]chan struct{}), } } // Acquire 获取租户的执行槽位 func (rl *TenantRateLimiter) Acquire(ctx context.Context, tenantID string, maxConcurrent int) error { rl.mu.Lock() ch, ok : rl.tokens[tenantID] if !ok { ch make(chan struct{}, maxConcurrent) rl.tokens[tenantID] ch } rl.mu.Unlock() select { case ch - struct{}{}: return nil case -ctx.Done(): return ctx.Err() } } // Release 释放租户的执行槽位 func (rl *TenantRateLimiter) Release(tenantID string) { rl.mu.Lock() defer rl.mu.Unlock() if ch, ok : rl.tokens[tenantID]; ok { -ch } } // 成本隔离 // TenantCostTracker 租户成本追踪 type TenantCostTracker struct { usage map[string]*TenantUsage // 租户 ID → 用量 mu sync.RWMutex } type TenantUsage struct { TenantID string TotalTokens int64 TotalCost float64 DailyTokens int64 DailyCost float64 DailyDate string MonthlyTokens int64 MonthlyCost float64 MonthlyKey string } func NewTenantCostTracker() *TenantCostTracker { return TenantCostTracker{ usage: make(map[string]*TenantUsage), } } // RecordUsage 记录租户的 Token 使用量 func (ct *TenantCostTracker) RecordUsage(tenantID string, inputTokens, outputTokens int64, cost float64) error { ct.mu.Lock() defer ct.mu.Unlock() if _, ok : ct.usage[tenantID]; !ok { ct.usage[tenantID] TenantUsage{TenantID: tenantID} } usage : ct.usage[tenantID] usage.TotalTokens inputTokens outputTokens usage.TotalCost cost // 每日统计 today : time.Now().Format(2006-01-02) if usage.DailyDate ! today { usage.DailyDate today usage.DailyTokens 0 usage.DailyCost 0 } usage.DailyTokens inputTokens outputTokens usage.DailyCost cost return nil } // GenerateBill 生成租户账单 func (ct *TenantCostTracker) GenerateBill(tenantID string, startTime, endTime time.Time) *TenantBill { // 实际项目中从数据库查询区间用量 return TenantBill{ TenantID: tenantID, TotalCost: 0, ModelBreakdown: make(map[string]float64), } } type TenantBill struct { TenantID string TotalCost float64 ModelBreakdown map[string]float64 // 各模型的费用明细 } // 工具权限控制 // ToolPermissionChecker 工具权限检查 type ToolPermissionChecker struct { tenantTools map[string][]string // 租户 ID → 允许的工具列表 mu sync.RWMutex } func (tc *ToolPermissionChecker) CanUseTool(ctx context.Context, toolName string) bool { tenantID, ok : TenantIDFromContext(ctx) if !ok { return false } tc.mu.RLock() defer tc.mu.RUnlock() allowed, ok : tc.tenantTools[tenantID] if !ok { return false } for _, t : range allowed { if t toolName || t * { return true } } return false } // 接口抽象 type mockDB struct{} // 使用示例 func ProcessTenantRequest(ctx context.Context, tenantID, message string) (string, error) { // 1. 加载租户配置 tenantInfo : TenantInfo{ ID: tenantID, Tier: pro, MaxConcurrent: 5, AllowedTools: []string{query_order, check_status}, RateLimitQPS: 10, } // 2. 注入租户上下文 ctx WithTenant(ctx, tenantInfo) // 3. 后续所有操作都自动携带租户 ID // 数据查询 → 自动过滤 // 上下文存储 → 自动隔离 // 成本记录 → 自动归集 _ ctx _ message return 处理完成, nil }四、多租户隔离的边界与陷阱不要用数据库字段隔离代替真正的主键隔离。在 SQL 中加WHERE tenant_id ?是对的但不能只靠这一层。如果开发忘了加这个条件数据就泄露了。更安全的做法是数据库层面的 Row-Level Security 策略。共享知识的租户特殊处理。有些场景下多个租户共享同一套知识库如同一个行业的多租户共享法规知识。这时需要区分共享知识和私有数据共享部分不需要租户过滤私有数据严格要求。租户级别的资源配额要留缓冲。如果 10 个租户共享一个 Agent 集群每个租户分到 10% 的资源配额总共 100%。实际运行中资源利用率很少达到 100%总会有一个租户的配额浪费。用弹性配额而非硬配额——未使用的配额可以被其他租户临时借用。隔离带来的运维成本。如果每个租户有独立的向量库索引、独立的 Agent 实例隔离性最好但运维成本也最高。大多数场景下轻量隔离数据级 上下文级已经足够不需要做到实例级隔离。五、总结多租户隔离的四个核心数据操作必须带 TenantID、上下文存储按租户分区、任务配额按租户限制、成本统计按租户独立。实施策略先用 Context 传递 TenantID 的统一入口模式再逐步加固数据层和资源层的隔离。隔离做得越早代价越小——等到数据混了再拆分比脏了再洗难十倍。

相关新闻

2026/9/6 20:21:58

如何解决Windows下LG Ultrafine显示器亮度无法调节的困扰

如何解决Windows下LG Ultrafine显示器亮度无法调节的困扰 【免费下载链接】LG-Ultrafine-Brightness A tool to adjust brightness of LG Ultrafine 4k/5K on Windows 项目地址: https://gitcode.com/gh_mirrors/lg/LG-Ultrafine-Brightness 你是否正在使用LG Ultrafine…

2026/9/6 21:12:21

毫米波雷达芯片静电防护:从AWR1843AOP封装剖析到全链路工程实践

1. 毫米波雷达芯片的静电防护:从原理到实践的深度解析在毫米波雷达传感器的硬件开发中,尤其是面对AWR1843AOP这类集成了天线、射频前端和数字处理单元的先进芯片,工程师们往往将大部分精力倾注在射频性能调优、算法开发和系统集成上。然而&am…

2026/9/6 7:55:34

AI编码代理可靠性工程:从指令设计到验证体系的完整实践

如果你最近在关注 AI 编程助手,可能会发现一个有趣的现象:很多团队投入大量精力去追求更强大的模型,但实际使用中,真正影响开发效率的往往不是模型本身的能力,而是整个编码流程的可靠性。一个看似智能的 AI 代理&#…

2026/9/6 22:23:23

数字电路课设经典:四人抢答器设计与调试深度解析

简介:这是一份数电课程设计“四人抢答器”的完整报告,面向电子信息工程及相关专业学生,可用于数字电子技术课程设计、综合实训或竞赛备赛参考。报告基于74LS175、74LS00、74LS21、555定时器、74LS192、74LS47等常用器件,实现抢答鉴…

2026/9/6 22:23:23

四人抢答器课程设计全攻略:从编码器到锁存器的数字电路实战

简介:面向数字电子技术课程设计的四人抢答器完整设计说明书,适合电子信息工程及相关专业学生用于课程巩固与综合训练,系统梳理门电路、触发器、计数器、定时器等知识。文档完整覆盖设计任务、方案论证、实现步骤,围绕74LS175、74L…

2026/9/6 22:23:23

UL 943 2023-09版GFCI标准研读:跳闸测试与整改要点

简介:一份2023年9月5日发布的北美接地故障断路器(GFCI)安全标准PDF文件,面向电气安全工程师、产品设计人员、认证测试与合规人员,用于掌握最新合规要求。文件为单个PDF文档,压缩包仅2.69MB,便于…

2026/9/6 22:23:23

基于C++/Qt/MySQL的大学志愿填报系统设计与实现

简介:基于C的大学志愿填报系统项目实例,面向高考考生及家长、高校招生管理人员、教育信息化研究者与C开发者,聚焦志愿填报效率低、信息分散、决策难等痛点。资源采用docx格式,仅含1个文档,压缩包约89KB,内容…

2026/9/6 22:18:23

WeKnora 知识图谱:把文档关联讲清楚的完整指南

WeKnora 知识图谱:把文档关联讲清楚的完整指南 【免费下载链接】WeKnora Open-source LLM knowledge platform: turn raw documents into a queryable RAG, an autonomous reasoning agent, and a self-maintaining Wiki. 项目地址: https://gitcode.com/GitHub_T…

2026/9/6 0:06:59

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/6 0:06:59

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/6 0:06:59

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/6 0:06:59

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/6 0:06:59

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/6 0:06:59

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/6 11:40:10

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/6 19:33:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/6 10:19:40

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…