☰
垂直领域AI问答系统:RAG、知识库与引用溯源实践
2026/10/23 0:33:22 网站建设 项目流程

大约两年前,我在一个技术社群里看到有人讨论一个很有意思的问题:如果用户问的不是“今天天气怎么样”,而是“某部经典中的某句话到底是什么意思”,大模型能不能给一个准确、有出处、可验证的回答?

当时大多数人的判断是:能给出语义相近的答案,但很难保证引文准确。这个判断在今天依然成立,只不过出现了一批专门针对“高严谨度知识领域”做的 AI 问答产品,它们试图把大模型的生成能力、检索增强架构和领域知识库三者结合起来,把“答得流畅”变成“答得可靠”。Sikhbani.ai 就是其中一个很有代表性的案例。

Sikhbani.ai 的全称是 “Ask the Sri Guru Granth Sahib”,从产品名字就能看出来,它是一个针对锡克教圣典《Sri Guru Granth Sahib》的 AI 问答系统。如果你不熟悉锡克教,可能会觉得这类产品和普通开发者关系不大。但换个角度看,它属于一个正在快速增长的 AI 应用类型:面向严肃知识领域、需要强引用溯源、需要多语言支持的垂直问答系统。

这篇文章不打算做宗教内容解读,而是从 AI 工程视角拆解这个项目:它解决了什么问题、底层大概是什么架构、数据准备和检索增强怎么做、如何评估回答质量,以及这类垂直领域 AI 应用能给我们做知识库问答、企业文档问答带来哪些可复用的经验。

读完你至少能搞清楚三件事:一是垂直领域 AI 问答的真实技术难点在哪里;二是 RAG(检索增强生成)在严肃知识领域需要比通用场景多做的工程工作;三是如果你要做一个“只回答某个特定知识库”的 AI 应用,从数据到评测的完整路径应该怎么设计。

1. Sikhbani.ai 是什么:不是聊天机器人,而是“知识答案引擎”

先看产品本身的定位。

《Sri Guru Granth Sahib》是锡克教最重要的宗教经典,由多位宗师和圣人的诗篇、赞美诗组成,内容涵盖哲学、伦理、人生指引等主题。它不仅是宗教文本,也是一种持续存在的文化权威。传统上,信众想要查阅某段圣典的含义,需要依赖学者解读、纸质版本或者专门的数据库,门槛不低。

Sikhbani.ai 做的事情,就是把这个“查询+解读”的过程 AI 化。用户可以用自然语言提问,比如询问某句诗的含义、某段内容在什么背景下写成、某两个概念之间的关系,系统会基于圣典原文和相关解读返回回答,并且在回答中给出引用出处。

这里要注意:它不是一个“开放域”聊天机器人。它不会回答“今天股票怎么走”,也不会和你聊天气。它的边界非常清晰——所有回答都围绕圣典内容展开。这种产品设计在 AI 应用里有一个专门的名字,叫垂直领域问答系统。

垂直领域问答系统和通用 AI 助手最大的区别在于三点:

  • 知识范围有边界:模型只被允许基于指定的知识库回答,超出范围的问题应该拒绝回答或明确告知。
  • 回答要求可溯源:用户需要能回溯到原文,而不是相信一句“大模型生成的话”。
  • 答案稳定性要求高:同一问题在不同时间问,回答核心含义应该一致,不能“每次都不一样”。

从技术形态上看,Sikhbani.ai 这类产品基本都会采用 RAG 架构,而不是直接把所有知识塞进模型参数。原因后面会详细讲,这里先记住一个判断:在严肃知识领域,检索质量决定了回答质量,生成模型只是最后一步的“组织者”。

2. 为什么这类 AI 问答系统比通用大模型更复杂

很多人会有一个直觉:大模型知识那么丰富,直接把圣典、古籍、法律条文喂给它,不就能回答了吗?

这个直觉只对了一半。

大模型确实能在预训练过程中吸收大量文本知识,但它存在三个致命问题:

第一个问题是幻觉。模型在不确定时会自动“脑补”内容。如果用户问“某句话在某页的原文是什么”,模型可能生成一句看起来风格很像、但实际上不存在的句子。这在通用聊天里问题不大,但在宗教经典、法律条文、医疗知识等严谨领域,幻觉是不可接受的错误。

