4.8 基于两阶段索引的 RAG Agent 搭建#

在上一节内容中,我们介绍了基于 Qwen3 Reranking 模型的原理及使用方法,并且对 Qwen3 Reranking 模型的实现代码也进行了简单的介绍。在本节内容中,我们将以第4.4节中介绍的 RAG Agent 为基础,来搭建一个包含有两阶段索引的 RAG Agent 。

4.8.1 两阶段索引实现#

因为整个 RAG 框架我们已经在第4.4节内容中介绍过了,所以这里只需要再实现两阶段索引部分的流程,并进行简单改造即可。具体地,对于重排序部分的逻辑实现如下:

 1 def text_rerank(query: str = None, docs: List[Document] = None, top_n=3):
 2     documents = [doc.page_content for doc in docs]
 3     resp = dashscope.TextReRank.call(model="qwen3-rerank", query=query,
 4         documents=documents, top_n=top_n, return_documents=False,
 5         instruct="Given a web search query, retrieve relevant passages that answer the query.")
 6     if resp.status_code == HTTPStatus.OK:
 7         results = resp.output.results
 8         filtered_info = [[item.index, item.relevance_score] for item in results]
 9         filtered_docs = [docs[item[0]] for item in filtered_info]
10         return filtered_docs, filtered_info
11     else:
12         return docs[:top_n], []

在上述代码中,第1行代码 docs 表示对于当前 query 来说从向量库中检索到的与之相关的候选文档,为一个列表。第2行取每个 Document 对象中的 page_content ,即文本内容。第3~5行是得到排序后的结果,并取前 top_n 个,详细内容参见第4.7.2节。第6~9行是过滤无用信息,仅返回前 top_nDocument 对象以及对应的索引和相似性评分。

4.8.2 检索工具改造#

进一步,需要对检索工具 retrieve_context 进行一定的改造,使得其能够对从向量库检索到的内容进行二次排序。具体地,实现过程如下:

 1 @tool
 2 def retrieve_context(query: str, config: RunnableConfig):
 3     vector_store = config['configurable'].get("vector_store")
 4     k = config['configurable'].get("k")
 5     top_n = config['configurable'].get("top_n")
 6     artifact = vector_store.similarity_search(query, k=k)
 7     filtered_doc, filter_info = text_rerank(query, artifact, top_n=top_n)
 8     print(f"从向量数据库检索到的{k}篇文档中,相关度前{top_n}个文档的来源为:")
 9     for i in range(top_n):
10         print(f"\t{artifact[i].metadata}")
11     print(f"\n经过排序模型处理后,相关度前{top_n}个文档的来源为:")
12     for i in range(top_n):
13         print(f"\t{filtered_doc[i].metadata}")
14     if len(filtered_doc) == 0:
15         print(f"text_rerank 执行失败!")
16     print(f"一共从向量库检索到{len(artifact)}篇文章,经过reranking模型处理后,返回{len(filtered_doc)}"
17           f"篇最相似的文档,相似性评分分别为: {filter_info}")
18     content = "\n\n".join(
19         (f"Source: {doc.metadata}\nContent: {doc.page_content}")
20         for doc in filtered_doc)
21     return content

在上述代码中,第7行则是重排序后的结果。第8~17行是作为验证的相关输出信息。第18~20行则是处理返回输入到模型中的参考内容,与之前一致。

4.8.3 输出结果分析#

此时,便可以运行上述代码得到类似如下结果:

# ================================ Human Message =================================
# 郭靖这个人物在这部小说当中是谁?它的出生背景是什么?
# ================================== Ai Message ==================================
# Tool Calls:
#   retrieve_context (call_a80494c707914126a3b383)
#  Call ID: call_a80494c707914126a3b383
#   Args:
#     query: 郭靖 出生背景 小说人物
# 从向量数据库检索到的20篇文档中,相关度前3个文档的来源为:
#   {'level_1': '第七回 比武招亲', 'pk': 465261515437506672, 'source': 'RAGWithMe/data/jinyong/金庸-射雕英雄传精校版.txt', 'start_index': 5796}
#   {'level_1': '第七回 比武招亲', 'pk': 465261515437506675, 'source': 'RAGWithMe/data/jinyong/金庸-射雕英雄传精校版.txt', 'start_index': 7025}
#   {'level_1': '第三十九回 是非善恶', 'pk': 465290802323521722, 'source': 'RAGWithMe/data/jinyong/金庸-射雕英雄传精校版.txt', 'start_index': 0}

# 经过排序模型处理后,相关度前3个文档的来源为:
#   {'level_1': '第三回 黄沙莽莽', 'pk': 464859367376683180, 'source': 'RAGWithMe/data/jinyong/金庸-射雕英雄传精校版.txt', 'start_index': 7398}
#   {'level_1': '第七回 比武招亲', 'pk': 465261515437506672, 'source': 'RAGWithMe/data/jinyong/金庸-射雕英雄传精校版.txt', 'start_index': 5796}
#   {'level_1': '第三十八回 锦囊密令', 'pk': 465290802323521715, 'source': 'RAGWithMe/data/jinyong/金庸-射雕英雄传精校版.txt', 'start_index': 15090}

