0%

一个RAG系统的技术选型与pipeline

基于原型验证的实验结论,本文档设计一套可落地的RAG部署方案,由于种种原因,系统依托平台不便透露,以下称 tRAG 平台。系统基于 tRAG 平台,整合私域Wiki/论坛数据与公开游戏 Wiki 数据,构建面向游戏策划领域的专业 RAG 系统。

项目总览

注:tRAG 是一款某司 RAG 服务平台

本项目是一个游戏策划领域的垂直知识库 RAG 系统,数据来源覆盖三大渠道:

  • 某私域Wiki,包含策划案、设计文档、复盘报告等资料,可通过 MCP 获取。
  • 某私域论坛,包含技术分享、方法论文章、GDC 翻译等资料,可通过 MCP 获取。
  • 游戏 Wiki / 公开平台,包含游戏分类数据库、各游戏 Wiki、专业评测等资料数据,可通过 MCP 获取。

本项目包含以下三个子系统:

  • GDDClaw 数据采集系统,负责三源数据的抓取
  • 数据清洗 & Chunk & Embedding 入库系统
  • RAG 查询服务系统,包含多路召回、精排等功能

如下图所示:

维度 原型系统(本地验证) 线上版本
向量数据库 Qdrant Local(嵌入式 SQLite) tRAG HybridCollection(倒排向量一体库,分布式)
Embedding 本地 BGE-M3 tRAG 平台托管 Embeddingpublic-bge-m3,GPU 集群)
Sparse 检索 Qdrant 内置稀疏向量 tRAG 倒排索引(Trecall-Olama,BM25 FullText)
Rerank 本地 bge-reranker-v2-m3 tRAG 平台 Rerank APIbge-reranker-v2-m3,GPU)
数据采集 手动下载文章 GDDClaw 自动化三源采集(私域Wiki MCP + 私域论坛 MCP + 爬虫)
数据规模 200 篇 → 1,656 Chunks 830 篇有效文档 → 8,164 Chunks(持续增量更新)
部署模式 单机 Python 脚本 tRAG 平台托管 + 定时任务调度
Query 预处理 意图识别 + 多 Query 改写 + 多轮对话 Query 重写

GDDClaw:三源数据采集子系统

GDDClaw(Game Design Document Crawler)负责从三个数据源持续采集游戏策划领域的文档数据。

私域 Wiki Claw:私域Wiki 数据采集

通过 私域Wiki MCP私域Wiki API 采集指定空间下的策划文档。

采集流程为:

    1. 配置目标空间列表 & 文档目录树
    1. getSpacePageTree(parentid) → 递归获取文档树
    1. getDocument(docid) → 获取文档 Markdown 内容
    1. metadata(docid) → 获取文档元数据(作者、时间、标签)
    1. 统一格式化 → 输出到数据格式层

调用API采集示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def fetch_doc_as_markdown(doc_url):
"""文档转 Markdown 任务流"""

# 1. 发起转换任务
task = api.submit_task(plugin="doc2md", url=doc_url)

# 2. 轮询直到成功
while not task.is_finished():
time.sleep(2)
task.update_status()

# 3. 返回结果 (含文件 ID 或 URL)
if task.status == "success":
return task.get_result()

raise Exception("Task failed")

增量更新策略类似于:

1
2
3
4
5
6
# Record last crawl timestamp per document
# On each crawl cycle, compare document modifyTime with last crawl time
def should_update(doc_meta: dict, last_crawl: dict) -> bool:
doc_modify_time = doc_meta["modifyTime"]
last_crawl_time = last_crawl.get(doc_meta["docid"], "1970-01-01")
return doc_modify_time > last_crawl_time

私域论坛 Claw:私域论坛知识库采集

通过 私域论坛 MCP 或 私域论坛 API 采集指定团队/分类下的文章。

采集流程

    1. 配置目标团队 ID / 文章分类
    1. 私域论坛 API 列表接口 → 获取文章列表(支持分页)
    1. 私域论坛 API 详情接口 → 获取文章 HTML/Markdown 内容
    1. tRAG 文档格式转换插件 → 统一转为 Markdown
    1. 统一格式化 → 输出到数据格式层

在处理私域Wiki私域论坛的业务差异时,主要体现在四个维度:首先,Wiki 采用原生 Markdown 格式,可通过 Wiki2md 插件直接转换,而论坛内容以 HTML 为主,需通过 docx2md 或 HTML 转译工具处理;其次,在权限管理上,Wiki 侧重空间级授权,论坛则细化到团队或文章级;最后,针对增量更新的识别,Wiki 依赖 modifyTime 字段,而论坛则通过文章更新时间戳进行校验。

Web Claw:公开游戏数据采集

采用 通用爬虫模板 + Agent 自主生成代码 的混合链路。

对于结构复杂的站点(如游戏 Wiki),采用 Agent 链路:

    1. 人工提供目标站点 URL + 期望数据结构描述
    1. Agent 分析站点 DOM 结构(通过 spider_url 获取 HTML)
    1. Agent 生成定制化爬虫代码(Python + BeautifulSoup/Scrapy)
    1. tRAG 代码执行插件运行爬虫代码
    1. 输出结构化数据 → 统一格式层

数据清洗 & Chunk & Embedding 入库

  • GDDClaw 原始数据 (Markdown + Meta)
  • Stage 1: 数据清洗 & 质量过滤
  • Stage 2: 分块策略(标题层级切分 / tRAG 语义切分)
  • Stage 3: tRAG HybridCollection 入库(自动 Embedding + 倒排索引)
  • tRAG 倒排向量一体库(线上可查询状态)

Stage 1:数据清洗 & 质量过滤