第二个问题是引用不可靠。即使模型给出了原文,它也没有机制保证引用的原文一定存在于知识库中。它会基于概率生成“最像答案的文字”,而不是从数据库里精确检索出来。

第三个问题是知识更新和维护困难。如果要把新解读、新资料加进去,重新训练模型成本极高。而检索方案只需要把新增文档切块、向量化、写入索引,几分钟就能生效。

所以,对于 Sikhbani.ai 这种对准确性要求极高的产品,RAG 几乎是必然选择。它的核心逻辑是:

用户问题 → 检索器从知识库找到最相关的文本块 → 把文本块和问题一起交给大模型 → 大模型只基于这些文本块组织回答 → 输出答案并附引用

这就像考试时给学生一叠参考资料,允许他翻书作答,但要求答案必须来自这叠资料,并且要标注“答案在第几页”。学生(大模型)的自由发挥空间被限制,但答案的可靠性大幅提升。

当然,RAG 不是银弹。它把一个“模型问题”变成了“检索问题+组合问题”。检索不准,生成再强也没用;文本切片策略不对,原文语义可能被切断;引用格式不规范,用户无法验证。这些细节会在后面的实操章节展开。

3. 系统架构推演:一个垂直经典问答应用由哪些模块组成

因为 Sikhbani.ai 没有开源全部工程细节,下面基于产品公开信息和同类项目通用实践,推演一套可行的技术架构。这套架构你也可以直接用于其他垂直领域问答项目。

整个系统可以拆成六个核心模块:

3.1 文档采集与预处理层

这个层负责把纸质或数字文本变为结构化数据。对于圣典类文本,通常包含原文(Gurmukhi 文)、罗马化转写、英文翻译、注释解读等多个版本。预处理要做的是:

  • 去除 OCR 噪声、页码标记、脚注等无关信息。
  • 建立原文、翻译、注释之间的对齐关系。
  • 按章节、诗节、段落维护元数据。

3.2 文本分割与索引层

经典的 RAG 文本切片并不是“按固定字符数切”这么简单。对于诗体文本,切片需要尽量保持语义完整,避免把一句完整的诗句截断。切好的文本块会通过 Embedding 模型转换为向量,写入向量数据库,同时保留原文 ID、章节号等字段,用于后续引用。

3.3 检索层

用户提问后,系统先把问题转为向量,从向量数据库召回 Top-K 相关文本块。这里通常会结合两种检索方式:

  • 向量检索(语义召回):适合“意思相近但用词不同”的问题。
  • 关键词检索(BM25):适合“包含特定人名、术语、编号”的精确查询。

两者结果做融合,再经过一个重排序模型(Rerank),把最相关的文本块排在前面。

3.4 上下文组装层

召回结果并非全部塞进 Prompt。需要做去重、过滤、按相关性排序、截断,控制在模型上下文窗口内。Prompt 中还要加入系统指令,比如“只基于提供的文本回答,不要使用外部知识”“如果文本不足以回答问题,请明确说明不知道”。

3.5 生成与引用层

大模型基于限定上下文生成回答,同时输出引用标记,例如 [1]、[2]。引用标记对应知识库中的文本块 ID、章节号、原文片段。前端展示时,用户可以点击引用看到完整原文。

3.6 反馈与评测层

每次用户提问、回答、引用、点赞/点踩数据都需要记录,形成评测集。定期用评测集评估检索命中率和回答准确率,迭代优化。

下面是这个架构的简化流程图(文字描述版):

用户问题 ↓ 问题规范化(多语言翻译、拼写纠正) ↓ 混合检索(向量召回 + 关键词召回) ↓ 重排序(Rerank) ↓ 上下文组装(去重、截断、构造 Prompt) ↓ 大模型生成回答 + 引用标记 ↓ 校验与展示(引用可点击、来源可回溯)

相比通用 RAG,这个流程多出来的关键环节是多语言处理、重排序、引用校验、反馈闭环。通用 RAG 可以跳过其中很多步骤,但严肃知识问答不能跳。

4. 数据准备与文本处理实践

前面讲过数据是这个项目的命门。现在用代码演示一套最核心的数据处理流程。下面示例使用 Python,向量数据库选用轻量级的 Chroma,Embedding 模型可以替换为任意兼容 OpenAI API 的模型或本地模型。

