DeepSeek-R1 + LlamaIndex:基于推理模型的RAG/RAT应用新玩法




点击上方蓝字关注我们




各位朋友们,新年好!
春节期间,科技界最火爆出圈的恐怕非国产大模型DeepSeek莫属,特别是其在V3后推出的对标OpenAI o1的推理模型DeepSeek-R1,以其强大的性能与低廉的成本震惊了全球。
不过,再强大的模型终究要服务于上层,让我们来关注基于DeepSeek-R1的RAG应用及其特别的玩法。
  • 基于DeepSeek-R1+LlamaIndex的RAG
  • 将RAG转化为RAT(检索增强思维)

基于DeepSeek-R1的RAG

首先我们来快速构建一个DeepSeek-R1的RAG原型,以了解其不同之处。
原料
  • 模型:开源DeepSeek-R1+Ollama本地推理
  • RAG框架:LlamaIndex
  • UI原型:Streamlit

准备Ollama与模型服务
  • 下载Ollama:https://ollama.com/
  • 安装后运行:ollama run deepseek-r1
等待模型下载完成后,我们就拥有了可完全离线使用的本地deepseek-r1模型(根据硬件条件选择不同的参数大小,默认为7b)
基于LlamaIndex的RAG管道
1. 设置模型
在LlamaIndex中使用Ollama模型很简单:
...
llm = Ollama(model="deepseek-r1")
embed_model = OllamaEmbedding(model_name="milkey/dmeta-embedding-zh:f16")

Settings.llm = llm
Settings.embed_model = embed_model
2. 上传文档处理
提供一个上传知识文档的地方并做处理(为了测试方便,这里允许重新上传):
...
# 上传文件(这里允许上传的类型为 txt/ PDF)
uploaded_file = st.file_uploader("", type=["pdf", "txt"])

if uploaded_file is not None:
    
    # 计算文件内容的哈希值
    file_content = uploaded_file.getvalue()
    new_file_hash = hashlib.md5(file_content).hexdigest()

    # 如果文件发生变化或第一次上传,则重新创建 query_engine
    if st.session_state.file_hash != new_file_hash:
        st.session_state.file_hash = new_file_hash

        with st.spinner("正在处理文档..."):

            st.session_state.messages = []

            # 将上传的文件保存到临时位置
            with open("temp.pdf", "wb") as f:
                f.write(file_content)
...
3. 创建基于LlamaIndex的RAG管道
然后用最简洁的方式创建LlamaIndex的RAG管道,采用默认的内存向量存储:
...
            # 加载并处理文档
            docs = SimpleDirectoryReader(input_files=["temp.pdf"]).load_data()
            
            # 创建向量存储索引
            index = VectorStoreIndex.from_documents(docs)

            # 使用自定义提示创建查询引擎
            query_prompt = """
            1. 使用以下上下文来回答最后的问题。
            2. 如果你不知道答案,请直接说"我不知道",不要编造答案。
            3. 保持答案简洁,限制在3-4句话内。
            上下文:{context_str}
            问题:{query_str}
            答案:"""

            
            st.session_state.query_engine = index.as_query_engine(
                text_qa_template=PromptTemplate(query_prompt),
                similarity_top_k=3,
            )
4. 显示聊天历史并输入
接下来就可以接收问题输入,并生成响应,同时添加到对话历史:
...
    # 显示聊天历史
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.write(message["content"])

    # 聊天输入
    if prompt := st.chat_input("请输入您的问题"):
        # 显示用户问题
        with st.chat_message("user"):
            st.write(prompt)
        st.session_state.messages.append({"role": "user", "content": prompt})

        # 生成回答
        with st.chat_message("assistant"):
            with st.spinner("思考中..."):
                response = st.session_state.query_engine.query(prompt)
                st.markdown(response.response)
                st.session_state.messages.append({"role": "assistant", "response": response.response})
else:
    st.write("上传 PDF 文件以开始对话。")
5. 清除历史记录按钮
为了方便测试,添加一个清除对话历史的按钮:
...
# 添加清除对话历史的按钮
if st.button("清除对话历史"):
    st.session_state.messages = []
    st.rerun()
6. 测试效果
在当前目录下运行streamlit run xxx.py,会自动打开浏览器访问应用,能够看到类似如下界面:
现在上传一个简单的PDF文件,开始对话。效果如下:
这里我们可以看到,DeepSeek-R1这样的推理模型在输出时会带有一个“思考”过程,这个逐步推理过程会放在输出内容的标签内:

7. 优化显示
实际应用时,可以对中的内容进行简单处理,比如隐藏。这里我们用简单的方法将这部分内容用不同的样式显示,并在保存历史消息时去除这部分。把上面代码中的响应处理部分做如下修改:
...
                # 处理回答中的标签
                text = response.response
                text = re.sub(
                    r'([\s\S]*?)',
                    lambda m: f'
{m.group(1)}
'
,
                    text,
                    flags=re.DOTALL
                )
                st.markdown(text, unsafe_allow_html=True)
                clean_text = re.sub(r'[\s\S]*?', '', response.response).strip()
                st.session_state.messages.append({"role": "assistant", "content": clean_text})