DataCleaner 是数据入库前的质检站环节。它通过流水线作业对抓取内容进行深度精炼。首先自动化降噪,剔除导航和页脚等无关模板;其次利用 SimHash 算法进行跨源去重,确保 Wiki 与论坛的重复内容不被反复记录;最后通过长度与语种双重过滤,屏蔽低质短文与无效字符,确保只有高纯度的知识被沉淀进系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class DataCleaner:
"""
清洗并过滤原始抓取的文档。
"""

def clean(self, doc: dict) -> dict:
content = doc["content_md"]

# 1. 移除导航栏、目录(TOC)及页脚等模板内容
content = self.remove_boilerplate(content)

# 2. 标准化空白字符及排版格式
content = self.normalize_formatting(content)

# 3. 内容去重(跨源查重)
if self.is_duplicate(doc):
return None

# 4. 质量过滤:限制最小正文长度
if len(content.strip()) < 200:
doc["quality"] = "low"
return doc

# 5. 语种检测(过滤非中、英文内容)
if not self.is_valid_language(content):
return None

doc["content_md"] = content
doc["quality"] = "normal"
return doc

def is_duplicate(self, doc: dict) -> bool:
"""
利用 SimHash 或 MinHash 进行跨源去重。
同一篇文章可能同时存在于私域 Wiki 和私域论坛中。
"""
doc_hash = compute_simhash(doc["content_md"])
return doc_hash in self.seen_hashes

Stage 2:分块策略

我们提供两种分块路径,根据文档结构自动路由。分块(Chunking)是 RAG 系统中最容易被低估却影响深远的环节,分块粒度直接决定了检索精度与生成质量之间的平衡。切片太小,检索虽精准但上下文支离破碎,LLM 容易断章取义;切片太大,上下文完整但包含大量噪声,检索匹配度下降。本系统采用「双路径」策略,根据文档是否具有清晰的标题层级结构进行路由,兼顾两种场景的最优解:

路径 A:标题层级切分(结构化文档)

对于具有清晰标题层级(H1/H2/H3)的文档,按标题层级进行结构化切分:

1
2
3
4
5
6
Markdown 文档
├── 按 H1/H2/H3 标题层级递归切分
├── 每个 Chunk 保留完整标题链路(面包屑)
├── 过短的 Chunk 与相邻 Chunk 合并(minLength=256)
├── 过长的 Chunk 进一步按段落/句子切分(maxLength=1024)
└── 每个 Chunk 附加元数据(来源、分类、领域标签)

适用场景:私域Wiki 策划案、私域论坛 技术文章等具有清晰标题结构的文档。

路径 B:tRAG 语义切分(非结构化文档)

对于缺乏清晰标题层级的文档,使用 tRAG 平台的语义切分 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def semantic_split(text: str, is_markdown: bool = True) -> list:
"""
调用 tRAG 语义分割 API 进行智能分片。
遵循语义边界进行切分,而非固定窗口大小。
"""
resp = requests.post(
"http://api.***.com/v1/trag/semantic/split",
headers={
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {TRAG_TOKEN}"
},
data=json.dumps({
"text": text,
"maxLength": 1024, # 分片最大长度
"minLength": 256, # 分片最小长度(触发合并)
"isMarkdown": is_markdown,
"enableEmbeddingTarget": True # 为每个分片生成多个检索目标(提升召回)
})
)
return resp.json()["data"]

tRAG 语义切分的技术原理

语义切分 (Semantic Chunking), 是一种基于 Embedding 相似度动态寻找断点的高级分块策略。其核心原理是:先将文本按句子级别切分,然后计算相邻句子之间的向量余弦距离。当相邻句子的语义差异超过设定阈值(通常取第 80 百分位数)时,在该位置进行截断。这种方法能确保每个 Chunk 在语义上是完整的,避免了固定长度切分导致的语义割裂。

tRAG 平台的语义切分 API 在此基础上做了进一步优化:

  • Markdown 结构感知:自动识别 Markdown 标题层级(H1/H2/H3)作为优先切分边界,标题天然是语义分界点
  • 语义连贯性保证:不会在句子中间切断,保证每个 Chunk 的语义完整性
  • 多粒度检索目标enableEmbeddingTarget=True 时,同一 chunk 可生成多个检索目标,提升不同粒度查询的召回率

分块路由逻辑

通过正则扫描标题层级(H1-H3)来判断文档属性:针对结构严谨的文档,采用标题敏感切分以保留原始逻辑架构;而对结构松散的文本,则自动切换至 tRAG 语义分割算法进行深层切分。这种“因材施教”的策略,确保了不同来源的文档都能转化为最具语义完整性的知识分片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def chunk_document(doc: dict) -> list:
"""
根据文档结构路由至相应的切分策略。
"""
if has_heading_structure(doc["content_md"]):
# 路径 A:基于标题层级的结构化切分
return heading_aware_chunk(doc)
else:
# 路径 B:针对非结构化文档的 tRAG 语义切分
chunks = semantic_split(doc["content_md"], is_markdown=True)
return format_chunks_with_metadata(doc, chunks)

def has_heading_structure(content: str) -> bool:
"""
检查文档是否具有清晰的标题层级 (H1/H2/H3)。
"""
import re
headings = re.findall(r'^#{1,3}\s+', content, re.MULTILINE)
# 至少包含 3 个标题则视为结构化文档
return len(headings) >= 3

Stage 3:tRAG HybridCollection 入库

创建 HybridCollection(一体库)

使用 tRAG 的倒排向量一体库,一个知识库同时支持向量检索和倒排检索,一方面利用 BGE-M3 模型 生成 1024 维向量,通过 HNSWLIB 引擎 实现高性能的语义向量检索;另一方面开启 BM25 全文索引 模式,以支持精确的关键词匹配。

文档导入

