基于RAG技术构建《天龙八部》智能问答系统:从向量检索到生成式AI的实践
2026/8/5 22:30:02 网站建设 项目流程

1. 项目概述:当武侠经典遇上AI记忆

最近在捣鼓RAG(检索增强生成)技术,总想找个有意思的领域来练手。那些常规的文档问答、客服机器人,说实话有点腻了。直到有天晚上重温97版《天龙八部》,看到乔峰在聚贤庄大战群雄,突然灵光一闪:金庸老爷子的这部鸿篇巨制,人物关系错综复杂,武功门派源远流长,情节更是草蛇灰线伏脉千里。别说普通读者,就是资深金迷,也未必能把所有细节都记得清清楚楚。比如,段誉的“凌波微步”到底是从哪个山洞里的玉像学来的?虚竹破解的“珍珑棋局”,除了无崖子,还有哪些高手在场?如果有一个系统,能把整部小说的文本“吃”进去,然后像一位无所不知的武林百晓生一样,随时回答你的任何问题,那该多酷?

这就是我启动这个“《天龙八部》RAG智能问答系统”项目的初衷。它不是一个简单的关键词匹配搜索,而是要让AI真正理解小说内容,结合上下文进行推理和回答。比如,你问“乔峰为什么一定要死?”,系统不能只回复“他在雁门关外自尽”,而应该能梳理出他的身世之谜、宋辽对立、承诺与道义之间的重重矛盾,给出一个有深度的分析。这个项目,本质上是在用现代AI技术,为一部传统文学经典构建一个动态的、可交互的“超级索引”和“智能导读”。

整个系统的工作流程,可以类比为一位勤奋的图书管理员(检索系统)加上一位博学的解说员(生成模型)。首先,我们需要把《天龙八部》的全本电子书“喂”给系统,系统会像管理员一样,把这本书拆解、消化、分门别类地存储到自己的“记忆库”(向量数据库)里。当你提出一个问题时,管理员会迅速从记忆库中找出与问题最相关的几个段落(检索)。然后,解说员拿到这些关键段落和你的原始问题,组织语言,生成一个连贯、准确且包含出处的答案(增强生成)。这个过程,就是RAG的核心。

2. 核心需求解析与方案选型

2.1 需求拆解:我们要解决什么问题?

搭建这个系统,远不止是“能回答问题”那么简单。经过仔细分析,我梳理出了几个层次的核心需求:

  1. 精准的事实性问答:这是基础。系统必须能准确回答关于人物、地点、事件、武功、关系等具体事实的问题。例如:“阿朱和阿紫是什么关系?”、“‘降龙十八掌’的第一招叫什么?”、“少林寺大战发生在第几章?” 答案必须绝对忠于原著,不能胡编乱造。

  2. 复杂的推理与归纳:这是进阶。很多问题需要联系多个分散的章节信息进行推理。例如:“分析段誉的爱情线”,这就需要系统从段誉与木婉清、钟灵、王语嫣的多次相遇、冲突、情感变化中提取信息,并归纳出一条脉络。再比如:“比较‘北乔峰,南慕容’的武功特点与人生结局”,这涉及对比分析和深层次原因探究。

  3. 答案的可解释性与溯源:这是信任的关键。AI生成的答案,必须告诉用户“这个信息是来自书中哪里的”。每一条重要陈述,最好都能引用原文的章节或片段。这样用户才能验证,也更容易接受。你不能光说“虚竹是灵鹫宫宫主”,还得能指出“此情节见于原著第X章”。

  4. 对武侠语境和文学性的理解:这是特色。系统需要能理解一些武侠特有的概念和文学表达。比如,当问题中提到“内力”、“招式”、“奇遇”、“正邪”时,系统应能在武侠的语境下处理这些词,而不是按字面意思理解。

2.2 技术方案选型:为什么是RAG?

