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

337 lines
14 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 -*-
"""
海量真实多轮对话生成 — 模拟真实用户,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()