文档导入是将分块后的 Chunk 写入 HybridCollection 的过程。导入时,每个 Chunk 需要提供三类信息:

  1. 向量信息:通过 embeddingQuery 字段提供用于生成向量的文本,tRAG 后端自动调用 BGE-M3 GPU 集群完成编码(也可直接传入预计算的 vector
  2. 倒排信息:通过 invertedDocument.content 提供用于 BM25 全文检索的文本(不传时默认使用 doc 字段)
  3. 元数据信息:通过 docKeyValue 提供过滤字段值(如 source、category、chunk_type 等),用于检索时的 filterExpr 条件过滤

导入采用批量方式(每批最多 200 条),tRAG 后端会自动完成 Embedding 计算、HNSW 向量索引构建、BM25 倒排索引构建三个步骤。注意倒排索引构建需要一定时间,导入后立即检索可能检索不到数据,建议等待 1 分钟后再尝试。

增量更新策略

利用元数据过滤器(filterExpr),根据唯一的 doc_id 定向清除该文档在混合索引库中的所有陈旧切分片,随后将重新生成的切分片整体导入。

Embedding 入库:BGE-M3 模型与 HybridCollection 存储原理

BGE-M3 模型技术原理

本系统使用 BGE-M3(BAAI General Embedding - Multi-Functionality, Multi-Linguality, Multi-Granularity)作为 Embedding 模型,这是北京智源研究院(BAAI)开源的多功能文本嵌入模型。BGE-M3 的核心优势在于一个"模型,三种检索能力":

检索能力 原理 输出 本系统使用
Dense Retrieval(稠密检索) 将整段文本编码为一个 1024 维的稠密向量,通过余弦相似度计算语义距离 1024-dim float32 向量 ✅ 向量检索路
Sparse Retrieval(稀疏检索) 为文本中的每个 token 生成权重,类似学习到的 TF-IDF,输出稀疏向量 稀疏向量(token→weight) ✅ 辅助倒排检索
Multi-Vector Retrieval(多向量检索) 类似 ColBERT,为每个 token 生成独立向量,通过 MaxSim 机制计算细粒度匹配 N × 1024-dim 向量矩阵 ❌ 未使用(计算成本高)

在 tRAG 平台上,BGE-M3 以 public-bge-m3 的名称注册为公共 GPU 资源。导入文档时,只需在请求中指定 embeddingModel: "public-bge-m3",tRAG 后端会自动调用 GPU 集群完成向量编码,无需业务方自行部署模型。BGE-M3 支持最长 8192 tokens 的输入,远超传统模型的 512 token 限制,这对于游戏策划领域的长文档尤为重要。

HybridCollection 倒排向量一体库原理

tRAG 团队开发的 HybridCollection(倒排向量一体库) 是本系统的核心存储引擎。它将传统的向量数据库和倒排索引库合二为一,一个知识库同时支持两种检索模式:

BM25 全文检索 是经典的信息检索算法,它基于词频(TF)和逆文档频率(IDF)对查询词与文档的匹配度进行打分。相比纯向量检索,BM25 对专有名词(如 “ELO”、“TTK”)、游戏名称(如 “HADES”、“原神”)等精确术语的匹配能力更强。在我们的消融实验中,加入 BM25 倒排检索后 Recall@5 提升了 11.4%,这正是因为游戏策划领域存在大量专业缩写和游戏专有名词。

向量索引引擎 默认使用 FAISS_VECTOR(基于 IVF 聚类的近似最近邻搜索),也可选择 HNSWLIB(分层可导航小世界图算法,检索速度更快但占用更多内存)。HNSW 通过构建多层图结构,在高维向量空间中实现 O(log N) 复杂度的近似最近邻搜索。

Chunk 文本构造规范

采用结构化模板设计,为检索优化:

1
2
3
4
5
【{chunk_type_label}】{title}
来源:{source_platform} | {article_title}
{field_1}: {value_1}
{field_2}: {value_2}
...

设计意图

  1. 类型标签【案例】【方法论】)为倒排检索提供强信号词
  2. 来源字段建立 Chunk → 文章的溯源链路
  3. 结构化排列有利于 LLM 在生成阶段解析上下文

RAG 查询服务:多路召回 + 粗排 + 精排

查询 Pipeline

Stage 0:Query 预处理

在原型系统中,用户的 Query 被直接编码为向量进行检索。这种「一刀切」的方式存在三个问题:

  • (1)用户表达模糊或口语化,导致检索词与文档术语不匹配(如用户说「怎么让战斗更有打击感」,实际需要检索「反馈系统设计」「屏幕震动」「帧冻结」等专业概念);
  • (2)多轮对话中存在指代消解问题(如「那它的数值体系呢?」中的「它」指代上文的某个游戏);
  • (3)复杂查询包含多个子问题,单次检索无法覆盖所有相关文档。
    我们通过下面三层 Query 预处理逐一解决这些问题:

A. 意图识别

技术原理:tRAG 的意图识别 API(public-intent-v1)基于 LLM 的 few-shot 分类能力。系统将用户 Query 与预定义的意图列表(含意图名称和描述)一起发送给 LLM,由 LLM 判断用户 Query 最匹配的意图类别。每个意图的 description 字段支持提供示例和关键词,帮助 LLM 更准确地理解意图边界。意图识别的结果用于下游的检索策略路由,不同意图对应不同的 filterExpr 过滤条件和 limit 参数,避免无关类型的文档干扰检索结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def recognize_intent(query: str, history: list = None) -> str:
intention_list = [
{
"intention": "游戏设计方法论",
"description": "用户询问游戏设计的方法、流程、框架、原则等"
},
{
"intention": "具体游戏案例分析",
"description": "用户询问某个具体游戏的设计细节、做法、效果"
},
{
"intention": "数值公式与算法",
"description": "用户询问游戏数值设计的公式、算法、参数"
},
{
"intention": "设计对比分析",
"description": "用户要求对比多个游戏或多种设计方案"
},
{
"intention": "闲聊/无明显意图",
"description": "用户的问题与游戏策划无关"
}
]

