ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

垂直领域AI问答系统:RAG、知识库与引用溯源实践

2026/8/31 22:24:56 拓冰建站 浏览量
垂直领域AI问答系统:RAG、知识库与引用溯源实践 大约两年前我在一个技术社群里看到有人讨论一个很有意思的问题如果用户问的不是“今天天气怎么样”而是“某部经典中的某句话到底是什么意思”大模型能不能给一个准确、有出处、可验证的回答当时大多数人的判断是能给出语义相近的答案但很难保证引文准确。这个判断在今天依然成立只不过出现了一批专门针对“高严谨度知识领域”做的 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向量数据库选用轻量级的 ChromaEmbedding 模型可以替换为任意兼容 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( namesikhbani, metadata{description: Sri Guru Granth Sahib verses} ) # 读取原始 JSON 数据 with open(data/verses.json, r, encodingutf-8) as f: verses json.load(f) # 逐节构建 chunk 并写入 all_chunks [] for verse in verses: all_chunks.extend(build_chunks(verse)) ids [fchunk_{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( idsids, documentstexts, metadatasmetadatas, ) 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_resultsself.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(), keylambda x: x[1], reverseTrue) 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 与向量检索做 RRFReciprocal 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(keylambda x: x[1], reverseTrue) 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[{i1}] {c[text]} for i, c in enumerate(contexts) ) user_prompt f用户问题{query}\n\n知识库片段\n{context_text}\n\n请回答问题并标注引用编号。 response client.chat.completions.create( modelmodel, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_prompt}, ], temperature0.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 系统的评测要分两层检索层用包含标准答案的评测集计算 RecallK即前 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_embeddingsTrue) emb_golden model.encode(golden, normalize_embeddingsTrue) 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 在可预见的未来仍然是严肃知识问答的主流架构。最后提醒一句如果要在生产环境搭建类似系统先从最小业务闭环开始用几百条高质量数据验证流程再慢慢扩大知识库。很多项目失败不是因为模型不够强而是因为知识库本身混乱、检索链路没打通、评测标准不明确。这三件事做好了垂直知识问答应用的地基就稳了。