面对这些需求,我评估了几种主流方案:

  • 传统全文检索:比如用Elasticsearch。它能快速找到包含关键词的段落,但无法理解语义。你搜“乔峰的悲剧”,它可能找不到,因为原文没有“悲剧”这个词。它更擅长“查找”,而非“理解”和“回答”。
  • 微调大型语言模型:直接用《天龙八部》的文本去训练或微调一个像ChatGPT这样的模型。这理论上能让模型“学会”整本书,但成本极高,需要大量的算力和数据,且容易导致模型“遗忘”原有知识(灾难性遗忘)。对于个人开发者来说,这几乎不可行。
  • 检索增强生成:这正是我选择的路径。它完美地结合了前两者的优点:利用高效的检索系统从海量文本中快速找到相关信息(解决记忆问题),再让强大的大语言模型基于这些信息生成答案(解决理解和生成问题)。它成本相对较低,可解释性强(答案有出处),并且能方便地更新知识库(比如以后想加入《射雕英雄传》,直接导入文本即可)。

因此,RAG是当前构建此类垂直领域、高精度知识问答系统的最优解。它的架构清晰分为三部分:文档处理 -> 检索 -> 生成,接下来我们就按这个脉络,一步步拆解实现。

2.3 工具栈选择:我的“兵器谱”

工欲善其事,必先利其器。以下是我为这个项目挑选的核心工具,并解释为什么选它们:

  1. 文本向量模型:text-embedding-ada-002/BAAI/bge-small-zh-v1.5

    • 作用:将文本转换为计算机能理解的数字向量(一组高维数字)。语义相近的文本,其向量在空间中的距离也相近。
    • 选型理由:OpenAI的ada-002是闭源中的佼佼者,效果稳定,API调用简单。但我更倾向于本地部署,因此选择了智源的BGE中文模型,它在中文语义理解任务上表现优异,且完全免费开源。对于武侠小说这种纯中文场景,BGE足矣。
  2. 向量数据库:ChromaDB

    • 作用:存储上一步生成的文本向量和对应的原始文本片段。当新问题来时,将问题也转为向量,并在数据库中快速找到最相似的几个文本片段(即“检索”)。
    • 选型理由:轻量、易用、纯Python环境集成简单。它属于“嵌入式数据库”,无需单独部署服务器,对于本项目这种数据量(一部小说)和原型开发阶段非常友好。相比Milvus、Pinecone等,Chroma的学习成本和部署成本最低。
  3. 大语言模型:DeepSeek-Chat / Qwen2.5-7B-Instruct

    • 作用:作为最终的“解说员”。接收用户问题和检索到的相关文本片段,生成一个流畅、准确、有逻辑的答案。
    • 选型理由:GPT-4固然强大,但API调用有成本和延迟。国内开源的DeepSeek和通义千问系列模型近年来进步神速,在中文理解和生成上已非常出色。我选择在本地或通过兼容OpenAI API的服务器部署一个7B参数的模型,在保证回答质量的同时,实现完全自主可控和零调用成本。
  4. 应用框架:LangChain / LlamaIndex

    • 作用:它们是构建RAG应用的“脚手架”或“工具箱”,提供了连接向量数据库、大模型、处理文档流水线的一系列标准化组件,能极大提升开发效率。
    • 选型理由:两者都是优秀的选择。LangChain更像“乐高”,组件多且灵活;LlamaIndex则更专注于RAG场景,对文档处理和数据连接的抽象更好。本项目我选择LlamaIndex,因为它对中文支持友好,且其“索引-查询引擎”的概念与我们的需求非常贴合,代码写起来更简洁直观。

注意:工具选型没有绝对的对错,只有是否适合当前场景。对于个人学习项目,我的原则是“轻量化、本地化、开源优先”,以降低复杂度和成本,聚焦于核心逻辑的理解与实现。

3. 实战第一步:构建《天龙八部》知识库

3.1 文本获取与预处理

第一步是准备“粮食”。我找到了一个校对相对精良的《天龙八部》全本TXT文件。预处理是关键,直接决定后续检索的质量。

核心操作:文本分块你不能把整本100多万字的小说作为一个文档扔给系统。那样检索时,会返回一个包含无数信息的巨大文本,大模型很难从中精准提取答案。必须进行“分块”。

