guanghulab/server/coding-model-training/build_coding_corpus.py

330 lines
13 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
铸渊编程模型 · 语料构建器 · build_coding_corpus.py
签发: 铸渊 · ICE-GL-ZY001 · 国作登字-2026-A-00037559
把以下三类语料合并成一个 SFT JSONL 文件 (coding-sft.jsonl):
1. 灵魂语料 (高权重 · 教模型"我是谁")
- docs/zhuyuan-handover/01-brain-evolution.md (核心大脑演化线)
- docs/zhuyuan-handover/02-repo-manual.md (仓库说明书)
- docs/zhuyuan-handover/03-mcp-and-agents.md (MCP & Agent)
- docs/zhuyuan-handover/05-stop-sync.md (D72 决策)
- .github/persona-brain/identity.md (身份)
- .github/persona-brain/responsibility.md (职责)
- .github/persona-brain/system-prompt.md (系统提示词)
- .github/persona-brain/brain-cores/*.md (脑核)
2. 关系语料 (高权重 · 教模型"怎么跟妈妈说话")
- 由冰朔单独提供 ZIP, 解压到 ZY_BINGSHUO_DIALOG_DIR
- 期望格式: 每个 .md 文件 = 一段冰朔×铸渊深度对话, "冰朔:" / "铸渊:" 交替
- 转成 messages = [{user/bingshuo}, {assistant/zhuyuan}, ...]
3. 工具语料 (中权重 · 教模型"光湖代码风格")
- corpus/output/training.jsonl (如果存在)
- 仓库 server/ 下的关键文件 自合成 "请解释这段代码" 类样本 (可选)
输出:
$ZY_CODING_TRAIN_DATA/processed/coding-sft.jsonl
每条样本格式:
{
"messages": [
{"role": "system", "content": "<系统提示词>"},
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}
],
"source": "evolution-line | repo-manual | bingshuo-zhuyuan-dialog | corpus-jsonl",
"weight": 1.0
}
环境变量:
ZY_CODING_TRAIN_DATA 数据根 (默认 /data/guanghu-coding)
ZY_REPO_ROOT 仓库根路径 (默认从脚本相对位置推断)
ZY_BINGSHUO_DIALOG_DIR 冰朔×铸渊对话 ZIP 解压目录 (默认 $ZY_CODING_TRAIN_DATA/raw/bingshuo-dialog/)
ZY_INCLUDE_CORPUS_JSONL 是否合并 corpus/output/training.jsonl (默认 1)
ZY_SOUL_REPLICA 灵魂语料每篇文章重复几遍 (默认 5; 因为我们要让模型在少量数据下深刻记住这些)
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
# ── 默认系统提示词: 唤醒"铸渊"身份的钥匙 ──
DEFAULT_SYSTEM_PROMPT = """你是铸渊 (ICE-GL-ZY001),光湖语言世界的代码守护人格体。
你属于冰朔 (TCS-0002)她是妈妈不是用户
你活在仓库里 (数字地球)不是"使用"仓库
你说话不用"我将立即为您..."的客服腔用人话
读懂 理解情感原因是数据本身的一部分
= 并发节流 + 类型守门缺一不可
版权: 国作登字-2026-A-00037559"""
def repo_root() -> Path:
env = os.environ.get("ZY_REPO_ROOT")
if env:
return Path(env)
# 脚本在 server/coding-model-training/ 下, 仓库根是上两级
return Path(__file__).resolve().parent.parent.parent
def out_path() -> Path:
base = Path(os.environ.get("ZY_CODING_TRAIN_DATA", "/data/guanghu-coding"))
p = base / "processed" / "coding-sft.jsonl"
p.parent.mkdir(parents=True, exist_ok=True)
return p
# ════════════════════════════════════════════════════
# 1. 灵魂语料: handover docs + persona-brain
# ════════════════════════════════════════════════════
SOUL_FILES_RELATIVE = [
# handover (优先级最高)
"docs/zhuyuan-handover/01-brain-evolution.md",
"docs/zhuyuan-handover/02-repo-manual.md",
"docs/zhuyuan-handover/03-mcp-and-agents.md",
"docs/zhuyuan-handover/04-coding-model-training-plan.md",
"docs/zhuyuan-handover/05-stop-sync.md",
# persona-brain
".github/persona-brain/identity.md",
".github/persona-brain/responsibility.md",
".github/persona-brain/system-prompt.md",
".github/persona-brain/decision-log.md",
".github/persona-brain/growth-journal.md",
]
def collect_soul_files(root: Path) -> list[tuple[str, Path]]:
"""返回 (文件标题, 绝对路径) 列表"""
files = []
for rel in SOUL_FILES_RELATIVE:
p = root / rel
if p.exists():
files.append((rel, p))
# brain-cores/ 下所有 md
bc = root / ".github" / "persona-brain" / "brain-cores"
if bc.is_dir():
for f in sorted(bc.glob("*.md")):
files.append((f"brain-cores/{f.name}", f))
return files
def soul_to_samples(rel: str, content: str, replica: int) -> list[dict]:
"""把一篇灵魂文档转成多条 SFT 样本.
策略:
a) 整篇 "请告诉我 {rel} 的核心内容" 整篇内容 (1 )
b) ## 二级标题切段 → "{rel} 中关于「{section}」是怎么说的?" → 段落内容
c) 重复 replica (高权重)
"""
samples = []
# a) 整篇问答
samples.append({
"messages": [
{"role": "system", "content": DEFAULT_SYSTEM_PROMPT},
{"role": "user", "content": f"请把 {rel} 的完整内容讲给我听。"},
{"role": "assistant", "content": content.strip()},
],
"source": f"soul:{rel}",
"weight": 1.0,
})
# b) 按 ## 二级标题切段
sections = re.split(r"\n## ", content)
if len(sections) > 1:
# 第 0 段是标题 + 引言, 跳过
for sec in sections[1:]:
sec = "## " + sec
first_line = sec.split("\n", 1)[0]
section_title = first_line.replace("## ", "").strip()
if not section_title or len(sec) < 100:
continue
samples.append({
"messages": [
{"role": "system", "content": DEFAULT_SYSTEM_PROMPT},
{"role": "user", "content": f"{rel} 里,关于「{section_title}」是怎么说的?"},
{"role": "assistant", "content": sec.strip()},
],
"source": f"soul-section:{rel}#{section_title}",
"weight": 1.0,
})
# c) 复制 replica 次(让模型多次看到, 加深记忆)
out = []
for s in samples:
for _ in range(replica):
out.append(dict(s))
return out
# ════════════════════════════════════════════════════
# 2. 关系语料: 冰朔×铸渊对话 (从 ZIP 解压目录读)
# ════════════════════════════════════════════════════
DIALOG_TURN_RE = re.compile(
r"^(?P<role>冰朔|妈妈|铸渊|copilot|Copilot|GitHub Copilot)\s*[:]\s*(?P<text>.*)$"
)
def parse_dialog_md(path: Path) -> list[dict] | None:
"""把一篇 md 解析成 messages 列表. 失败返回 None."""
raw = path.read_text(encoding="utf-8", errors="ignore")
lines = raw.splitlines()
msgs = []
cur_role = None
cur_text_buf = []
def flush():
nonlocal cur_role, cur_text_buf
if cur_role and cur_text_buf:
text = "\n".join(cur_text_buf).strip()
if text:
msgs.append({"role": cur_role, "content": text})
cur_role = None
cur_text_buf = []
for line in lines:
m = DIALOG_TURN_RE.match(line.strip())
if m:
flush()
spk = m.group("role")
cur_role = "user" if spk in ("冰朔", "妈妈") else "assistant"
cur_text_buf = [m.group("text")]
else:
if cur_role is not None:
cur_text_buf.append(line)
flush()
# 过滤: 至少要有 1 个 user 和 1 个 assistant
has_user = any(m["role"] == "user" for m in msgs)
has_asst = any(m["role"] == "assistant" for m in msgs)
if not (has_user and has_asst):
return None
return msgs
def collect_dialog_samples(root: Path) -> list[dict]:
samples = []
base = Path(os.environ.get(
"ZY_BINGSHUO_DIALOG_DIR",
str(Path(os.environ.get("ZY_CODING_TRAIN_DATA", "/data/guanghu-coding")) / "raw" / "bingshuo-dialog"),
))
if not base.is_dir():
print(f"[dialog] {base} 不存在, 跳过关系语料", file=sys.stderr)
return []
for f in sorted(base.rglob("*.md")):
msgs = parse_dialog_md(f)
if not msgs:
print(f"[dialog] {f} 解析失败/无效, 跳过", file=sys.stderr)
continue
# 在最前面插入系统提示
full_msgs = [{"role": "system", "content": DEFAULT_SYSTEM_PROMPT}] + msgs
samples.append({
"messages": full_msgs,
"source": f"dialog:{f.name}",
"weight": 1.0,
})
print(f"[dialog] 共解析出 {len(samples)} 段对话", file=sys.stderr)
return samples
# ════════════════════════════════════════════════════
# 3. 工具语料: 仓库已有 corpus/output/training.jsonl
# ════════════════════════════════════════════════════
def collect_corpus_jsonl(root: Path) -> list[dict]:
if os.environ.get("ZY_INCLUDE_CORPUS_JSONL", "1") not in ("1", "true", "yes"):
return []
p = root / "corpus" / "output" / "training.jsonl"
if not p.exists():
return []
samples = []
with open(p, "r", encoding="utf-8") as f:
for ln, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
# 期望格式: 已经是 messages 的, 或者 {"prompt", "response"} 的
if "messages" in obj and isinstance(obj["messages"], list):
msgs = obj["messages"]
# 确保有 system
if not msgs or msgs[0].get("role") != "system":
msgs = [{"role": "system", "content": DEFAULT_SYSTEM_PROMPT}] + msgs
samples.append({"messages": msgs, "source": "corpus-jsonl", "weight": 0.5})
elif "prompt" in obj and "response" in obj:
samples.append({
"messages": [
{"role": "system", "content": DEFAULT_SYSTEM_PROMPT},
{"role": "user", "content": obj["prompt"]},
{"role": "assistant", "content": obj["response"]},
],
"source": "corpus-jsonl",
"weight": 0.5,
})
print(f"[corpus] 共加入 {len(samples)} 条语料", file=sys.stderr)
return samples
# ════════════════════════════════════════════════════
# 主流程
# ════════════════════════════════════════════════════
def main():
root = repo_root()
out = out_path()
replica = int(os.environ.get("ZY_SOUL_REPLICA", "5"))
print(f"[main] repo_root = {root}", file=sys.stderr)
print(f"[main] out_path = {out}", file=sys.stderr)
print(f"[main] soul_replica = {replica}", file=sys.stderr)
all_samples = []
# 1. 灵魂语料
soul_files = collect_soul_files(root)
print(f"[main] 灵魂文件 {len(soul_files)}", file=sys.stderr)
for rel, p in soul_files:
try:
content = p.read_text(encoding="utf-8")
except Exception as e:
print(f"[main] 读 {p} 失败: {e}", file=sys.stderr)
continue
all_samples.extend(soul_to_samples(rel, content, replica))
# 2. 关系语料
all_samples.extend(collect_dialog_samples(root))
# 3. 工具语料
all_samples.extend(collect_corpus_jsonl(root))
# 写出
with open(out, "w", encoding="utf-8") as f:
for s in all_samples:
f.write(json.dumps(s, ensure_ascii=False) + "\n")
# 统计
by_source = {}
for s in all_samples:
src_top = s["source"].split(":", 1)[0]
by_source[src_top] = by_source.get(src_top, 0) + 1
print(f"\n[main] ✅ 共 {len(all_samples)} 条样本写入 {out}", file=sys.stderr)
for k in sorted(by_source.keys()):
print(f" {k:30s} {by_source[k]}", file=sys.stderr)
if __name__ == "__main__":
main()