#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 海量通用数据蒸馏 v3 — 不设场景标签,纯自然对话,多策略并行 目标:500万+ 纯 (question, answer) 对 """ import json, requests, time, os, sys, random, re from concurrent.futures import ThreadPoolExecutor, as_completed sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1) MODEL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT_DIR = os.path.join(MODEL_DIR, 'general_outputs') os.makedirs(OUT_DIR, exist_ok=True) # ====== Astron 配置 ====== ASTRON_URL = "https://maas-coding-api.cn-huabei-1.xf-yun.com/v2/chat/completions" ASTRON_KEY = os.environ.get("ASTRON_KEY", "") ASTRON_MODEL = "astron-code-latest" # ====== 主题全覆盖(六大类 × 子类) ====== TOPICS = { "日常": [ "起床洗漱", "吃早餐", "上学路上", "上课", "课间休息", "午餐时间", "放学回家", "做作业", "晚饭", "睡前时光", "周末活动", "天气变化", "穿衣打扮", "零花钱", "家务劳动", "看电视", "玩玩具", "去公园", "逛超市", "去亲戚家", ], "情绪": [ "开心高兴", "难过伤心", "生气愤怒", "害怕恐惧", "紧张焦虑", "孤独寂寞", "委屈冤枉", "失望沮丧", "感动温暖", "兴奋激动", "无聊烦闷", "尴尬害羞", "后悔内疚", "羡慕嫉妒", "感激感恩", ], "社交": [ "交新朋友", "和好朋友玩", "和同学相处", "和老师交流", "和兄弟姐妹", "和爸爸妈妈", "和爷爷奶奶", "和邻居", "和陌生人", "和喜欢的人", "吵架和好", "被欺负", "被误解", "被表扬", "被批评", "帮助别人", "请求帮助", "分享东西", "邀请做客", "告别分开", ], "知识": [ "动物世界", "植物花草", "天文星空", "天气自然", "人体健康", "食物营养", "颜色形状", "数字计算", "汉字词语", "科学现象", "历史故事", "地理知识", "交通工具", "乐器音乐", "绘画手工", "运动体育", "节日习俗", "各国文化", "发明创造", "宇宙太空", ], "想象": [ "超能力", "魔法世界", "未来科技", "外星人", "恐龙时代", "童话故事", "变成动物", "飞行梦", "海底世界", "神秘岛屿", "时间旅行", "超级英雄", "会说话的玩具", "隐形衣", "变身药水", "机器人朋友", "彩虹桥", "星星糖果", "云朵房子", "梦中冒险", ], "成长": [ "学新本领", "克服困难", "不怕失败", "坚持努力", "自我肯定", "梦想未来", "职业理想", "好习惯", "改掉坏毛病", "学会分享", "学会等待", "学会道歉", "学会感恩", "学会勇敢", "学会理解", "交心谈话", "秘密分享", "烦恼倾诉", "人生道理", "温暖鼓励", ], } FLAT_TOPICS = [] for cat, subs in TOPICS.items(): for sub in subs: FLAT_TOPICS.append((cat, sub)) # ====== 多样化的开场方式 ====== OPENERS = [ "小狸", "嗨小狸", "小狸在吗", "嘿嘿", "那个…", "喂喂喂", "小狸小狸", "我想问你", "你知道吗", "你说", "我突然想到", "我跟你讲", "小狸你听我说", "问你个事", "我有个问题", "好无聊啊", "好开心呀", "好烦", "怎么办呀", "小狸你在干嘛", "我来了", "出来玩", "跟你说个事", ] # ====== 策略1: 直接生成纯问答对 ====== def make_direct_pairs_prompt(count=15): """无场景标签,直接生成纯问答对""" # 随机选3-5个主题混搭 selected = random.sample(FLAT_TOPICS, min(random.randint(3, 5), len(FLAT_TOPICS))) topics_str = "、".join([f"{c}({s})" for c, s in selected]) prompt = f"""请直接生成 {count} 条聊天问答对。 格式要求(严格按此格式): 问:xxxxxxxxx | 答:xxxxxxxxx 用户的问题要:口语化、自然、像真人(5-25字) 小狸的回答要:8岁小女孩语气,可爱活泼(10-40字) 话题要多样:{topics_str} 示例: 问:你怕不怕黑呀? | 答:有一点点…但是抱着小熊玩偶就不怕啦! 问:我今天跑步摔了一跤 | 答:疼不疼呀?我给你吹吹就不疼了! 现在生成 {count} 条:""" return prompt # ====== 策略2: 短对话(3-5轮) ====== def make_mini_chat_prompt(count=8): """微型多轮对话,每段3-5轮""" ops = random.sample(OPENERS, min(count, len(OPENERS))) prompt = f"""请生成 {count} 段微型聊天对话,每段3-5轮(一来一回算1轮)。 要求: - 每段一个独立话题,话题要多样(日常、情绪、知识、想象、社交、成长等) - 用户说话要特别自然、口语化,像真人随口说的 - 小狸像8岁小女孩,回答要可爱、有画面感 - 话题要自然延伸,不要生硬转折 格式例子: 话题:日常 用户:今天好冷啊 小狸:是呀!我穿了厚厚的羽绒服,像只小企鹅一样~ 用户:哈哈哈真的好像 小狸:(鼓起腮帮子)才不像呢!企鹅走路摇摇摆摆的,我走路可稳啦! ... 现在生成 {count} 段,每段用"话题:xxx"开头:""" return prompt # ====== 策略3: 长对话(6-10轮自由展开) ====== def make_long_chat_prompt(): """一场长对话,话题自然展开""" topic = random.choice(FLAT_TOPICS) opener = random.choice(OPENERS) emotions = ["开心", "难过", "兴奋", "沮丧", "好奇", "担心", "轻松", "紧张"] emotion = random.choice(emotions) prompt = f"""请生成一段自然的日常聊天,{topic[0]}类-{topic[1]}话题。 用户开场:{opener} 用户当下的心情:{emotion} 要求: - 用户很自然地说出开场白,就像真人发微信一样 - 小狸正常回复,话题慢慢展开,不要硬聊 - 聊6-10轮(一问一答算一轮) - 每轮格式:用户:xxx\n小狸:xxx - 真实感第一,不要刻意 请直接输出对话,不要加说明:""" return prompt # ====== 策略4: 单轮问答(高吞吐) ====== def make_single_turn_prompt(batch_size=20): """纯单轮问答,批量生成""" selected = random.sample(FLAT_TOPICS, min(6, len(FLAT_TOPICS))) prompt = f"""生成 {batch_size} 条日常问答对。 要求: 1. 用户的话:7-25字,口语化,真实感强 2. 小狸回答:1-2句,8岁小女孩语气,可爱活泼 3. 覆盖多样化话题:{', '.join([s for _, s in selected[:6]])}(但不仅限于这些) 4. 每行一条,格式:问: ... | 答: ... 5. 不要重复相同的问题 示例: 问: 你喜不喜欢下雨天呀? | 答: 喜欢!因为可以踩水坑,啪嗒啪嗒的超好玩! 问: 我今天被同学起外号了 | 答: (歪着头)那…你喜欢这个外号吗?不喜欢的话要大声说出来哦! 问: 你说世界上有外星人吗? | 答: 我觉得有!说不定他们正在用望远镜看我们呢~ 生成:""" return prompt # ====== API调用 ====== def call_astron(prompt, temp=0.82, max_tokens=4096): for attempt in range(3): try: headers = {"Content-Type": "application/json", "Authorization": f"Bearer {ASTRON_KEY}"} payload = { "model": ASTRON_MODEL, "messages": [ {"role": "system", "content": "你是一个擅长写日常对话的助手。小狸是一个8岁的小女孩,说话可爱自然。"}, {"role": "user", "content": prompt} ], "temperature": temp, "max_tokens": max_tokens, } resp = requests.post(ASTRON_URL, headers=headers, json=payload, timeout=150) if resp.status_code == 200: content = resp.json()['choices'][0]['message']['content'].strip() if content and len(content) > 20: return content else: time.sleep(0.5 + attempt) except Exception: time.sleep(1 + attempt) return None # ====== 解析 ====== def parse_pairs(text): """从"问:xxx | 答:xxx"格式解析问答对""" pairs = [] for line in text.split('\n'): line = line.strip() parts = re.split(r'\s*[||]\s*', line) if len(parts) == 2: q = re.sub(r'^问\s*[::]\s*', '', parts[0]).strip() a = re.sub(r'^答\s*[::]\s*', '', parts[1]).strip() if q and a and len(q) >= 2 and len(a) >= 2 and len(a) < 300 and len(q) < 200: pairs.append((q, a)) return pairs def parse_mini_chats(text): """解析微型多轮对话""" pairs = [] segments = re.split(r'话题[::].*?\n', text) for seg in segments: seg = seg.strip() lines = seg.split('\n') q = None for line in lines: line = line.strip() if not line: continue q_match = re.match(r'^(用户|我)[::]\s*(.*)', line) a_match = re.match(r'^(小狸|答)[::]\s*(.*)', line) if q_match: q_text = q_match.group(2).strip() if q_text and len(q_text) > 1: q = q_text elif a_match and q: a_text = a_match.group(2).strip() if a_text and len(a_text) > 1 and len(a_text) < 300: pairs.append((q, a_text)) q = None return pairs def parse_long_chat(text): """解析长对话""" pairs = [] q = None for line in text.split('\n'): line = line.strip() if not line: continue q_match = re.match(r'^(用户|我)[::]\s*(.*)', line) a_match = re.match(r'^(小狸)[::]\s*(.*)', line) if q_match: q_text = q_match.group(2).strip() if q_text and len(q_text) > 1: q = q_text elif a_match and q: a_text = a_match.group(2).strip() if a_text and len(a_text) > 1 and len(a_text) < 300: pairs.append((q, a_text)) q = None return pairs # ====== 生成器(按策略分发) ====== GENERATORS = [ # (name, prompt_fn, parse_fn, batch_size_estimate, weight_for_diversity) ("direct", make_direct_pairs_prompt, parse_pairs, 15, 35), ("mini_chat", make_mini_chat_prompt, parse_mini_chats, 20, 25), ("long_chat", make_long_chat_prompt, parse_long_chat, 20, 20), ("single", make_single_turn_prompt, parse_pairs, 20, 20), ] def run_one_generation(): """运行一次生成任务(随机选一种策略)""" # 按权重选策略 weights = [w for _, _, _, _, w in GENERATORS] gen = random.choices(GENERATORS, weights=weights, k=1)[0] name, prompt_fn, parse_fn, batch_est, _ = gen prompt = prompt_fn() result = call_astron(prompt, temp=random.uniform(0.75, 0.92), max_tokens=4096 if 'long' not in name else 6144) if not result: return None pairs = parse_fn(result) if not pairs: return None return pairs def main(): print("=" * 65) print(" 海量通用数据蒸馏 v3") print(" 不设场景标签 | 6大类120+子话题 | 4种策略") print("=" * 65) # 加载已有进度 all_pairs = [] existing_keys = set() progress_file = os.path.join(OUT_DIR, 'all_general_progress.json') if os.path.exists(progress_file): existing = json.load(open(progress_file, encoding='utf-8')) all_pairs = existing for p in existing: existing_keys.add((p.get('question',''), p.get('answer',''))) print(f"已有进度: {len(all_pairs):,} 对") # ====== 参数 ====== TARGET = 2000000 # 目标200万对 BATCH = 40 # 每批40个任务 MAX_WORKERS = 8 # 8并发(比之前5更猛) CHECKPOINT = 5000 # 每5000对保存 to_generate = TARGET - len(all_pairs) total_batches = (to_generate + BATCH - 1) // BATCH print(f"目标: {TARGET:,} 对") print(f"已有: {len(all_pairs):,} 对") print(f"还需: {to_generate:,} 对") print(f"批次: {total_batches:,} 批 ({BATCH}任务/批, {MAX_WORKERS}并发)") # 策略统计 strategy_stats = {g[0]: {'runs': 0, 'pairs': 0} for g in GENERATORS} t_start = time.time() last_checkpoint = len(all_pairs) for batch_idx in range(total_batches): t0 = time.time() # 并发 with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: futures = [executor.submit(run_one_generation) for _ in range(BATCH)] batch_pairs = [] for future in as_completed(futures): try: r = future.result() if r: batch_pairs.extend(r) except: pass # 去重 new_pairs = [] for q, a in batch_pairs: key = (q, a) if key not in existing_keys: existing_keys.add(key) new_pairs.append({'question': q, 'answer': a}) all_pairs.extend(new_pairs) elapsed = time.time() - t0 total_elapsed = time.time() - t_start rate = len(all_pairs) / max(total_elapsed, 1) * 60 # 估算 pairs_this_batch = len(new_pairs) total_raw = len(batch_pairs) dedup_rate = pairs_this_batch / max(total_raw, 1) * 100 print(f"[{batch_idx+1}/{total_batches}] " f"+{pairs_this_batch}对(有效{total_raw}/{dedup_rate:.0f}%) " f"累计{len(all_pairs):,}/{TARGET:,} " f"{rate:.0f}对/分 " f"剩余{(TARGET-len(all_pairs))/max(rate,0.1):.0f}分") # 定期保存 if len(all_pairs) - last_checkpoint >= CHECKPOINT: with open(progress_file, 'w', encoding='utf-8') as f: json.dump(all_pairs, f, ensure_ascii=False) last_checkpoint = len(all_pairs) # 也保存到数据目录一份 merged_path = os.path.join(MODEL_DIR, 'data', 'general_ongoing.json') with open(merged_path, 'w', encoding='utf-8') as f: json.dump(all_pairs, f, ensure_ascii=False) # 速率统计 stable_rate = len(all_pairs) / max(time.time() - t_start, 1) * 60 print(f" 💾 保存 ({len(all_pairs):,} 对, {stable_rate:.0f}对/分)") # ====== 完成 ====== total_elapsed = time.time() - t_start with open(progress_file, 'w', encoding='utf-8') as f: json.dump(all_pairs, f, ensure_ascii=False) print(f"\n{'='*65}") print(f"✅ 生成完成!") print(f" 总对数: {len(all_pairs):,}") print(f" 用时: {total_elapsed/60:.1f} 分") print(f" 平均速率: {len(all_pairs)/max(total_elapsed,1)*60:.0f} 对/分") print(f" 保存: {progress_file}") print(f"{'='*65}") if __name__ == '__main__': main()