现在显示效果如下,你可以快速的区分出“思考”与“答案”部分:
如果你使用一些开源的RAG构建平台或类似open-webui这样开箱即用的应用来对接R1,则需要自行对的部分进行处理,比如做折叠显示等。

DeepSeek-R1将RAG转化为RAT

将DeepSeek-R1这样的推理模型用于RAG的另一种改进思路是:将DeepSeek-R1卓越的推理能力与其他模型(如GPT-4o)的生成能力结合,生成更具思考深度与准确性的答案。这种改进形式也被称为RAT(Retrieval Augmented Thoughts),检索增强思维
什么是RAT
RAT是 RAG的一种改进,它结合了 CoT(Chain of Thought,思维链)推理,让模型在回答问题时不仅能够参考外部知识,还能更好地进行深度的推理与思维。
RAT的标准流程为:
1. 生成初步的思维链(CoT):即推理时的一步步思考过程,通常会分成n个步骤,直到生成最终答案,但此时的CoT可能不够完善或者有错误。
2. 迭代完善思维链: 根据已有CoT的步骤来检索外部知识,并用来不断修正CoT中的错误或遗漏,使CoT更加精准与丰富。经过多次这样的迭代,最终形成一个完善的CoT思维过程。
3. 生成最终答案:将最终的CoT,即更完善的思维过程,交给负责生成最终答案的LLM,获得最终响应。
整个过程可以下图表示:
在推理模型之前,你只能借助Prompt来生成CoT步骤,后来出现了o1但价格昂贵,现在DeepSeek-R1的开源给了我们一种新的选择:
让DeepSeek- R1来负责生成与完善CoT,以“提取”其思维过程。
RAT的特点
在理解了RAT的工作流程后,可以总结出RAT的特点:
  • 基于CoT生成:借助CoT模式生成更加深思熟虑与完善的答案。
  • 迭代推理:通过多次迭代推理精细化CoT,借助外部知识完善。
  • 动态检索:每次根据新的迭代推理结果,来调整检索的上下文。

由于RAT的流程复杂度增高,所以会带来一定程度的响应速度下降。因此,也并非所有的场景都适用RAT,其更适用于需要长距推理、需要借助外部知识(本地知识库或网络搜索)的一些复杂应用
  • 综合问答:比如一些需要结合多个关联内容的对比、计算、总结等
  • 编程任务:借助CoT可以让LLM更准确的理解需求并生成更精准的代码
  • 数学推理、游戏步骤推理、机器人路径规划等

RAT的简单实现
参考RAT的流程,我们在上面的RAG代码增加一个“思考”的核心步骤:即借助deepseek迭代生成一个CoT结果。流程稍作简化:
  • 迭代完善CoT时,使用上一次的CoT结果进行检索,并把检索出的上下文交给DeepSeek,用来再次完善CoT

...
THINK_PROMPT = """
    请结合下面提供的上下文(context字段),仔细思考我的问题(question字段),完善之前的推理过程(thoughts字段)。
    如果上下文与问题并不相关,请忽略context字段,直接思考问题。
    注意你只需思考该问题,不需要输出答案,答案部分直接输出"Reasoning Done"即可。
"""


# 初始化组件
reasoning_llm = Ollama(model="deepseek-r1:1.5b")

class DeepSeek:
    def __init__(self, reasoning_llm, reasoning_times=1):
        self.reasoning_llm = reasoning_llm
        self.reasoning_times = reasoning_times

    #检索相关上下文
    def retrieve(self, query):
        nodes = st.session_state.query_engine.retrieve(query["question"])
        return {
            "context": nodes,
            "question": query["question"]
        }

    #迭代推理,并返回最终COT
    def think(self, input: str) -> str:

        thoughts = input
        for _ in range(self.reasoning_times):

            retrieved_docs = self.retrieve({"question": thoughts})
            docs_content = "\n\n".join(
                node.text for node in retrieved_docs["context"]
            )

            prompt_json = {
                "context": docs_content,
                "question": input,
                "thoughts": thoughts
            }

            response = self.reasoning_llm.complete(THINK_PROMPT + '\n\n' + dumps(prompt_json, ensure_ascii=False))

            think = re.findall(r'(.*?)', response.text, re.DOTALL)
            if not think:
                return thoughts or "No reasoning available."
            thoughts = think[0]

            st.write(f"thoughts: {thoughts}")

        return thoughts
测试这里的代码,每次迭代都可以获得一个CoT的“思维”结果。比如:
如果知识库很完善,那么经过多次的迭代,这里的CoT将会更完善,最后就可以交给LLM生成更精准的响应结果。
以上研究了DeepSeek-R1推理模型在RAG中的应用、特点,以及一种新的改进形式:RAT,我们也期待有更多的应用形式让DeepSeek-R1这种优秀的国产模型发挥更大的作用与潜能。

THE END


福利时间


为了帮助LLM开发人员更系统性与更深入的学习RAG应用,特别是企业级的RAG应用场景下,当前主流的优化方法与技术实现,我们编写了《基于大模型的RAG应用开发与优化 — 构建企业级LLM应用》这本长达500页的开发与优化指南,与大家一起来深入到LLM应用开发的全新世界。

更多细节,点击链接了解

此处购买享5折优惠


交流请识别以下名片

请使用浏览器的分享功能分享到微信等