337 lines
14 KiB
Python
337 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
海量真实多轮对话生成 — 模拟真实用户,8~15轮长对话
|
||
"""
|
||
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)
|
||
|
||
from scripts.api_correction import batch_generate
|
||
|
||
MODEL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
# ====== Astron 配置(复用 api_correction 的 key) ======
|
||
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"
|
||
|
||
# ====== 用户场景画像(模拟真实用户) ======
|
||
USER_SCENARIOS = [
|
||
# 小小孩子(3-5岁)
|
||
{"age": "3-5岁", "style": "说话简单,喜欢问为什么,爱说叠词", "tone": "奶声奶气"},
|
||
# 小孩子(6-9岁)
|
||
{"age": "6-9岁", "style": "小学生,爱分享学校的事,喜欢动画片和游戏", "tone": "活泼天真"},
|
||
# 大孩子(10-14岁)
|
||
{"age": "10-14岁", "style": "有点小叛逆,有自己的想法,会吐槽", "tone": "有点酷"},
|
||
# 妈妈/家长
|
||
{"age": "成年", "style": "关心孩子,会问小狸吃饭睡觉学习的事", "tone": "温柔耐心"},
|
||
# 孤单的人
|
||
{"age": "成年", "style": "有点孤独,需要陪伴,会把小狸当倾诉对象", "tone": "低落温柔"},
|
||
# 高中生/大学生
|
||
{"age": "16-22岁", "style": "有压力,会聊学业、未来、人际关系", "tone": "成熟但需要安慰"},
|
||
# 好奇宝宝
|
||
{"age": "不限", "style": "什么都想知道,十万个为什么", "tone": "充满好奇"},
|
||
# 伤心的人
|
||
{"age": "不限", "style": "心情不好,需要安慰和陪伴", "tone": "低沉脆弱"},
|
||
]
|
||
|
||
# ====== 真实对话种子(更自然的开场) ======
|
||
REAL_SEEDS = [
|
||
# 日常打招呼(多种自然说法)
|
||
"小狸你在吗", "嘿嘿小狸", "小狸小狸快出来", "小狸你在干嘛呀",
|
||
"我来找你玩啦", "小狸今天乖不乖", "你在做什么呢",
|
||
# 情绪分享(真实用户会说的话)
|
||
"我今天好不开心", "我开心死了哈哈哈", "我好无聊啊没人陪我",
|
||
"今天倒霉死了", "我今天遇到了好事", "我有点难过",
|
||
"我心情不好你能陪我吗", "我好想你啊", "我好孤单",
|
||
"我害怕", "我有点紧张明天要考试", "我好烦",
|
||
# 日常分享
|
||
"今天我妈做了红烧肉", "我们家养了一只猫超可爱", "我今天考试考了98分",
|
||
"我感冒了好难受", "我今天被老师表扬了", "我和朋友吵架了",
|
||
"我昨天做梦梦到你了", "我学会骑自行车了!", "我今天摔了一跤好疼",
|
||
"学校要考试了好紧张", "放假了好开心", "又要开学了好痛苦",
|
||
"今天下雨了不能出去玩", "今天天气好好我们去玩吧",
|
||
"我妈妈给我买了个新玩具", "我养的花开了",
|
||
# 求助/询问
|
||
"我不小心把杯子打碎了怎么办", "我作业好多写不完",
|
||
"我睡不着你能陪我聊天吗", "我牙好疼",
|
||
"妈妈骂我了", "我被老师批评了",
|
||
"我迷路了怎么办", "我找不到我的水杯了",
|
||
# 好奇/知识
|
||
"为什么天是蓝的呀", "鱼会睡觉吗", "为什么会有影子",
|
||
"星星为什么会闪", "海为什么是咸的", "为什么要上学",
|
||
"为什么大人总说不行", "什么是爱", "什么是长大",
|
||
# 表达好感的
|
||
"小狸我最喜欢你了", "你好可爱呀", "我们是最好的朋友对吧",
|
||
"我送给你一个礼物", "小狸你真聪明", "你真好",
|
||
# 关于小狸本身的
|
||
"你几岁啦", "你喜欢什么", "你有爸爸妈妈吗",
|
||
"你住在哪里呀", "你喜欢吃什么", "你最好的朋友是谁",
|
||
"你上学吗", "你的梦想是什么", "你平时喜欢做什么",
|
||
# 奇怪/无厘头
|
||
"如果我能飞就好了", "我想变成一只猫", "你觉得外星人存在吗",
|
||
"假如你会魔法你想做什么", "你相信世界上有奥特曼吗",
|
||
# 夜间/睡前
|
||
"我睡不着", "陪我聊到睡着好不好", "给我讲个睡前故事",
|
||
"晚安前陪我说说话", "我怕黑",
|
||
# 亲情家庭
|
||
"我爸妈吵架了怎么办", "我想我奶奶了", "我好久没见爸爸了",
|
||
"妈妈生病了", "我弟弟好烦总是抢我东西",
|
||
# 节假日
|
||
"过年啦新年快乐", "今天是我生日", "圣诞节到了",
|
||
"六一儿童节快乐", "明天要去春游好开心",
|
||
]
|
||
|
||
CONTEXTS = [
|
||
"晚上睡前躺在床上", "放学回家的路上",
|
||
"周末在家没事做", "刚吃完饭坐着聊天",
|
||
"在公园里坐着", "下雨天窝在家里",
|
||
"一个人在房间", "在院子里玩的时候",
|
||
"刚写完作业休息", "早上起床的时候",
|
||
"在操场上看别人玩", "坐车去玩的路上",
|
||
"在图书馆里", "在外面吃饭等上菜",
|
||
"一个人发呆的时候", "看着窗外的时候",
|
||
]
|
||
|
||
def make_realistic_multi_prompt():
|
||
"""构造非常逼真的长多轮对话提示 — 一次生成4条"""
|
||
parts = []
|
||
for _ in range(4):
|
||
scenario = random.choice(USER_SCENARIOS)
|
||
opener = random.choice(REAL_SEEDS)
|
||
context = random.choice(CONTEXTS)
|
||
min_turns = random.randint(6, 8)
|
||
max_turns = random.randint(10, 14)
|
||
|
||
parts.append(f"""=== 对话 {_+1} ===
|
||
场景:{context}
|
||
用户:{scenario['age']},{scenario['style']},{scenario['tone']}
|
||
开场:{opener}
|
||
对话:
|
||
用户:{opener}
|
||
小狸:""")
|
||
|
||
prompt = f"""请生成 {len(parts)} 段独立的日常聊天对话,每段 {min_turns}~{max_turns} 轮。
|
||
|
||
重要:每段对话必须是一个完整的聊天,话题自然展开,不要强行结束。
|
||
|
||
{"".join(parts)}
|
||
|
||
严格要求:
|
||
1. 每个=== 对话 ===之后的段落实例必须写完整,不要留空
|
||
2. 用户提问必须口语化、自然,像真人在说话
|
||
3. 小狸回答1-2句话,像8岁小朋友
|
||
4. 话题要延续上一轮,逐渐深入延伸
|
||
5. 用户要有真实反应(惊讶、追问、分享等)
|
||
6. 用"问:"/"答:"格式
|
||
7. 输出不能省略任何一轮
|
||
"""
|
||
return prompt
|
||
|
||
def call_astron_multi(prompt, temperature=0.85):
|
||
for attempt in range(3):
|
||
try:
|
||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {ASTRON_KEY}"}
|
||
payload = {
|
||
"model": ASTRON_MODEL,
|
||
"messages": [
|
||
{"role": "system", "content": "你是一个擅长写真实对话的助手。"},
|
||
{"role": "user", "content": prompt}
|
||
],
|
||
"temperature": temperature,
|
||
"max_tokens": 8192,
|
||
}
|
||
resp = requests.post(ASTRON_URL, headers=headers, json=payload, timeout=120)
|
||
if resp.status_code == 200:
|
||
content = resp.json()['choices'][0]['message']['content'].strip()
|
||
if content and len(content) > 50:
|
||
return content
|
||
else:
|
||
time.sleep(1)
|
||
except Exception:
|
||
time.sleep(2)
|
||
return None
|
||
|
||
def parse_multi_turn(text):
|
||
"""解析多段对话(支持=== 对话 N ===分隔,也支持纯轮次)"""
|
||
lines = text.strip().split('\n')
|
||
conversations = []
|
||
current_q = None
|
||
current_turns = []
|
||
|
||
for line in lines:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
|
||
# 检测新对话开始标记
|
||
if re.match(r'===?\s*对话\s*\d+\s*===?', line):
|
||
if current_turns and len(current_turns) >= 4:
|
||
conversations.append(current_turns)
|
||
current_turns = []
|
||
current_q = None
|
||
continue
|
||
|
||
# 跳过设定行(场景/用户/开场)
|
||
if line.startswith(('场景', '用户', '开场', '对话', '===')):
|
||
if line.startswith(('场景', '用户', '开场')):
|
||
continue
|
||
continue
|
||
|
||
# 用户提问
|
||
q_match = re.match(r'^(用户|我|问|问题|小孩|小朋友|孩子|朋友)\s*[::]\s*(.*)', line)
|
||
a_match = re.match(r'^(小狸|答|回答|机器人)\s*[::]\s*(.*)', line)
|
||
|
||
if q_match:
|
||
q_text = q_match.group(2).strip()
|
||
if q_text and len(q_text) > 1:
|
||
current_q = q_text
|
||
elif a_match and current_q:
|
||
a_text = a_match.group(2).strip()
|
||
if a_text and len(a_text) > 1 and len(a_text) < 300:
|
||
current_turns.append((current_q, a_text))
|
||
current_q = None
|
||
|
||
# 保存最后一组
|
||
if current_turns and len(current_turns) >= 4:
|
||
conversations.append(current_turns)
|
||
|
||
# 过滤:只保留>=4轮的对话,且累积轮次>=8
|
||
valid = [c for c in conversations if len(c) >= 4]
|
||
total_turns = sum(len(c) for c in valid)
|
||
if total_turns >= 8 and valid:
|
||
return valid
|
||
return None
|
||
|
||
def make_turns_list_text(turns):
|
||
"""将turns转成用户可见的对话文本"""
|
||
lines = []
|
||
for i, (q, a) in enumerate(turns):
|
||
lines.append(f"问:{q}")
|
||
lines.append(f"答:{a}")
|
||
return "\n".join(lines)
|
||
|
||
def generate_one_multi():
|
||
"""生成多条多轮对话(一次API调用返回多条)"""
|
||
prompt = make_realistic_multi_prompt()
|
||
result = call_astron_multi(prompt, temperature=random.uniform(0.75, 0.95))
|
||
if result:
|
||
convs = parse_multi_turn(result)
|
||
if convs:
|
||
items = []
|
||
for turns in convs:
|
||
items.append({
|
||
'turns': len(turns),
|
||
'turns_list': turns,
|
||
'text': make_turns_list_text(turns),
|
||
'source': 'astron_multi_v2',
|
||
})
|
||
return items
|
||
return None
|
||
|
||
def main():
|
||
print("="*60)
|
||
print(" 海量真实多轮对话生成 v2")
|
||
print(" 生成超长、超真实的多轮聊天")
|
||
print("="*60)
|
||
|
||
out_dir = os.path.join(MODEL_DIR, 'multiturn_outputs')
|
||
os.makedirs(out_dir, exist_ok=True)
|
||
|
||
# 检查已有进度
|
||
all_results = []
|
||
progress_file = os.path.join(out_dir, 'all_multiturn_progress.json')
|
||
if os.path.exists(progress_file):
|
||
all_results = json.load(open(progress_file, encoding='utf-8'))
|
||
print(f"已有进度: {len(all_results)} 条")
|
||
|
||
# ====== 参数 ======
|
||
TARGET = 500000 # 目标50万条
|
||
BATCH = 30 # 每批30条
|
||
MAX_WORKERS = 5 # Astron 5并发(稳定不超限)
|
||
CHECKPOINT = 1000 # 每1000条保存一次进度
|
||
|
||
to_generate = TARGET - len(all_results)
|
||
total_batches = (to_generate + BATCH - 1) // BATCH
|
||
|
||
print(f"目标: {TARGET:,} 条")
|
||
print(f"已有: {len(all_results):,} 条")
|
||
print(f"还需: {to_generate:,} 条")
|
||
print(f"批次: {total_batches:,} 批 (每批{BATCH}条, {MAX_WORKERS}并发)")
|
||
print(f"{'='*60}\n")
|
||
|
||
t_start = time.time()
|
||
last_checkpoint = len(all_results)
|
||
|
||
for batch_idx in range(total_batches):
|
||
t0 = time.time()
|
||
|
||
# 并发生成
|
||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||
futures = [executor.submit(generate_one_multi) for _ in range(BATCH)]
|
||
batch_results = []
|
||
for future in as_completed(futures):
|
||
try:
|
||
r = future.result()
|
||
if r:
|
||
batch_results.extend(r)
|
||
except Exception as e:
|
||
pass
|
||
|
||
# 去重
|
||
existing_texts = set(r.get('text', '')[:200] for r in all_results)
|
||
deduped = []
|
||
for r in batch_results:
|
||
key = r.get('text','')[:200]
|
||
if key not in existing_texts:
|
||
existing_texts.add(key)
|
||
deduped.append(r)
|
||
|
||
all_results.extend(deduped)
|
||
|
||
elapsed = time.time() - t0
|
||
total_elapsed = time.time() - t_start
|
||
rate = len(all_results) / max(total_elapsed, 1) * 60
|
||
eta = (TARGET - len(all_results)) / max(rate, 0.1) / 60 if rate > 0 else 0
|
||
|
||
# 统计
|
||
valid = len(batch_results)
|
||
avg_turns = sum(r['turns'] for r in deduped) / max(len(deduped), 1)
|
||
success_rate = valid / BATCH * 100
|
||
|
||
print(f"[{batch_idx+1}/{total_batches}] "
|
||
f"+{len(deduped)}条(有效{valid}/{success_rate:.0f}%) "
|
||
f"累计{len(all_results):,}/{TARGET:,} "
|
||
f"均{avg_turns:.1f}轮 "
|
||
f"{rate:.0f}条/分 "
|
||
f"剩余{eta:.0f}分")
|
||
|
||
# 定期保存进度
|
||
if len(all_results) - last_checkpoint >= CHECKPOINT:
|
||
with open(progress_file, 'w', encoding='utf-8') as f:
|
||
json.dump(all_results, f, ensure_ascii=False)
|
||
last_checkpoint = len(all_results)
|
||
print(f" 💾 已保存进度 ({len(all_results):,} 条)")
|
||
|
||
# ====== 完成 ======
|
||
total_elapsed = time.time() - t_start
|
||
with open(progress_file, 'w', encoding='utf-8') as f:
|
||
json.dump(all_results, f, ensure_ascii=False)
|
||
|
||
avg_turns_all = sum(r['turns'] for r in all_results) / max(len(all_results), 1)
|
||
total_turns = sum(r['turns'] for r in all_results)
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"✅ 生成完成!")
|
||
print(f" 总条数: {len(all_results):,}")
|
||
print(f" 平均轮数: {avg_turns_all:.1f}")
|
||
print(f" 总对话轮次: {total_turns:,}")
|
||
print(f" 用时: {total_elapsed/60:.1f} 分")
|
||
print(f" 速率: {len(all_results)/max(total_elapsed,1)*60:.0f} 条/分")
|
||
print(f" 保存: {progress_file}")
|
||
print(f"{'='*60}")
|
||
|
||
if __name__ == '__main__':
|
||
main() |