# 一共从向量库检索到20篇文章,经过 reranking 模型处理后,返回3篇最相似的文档,相似性评分分别为: [[0, 0.8125869388341607], [17, 0.8091164423628996], [5, 0.7374832867736165]]
# ================================== Ai Message ==================================
# 郭靖是《射雕英雄传》中的主人公。他的母亲李萍是浙江临安人,父亲是山东好汉。郭靖出生于临安府牛家村,幼年时因战乱随母亲流落蒙古大漠。母亲李萍含辛茹苦抚养他,在大漠中以畜牧为生,并依丈夫遗言为其取名“郭靖”。他四岁才会说话,性格憨厚朴实,六岁时已能在草原上放牧牛羊。母子以临安乡音对话,但均通晓蒙古话。丘处机道长曾于其出生时赠短剑,并为其取名“郭靖”,寓意不忘“靖康之耻”。

根据上面的输出结果可以看出,对于用户提问来说,从向量库检索到的相关性最高的前3篇参考文档与经过排序模型从20篇文档中筛选的前3篇文章仅有一篇是相同的,即 pk=465261515437506672 这篇。同时,在排序模型眼里相关性最高的章节 “第三回 黄沙莽莽” 却并没有出现在向量库检索到的前3的结果中;而在语义检索中相似度排序第17的参考文档在排序模型看来却是非常重要的。

最后,如果仅根据向量库检索到的前3篇参考文档将会得到类似如下结果:

郭靖是《射雕英雄传》的主人公。他的母亲是浙江临安人,他从小在蒙古大漠长大,受江南六怪教导,听惯江南口音。

这里可以看出,在加入排序模型以后的回答效果相较于之前有了大幅的提升。

到此,对于两阶段索引的应用就介绍完了,以上完整示例代码可参见 Code/Chapter04/C08_reranking_semantic_search.py 文件。

#

排序模型从20个候选结果中返回最相关的前3个文档内容。

{'level_1': '第七回 比武招亲', 'pk': 465261515437506672, 'source': 'RAGWithMe/data/jinyong/金庸-射雕英雄传精校版.txt', 'start_index': 5796}
郭靖之母是浙江临安人,江南六怪都是嘉兴左近人氏,他从小听惯了江南口音,听那少年说的正是自己乡音,很感喜悦。那少年走到桌边坐下,郭靖吩咐店小二再拿饭菜。店小二见了少年这副肮脏穷样,老大不乐意,叫了半天,才懒洋洋地拿了碗碟过来。
那少年发作道:“你道我穷,不配吃你店里的饭菜吗?只怕你拿最上等的酒菜来,还不合我口味呢。”店小二冷冷地道:“是么?你老人家点得出,我们总做得出,就怕吃了没人会钞。”那少年向郭靖道:“任我吃多少,你都做东么?”郭靖道:“当然,当然。”转头向店小二道:“快切一斤牛肉,半斤羊肝来。”他只道牛肉羊肝便是天下最好的美味,又问少年,说的也是江南话:“喝酒不喝?”

{'level_1': '第三回 黄沙莽莽', 'pk': 464859367376683180, 'source': 'RAGWithMe/data/jinyong/金庸-射雕英雄传精校版.txt', 'start_index': 7398}
李萍含辛茹苦地抚养婴儿,在大漠中熬了下来。她在水草旁用树枝搭了一所茅屋,畜养牲口,又将羊毛纺条织毡,与牧人交换粮食。蒙古人传统善待旅客、外人,见她可怜,常送些羊乳、乳酪、羊肉给她。
忽忽数年,孩子已经六岁了。李萍依着丈夫的遗言,替他取名为郭靖。这孩子学话甚慢,有点儿呆头呆脑,直到四岁时才会说话,好在身子粗壮,筋骨强健,已能在草原上放牧牛羊。母子两人相依为命,勤勤恳恳,牲口渐繁,生计也过得好些了,又都学会了蒙古话,但母子对话,说的却仍是临安故乡言语。李萍瞧着儿子憨憨的模样,说着什么“羊儿、马儿”,全带着自己柔软的临安乡下土音,时时不禁心酸:“你爹是山东好汉,你也该当说山东话才是。只可惜我跟随你爹的时日太短,没学会他的卷舌头说话,无法教你。”
这一年方当十月,天日渐寒,郭靖骑了一匹小马,带了一条牧羊犬出去牧羊。中午时分,空中忽然飞来一头黑雕,向羊群猛扑下来,一头小羊受惊,向东疾奔而去。郭靖连声呼喝,那小羊却头也不回地急逃。

{'level_1': '第三十八回 锦囊密令', 'pk': 465290802323521715, 'source': 'RAGWithMe/data/jinyong/金庸-射雕英雄传精校版.txt', 'start_index': 15090}
郭靖望着母亲,就欲出口答应,但想起母亲平日教诲,又想起西域各国为蒙古征服后百姓家破人亡的惨状,委实左右为难。
成吉思汗一双老虎般的眼睛凝望着他,等他说话。金帐中数百人默无声息,目光全都集于郭靖身上。郭靖道:“我……”走上一步,却又说不下去了。
李萍忽道:“大汗,只怕这孩子一时想不明白,待我劝劝他如何?”成吉思汗大喜,连说:“好,你快劝他。”李萍走上前去,拉着郭靖臂膀,走到金帐的角落,两人一齐坐下。李萍将儿子搂在怀里,轻轻说道:“二十年前,我在临安府牛家村,身上有了你这孩子。一天大雪,丘处机丘道长与你爹结识,赠了两把短剑,一把给你爹,一把给你杨叔父。”一面说,一面从郭靖怀中取出那柄短剑,指着柄上“郭靖”两字,说道:“丘道长给你取名郭靖,给杨叔父的孩子取名杨康,你可知是什么意思?”郭靖道:“丘道长是叫我们不可忘了靖康之耻。”