# 示例:使用LangChain的递归字符文本分割器 from langchain.text_splitter import RecursiveCharacterTextSplitter with open('tianlongbabu.txt', 'r', encoding='utf-8') as f: full_text = f.read() # 创建分割器 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 每个块的最大字符数 chunk_overlap=50, # 块与块之间的重叠字符数,避免上下文断裂 separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] # 按此优先级分割 ) # 执行分割 chunks = text_splitter.split_text(full_text) print(f"原文被分割成了 {len(chunks)} 个文本块。")
  • chunk_size=500:为什么是500?经过测试,对于中文小说,500-800字是一个比较合适的范围。它既能包含一个相对完整的情节片段(比如一段对话加描述),又不会因为太长而引入过多噪声。太短则信息不全,太长则检索精度下降。
  • chunk_overlap=50:设置重叠是为了防止一个完整的句子或关键信息被硬生生切在两块之间。例如,一个重要的线索可能在一段的末尾提出,在下一段开头揭示,重叠能确保它们同时出现在某个块中。
  • separators:分割符的优先级设置很重要。优先按“段落(\n\n)”分割,保持叙事连贯性;不行再按“句子(。!?)”分割,保证语义完整。

实操心得: 预处理时,我还手动做了一些清洗工作:删除了纯数字的页码、统一了全半角标点、将“第XX章”这样的标题单独提取出来作为块的元数据(方便溯源)。这些细节能显著提升后续步骤的体验。

3.2 向量化与入库:让文本拥有“位置”

文本分块后,接下来就是用嵌入模型将每一块文本转化为向量,并存入向量数据库。

# 示例:使用LlamaIndex结合BGE模型和ChromaDB from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, StorageContext from llama_index.vector_stores.chroma import ChromaVectorStore from llama_index.embeddings.huggingface import HuggingFaceEmbedding import chromadb # 1. 初始化嵌入模型(使用BGE) embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-zh-v1.5") # 2. 初始化Chroma客户端和集合 chroma_client = chromadb.PersistentClient(path="./chroma_db") # 数据持久化到本地目录 chroma_collection = chroma_client.get_or_create_collection("tianlongbabu") # 3. 创建向量存储 vector_store = ChromaVectorStore(chroma_collection=chroma_collection) storage_context = StorageContext.from_defaults(vector_store=vector_store) # 4. 读取并处理文档(假设分块后的文本已保存为多个小文件在`./data`目录) documents = SimpleDirectoryReader("./data").load_data() # 5. 创建索引:这一步会自动调用embed_model将文档向量化并存入vector_store index = VectorStoreIndex.from_documents( documents, storage_context=storage_context, embed_model=embed_model ) print("知识库构建完成!")
  • PersistentClient:将向量数据库持久化到本地./chroma_db目录。这样下次启动程序时,无需重新计算向量,直接加载即可,大大节省时间。
  • from_documents:这是LlamaIndex的核心魔法。它内部完成了读取文档、调用嵌入模型生成向量、并将(向量, 文本, 元数据)存储到ChromaDB的全流程。
  • 嵌入模型的选择:这里指定了embed_model。如果不指定,LlamaIndex会使用默认的嵌入模型,但对于中文,强烈建议指定一个优秀的中文模型。

重要提示:向量化过程可能是最耗时的步骤,取决于文本量和模型大小。一部《天龙八部》全文,使用bge-small模型,在普通CPU上可能需要十几分钟到半小时。耐心等待,这是构建知识库的必经之路。

4. 核心引擎搭建:检索与生成的精妙配合

知识库建好后,就进入了系统的核心环节:查询引擎。它的设计直接决定了问答的智能程度。

4.1 基础查询引擎

最简单的形式是“检索-然后生成”。

# 接续上面的代码 # 从索引创建查询引擎 query_engine = index.as_query_engine() # 进行查询 response = query_engine.query("乔峰的降龙十八掌是谁教的?") print(response)

