紧接系列第五、六篇(英文 corpus.jsonl 与后端 reference_material 注入),本文记录 中文知识库 从 0 到可验收的完整路径:为何在已有「英文语料 + 中文 hint 桥接」后仍要建中文库语料来源如何选取如何通过爬虫批量获得正文corpus_zh.jsonl 如何构建与打标签检索层如何从纯英文 token 升级为 jieba + 短语匹配;以及 Git 与运维策略。文末用一小节说明同期对 静态前端顶栏 的统一改造。


一、动机:英文参考够用,为什么还要中文语料?

第五、六篇已经跑通一条可演示的 RAG 链路:

  • 离线语料 corpus.jsonl(英文童话,language: en);
  • 运行时中文创作参数(如「神秘森林」「勇敢小狮子」)经 _ZH_EN_HINTS 映射为英文检索词;
  • 检索结果以 reference_material 注入编剧 prompt,输出语言仍由 language: zh-CN 约束

这条路径 工程上成立,但在答辩与产品叙事上有三个明显短板:

  1. 命中解释成本高:听众会问「中文场景怎么匹配英文正文?」——需要额外讲 hint 表与 fallback,不够直观。
  2. 语义对齐弱:「深海世界」「糖果城堡」等选项在 hint 表中覆盖有限,Top-K 常靠 SHA256 确定性兜底 凑数,相关性波动大。
  3. 与输出语言不一致:参考节选是英文,模型需在「结构启发」与「禁止直译」之间自行权衡,坏例里偶现英文词渗入选题。

因此本轮迭代目标不是推翻第六篇,而是 增加一条平行语料与检索分支

维度 英文库 corpus.jsonl 中文库 corpus_zh.jsonl
来源 项目根 fairy_tales/{n}.txt storynook.cn 爬取 → fairy_tales_zh/
规模(当前) 1651 120 条(可继续扩)
检索 英文 token + 中文→英文 hint jieba 分词 + 场景/主角短语
切换方式 .envRAG_CORPUS_FILENAME=corpus.jsonl RAG_CORPUS_FILENAME=corpus_zh.jsonl

生成服务入口不变:仍由 StoryGenerationService._reference_material_for_generation 消费字符串块;仅 retrieve_for_story 内部 按语料语言切换策略。


二、语料选取:来源、范围与合规边界

2.1 为什么选 storynook.cn

中文儿童向短篇、栏目清晰(睡前故事、格林/安徒生等分类),正文集中在 #storyContent / .article-content,适合 规则解析 + 批量离线,无需在生成请求里实时爬页。

仓库内已有 fairy_tale_crawler_storynook.py(多站点爬虫,含 storynook / storyberries)。本轮在 scripts/crawl_zh_fairy_tales.py复用其 HTTP、列表收集与正文解析,避免重复造轮子。

2.2 选取原则

  • 儿童向、篇幅适中:单篇多为几百~几千字,截断后仍适合作为 prompt 参考(默认单条 body 上限 12000 字符)。
  • 结构完整:有标题 + 叙事正文,便于 theme_tags 从标题与开头抽取。
  • 可离线重建:语料 不绑运行时网络;队友 clone 仓库后执行脚本即可得到 jsonl。
  • 合规:仅作 叙事结构与语气启发,对外说明「AI 生成 + 语料参考」;爬取时 --delay ≥ 1.0,遵守站点访问礼仪。

2.3 未选用方案(简要对比)

方案 未采用原因
机翻英文 fairy_tales 翻译腔重,且与「中文原生童话语感」不符
纯手搓 JSONL 100+ 条人力成本高,不利于迭代
运行时 Wikipedia API 增加演示依赖与延迟,违背离线 jsonl 策略
第一版上向量库 运维与 embedding 费用高;关键词 + jieba 已可验收

三、语料数据的获得:爬虫流水线

3.1 一键命令

在项目根目录(依赖 beautifulsoup4,见爬虫模块说明):

cd D:\agent_proj\AIstorygenerator

# 连通性探测:首页 ID 数量 + 试抓一篇
backend\.venv\Scripts\python.exe scripts\crawl_zh_fairy_tales.py --probe

# 爬取 120 篇并构建 jsonl(建议 delay >= 1.0)
backend\.venv\Scripts\python.exe scripts\crawl_zh_fairy_tales.py --max-articles 120 --delay 1.0 --build-corpus

--build-corpus 会在爬取结束后 自动调用 build_knowledge_corpus_zh.py,产出 backend/data/knowledge/corpus_zh.jsonl

