banner
约 7,200 字
24 分钟

RAG技术与调优实践

摘要

本文系统整理 RAG 项目中的知识库处理、高效召回、Rerank、双向改写、Small-to-Big、GraphRAG 和 Agentic RAG。文章以实际问答场景为线索,结合流程、代码逻辑、结果和方法对比,说明不同技术在工程开发中的适用条件和调优边界。

RAG技术与调优实践

写在前面

RAG 的基础流程是索引、检索、生成。这个流程容易搭出来,但很难稳定运行。生产问题通常不是模型不会回答,而是证据没有找到、证据过期、证据互相冲突,或者检索结果看起来相关但无法支撑最终答案。

因此,RAG 调优不能只调 top_k、Embedding 模型或提示词。更合理的工程视角是把系统拆成四层:知识库处理、高效召回、GraphRAG、Agentic RAG。知识库处理解决“有没有正确知识”,高效召回解决“能不能找对证据”,GraphRAG 解决“跨文档和多跳关系”,Agentic RAG 解决“复杂任务的过程控制”。

RAG 调优框架(图由AI辅助绘制)
RAG 调优框架(图由AI辅助绘制)

这篇文章按照个人工程笔记的方式整理。先给出 RAG 调优的整体框架,再分别讨论知识库处理、召回优化、GraphRAG 和 Agentic RAG,最后总结不同方法的选择顺序。

Native RAG 的问题

一个基础 RAG 系统一般包含三个阶段。

阶段

主要工作

常见问题

Indexing

文档解析、切片、Embedding、建索引

切片粒度不合理,知识过期,内容冲突

Retrieval

根据用户问题召回相关片段

问法不一致,关键词缺失,召回噪声大

Generation

拼接上下文并生成答案

引用不稳定,证据不足,模型补全幻觉

如果知识库本身缺失或者错误,后面的召回和生成都无法补救。如果召回结果不包含答案,模型只能猜。如果召回片段过多且噪声大,模型会被无关信息干扰。因此,RAG 调优应从知识库开始,再做召回,再考虑图谱和 Agent。

一、知识库处理

1.1 知识库问题生成与检索优化

基础做法是直接对知识切片建索引。问题在于,用户查询和原文表达可能不一致。例如原文写“创极速光轮位于明日世界主题园区”,用户问“如果我想体验最刺激的过山车,应该去哪个区域”。原文和问题之间存在语义跳转,单纯 BM25 或向量检索都可能漏召回。

这里使用一个景区问答场景说明问题。知识库内容围绕上海迪士尼,包括开园时间、地理位置、主题园区、游玩项目、门票和交通等信息。用户问题并不一定直接问“创极速光轮在哪里”,而可能问“想体验刺激项目应该去哪”。因此,检索目标不是简单匹配原文,而是让系统能覆盖用户的真实问法。

具体做法是对每个知识切片生成一组可能的问题,再分别建立“原文索引”和“问题索引”。查询时既可以匹配原文,也可以匹配这些预生成问题。这个方法本质上是 Doc2Query:把文档提前翻译成用户可能会问的问题。

核心逻辑如下。这里的代码不是完整工程代码,而是表达一个关键思想:原文和生成问题要分别建索引,但最终要通过 question_to_chunk 映射回原始知识切片。

Python
def generate_questions_for_chunk(chunk: str, llm) -> list[str]:
    prompt = f"""
    请根据下面的知识切片生成用户可能提出的问题。
    要求覆盖直接事实、比较问题、推理问题和场景化问题。

    知识切片:
    {chunk}
    """
    return llm.invoke(prompt).splitlines()


def build_dual_bm25_index(chunks: list[str]):
    original_index = BM25Okapi([tokenize(c) for c in chunks])

    generated_questions = []
    question_to_chunk = []
    for idx, chunk in enumerate(chunks):
        questions = generate_questions_for_chunk(chunk, llm)
        generated_questions.extend(questions)
        question_to_chunk.extend([idx] * len(questions))

    question_index = BM25Okapi([tokenize(q) for q in generated_questions])
    return original_index, question_index, question_to_chunk

