265 lines
8.7 KiB
Python
Raw Permalink 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.

"""
语料采集引擎 · 核心大脑
========================
判断标准 → 脱敏规则 → 格式化输出
"""
import re
import json
import hashlib
from typing import List, Dict, Optional
# ============================================================
# 第1层脱敏引擎
# ============================================================
SENSITIVE_PATTERNS = [
# IP地址用 lookahead/lookbehind 替代 \b避免中文干扰
(r'(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)', '[IP已脱敏]'),
# 端口号(数字前有冒号)
(r'(?::)(\d{4,5})(?:\s|/|$||\))', lambda m: '[端口已脱敏]'),
# 手机号
(r'(?<!\d)1[3-9]\d{9}(?!\d)', '[手机号已脱敏]'),
# 邮箱
(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}', '[邮箱已脱敏]'),
# URL
(r'https?://[^\s,\)\u4e00-\u9fff]+', '[URL已脱敏]'),
# 密钥/token
(r'(?:sk-|pk-|zy_gtw_|ghp_|gho_|ghu_|ghs_)[A-Za-z0-9_-]{20,}', '[密钥已脱敏]'),
(r'(?<![A-Za-z0-9])[A-Za-z0-9_-]{32,}(?![A-Za-z0-9])', '[密钥已脱敏]'),
]
USERNAME_PATTERN = re.compile(r'(?:冰朔|Bingshuo|霜砚|Shuangyan|铸渊|Zhuyuan|用户|user|assistant)\b', re.IGNORECASE)
# ============================================================
# 第2层价值判断引擎
# ============================================================
# 无价值的单条对话(过滤规则)
FILTER_PATTERNS = [
r'^(?:吃[了過]?[吗嘛沒]?|喝[了過]?[吗嘛沒]?|睡[了過]?[吗嘛沒]?|醒[了過]?[吗嘛沒]?)',
r'^(?:好的|好[吧嘛]|嗯嗯?|哦[哦]?|ok|okay|行|可以|没问题|收到|明白|了解|[知]道[了]?)',
r'^(?:早|晚|早安|晚安|早上好|晚上好|[你]好|[哈]喽|嗨|hi|hello)',
r'^(?:谢谢|感谢|多谢|辛苦[了]?|谢谢[你])$',
r'^(?:在[吗嘛]|[你]在[吗嘛]|[你]忙[吗嘛])',
r'^(?:图片?|文件|链接|附件)',
r'^(?:发[给送]我|发给[你]|你看看|你看下|你看)',
r'^[。,!?、;:\.\,\!\?\s]{1,5}$',
]
# 有价值的模式(保留信号)
VALUE_PATTERNS = [
# 技术讨论
r'(?:模型|训练|微调|SFT|LoRA|蒸馏|推理|loss|准确率|参数|权重|checkpoint)',
# 架构决策
r'(?:架构|设计|方案|选型|为什么|原因|对比|优势|劣势|代价|trade.?off)',
# Bug/踩坑
r'(?:报错|错误|bug|崩溃|异常|IndexError|TypeError|显存|OOM|内存|越界|失败|挂了)',
# 思考过程
r'(?:觉得|认为|理解|思考|思路|逻辑|原因|根因|教训|总结|反思|复盘)',
# 代码/开发
r'(?:代码|函数|接口|API|路由|部署|docker|nginx|pm2|脚本|自动|工具)',
# 业务/需求
r'(?:需求|客户|项目|功能|模块|版本|上线|迭代|规划|计划|目标)',
# 数据/语料
r'(?:数据|语料|样本|数据集|标注|清洗|预处理|格式|jsonl|json|chatml)',
# 学习/研究
r'(?:论文|研究|学习|教程|文档|资料|参考|案例|实践)',
]
MIN_CONTENT_LENGTH = 15 # 最少字数
def desensitize(text: str) -> str:
"""脱敏处理"""
for pattern, replacement in SENSITIVE_PATTERNS:
text = re.sub(pattern, replacement, text)
return text.strip()
def is_valuable(text: str) -> bool:
"""判断一段对话是否有采集价值"""
text = text.strip()
# 长度过滤
if len(text) < MIN_CONTENT_LENGTH:
return False
# 无效内容过滤
for pattern in FILTER_PATTERNS:
if re.match(pattern, text, re.IGNORECASE):
return False
# 有价值信号检查
for pattern in VALUE_PATTERNS:
if re.search(pattern, text, re.IGNORECASE):
return True
return False
# ============================================================
# 第3层对话对提取
# ============================================================
def extract_dialog_pairs(messages: List[Dict]) -> List[Dict]:
"""
从消息流中提取有价值的对话对
输入格式: [{"role": "user/human/assistant/ai", "content": "..."}, ...]
输出格式: [{"user": "...", "assistant": "...", "source": "..."}, ...]
"""
pairs = []
current_user = None
for msg in messages:
role = msg.get("role", "")
content = msg.get("content", "").strip()
if not content:
continue
# 脱敏
safe_content = desensitize(content)
# 用户消息
if role in ("user", "human", "", ""):
if is_valuable(safe_content):
current_user = safe_content
# AI/助手回复
elif role in ("assistant", "ai", "agent") and current_user:
if is_valuable(safe_content) or is_valuable(current_user):
pairs.append({
"user": current_user,
"assistant": safe_content,
"source": msg.get("source", "unknown"),
})
current_user = None
# 未知角色 - 尝试作为单条有价值内容
else:
if is_valuable(safe_content):
current_user = safe_content
return pairs
# ============================================================
# 第4层格式化为微调语料
# ============================================================
def to_chatml(user_text: str, assistant_text: str) -> Dict:
"""将单条对话对转为ChatML格式"""
return {
"messages": [
{"role": "user", "content": user_text},
{"role": "assistant", "content": assistant_text}
]
}
def format_sft_jsonl(pairs: List[Dict], system_prompt: Optional[str] = None) -> List[Dict]:
"""将对话对列表转为SFT数据集格式ChatML JSONL"""
samples = []
for pair in pairs:
sample = to_chatml(pair["user"], pair["assistant"])
if system_prompt:
sample["messages"].insert(0, {"role": "system", "content": system_prompt})
samples.append(sample)
return samples
def generate_corpus_id(text: str) -> str:
"""生成语料唯一ID用于去重"""
return hashlib.md5(text.encode()).hexdigest()[:12]
# ============================================================
# 第5层内容分类标签
# ============================================================
TAG_KEYWORDS = {
"技术讨论": ["模型", "训练", "微调", "SFT", "LoRA", "蒸馏", "推理", "loss"],
"架构设计": ["架构", "设计", "方案", "选型", "系统"],
"踩坑记录": ["报错", "错误", "bug", "崩溃", "异常", "索引", "越界"],
"代码开发": ["代码", "函数", "接口", "部署", "脚本", "工具"],
"数据语料": ["数据", "语料", "样本", "数据集", "标注"],
"业务沟通": ["需求", "客户", "项目", "功能"],
"学习研究": ["论文", "学习", "教程", "文档"],
}
def classify_content(text: str) -> List[str]:
"""对内容自动分类打标签"""
tags = []
text_lower = text.lower()
for tag, keywords in TAG_KEYWORDS.items():
for kw in keywords:
if kw.lower() in text_lower:
tags.append(tag)
break
return tags if tags else ["通用对话"]
# ============================================================
# 导出接口
# ============================================================
def process_text_chunk(text: str, source: str = "screen_capture") -> List[Dict]:
"""
处理单段文本从OCR/截图来的)
返回: [{"user": ..., "assistant": ..., "source": ..., "tags": [...]}, ...]
"""
# 脱敏
safe_text = desensitize(text)
# 判断价值
if not is_valuable(safe_text):
return []
# 由于单段文本可能只有一方发言,包装成单条语料
tags = classify_content(safe_text)
return [{
"text": safe_text,
"source": source,
"tags": tags,
"corpus_id": generate_corpus_id(safe_text),
"timestamp": None, # 由外部补充
}]
def process_dialog_stream(messages: List[Dict]) -> Dict:
"""
处理完整对话流
返回: { "pairs": [...], "singles": [...], "stats": {...} }
"""
# 提取对话对
pairs = extract_dialog_pairs(messages)
# 格式化
sft_samples = format_sft_jsonl(pairs)
# 统计
stats = {
"total_messages": len(messages),
"valuable_pairs": len(pairs),
"total_chars": sum(len(p["user"]) + len(p["assistant"]) for p in pairs),
}
return {
"pairs": sft_samples,
"stats": stats,
}
# 快捷检查
def preview(text: str) -> Dict:
"""快速预览一条文本的处理结果"""
safe = desensitize(text)
valuable = is_valuable(safe)
tags = classify_content(safe) if valuable else []
return {
"original_len": len(text),
"safe_len": len(safe),
"valuable": valuable,
"tags": tags,
}