LlamaIndex的as_query_engine()提供了一个默认配置的引擎。当你提问时,它会:

  1. 将问题“乔峰的降龙十八掌是谁教的?”转化为向量。
  2. 在ChromaDB中搜索与问题向量最相似的Top K个文本块(默认K=2)。
  3. 将这K个文本块和原始问题一起,组合成一个提示词(Prompt),发送给大语言模型。
  4. 大模型基于这些上下文,生成最终答案。

但这里存在明显问题:如果检索到的文本块没有直接答案,或者答案分散在多个块中,大模型可能会“胡编乱造”(幻觉),或者给出不完整的答案。

4.2 进阶优化:提升检索与生成质量

为了让系统更可靠,我进行了以下几项关键优化:

1. 优化检索策略:增加检索数量与重排序

from llama_index.core import VectorStoreIndex from llama_index.core.postprocessor import SentenceTransformerRerank from llama_index.core.retrievers import VectorIndexRetriever # 创建检索器,增加检索数量 retriever = VectorIndexRetriever( index=index, similarity_top_k=5, # 从数据库中检索出5个最相似的块 ) # 创建重排序器(使用一个更精细的模型对初筛结果进行二次排序) reranker = SentenceTransformerRerank(model="BAAI/bge-reranker-base", top_n=2) # 组装查询引擎 query_engine = RetrieverQueryEngine( retriever=retriever, node_postprocessors=[reranker], # 将重排序器作为后处理环节 )
  • similarity_top_k=5:先“广撒网”,多召回一些可能相关的文本块(比如5个)。
  • 重排序:初筛是基于“语义相似度”,但相似度高不一定代表最能回答问题。重排序器(如bge-reranker)专门用于评估“问题-段落”之间的相关性,它能从5个候选中挑出最可能包含答案的2个(top_n=2)。这相当于双重保险,显著提升了检索精度。

2. 优化提示词工程:给大模型更清晰的指令默认的提示词可能不够强。我们需要明确告诉大模型该怎么做。

from llama_index.core import PromptTemplate # 自定义提示词模板 qa_prompt_tmpl = ( "你是一位精通《天龙八部》的专家。请严格根据以下提供的上下文信息来回答问题。\n" "如果上下文信息足以回答问题,请给出准确、简洁的答案,并引用相关上下文。\n" "如果上下文信息不足以完全回答问题,你可以基于常识进行合理推断,但必须明确指出哪些部分是推断。\n" "如果上下文信息与问题完全无关,请直接回答‘根据已知信息无法回答此问题’。\n" "严禁编造上下文信息中不存在的内容。\n\n" "上下文信息如下:\n" "---------------------\n" "{context_str}\n" "---------------------\n" "问题:{query_str}\n" "答案:" ) qa_prompt = PromptTemplate(qa_prompt_tmpl) # 创建引擎时应用自定义提示词 query_engine = index.as_query_engine(text_qa_prompt=qa_prompt)

这个提示词做了几件事:限定角色强调依据上下文规定不同情况下的回答策略严厉禁止幻觉。一个清晰的指令,能极大约束大模型的行为,使其输出更可控、更可靠。

3. 启用引用溯源:让答案有据可查这是建立信任的核心功能。我们需要让引擎在回答时,注明信息来源于哪个文本块。