在这个景区问答案例中,系统为“开园时间、主题园区、面积、项目”等内容生成多样化问题。测试查询数量为 3 个,原文 BM25 检索准确率为 66.7%,问题索引 BM25 检索准确率为 100.0%。其中,“如果我想体验最刺激的过山车,应该去哪个区域?”原文检索失败,问题索引检索成功。

问题生成后的检索结果对比
问题生成后的检索结果对比

这个方法适合内容稳定、用户问法多样的知识库,例如产品说明、政策制度、景区助手、内部知识库。它的风险是问题生成过度发散,导致无关查询被误召回。工程上建议把生成问题作为召回增强,不直接作为生成答案的依据。最终回答仍应回到原始知识切片。

1.2 对话知识沉淀

线上 RAG 系统会产生大量对话。对话中包含用户真实关心的问题,也包含临时需求和个性化信息。直接把对话全文写入知识库会带来噪声,因此需要先结构化抽取,再过滤、合并、审核。

仍以景区问答系统为例,用户在多轮对话中可能询问门票价格、营业时间、停车、地铁路线、餐饮限制和游玩建议。这些对话不是都值得入库。比如“我周六下午去合适吗”是一次性需求,“周末和节假日人流较多,建议提前购票并确认营业时间”则可以沉淀为通用知识。

因此,对话知识沉淀要先做结构化抽取。抽取字段可以包含知识类型、内容、置信度、来源、关键词、分类、对话摘要和用户意图。

JSON
{
  "knowledge_type": "事实 | 需求 | 问题 | 流程 | 注意",
  "content": "可沉淀的知识内容",
  "confidence": 0.95,
  "source": "conversation_id",
  "keywords": ["门票", "营业时间", "停车"],
  "category": "旅游服务",
  "conversation_summary": "本轮对话摘要",
  "user_intent": "用户真实意图"
}

处理流程可以拆成三步:先抽取,再过滤,再合并。下面的伪代码表达的是数据流,不是为了展示某个函数实现。

Python
def extract_knowledge_from_conversation(conversation):
    """从一段对话中抽取结构化知识点。"""
    return llm_extract(conversation)


def filter_reusable_knowledge(items):
    """过滤临时需求和一次性问题,只保留可复用知识。"""
    reusable_types = {"事实", "流程", "注意"}
    return [item for item in items if item["knowledge_type"] in reusable_types]


def merge_similar_knowledge(items):
    """按类型和主题聚类,使用 LLM 合并重复知识。"""
    groups = group_by_type_and_topic(items)
    return [llm_merge(group) for group in groups]

在这个对话沉淀场景中,3 段对话先抽取出 22 个知识点,过滤掉临时“需求”和“问题”后剩余 17 个,最终合并为 3 类知识:事实、流程、注意事项。这个结果说明,对话沉淀的关键不是把对话存进去,而是把对话压缩成稳定、可复用、可追踪的知识。

实际应用中要注意两点。第一,价格、时间、政策、医疗、金融等高风险信息不能直接从对话进入正式知识库,必须有来源校验。第二,对话沉淀适合补充“用户关心但原文没有显式写清楚”的内容,不适合替代正式文档。

1.3 知识库健康度检查

知识库健康度检查主要看三个维度:完整性、时效性、一致性。

完整性回答“用户常问的问题是否有知识覆盖”。时效性回答“价格、活动、时间、政策是否过期”。一致性回答“不同切片之间是否有冲突”。这三类问题比召回参数更基础,因为它们直接决定答案可信度。

检查逻辑可以写成固定流程。

