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