| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- # 【数据准备 D:独立脚本】在会务套餐检索前准备 Milvus 集合;不在每轮聊天中执行。
- # 运行顺序:D1 读取配置和样例数据 → D2 删除重建集合及索引
- # → D3 为套餐生成向量并入库 → D4 用样例查询验证检索。
- # 注意 D2 会删除同名集合及其已有数据;本文件不是增量更新脚本。
- """
- 需求:初始化会务套餐 RAG 系统
- 思路:
- 1. 定义会务套餐数据(城市、线路名称、天数、价格、方案亮点等)
- 2. 使用 Qwen 的 Embedding API 将每个套餐的方案亮点生成向量
- 3. 在本地 Milvus 中创建 Collection 并插入数据
- 4. 支持后续通过语义搜索匹配用户意图的会务套餐
- """
- import json
- import os
- import sys
- import requests
- from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType, utility
- # 将项目根目录加入 Python 路径,以便导入配置
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
- # ==================== Qwen Embedding API ====================
- # 使用 DashScope 的 text-embedding-v3 模型生成向量
- # 返回 1024 维向量
- from config import Config
- # 【数据准备 D1】读取向量服务配置;下面 MEETING_PACKAGES 是脚本内置的样例数据。
- conf = Config()
- DASHSCOPE_API_KEY = conf.embedding_api_key # 向量模型使用独立的 DashScope API Key
- EMBEDDING_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"
- def get_embedding(text: str) -> list:
- """
- 调用 Qwen Embedding API 生成文本向量
- 参数:
- text (str): 需要生成向量的文本
- 返回值:
- list: 1024 维向量
- """
- # 【数据准备 D3/D4 共用】文本 → 向量接口 → 1024 维数值列表。
- # 入库的套餐文本和后续用户查询必须用兼容的模型及维度才能比较。
- # 输入 text 是拼好的套餐文本,或测试用的查询文本;输出一条 1024 维向量。
- # 同样的模型用于入库和查询,使两类文本被映射到可相互比较的向量空间。
- # 调用可能产生外部 API 请求;这个函数没有本地缓存,重复调用也会再次请求。
- headers = {
- "Authorization": f"Bearer {DASHSCOPE_API_KEY}",
- "Content-Type": "application/json"
- }
- payload = {
- "model": "text-embedding-v3",
- "input": [text],
- "dimensions": 1024
- }
- response = requests.post(EMBEDDING_URL, headers=headers, json=payload, timeout=30)
- result = response.json()
- # 响应 data 是按输入文本顺序排列的列表;当前 input 只包含一个 text,因此取索引 0。
- return result["data"][0]["embedding"]
- # ==================== 会务套餐数据 ====================
- # 这是用于入库的本地套餐样例列表,每个字典是一条套餐;定义列表不会向数据库写入。
- # package_id 是业务编号,embedding 尚未在这些样例中生成;后面 insert_meeting_packages 才逐条补上。
- MEETING_PACKAGES = [
- {
- "package_id": "CD001",
- "package_name": "成都数字化转型峰会3日会务套餐",
- "city": "成都",
- "days": 3,
- "price": 2380.00,
- "total_seats": 80,
- "remaining_seats": 26,
- "agency": "携程商旅",
- "rating": 4.8,
- "departure_dates": ["2026-05-01", "2026-06-01", "2026-06-15"],
- "highlights": "覆盖机场接送、会场签到、四星商务酒店、数字化转型主论坛、企业案例圆桌、成都高新区企业参访,适合中高层管理者集中参会"
- },
- {
- "package_id": "CD002",
- "package_name": "成都客户拜访2日接待套餐",
- "city": "成都",
- "days": 2,
- "price": 1280.00,
- "total_seats": 40,
- "remaining_seats": 18,
- "agency": "同程商旅",
- "rating": 4.7,
- "departure_dates": ["2026-05-01", "2026-06-01", "2026-06-08"],
- "highlights": "包含机场/高铁站接送、客户会议室预订、商务午餐、企业展厅参观、晚间轻接待路线,适合销售团队与客户成功团队使用"
- },
- {
- "package_id": "CD003",
- "package_name": "成都团队共创工作坊2日套餐",
- "city": "成都",
- "days": 2,
- "price": 980.00,
- "total_seats": 60,
- "remaining_seats": 31,
- "agency": "会小二",
- "rating": 4.6,
- "departure_dates": ["2026-05-02", "2026-06-03", "2026-06-20"],
- "highlights": "提供创意会议空间、分组讨论物料、茶歇、团队共创主持人、晚间团队餐,可按部门目标设计议程"
- },
- {
- "package_id": "BJ001",
- "package_name": "北京政企拜访3日差旅套餐",
- "city": "北京",
- "days": 3,
- "price": 2680.00,
- "total_seats": 50,
- "remaining_seats": 14,
- "agency": "国旅运通商旅",
- "rating": 4.8,
- "departure_dates": ["2026-05-06", "2026-06-02", "2026-06-16"],
- "highlights": "覆盖政企客户拜访、会议室协调、商务酒店、用车排班、材料打印与接待动线,适合法务、销售和公共事务团队"
- },
- {
- "package_id": "BJ002",
- "package_name": "北京研发交流2日套餐",
- "city": "北京",
- "days": 2,
- "price": 1580.00,
- "total_seats": 35,
- "remaining_seats": 12,
- "agency": "飞猪企业商旅",
- "rating": 4.7,
- "departure_dates": ["2026-05-10", "2026-06-05"],
- "highlights": "安排技术交流会议、园区参访、商务茶歇、投影与白板设备、晚间简餐,适合研发团队跨城对齐方案"
- },
- {
- "package_id": "SH001",
- "package_name": "上海总部培训4日会务套餐",
- "city": "上海",
- "days": 4,
- "price": 3280.00,
- "total_seats": 90,
- "remaining_seats": 37,
- "agency": "锦江会务",
- "rating": 4.9,
- "departure_dates": ["2026-05-08", "2026-06-01", "2026-06-18"],
- "highlights": "包含培训教室、签到系统、四星酒店、浦东机场接送、讲师休息室、分组研讨区,适合新人训和区域经理培训"
- },
- {
- "package_id": "SH002",
- "package_name": "上海大型展会参展3日套餐",
- "city": "上海",
- "days": 3,
- "price": 2980.00,
- "total_seats": 70,
- "remaining_seats": 22,
- "agency": "携程商旅",
- "rating": 4.8,
- "departure_dates": ["2026-05-12", "2026-06-10"],
- "highlights": "覆盖展馆通行、展位物料运输、酒店班车、客户邀约晚宴、现场证件管理,适合市场与渠道团队参展"
- },
- {
- "package_id": "HZ001",
- "package_name": "杭州电商标杆研学3日套餐",
- "city": "杭州",
- "days": 3,
- "price": 2180.00,
- "total_seats": 45,
- "remaining_seats": 16,
- "agency": "浙商会务",
- "rating": 4.7,
- "departure_dates": ["2026-05-09", "2026-06-06", "2026-06-21"],
- "highlights": "安排电商园区参访、直播基地交流、业务复盘会议、团队晚餐和西湖周边轻接待,适合运营与增长团队"
- },
- {
- "package_id": "SZ001",
- "package_name": "深圳创新企业参访3日套餐",
- "city": "深圳",
- "days": 3,
- "price": 2580.00,
- "total_seats": 55,
- "remaining_seats": 20,
- "agency": "南方商旅",
- "rating": 4.8,
- "departure_dates": ["2026-05-11", "2026-06-09"],
- "highlights": "覆盖科技园企业参访、创新产品交流、商务用车、酒店会议室、闭门分享会,适合产品、战略和投资团队"
- },
- {
- "package_id": "GZ001",
- "package_name": "广州供应链展会3日套餐",
- "city": "广州",
- "days": 3,
- "price": 2280.00,
- "total_seats": 65,
- "remaining_seats": 28,
- "agency": "广之旅商旅",
- "rating": 4.6,
- "departure_dates": ["2026-05-15", "2026-06-12"],
- "highlights": "适配采购团队参展,包含展馆接送、供应商洽谈室、商务午餐、样品寄送协助和晚间客户接待"
- },
- {
- "package_id": "XA001",
- "package_name": "西安制造业客户答谢2日套餐",
- "city": "西安",
- "days": 2,
- "price": 1380.00,
- "total_seats": 50,
- "remaining_seats": 19,
- "agency": "陕旅会务",
- "rating": 4.6,
- "departure_dates": ["2026-05-18", "2026-06-14"],
- "highlights": "包含客户答谢晚宴、工厂参观车辆、会议设备、伴手礼管理和历史文化轻接待路线,适合制造业销售团队"
- },
- {
- "package_id": "QD001",
- "package_name": "青岛销售年会3日套餐",
- "city": "青岛",
- "days": 3,
- "price": 2480.00,
- "total_seats": 120,
- "remaining_seats": 48,
- "agency": "海滨会务",
- "rating": 4.7,
- "departure_dates": ["2026-05-20", "2026-06-20"],
- "highlights": "支持百人销售年会、主会场搭建、分会场讨论、颁奖晚宴、团队拓展和海边商务晚餐"
- },
- {
- "package_id": "CQ001",
- "package_name": "重庆渠道大会2日套餐",
- "city": "重庆",
- "days": 2,
- "price": 1180.00,
- "total_seats": 100,
- "remaining_seats": 42,
- "agency": "山城会务",
- "rating": 4.6,
- "departure_dates": ["2026-05-16", "2026-06-11"],
- "highlights": "覆盖渠道商签到、主论坛、圆桌沟通、商务晚宴、酒店会议室和机场接送,适合区域渠道大会"
- },
- {
- "package_id": "NJ001",
- "package_name": "南京项目交付驻场5日套餐",
- "city": "南京",
- "days": 5,
- "price": 3680.00,
- "total_seats": 30,
- "remaining_seats": 9,
- "agency": "苏宁商旅",
- "rating": 4.7,
- "departure_dates": ["2026-05-13", "2026-06-07"],
- "highlights": "适合项目交付团队驻场,包含长住商务酒店、每日通勤用车、会议室、打印服务、加班简餐和周报复盘空间"
- },
- {
- "package_id": "XM001",
- "package_name": "厦门高管闭门会3日套餐",
- "city": "厦门",
- "days": 3,
- "price": 3980.00,
- "total_seats": 25,
- "remaining_seats": 7,
- "agency": "建发商旅",
- "rating": 4.9,
- "departure_dates": ["2026-05-22", "2026-06-18"],
- "highlights": "面向高管闭门战略会,包含海景会议室、贵宾接送、保密签到、商务晚宴、会议纪要服务和安静住宿环境"
- },
- ]
- # ==================== Milvus 初始化 ====================
- MILVUS_HOST = "localhost"
- MILVUS_PORT = 19530
- COLLECTION_NAME = "meeting_packages"
- DIMENSION = 1024 # text-embedding-v3 输出维度
- def create_meeting_package_collection():
- """
- 在 Milvus 中创建会务套餐 Collection
- 字段设计:
- - id: 主键(自增)
- - package_id: 套餐编号(字符串,如 YN001)
- - package_name: 套餐名称
- - city: 城市
- - days: 天数
- - price: 价格
- - total_seats: 总座位
- - remaining_seats: 剩余座位
- - agency: 差旅服务商
- - rating: 评分
- - departure_dates: 出发日期列表(JSON 字符串)
- - highlights: 方案亮点(用于生成向量)
- - embedding: 向量(1024 维)
- """
- # 连接 Milvus
- # 【数据准备 D2.1】先连接 Milvus,再检查同名集合。
- # 由文件末尾主程序首先调用;输出新建好的 Collection 对象,交给下一步入库函数。
- # 普通 FieldSchema 保存套餐字段,FLOAT_VECTOR 字段承载语义检索向量。
- # 已有集合删除后,新的自增 id 会重新由 Milvus 分配;它与样例中的 package_id 不是一个字段。
- connections.connect(alias="default", host=MILVUS_HOST, port=MILVUS_PORT)
- print(f"已连接 Milvus: {MILVUS_HOST}:{MILVUS_PORT}")
- # 如果已存在则删除
- # 【数据准备 D2.2】已有集合会被删除后重建;此操作不会保留旧集合中的套餐。
- if utility.has_collection(COLLECTION_NAME):
- print(f"集合 {COLLECTION_NAME} 已存在,删除重建")
- utility.drop_collection(COLLECTION_NAME)
- # 定义字段
- # 【数据准备 D2.3】普通字段保存套餐信息,embedding 字段保存用于相似度检索的向量。
- fields = [
- FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
- FieldSchema(name="package_id", dtype=DataType.VARCHAR, max_length=20),
- FieldSchema(name="package_name", dtype=DataType.VARCHAR, max_length=200),
- FieldSchema(name="city", dtype=DataType.VARCHAR, max_length=50),
- FieldSchema(name="days", dtype=DataType.INT64),
- FieldSchema(name="price", dtype=DataType.FLOAT),
- FieldSchema(name="total_seats", dtype=DataType.INT64),
- FieldSchema(name="remaining_seats", dtype=DataType.INT64),
- FieldSchema(name="agency", dtype=DataType.VARCHAR, max_length=100),
- FieldSchema(name="rating", dtype=DataType.FLOAT),
- FieldSchema(name="departure_dates", dtype=DataType.VARCHAR, max_length=200),
- FieldSchema(name="highlights", dtype=DataType.VARCHAR, max_length=1000),
- FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=DIMENSION),
- ]
- # 字段列表组装成集合结构;Collection(name, schema) 才将这个结构用于创建集合。
- schema = CollectionSchema(fields, description="会务套餐信息表(RAG 向量检索)")
- collection = Collection(COLLECTION_NAME, schema)
- print(f"集合 {COLLECTION_NAME} 创建成功")
- # 创建向量索引
- # IVF_FLAT 是选用的向量索引类型;COSINE 是相似度度量;nlist 控制索引分组参数。
- # 这些参数影响检索方式,不是套餐数量、向量维度或模型生成的 temperature。
- index_params = {
- "index_type": "IVF_FLAT",
- "metric_type": "COSINE",
- "params": {"nlist": 64}
- }
- # 【数据准备 D2.4】为 embedding 创建 COSINE 索引,然后返回集合给下一步使用。
- collection.create_index(field_name="embedding", index_params=index_params)
- print("向量索引创建成功 (IVF_FLAT, COSINE)")
- # 返回的是可操作该集合的客户端对象;不是套餐列表,也不是已经生成好的向量。
- return collection
- def insert_meeting_packages(collection):
- """
- 将会务套餐数据生成向量后插入 Milvus
- """
- # 【数据准备 D3.1】依次处理套餐:拼检索文本 → 请求向量 → 将向量和原字段放入 rows。
- # 输入 collection 是上一步返回的集合;无业务返回值,效果是将 rows 插入并刷入存储。
- # 每个样例先组成 embedding_text,再调用向量接口,最后把向量与业务字段放在同一行中。
- # 全部样例处理完以后才调用一次 collection.insert,前面的 rows.append 仅修改 Python 列表。
- print(f"\n开始为 {len(MEETING_PACKAGES)} 个会务套餐生成向量...")
- rows = []
- # enumerate 给出从 0 开始的索引 i 和当前套餐 group,进度显示用 i+1 转成从 1 开始的计数。
- for i, group in enumerate(MEETING_PACKAGES):
- # 使用"城市 + 套餐名称 + 方案亮点"作为 embedding 输入文本
- # 这是入库向量对应的文本;查询时会把用户需求另行转成向量与它比较。
- # 只把城市、名称和亮点拼成向量文本;价格、余位等普通字段随后原样保存,不通过向量生成。
- embedding_text = f"{group['city']} {group['package_name']} {group['highlights']}"
- if (i + 1) % 5 == 0:
- print(f" 已处理 {i + 1}/{len(MEETING_PACKAGES)} 个套餐...")
- embedding = get_embedding(embedding_text)
- rows.append({
- "package_id": group["package_id"],
- "package_name": group["package_name"],
- "city": group["city"],
- "days": group["days"],
- "price": group["price"],
- "total_seats": group["total_seats"],
- "remaining_seats": group["remaining_seats"],
- "agency": group["agency"],
- "rating": group["rating"],
- # 出发日期原来是列表,集合 schema 对应字段是 VARCHAR,所以先编码成 JSON 字符串保存。
- "departure_dates": json.dumps(group["departure_dates"], ensure_ascii=False),
- "highlights": group["highlights"],
- "embedding": embedding,
- })
- # 批量插入
- # 【数据准备 D3.2】全部向量准备完才批量插入,并 flush;这里没有逐条重试或断点续写。
- collection.insert(rows)
- # flush 要求刷入待持久化数据;它与后续查询前的 load 用途不同,load 是准备读取检索。
- collection.flush() # 刷盘
- print(f"成功插入 {len(MEETING_PACKAGES)} 个会务套餐数据")
- def test_search(collection):
- """
- 测试语义搜索:用自然语言查询会务套餐
- """
- # 测试查询
- # 【数据准备 D4.1】每条测试查询也先向量化,然后检索;本函数会实际调用向量接口。
- # 输入是已入库的 collection;输出方式为 print,便于人查看检索候选。
- # 每个 query 单独求向量,检索结果按“查询向量→命中列表”组织,所以有两层 for。
- # limit=2 限制候选条数,不表示验证了两个测试用例,也不是准确率阈值。
- test_queries = [
- "成都办数字化转型峰会,需要接送和酒店",
- "上海大型展会参展,需要展馆通行和客户晚宴",
- "适合销售团队的青岛年会方案",
- "深圳创新企业参访,最好包含闭门分享",
- "南京项目驻场,需要长住酒店和每日通勤",
- ]
- print("\n=== 语义搜索测试 ===")
- # 外层依次执行测试需求;前一条查询完成以后才开始下一条,这里没有并发任务调度。
- for query in test_queries:
- print(f"\n查询: {query}")
- query_embedding = get_embedding(query)
- collection.load()
- # 【数据准备 D4.2】取相似度靠前的两条并打印,供人工核对;不是自动质量评分。
- results = collection.search(
- data=[query_embedding],
- anns_field="embedding",
- param={"metric_type": "COSINE", "params": {"nprobe": 10}},
- limit=2,
- output_fields=["package_id", "package_name", "city", "days", "price", "rating", "agency", "highlights"]
- )
- for hits in results:
- for hit in hits:
- print(f" [{hit.entity.get('package_id')}] {hit.entity.get('package_name')} "
- f"- {hit.entity.get('city')} {hit.entity.get('days')}天 "
- f"¥{hit.entity.get('price')} 评分{hit.entity.get('rating')} "
- f"距离: {hit.distance:.4f}")
- # 【数据准备入口】仅直接运行本脚本时按 D2 → D3 → D4 执行;导入不会进入此区块。
- if __name__ == '__main__':
- # 本文件的实际运行入口:先拿到集合对象,再把同一个对象依次传给入库和测试函数。
- # 上面的函数定义在加载文件时被创建,但直到这里调用才真正连接服务、生成向量和写入数据。
- collection = create_meeting_package_collection()
- insert_meeting_packages(collection)
- test_search(collection)
|