3.2 爬取逻辑概要

crawl_zh_fairy_tales.py 主要步骤:

  1. collect_story_urls:从 storynook 首页与各分类列表页收集 /story/{id} 链接(底层实现在 fairy_tale_crawler_storynook.py)。
  2. 逐篇 http_get + parse_story_page:提取标题与 #storyContent 正文。
  3. 质量过滤:正文少于 30 字则跳过。
  4. 落盘
    • fairy_tales_zh/0001.txt … 连续编号正文;
    • fairy_tales_zh/manifest.jsonl 每行记录 seqtitleurltxt_filesource_site

manifest 示例字段:

{
  "seq": 1,
  "title": "小老鼠打电话",
  "url": "https://storynook.cn/story/1",
  "txt_file": "0001.txt",
  "source_site": "storynook.cn"
}

3.3 运维参数

参数 建议 说明
--delay 1.0~1.5 请求间隔,避免对站点造成压力
--max-articles 120~200 满足「≥100 条语料」类指标
--proxy 按需 校园网/公司网不通时可设 http://127.0.0.1:7890
--category-from/to 默认 1~10 控制列表爬取分类范围

失败单篇不会中断整批:控制台打印 [fail] 并继续下一 URL。


四、语料库构建:corpus_zh.jsonl 的字段与标签

4.1 输出路径与格式

与英文库相同,一行一条 JSON,路径:

backend/data/knowledge/corpus_zh.jsonl

字段仍对齐 KnowledgeRecordidtitletheme_tagsbodysourcelanguage(固定 zh)。

单条示例(节选):

{
  "id": "ft_zh_0027",
  "title": "森林中的小河",
  "theme_tags": ["chinese", "fairy_tale", "童话", "森林", "友谊", "善良"],
  "body": "森林里有一条小小的小河……",
  "source": "https://storynook.cn/story/27",
  "language": "zh"
}

4.2 标题与正文处理

scripts/build_knowledge_corpus_zh.py 核心逻辑:

  • 标题:优先 manifest 中的 title;否则取 txt 首个非空行(上限 200 字)。
  • 正文normalize_body 压缩连续空行;过短(<30 字)跳过;过长截断至 --max-body-chars(默认 12000)并加「…」。
  • IDft_zh_{seq:04d},与 manifest 序号一致,便于日志对照。

4.3 theme_tags:规则表 + 标题抽词

中文库不用「从英文标题抠词」,而采用 固定主题词表 扫描标题与正文前 800 字:

_ZH_THEME_KEYWORDS: tuple[str, ...] = (
    "森林",
    "公主",
    "王子",
    ...
    "家庭",
)
def tags_from_content(title: str, body: str, category: str = "") -> list[str]:
    base = ["chinese", "fairy_tale", "童话"]
    hay = f"{title}\n{body[:800]}\n{category}"
    seen = set(base)
    out = list(base)
    for kw in _ZH_THEME_KEYWORDS:
        if kw in hay and kw not in seen:
            seen.add(kw)
            out.append(kw)
    for m in re.finditer(r"[\u4e00-\u9fff]{2,4}", title):
        w = m.group(0)
        if w not in seen and len(out) < 16:
            seen.add(w)
            out.append(w)
    return out

思考要点:

  • 基底三项保证检索器总能命中「童话」类 fallback。
  • 主题词表与创作页选项(神秘森林、勇敢小狮子等) deliberately 重叠,提高「场景/主角」与 tags 的共现概率。
  • 标题 2~4 字抽词作为 弱特征,避免 tags 过少;总数封顶 16,控制噪声。

五、检索升级:从「英文 token」到「中文分词 + 短语」

5.1 新增 tokenizer.py 与语言自动识别

将查询侧与语料语言判断从 retriever.py 拆出 backend/app/services/rag/tokenizer.py

  • 英文路径query_terms_en — 正则 [A-Za-z]{3,} + _expand_chinese_hints(与第六篇兼容)。
  • 中文路径query_terms_zh — 把 sceneprotagonistinterest_tags 等作为 高权重短语;再用 jieba 切分补充 token;无 jieba 时退化为 CJK 二字词 + bigram
def query_terms_zh(payload: CreateStoryRequest) -> QueryTerms:
    parts = _payload_text_parts(payload)
    text = " ".join(parts)
    phrases: set[str] = set()
    for part in parts:
        part = (part or "").strip()
        if len(part) >= 2:
            phrases.add(part)
    ...
    if _HAS_JIEBA:
        tokens.update(_jieba_tokens(text))
    else:
        tokens.update(_fallback_zh_tokens(text))
    stop = {"的", "了", "在", "是", "和", "与", "一个", "故事", "小"}
    tokens = {t for t in tokens if t not in stop and len(t) >= 2}
    return QueryTerms(tokens=tokens, phrases=phrases)

