init_meeting_package_rag.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. # 【数据准备 D:独立脚本】在会务套餐检索前准备 Milvus 集合;不在每轮聊天中执行。
  2. # 运行顺序:D1 读取配置和样例数据 → D2 删除重建集合及索引
  3. # → D3 为套餐生成向量并入库 → D4 用样例查询验证检索。
  4. # 注意 D2 会删除同名集合及其已有数据;本文件不是增量更新脚本。
  5. """
  6. 需求:初始化会务套餐 RAG 系统
  7. 思路:
  8. 1. 定义会务套餐数据(城市、线路名称、天数、价格、方案亮点等)
  9. 2. 使用 Qwen 的 Embedding API 将每个套餐的方案亮点生成向量
  10. 3. 在本地 Milvus 中创建 Collection 并插入数据
  11. 4. 支持后续通过语义搜索匹配用户意图的会务套餐
  12. """
  13. import json
  14. import os
  15. import sys
  16. import requests
  17. from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType, utility
  18. # 将项目根目录加入 Python 路径,以便导入配置
  19. sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
  20. # ==================== Qwen Embedding API ====================
  21. # 使用 DashScope 的 text-embedding-v3 模型生成向量
  22. # 返回 1024 维向量
  23. from config import Config
  24. # 【数据准备 D1】读取向量服务配置;下面 MEETING_PACKAGES 是脚本内置的样例数据。
  25. conf = Config()
  26. DASHSCOPE_API_KEY = conf.embedding_api_key # 向量模型使用独立的 DashScope API Key
  27. EMBEDDING_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"
  28. def get_embedding(text: str) -> list:
  29. """
  30. 调用 Qwen Embedding API 生成文本向量
  31. 参数:
  32. text (str): 需要生成向量的文本
  33. 返回值:
  34. list: 1024 维向量
  35. """
  36. # 【数据准备 D3/D4 共用】文本 → 向量接口 → 1024 维数值列表。
  37. # 入库的套餐文本和后续用户查询必须用兼容的模型及维度才能比较。
  38. # 输入 text 是拼好的套餐文本,或测试用的查询文本;输出一条 1024 维向量。
  39. # 同样的模型用于入库和查询,使两类文本被映射到可相互比较的向量空间。
  40. # 调用可能产生外部 API 请求;这个函数没有本地缓存,重复调用也会再次请求。
  41. headers = {
  42. "Authorization": f"Bearer {DASHSCOPE_API_KEY}",
  43. "Content-Type": "application/json"
  44. }
  45. payload = {
  46. "model": "text-embedding-v3",
  47. "input": [text],
  48. "dimensions": 1024
  49. }
  50. response = requests.post(EMBEDDING_URL, headers=headers, json=payload, timeout=30)
  51. result = response.json()
  52. # 响应 data 是按输入文本顺序排列的列表;当前 input 只包含一个 text,因此取索引 0。
  53. return result["data"][0]["embedding"]
  54. # ==================== 会务套餐数据 ====================
  55. # 这是用于入库的本地套餐样例列表,每个字典是一条套餐;定义列表不会向数据库写入。
  56. # package_id 是业务编号,embedding 尚未在这些样例中生成;后面 insert_meeting_packages 才逐条补上。
  57. MEETING_PACKAGES = [
  58. {
  59. "package_id": "CD001",
  60. "package_name": "成都数字化转型峰会3日会务套餐",
  61. "city": "成都",
  62. "days": 3,
  63. "price": 2380.00,
  64. "total_seats": 80,
  65. "remaining_seats": 26,
  66. "agency": "携程商旅",
  67. "rating": 4.8,
  68. "departure_dates": ["2026-05-01", "2026-06-01", "2026-06-15"],
  69. "highlights": "覆盖机场接送、会场签到、四星商务酒店、数字化转型主论坛、企业案例圆桌、成都高新区企业参访,适合中高层管理者集中参会"
  70. },
  71. {
  72. "package_id": "CD002",
  73. "package_name": "成都客户拜访2日接待套餐",
  74. "city": "成都",
  75. "days": 2,
  76. "price": 1280.00,
  77. "total_seats": 40,
  78. "remaining_seats": 18,
  79. "agency": "同程商旅",
  80. "rating": 4.7,
  81. "departure_dates": ["2026-05-01", "2026-06-01", "2026-06-08"],
  82. "highlights": "包含机场/高铁站接送、客户会议室预订、商务午餐、企业展厅参观、晚间轻接待路线,适合销售团队与客户成功团队使用"
  83. },
  84. {
  85. "package_id": "CD003",
  86. "package_name": "成都团队共创工作坊2日套餐",
  87. "city": "成都",
  88. "days": 2,
  89. "price": 980.00,
  90. "total_seats": 60,
  91. "remaining_seats": 31,
  92. "agency": "会小二",
  93. "rating": 4.6,
  94. "departure_dates": ["2026-05-02", "2026-06-03", "2026-06-20"],
  95. "highlights": "提供创意会议空间、分组讨论物料、茶歇、团队共创主持人、晚间团队餐,可按部门目标设计议程"
  96. },
  97. {
  98. "package_id": "BJ001",
  99. "package_name": "北京政企拜访3日差旅套餐",
  100. "city": "北京",
  101. "days": 3,
  102. "price": 2680.00,
  103. "total_seats": 50,
  104. "remaining_seats": 14,
  105. "agency": "国旅运通商旅",
  106. "rating": 4.8,
  107. "departure_dates": ["2026-05-06", "2026-06-02", "2026-06-16"],
  108. "highlights": "覆盖政企客户拜访、会议室协调、商务酒店、用车排班、材料打印与接待动线,适合法务、销售和公共事务团队"
  109. },
  110. {
  111. "package_id": "BJ002",
  112. "package_name": "北京研发交流2日套餐",
  113. "city": "北京",
  114. "days": 2,
  115. "price": 1580.00,
  116. "total_seats": 35,
  117. "remaining_seats": 12,
  118. "agency": "飞猪企业商旅",
  119. "rating": 4.7,
  120. "departure_dates": ["2026-05-10", "2026-06-05"],
  121. "highlights": "安排技术交流会议、园区参访、商务茶歇、投影与白板设备、晚间简餐,适合研发团队跨城对齐方案"
  122. },
  123. {
  124. "package_id": "SH001",
  125. "package_name": "上海总部培训4日会务套餐",
  126. "city": "上海",
  127. "days": 4,
  128. "price": 3280.00,
  129. "total_seats": 90,
  130. "remaining_seats": 37,
  131. "agency": "锦江会务",
  132. "rating": 4.9,
  133. "departure_dates": ["2026-05-08", "2026-06-01", "2026-06-18"],
  134. "highlights": "包含培训教室、签到系统、四星酒店、浦东机场接送、讲师休息室、分组研讨区,适合新人训和区域经理培训"
  135. },
  136. {
  137. "package_id": "SH002",
  138. "package_name": "上海大型展会参展3日套餐",
  139. "city": "上海",
  140. "days": 3,
  141. "price": 2980.00,
  142. "total_seats": 70,
  143. "remaining_seats": 22,
  144. "agency": "携程商旅",
  145. "rating": 4.8,
  146. "departure_dates": ["2026-05-12", "2026-06-10"],
  147. "highlights": "覆盖展馆通行、展位物料运输、酒店班车、客户邀约晚宴、现场证件管理,适合市场与渠道团队参展"
  148. },
  149. {
  150. "package_id": "HZ001",
  151. "package_name": "杭州电商标杆研学3日套餐",
  152. "city": "杭州",
  153. "days": 3,
  154. "price": 2180.00,
  155. "total_seats": 45,
  156. "remaining_seats": 16,
  157. "agency": "浙商会务",
  158. "rating": 4.7,
  159. "departure_dates": ["2026-05-09", "2026-06-06", "2026-06-21"],
  160. "highlights": "安排电商园区参访、直播基地交流、业务复盘会议、团队晚餐和西湖周边轻接待,适合运营与增长团队"
  161. },
  162. {
  163. "package_id": "SZ001",
  164. "package_name": "深圳创新企业参访3日套餐",
  165. "city": "深圳",
  166. "days": 3,
  167. "price": 2580.00,
  168. "total_seats": 55,
  169. "remaining_seats": 20,
  170. "agency": "南方商旅",
  171. "rating": 4.8,
  172. "departure_dates": ["2026-05-11", "2026-06-09"],
  173. "highlights": "覆盖科技园企业参访、创新产品交流、商务用车、酒店会议室、闭门分享会,适合产品、战略和投资团队"
  174. },
  175. {
  176. "package_id": "GZ001",
  177. "package_name": "广州供应链展会3日套餐",
  178. "city": "广州",
  179. "days": 3,
  180. "price": 2280.00,
  181. "total_seats": 65,
  182. "remaining_seats": 28,
  183. "agency": "广之旅商旅",
  184. "rating": 4.6,
  185. "departure_dates": ["2026-05-15", "2026-06-12"],
  186. "highlights": "适配采购团队参展,包含展馆接送、供应商洽谈室、商务午餐、样品寄送协助和晚间客户接待"
  187. },
  188. {
  189. "package_id": "XA001",
  190. "package_name": "西安制造业客户答谢2日套餐",
  191. "city": "西安",
  192. "days": 2,
  193. "price": 1380.00,
  194. "total_seats": 50,
  195. "remaining_seats": 19,
  196. "agency": "陕旅会务",
  197. "rating": 4.6,
  198. "departure_dates": ["2026-05-18", "2026-06-14"],
  199. "highlights": "包含客户答谢晚宴、工厂参观车辆、会议设备、伴手礼管理和历史文化轻接待路线,适合制造业销售团队"
  200. },
  201. {
  202. "package_id": "QD001",
  203. "package_name": "青岛销售年会3日套餐",
  204. "city": "青岛",
  205. "days": 3,
  206. "price": 2480.00,
  207. "total_seats": 120,
  208. "remaining_seats": 48,
  209. "agency": "海滨会务",
  210. "rating": 4.7,
  211. "departure_dates": ["2026-05-20", "2026-06-20"],
  212. "highlights": "支持百人销售年会、主会场搭建、分会场讨论、颁奖晚宴、团队拓展和海边商务晚餐"
  213. },
  214. {
  215. "package_id": "CQ001",
  216. "package_name": "重庆渠道大会2日套餐",
  217. "city": "重庆",
  218. "days": 2,
  219. "price": 1180.00,
  220. "total_seats": 100,
  221. "remaining_seats": 42,
  222. "agency": "山城会务",
  223. "rating": 4.6,
  224. "departure_dates": ["2026-05-16", "2026-06-11"],
  225. "highlights": "覆盖渠道商签到、主论坛、圆桌沟通、商务晚宴、酒店会议室和机场接送,适合区域渠道大会"
  226. },
  227. {
  228. "package_id": "NJ001",
  229. "package_name": "南京项目交付驻场5日套餐",
  230. "city": "南京",
  231. "days": 5,
  232. "price": 3680.00,
  233. "total_seats": 30,
  234. "remaining_seats": 9,
  235. "agency": "苏宁商旅",
  236. "rating": 4.7,
  237. "departure_dates": ["2026-05-13", "2026-06-07"],
  238. "highlights": "适合项目交付团队驻场,包含长住商务酒店、每日通勤用车、会议室、打印服务、加班简餐和周报复盘空间"
  239. },
  240. {
  241. "package_id": "XM001",
  242. "package_name": "厦门高管闭门会3日套餐",
  243. "city": "厦门",
  244. "days": 3,
  245. "price": 3980.00,
  246. "total_seats": 25,
  247. "remaining_seats": 7,
  248. "agency": "建发商旅",
  249. "rating": 4.9,
  250. "departure_dates": ["2026-05-22", "2026-06-18"],
  251. "highlights": "面向高管闭门战略会,包含海景会议室、贵宾接送、保密签到、商务晚宴、会议纪要服务和安静住宿环境"
  252. },
  253. ]
  254. # ==================== Milvus 初始化 ====================
  255. MILVUS_HOST = "localhost"
  256. MILVUS_PORT = 19530
  257. COLLECTION_NAME = "meeting_packages"
  258. DIMENSION = 1024 # text-embedding-v3 输出维度
  259. def create_meeting_package_collection():
  260. """
  261. 在 Milvus 中创建会务套餐 Collection
  262. 字段设计:
  263. - id: 主键(自增)
  264. - package_id: 套餐编号(字符串,如 YN001)
  265. - package_name: 套餐名称
  266. - city: 城市
  267. - days: 天数
  268. - price: 价格
  269. - total_seats: 总座位
  270. - remaining_seats: 剩余座位
  271. - agency: 差旅服务商
  272. - rating: 评分
  273. - departure_dates: 出发日期列表(JSON 字符串)
  274. - highlights: 方案亮点(用于生成向量)
  275. - embedding: 向量(1024 维)
  276. """
  277. # 连接 Milvus
  278. # 【数据准备 D2.1】先连接 Milvus,再检查同名集合。
  279. # 由文件末尾主程序首先调用;输出新建好的 Collection 对象,交给下一步入库函数。
  280. # 普通 FieldSchema 保存套餐字段,FLOAT_VECTOR 字段承载语义检索向量。
  281. # 已有集合删除后,新的自增 id 会重新由 Milvus 分配;它与样例中的 package_id 不是一个字段。
  282. connections.connect(alias="default", host=MILVUS_HOST, port=MILVUS_PORT)
  283. print(f"已连接 Milvus: {MILVUS_HOST}:{MILVUS_PORT}")
  284. # 如果已存在则删除
  285. # 【数据准备 D2.2】已有集合会被删除后重建;此操作不会保留旧集合中的套餐。
  286. if utility.has_collection(COLLECTION_NAME):
  287. print(f"集合 {COLLECTION_NAME} 已存在,删除重建")
  288. utility.drop_collection(COLLECTION_NAME)
  289. # 定义字段
  290. # 【数据准备 D2.3】普通字段保存套餐信息,embedding 字段保存用于相似度检索的向量。
  291. fields = [
  292. FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
  293. FieldSchema(name="package_id", dtype=DataType.VARCHAR, max_length=20),
  294. FieldSchema(name="package_name", dtype=DataType.VARCHAR, max_length=200),
  295. FieldSchema(name="city", dtype=DataType.VARCHAR, max_length=50),
  296. FieldSchema(name="days", dtype=DataType.INT64),
  297. FieldSchema(name="price", dtype=DataType.FLOAT),
  298. FieldSchema(name="total_seats", dtype=DataType.INT64),
  299. FieldSchema(name="remaining_seats", dtype=DataType.INT64),
  300. FieldSchema(name="agency", dtype=DataType.VARCHAR, max_length=100),
  301. FieldSchema(name="rating", dtype=DataType.FLOAT),
  302. FieldSchema(name="departure_dates", dtype=DataType.VARCHAR, max_length=200),
  303. FieldSchema(name="highlights", dtype=DataType.VARCHAR, max_length=1000),
  304. FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=DIMENSION),
  305. ]
  306. # 字段列表组装成集合结构;Collection(name, schema) 才将这个结构用于创建集合。
  307. schema = CollectionSchema(fields, description="会务套餐信息表(RAG 向量检索)")
  308. collection = Collection(COLLECTION_NAME, schema)
  309. print(f"集合 {COLLECTION_NAME} 创建成功")
  310. # 创建向量索引
  311. # IVF_FLAT 是选用的向量索引类型;COSINE 是相似度度量;nlist 控制索引分组参数。
  312. # 这些参数影响检索方式,不是套餐数量、向量维度或模型生成的 temperature。
  313. index_params = {
  314. "index_type": "IVF_FLAT",
  315. "metric_type": "COSINE",
  316. "params": {"nlist": 64}
  317. }
  318. # 【数据准备 D2.4】为 embedding 创建 COSINE 索引,然后返回集合给下一步使用。
  319. collection.create_index(field_name="embedding", index_params=index_params)
  320. print("向量索引创建成功 (IVF_FLAT, COSINE)")
  321. # 返回的是可操作该集合的客户端对象;不是套餐列表,也不是已经生成好的向量。
  322. return collection
  323. def insert_meeting_packages(collection):
  324. """
  325. 将会务套餐数据生成向量后插入 Milvus
  326. """
  327. # 【数据准备 D3.1】依次处理套餐:拼检索文本 → 请求向量 → 将向量和原字段放入 rows。
  328. # 输入 collection 是上一步返回的集合;无业务返回值,效果是将 rows 插入并刷入存储。
  329. # 每个样例先组成 embedding_text,再调用向量接口,最后把向量与业务字段放在同一行中。
  330. # 全部样例处理完以后才调用一次 collection.insert,前面的 rows.append 仅修改 Python 列表。
  331. print(f"\n开始为 {len(MEETING_PACKAGES)} 个会务套餐生成向量...")
  332. rows = []
  333. # enumerate 给出从 0 开始的索引 i 和当前套餐 group,进度显示用 i+1 转成从 1 开始的计数。
  334. for i, group in enumerate(MEETING_PACKAGES):
  335. # 使用"城市 + 套餐名称 + 方案亮点"作为 embedding 输入文本
  336. # 这是入库向量对应的文本;查询时会把用户需求另行转成向量与它比较。
  337. # 只把城市、名称和亮点拼成向量文本;价格、余位等普通字段随后原样保存,不通过向量生成。
  338. embedding_text = f"{group['city']} {group['package_name']} {group['highlights']}"
  339. if (i + 1) % 5 == 0:
  340. print(f" 已处理 {i + 1}/{len(MEETING_PACKAGES)} 个套餐...")
  341. embedding = get_embedding(embedding_text)
  342. rows.append({
  343. "package_id": group["package_id"],
  344. "package_name": group["package_name"],
  345. "city": group["city"],
  346. "days": group["days"],
  347. "price": group["price"],
  348. "total_seats": group["total_seats"],
  349. "remaining_seats": group["remaining_seats"],
  350. "agency": group["agency"],
  351. "rating": group["rating"],
  352. # 出发日期原来是列表,集合 schema 对应字段是 VARCHAR,所以先编码成 JSON 字符串保存。
  353. "departure_dates": json.dumps(group["departure_dates"], ensure_ascii=False),
  354. "highlights": group["highlights"],
  355. "embedding": embedding,
  356. })
  357. # 批量插入
  358. # 【数据准备 D3.2】全部向量准备完才批量插入,并 flush;这里没有逐条重试或断点续写。
  359. collection.insert(rows)
  360. # flush 要求刷入待持久化数据;它与后续查询前的 load 用途不同,load 是准备读取检索。
  361. collection.flush() # 刷盘
  362. print(f"成功插入 {len(MEETING_PACKAGES)} 个会务套餐数据")
  363. def test_search(collection):
  364. """
  365. 测试语义搜索:用自然语言查询会务套餐
  366. """
  367. # 测试查询
  368. # 【数据准备 D4.1】每条测试查询也先向量化,然后检索;本函数会实际调用向量接口。
  369. # 输入是已入库的 collection;输出方式为 print,便于人查看检索候选。
  370. # 每个 query 单独求向量,检索结果按“查询向量→命中列表”组织,所以有两层 for。
  371. # limit=2 限制候选条数,不表示验证了两个测试用例,也不是准确率阈值。
  372. test_queries = [
  373. "成都办数字化转型峰会,需要接送和酒店",
  374. "上海大型展会参展,需要展馆通行和客户晚宴",
  375. "适合销售团队的青岛年会方案",
  376. "深圳创新企业参访,最好包含闭门分享",
  377. "南京项目驻场,需要长住酒店和每日通勤",
  378. ]
  379. print("\n=== 语义搜索测试 ===")
  380. # 外层依次执行测试需求;前一条查询完成以后才开始下一条,这里没有并发任务调度。
  381. for query in test_queries:
  382. print(f"\n查询: {query}")
  383. query_embedding = get_embedding(query)
  384. collection.load()
  385. # 【数据准备 D4.2】取相似度靠前的两条并打印,供人工核对;不是自动质量评分。
  386. results = collection.search(
  387. data=[query_embedding],
  388. anns_field="embedding",
  389. param={"metric_type": "COSINE", "params": {"nprobe": 10}},
  390. limit=2,
  391. output_fields=["package_id", "package_name", "city", "days", "price", "rating", "agency", "highlights"]
  392. )
  393. for hits in results:
  394. for hit in hits:
  395. print(f" [{hit.entity.get('package_id')}] {hit.entity.get('package_name')} "
  396. f"- {hit.entity.get('city')} {hit.entity.get('days')}天 "
  397. f"¥{hit.entity.get('price')} 评分{hit.entity.get('rating')} "
  398. f"距离: {hit.distance:.4f}")
  399. # 【数据准备入口】仅直接运行本脚本时按 D2 → D3 → D4 执行;导入不会进入此区块。
  400. if __name__ == '__main__':
  401. # 本文件的实际运行入口:先拿到集合对象,再把同一个对象依次传给入库和测试函数。
  402. # 上面的函数定义在加载文件时被创建,但直到这里调用才真正连接服务、生成向量和写入数据。
  403. collection = create_meeting_package_collection()
  404. insert_meeting_packages(collection)
  405. test_search(collection)