用 RAGFAISS 构建古诗词 AI 取名系统从向量检索到六层质量过滤的实战用 LLM 生成古诗词名字最大的问题不是好不好听而是它会编造出处。让大模型给你推荐一个出自《诗经》的名字它大概率会自信满满地给你一个假的诗句、假的篇名、假的释义。这不是模型能力问题而是幻觉的结构性根源——它没有引用真实语料的机制。所以我做了一个系统每个名字都必须来自真实可检索的古诗词原文通过 RAG检索增强生成架构强制约束。以下是完整的技术拆解。一、架构全景先看整体数据流用户输入 离线预处理 ───────── ────────── 姓氏性别出生时间 270万句古诗词原文 │ │ ▼ ▼ 八字计算 → 五行分析 繁→简转换(OpenCC) │ │ 句子切分(≥4字) │ 喜用元素 负面意象过滤 │ │ │ │ 映射意象关键词 │ │ │ │ ▼ ▼ ▼ 拼接搜索query Embedding向量化 │ │ │ ▼ │ ▼ Query向量化 │ FAISS索引(525MB) │ │ │ └────┬─────┘ │ ▼ │ Top-K相似检索 ◄───────────┘ │ ▼ 候选诗句池(50首) │ ▼ LLM Prompt生成(严格约束不许编造) │ ▼ 流式输出 → SSE → 前端逐个渲染 │ ▼ 六层质量过滤 ┌─────────────────────────────┐ │ 1. 烂大街字黑名单 │ │ 2. 谐音梗检测 │ │ 3. 生僻字过滤 │ │ 4. 音韵评分(pypinyin) │ │ 5. 五格数理(81数理吉凶) │ │ 6. 历史去重 │ └─────────────────────────────┘ │ ▼ 最终名字 诗词出处 完整解析技术选型一览环节选择理由向量化Zhipu Embedding-3中文语义理解能力强1024 维向量检索FAISS本地无外部依赖支持 mmap 加载LLMDeepSeek-V4中文能力强OpenAI 兼容 API元数据SQLite JSON 双备轻量WAL 模式支持并发读流式协议SSE比 WebSocket 简单单向推送够用二、向量检索 域知识注入2.1 基础检索古诗词语料经过预处理后繁→简转换、按句切分、负面过滤通过 Embedding-3 向量化存入 FAISS 索引。查询时将用户需求编码为自然语言queryf姓氏{surname}性别{gender}风格古典诗词意境优美query_vecembedding_client.embed(query)faiss.normalize_L2(query_vec)# L2归一化 → 等价余弦相似度_,indicesindex.search(query_vec,k50)candidates[metadata[i]foriinindices[0]]用自然语言而非关键词拼接是因为 Embedding-3 对语义的理解远比关键词匹配准确。意境优美的古诗词这样的描述能检索到包含山水、清风、明月等意象的诗句而关键词优美本身可能一句都匹配不上。2.2 八字五行 → 向量偏移这是整个系统最独特的设计。传统取名软件的五行补救是推荐固定偏旁缺水带氵缺木带木。但古诗词取名不能这样做——你不能限制用户只能用带氵的字。我的方案是把五行喜用转化为古诗意象关键词注入 FAISS 检索 query让向量检索自然偏移到含对应意象的诗句。出生时间 │ ▼ 四柱八字(年月日时 → 天干地支) │ ▼ 十神分析 → 日主强弱 → 旺衰法 │ ▼ 喜用神 → 意象关键词映射 │ ▼ 五行 → 意象关键词 │ ────────────────────── │ 水 → 雨 云 河 溪 澜 泉 润 │ 木 → 林 松 竹 兰 芳 蕊 │ 金 → 玉 银 锋 铭 锐 钰 │ 火 → 日 炎 晖 曜 煜 烨 │ 土 → 山 岳 峰 坤 垣 培 ▼ 注入检索query → 偏移到含对应意象的古诗核心代码WUXING_KEYWORDS{金:[玉,银,锋,铭,锐,钰,鑫],木:[林,松,竹,兰,芳,蕊,萱],水:[雨,云,河,溪,澜,泉,润],火:[日,炎,晖,曜,煜,烨,灿],土:[山,岳,峰,坤,垣,培,城],}defbuild_query(surname,gender,favored_elements):将五行喜用映射为古诗意象注入检索querybasef姓氏{surname}性别{gender}风格古典诗词imagery[]foreleminfavored_elements:imagery.extend(WUXING_KEYWORDS[elem][:3])ifimagery:returnf{base}意象{ .join(imagery)}returnbase为什么不直接用关键词过滤“包含’水’字的诗句”因为水的意象远不止字面包含水的诗句。落霞与孤鹜齐飞秋水共长天一色里水不是主角但整体意象是水。Embedding-3 能捕捉这种语义关联关键词过滤做不到。实测效果缺水的八字注入雨 云 河 溪后检索到的候选诗句中含水意象的比例从 15% 提升到 60% 以上。向量检索确实会往对应意象偏移。2.3 换一批时的多样性策略用户点换一批时不能简单地再跑一次同样的检索——大概率会得到相似的结果。解决方案是方向词轮转维护一组主题方向每次换一批切换一个让 FAISS 检索偏移到不同诗歌集群。DIRECTION_WORDS[山水田园,日月星辰,花鸟鱼虫,德行修养,志向抱负,清雅温婉]defrotate_query(base_query,used_directions):换一批时切换意象方向保证每轮结果不同available[dfordinDIRECTION_WORDSifdnotinused_directions]directionrandom.choice(available)used_directions.append(direction)returnf{base_query}主题方向{direction}山水田园会偏移到王维、孟浩然的诗句日月星辰则偏向李白、苏轼。每轮结果自然不同。三、Prompt 工程与流式解析3.1 约束 LLM 不编造出处Prompt 设计中最关键的约束【硬约束】只能使用上述提供的诗句原文绝不编造任何诗句、出处或释义。 如果候选诗句中没有合适的宁可少生成一个名字也不许杜撰。 【跨诗规则】名字的每个字必须来自不同诗句。 最多一个字来自同一首诗以保证文化关联的丰富性。 【格式要求】使用 NAME 分隔符逐个输出每个名字 每个名字包含名字、出处、诗句原文、释义、思考过程跨诗规则是容易忽略但很重要的设计——如果两个字来自同一首诗名字的文化厚度就局限在一首诗里。跨诗取字让每个名字有多重文化关联解释空间更大。但即使有严格的 prompt 约束LLM 仍然可能编造。所以还需要后处理校验把 LLM 引用的诗句在原文库里做精确匹配不匹配的直接丢弃触发重新生成。两道防线确保零编造。3.2 流式解析LLM 流式输出最大的工程挑战是输出中途不是合法的 JSON。假设 LLM 要输出{names: [{name: 清涵}, ...]}在流式传输过程中你收到的前几个 chunk 可能是{names: [{name: 清——这不是合法 JSON无法解析。常见方案对比方案优点缺点等完整输出再解析实现简单失去流式意义延迟高JSON 流式解析器能增量解析复杂、脆弱、LLM 输出风格微变就挂自定义分隔符简单、通用、零依赖需要在 prompt 里约定格式我选了自定义分隔符方案。Prompt 要求 LLM 用NAME作为名字之间的分隔标记解析端只需按这个标记切分缓冲区asyncdefstream_names(llm_stream):从LLM流中增量解析每个名字块bufferasyncforchunkinllm_stream:bufferchunk# 每发现一个NAME标记就切出一个完整的名字块whileNAMEinbuffer:block,bufferbuffer.split(NAME,1)ifblock.strip():yieldparse_name_block(block)# 最后一个块没有后续NAME标记单独处理ifbuffer.strip():yieldparse_name_block(buffer)defparse_name_block(text):从名字块中提取结构化数据result{}forlineintext.strip().split(\n):ifinline:key,valueline.split(,1)result[key.strip()]value.strip()returnresult前端通过 SSEServer-Sent Events接收每收到一个完整的名字块就立即渲染用户看到的效果是名字一个接一个地跳出来——比等 10 秒后一次性展示 5 个名字的体验好得多。SSE 在 Nginx 后面有个经典坑Nginx 默认会 buffer 响应导致前端收不到实时推送。需要两处配置# nginx.conf location /api/ { proxy_buffering off; # 关闭proxy层buffering proxy_set_header X-Accel-Buffering no; # 关闭X-Accel层buffering proxy_pass http://backend; }同时后端响应头也要加X-Accel-Buffering: no双保险。四、六层质量过滤LLM 生成的名字不能直接展示需要经过多层质量把关。每一层解决一个具体问题第 1 层烂大街字黑名单BANNED_CHARSset(梓涵轩诺萱妍泽宇欣怡浩瑞昊辰)为什么要过滤这些字2023 年新生儿重名统计“梓涵连续多年霸榜。用了这些字再好的诗词出处也白搭——别人一看就知道是 AI 起的模板名”。宁可少生成也不能出烂大街的字。第 2 层谐音梗检测姓名组合的谐音问题比如杨伟、“范统”、“杜子腾”。维护一个谐音黑名单每个新名字都做组合检测。这类问题一旦漏掉用户体验直接归零。第 3 层生僻字过滤拒绝 CJK 扩展区0x9FFF 以上的字。名字是用来叫的别人叫不出来、打不出来再有文化也白搭。defhas_rare_char(name):returnany(ord(c)0x9FFFforcinname)第 4 层音韵评分用 pypinyin 拆解声母韵母和声调对名字做可读性评分。主要检测同声母连读如赵兆zh-zh 连读拗口同韵母连读如林音in-in 听起来像一个字三声连读两个三声字连读需要变调处理不好会奇怪整体韵律声调起伏太平或太陡都不好评分低于 60 分的直接过滤。第 5 层五格数理这是最不技术的一层但对产品转化至关重要。五格数理天格、人格、地格、外格、总格是中国传统取名的参考体系用姓名笔画数对应 81 数理的吉凶。开发者可能觉得这是迷信但准爸妈们在乎——他们会拿着你给的名字去起名网验证五格如果人格是大凶他们不会用你的产品。所以系统用康熙字典笔画数自动计算五格人格落大凶的组合直接排除。这层不是给开发者看的是给用户信心的。defwuge_is_bad(surname,name):检查五格数理是否为大凶strokesget_kangxi_strokes(surnamename)ren_gridstrokes[0]strokes[1]# 人格 姓末字名首字笔画returnren_gridinBAD_NUMBERS# 81数理中的大凶数第 6 层历史去重用户点换一批时新生成的名字不能和之前展示过的重复。session 中维护已展示名字集合每层过滤后做最终去重。六层全通过才算一个合格的名字。实测下来LLM 每轮生成 5 个名字平均有 1-2 个会被过滤掉所以系统会多生成几个作为缓冲。五、踩过的坑坑 1让 LLM 不编造有多难最初 prompt 只写了请使用提供的诗句编造率约 30%。加了绝不编造任何诗句或出处后降到 10%。加上后处理的精确匹配校验才做到零编造。经验prompt 约束是第一道防线后处理校验是兜底。两道都不能少。只靠 prompt 就像只靠自律——大部分时候有用但总会翻车。坑 2FAISS 索引的冷启动525MB 的索引文件首次加载要几秒。改用 mmap 后 RSS 占用从几个 GB 降到几十 MB但第一次查询仍然有冷启动延迟需要把向量数据从磁盘读入页缓存。解决方案服务启动时做一次空查询预热把常用页拉入缓存。# 服务启动后预热FAISS索引warmup_vecnp.zeros((1,dim),dtypenp.float32)index.search(warmup_vec,k1)# 触发页缓存加载坑 3Embedding 查询缓存的必要性相同输入同姓氏同性别同风格的查询会反复出现。为 query embedding 加了 LRU 缓存maxsize128命中率出乎意料地高——同一个 session 内的换一批操作只有方向词在变基础 query 的 embedding 可以复用。六、实际效果以上技术方案已经落地为一个完整产品——文间拾字。展示几张实际运行效果输入界面用户填写姓氏、性别、出生时间选择风格偏好生成过程通过 SSE 流式推送名字逐个跳出来加载期间展示古诗词轮播结果展示每个名字附带完整信息——诗词出处朝代作者原句、五行标注、音韵分析、取名思路每个名字卡片的信息密度很高诗词原文保证有据可查五行标注让用户知道补了什么音韵分析让用户知道念起来好不好听取名思路让用户理解为什么选这两个字。七、写在最后这个系统的核心思路是用 RAG 架构约束 LLM 的幻觉用向量检索保证诗词出处的真实性用域知识八字五行增强检索的相关性用多层过滤保证名字的质量。技术栈并不复杂——FAISS、Embedding、LLM、SQLite都是成熟的工具。真正的难点在于把古诗词取名这个垂直领域的业务逻辑五行、音韵、五格和技术架构向量检索、流式输出、质量过滤有机地融合在一起。如果你也在做 RAG 相关的项目欢迎在评论区交流。特别是如何约束 LLM 不编造引用源这个问题很想听听大家的方案。体验一下上面拆解的所有技术都已经跑在这个产品里了微信搜索小程序「文间拾字」即可体验。收录《诗经》《楚辞》《全唐诗》《宋词》等经典所有名字 100% 有诗词原文出处零编造。如果你身边有正在给宝宝起名的朋友可以把这个工具分享给他们——比起花钱找大师不如让 AI 翻遍 270 万句古诗词给名字找一个真正有据可查的出处。评论区互动评论区留下「姓氏 性别」如张 男孩我用这个系统帮你跑一版看看效果如何