Python
def check_knowledge_base_health(kb, test_queries, current_date):
    missing = check_missing_knowledge(kb, test_queries)
    outdated = check_outdated_knowledge(kb, current_date)
    conflicts = check_conflicting_knowledge(kb)

    return {
        "coverage_score": score_coverage(missing),
        "freshness_score": score_freshness(outdated),
        "consistency_score": score_consistency(conflicts),
        "missing_knowledge": missing,
        "outdated_knowledge": outdated,
        "conflicts": conflicts,
    }

在景区问答系统中,知识库健康度检查可以用一组固定测试问题触发。例如用户会问“有什么特别活动”“停车费是多少”“门票多少钱”“营业时间是什么”。这些问题可以暴露三类风险:知识没有覆盖、知识已经过期、不同切片说法冲突。

本次健康度检查给出整体评分 0.60,健康等级为“良好”,但仍发现 2 个缺失知识点、2 个过期知识点和 2 个冲突点。缺失知识包括“有什么特别活动”和“停车费是多少”;过期知识涉及价格和活动信息;冲突包括价格差异和时间不一致。

知识库健康度报告一
知识库健康度报告一

知识库健康度报告二
知识库健康度报告二

这个模块适合做成定期任务。每次知识库更新后,先跑健康度检查,再跑检索评估。否则上线后发现“答案不对”,很难判断是知识缺失、召回失败,还是生成阶段幻觉。

1.4 知识库版本管理与性能比较

知识库不是静态文件,而是系统的一部分。更新知识库也可能引入回归问题,因此需要版本管理。

一个可执行的版本管理模块应至少包含四个功能。

功能

作用

版本创建

保存版本名、描述、切片数量、平均长度、分类分布

哈希标识

使用 MD5 或内容哈希记录版本唯一性

版本比较

找出新增、删除、修改的知识切片

性能评估

在固定测试集上比较准确率、响应时间和回归通过率

核心逻辑如下。

Python
def compare_versions(v1, v2):
    v1_map = {item["id"]: item["content"] for item in v1}
    v2_map = {item["id"]: item["content"] for item in v2}

    added = set(v2_map) - set(v1_map)
    removed = set(v1_map) - set(v2_map)
    modified = [
        key for key in set(v1_map) & set(v2_map)
        if v1_map[key] != v2_map[key]
    ]

    return {"added": added, "removed": removed, "modified": modified}


def evaluate_version(version_name, test_queries):
    correct = 0
    total_time = 0

    for item in test_queries:
        chunks, latency = retrieve_relevant_chunks(item["query"], version_name)
        if item["expected_answer"] in " ".join(chunks):
            correct += 1
        total_time += latency

    return {
        "accuracy": correct / len(test_queries),
        "avg_response_time": total_time / len(test_queries),
    }

版本对比仍沿用景区问答场景。v1.0 是基础知识库,只包含位置、票价和营业时间 3 个切片。v2.0 是增强知识库,新增交通方式和特色项目,并补充票价、园区面积、主题园区和营业时间确认等信息。

评估结果显示,准确率从 60.0% 提升到 100.0%,平均响应时间从 115.8ms 增加到 120.2ms,回归测试通过率为 100.0%。

知识库版本性能比较
知识库版本性能比较

这个案例说明,版本调优不能只看准确率。准确率提升 40.0% 的同时,响应时间增加约 4.4ms,这是可接受的。如果准确率提升很小但延迟显著增加,就不一定值得上线。

二、高效召回

下面用一个制度问答场景展开高效召回。知识库内容来自银行客户经理管理考核办法,用户问题包括“客户经理被投诉一次扣多少分”“客户经理每年评聘申报时间是什么”。这类文档有明显特点:条款多、表述正式、关键词和数字很重要,因此不能只依赖语义相似度。

基础系统使用 DashScope Embedding 将制度文档切片向量化,使用 FAISS 建索引,再用 DeepSeek 模型根据召回片段回答问题。下面的代码只保留最小链路,表达“切片 -> 向量索引 -> LLM 回答”的基础结构。

Python
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.llms import Tongyi

embeddings = DashScopeEmbeddings(
    model="text-embedding-v1",
    dashscope_api_key=DASHSCOPE_API_KEY,
)

