基于RAG技术构建《天龙八部》智能问答系统:从向量检索到生成式AI的实践

📅 2026/8/5 22:30:13
基于RAG技术构建《天龙八部》智能问答系统:从向量检索到生成式AI的实践
1. 项目概述当武侠经典遇上AI记忆最近在捣鼓RAG检索增强生成技术总想找个有意思的领域来练手。那些常规的文档问答、客服机器人说实话有点腻了。直到有天晚上重温97版《天龙八部》看到乔峰在聚贤庄大战群雄突然灵光一闪金庸老爷子的这部鸿篇巨制人物关系错综复杂武功门派源远流长情节更是草蛇灰线伏脉千里。别说普通读者就是资深金迷也未必能把所有细节都记得清清楚楚。比如段誉的“凌波微步”到底是从哪个山洞里的玉像学来的虚竹破解的“珍珑棋局”除了无崖子还有哪些高手在场如果有一个系统能把整部小说的文本“吃”进去然后像一位无所不知的武林百晓生一样随时回答你的任何问题那该多酷这就是我启动这个“《天龙八部》RAG智能问答系统”项目的初衷。它不是一个简单的关键词匹配搜索而是要让AI真正理解小说内容结合上下文进行推理和回答。比如你问“乔峰为什么一定要死”系统不能只回复“他在雁门关外自尽”而应该能梳理出他的身世之谜、宋辽对立、承诺与道义之间的重重矛盾给出一个有深度的分析。这个项目本质上是在用现代AI技术为一部传统文学经典构建一个动态的、可交互的“超级索引”和“智能导读”。整个系统的工作流程可以类比为一位勤奋的图书管理员检索系统加上一位博学的解说员生成模型。首先我们需要把《天龙八部》的全本电子书“喂”给系统系统会像管理员一样把这本书拆解、消化、分门别类地存储到自己的“记忆库”向量数据库里。当你提出一个问题时管理员会迅速从记忆库中找出与问题最相关的几个段落检索。然后解说员拿到这些关键段落和你的原始问题组织语言生成一个连贯、准确且包含出处的答案增强生成。这个过程就是RAG的核心。2. 核心需求解析与方案选型2.1 需求拆解我们要解决什么问题搭建这个系统远不止是“能回答问题”那么简单。经过仔细分析我梳理出了几个层次的核心需求精准的事实性问答这是基础。系统必须能准确回答关于人物、地点、事件、武功、关系等具体事实的问题。例如“阿朱和阿紫是什么关系”、“‘降龙十八掌’的第一招叫什么”、“少林寺大战发生在第几章” 答案必须绝对忠于原著不能胡编乱造。复杂的推理与归纳这是进阶。很多问题需要联系多个分散的章节信息进行推理。例如“分析段誉的爱情线”这就需要系统从段誉与木婉清、钟灵、王语嫣的多次相遇、冲突、情感变化中提取信息并归纳出一条脉络。再比如“比较‘北乔峰南慕容’的武功特点与人生结局”这涉及对比分析和深层次原因探究。答案的可解释性与溯源这是信任的关键。AI生成的答案必须告诉用户“这个信息是来自书中哪里的”。每一条重要陈述最好都能引用原文的章节或片段。这样用户才能验证也更容易接受。你不能光说“虚竹是灵鹫宫宫主”还得能指出“此情节见于原著第X章”。对武侠语境和文学性的理解这是特色。系统需要能理解一些武侠特有的概念和文学表达。比如当问题中提到“内力”、“招式”、“奇遇”、“正邪”时系统应能在武侠的语境下处理这些词而不是按字面意思理解。2.2 技术方案选型为什么是RAG面对这些需求我评估了几种主流方案传统全文检索比如用Elasticsearch。它能快速找到包含关键词的段落但无法理解语义。你搜“乔峰的悲剧”它可能找不到因为原文没有“悲剧”这个词。它更擅长“查找”而非“理解”和“回答”。微调大型语言模型直接用《天龙八部》的文本去训练或微调一个像ChatGPT这样的模型。这理论上能让模型“学会”整本书但成本极高需要大量的算力和数据且容易导致模型“遗忘”原有知识灾难性遗忘。对于个人开发者来说这几乎不可行。检索增强生成这正是我选择的路径。它完美地结合了前两者的优点利用高效的检索系统从海量文本中快速找到相关信息解决记忆问题再让强大的大语言模型基于这些信息生成答案解决理解和生成问题。它成本相对较低可解释性强答案有出处并且能方便地更新知识库比如以后想加入《射雕英雄传》直接导入文本即可。因此RAG是当前构建此类垂直领域、高精度知识问答系统的最优解。它的架构清晰分为三部分文档处理 - 检索 - 生成接下来我们就按这个脉络一步步拆解实现。2.3 工具栈选择我的“兵器谱”工欲善其事必先利其器。以下是我为这个项目挑选的核心工具并解释为什么选它们文本向量模型text-embedding-ada-002/BAAI/bge-small-zh-v1.5作用将文本转换为计算机能理解的数字向量一组高维数字。语义相近的文本其向量在空间中的距离也相近。选型理由OpenAI的ada-002是闭源中的佼佼者效果稳定API调用简单。但我更倾向于本地部署因此选择了智源的BGE中文模型它在中文语义理解任务上表现优异且完全免费开源。对于武侠小说这种纯中文场景BGE足矣。向量数据库ChromaDB作用存储上一步生成的文本向量和对应的原始文本片段。当新问题来时将问题也转为向量并在数据库中快速找到最相似的几个文本片段即“检索”。选型理由轻量、易用、纯Python环境集成简单。它属于“嵌入式数据库”无需单独部署服务器对于本项目这种数据量一部小说和原型开发阶段非常友好。相比Milvus、Pinecone等Chroma的学习成本和部署成本最低。大语言模型DeepSeek-Chat / Qwen2.5-7B-Instruct作用作为最终的“解说员”。接收用户问题和检索到的相关文本片段生成一个流畅、准确、有逻辑的答案。选型理由GPT-4固然强大但API调用有成本和延迟。国内开源的DeepSeek和通义千问系列模型近年来进步神速在中文理解和生成上已非常出色。我选择在本地或通过兼容OpenAI API的服务器部署一个7B参数的模型在保证回答质量的同时实现完全自主可控和零调用成本。应用框架LangChain / LlamaIndex作用它们是构建RAG应用的“脚手架”或“工具箱”提供了连接向量数据库、大模型、处理文档流水线的一系列标准化组件能极大提升开发效率。选型理由两者都是优秀的选择。LangChain更像“乐高”组件多且灵活LlamaIndex则更专注于RAG场景对文档处理和数据连接的抽象更好。本项目我选择LlamaIndex因为它对中文支持友好且其“索引-查询引擎”的概念与我们的需求非常贴合代码写起来更简洁直观。注意工具选型没有绝对的对错只有是否适合当前场景。对于个人学习项目我的原则是“轻量化、本地化、开源优先”以降低复杂度和成本聚焦于核心逻辑的理解与实现。3. 实战第一步构建《天龙八部》知识库3.1 文本获取与预处理第一步是准备“粮食”。我找到了一个校对相对精良的《天龙八部》全本TXT文件。预处理是关键直接决定后续检索的质量。核心操作文本分块你不能把整本100多万字的小说作为一个文档扔给系统。那样检索时会返回一个包含无数信息的巨大文本大模型很难从中精准提取答案。必须进行“分块”。# 示例使用LangChain的递归字符文本分割器 from langchain.text_splitter import RecursiveCharacterTextSplitter with open(tianlongbabu.txt, r, encodingutf-8) as f: full_text f.read() # 创建分割器 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块的最大字符数 chunk_overlap50, # 块与块之间的重叠字符数避免上下文断裂 separators[\n\n, \n, 。, , , , , ] # 按此优先级分割 ) # 执行分割 chunks text_splitter.split_text(full_text) print(f原文被分割成了 {len(chunks)} 个文本块。)chunk_size500为什么是500经过测试对于中文小说500-800字是一个比较合适的范围。它既能包含一个相对完整的情节片段比如一段对话加描述又不会因为太长而引入过多噪声。太短则信息不全太长则检索精度下降。chunk_overlap50设置重叠是为了防止一个完整的句子或关键信息被硬生生切在两块之间。例如一个重要的线索可能在一段的末尾提出在下一段开头揭示重叠能确保它们同时出现在某个块中。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_nameBAAI/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_collectionchroma_collection) storage_context StorageContext.from_defaults(vector_storevector_store) # 4. 读取并处理文档假设分块后的文本已保存为多个小文件在./data目录 documents SimpleDirectoryReader(./data).load_data() # 5. 创建索引这一步会自动调用embed_model将文档向量化并存入vector_store index VectorStoreIndex.from_documents( documents, storage_contextstorage_context, embed_modelembed_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()提供了一个默认配置的引擎。当你提问时它会将问题“乔峰的降龙十八掌是谁教的”转化为向量。在ChromaDB中搜索与问题向量最相似的Top K个文本块默认K2。将这K个文本块和原始问题一起组合成一个提示词Prompt发送给大语言模型。大模型基于这些上下文生成最终答案。但这里存在明显问题如果检索到的文本块没有直接答案或者答案分散在多个块中大模型可能会“胡编乱造”幻觉或者给出不完整的答案。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( indexindex, similarity_top_k5, # 从数据库中检索出5个最相似的块 ) # 创建重排序器使用一个更精细的模型对初筛结果进行二次排序 reranker SentenceTransformerRerank(modelBAAI/bge-reranker-base, top_n2) # 组装查询引擎 query_engine RetrieverQueryEngine( retrieverretriever, node_postprocessors[reranker], # 将重排序器作为后处理环节 )similarity_top_k5先“广撒网”多召回一些可能相关的文本块比如5个。重排序初筛是基于“语义相似度”但相似度高不一定代表最能回答问题。重排序器如bge-reranker专门用于评估“问题-段落”之间的相关性它能从5个候选中挑出最可能包含答案的2个top_n2。这相当于双重保险显著提升了检索精度。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_promptqa_prompt)这个提示词做了几件事限定角色、强调依据上下文、规定不同情况下的回答策略、严厉禁止幻觉。一个清晰的指令能极大约束大模型的行为使其输出更可控、更可靠。3. 启用引用溯源让答案有据可查这是建立信任的核心功能。我们需要让引擎在回答时注明信息来源于哪个文本块。query_engine index.as_query_engine( similarity_top_k3, response_modecompact, # 或 refine两种生成模式 text_qa_promptqa_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[来源{idx1}] 相关度{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( fnanswer_question, inputsgr.Textbox(label请输入关于《天龙八部》的问题, placeholder例如乔峰的父亲是谁), outputsgr.Markdown(label智能回答), title 《天龙八部》RAG智能问答系统, description基于检索增强生成技术为您解答金庸《天龙八部》中的各类问题。答案均源自原著。, examples[ [乔峰的降龙十八掌是谁教的], [段誉最后和谁在一起了], [‘珍珑棋局’是谁布下的], ] ) if __name__ __main__: demo.launch(server_name0.0.0.0, server_port7860) # 在本地7860端口启动这个界面虽然简陋但功能完整输入问题输出答案和引用来源。examples参数提供了一些示例问题方便用户快速体验。6. 效果评测与调优实录系统跑起来了但效果如何我设计了一系列测试问题来检验。6.1 测试案例与结果分析问题类型测试问题期望答案要点系统实际表现分析与调优简单事实“无量剑派的东西宗之争因何而起”因对“无量玉璧”上仙人舞剑的见解不同而分裂。✅ 回答准确并引用了相关段落。基础检索功能正常分块大小合适。跨章节推理“虚竹破解珍珑棋局后得到了哪些传承”得到无崖子七十余年功力、逍遥派掌门之位七宝指环、逍遥派武功图谱。⚠️ 只提到了功力和掌门指环遗漏了武功图谱。检索到的前几个块可能只强调了前两者。调优将similarity_top_k从3增加到5让模型看到更多上下文。人物关系“阿朱、阿紫、阮星竹之间的关系是什么”阮星竹是阿朱和阿紫的母亲她们是姐妹。✅ 回答正确并说明了她们都是段正淳的女儿。关系类问题通常集中在一个或相邻章节检索难度较低。情节归纳“简述‘雁门关外’事件的前因后果。”前因慕容博假传消息。经过中原高手伏击萧远山一家。后果萧远山跳崖未死乔峰成孤儿引发三十年恩怨。⚠️ 能说出主要经过但对“慕容博假传消息”这一关键前因提及模糊。问题涉及长跨度情节。调优在提示词中更加强调“前因后果”的梳理要求并尝试使用response_moderefine迭代细化模式让模型多次处理检索结果可能生成更全面的摘要。开放解读“你认为乔峰之死是必然的吗”期望一个基于原著情节身世、恩怨、宋辽矛盾、个人性格的分析而非简单“是”或“否”。⚠️ 给出了分析但部分论据略显空泛有轻微脱离上下文的“泛泛而谈”。开放性问题对模型要求高容易触发其通用知识而非特定上下文。调优在提示词中增加约束“请严格结合上下文中乔峰的具体经历和选择进行分析”。同时确保检索到的块包含其最后在雁门关的心理描写和对话。6.2 常见问题与排查技巧在调试过程中我遇到了几个典型问题以下是排查思路问题答案出现明显事实错误幻觉排查首先检查response.source_nodes。如果引用的来源文本本身正确但模型答错了问题在生成端。强化提示词中的“严禁编造”指令或换用推理能力更强的模型。如果来源文本就不相关或错误问题在检索端。检查分块是否过碎导致信息不完整similarity_top_k是否太小嵌入模型对中文武侠词汇的语义捕捉是否不准可以尝试换用bge-large等更大模型或引入重排序。技巧启用引用是调试幻觉的最重要工具。一眼就能定位问题是检索不准还是生成乱说。问题答案不完整遗漏关键信息排查通常是检索到的上下文不足。增加similarity_top_k值例如从2调到5。检查分块策略对于可能包含多个要点的长段落如介绍一个复杂事件适当增加chunk_size或尝试按“章节”进行更大粒度的分块作为补充检索策略。技巧对于归纳类问题可以尝试LlamaIndex的SummaryIndex或使用refine响应模式让模型像滚雪球一样整合多个节点的信息。问题系统回答“根据已知信息无法回答”但明明书里有排查最可能的原因是检索失败。计算出的问题向量与知识库中正确答案的向量“距离”太远。这可能是因为表述差异用户问“萧峰”但书中多用“乔峰”。考虑在查询前对问题做同义词扩展。嵌入模型局限某些专有名词或复杂表述语义捕捉不佳。可以尝试在嵌入前对文本进行轻度优化如核心实体标准化。阈值过高向量数据库有相似度分数阈值低于阈值的会被过滤。检查或调整该阈值。问题响应速度慢排查分阶段测试。query_engine.query()耗时嵌入问题时间检索时间生成时间。用time.time()记录各阶段耗时。嵌入慢考虑使用更轻量的嵌入模型或缓存问题的嵌入结果如果问题重复度高。检索慢ChromaDB在数据量不大时很快。如果慢检查是否每次查询都新建连接。生成慢这是主要瓶颈。7B模型在CPU上生成可能需10-30秒。考虑使用GPU加速或换用响应更快的API服务如有条件。7. 项目总结与未来展望经过从数据准备、向量化、引擎搭建到前端集成的全流程实践这个《天龙八部》RAG问答系统已经能够相当可靠地回答关于这部小说的各类问题。它不再是简单的字符串匹配而是真正理解了问题意图并从原著中寻找依据来组织答案。我个人最深的几点体会是数据质量决定上限文本的清洁度、分块的合理性直接决定了检索的质量。在预处理阶段多花一小时可能在调试阶段省下一天。RAG是一个“系统工程”它不仅仅是调用两个API。检索策略top-k, 重排序、提示词工程、生成模型的选择每一个环节都像齿轮一样紧密咬合需要反复调试以达到最佳平衡。可解释性至关重要对于知识问答用户需要信任。引用溯源功能不仅是炫技更是建立信任、辅助调试的必需品。没有“银弹”不同的提问方式可能需要不同的RAG策略。对于简单事实基础检索即可对于复杂推理可能需要更复杂的多步检索或图检索技术。本项目目前是一个强大的基础版本。这个项目就像一个完整的“样板间”其方法论可以平移到任何垂直领域你可以替换掉《天龙八部》的文本放入公司内部文档、产品手册、学术论文库、甚至法律条文快速构建起一个专属的智能知识助手。最后分享一个调试小技巧在开发过程中我专门维护了一个“问题-期望答案”测试集。每次对系统如改分块大小、换模型、调提示词做任何更改后都会跑一遍这个测试集用简单的脚本对比输出与期望答案的重合度哪怕只是粗略的关键词匹配这能快速量化你的改动是提升还是倒退让优化过程更有方向。