用Python和规则引擎实现古诗词生成器:91行代码的创意实践

用Python和规则引擎实现古诗词生成器:91行代码的创意实践 前阵子参加了一个“91行代码创意赛”规则特别简单用不超过91行代码实现一个有意思的项目语言不限。当时我翻了不少往届作品有做贪吃蛇的有做网页爬虫的还有做聊天机器人的。我琢磨着能不能做一个跟传统文化沾边、又带点“智能感”的东西最后定了古诗词生成器。很多人一听“诗词生成”就觉得得靠大模型但这次赛制的亮点恰恰在于极简代码要求你在这么少的行数里做出一个能跑、有输出、有创意的作品。我最后用Python写了一个基于规则和随机采样的“智能诗词生成器”输入主题词它能在几毫秒内生成一首押韵的五言或七言绝句虽然谈不上什么深度学习但作为创意赛作品已经有足够的趣味性和可玩性。这个项目适合刚接触Python或者对文本生成感兴趣的开发者参考尤其是那些想在代码比赛中快速出作品、又不想堆复杂依赖的朋友。这篇文章会把我的设计思路、关键代码逻辑、踩过的坑全部整理出来内容包括用韵脚表替代语音模型、用词库和模板实现“智能”、如何在行数限制内做格律校验以及我用过的排查技巧。如果你也想写一个类似的生成器可以直接照着思路抄作业。1. 项目初衷与整体设计思路1.1 为什么选诗词生成器诗词生成算是文本生成领域里一个很有意思的切入口。它有一个特别好的优势形式高度固定五言就是五个字七言就是七个字绝句就是四句。这种强结构意味着不需要复杂的自然语言生成模型只要把字词填进合适的框架里读起来就会自带一点“诗感”。再加上押韵和平仄这两条约束反而让生成器有了明确的目标代码写起来不会像无头苍蝇一样乱撞。从创意赛的角度看诗词生成器也特别容易吸引观众。你现场跑一下输入“山水”或者“思乡”马上出来一首押韵的诗效果非常直观。相比之下做几个普通的数据处理Demo远没有这种“生成类工具”令人眼前一亮。我在决定选题时还考虑过“对联生成器”后来觉得对联的格律和词性要求更严格91行内很难做像样于是退一步选了绝句生成。绝句允许一定程度的语义跳跃只要意象风格统一句子之间不太讲究严密的逻辑这给随机生成留下了很大的空间。确定方向后我对“智能”这两个字做了重新定义。在这个项目中智能不是说机器真的理解诗意而是指它能根据用户输入的主题词从词库中选出相关意象再按照押韵和平仄规则组装成诗。用户看到的是“我说了一个词它真的围绕这个词写了四句诗”这就够了。这种规则加随机的方案在91行约束下算是性价比最高的选择。1.2 91行代码的规划与取舍开始写之前我先在草稿纸上把功能拆成几块数据、生成逻辑、命令行交互。数据就是词库和韵脚表生成逻辑负责把词拼成句子命令行交互负责接收参数和打印结果。这个拆分看起来简单但恰恰是它决定了后续能塞下多少东西。如果一上来就想做GUI、做用户学习、做语料训练那91行肯定不够用必须提前砍掉。最占行数的是数据部分。早期版本里我试图把词库放在外部文本文件里每次运行时用open()读取。这样的好处是代码行数少但运行时依赖一个词库文件提交作品时要多打包一个文件而且文件读写的中文编码问题会让人抓狂。后来为了省事我直接把词库写成了Python字典放在代码文件里。这样虽然占了几十行但程序自包含任何机器上只要装了Python就能跑省去了各种环境问题。对于一个小型创意赛来说自包含往往比优雅更重要。另一个果断放弃的东西是平仄全量字典。如果想把每一个常用汉字都标注平仄代码量会爆炸。我采取的办法是只给词库里出现的字做平仄标注说白了就是“用到什么标什么”。这样词库本身就包含了平仄信息生成时直接读取不用临时计算。这个思路在代码比赛里很常见用空间换时间不追求全量数据只保证已有素材的可用性。1.3 技术选型Python 还是传统方案选Python几乎是必然的。它语法简洁内置了random、sys、collections这些常用模块写起来非常紧凑。而且Python3默认Unicode对中文字符的处理比C语言省心太多不会动不动就蹦出编码错误。C语言当然能写但光实现一个像样的字符串分割和哈希表就够呛91行根本不够用Java就更不用说了一个类定义加public static void main就得占好几行。创意赛不限制语言自然要选最短路径。我用Python写的时候还特别注意“一行能做完的事绝不写两行”。比如判断一个词是否押韵通常需要取最后一个字再查韵脚字典我用line[-1] in RHYMES[rhyme]一行搞定。列表推导式在这种场景下也是神器代码紧凑逻辑也很明确。不过后来我发现过度压缩也不一定是好事这个在后面的“参赛提交”章节里我会详细吐槽。2. 核心实现诗词生成引擎拆解2.1 押韵引擎用韵母表代替语音模型押韵是整个生成器最重要的部分也是最容易被新手忽视的地方。很多初版生成的“诗”读起来别扭问题就出在句尾字不押韵。要想在91行内解决押韵最直接的方法是建一张“韵脚表”。它的结构很简单一个字典键是韵母值是一组常用平声字。RHYMES { ang: [光, 香, 霜, 乡, 长, 芳], an: [山, 寒, 残, 烟, 船, 湾], ao: [高, 遥, 桥, 涛, 箫, 潮], a: [花, 霞, 家, 纱, 涯, 斜], }看到这里有人会问为什么选这几个韵我的经验是优先选平声字多、常用字多、意象范围广的韵。ang、an、ao这三个韵在现代普通话里属于“开口音”唱出来响亮古人写诗也特别喜欢用。而一些窄韵比如ei、ou虽然也有名句但常用字太少随机生成时翻来覆去就是那几个字读几遍就腻了。韵脚表的价值在于“押韵”不再是运行时去查拼音库再用算法计算韵母而是提前把字按韵分好用字典倒排索引用的时候直接抽。还有一个细节古诗词里的很多字和现代读音并不一致。比如“斜”在平水韵里属麻韵和“花”“家”可以通押但现代人读“xié”如果硬要按普通话韵母分组它就该归到ie韵。我在这个项目里选择了按现代普通话韵母来押韵而不是严格遵循古韵。原因很简单作品最终是给现代读者看的普通听众不会去翻平水韵只要听感舒服就算押韵成功。如果你将来想做更考究的版本可以再引入平水韵表但91行内没有必要。2.2 词库组织与随机采样有了韵脚表下一步是准备句子素材。我没有把整句诗作为生成单元而是把词打散成“意象元素”按主题分类放进词库。比如“山水”主题下有“青山、碧水、白云、孤舟、松风、溪流”“思乡”主题下有“故乡、明月、归雁、离愁、客船、远山”。每个词的长度尽量控制在两个字或三个字这样方便拼接成五言和七言。POOLS { 山水: [青山, 碧水, 白云, 孤舟, 松风, 溪流, 月, 花], 思乡: [故乡, 明月, 归雁, 离愁, 客船, 远山, 梦, 秋], 默认: [春风, 落日, 长江, 古道, 飞鸟, 人间, 雨, 烟], }为什么不用整句模板因为整句模板的生成结果太有限比如你写死“孤舟蓑笠翁”那不管随机多少次都只是在换前后句核心还是那一句。而“词袋”加“句式模板”的组合可以从几十个词里随机搭配出几百种句子生成结果的多样性要强得多。为了进一步增加变化我还在每一种主题里混入一些通用词比如“春”“秋”“风”“云”“愁”“梦”这类万物皆可搭配的意象词。它们能在一定程度上冲淡主题词重复带来的单调感。随机采样主要用random.choice非常简单。但直接随机拼接容易出现逻辑不通的句子比如“孤舟长江月”这种词性与意象还能接受但有时候会拼出“白云飞鸟愁”感觉像三个名词硬凑在一起。为了解决这个问题我在句式模板上做了手脚。五言诗我常用的模板是“2字名词 2字名词 1字形容词”或者“2字名词 2字动词 1字名词”七言则在五言基础上多加一个“2字状语”或“3字短语”。模板把词性顺序固定下来即使随机生成也不会跑出太离谱的组合。2.3 格律校验五言七言与平仄格律是诗词的骨架但91行内做完整格律校验不现实所以我的方案是“近似校验”。绝句最常见的押韵方式是第二句和第四句押韵第一句可押可不押第三句通常不押韵而且末字常用仄声。我依照这个规则确保第一句、第二句、第四句的末尾字从韵脚表里取第三句末尾字则从仄声字池里取。平仄方面我提前给词库中的每个字标注了平仄。按照普通话发音一声二声为平三声四声为仄。比如“青”是平声“水”是仄声。在拼接每一句时我并不是生成完再检查平仄而是在取词阶段就做约束。比如五言句式“平平仄仄平”那么第一个字必须从平声开头第二字也必须平第三四字要仄最后是平。如果random.choice抽到的词不符合当前槽位的平仄就重新抽直到抽到满足条件的词为止。一开始我用的是“生成后校验”策略先随机拼一句再算平仄匹配度不匹配就整句重来。这样做的缺点是失败率太高。五言句有5个位置每个位置平仄匹配的概率大约一半整体不匹配的概率非常高经常要重试几十次才能得到一句合格诗。后来我改成“约束采样”按平仄槽位直接过滤可选的词生成效率和成功率都大幅提升。这个思路虽然是针对诗词生成的但放到很多领域都有启发如果你的输出需要满足一系列条件最好在设计阶段就把条件嵌进采样过程而不是事后反复试错。2.4 主循环与交互命令行交互是整个项目最外面的壳。我最终设计成两种用法一种是不带参数直接运行程序会提示你输入主题和韵脚另一种是通过sys.argv读取参数比如执行python poem.py --theme shanshui --rhyme ang适合批量展示。为了减少行数我用sys.argv自己解析参数没有引入argparse。主循环其实非常简单def main(): theme sys.argv[1] if len(sys.argv) 1 else 默认 rhyme sys.argv[2] if len(sys.argv) 2 else ang poem generate_poem(theme, rhyme) print(poem)每次运行生成一首诗如果用户想多生成几首可以加一个循环。但创意赛现场演示时一般只展示一首最有代表性的作品所以默认生成一首就够了。为了让结果更“惊艳”我还在内部实现了一个“生成十首选最优”的隐藏逻辑这在我后面讲调优时再展开。整个交互设计的原则就是把用户的输入降到最低一键出结果让人第一眼就感受到“哇居然能生成诗”。3. 实操过程与核心环节实现3.1 数据准备从爬虫到内置列表这个项目真正耗时的环节不是写代码而是整理数据。我第一次尝试做诗词生成器时第一反应是去网上下载《唐诗三百首》的文本然后用程序切分句子、统计字频甚至想过训练一个简单的马尔可夫链。但在这个创意赛里如果语料放在外部文件读写文件至少要多花5到10行代码如果把语料直接嵌进代码一个《唐诗三百首》的文本能占几百行直接超限。所以我把数据策略改成了“手工精选”。我从两三百首脍炙人口的古诗里抽取高频意象词再按照主题分类。比如“月亮”这个词在思乡诗里出现频率极高我就把它放进“思乡”主题“江水”“孤舟”常出现在羁旅题材我就放进“山水”或“远游”主题。经过几次迭代最终每个主题下保持十到二十个词全表加起来也就三四十个词数据量非常小但覆盖面足够应付随机生成。整理词库时我特别注意三个问题一是词的通用性避免出现太生僻的典故词二是词的长度尽量统一为两个字方便五言和七言拼接三是词的平仄宁可使用意象不那么惊艳但平仄明确的词也不要为了辞藻牺牲格律。很多初学者做生成器容易把词库弄得又大又杂结果生成的句子平仄混乱、押韵失败其实问题不在代码逻辑而在词库的“干净程度”。词库干净了后面的规则才能稳定生效。3.2 关键代码段实现与讲解这里贴一段我从最终版本里摘出来的核心函数职责是“根据主题和押韵生成一行五言诗”。虽然为了阅读方便我做了些简化但思路和最终版是一致的。def gen_line(pool, rhyme, pattern): line [] for length in pattern: candidates [w for w in pool if len(w) length] if not candidates: candidates pool line.append(random.choice(candidates)) line[-1] random.choice(RHYMES[rhyme]) return .join(line)这段代码接收三个参数词库、韵脚、句式模板。pattern是一个长度列表比如[2, 2, 1]表示“两个两字词加一个一字词”。循环里按照每个槽位的长度去词库中筛选候选词然后随机选一个填进去。最后强制把末字替换成韵脚字这样就能保证句尾押韵。这里有个小技巧把末字替换掉而不是在选词时就限制末字。因为一首绝句里每一句的末尾字并不一定都来自主题词库可能是单独的韵脚字。比如“青山遮不住毕竟东流去”如果强行要求“不住”里的“住”也变成韵脚字句子意思就会被破坏。把韵脚字单独管理会让生成更灵活。当然替换完末字后整句的平仄可能被破坏所以我还会加一轮平仄过滤如果末字导致整句平仄不符合模板就重新抽一个韵脚字直到匹配为止。3.3 调优让诗词更像“诗”第一版生成器跑出来说实话很灾难。举一个例子它生成过“青山碧水白云孤舟月”句子本身全是意象却不像诗更像一个景点列表。问题出在两点一是动词太少名词堆太多二是缺少虚词和连接语句子没有“起承转合”。调优的第一步是增加动词和虚词。我在词库里加入了“入、照、悬、落、生、摇、带、随”这类单字动词也加入了“自、独、空、何、谁、又”这类虚词。让句式从“名词名词名词”变成“名词动词名词”。比如“白云入孤舟”立刻就有了一丝动态感。第二步是引入“生成再筛选”的机制。我先让它生成十句候选然后用一个简单评分函数打分分数最高的留下。评分项包括句子中是否包含主题词、末字是否押韵、平仄匹配度、是否包含动词或虚词。每项都能用一两行代码实现折算成评分权重后最后挑出来的句子明显“诗味”更浓。这个机制在很多生成器里都能见到本质是“从大量随机结果中选择一个满足约束的最优解”虽然笨但非常有效。3.4 打包与参赛提交代码写完之后我做的第一件事是用wc -l poem.py统计总行数。第一次统计出来是107行超出指标不得不开始精简。精简动作主要分三步去掉多余空行和注释把import random和import sys合并到一行把一些重复的列表推导式提取成公共函数。但我也遇到一个坑压缩代码时把变量名改成了单字母函数名也改成了f1、f2之类的结果第二天自己都看不懂了调试起来非常痛苦。后来我意识到91行限制并不是要你把代码写成“天书”而是在保证可读性的前提下尽量精简。我的最终版保留了清晰的函数命名和少量关键注释只去掉空行和冗余逻辑。提交时我在同一个压缩包里放了一个README说明项目思路和用法README不计入行数但起到了很重要的说明作用。评审老师不会因为代码行数刚好91就把你当神仙他们更看重思路是否清晰、效果是否打动人心。4. 常见问题与排查技巧实录4.1 中文编码问题中文编码是所有中文Python项目绕不开的坑。我在自己的电脑上运行一切正常一放到旧版Windows命令行里中文全部变成乱码输出的诗像外星文。原因很简单Windows控制台默认代码页是GBK而Python3源码保存为UTF-8两边不对付。解决方法是在脚本开头加上import sys和sys.stdout.reconfigure(encodingutf-8)强制标准输出使用UTF-8。如果运行环境特别老还可以在命令行执行chcp 65001切换代码页。另外源代码文件最好在编辑器里设置为“UTF-8无BOM”格式避免在文件开头出现不可见字符。这些小细节在你本地开发时察觉不到等提交到比赛服务器演示时就容易翻车。4.2 随机结果不稳定生成器每次输出的诗都不一样这是特性但调试的时候很讨厌。你刚发现某一个韵脚字导致平仄错误想复现问题结果下一次运行随机到的完全是另一批字压根没法检查。我的办法是给程序加一个可选的随机种子参数调试时固定使用random.seed(42)这样每次生成结果完全一致定位问题特别方便。展示作品时随机种子还能帮大忙。你可以先在家里用某个种子生成一首特别好的诗记住种子值现场演示时用同一个种子跑效果稳定不会出现随机到一句烂诗的尴尬。等观众看腻了再取消种子让他们感受“每次生成都不一样”的惊喜。这算是一个很实用的参赛技巧。4.3 押韵不准的排查写了几代版本后我发现押韵不准的问题往往不在生成逻辑而在韵脚表本身。有些字是多音字比如“长”既读cháng又读zhǎng我把它放在ang韵脚表里在普通话中确实押韵但如果程序内部按拼音标注就可能出现读取到另一个读音的情况。解决办法是每个韵脚表的字只保留一个常用读音并且只用于该韵。还有一个容易踩的坑近似韵的合并。现代普通话里in和ing、en和eng在某些方言里分不清但在诗词创作里如果混用读起来会有一点不舒服。我自己的选择是在创意赛里可以适当合并ang和iang、an和ian因为发音接近听感不错但尽量不要合并in和ing否则容易让人听出“别扭”。我的排查手段也很简单单独写一个测试脚本把每个韵脚表里的字和它对应的韵母打印出来用眼睛扫一遍基本就能发现错误。4.4 性能与随机尝试次数在加入平仄约束采样后生成效率一度变得很低。原因是词库不够大的时候能同时满足长度、平仄和押韵条件的词可能只有一个甚至没有程序就陷入无限重试。我最开始没意识到还在循环里写了while True结果直接卡死。后来优化成“如果某个槽位没有候选词就放宽条件从更大范围的通用词库中取词”。另一个性能优化是给词库建索引。提前按“首字平仄”和“末字平仄”分类比如ping_start [w for w in pool if 平仄[w[0]] 1]采样时直接从这个分类里选不用每次临时遍历整个词库。批量生成几百首诗的时候这个优化能明显减少延迟。还有一个很细节的优化用.join(line)拼接字符串而不是在循环里不断line word。字符串在Python里是不可变对象频繁拼接会产生大量临时对象拖慢速度。虽然这里生成量不大但养成了好习惯后面扩展到大规模文本生成时就不会踩坑。5. 一些个人体会与后续扩展5.1 从91行到更多可能比赛结束之后我并没有把代码丢在一边。在后续的版本里我逐步把它扩展到了几百行加入严格平水韵字表、支持七言绝对、加入藏头诗模式、增加更多主题词库还尝试用简单的马尔可夫链从真正的古诗中抽取转移概率让句子之间的衔接更自然。每一步扩展都让生成器变得更“聪明”但我也越来越怀念91行版本的那种纯粹与克制。如果你想继续玩下去有几个方向值得试试。一个是“藏头诗生成”在首字位置固定用户想要的词其余位置继续用随机和规则填充另一个是“词牌名生成”比如模仿《清平乐》《浣溪沙》的字数和平仄结构还有一个是“诗词接龙”随机从前一句的末字出发找到同韵字开头的下一句。这些扩展都建立在这次的基础之上代码逻辑差别不大主要是数据和规则的变化。5.2 给参赛者的实用建议如果你也想参加类似的行数限制创意赛我有几句非常实在的建议。第一先把功能砍到“不可再砍”的地步先跑出一个最小可用的版本再考虑加花活。第二数据准备工作要提前做很多项目表面上卡在代码逻辑实际卡在素材质量上。第三提交前一定要换一台干净环境跑一遍不要只在自己电脑上测试编码问题、依赖缺失、文件路径问题都会在陌生环境里暴露出来。最后再分享一个我印象很深的技巧在命令行里跑python poem.py --theme 山水 --rhyme ang之后如果随机到一句特别好的句子赶紧用--seed 42固定下来再微调词库继续生成。这个工作流让我在比赛展示时随时都能复现最好看的作品。后来我又把韵脚表和词库拆成了两个外部JSON文件虽然需要处理文件读写但主代码反而更精简了。不过这些都是后话。参加完这次91行代码创意赛我最深的体会是当限制足够大好的设计比堆功能重要得多。