knowledge_base = FAISS.from_texts(chunks, embeddings)

llm = Tongyi(
    model_name="deepseek-v3",
    dashscope_api_key=DASHSCOPE_API_KEY,
)

基础系统可以回答明确问题。例如“客户经理被投诉了,投诉一次扣多少分”,系统能够召回制度条文,并回答“每投诉一次扣 2 分”。但基础向量检索在复杂问法、专业术语、短查询和多文档问题上仍然容易失效。

2.1 只增加 Top-K 不等于提升质量

最简单的方式是增加召回数量。

Python
docs = knowledge_base.similarity_search(query, k=10)

增加 k 可以提高召回覆盖率,但也会引入更多噪声。对于短文档或知识库规模较小的场景,这个方法可能有效;对于制度、合同、论文、手册等长文档,单纯增加 k 往往会让生成阶段更混乱。工程上应把增加 k 当作粗召回手段,而不是完整调优方案。

2.2 MultiQuery:用查询改写提升召回覆盖

MultiQuery 的思路是把用户问题改写成多个语义相近但表达不同的查询,从不同角度检索,再合并结果。它适合处理短查询、省略查询和口语化查询。

Python
def generate_multi_queries(query: str, llm, num_queries: int = 3) -> list[str]:
    prompt = f"""
    你是一个 AI 助手,负责生成多个不同视角的搜索查询。
    给定一个用户问题,生成 {num_queries} 个不同但相关的查询,
    以帮助检索更全面的信息。

    原始问题:{query}

    请直接输出 {num_queries} 个查询,每行一个,不要编号。
    """
    return parse_lines(llm.invoke(prompt))


def multi_query_search(query, retriever, llm):
    queries = [query] + generate_multi_queries(query, llm)
    docs = []
    for q in queries:
        docs.extend(retriever.search(q, k=4))
    return deduplicate(docs)

在制度问答场景中,原始问题是“客户经理被投诉了,投诉一次扣多少分”。这个问题虽然口语化,但文档原文可能写成“服务质量考核”“客户投诉”“每投诉一次扣 2 分”。为了提高命中率,系统生成了以下查询变体:

纯文本
客户经理被投诉了,投诉一次扣多少分
客户经理投诉扣分标准是什么
银行客户经理被投诉一次会扣除多少绩效分
金融机构客户经理投诉处罚机制

最终找到 4 个相关文档,并定位到制度中“有客户投诉的,每投诉一次扣 2 分”。这个例子说明,MultiQuery 的价值在于扩大表达覆盖,而不是改变问题本身。

实际使用时要控制两个风险。第一,改写不能偏离原问题,例如把“投诉扣分”扩展成“客户满意度评价体系”就会引入噪声。第二,多查询结果必须去重,否则相同片段会在上下文中重复出现,占用窗口。

2.3 索引扩展:离散索引、连续索引与混合索引

索引扩展解决的是“单一路径不稳定”的问题。在制度、技术文档和企业知识库中,单纯向量检索经常漏掉条款号、编号、专有名词;单纯关键词检索又不理解同义表达。因此索引扩展可以分为三类。

类型

方法

适用场景

离散索引扩展

关键词抽取、实体识别、BM25

专有名词、编号、制度条文、精确匹配

连续索引扩展

多种 Embedding 模型多路召回

语义丰富、表达差异大、跨语言或多领域

混合索引召回

BM25 + Vector + 融合排序

大多数工程场景的默认选择

BM25 是 TF-IDF 的改进版本,通过词频饱和和文档长度归一化计算相关性。它不理解语义,但对关键词、编号、人名、机构名、条款号非常敏感。向量检索理解语义,但可能漏掉精确关键词。两者互补。

混合召回与 Rerank 流程(图由AI辅助绘制)
混合召回与 Rerank 流程(图由AI辅助绘制)

2.4 Hybrid Search:BM25 + Vector