4.1 原始文本结构

假设原始 JSON 结构如下:

{ "chapter": "Chapter 1", "verse": 1, "original_gurmukhi": "ikk oankaar sat naam karataa purakh nirbhau nirvair", "romanized": "Ik Onkar Sat Nam Karta Purakh Nirbhau Nirvair", "translation_en": "One Universal Creator God. The Name Is Truth. Creative Being Personified. No Fear. No Hatred.", "notes": "This verse establishes the fundamental doctrine of Ik Onkar..." }

4.2 文本切块逻辑

对于宗教经典这类文本,每节(verse)本身就是一个较完整的语义单位,不需要再按字符数切。但有些注释文本很长,需要进一步切分。一个可复用的策略是:优先按语义边界切分,而不是固定长度。

# 文件路径:data_preprocess/chunk_text.py import re from typing import List, Dict def split_into_sentences(text: str) -> List[str]: """按句子边界切分,保留句子完整性""" sentences = re.split(r'(?<=[.!?。!?])\s+', text.strip()) return [s for s in sentences if s] def build_chunks(verse_data: Dict, max_chars: int = 500) -> List[Dict]: """构建文本块:一个块优先包含一节完整诗歌。 如果该节过长,则按句子边界二次切分。 """ base = { "chapter": verse_data["chapter"], "verse": verse_data["verse"], "original_gurmukhi": verse_data["original_gurmukhi"], "romanized": verse_data["romanized"], } translation = verse_data.get("translation_en", "") notes = verse_data.get("notes", "") chunks = [] if len(translation) <= max_chars: chunks.append({ **base, "text": translation, "chunk_type": "translation", }) else: for i, sent in enumerate(split_into_sentences(translation)): chunks.append({ **base, "text": sent, "chunk_type": "translation_part", "part_index": i, }) if notes: if len(notes) <= max_chars: chunks.append({ **base, "text": notes, "chunk_type": "notes", }) else: for i, sent in enumerate(split_into_sentences(notes)): chunks.append({ **base, "text": sent, "chunk_type": "notes_part", "part_index": i, }) return chunks

这段代码的核心思想是:尽量保持文本块语义完整,每块都携带章节、诗节、原文等元数据,确保检索命中后可以回溯源。实际项目中,文本块数量可能很大,建议用 UUID 作为主键,并把章节号、诗节号设计为可过滤字段。

4.3 写入向量库

切好文本块后,需要把它们向量化并写入向量数据库。下面是一个最小可运行的写入示例。

# 文件路径:data_preprocess/index_verses.py from chromadb import Client from chromadb.config import Settings from data_preprocess.chunk_text import build_chunks import json # 初始化 Chroma 客户端 client = Client(Settings(persist_directory="./chroma_db")) collection = client.get_or_create_collection( name="sikhbani", metadata={"description": "Sri Guru Granth Sahib verses"} ) # 读取原始 JSON 数据 with open("data/verses.json", "r", encoding="utf-8") as f: verses = json.load(f) # 逐节构建 chunk 并写入 all_chunks = [] for verse in verses: all_chunks.extend(build_chunks(verse)) ids = [f"chunk_{i}" for i in range(len(all_chunks))] texts = [c["text"] for c in all_chunks] metadatas = [ { "chapter": c["chapter"], "verse": str(c["verse"]), "chunk_type": c["chunk_type"], "romanized": c.get("romanized", ""), } for c in all_chunks ] collection.add( ids=ids, documents=texts, metadatas=metadatas, ) print(f"已写入 {len(all_chunks)} 个文本块")

注意:上面的 Embedding 默认使用 Chroma 内置的默认嵌入函数,建议在生产环境替换为更专业的 Embedding 模型,并且要保证问题和文本块使用同一个 Embedding 模型,否则向量空间不一致,检索效果会大打折扣。

5. 检索增强生成(RAG)的实现思路

文本入库只是第一步。真正决定用户体验的是检索链路。下面给出一个混合检索 + 重排序 + 生成的最小实现。

5.1 混合检索:向量召回 + 关键词召回

