Files
xiaoli-llm-chat/generate_fresh_qa.py
T
shuiyu1123 91db488ad4 同步代码更新 (2026-06-16)
- 移除 generate_diverse_answers.py(多样化回答已废止)
- 更新 generate_fresh_qa.py(目标50万条新问答)
- 更新 generate_multiturn.py(6~12轮长对话,目标50万)
- 更新 regenerate_all_high_temp.py(独立保存,不追加训练集)
- 更新 .gitignore(忽略新输出目录)
- 所有API Key改为环境变量读取
2026-06-16 11:48:30 +00:00

240 lines
9.1 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 -*-
"""
全新问答对生成 — Astron 凭空造(新问题+新回答),5并发
不依赖现有问题列表,从种子话题衍生
增量保存防丢
"""
import json, requests, time, os, sys, random, hashlib
from concurrent.futures import ThreadPoolExecutor, as_completed
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
# ====== 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"
SYSTEM_PROMPT = """你是一个名叫小狸的8岁中国小女孩。
你的人设:
- 8岁女孩子,有点害羞但很可爱
- 说话简单自然,符合小朋友的语气
- 回答问题要简短,1-2句话就够了
- 不要用"喵"结尾,不要过度卖萌
回答要求:
1. 语气自然,像真实的小朋友说话
2. 不懂就说"不知道"
3. 回答简洁但要说清楚"""
# ====== 种子场景(生成对话的起点) ======
SCENES = [
# 日常场景
("问候", ["打招呼", "问好", "早上起来", "放学回家", "见到朋友"]),
("情绪", ["开心", "难过", "生气", "无聊", "害怕", "惊喜", "委屈", "害羞"]),
("分享", ["今天发生的事", "周末做了什么", "假期计划", "新玩具", "考试成绩"]),
("喜好", ["喜欢的颜色", "喜欢的动物", "喜欢的食物", "喜欢的动画片", "喜欢的游戏"]),
("好奇", ["为什么问题", "想知道的事", "不懂的事", "想象的事", "奇怪的问题"]),
("日常", ["起床", "刷牙", "吃饭", "上学", "做作业", "睡觉"]),
("朋友", ["好朋友", "吵架", "一起玩", "分享秘密", "帮忙"]),
("家庭", ["爸爸妈妈", "爷爷奶奶", "兄弟姐妹", "家人一起", "家庭活动"]),
("想象", ["童话故事", "梦想", "如果我有魔法", "长大想做什么", "幻想朋友"]),
("安慰", ["受伤了", "丢东西", "被骂了", "考试没考好", "被欺负", "做噩梦"]),
("节日", ["生日", "过年", "圣诞节", "儿童节", "中秋"]),
("自然", ["天气", "动物", "植物", "星空", "大海", "四季"]),
("学校", ["老师", "同学", "上课", "课间", "课外活动", "考试"]),
("晚安", ["说晚安", "睡前", "梦", "明天见", "晚安故事"]),
]
def call_astron(prompt, temperature=0.9):
for attempt in range(3):
try:
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {ASTRON_KEY}"}
payload = {
"model": ASTRON_MODEL,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt}
],
"temperature": temperature,
"max_tokens": 4096,
}
resp = requests.post(ASTRON_URL, headers=headers, json=payload, timeout=60)
if resp.status_code == 200:
content = resp.json()['choices'][0]['message']['content'].strip()
if content and len(content) > 10:
return content
else:
time.sleep(1)
except Exception:
time.sleep(2)
return None
def parse_qa_pairs(text):
"""解析 Astron 返回的问答对列表"""
lines = text.strip().split('\n')
pairs = []
current_q = None
for line in lines:
line = line.strip()
if not line:
continue
q_match = __import__('re').match(r'^[Q问][::]\s*(.*)', line)
a_match = __import__('re').match(r'^[A答][::]\s*(.*)', line)
if q_match:
current_q = q_match.group(1).strip()
elif a_match and current_q:
a_text = a_match.group(1).strip()
if current_q and a_text and len(a_text) > 2 and len(current_q) > 2:
pairs.append((current_q, a_text))
current_q = None
return pairs
def pair_fingerprint(q, a):
"""QA对指纹"""
raw = f"{q[:40]}|{a[:40]}"
return hashlib.md5(raw.encode()).hexdigest()
def generate_batch_pairs(batch_size=10, existing_fps=None):
"""生成一批全新的问答对"""
if existing_fps is None:
existing_fps = set()
# 随机选场景
category, sub_scenes = random.choice(SCENES)
sub = random.choice(sub_scenes)
# 随机生成要求
style = random.choice(["日常聊天", "撒娇可爱", "有点害羞", "好奇宝宝", "暖心安慰"])
mood = random.choice(["开心", "平静", "有点小情绪", "好奇", "温暖"])
prompt = f"""请以小狸的身份,生成{batch_size}个不同的日常对话问答对。
对话背景:
- 场景:{category}({sub})
- 风格:{style}
- 小狸心情:{mood}
要求:
1. 每个问答对包含一问一答,问是用户说的,答是小狸回答
2. 问要自然,像真实用户会说的话(不要提"小狸")
3. 答要是小狸的风格,1-2句话,简短自然
4. {batch_size}个问答要对同一个话题但角度不同
5. 不要重复一模一样的问法
请按以下格式输出:
Q:用户提问内容
A:小狸回答内容
---(分隔)
Q:用户提问内容
A:小狸回答内容
---"""
result = call_astron(prompt, temperature=random.uniform(0.85, 1.0))
if not result:
return []
pairs = parse_qa_pairs(result)
# 去重
deduped = []
for q, a in pairs:
fp = pair_fingerprint(q, a)
if fp not in existing_fps:
existing_fps.add(fp)
deduped.append({'question': q, 'answer': a})
return deduped
def main():
output_dir = '/workspace/xiaoli-llm-chat/fresh_qa_outputs'
os.makedirs(output_dir, exist_ok=True)
# ====== 恢复进度 ======
progress_file = os.path.join(output_dir, 'fresh_qa_progress.json')
all_pairs = []
if os.path.exists(progress_file):
with open(progress_file, encoding='utf-8') as f:
all_pairs = json.load(f)
print(f"已有进度: {len(all_pairs)} 条")
# 加载已有训练集做去重
train_path = '/workspace/xiaoli-llm-chat/xiaoli_train_v2.json'
with open(train_path, encoding='utf-8') as f:
train_data = json.load(f)
existing_fps = set(pair_fingerprint(item.get('question',''), item.get('answer','')) for item in train_data)
for p in all_pairs:
existing_fps.add(pair_fingerprint(p['question'], p['answer']))
print(f"去重指纹池: {len(existing_fps)} 个")
# ====== 参数 ======
TARGET = 500000 # 目标 50 万条全新问答
BATCH_SIZE = 15 # 每批次生成 15 条(更多问题,效率更高)
BATCHES_PER_SAVE = 20 # 每 20 批次保存一次
MAX_WORKERS = 5 # Astron 5并发
remaining = TARGET - len(all_pairs)
total_iterations = (remaining + BATCH_SIZE - 1) // BATCH_SIZE
print(f"目标: {TARGET:,} 条, 还需: {remaining:,} 条")
print(f"轮次: {total_iterations} 轮 (每轮{BATCH_SIZE}条, {MAX_WORKERS}并发)")
print(f"{'='*60}\n")
t_start = time.time()
batch_counter = 0
for iteration in range(total_iterations):
t0 = time.time()
# 并发生成
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = [
executor.submit(generate_batch_pairs, BATCH_SIZE, existing_fps)
for _ in range(MAX_WORKERS)
]
for future in as_completed(futures):
try:
new_pairs = future.result()
if new_pairs:
for p in new_pairs:
existing_fps.add(pair_fingerprint(p['question'], p['answer']))
all_pairs.append(p)
except Exception:
pass
batch_counter += 1
elapsed = time.time() - t_start
rate = len(all_pairs) / max(elapsed, 1) * 60
# 定期保存
if batch_counter % BATCHES_PER_SAVE == 0 or iteration == total_iterations - 1:
with open(progress_file, 'w', encoding='utf-8') as f:
json.dump(all_pairs, f, ensure_ascii=False, indent=2)
eta = (TARGET - len(all_pairs)) / max(rate, 0.1) / 60 if rate > 0 else 0
if iteration % 5 == 0 or iteration == total_iterations - 1:
print(f"轮 {iteration+1}/{total_iterations} | "
f"累计 {len(all_pairs):,}/{TARGET:,} | "
f"{rate:.0f}条/分 | "
f"已用 {elapsed/60:.1f}分 | "
f"预计剩余 {eta:.1f}分")
# ====== 完成 ======
elapsed_total = time.time() - t_start
print(f"\n{'='*60}")
print(f"全新问答生成完成!")
print(f"总数: {len(all_pairs):,} 条")
print(f"用时: {elapsed_total/60:.1f} 分钟")
print(f"速率: {len(all_pairs)/max(elapsed_total,1)*60:.0f} 条/分")
# 保存最终
final_path = '/data/user/work/fresh_qa_v1.json'
with open(final_path, 'w', encoding='utf-8') as f:
json.dump(all_pairs, f, ensure_ascii=False, indent=2)
print(f"保存到: {final_path}")
if __name__ == '__main__':
main()