混合检索的核心流程是:用户查询进入系统,同时执行 BM25 和向量检索;将两种分数归一化到 [0, 1];再按权重融合;最后按融合分数排序返回 Top-K。

Python
def hybrid_search(query: str, alpha: float = 0.5, k: int = 5):
    tokenized_query = tokenize_chinese(query)
    bm25_scores = bm25.get_scores(tokenized_query)
    max_bm25 = max(bm25_scores) if max(bm25_scores) > 0 else 1
    bm25_scores = [score / max_bm25 for score in bm25_scores]

    vector_results = vectorstore.similarity_search_with_score(
        query,
        k=len(chunks),
    )

    max_distance = max(distance for _, distance in vector_results)
    vector_scores = {}
    for doc, distance in vector_results:
        idx = doc.metadata["chunk_id"]
        vector_scores[idx] = 1 - (distance / max_distance)

    combined = []
    for idx in range(len(chunks)):
        score = alpha * vector_scores.get(idx, 0) + (1 - alpha) * bm25_scores[idx]
        combined.append((idx, score))

    return sorted(combined, key=lambda x: x[1], reverse=True)[:k]

alpha 决定检索倾向。

alpha

检索倾向

适用场景

0.0

纯 BM25

专业术语、编号、精确查找

0.3

偏关键词

技术文档、法规条文、制度检索

0.5

平衡

通用默认值

0.7

偏语义

口语化问答、模糊查询

1.0

纯向量

同义表达丰富、语义匹配为主

在制度、合同、技术规范中,我倾向从 alpha = 0.30.5 开始。因为这些文档中的编号、术语和实体往往是关键证据。纯向量检索有时会找出语义相近但条款不对应的内容。

2.5 Rerank:粗召回之后再精排

Hybrid Search 解决召回覆盖问题,但 Top-K 中仍可能有噪声。Rerank 的作用是对候选片段重新排序,提高最终上下文的相关性。

Rerank 模型通常采用 Cross-Encoder 结构,把 (Query, Document) 成对输入模型,直接输出相关性分数。相比向量检索,它的交互更充分,排序更准,但计算成本更高。

模型

部署方式

优势

适用场景

BGE-Rerank

本地部署

开源、中文任务友好、数据不出本地

私有知识库、垂直领域

Cohere Rerank

API 调用

接入简单、多语言效果稳定

快速集成、云端应用

BGE-Rerank 的核心用法如下。

Python
from transformers import AutoModelForSequenceClassification, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-reranker-base")
model = AutoModelForSequenceClassification.from_pretrained("BAAI/bge-reranker-base")
model.eval()

pairs = [
    [query, doc.page_content]
    for doc in candidate_docs
]

inputs = tokenizer(
    pairs,
    padding=True,
    truncation=True,
    max_length=512,
    return_tensors="pt",
)

scores = model(**inputs).logits.squeeze(-1).tolist()

完整流程一般是:先用 MultiQuery + Hybrid Search 粗召回 10 到 20 个候选;去重;再用 Rerank 精排;最终保留 3 到 5 个片段。

Python
def hybrid_multi_query_search_with_rerank(
    query,
    hybrid_retriever,
    reranker,
    llm,
    initial_k=10,
    final_k=4,
):
    queries = [query] + generate_multi_queries(query, llm)

    candidate_docs = []
    for q in queries:
        docs = hybrid_retriever.search(q, k=initial_k)
        candidate_docs.extend(docs)

    candidate_docs = deduplicate(candidate_docs)
    reranked_docs = reranker.rerank(query, candidate_docs, top_k=final_k)
    return reranked_docs

参数建议如下。

参数

含义

建议

initial_k

粗召回候选数量

10-20,越多越全,但越慢

final_k

最终返回数量

3-5,太多会引入噪声

max_length

Rerank 输入长度

512 起步,长文档需分段

alpha

混合检索权重

0.5 起步,再按评估集调

