Files
xiaoli-llm-chat/generate_diverse_answers.py
T

256 lines
9.9 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 高温生成 50 个不同回答
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. 回答简洁但要说清楚,不要讲一半就停"""
def call_astron(q, temperature):
"""调用 Astron,返回回答或 None"""
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": q}
],
"temperature": temperature,
"max_tokens": 2048,
}
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) > 2:
return content
else:
time.sleep(1)
except Exception:
time.sleep(2)
return None
def answer_fingerprint(text):
"""回答指纹 — 去空格标点后取前 30 字"""
clean = ''.join(text.split())[:30]
return hashlib.md5(clean.encode()).hexdigest()
def is_duplicate(new, existing):
"""检查是否与已有回答重复(精确+模糊)"""
fp = answer_fingerprint(new)
if fp in existing:
return True
# 模糊:如果新回答是某个已有回答的子串或反之
short_new = ''.join(new.split())[:20]
for old_fp, old_text in existing.items():
short_old = ''.join(old_text.split())[:20]
if short_new in short_old or short_old in short_new:
return True if len(short_new) > 8 else False
return False
def generate_answers_for_question(q, target=50, max_attempts=100, temperature=0.95):
"""为单个问题生成 target 个不同回答"""
answers = {} # fp -> full_text
attempt = 0
t_start = time.time()
while len(answers) < target and attempt < max_attempts:
# 温度在 0.85~1.0 之间随机波动,避免卡在同一模式
temp = temperature + random.uniform(-0.1, 0.05)
temp = max(0.85, min(1.0, temp))
result = call_astron(f"问题:{q}\n\n请以小狸的身份回答:", temp)
if result:
if not is_duplicate(result, answers):
fp = answer_fingerprint(result)
answers[fp] = result
attempt += 1
if attempt % 20 == 0:
elapsed = time.time() - t_start
rate = len(answers) / max(elapsed, 1)
if len(answers) < target and rate > 0:
eta = (target - len(answers)) / rate
else:
eta = 0
print(f" [{len(answers)}/{target}] attempt={attempt}, {rate:.1f}条/秒, 预计剩余{eta:.0f}秒")
sys.stdout.flush()
return list(answers.values())
def main():
model_dir = '/workspace/xiaoli-llm-chat'
output_dir = os.path.join(model_dir, 'diverse_answers')
os.makedirs(output_dir, exist_ok=True)
# ====== 参数(可调) ======
QUESTIONS_PER_BATCH = 100 # 每批处理多少问题
ANSWERS_PER_QUESTION = 50 # 每个问题生成多少个不同回答
MAX_ATTEMPTS = 150 # 最多尝试多少次(超过就放弃)
MAX_WORKERS = 5 # Astron 5并发
TEMPERATURE = 0.95 # 高温(0.85~1.0 浮动)
# ====== 加载问题列表 ======
train_path = os.path.join(model_dir, 'xiaoli_train_v2.json')
with open(train_path, encoding='utf-8') as f:
data = json.load(f)
# 取所有不重复的问题
all_questions = list(set(item.get('question', '') for item in data))
random.shuffle(all_questions)
print(f"训练集总条数: {len(data)}")
print(f"不重复问题数: {len(all_questions)}")
# 统计问题出现频次,优先处理高频问题
q_freq = {}
for item in data:
q = item.get('question', '')
q_freq[q] = q_freq.get(q, 0) + 1
# 按频次排序(高频靠前,低频靠后,随机打乱同频)
freq_buckets = {}
for q in all_questions:
f = q_freq.get(q, 1)
freq_buckets.setdefault(f, []).append(q)
sorted_questions = []
for freq in sorted(freq_buckets.keys(), reverse=True):
bucket = freq_buckets[freq]
random.shuffle(bucket)
sorted_questions.extend(bucket)
print(f"按频次排序完成 (高频优先处理)")
# ====== 恢复已有进度 ======
progress_file = os.path.join(output_dir, 'diversity_progress.json')
completed = {} # question -> list of answers
if os.path.exists(progress_file):
with open(progress_file, encoding='utf-8') as f:
completed = json.load(f)
# 过滤掉已处理的问题
processed_qs = set(completed.keys())
sorted_questions = [q for q in sorted_questions if q not in processed_qs]
print(f"已有进度: {len(completed)} 个问题已完成")
print(f"剩余未处理: {len(sorted_questions)} 个问题")
# 限制要处理的问题数
max_q = len(sorted_questions) # 全部问题
total_questions = len(sorted_questions)
total_batches = (total_questions + QUESTIONS_PER_BATCH - 1) // QUESTIONS_PER_BATCH
print(f"\n目标: {total_questions} 个问题 × {ANSWERS_PER_QUESTION} 个回答 = {total_questions * ANSWERS_PER_QUESTION} 条")
print(f"批次: {total_batches} 批 (每批 ≤{QUESTIONS_PER_BATCH} 问题)")
print(f"并发: {MAX_WORKERS}")
if total_questions > 1000:
minutes = total_questions * ANSWERS_PER_QUESTION / (MAX_WORKERS * 8) / 60
print(f"预估耗时: ~{minutes:.0f} 分钟 (~{minutes/60:.1f} 小时)")
print(f"{'='*70}\n")
t_start = time.time()
total_new_pairs = 0
for batch_idx in range(total_batches):
batch_qs = sorted_questions[batch_idx * QUESTIONS_PER_BATCH : (batch_idx + 1) * QUESTIONS_PER_BATCH]
batch_results = {}
print(f"\n批次 {batch_idx + 1}/{total_batches} ({len(batch_qs)} 个问题)")
# 并发生成
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = {
executor.submit(generate_answers_for_question, q, ANSWERS_PER_QUESTION, MAX_ATTEMPTS, TEMPERATURE): q
for q in batch_qs
}
for future in as_completed(futures):
q = futures[future]
try:
answers = future.result()
if answers:
batch_results[q] = answers
print(f" ✅ 『{q[:30]}』 → {len(answers)} 个不同回答")
else:
print(f" ❌ 『{q[:30]}』 → 生成失败")
except Exception as e:
print(f" ❌ 『{q[:30]}』 → 错误: {e}")
# 追加到总进度
completed.update(batch_results)
batch_new_pairs = sum(len(v) for v in batch_results.values())
total_new_pairs += batch_new_pairs
# 保存进度
with open(progress_file, 'w', encoding='utf-8') as f:
json.dump(completed, f, ensure_ascii=False, indent=2)
elapsed = time.time() - t_start
rate = total_new_pairs / max(elapsed, 1) * 60
# 统计
print(f"\n批次 {batch_idx + 1} 完成 | "
f"本批新增 {batch_new_pairs} 条 | "
f"累计 {total_new_pairs:,} 条 ({len(completed)} 问题) | "
f"{rate:.0f}条/分 | "
f"已用 {elapsed/60:.1f}分")
# ====== 完成 ======
elapsed_total = time.time() - t_start
print(f"\n{'='*70}")
print(f"全部生成完成!")
print(f"处理问题: {len(completed)} 个")
print(f"总回答数: {total_new_pairs:,} 条")
print(f"用时: {elapsed_total/60:.1f} 分钟")
# ====== 保存为训练格式 ======
train_format = []
for q, answers in completed.items():
for a in answers:
train_format.append({'question': q, 'answer': a})
output_file = os.path.join(output_dir, 'diverse_train_data_v1.json')
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(train_format, f, ensure_ascii=False, indent=2)
# 也保存一份到工作目录
work_file = '/data/user/work/diverse_train_data_v1.json'
with open(work_file, 'w', encoding='utf-8') as f:
json.dump(train_format, f, ensure_ascii=False, indent=2)
print(f"\n结果保存:")
print(f" 📄 {output_file}")
print(f" 📄 {work_file}")
print(f" 📊 平均每问题 {total_new_pairs/max(len(completed),1):.1f} 个回答")
print(f"\n如需合并到训练集:")
print(f" python3 -c \"import json; d1=json.load(open('xiaoli_train_v2.json')); d2=json.load(open('{output_file}')); d1.extend(d2); json.dump(d1, open('xiaoli_train_v2.json','w'), ensure_ascii=False, indent=2)\"")
print(f"{'='*70}")
if __name__ == '__main__':
main()