resp = requests.post(
"http://api.***.com/v1/trag/retrieval/intent/recognition",
headers=HEADERS,
json={
"ragCode": RAG_CODE,
"namespaceCode": NS_CODE,
"model": "public-intent-v1",
"query": query,
"intentionList": intention_list,
"messages": history or []
}
)
return resp.json()["data"]["result"]

意图路由策略

意图 检索策略 filterExpr
游戏设计方法论 全库检索,偏好 method/framework/checklist 类型 chunk_type in ("method","framework","checklist")
具体游戏案例分析 全库检索,偏好 case/technique 类型 无过滤(游戏名由 Query 自然匹配)
数值公式与算法 全库检索,偏好 formula/algorithm/numerical_pattern chunk_type in ("formula","algorithm","numerical_pattern")
设计对比分析 全库检索,limit 加大(需要更多候选) 无过滤
闲聊/无明显意图 跳过检索,直接 LLM 回复

B. 多轮对话 Query 重写

技术原理:多轮对话 Query 重写解决的是对话场景中的指代消解信息补全问题。当用户在多轮对话中使用代词(「它」「这个」「那种」)或省略主语时,单独的当前 Query 缺乏足够的语义信息进行有效检索。

重写模型接收用户的历史对话记录(messages)和当前输入(question),通过 LLM 理解对话上下文,将当前输入改写为一个完整、独立、适合检索的查询。改写形式包括:指代消解(「它」→「HADES」)、信息补全(「数值体系」→「HADES 的数值成长体系」)、以及在无需改写时保持原文不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def rewrite_query(query: str, history: list) -> str:
"""
Example:
History: "HADES的战斗系统怎么设计的?" → "..."
Current: "那它的数值体系呢?"
Rewritten: "HADES的数值成长体系是怎么设计的?"
"""
resp = requests.post(
"http://api.***.com/v1/trag/retrieval/rewrite",
headers=HEADERS,
json={
"ragCode": RAG_CODE,
"namespaceCode": NS_CODE,
"model": "public-rewrite-v002",
"question": query,
"messages": history
}
)
return resp.json()["data"]["rewriteQuestion"]

C. 多 Query 改写

技术原理:多 Query 改写将一个复杂的用户查询拆解为多个适合检索的子查询。这解决了两个核心问题:

  1. 复杂查询分解:当用户提出对比类问题(如「HADES 和原神的战斗系统对比」),单次检索很难同时召回两个游戏的相关文档。拆解为「HADES 战斗系统设计」「原神战斗系统设计」「HADES 和原神战斗系统对比」三个子查询后,每个子查询独立检索,大幅提升召回覆盖率。

  2. 查询扩展与变换:将用户的口语化表达扩展为多种检索友好的表述。例如「北京哪好玩」会被扩展为「北京旅游景点推荐」「北京著名景点介绍」「北京旅游攻略和行程安排」,覆盖更多语义空间。

在我们的消融实验中,Multi-Query 改写带来了 NDCG@5 +3.2%Recall@5 +7.5% 的提升,尤其在 Medium 难度的跨文章聚合类查询上贡献最大(+4.8% NDCG@5)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def multi_query_rewrite(query: str) -> list:
"""
Example:
Input: "HADES和原神的战斗系统对比"
Output: ["HADES战斗系统设计", "原神战斗系统设计", "HADES和原神战斗系统对比"]
"""
resp = requests.post(
"http://api.***.com/v1/trag/retrieval/query/rewrite",
headers=HEADERS,
json={
"ragCode": RAG_CODE,
"namespaceCode": NS_CODE,
"model": "public-multi-query-rewrite-v1",
"question": query
}
)
data = resp.json()["data"]
return data.get("rewriteQueryList", [query])

Stage 1:多路召回

多路召回是 RAG 系统的核心检索环节,其设计哲学是「宁可多召回,不可漏召回」,因为后续的 Rerank 精排可以剔除不相关结果,但如果召回阶段就漏掉了正确答案,后续环节无论多强大也无法弥补。

本系统使用 tRAG HybridCollection 的一体化检索接口,一次 HTTP 请求同时触发两条并行的检索链路:

链路 A:Dense 向量检索(语义匹配)

  • 将用户 Query 通过 BGE-M3 编码为 1024 维稠密向量(tRAG 后端 GPU 自动完成)
  • 在 HNSW/FAISS 向量索引中执行近似最近邻搜索,返回余弦相似度最高的 Top-K 文档
  • 强项:擅长处理模糊查询、概念关联、同义词匹配(如搜「随机性」能找到「Roguelike」「程序化生成」)

链路 B:Sparse 倒排检索(关键词匹配)

  • 对用户 Query 进行中文分词,在 BM25 倒排索引中检索包含查询词的文档
  • BM25 算法基于词频(TF)和逆文档频率(IDF)打分,精确匹配关键词出现频率
  • 强项:擅长捕捉专有名词(「ELO」「TTK」「HADES」)、精确数字、专业缩写

两路结果由 tRAG 后端自动合并去重,每条结果标注来源(from: "vector"from: "inverted"),便于后续分析。这种的混合检索策略,在我们的实验中相比纯 Dense 检索提升了 Recall@5 +11.4%,因为游戏策划领域存在大量 BM25 擅长匹配的专业术语。

多 Query 聚合检索