Rerank 最常见的问题是成本和截断。候选数量太多会明显增加延迟;文档太长会被截断,导致真正相关的证据没有进入模型视野。因此,Rerank 前应先做好切片、去重和候选控制。

2.6 双向改写:Query2Doc 与 Doc2Query

双向改写用于缓解短文本向量化效果差的问题。

Query2Doc 是把用户查询扩展成一段“假想文档”。例如用户问“如何提高深度学习模型的训练效率”,系统可以扩展为包含 AdamW、混合精度训练、分布式训练、数据预处理、学习率调度等内容的短文档。这个扩展文本比原始短查询更适合做语义检索。

Python
def query_to_doc(query: str, llm) -> str:
    prompt = f"""
    请把下面的短查询改写成一段用于检索的技术说明。
    要求保留原始意图,补充可能相关的术语和上下文。

    查询:{query}
    """
    return llm.invoke(prompt)

Doc2Query 是为文档生成可能的用户问题。前面知识库问题生成就是 Doc2Query 的应用。它适合文档表达正式、用户表达口语化的场景。

Python
def doc_to_query(chunk: str, llm) -> list[str]:
    prompt = f"""
    请根据文档内容生成用户可能提出的问题。
    问题应覆盖事实、方法、比较、原因和场景化使用。

    文档内容:{chunk}
    """
    return parse_lines(llm.invoke(prompt))

两者的区别如下。

方法

改写对象

解决问题

风险

Query2Doc

用户查询

查询太短、缺少上下文

LLM 补充内容可能偏离意图

Doc2Query

文档切片

用户问法和文档表达不一致

生成问题过多会扩大噪声

实际选择上,如果用户问题普遍很短,优先 Query2Doc;如果文档稳定且用户问法多样,优先 Doc2Query;如果是高价值知识库,可以同时使用,但必须有评估集验证。

2.7 Small-to-Big

Small-to-Big 的核心思想是“小块检索,大块生成”。系统先用摘要、关键句、段落等小粒度内容建立索引。检索命中小块后,再通过 parent_id 找到对应的大块上下文,提供给生成模型。

这个方法适合长文档、多文档和论文问答。直接用整篇文档做向量检索太粗,用过小切片生成答案又缺上下文。Small-to-Big 用小块提高定位能力,用大块保证答案连贯性。

Python
small_index_items = [
    {
        "small_text": "Transformer 通过自注意力机制实现高效并行计算。",
        "parent_id": "paper_001_section_3",
    },
    {
        "small_text": "混合精度训练可以减少显存占用并提升训练速度。",
        "parent_id": "paper_002_section_4",
    },
]

parent_chunks = {
    "paper_001_section_3": "完整章节内容……",
    "paper_002_section_4": "完整章节内容……",
}


def small_to_big_retrieve(query):
    small_hits = small_vectorstore.search(query, k=5)
    parent_ids = deduplicate([hit.metadata["parent_id"] for hit in small_hits])
    return [parent_chunks[parent_id] for parent_id in parent_ids]

Small-to-Big 的关键是建立稳定映射关系。小块不能脱离父块,父块也不能太大。父块过大会重新引入上下文噪声;父块过小则失去补充上下文的意义。工程上可以从“句子或段落作为 small chunk,章节或相邻段落窗口作为 big chunk”开始。

2.8 高效召回方法选择

不同方法解决的问题不同,不应混用为一个大而全流程。

问题

优先方法

用户问题太短

Query2Doc、MultiQuery

用户问法和文档表达差异大

Doc2Query、问题索引

专有名词、编号、条款号重要

BM25、关键词索引、实体索引

同义表达多

向量检索、多 Embedding 召回

Top-K 噪声大

Rerank、阈值过滤

长文档定位难

Small-to-Big

多跳关系或跨文档归纳

GraphRAG、Agentic RAG

我的工程判断是,通用 RAG 的默认组合可以是:Hybrid Search + Rerank + 固定评估集。只有当这条链路稳定后,再引入 Doc2Query、Small-to-Big、GraphRAG 或 Agent。

