用 RAG+FAISS 构建古诗词 AI 取名系统:从向量检索到六层质量过滤的实战

用 RAG+FAISS 构建古诗词 AI 取名系统:从向量检索到六层质量过滤的实战

用 LLM 生成古诗词名字,最大的问题不是"好不好听",而是它会编造出处

让大模型给你推荐一个出自《诗经》的名字,它大概率会自信满满地给你一个假的诗句、假的篇名、假的释义。这不是模型能力问题,而是幻觉的结构性根源------它没有引用真实语料的机制。

所以我做了一个系统:每个名字都必须来自真实可检索的古诗词原文,通过 RAG(检索增强生成)架构强制约束。以下是完整的技术拆解。

一、架构全景

先看整体数据流:

scss 复制代码
用户输入                      离线预处理
─────────                    ──────────
姓氏+性别+出生时间      38万句古诗词原文
     │                        │
     ▼                        ▼
 八字计算 → 五行分析      繁→简转换(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 加载
LLM DeepSeek-V4 中文能力强,OpenAI 兼容 API
元数据 SQLite + JSON 双备 轻量,WAL 模式支持并发读
流式协议 SSE 比 WebSocket 简单,单向推送够用

二、向量检索 + 域知识注入

2.1 基础检索

古诗词语料经过预处理后(繁→简转换、按句切分、负面过滤),通过 Embedding-3 向量化存入 FAISS 索引。查询时,将用户需求编码为自然语言:

python 复制代码
query = f"姓氏:{surname},性别:{gender},风格:古典诗词,意境优美"

query_vec = embedding_client.embed(query)
faiss.normalize_L2(query_vec)  # L2归一化 → 等价余弦相似度
_, indices = index.search(query_vec, k=50)

candidates = [metadata[i] for i in indices[0]]

用自然语言而非关键词拼接,是因为 Embedding-3 对语义的理解远比关键词匹配准确。"意境优美的古诗词"这样的描述,能检索到包含山水、清风、明月等意象的诗句,而关键词"优美"本身可能一句都匹配不上。

2.2 八字五行 → 向量偏移

这是整个系统最独特的设计。传统取名软件的五行补救,是推荐固定偏旁(缺水带"氵",缺木带"木")。但古诗词取名不能这样做------你不能限制用户只能用带"氵"的字。

我的方案是:把五行喜用转化为古诗意象关键词,注入 FAISS 检索 query,让向量检索自然偏移到含对应意象的诗句

erlang 复制代码
出生时间
  │
  ▼
四柱八字(年月日时 → 天干地支)
  │
  ▼
十神分析 → 日主强弱 → 旺衰法
  │
  ▼
喜用神 → 意象关键词映射
  │
  ▼          五行 → 意象关键词
  │         ──────────────────────
  │          水 → "雨 云 河 溪 澜 泉 润"
  │          木 → "林 松 竹 兰 芳 蕊"
  │          金 → "玉 银 锋 铭 锐 钰"
  │          火 → "日 炎 晖 曜 煜 烨"
  │          土 → "山 岳 峰 坤 垣 培"
  ▼
注入检索query → 偏移到含对应意象的古诗

核心代码:

python 复制代码
WUXING_KEYWORDS = {
    "金": ["玉", "银", "锋", "铭", "锐", "钰", "鑫"],
    "木": ["林", "松", "竹", "兰", "芳", "蕊", "萱"],
    "水": ["雨", "云", "河", "溪", "澜", "泉", "润"],
    "火": ["日", "炎", "晖", "曜", "煜", "烨", "灿"],
    "土": ["山", "岳", "峰", "坤", "垣", "培", "城"],
}

def build_query(surname, gender, favored_elements):
    """将五行喜用映射为古诗意象,注入检索query"""
    base = f"姓氏:{surname},性别:{gender},风格:古典诗词"

    imagery = []
    for elem in favored_elements:
        imagery.extend(WUXING_KEYWORDS[elem][:3])

    if imagery:
        return f"{base},意象:{' '.join(imagery)}"
    return base

为什么不直接用关键词过滤("包含'水'字的诗句")?因为"水"的意象远不止字面包含"水"的诗句。"落霞与孤鹜齐飞,秋水共长天一色"里"水"不是主角,但整体意象是水。Embedding-3 能捕捉这种语义关联,关键词过滤做不到。

实测效果:缺水的八字,注入"雨 云 河 溪"后,检索到的候选诗句中含水意象的比例从 15% 提升到 60% 以上。向量检索确实会往对应意象偏移。

2.3 换一批时的多样性策略

用户点"换一批"时,不能简单地再跑一次同样的检索------大概率会得到相似的结果。

解决方案是方向词轮转:维护一组主题方向,每次换一批切换一个,让 FAISS 检索偏移到不同诗歌集群。

python 复制代码
DIRECTION_WORDS = [
    "山水田园", "日月星辰", "花鸟鱼虫",
    "德行修养", "志向抱负", "清雅温婉"
]

def rotate_query(base_query, used_directions):
    """换一批时切换意象方向,保证每轮结果不同"""
    available = [d for d in DIRECTION_WORDS if d not in used_directions]
    direction = random.choice(available)
    used_directions.append(direction)
    return f"{base_query},主题方向:{direction}"

"山水田园"会偏移到王维、孟浩然的诗句;"日月星辰"则偏向李白、苏轼。每轮结果自然不同。

三、Prompt 工程与流式解析

3.1 约束 LLM 不编造出处

Prompt 设计中最关键的约束:

markdown 复制代码
【硬约束】只能使用上述提供的诗句原文,绝不编造任何诗句、出处或释义。
         如果候选诗句中没有合适的,宁可少生成一个名字,也不许杜撰。

【跨诗规则】名字的每个字必须来自不同诗句。
            最多一个字来自同一首诗,以保证文化关联的丰富性。

【格式要求】使用 <<NAME>> 分隔符逐个输出每个名字,
            每个名字包含:名字、出处、诗句原文、释义、思考过程

跨诗规则是容易忽略但很重要的设计------如果两个字来自同一首诗,名字的文化厚度就局限在一首诗里。跨诗取字让每个名字有多重文化关联,解释空间更大。

但即使有严格的 prompt 约束,LLM 仍然可能编造。所以还需要后处理校验:把 LLM 引用的诗句在原文库里做精确匹配,不匹配的直接丢弃,触发重新生成。两道防线确保零编造。

3.2 <> 流式解析

LLM 流式输出最大的工程挑战是:输出中途不是合法的 JSON

假设 LLM 要输出 {"names": [{"name": "清涵"}, ...]},在流式传输过程中,你收到的前几个 chunk 可能是 {"names": [{"name": "清------这不是合法 JSON,无法解析。

常见方案对比:

方案 优点 缺点
等完整输出再解析 实现简单 失去流式意义,延迟高
JSON 流式解析器 能增量解析 复杂、脆弱、LLM 输出风格微变就挂
自定义分隔符 简单、通用、零依赖 需要在 prompt 里约定格式

我选了自定义分隔符方案。Prompt 要求 LLM 用 <<NAME>> 作为名字之间的分隔标记,解析端只需按这个标记切分缓冲区:

python 复制代码
async def stream_names(llm_stream):
    """从LLM流中增量解析每个名字块"""
    buffer = ""
    async for chunk in llm_stream:
        buffer += chunk
        # 每发现一个<<NAME>>标记,就切出一个完整的名字块
        while "<<NAME>>" in buffer:
            block, buffer = buffer.split("<<NAME>>", 1)
            if block.strip():
                yield parse_name_block(block)

    # 最后一个块没有后续<<NAME>>标记,单独处理
    if buffer.strip():
        yield parse_name_block(buffer)


def parse_name_block(text):
    """从名字块中提取结构化数据"""
    result = {}
    for line in text.strip().split("\n"):
        if ":" in line:
            key, value = line.split(":", 1)
            result[key.strip()] = value.strip()
    return result

前端通过 SSE(Server-Sent Events)接收,每收到一个完整的名字块就立即渲染,用户看到的效果是名字一个接一个地"跳出来"------比等 10 秒后一次性展示 5 个名字的体验好得多。

SSE 在 Nginx 后面有个经典坑:Nginx 默认会 buffer 响应,导致前端收不到实时推送。需要两处配置:

nginx 复制代码
# 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 层:烂大街字黑名单

python 复制代码
BANNED_CHARS = set("梓涵轩诺萱妍泽宇欣怡浩瑞昊辰")

为什么要过滤这些字?2023 年新生儿重名统计,"梓涵"连续多年霸榜。用了这些字,再好的诗词出处也白搭------别人一看就知道是 AI 起的"模板名"。宁可少生成,也不能出烂大街的字。

第 2 层:谐音梗检测

姓+名组合的谐音问题,比如"杨伟"、"范统"、"杜子腾"。维护一个谐音黑名单,每个新名字都做组合检测。这类问题一旦漏掉,用户体验直接归零。

第 3 层:生僻字过滤

拒绝 CJK 扩展区(0x9FFF 以上)的字。名字是用来叫的,别人叫不出来、打不出来,再有文化也白搭。

python 复制代码
def has_rare_char(name):
    return any(ord(c) > 0x9FFF for c in name)

第 4 层:音韵评分

用 pypinyin 拆解声母韵母和声调,对名字做可读性评分。主要检测:

  • 同声母连读:如"赵兆",zh-zh 连读拗口
  • 同韵母连读:如"林音",in-in 听起来像一个字
  • 三声连读:两个三声字连读需要变调,处理不好会奇怪
  • 整体韵律:声调起伏太平或太陡都不好

评分低于 60 分的直接过滤。

第 5 层:五格数理

这是最"不技术"的一层,但对产品转化至关重要。

五格数理(天格、人格、地格、外格、总格)是中国传统取名的参考体系,用姓名笔画数对应 81 数理的吉凶。开发者可能觉得这是迷信,但准爸妈们在乎------他们会拿着你给的名字去"起名网"验证五格,如果人格是"大凶",他们不会用你的产品。

所以系统用康熙字典笔画数自动计算五格,人格落"大凶"的组合直接排除。这层不是给开发者看的,是给用户信心的。

python 复制代码
def wuge_is_bad(surname, name):
    """检查五格数理是否为大凶"""
    strokes = get_kangxi_strokes(surname + name)
    ren_grid = strokes[0] + strokes[1]  # 人格 = 姓末字+名首字笔画
    return ren_grid in BAD_NUMBERS  # 81数理中的大凶数

第 6 层:历史去重

用户点"换一批"时,新生成的名字不能和之前展示过的重复。session 中维护已展示名字集合,每层过滤后做最终去重。

六层全通过,才算一个合格的名字。实测下来,LLM 每轮生成 5 个名字,平均有 1-2 个会被过滤掉,所以系统会多生成几个作为缓冲。

五、踩过的坑

坑 1:让 LLM "不编造"有多难

最初 prompt 只写了"请使用提供的诗句",编造率约 30%。加了"绝不编造任何诗句或出处"后降到 10%。加上后处理的精确匹配校验,才做到零编造。

经验:prompt 约束是第一道防线,后处理校验是兜底。两道都不能少。只靠 prompt 就像只靠自律------大部分时候有用,但总会翻车。

坑 2:FAISS 索引的冷启动

525MB 的索引文件,首次加载要几秒。改用 mmap 后 RSS 占用从几个 GB 降到几十 MB,但第一次查询仍然有冷启动延迟(需要把向量数据从磁盘读入页缓存)。

解决方案:服务启动时做一次空查询"预热",把常用页拉入缓存。

python 复制代码
# 服务启动后预热FAISS索引
warmup_vec = np.zeros((1, dim), dtype=np.float32)
index.search(warmup_vec, k=1)  # 触发页缓存加载

坑 3:Embedding 查询缓存的必要性

相同输入(同姓氏+同性别+同风格)的查询会反复出现。为 query embedding 加了 LRU 缓存(maxsize=128),命中率出乎意料地高------同一个 session 内的"换一批"操作,只有方向词在变,基础 query 的 embedding 可以复用。

六、实际效果

以上技术方案已经落地为一个完整产品------文间拾字。展示几张实际运行效果:

输入界面:用户填写姓氏、性别、出生时间,选择风格偏好:

生成过程:通过 SSE 流式推送,名字逐个"跳出来",加载期间展示古诗词轮播:

结果展示:每个名字附带完整信息------诗词出处(朝代+作者+原句)、五行标注、音韵分析、取名思路:

每个名字卡片的信息密度很高:诗词原文保证有据可查,五行标注让用户知道"补了什么",音韵分析让用户知道"念起来好不好听",取名思路让用户理解"为什么选这两个字"。

七、写在最后

这个系统的核心思路是:用 RAG 架构约束 LLM 的幻觉,用向量检索保证诗词出处的真实性,用域知识(八字五行)增强检索的相关性,用多层过滤保证名字的质量

技术栈并不复杂------FAISS、Embedding、LLM、SQLite,都是成熟的工具。真正的难点在于把"古诗词取名"这个垂直领域的业务逻辑(五行、音韵、五格)和技术架构(向量检索、流式输出、质量过滤)有机地融合在一起。

如果你也在做 RAG 相关的项目,欢迎在评论区交流。特别是"如何约束 LLM 不编造引用源"这个问题,很想听听大家的方案。


体验一下

上面拆解的所有技术,都已经跑在这个产品里了:

微信搜索小程序「文间拾字」 即可体验。

收录《诗经》《楚辞》《全唐诗》《宋词》等经典,所有名字 100% 有诗词原文出处,零编造。

如果你身边有正在给宝宝起名的朋友,可以把这个工具分享给他们------比起花钱找大师,不如让 AI 翻遍 270 万句古诗词,给名字找一个真正有据可查的出处。

评论区互动

评论区留下「姓氏 + 性别」(如"张 + 男孩"),我用这个系统帮你跑一版,看看效果如何 👇

相关推荐
小碗细面1 小时前
从 PRD 到上线:SpecKit 如何让 AI 真正参与前端交付
前端·ai编程·claude
正儿八经的少年1 小时前
Spring Boot
java·spring boot·后端
卖酸奶的不错1 小时前
# 国产模型注意力衰减更严重,海外模型衰减更平缓的完整底层原因
ai编程
未秃头的程序猿1 小时前
虚拟线程用了一年,我真香了——10倍吞吐量是真实存在的
java·后端·面试
namexingyun1 小时前
Scaling Law bug实战启示:从“虚胖“到“精瘦“的算力效率革命
开发语言·网络·人工智能·bug·ai编程
神奇的代码在哪里2 小时前
AI编程时代,PPT已成为过去,单个HTML文件创造无限可能
javascript·html·powerpoint·ai编程·ppt
海兰2 小时前
【AI编程思考:第八篇】从演示到生产:AI工程部署实战指南
大数据·人工智能·ai编程
地铁潜行者2 小时前
从单体思维到微服务:服务拆分与组件选型实践
java·后端
花生了什么事o2 小时前
线程池参数调优:corePoolSize 怎么设置
java·后端