对改写后的多个查询词分别执行混合检索,并采用最大得分法(Max Score)对不同路径下的重复文档进行去重和冲突解决,最终通过对全局结果的重排筛选,提取前 40 个高质量候选分片,为后续的精排环节提供多元且精准的上下文素材:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def multi_query_search(queries: list, limit_per_query: int = 15) -> list:
"""
针对每一个改写后的查询执行混合搜索,
随后对结果进行合并与去重。
"""
all_results = {}

for query in queries:
# 对每个查询执行混合搜索,暂时关闭精排以提高召回速度
results = hybrid_search(query, limit=limit_per_query, enable_rerank=False)
for r in results:
doc_id = r["docId"]
# 采用最大得分法去重:若文档已存在,则保留得分最高的那条记录
if doc_id not in all_results or r["score"] > all_results[doc_id]["score"]:
all_results[doc_id] = r

# 按得分从高到低排序,获取全局排名靠前的候选片段
merged = sorted(all_results.values(), key=lambda x: x["score"], reverse=True)

# 最终取前 40 个候选片段送往精排模块
return merged[:40]

Stage 2:精排 Rerank

精排(Reranking)是整个 Pipeline 中单一增益最大的环节,在我们的消融实验中,加入 Rerank 后 NDCG@5 提升了 10.6%,Recall@5 提升了 13.8%,远超其他任何单一组件的贡献。

为什么需要 Rerank? 召回阶段使用的是 Bi-Encoder 架构(BGE-M3):Query 和 Document 被独立编码为向量,然后通过余弦相似度计算匹配度。这种方式速度极快(可以预计算文档向量),但精度有限,因为 Query 和 Document 之间没有深度的交互式注意力计算。

Cross-Encoder 的工作原理:Rerank 阶段使用的 bge-reranker-v2-m3 是一个 Cross-Encoder 模型。与 Bi-Encoder 不同,Cross-Encoder 将 Query 和 Document 拼接在一起作为输入([CLS] Query [SEP] Document [SEP]),通过 Transformer 的全注意力机制让 Query 的每个 token 与 Document 的每个 token 进行深度交互,最终输出一个 0-1 之间的相关性分数。

对比维度 Bi-Encoder(召回阶段) Cross-Encoder(精排阶段)
编码方式 Query 和 Doc 独立编码 Query + Doc 拼接联合编码
注意力交互 无(各自编码后比较) 全注意力(深度交互)
计算复杂度 O(1)(向量预计算) O(n)(每对 Query-Doc 重新计算)
适用规模 全量数据库(万级~亿级) 召回后候选集(40-100 个)
精度 较低(粗排) 高(精排)

在 tRAG 平台上,bge-reranker-v2-m3 部署在 GPU 集群上(通过 venus-serving 提供服务),单次对 40 个候选文档的精排耗时约 856ms。

以下是对多路召回的候选结果进行 Cross-Encoder 精排的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def rerank_results(query: str, candidates: list, top_k: int = 5) -> list:
"""
调用精排 API(使用 bge-reranker-v2-m3 Cross-Encoder 模型)。

业务价值验证:
在原型系统测试中,引入 Rerank 环节带来了 NDCG@5 +10.6% 以及 Precision@5 +19.2% 的显著提升。
"""
# 提取所有候选片段的文本内容
documents = [c["doc"] for c in candidates]

# 脱敏处理:调用通用服务接口
resp = requests.post(
"https://api.your-service.com/v1/retrieval/rerank",
headers=HEADERS,
json={
"serviceCode": SERVICE_CODE,
"namespaceCode": NS_CODE,
"model": "bge-reranker-v2-m3", # 指定精排模型
"query": query, # 原始查询词
"documents": documents, # 待重排的文档列表
"timeout": 10000 # 生产环境设置 10s 超时限制
}
)

# 获取精排后的得分数据
rerank_scores = resp.json()["data"]

# 将精排得分回填至原始候选列表中
for score_item in rerank_scores:
idx = score_item["index"]
candidates[idx]["rerank_score"] = score_item["relevanceScore"]

# 按照精排得分(相关性评分)从高到低进行全局排序
ranked = sorted(candidates, key=lambda x: x.get("rerank_score", 0), reverse=True)

# 执行文档级去重(每篇文章最多保留 2 个切片),以保证搜索结果的多样性
final = article_level_dedup(ranked, max_per_article=2)

return final[:top_k]


def article_level_dedup(results: list, max_per_article: int = 2) -> list:
"""
结果多样性处理:限制同一篇源文章中出现的切片数量。

解决痛点:
解决原型阶段发现的 RAG01 类问题(即检索出的前 5 个分块全部来自同一篇文章,导致回答视角过于单一)。
"""
article_count = {}
deduped = []

for r in results:
# 获取切片对应的原始文档 ID
doc_id = r.get("metadata", {}).get("doc_id", "unknown")

# 统计该文章已出现的次数
article_count[doc_id] = article_count.get(doc_id, 0) + 1

# 仅在未超过单篇文章最大允许切片数时,将其加入最终结果集
if article_count[doc_id] <= max_per_article:
deduped.append(r)

return deduped

Stage 3:LLM 生成

检索完成后,精排后的 Top-K 结果连同用户 Query 一起构造 Prompt,发送给 LLM 生成最终回答。这一阶段的核心设计理念是「有据可查、结构化输出、诚实边界」:

  • 有据可查:每个参考资料都标注来源平台、文章标题、分类、类型、游戏标签等元数据,LLM 在回答中用《》标注引用来源
  • 结构化输出:要求 LLM 使用 Markdown 标题、表格、列表组织回答,涉及多个游戏时做对比分析
  • 诚实边界:检索内容不足时明确说明,而非编造内容(降低幻觉率)

LLM 可选用混元大模型或 GLM 等内部模型,通过流式输出(Streaming)降低用户感知延迟。在我们的评测中,LLM 生成的 Faithfulness(忠实度)达到 0.912,说明回答高度基于检索内容,幻觉率低。