三、GraphRAG

普通 RAG 依赖文本片段相似度。它适合回答局部事实问题,但不擅长回答“整体主题是什么”“不同实体之间有什么关系”“多个文档如何共同支持一个结论”这类问题。

GraphRAG 的核心思路是把非结构化文本转换为结构化图谱。索引阶段先把原始文档切成 TextUnits,再用 LLM 抽取实体、关系和主张,然后进行实体合并、社区发现和社区摘要。查询阶段根据问题类型选择全局搜索或局部搜索。

GraphRAG 工作流
GraphRAG 工作流

GraphRAG 的基本流程可以概括如下。

GraphRAG 查询模式(图由AI辅助绘制)
GraphRAG 查询模式(图由AI辅助绘制)

3.1 GraphRAG 索引流程

GraphRAG 索引阶段较重,主要包括六步。

步骤

内容

文档切片

将输入文档转换为 TextUnits

图谱抽取

从 TextUnits 中抽取实体、关系、主张

图谱增强

合并同名实体,进行社区检测和图嵌入

社区总结

为每个社区生成摘要

文档处理

建立文档表、文本块表、实体表、关系表

可视化

使用 UMAP 等方法生成图谱可视化

如果把 GraphRAG 用在《三国演义》或人物关系类文本中,任务就不是简单检索某一句话,而是回答“和曹操相关的人物有哪些”“关羽战胜过哪些武将”这类关系型问题。普通 RAG 可能召回几个片段,但很难稳定组织实体和关系。GraphRAG 的优势在这里更明显。

使用 Microsoft GraphRAG 时,命令行流程如下。

bash
git clone https://github.com/microsoft/graphrag.git
cd graphrag
pip install -e .
graphrag init --root .

初始化后会生成 .envsettings.yamlprompts。其中 .env 配置模型 API Key,settings.yaml 配置 LLM、Embedding、输入目录、输出目录、缓存目录、向量存储和查询设置。将待检索文档放入 input 后,执行索引。

bash
graphrag index --root .

索引完成后,可以分别执行 global 和 local 查询。

bash
graphrag query --root . --method global --query "和曹操相关的人物都有哪些?"
graphrag query --root . --method local --query "关羽战胜过哪些武将?"

GraphRAG 的查询模式分为 Global Search 和 Local Search。

维度

Global Search

Local Search

适用问题

全局主题、跨文档归纳、整体趋势

具体实体、关系、局部事实

数据来源

社区报告为主

实体、关系、原文 TextUnits、社区报告

查询方式

Map-Reduce 汇总多个社区报告

从相关实体扩展邻居和原文证据

成本

高,需要多次 LLM 调用

较高,但通常低于 Global

风险

容易生成宏观但空泛的答案

依赖实体抽取和关系质量

Global Search 适合回答“数据集的主要主题是什么”这类问题。它会对社区报告进行 Map,然后 Reduce 成最终上下文。Local Search 适合回答“某个实体有什么关系”这类问题。它从查询中识别相关实体,再找实体邻居、关系、原始文本和社区摘要。

GraphRAG 的价值在于结构化理解,而不是替代普通 RAG。对于 FAQ 和单文档事实问答,GraphRAG 的成本通常不划算。对于跨文档归纳、人物关系、事件脉络、研究主题演化等问题,GraphRAG 才有明显优势。

3.3 GraphRAG 参数调优

GraphRAG 的效果很依赖查询参数。下面几个参数会直接影响实体召回、关系扩展、原文证据和社区摘要在上下文中的比例。

参数

作用

调优方向

GRAPHRAG_LOCAL_SEARCH_TOP_K_ENTITIES

Local Search 检索相关实体数

实体召回不足时增大

GRAPHRAG_LOCAL_SEARCH_TOP_K_RELATIONSHIPS

引入上下文的关系数量

关系链断裂时增大

GRAPHRAG_LOCAL_SEARCH_TEXT_UNIT_PROP