语言解析settings.rag_corpus_language,默认 auto):

  • 文件名含 corpus_zhzh
  • 否则读首条 record 的 language 字段;
  • 显式 en / zh 可强制覆盖。

5.2 中文打分:短语优先于 token

retriever.py_score_record_zh完整场景/主角字符串 赋予更高权重(标题 +6、tags +4、正文 +2 起),再叠加 jieba token 命中——实测「神秘森林 + 勇敢小狮子」可稳定命中 ft_zh_0027(森林中的小河) 等条目,分数显著高于纯 fallback。

日志字段扩展为:

rag_retrieval lang=zh top_k=3 ids=[...] scores=[...] token_count=... phrase_count=... truncated_total=...

5.3 配置项(.env

RAG_ENABLED=true
RAG_CORPUS_FILENAME=corpus_zh.jsonl
RAG_CORPUS_LANGUAGE=auto
RAG_TOP_K=3
RAG_MAX_CONTEXT_CHARS=6000
RAG_MAX_EXCERPT_CHARS_PER_RECORD=1200
RAG_FALLBACK_WHEN_NO_MATCH=true

requirements.txt 增加 jieba;加载时会写一次 jieba 缓存,首请求略慢属正常现象。

5.4 与生成链路的衔接(不变部分)

注入点仍在 generation.py_reference_material_for_generationassert_text_safe(..., phase="rag_context") → LangChain reference_material 占位符。
切换 corpus.jsonl / corpus_zh.jsonl 无需改 prompt 模板结构;v6 两阶段编剧同样受益。


六、英文库扩充(同一阶段的并行工作)

中文库建设的同时,将 fairy_tales/ 全量重建进 corpus.jsonl

backend\.venv\Scripts\python.exe scripts\build_knowledge_corpus_en.py --from-id 1 --to-id 1730

当前仓库:1651 条有效记录(编号区间 1~1730 内 79 个缺号文件自动 skip)。
英文库与中文库 并存,通过 RAG_CORPUS_FILENAME 切换,英文 hint,中文 jieba


七、同期前端调整

在联调 RAG 与多页流程时,暴露出一个 纯 UX 问题:从首页进入「创作工坊 / 绘本库 / 阅读页」后,首页同款顶栏消失,只剩「← 返回首页」,与多页应用的信息架构不一致。

改动集中在 storybook-ui-static/auth.js,在 boot() 中最先执行 mountSiteNav()

  • 除登录页、分享页外,统一注入与首页一致的 header.topbar.site-nav(品牌、首页/开启创作/绘本库/阅读/家长中心、登录、立即开始);
  • currentPagePath() 高亮当前项;
  • 无顶栏的页面(如 create.html)在 <body> 开头 动态插入 header;
  • 随后仍走原有 mountAuthControls() 挂载用户信息与退出。
  function mountSiteNav() {
    if (!shouldMountSiteNav()) return;
    ...
    header.innerHTML = `
    <div class="brand">
      <a class="brand-link" href="${brandHref}">
        ...
      </a>
    </div>
    <nav class="nav">
      ${links}
      <a href="${staticHref("login.html")}" id="navLogin">登录</a>
    </nav>
    <a class="btn btn-primary" href="${staticHref("create.html")}" data-perm="jobs:create">立即开始</a>
  `;
    applyPermissions(header);
  }

styles.css.topbar.site-nav 增加 position: sticky,滚动时顶栏贴顶;阅读页保留下方 reader-head 作为 二级工具栏(改故事、页码、书架等),不替代全局导航。

设计取向: 静态多页站点没有引入构建工具,因此用 单文件 boot 注入 而非抽 React/Vue 布局组件——改动面小,与现有 data-perm 权限隐藏 机制兼容。


八、结语

第七篇在系列中的位置,是把 RAG 从 「英文参考 + 跨语言桥接」 推进到 「中文语料 + 中文检索」 的可切换双轨:

  • 数据侧:爬虫 → manifest → jsonl,字段与英文库对齐;
  • 检索侧:jieba + 短语,解释性优于纯 hint;
  • 工程侧:语料 gitignore、脚本入库、.env 切换文件名即可 A/B;
  • 体验侧:顶栏统一降低多页导航成本,与后端能力迭代正交。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