基于 AI agent 的童话编剧与绘本生成器(七)中文 RAG:语料选取、爬取构建与检索升级
紧接系列第五、六篇(英文
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约束。
这条路径 工程上成立,但在答辩与产品叙事上有三个明显短板:
- 命中解释成本高:听众会问「中文场景怎么匹配英文正文?」——需要额外讲 hint 表与 fallback,不够直观。
- 语义对齐弱:「深海世界」「糖果城堡」等选项在 hint 表中覆盖有限,Top-K 常靠 SHA256 确定性兜底 凑数,相关性波动大。
- 与输出语言不一致:参考节选是英文,模型需在「结构启发」与「禁止直译」之间自行权衡,坏例里偶现英文词渗入选题。
因此本轮迭代目标不是推翻第六篇,而是 增加一条平行语料与检索分支:
| 维度 | 英文库 corpus.jsonl |
中文库 corpus_zh.jsonl |
|---|---|---|
| 来源 | 项目根 fairy_tales/{n}.txt |
storynook.cn 爬取 → fairy_tales_zh/ |
| 规模(当前) | 1651 条 | 120 条(可继续扩) |
| 检索 | 英文 token + 中文→英文 hint | jieba 分词 + 场景/主角短语 |
| 切换方式 | .env 中 RAG_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 主要步骤:
collect_story_urls:从 storynook 首页与各分类列表页收集/story/{id}链接(底层实现在fairy_tale_crawler_storynook.py)。- 逐篇
http_get+parse_story_page:提取标题与#storyContent正文。 - 质量过滤:正文少于 30 字则跳过。
- 落盘:
fairy_tales_zh/0001.txt… 连续编号正文;fairy_tales_zh/manifest.jsonl每行记录seq、title、url、txt_file、source_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
字段仍对齐 KnowledgeRecord:id、title、theme_tags、body、source、language(固定 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)并加「…」。 - ID:
ft_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— 把scene、protagonist、interest_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_zh→ zh; - 否则读首条 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_generation → assert_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; - 体验侧:顶栏统一降低多页导航成本,与后端能力迭代正交。
更多推荐
所有评论(0)