上下文窗口中文本块占比

需要更多原文证据时增大

GRAPHRAG_LOCAL_SEARCH_COMMUNITY_PROP

社区报告占比

需要背景摘要时增大

GRAPHRAG_GLOBAL_SEARCH_MAX_TOKENS

Global Search 上下文上限

根据模型窗口调整

GRAPHRAG_GLOBAL_SEARCH_CONCURRENCY

Global Search 并发数

在成本和速度之间权衡

这些参数本质上控制“图结构”和“原文证据”的比例。Local Search 中,如果答案缺少事实依据,应增加 TextUnits 占比;如果答案缺少背景理解,可以增加社区报告占比;如果实体关系召回不足,应提高实体和关系的 Top-K。

四、Agentic RAG

Agentic RAG 是把 RAG 放进 Agent 工作流中。普通 RAG 是一次检索和一次生成;Agentic RAG 可以根据任务动态决定是否检索、检索哪个工具、是否并行阅读多个文档、是否继续追问、是否验证证据。

在工程上,Agentic RAG 可以拆成三个层次。

层次

能力

适用问题

Level 1

基础 RAG,一次检索后回答

单文档事实问答

Level 2

并行阅读,多路检索后汇总

多文档比较、批量阅读

Level 3

多跳推理,动态规划检索步骤

复杂关系、分步推理、证据链问题

Agentic RAG 的关键不是让模型自由发挥,而是给它明确的工具边界和验证机制。系统需要规定什么时候检索、最多检索几次、答案必须引用哪些证据、证据不足时如何返回。否则 Agent 会把 RAG 的不确定性放大。

一个合理的 Agentic RAG 控制流程如下。

Python
def agentic_rag(question):
    plan = planner(question)

    evidence = []
    for sub_query in plan.sub_queries:
        docs = retriever.search(sub_query)
        ranked_docs = reranker.rerank(sub_query, docs)
        evidence.extend(ranked_docs)

    if not evidence_is_enough(question, evidence):
        return "当前知识库证据不足,无法可靠回答。"

    answer = generator(question, evidence)
    return verify_answer_with_evidence(answer, evidence)

这个流程比普通 RAG 成本更高,但适合复杂问题。普通事实问答不需要 Agent,多跳问题才值得引入。

五、工程落地顺序

RAG 项目调优建议按以下顺序推进。

顺序

工作

目标

1

构建基础 RAG

跑通解析、切片、索引、检索、生成

2

建立测试集

固定高频问题、边界问题、失败样例

3

知识库健康检查

处理缺失、过期、冲突知识

4

版本管理与回归

每次知识更新都可评估

5

Hybrid Search

同时覆盖关键词和语义召回

6

MultiQuery / Query2Doc / Doc2Query

处理问法差异和短查询问题

7

Rerank

降低 Top-K 噪声,提高证据质量

8

Small-to-Big

处理长文档定位和上下文不足

9

GraphRAG

处理跨文档、全局归纳和实体关系

10

Agentic RAG

处理多步任务和复杂推理

这个顺序的重点是先建立可评估的基础链路。没有测试集时,任何调优都无法判断是否有效。没有版本管理时,知识库越改越难维护。没有健康度检查时,很多召回和生成问题会被误判为模型能力问题。

小结

RAG 调优的核心不是堆组件,而是建立证据链路。知识库处理保证知识可靠,Hybrid Search 保证召回覆盖,Rerank 保证证据排序,双向改写解决表达不一致,Small-to-Big 解决长文档上下文,GraphRAG 解决结构化关系,Agentic RAG 解决复杂任务控制。

后续做 RAG 工程时,我会优先遵循三个原则。

第一,任何调优都必须有测试集和回归结果。第二,任何答案都应能追溯到原始证据,而不是生成问题或中间摘要。第三,复杂方法必须对应明确问题;如果基础 RAG + Hybrid Search + Rerank 已经足够,就不必引入 GraphRAG 或 Agent。

END