纯向量检索对术语、人名、编号的精确匹配不敏感,比如用户直接问 “Chapter 5, Verse 3”,向量匹配不一定能精确命中。这时需要结合关键词检索。

# 文件路径:rag/hybrid_retriever.py from typing import List, Dict from chromadb import Client class HybridRetriever: def __init__(self, collection, top_k: int = 8): self.collection = collection self.top_k = top_k def retrieve(self, query: str) -> List[Dict]: """先向量检索,再结合关键词加权,返回 Top-K 文本块""" # 向量检索 vector_results = self.collection.query( query_texts=[query], n_results=self.top_k ) # 关键词检索(简易版,实际可用 BM25) keyword_matches = [] keywords = [k.strip().lower() for k in query.lower().replace(",", " ").split() if len(k.strip()) > 2] all_docs = self.collection.get() for idx, doc in enumerate(all_docs["documents"]): doc_lower = doc.lower() score = sum(1 for kw in keywords if kw in doc_lower) if score > 0: keyword_matches.append((idx, score)) # 融合(这里用简单的加权去重,生产环境建议用 RRF 或 RAGFusion) merged = {} for i, doc in enumerate(vector_results["documents"][0]): merged[i] = 1.0 for idx, score in keyword_matches: if idx in merged: merged[idx] += score * 0.5 else: merged[idx] = score * 0.5 sorted_items = sorted(merged.items(), key=lambda x: x[1], reverse=True) top_indices = [idx for idx, _ in sorted_items[:self.top_k]] return [ { "text": vector_results["documents"][0][idx], "metadata": vector_results["metadatas"][0][idx], } for idx in top_indices ]

真实项目中,推荐使用 Elasticsearch 的 BM25 与向量检索做 RRF(Reciprocal Rank Fusion)融合,效果更稳定。上面代码只是为了说明融合思路。

5.2 重排序(Rerank)

向量召回 Top-K 的结果虽然相关,但顺序不一定符合用户意图。重排序模型会基于“问题-文本块”的交互特征,重新计算相关度分数。生产中常用 BGE-Reranker、Cohere Rerank 等模型。

# 文件路径:rag/reranker.py from sentence_transformers import CrossEncoder class Reranker: def __init__(self, model_name: str = "BAAI/bge-reranker-base"): self.model = CrossEncoder(model_name) def rerank(self, query: str, candidates: List[Dict], top_n: int = 3) -> List[Dict]: pairs = [(query, c["text"]) for c in candidates] scores = self.model.predict(pairs) scored = list(zip(candidates, scores)) scored.sort(key=lambda x: x[1], reverse=True) return [c for c, _ in scored[:top_n]]

重排序的价值在于:它能在最终进入大模型的有限上下文里,尽量只放最相关的文本块,减少噪声对回答的干扰。如果预算有限,宁可减少向量召回的 Top-K,也不要省掉 Rerank。

5.3 Prompt 组装与生成

RAG 的 Prompt 设计比通用 Prompt 更严格。需要明确告诉模型:只能基于提供的文本回答;不能补充外部知识;如果文本不足以回答,要直接承认。这样可以显著减少幻觉和过度解读。

# 文件路径:rag/generate.py from openai import OpenAI SYSTEM_PROMPT = """你是一个严谨的宗教经典知识问答助手。 你只能基于用户问题下方提供的【知识库片段】回答。 规则: 1. 如果知识库片段包含答案,请用简洁准确的语言回答,并在句末标注引用来源编号,例如 [1][2]。 2. 如果知识库片段不包含答案,请明确回答“知识库中没有找到相关内容”,不要猜测,不要编造。 3. 不要使用外部知识补充回答,不要添加个人解读。 4. 回答语言请与用户问题语言保持一致。 """ def generate_answer(query: str, contexts: List[Dict], client: OpenAI, model: str = "gpt-4o-mini"): context_text = "\n\n".join( f"[{i+1}] {c['text']}" for i, c in enumerate(contexts) ) user_prompt = f"用户问题:{query}\n\n知识库片段:\n{context_text}\n\n请回答问题,并标注引用编号。" response = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ], temperature=0.2, ) return response.choices[0].message.content

把 temperature 设为 0.2 甚至更低,是为了减少生成随机性。在严肃知识问答场景中,创造力和发散不是目标,稳定和忠实才是。

6. 引用溯源与答案评估

