大约两年前,我在一个技术社群里看到有人讨论一个很有意思的问题:如果用户问的不是“今天天气怎么样”,而是“某部经典中的某句话到底是什么意思”,大模型能不能给一个准确、有出处、可验证的回答?
当时大多数人的判断是:能给出语义相近的答案,但很难保证引文准确。这个判断在今天依然成立,只不过出现了一批专门针对“高严谨度知识领域”做的 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 在可预见的未来仍然是严肃知识问答的主流架构。
最后提醒一句:如果要在生产环境搭建类似系统,先从最小业务闭环开始,用几百条高质量数据验证流程,再慢慢扩大知识库。很多项目失败不是因为模型不够强,而是因为知识库本身混乱、检索链路没打通、评测标准不明确。这三件事做好了,垂直知识问答应用的地基就稳了。