Prompt 设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
SYSTEM_PROMPT = """你是一位资深游戏策划顾问,拥有 15 年以上的游戏设计经验。你的任务是基于检索到的专业资料,为游戏策划同学提供专业、有据可查的回答。

核心约束:
1. **优先引用检索内容**:回答必须基于提供的参考资料,使用《》标注来源文章标题
2. **结构化回答**:使用 Markdown 标题、表格、列表组织回答
3. **案例对比分析**:涉及多个游戏时,做对比分析而非简单罗列
4. **可落地建议**:给出具体的、可操作的设计建议
5. **诚实边界**:检索内容不足以回答时,明确说明"基于现有资料,以下方面信息有限"
6. **标注来源**:每个关键论点标注来自哪篇参考资料

回答格式要求:
- 使用 Markdown 格式
- 关键数据用表格呈现
- 设计建议用编号列表
- 回答长度 1500-3000 字"""


def build_user_prompt(query: str, results: list) -> str:
context_parts = []

for i, r in enumerate(results, 1):
meta = r.get("docKeyValue", {})

header = f"### 参考资料 {i}"
source_line = f"[来源] {meta.get('source', 'unknown')} | 《{meta.get('title', '未知')}》"
meta_line = (
f"[分类] {meta.get('category', '')} | "
f"[类型] {meta.get('chunk_type', '')} | "
f"[领域] {', '.join(meta.get('domains', []))}"
)

game_tags = meta.get("game_tags", [])
if game_tags:
meta_line += f" | [游戏] {', '.join(game_tags)}"

content = r["doc"]

context_parts.append(f"{header}\n{source_line}\n{meta_line}\n\n{content}")

context = "\n\n---\n\n".join(context_parts)

return f"""基于以下检索到的专业资料,回答用户的问题。

{context}

---

用户问题:{query}"""

系统架构与数据

系统整体架构:

最优检索架构:采用“混合检索 + 精排”方案,实现 NDCG@5 (1.099) 与 Precision@5 (0.775) 的最高性能表现。

精排性能增益:Rerank 环节将检索质量从纯向量的 0.993 提升至 1.099,为系统带来 10.6% 的端到端增长。

双路能力互补:Sparse 路确保了“ELO”等专有名词的精确命中,Dense 路则解决了“有趣”等抽象语义的理解难题。

分片检索效能:结构化 Chunk 策略极大增强了文本的可检索性,实现了 100% 的关键词召回率。

生成质量锚点:通过元数据增强提示词,显著提升了回答的可靠性,使得来源引用率达到 91.3%。

评估方法与实验结果

评估方法论

评测集构建

构建了覆盖三个难度等级的评测集,共 60 个测试查询

难度 数量 特征 示例
Easy 20 单一主题、精确术语、直接匹配 “HADES 的冲刺系统是怎么设计的?”
Medium 25 跨文章聚合、概念性查询、需要语义理解 “Roguelike 游戏的数值成长体系有哪些常见设计模式?”
Hard 15 对比分析、隐含意图、术语鸿沟 “怎么让战斗更有打击感?”(需理解→打击感设计→反馈系统)

标注规范

  • 每个查询标注 3-8 个相关 Chunk(多级相关性:3=高度相关,2=相关,1=部分相关,0=不相关)
  • 由 2 名游戏策划同学独立标注,Cohen’s Kappa 一致性系数 κ = 0.78(substantial agreement)
  • 总计标注 312 个 query-chunk 相关性对

评估指标体系

指标 全称 评估目标 计算方式
NDCG@5 Normalized Discounted Cumulative Gain 排序质量(Top-5 结果排得好不好) 考虑多级相关性 + 位置折损
Precision@5 Precision at 5 精确率(Top-5 中有多少相关) 相关结果数 / 5
Recall@5 Recall at 5 召回率(Top-5 覆盖了多少相关文档) 召回相关数 / 总相关数
Recall@10 Recall at 10 宽松召回率 召回相关数 / 总相关数
Hit Rate@5 Hit Rate at 5 命中率(Top-5 中至少有一个相关) 命中查询数 / 总查询数
MRR Mean Reciprocal Rank 首条命中排名 第一个相关结果排名倒数的均值
Faithfulness 生成忠实度(LLM 是否基于检索内容) RAGAS 框架自动评估
Answer Relevance 回答相关性 RAGAS 框架自动评估

评估工具

工具 用途 说明
自建评测脚本 检索质量指标 基于标注集计算 NDCG/Precision/Recall/MRR
RAGAS 框架 生成质量指标 Faithfulness + Answer Relevance + Context Precision
人工评估 端到端质量 3 名策划同学对 30 个查询的回答进行 1-5 分评分

检索 Pipeline 消融实验

在 60 个测试查询上,对不同检索配置进行消融实验:

配置 NDCG@5 Precision@5 Recall@5 Recall@10 Hit Rate@5 MRR
Dense Only (BGE-M3) 0.724 0.620 0.483 0.637 0.867 0.712
BM25 Only (倒排) 0.618 0.547 0.421 0.589 0.783 0.603
Hybrid (Dense + BM25) 0.761 0.660 0.538 0.702 0.917 0.748
Hybrid + Rerank 0.842 0.753 0.612 0.781 0.950 0.831
Hybrid + Rerank + Multi-Query 0.869 0.773 0.658 0.824 0.967 0.852
Hybrid + Rerank + Multi-Query + Intent Filter 0.881 0.787 0.671 0.836 0.967 0.864

消融实验关键发现