回答生成完毕,引用溯源是最后一道防线。前面示例中让模型输出 [1][2] 这样的标记,但标记是否真的对应正确来源,还需要程序校验。

6.1 引用格式与校验

一种可靠做法是:让模型输出 JSON 结构,包含 answer 和 citations 两个字段。citations 数组里直接放文本块 ID。前端根据 ID 去查知识库,展示原文。这样引用就不能“造假”。

{ "answer": "Ik Onkar 是《Sri Guru Granth Sahib》开篇的重要表述,强调神的唯一性和遍在性。[1]", "citations": [ { "chunk_id": "chunk_103", "chapter": "Chapter 1", "verse": 1 } ] }

如果在最终输出时仍然用纯文本 [1] 标记,建议在后端做一个映射:把 [1] 替换为真实文本块链接。不要让模型直接生成 URL,因为模型生成的链接不可控。

6.2 评测:不能只靠“感觉回答不错”

RAG 系统的评测要分两层:

  • 检索层:用包含标准答案的评测集,计算 Recall@K,即前 K 个检索结果里是否包含能回答问题的文本块。
  • 生成层:用 LLM 作为裁判(LLM-as-a-Judge),从准确性、忠实度、完整性、引用正确性四个维度打分。

下面是生成层评测脚本的简化示例。先把检索命中作为前置条件,如果检索就没命中,生成分数再高也没有意义。

# 文件路径:evaluation/evaluate_rag.py def evaluate_answer(predicted: str, golden: str, contexts: list) -> dict: """简化版 RAG 答案评估,生产环境可用更细粒度指标""" from sentence_transformers import util import numpy as np # 计算语义相似度 model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") emb_pred = model.encode(predicted, normalize_embeddings=True) emb_golden = model.encode(golden, normalize_embeddings=True) semantic_similarity = float(util.cos_sim(emb_pred, emb_golden)[0][0]) # 检查回答是否提到了检索上下文中的关键信息(简化版:是否包含源文本中的关键词) key_terms = set(golden.split(" ")) & set(predicted.split(" ")) coverage = len(key_terms) / max(len(set(golden.split(" "))), 1) return { "semantic_similarity": round(semantic_similarity, 4), "keyword_coverage": round(coverage, 4), }

更严谨的做法是用 GPT-4 等强模型对回答逐项打分,并给出分数理由。评测集建议持续从用户真实问题中抽样补充,而不是只用人工编写的问题。

7. 常见问题与排查思路

垂直 RAG 应用的坑比通用 AI 应用多,很多问题表象相似,但根因完全不同。下面按排查优先级整理一张表:

问题现象可能原因排查方式解决方案
回答内容与原文不符检索未命中正确文本块直接查看检索 Top-K 结果,确认正确文本块是否在列优化切块粒度、召回数量或重排序模型
回答多次不一致生成温度过高或 Prompt 约束不足检查 temperature 配置和系统 Prompt 是否明确要求“只基于知识库片段”调低 temperature,增加确定性指令
用户问了知识库外的问题,AI 仍强行回答Prompt 没有设置“超出范围拒绝回答”指令检查 Prompt 中是否定义知识边界增加明确的拒绝规则和“不知道”提示
引用编号定位不到原文模型自由生成了编号改用 JSON 结构化输出,让引用从文本块元数据中生成后端根据文本块 ID 反查原始数据
多语言问题回答质量差Embedding 模型不支持目标语言检查用含旁遮普文、英文的测试集检索替换为多语言 Embedding 模型,或先用翻译管道统一语言
检索速度慢向量库未做索引或数据量过大查看向量库性能日志,检查查询耗时增加 HNSW 索引参数优化,或升级为生产级向量库
AI 回答带个人解读上下文中混入了注释类文本且未区分原文与注释检查 Prompt 是否区分“原始文本”和“学者解读”在文本块元数据中标注内容类型,Prompt 中明确优先级

这张表的核心方法论是:先查检索层,再查生成层。很多时候问题不在大模型,而在知识库里根本没有对应内容,或者检索没有把对应内容找出来。

8. 这类垂直领域 AI 应用的工程建议

