Files
xiaoli-llm-chat/scripts/generate_massive_general.py
T

389 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()