query_engine = index.as_query_engine( similarity_top_k=3, response_mode="compact", # 或 "refine",两种生成模式 text_qa_prompt=qa_prompt, ) response = query_engine.query("段誉的六脉神剑时灵时不灵的原因是什么?") print(f"答案:{response}") print("\n--- 来源引用 ---") for node in response.source_nodes: print(f"来自文本块 ID: {node.node_id}") print(f"内容片段: {node.text[:200]}...") # 打印前200字符 print(f"相似度分数: {node.score:.4f}\n")

通过访问response.source_nodes,我们可以获取到生成答案所依据的每一个文本块及其相关性分数。在UI界面上,可以将这些片段折叠或悬停显示,从而完美实现答案溯源。

5. 前端交互与系统集成

一个完整的系统还需要一个用户界面。我选择了用Gradio快速搭建一个Web界面,因为它简单易用,适合原型演示。

import gradio as gr from query_engine import get_qa_engine # 假设我们将上面的引擎封装成了函数 # 初始化查询引擎 qa_engine = get_qa_engine() def answer_question(question, history): """处理用户提问""" try: response = qa_engine.query(question) answer_text = str(response) # 提取来源信息 sources = [] if hasattr(response, 'source_nodes') and response.source_nodes: for idx, node in enumerate(response.source_nodes[:3]): # 显示前3个来源 source_preview = node.text[:150].replace('\n', ' ') + "..." sources.append(f"[来源{idx+1}] 相关度:{node.score:.3f}\n{source_preview}") full_response = f"{answer_text}\n\n---\n**参考来源:**\n" + "\n\n".join(sources) if sources else answer_text return full_response except Exception as e: return f"查询时出现错误:{e}" # 创建Gradio界面 demo = gr.Interface( fn=answer_question, inputs=gr.Textbox(label="请输入关于《天龙八部》的问题", placeholder="例如:乔峰的父亲是谁?"), outputs=gr.Markdown(label="智能回答"), title="🏯 《天龙八部》RAG智能问答系统", description="基于检索增强生成技术,为您解答金庸《天龙八部》中的各类问题。答案均源自原著。", examples=[ ["乔峰的降龙十八掌是谁教的?"], ["段誉最后和谁在一起了?"], ["‘珍珑棋局’是谁布下的?"], ] ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860) # 在本地7860端口启动

这个界面虽然简陋,但功能完整:输入问题,输出答案和引用来源。examples参数提供了一些示例问题,方便用户快速体验。

6. 效果评测与调优实录

系统跑起来了,但效果如何?我设计了一系列测试问题来检验。

6.1 测试案例与结果分析

问题类型测试问题期望答案要点系统实际表现分析与调优
简单事实“无量剑派的东西宗之争因何而起?”因对“无量玉璧”上仙人舞剑的见解不同而分裂。✅ 回答准确,并引用了相关段落。基础检索功能正常,分块大小合适。
跨章节推理“虚竹破解珍珑棋局后,得到了哪些传承?”得到无崖子七十余年功力、逍遥派掌门之位(七宝指环)、逍遥派武功图谱。⚠️ 只提到了功力和掌门指环,遗漏了武功图谱。检索到的前几个块可能只强调了前两者。调优:将similarity_top_k从3增加到5,让模型看到更多上下文。
人物关系“阿朱、阿紫、阮星竹之间的关系是什么?”阮星竹是阿朱和阿紫的母亲,她们是姐妹。✅ 回答正确,并说明了她们都是段正淳的女儿。关系类问题通常集中在一个或相邻章节,检索难度较低。
情节归纳“简述‘雁门关外’事件的前因后果。”前因:慕容博假传消息。经过:中原高手伏击萧远山一家。后果:萧远山跳崖未死,乔峰成孤儿,引发三十年恩怨。⚠️ 能说出主要经过,但对“慕容博假传消息”这一关键前因提及模糊。问题涉及长跨度情节。调优:在提示词中更加强调“前因后果”的梳理要求,并尝试使用response_mode="refine"(迭代细化模式),让模型多次处理检索结果,可能生成更全面的摘要。
开放解读“你认为乔峰之死是必然的吗?”期望一个基于原著情节(身世、恩怨、宋辽矛盾、个人性格)的分析,而非简单“是”或“否”。⚠️ 给出了分析,但部分论据略显空泛,有轻微脱离上下文的“泛泛而谈”。开放性问题对模型要求高,容易触发其通用知识而非特定上下文。调优:在提示词中增加约束:“请严格结合上下文中乔峰的具体经历和选择进行分析”。同时,确保检索到的块包含其最后在雁门关的心理描写和对话。

6.2 常见问题与排查技巧

在调试过程中,我遇到了几个典型问题,以下是排查思路:

  1. 问题:答案出现明显事实错误(幻觉)

    • 排查:首先检查response.source_nodes。如果引用的来源文本本身正确,但模型答错了,问题在生成端。强化提示词中的“严禁编造”指令,或换用推理能力更强的模型。如果来源文本就不相关或错误,问题在检索端。检查分块是否过碎导致信息不完整?similarity_top_k是否太小?嵌入模型对中文武侠词汇的语义捕捉是否不准?可以尝试换用bge-large等更大模型,或引入重排序。
    • 技巧启用引用是调试幻觉的最重要工具。一眼就能定位问题是检索不准还是生成乱说。
  2. 问题:答案不完整,遗漏关键信息

    • 排查:通常是检索到的上下文不足。增加similarity_top_k值(例如从2调到5)。检查分块策略,对于可能包含多个要点的长段落(如介绍一个复杂事件),适当增加chunk_size,或尝试按“章节”进行更大粒度的分块作为补充检索策略。
    • 技巧:对于归纳类问题,可以尝试LlamaIndex的SummaryIndex或使用refine响应模式,让模型像滚雪球一样整合多个节点的信息。
  3. 问题:系统回答“根据已知信息无法回答”,但明明书里有

    • 排查:最可能的原因是检索失败。计算出的问题向量与知识库中正确答案的向量“距离”太远。这可能是因为:
      • 表述差异:用户问“萧峰”,但书中多用“乔峰”。考虑在查询前对问题做同义词扩展。
      • 嵌入模型局限:某些专有名词或复杂表述语义捕捉不佳。可以尝试在嵌入前对文本进行轻度优化(如核心实体标准化)。
      • 阈值过高:向量数据库有相似度分数阈值,低于阈值的会被过滤。检查或调整该阈值。
  4. 问题:响应速度慢

    • 排查:分阶段测试。query_engine.query()耗时=嵌入问题时间+检索时间+生成时间。
      • time.time()记录各阶段耗时。
      • 嵌入慢:考虑使用更轻量的嵌入模型,或缓存问题的嵌入结果(如果问题重复度高)。
      • 检索慢:ChromaDB在数据量不大时很快。如果慢,检查是否每次查询都新建连接。
      • 生成慢:这是主要瓶颈。7B模型在CPU上生成可能需10-30秒。考虑使用GPU加速,或换用响应更快的API服务(如有条件)。

7. 项目总结与未来展望

经过从数据准备、向量化、引擎搭建到前端集成的全流程实践,这个《天龙八部》RAG问答系统已经能够相当可靠地回答关于这部小说的各类问题。它不再是简单的字符串匹配,而是真正理解了问题意图,并从原著中寻找依据来组织答案。

我个人最深的几点体会是:

  1. 数据质量决定上限:文本的清洁度、分块的合理性,直接决定了检索的质量。在预处理阶段多花一小时,可能在调试阶段省下一天。
  2. RAG是一个“系统工程”:它不仅仅是调用两个API。检索策略(top-k, 重排序)、提示词工程、生成模型的选择,每一个环节都像齿轮一样紧密咬合,需要反复调试以达到最佳平衡。
  3. 可解释性至关重要:对于知识问答,用户需要信任。引用溯源功能不仅是炫技,更是建立信任、辅助调试的必需品。
  4. 没有“银弹”:不同的提问方式,可能需要不同的RAG策略。对于简单事实,基础检索即可;对于复杂推理,可能需要更复杂的多步检索或图检索技术。本项目目前是一个强大的基础版本。

这个项目就像一个完整的“样板间”,其方法论可以平移到任何垂直领域:你可以替换掉《天龙八部》的文本,放入公司内部文档、产品手册、学术论文库、甚至法律条文,快速构建起一个专属的智能知识助手。

最后分享一个调试小技巧:在开发过程中,我专门维护了一个“问题-期望答案”测试集。每次对系统(如改分块大小、换模型、调提示词)做任何更改后,都会跑一遍这个测试集,用简单的脚本对比输出与期望答案的重合度(哪怕只是粗略的关键词匹配),这能快速量化你的改动是提升还是倒退,让优化过程更有方向。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询