265 lines
8.7 KiB
Python
Raw Permalink Normal View History

"""
语料采集引擎 · 核心大脑
========================
判断标准 脱敏规则 格式化输出
"""
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,
}