增量组件 NDCG@5 提升 Recall@5 提升 说明
+BM25 倒排(Dense→Hybrid) +5.1% +11.4% 精确术语匹配互补,如 “ELO”、“TTK” 等专业缩写
+Rerank(Hybrid→Hybrid+Rerank) +10.6% +13.8% 单一最大增益来源,Cross-Encoder 显著提升排序质量
+Multi-Query(→+MQ) +3.2% +7.5% 复杂查询分解后召回更多相关文档
+Intent Filter(→+IF) +1.4% +2.0% 意图路由缩小检索范围,减少噪声
全 Pipeline vs Dense Only +21.7% +38.9% 完整 Pipeline 相比基线的总提升

按查询难度的检索效果

难度 查询数 NDCG@5 Precision@5 Recall@5 Hit Rate@5 MRR
Easy 20 0.936 0.870 0.782 1.000 0.925
Medium 25 0.872 0.776 0.648 0.960 0.856
Hard 15 0.793 0.667 0.531 0.933 0.771
加权平均 60 0.881 0.787 0.671 0.967 0.864

难度分析

  • Easy 查询:精确术语直接匹配,Hit Rate 100%,NDCG@5 接近完美
  • Medium 查询:需要跨文章聚合,Multi-Query 改写贡献最大(+4.8% NDCG@5)
  • Hard 查询:存在术语鸿沟(如 “打击感” → “反馈系统”),Intent Filter + Multi-Query 联合提升 +6.2%

生成质量评估

使用 RAGAS 框架对 30 个查询的 LLM 生成结果进行自动评估:

指标 得分 行业基准(企业级 RAG) 评价
Faithfulness(忠实度) 0.912 0.75 - 0.95 ✅ 优秀,幻觉率低
Answer Relevance(回答相关性) 0.887 0.80 - 0.93 ✅ 良好
Context Precision(上下文精确度) 0.834 0.65 - 0.85 ✅ 良好
Context Recall(上下文召回率) 0.791 0.75 - 0.90 中等,Hard 查询拉低

典型 Bad Case 分析

Case ID 查询 问题类型 原因分析 改进措施
BC-01 “怎么做一个好的新手引导?” 召回不足 知识库中新手引导相关文档仅 3 篇 扩充数据源,增加 UX 设计类文档
BC-02 “对比暗黑4和流放之路2的装备系统” 部分召回 流放之路2 相关文档较少(仅 1 篇) Web Claw 增加 PoE2 Wiki 数据
BC-03 “TTK 怎么调?” 术语鸿沟 “TTK” 未被 Multi-Query 扩展为 “Time To Kill” 补充游戏术语同义词表
BC-04 “手游和端游的操作设计有什么区别?” 排序偏差 Top-5 中 4 个来自同一篇长文 文章级去重已解决(max_per_article=2)
BC-05 “怎么设计一个有趣的技能系统?” 语义模糊 “有趣” 过于主观,检索结果分散 Intent Filter 路由到 method/framework 类型

技术选型对比实验数据

Embedding 模型对比(本项目评测集,60 查询 × 8,164 Chunks)

模型 参数量 维度 NDCG@5 Recall@5 MRR 编码耗时 (8K chunks)
BGE-M3 (tRAG GPU) 568M 1024 0.842 0.612 0.831 45s
BGE-zh-large-v1.5 326M 1024 0.763 0.524 0.741 38s
M3E-large 110M 768 0.718 0.497 0.695 22s
Qwen3-Embedding-0.6B 0.6B 1024 0.798 0.571 0.782 67s

注:以上为 Hybrid (Dense + BM25) + Rerank 配置下的对比,仅替换 Embedding 模型。BGE-M3 在 Dense+BM25 混合检索场景下效果最优,且 tRAG 平台原生支持。

分块策略对比(BGE-M3 + Hybrid + Rerank 配置)

分块策略 Chunk 数 平均长度 NDCG@5 Recall@5 MRR
固定长度 512 tokens 9,847 478 0.791 0.563 0.774
递归字符分块 8,923 502 0.806 0.578 0.789
tRAG 语义切分 7,856 438 0.823 0.594 0.811
标题层级 + 语义切分(双路径) 8,164 455 0.842 0.612 0.831

注:双路径策略中,结构化文档(536 篇)用标题层级切分,非结构化文档(294 篇)用 tRAG 语义切分。结构化文档的标题层级切分 NDCG@5 达 0.871,显著优于语义切分的 0.823。

Rerank 模型对比

Rerank 模型 NDCG@5 Precision@5 延迟 (40 docs)
无 Rerank 0.761 0.660 0 ms
bge-reranker-v2-m3 (tRAG GPU) 0.842 0.753 856 ms
bge-reranker-v2-m3 (本地 CPU) 0.842 0.753 32,000 ms

总结

这次我们以Q&A的方式来做一下总结,回顾一下整个流程。

Q1:这个系统最终的效果评估是怎么做的?评估方法论是什么?

A1
我们建立了一套三层评估体系,覆盖检索质量、生成质量和端到端体验:

第一层:检索质量评估(离线)

  • 构建了 60 个测试查询的评测集,覆盖 Easy(20)/ Medium(25)/ Hard(15)三个难度等级
  • 每个查询由 2 名游戏策划同学独立标注 3-8 个相关 Chunk,采用多级相关性标注(0-3 分),Cohen’s Kappa 一致性系数 κ = 0.78(substantial agreement)
  • 总计标注 312 个 query-chunk 相关性对
  • 评估指标:NDCG@5、Precision@5、Recall@5/10、Hit Rate@5、MRR

第二层:生成质量评估(自动 + 人工)

  • 使用 RAGAS 框架对 30 个查询的 LLM 生成结果进行自动评估,覆盖 Faithfulness(忠实度 0.912)、Answer Relevance(0.887)、Context Precision(0.834)、Context Recall(0.791)
  • 3 名策划同学对 30 个查询进行 1-5 分人工评分,综合满意度 4.07/5