Sikhbani.ai 只是垂直领域知识问答的一个例子,但它的工程经验可以迁移到企业文档问答、法律问答、医疗科普问答、学术文献问答等场景。下面几条建议来自这类项目的通用实践,值得在动手前认真读一遍。

8.1 数据治理优先于模型选型

在垂直问答项目中,数据质量的重要性超过模型能力。一个事实错误的数据文本块,用 GPT-4 生成和用开源模型生成,结果都是错误。项目启动时,第一优先级不是“选哪个大模型”,而是把语料来源、版本管理、更新流程定清楚。

建议为每个文本块维护来源字段、版本字段、更新时间和审核状态。这样即使发现知识库内容有误,也可以快速定位并下线对应文本块,而不是把整个索引重建一遍。

8.2 Prompt 要写“边界”,而不是只写“任务”

给 RAG 写系统 Prompt,最重要的是定义边界。边界包括:

  • 知识边界:哪些内容可答,哪些不可答。
  • 行为边界:不确定时怎么表达,禁止编造。
  • 引用边界:引用格式、编号规则、来源优先级。

把这些内容写进 Prompt,比反复强调“请提供准确答案”有效得多。准确不是靠愿望实现的,而是靠约束实现的。

8.3 记录每一次用户反馈

垂直问答系统的迭代依赖反馈闭环。每次问答都应当记录:

  • 用户问题。
  • 检索 Top-K 结果。
  • 最终进入上下文的文本块。
  • 模型回答。
  • 用户是否点赞/点踩。
  • 人工审核标注(可选)。

有了这些数据,才能回答“哪些问题检索不到”“哪些 Prompt 导致回答偏离”“哪些文本块经常被误引用”。没有反馈数据的 RAG 系统,优化只能靠感觉。

8.4 合规与内容安全是最低要求

涉及严肃知识内容的应用,尤其是宗教、法律、医疗类,要特别注意合规问题。建议:

  • 明确声明 AI 回答仅供参考,不构成最终权威解读。
  • 对关键回答设置人工审核机制,不能完全依赖模型。
  • 对用户输入做安全过滤,防止注入类攻击(例如用户试图通过 Prompt 注入让系统输出知识库外内容)。
  • 涉及用户数据时,遵循最小化收集原则。

这里的核心原则是:AI 可以辅助知识获取,但权威结论必须来自经过审核的来源。

8.5 从“单轮问答”逐步扩展到“多轮对话”

单轮问答是基础,多轮对话是进阶。多轮对话的难点在于:用户新问题有时依赖上文语境。工程上一般会把前几轮问答压缩成摘要或直接拼接进上下文,但要控制总长度,避免占用大量上下文窗口导致检索结果被截断。

建议先跑通单轮问答闭环,再逐步加入对话记忆。不要一上来就做复杂度最高的多轮交互。

9. 总结与后续学习方向

Sikhbani.ai 这样的项目给 AI 开发者最大的启发不是“宗教文本也能做 AI”,而是:当领域足够严肃、答案必须可验证时,AI 应用的技术重心会从“模型能力”转向“数据与工程能力”。

回到我们最初的问题——如何让 AI 准确回答“某句话到底什么意思”?答案是:不依赖模型记忆,而依赖检索、引用、评测这套系统工程。

如果你想进一步深入,可以从这几个方向入手:

  • 熟悉 RAG 的完整技术栈,包括向量数据库选型、Embedding 模型选型、混合检索与重排序。
  • 动手做一个自己的垂直问答项目,数据规模不用大,关键是跑通“数据清洗 → 切块 → 索引 → 检索 → 生成 → 引用 → 评测”的完整闭环。
  • 研究多语言 Embedding 与多语言评测,这对非英文知识库非常关键。
  • 关注大模型上下文窗口变长之后,RAG 是否会被“长文本直接塞入”替代。一个值得记住的判断是:上下文窗口再长,也无法解决引用精确性和知识更新成本问题,RAG 在可预见的未来仍然是严肃知识问答的主流架构。

最后提醒一句:如果要在生产环境搭建类似系统,先从最小业务闭环开始,用几百条高质量数据验证流程,再慢慢扩大知识库。很多项目失败不是因为模型不够强,而是因为知识库本身混乱、检索链路没打通、评测标准不明确。这三件事做好了,垂直知识问答应用的地基就稳了。

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

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

立即咨询