微信扫码
添加专属顾问
串租问题如何避免?本文通过物理隔离与自动验证双管齐下,确保多租户RAG系统的数据安全。核心内容:1. 串租问题的根源分析:运行时风险与验证缺失2. Milvus Partition Key实现数据物理隔离3. AutoRAG框架构建自动化验证与评测机制
python3 -m venv .venvsource .venv/bin/activatepip install -U pippip install "autorag>=0.3" "pymilvus>=2.4.0" "openai" "pandas" "pyarrow"export OPENAI_API_KEY=sk-... #自行准备OpenAI_API_KEYexport MILVUS_URI=http://127.0.0.1:19530 # Milvus Standalone 服务地址export MILVUS_TOKEN="root:Milvus"
# 下载docker-compose.ymlwget https://github.com/milvus-io/milvus/releases/download/v2.6.8/milvus-standalone-docker-compose.yml -O docker-compose.yml# 启动Milvus(检查端口映射:19530:19530)docker-compose up -d# 验证服务启动docker ps | grep milvus# 应该看到3个容器:milvus-standalone, milvus-etcd, milvus-minio
⚠️ 说明1:AutoRAG 对输入字段有严格的命名约定(如
doc_id、retrieval_gt等),写错会导致解析报错。说明 2:以下 Step 2-4 中的 Python 代码块,各自保存为对应的
.py文件后,在激活的虚拟环境中用python3 文件名.py执行。
文件 | 必须字段 | 常见错误写法 |
|
| ❌ 写成 |
|
| ❌ 写成 |
retrieval_gt是检索标注字段,记录每条问题期望命中的doc_id列表,AutoRAG 用它计算 Recall / Precision。没有这个字段,评测无法运行。
import osimport pandas as pdos.makedirs("./data", exist_ok=True)corpus = pd.DataFrame([{"doc_id": "a-1", "contents": "A租户的报销规则:差旅上限为内部标准。", "metadata": {"tenant_id": "tenant_a"}, "tenant_id": "tenant_a"},{"doc_id": "a-2", "contents": "A租户合同模板要求法务审批。", "metadata": {"tenant_id": "tenant_a"}, "tenant_id": "tenant_a"},{"doc_id": "b-1", "contents": "B租户的报销规则:海外差旅需要二级审批。", "metadata": {"tenant_id": "tenant_b"}, "tenant_id": "tenant_b"},{"doc_id": "b-2", "contents": "B租户合同模板要求采购会签。", "metadata": {"tenant_id": "tenant_b"}, "tenant_id": "tenant_b"},])qa = pd.DataFrame([{"qid":"q1","query": "报销规则里差旅审批要求是什么?","retrieval_gt": [["a-1"]], # List[List[str]]:期望命中的 doc_id 集合"generation_gt": ["A租户内部标准。"], # List[str]:可接受的参考答案"tenant_id": "tenant_a",},{"qid": "q2","query": "报销规则里差旅审批要求是什么?", # 与 q1 文字完全相同的查询"retrieval_gt": [["b-1"]],"generation_gt": ["B租户海外差旅需二级审批。"],"tenant_id": "tenant_b",},])corpus.to_parquet("./data/corpus.parquet", index=False)qa.to_parquet("./data/qa.parquet",index=False)
import osfrom pymilvus import MilvusClient, DataTypeclient = MilvusClient(uri=os.getenv("MILVUS_URI", "http://127.0.0.1:19530"),token=os.getenv("MILVUS_TOKEN", ""),)COLLECTION = "kb_multi_tenant_pk"if client.has_collection(COLLECTION):client.drop_collection(COLLECTION)schema = client.create_schema(auto_id=False, enable_dynamic_field=False)schema.add_field("pk",DataType.VARCHAR, is_primary=True,max_length=64)schema.add_field("tenant_id", DataType.VARCHAR, is_partition_key=True, max_length=64)schema.add_field("doc_id", DataType.VARCHAR, max_length=64)schema.add_field("contents", DataType.VARCHAR, max_length=2048)# text-embedding-3-small 默认输出1536维度schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=1536)idx = client.prepare_index_params()idx.add_index(field_name="embedding", index_type="AUTOINDEX", metric_type="COSINE")client.create_collection(collection_name=COLLECTION,schema=schema,index_params=idx,num_partitions=16, # Partition Key 模式下的物理分区数,默认 16,最大 4096)print(f"✅ Collection '{COLLECTION}' created,Partition Key → tenant_id")
import osimport pandas as pdfrom openai import OpenAIfrom pymilvus import MilvusClientopenai_client = OpenAI()client = MilvusClient(uri=os.getenv("MILVUS_URI", "http://127.0.0.1:19530"),token=os.getenv("MILVUS_TOKEN", ""),)COLLECTION = "kb_multi_tenant_pk"def embed(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:resp = openai_client.embeddings.create(input=texts, model=model)return [item.embedding for item in resp.data]corpus_df= pd.read_parquet("./data/corpus.parquet")embeddings = embed(corpus_df["contents"].tolist())rows = [{"pk": row["doc_id"],"tenant_id": row["tenant_id"],# Partition Key 字段,决定物理路由"doc_id": row["doc_id"],"contents": row["contents"],"embedding": emb,}for (_, row), emb in zip(corpus_df.iterrows(), embeddings)]client.insert(collection_name=COLLECTION, data=rows)client.flush(collection_name=COLLECTION)print(f"✅ Inserted {len(rows)} documents into Milvus")
import osmilvus_uri = os.getenv("MILVUS_URI", "http://127.0.0.1:19530")milvus_token = os.getenv("MILVUS_TOKEN", "")collection_name = os.getenv("AUTORAG_COLLECTION", "kb_autorag_eval")config = f"""vectordb:- name: milvus_tenant_storedb_type: milvusembedding_model: openai_embed_3_smallcollection_name: {collection_name}uri: {milvus_uri}token: {milvus_token}node_lines:- node_line_name: retrieve_node_linenodes:- node_type: semantic_retrievalstrategy:metrics: [retrieval_recall, retrieval_precision, retrieval_f1]top_k: 5modules:- module_type: vectordbvectordb: milvus_tenant_store"""os.makedirs("./config", exist_ok=True)with open("./config/autorag_milvus_tenant.yaml", "w") as f:f.write(config.strip())print("✅ Config written to ./config/autorag_milvus_tenant.yaml")
# 按租户拆分评测集,保证评测数据不跨租户污染python3 -<< 'EOF'import pandas as pdqa= pd.read_parquet('./data/qa.parquet')corpus = pd.read_parquet('./data/corpus.parquet')for tid in ["tenant_a", "tenant_b"]:qa[qa["tenant_id"]== tid].to_parquet(f"./data/qa_{tid}.parquet", index=False)corpus[corpus["tenant_id"] == tid].to_parquet(f"./data/corpus_{tid}.parquet", index=False)EOF# 分别对两个租户执行评测,结果落到各自的 benchmark 目录# 注意:每个租户使用独立 collection,避免评测数据相互污染for TENANT in tenant_a tenant_b; doAUTORAG_COLLECTION=kb_autorag_eval_${TENANT} python3 step5_write_config.pyautorag evaluate \--config ./config/autorag_milvus_tenant.yaml \--qa_data_path ./data/qa_${TENANT}.parquet \--corpus_data_path ./data/corpus_${TENANT}.parquet \--project_dir ./benchmark/${TENANT}done
import osfrom openai import OpenAIfrom pymilvus import MilvusClientCOLLECTION = "kb_multi_tenant_pk"client = MilvusClient(uri=os.getenv("MILVUS_URI", "http://127.0.0.1:19530"),token=os.getenv("MILVUS_TOKEN", ""),)openai_client = OpenAI()def embed(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:resp = openai_client.embeddings.create(input=texts, model=model)return [item.embedding for item in resp.data]query = "报销规则里差旅审批要求是什么?"query_vector = embed([query])[0]# ✅ 带 tenant 条件查询for tid in ["tenant_a", "tenant_b"]:results = client.search(collection_name=COLLECTION,data=[query_vector],filter=f'tenant_id == "{tid}"',limit=5,output_fields=["doc_id", "tenant_id", "contents"],)print(f"=== 查询租户: {tid} ===")for hit in results[0]:e = hit["entity"]print(f" doc={e['doc_id']} tenant={e['tenant_id']} score={hit['distance']:.4f}")print(f" → {e['contents'][:40]}...")# ❌ 无过滤,语义相似度跨租户返回print("=== ⚠️ 无 tenant 过滤(危险示范)===")results_nf = client.search(collection_name=COLLECTION,data=[query_vector],limit=5,output_fields=["doc_id", "tenant_id", "contents"],)for hit in results_nf[0]:e = hit["entity"]print(f" doc={e['doc_id']} tenant={e['tenant_id']} score={hit['distance']:.4f}")
=== 查询租户: tenant_a ===doc=a-1 tenant=tenant_a score=0.6015→ A租户的报销规则:差旅上限为内部标准。...doc=a-2 tenant=tenant_a score=0.3933→ A租户合同模板要求法务审批。...=== 查询租户: tenant_b ===doc=b-1 tenant=tenant_b score=0.6914→ B租户的报销规则:海外差旅需要二级审批。...doc=b-2 tenant=tenant_b score=0.2637→ B租户合同模板要求采购会签。...=== ⚠️ 无 tenant 过滤(危险示范)===doc=b-1 tenant=tenant_b score=0.6914 ← 两个租户的文档混排doc=a-1 tenant=tenant_a score=0.6015doc=a-2 tenant=tenant_a score=0.3933doc=b-2 tenant=tenant_b score=0.2637
def validate_and_insert(doc: dict):if not doc.get("tenant_id"):raise ValueError(f"doc_id={doc.get('doc_id')} 缺少 tenant_id,拒绝入库。""不允许事后补填——无tenant_id 的向量进入集合后无法补救。")client.insert(collection_name=COLLECTION, data=[doc])
#❌ 错误:相信客户端传进来的值,可以被伪造tenant_id = request.params.get("tenant_id")filter_expr = f'tenant_id == "{tenant_id}"'# ✅ 正确:从服务端验证过的Token 中提取,不可伪造tenant_id = auth_token.claims["tenant_id"]filter_expr = f'tenant_id == "{tenant_id}"'results = client.search(collection_name=COLLECTION,data=[query_vector],filter=filter_expr, # 过滤条件由系统注入,不经过客户端limit=top_k,output_fields=["doc_id", "contents"],)
作者介绍
Zilliz黄金写手:尹珉
阅读推荐 官宣|我们推出了开源版Claude Tag,以及它背后记忆与工具引擎 MFS 如何通过修改Segment 形态,让你的 Milvus 性能原地翻倍 Agent时代,静态容量规划注定失败!聊聊 Zilliz Cloud 的AutoScale设计 官宣:Zilliz Vector Lakebase正式发布,作为向量数据库开创者,我们为何推出Vector Lakebase
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2026-06-30
知识库不是文件堆——我把RAG准确率从60%调到了92%
2026-06-30
本体论语义建设新思路,另类RAG来解决检索问题
2026-06-30
别把RAG当架构:Ontology(本体)才是Agent的业务世界
2026-06-29
PixelRAG:伯克利团队颠覆传统 RAG,用截图代替文本检索! 28 天狂揽 3000+ Star!
2026-06-29
腾讯WeKnora开源详解(三):检索引擎与生态集成
2026-06-29
腾讯开源WeKnora详解(二):知识库与对话核心能力
2026-06-29
RAG又被绕开了,MIT用MEMO给AI外挂记忆脑
2026-06-25
5.2k星星爆火开源!你的知识库迎来了史诗级更新,「像素级原生搜索」来了
2026-04-06
2026-04-27
2026-04-23
2026-04-02
2026-04-20
2026-04-09
2026-04-12
2026-04-22
2026-04-10
2026-05-14
2026-06-23
2026-06-23
2026-06-15
2026-06-10
2026-06-10
2026-05-20
2026-05-18
2026-05-11
欢迎您使用【53AI 官方网站】(以下简称“本网站”或“我们”)。本《会员服务协议》(以下简称“本协议”)是您(以下简称“会员”或“用户”)与【深圳市博思协创网络科技有限公司】之间关于注册、登录及使用本网站会员服务所订立的法律协议。
在您注册或登录前,请务必审慎阅读、充分理解各条款内容,特别是免除或限制责任的条款、知识产权条款、争议解决条款等。此类条款将以加粗形式提示您注意。 当您通过微信公众号授权、手机验证码验证或其他方式成功登录本网站时,即视为您已完全理解并同意接受本协议的全部内容。
一、 定义
本网站:指由【深圳市博思协创网络科技有限公司】运营的,域名为【53ai.com】的网站及相关移动端页面。
会员服务:指本网站向注册会员提供的知识库文章查阅、内容检索及其他相关增值服务。
知识库内容:指本网站发布的包括但不限于文字、图表、数据、研究报告、行业分析等数字化内容资源。
二、 账号注册与登录
登录方式:本网站支持以下登录方式,您可根据实际情况选择:
微信公众号授权登录:您同意将您的微信OpenID信息授权给本网站,用于创建或关联会员账号。
手机验证码登录:您需提供真实有效的手机号码,并通过短信验证码完成身份验证与登录/注册。
账号安全:您的账号仅限您本人使用,禁止赠与、借用、租用、转让或售卖。因您保管不善导致的账号被盗、密码泄露等损失,由您自行承担。
实名认证:根据相关法律法规要求,我们可能要求您在特定功能下完成实名认证。如您拒绝提供,可能无法使用部分或全部服务。
未成年人保护:若您未满18周岁,请在法定监护人的陪同下阅读本协议,并在征得监护人同意后使用本服务。
三、 服务内容与规范
知识库查阅权限:会员登录后,有权按照其会员等级对应的权限范围,在线浏览、检索本网站知识库中的相关文章及内容。
服务变更:我们有权根据业务发展需要,调整、变更或终止部分服务内容,并将以网站公告、公众号消息等方式提前通知。
禁止行为:您在使用服务时不得实施以下行为:
利用技术手段批量爬取、下载、转存知识库内容;
将知识库内容用于商业目的或未经授权地向第三方传播;
干扰本网站正常运行或侵犯其他用户合法权益;
发布违法违规信息或从事违反公序良俗的活动。
四、 知识产权声明
权利归属:本网站知识库中的排版设计、软件代码等内容的知识产权均归【公司全称】或原权利人所有,受《中华人民共和国著作权法》等法律保护。
有限许可:本网站授予会员一项非独占、不可转让、不可转授权的普通许可,仅限于个人学习、研究之目的在线查阅知识库内容。
侵权追责:未经书面许可,任何单位或个人不得以任何形式复制、转载、摘编、镜像、汇编或以其他方式使用上述内容。一经发现,我们保留追究其法律责任的权利。
五、 个人信息保护
我们重视对您个人信息的保护。关于我们如何收集、使用、存储和保护您的个人信息,请单独阅读 《隐私政策》。
您通过微信公众号授权或手机号验证所提供的信息,我们将严格按照《个人信息保护法》的规定处理,仅用于身份识别、服务提供及安全验证等必要用途。
您可以随时通过网站设置或联系客服行使查阅、更正、删除个人信息及撤回授权同意的权利。
六、 免责声明
内容准确性:知识库内容仅供参考,不构成专业建议。我们不对其完整性、准确性、时效性作任何明示或暗示的保证,您应自行判断并承担使用风险。
不可抗力:因自然灾害、政策法规变化、网络故障、第三方平台接口异常(如微信接口维护、运营商短信通道故障)等不可抗力导致的服务中断或延迟,我们不承担违约责任。
第三方链接:本网站可能包含指向第三方网站的链接,该等网站的内容和服务不受我们控制,请您自行甄别风险。
七、 违约责任
如您违反本协议约定,我们有权视情节采取警告、限制功能、暂停服务、注销账号等措施,并保留要求赔偿损失的权利。
如因您的违约行为导致我们遭受行政处罚、第三方索赔或商誉损失,您应承担全部赔偿责任(包括但不限于罚款、赔偿金、律师费、公证费等)。
八、 法律适用与争议解决
本协议的订立、执行和解释均适用中华人民共和国大陆地区法律。
因本协议产生的或与本协议有关的任何争议,双方应友好协商解决;协商不成的,任何一方均可向【公司所在地】有管辖权的人民法院提起诉讼。
九、 其他
本协议构成双方就本服务达成的完整协议,取代此前任何口头或书面约定。
本协议任一条款被认定为无效或不可执行的,不影响其他条款的效力。
我们对本协议享有最终解释权,并在法律允许的范围内保留随时修改的权利。修改后的协议一经公布即生效,继续使用服务即视为同意修订内容。