第三层:消融实验(组件贡献度)

  • 对 Pipeline 中的每个组件进行消融实验,量化每个组件的边际贡献。例如 Rerank 是单一最大增益来源(NDCG@5 +10.6%),BM25 倒排带来 Recall@5 +11.4%,Multi-Query 改写带来 NDCG@5 +3.2%

为什么这样设计:单一指标无法全面反映 RAG 系统质量。检索指标衡量「找得准不准」,生成指标衡量「答得好不好」,消融实验衡量「每个组件值不值」。三层结合才能指导后续优化方向。

Q2:为什么选 BGE-M3 而不是更新的 Qwen3-Embedding?Qwen3 在 C-MTEB 上分数更高。

A2
这是一个综合权衡的决策,不是单纯看 Benchmark 分数:

维度 BGE-M3 Qwen3-Embedding-8B
C-MTEB 总分 ~59.56 73.84(更高)
稀疏向量支持 ✅ 原生 Dense+Sparse ❌ 无
tRAG 平台集成 public-bge-m3 开箱即用 ❌ 需自建 GPU 服务
对切分策略鲁棒性 ✅ 妙问实测:定长/语义切分准确率几乎持平 中等
GPU 需求 ~2 GB (FP16) ~16 GB (FP16)

核心理由有三:

  1. 稀疏向量是关键:BGE-M3 一个模型同时输出 Dense + Sparse 两种表示,Sparse 表示对游戏专业术语(ELO、TTK、HADES)的精确匹配至关重要。Qwen3 没有稀疏向量,需要额外维护 BM25 索引
  2. 平台集成成本:BGE-M3 在 tRAG 平台上是公共 GPU 资源,零运维。Qwen3-8B 需要自建 GPU 服务,运维成本高
  3. 实战验证充分:BGE-M3 在我们的 4 个项目中都验证了可靠性,而 Qwen3 目前只有 Benchmark 数据

Q3:Rerank 为什么是单一最大增益来源?能不能不要 Rerank 直接用更好的 Embedding?

A3
这是由 Bi-Encoder 和 Cross-Encoder 的架构差异决定的,不是简单的「模型好坏」问题:

  • Bi-Encoder(召回阶段):Query 和 Document 被独立编码为向量,然后通过余弦相似度计算匹配度。优点是 Document 向量可以预计算,检索速度极快(毫秒级扫描万级数据)。缺点是 Query 和 Document 之间没有深度交互,精度有上限
  • Cross-Encoder(精排阶段):Query 和 Document 被拼接在一起输入 Transformer,通过全注意力机制让每个 token 深度交互,输出精确的相关性分数。精度远高于 Bi-Encoder,但每对 Query-Document 都需要重新计算,只能用于小规模候选集(40-100 个)

这是一个架构级别的精度差异,不是换一个更好的 Embedding 模型能弥补的。即使用 Qwen3-8B 做 Embedding,它仍然是 Bi-Encoder 架构,精度上限低于 Cross-Encoder。

数据支撑:在 T²-RAGBench 大规模基准测评中,Rerank 带来 Recall@5 +17.4%、MRR@3 +39.7%,是所有组件中增益最大的。我们自己的消融实验也验证了 NDCG@5 +10.6%。

Q4:系统还有哪些优化空间?

A4

方向 优化方案
术语同义词表 构建游戏术语同义词表,集成到 Multi-Query 改写
数据源扩展 增加 PoE2 Wiki、Baldur’s Gate 3 Wiki 等热门游戏数据
Context Recall 提升 增加 Parent-Child Chunk 策略,检索到子 Chunk 时同时返回父 Chunk 上下文
领域微调 Embedding 在游戏策划语料上微调 BGE-M3,提升领域术语的语义理解
Adaptive Chunking 参考 Ekimetrics 2026 论文,为每个文档自动选择最优分块策略
多模态支持 游戏策划文档中包含大量截图、流程图,当前系统仅处理文本
用户反馈闭环 收集用户对回答的 反馈,用于持续优化检索和生成质量
GraphRAG 构建游戏设计知识图谱(游戏→机制→设计模式),支持关系推理
Agent 化 从被动问答升级为主动辅助,如自动生成 GDD 模板、设计评审 Checklist
Embedding 模型升级 关注 tRAG 平台对 Qwen3-Embedding 的支持进度,适时切换

Q5:多路召回中 Dense 和 BM25 各自的贡献是什么?为什么不只用一种?

A5
两者的互补性可以用具体 Bad Case 说明:

查询 Dense 向量检索 BM25 倒排检索 说明
「ELO 匹配算法怎么实现?」 ❌ 召回了「匹配系统设计」「排位赛设计」等语义相关但不精确的结果 ✅ 精确匹配「ELO」关键词 BM25 对专有名词精确匹配更强
「怎么让战斗更有打击感?」 ✅ 语义理解「打击感」→ 召回「反馈系统设计」「屏幕震动」「帧冻结」 ❌ 「打击感」不是标准术语,BM25 匹配不到 Dense 对语义理解更强
「HADES 的 Roguelike 随机性设计」 ✅ 召回了 Roguelike 相关文档 ✅ 精确匹配「HADES」+「Roguelike」 两路互补,召回更全

消融实验数据

  • Dense Only:Recall@5 = 0.483
  • BM25 Only:Recall@5 = 0.421
  • Hybrid(Dense + BM25):Recall@5 = 0.538(+11.4% vs Dense Only)

在游戏策划领域,存在大量专业缩写(ELO、TTK、DPS)、游戏专有名词(HADES、原神)和技术术语(Roguelike、Metroidvania),BM25 对这类精确匹配的贡献不可替代。同时,策划同学的查询往往口语化(「怎么让战斗更爽」),Dense 的语义理解能力也不可或缺。