本文介绍了一种自适应RAG系统,通过LangGraph和本地LLM(Ollama+Mistral)构建,能在Web搜索和向量库检索间智能切换。系统包含两个主要分支:Web Search处理近期事件,Self-Corrective RAG针对知识库并实现自我纠错。该系统具备灵活路由、质量把控、本地化部署等特点,有效解决了大模型知识更新慢的短板,为不同类型问题提供最佳检索策略。

大模型越来越强大,但它们依旧有一个致命短板:知识更新慢。如果直接问 ChatGPT 之类的模型一个近期事件的问题,它很可能答不上来。这就是为什么 RAG(检索增强生成) 变得重要 —— 在回答问题之前,先去找相关资料,再让模型结合这些资料生成答案。

不过,RAG 并不是“一刀切”的方案:有些问题根本不需要检索(比如定义类问题),有些问题需要一次检索就能解决,而另一些则需要多次尝试(比如先改写问题,再检索)。这就是 自适应RAG 的核心:根据问题的不同,动态选择最合适的策略

本文我们将用 LangGraph + 本地 LLM(Ollama + Mistral) 搭建一个 Adaptive RAG 系统,能在 Web 搜索向量库检索 之间灵活切换,还能自我纠错。

注意:我们的 Adaptive RAG 系统有两个主要分支:

Web Search:处理最近事件相关的问题(因为向量库的数据是历史快照,不会包含最新信息)。借助 Tavily 搜索 API 获取网页结果,再交给 LLM 组织答案。

Self-Corrective RAG:针对我们自己构建的知识库(这里我们抓取了 Lilian Weng 的几篇经典博客:Agent、Prompt Engineering、Adversarial Attack)。向量库用****Chroma 搭建,文本向量用 Nomic 本地 Embedding 生成。如果第一次检索结果不相关,会尝试改写问题,再次检索。同时会过滤掉“答非所问”的文档,避免垃圾结果。

  1. 环境准备
%capture --no-stderr
%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local]

设置 API Key(Tavily 搜索 + Nomic embedding)。

import getpass, os

def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")

_set_env("TAVILY_API_KEY")
_set_env("NOMIC_API_KEY")
  1. 本地模型和向量库

我们将要构建了一个 向量数据库,内容是 Lilian Weng 的三篇博客。以后凡是涉及 Agent/Prompt Engineering/Adversarial Attack 的问题,就走这里。

# Ollama 模型
local_llm = "mistral"

# 文本切分 & 向量化
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import Chroma
from langchain_nomic.embeddings import NomicEmbeddings

urls = [
    "https://lilianweng.github.io/posts/2023-06-23-agent/",
    "https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/",
    "https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/",
]
docs = [WebBaseLoader(url).load() for url in urls]
docs_list = [item for sublist in docs for item in sublist]

text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=250, chunk_overlap=0
)
doc_splits = text_splitter.split_documents(docs_list)

vectorstore = Chroma.from_documents(
    documents=doc_splits,
    collection_name="rag-chroma",
    embedding=NomicEmbeddings(model="nomic-embed-text-v1.5", inference_mode="local"),
)
retriever = vectorstore.as_retriever()
  1. 问题路由器(Router)

假如这个问题和 Agent 相关,所以走向量库。

from langchain.prompts import PromptTemplate
from langchain_community.chat_models import ChatOllama
from langchain_core.output_parsers import JsonOutputParser

llm = ChatOllama(model=local_llm, format="json", temperature=0)

prompt = PromptTemplate(
    template="""You are an expert at routing a user question to a vectorstore or web search...
    Question to route: {question}""",
    input_variables=["question"],
)
question_router = prompt | llm | JsonOutputParser()

question = "llm agent memory"
print(question_router.invoke({"question": question}))

执行结果

{'datasource': 'vectorstore'}
  1. 检索质量评估(Retrieval Grader)

如果检索到的文档与问题相关。

retrieval_grader = prompt | llm | JsonOutputParser()
question = "agent memory"
docs = retriever.get_relevant_documents(question)
doc_txt = docs[1].page_content
print(retrieval_grader.invoke({"question": question, "document": doc_txt}))

执行结果

{'score': 'yes'}
  1. 答案生成(RAG Generate)

成了一段关于 “Agent Memory” 的解释。

from langchain import hub
from langchain_core.output_parsers import StrOutputParser

prompt = hub.pull("rlm/rag-prompt")
llm = ChatOllama(model=local_llm, temperature=0)

rag_chain = prompt | llm | StrOutputParser()

question = "agent memory"
generation = rag_chain.invoke({"context": docs, "question": question})
print(generation)

执行结果

In an LLM-powered autonomous agent system, the LargeLanguage Model (LLM) functions as the agent's brain...
  1. 幻觉检测(Hallucination Grader)

如果答案确实是基于文档生成的,没有瞎编。如果答案不靠谱,就让系统重新检索或改写问题。

hallucination_grader = prompt | llm | JsonOutputParser()
hallucination_grader.invoke({"documents": docs, "generation": generation})

执行结果

{'score': 'yes'}
  1. 答案有用性评估(Answer Grader)

如果这个答案对用户有用。

answer_grader.invoke({"question": question, "generation": generation})

执行结果

{'score': 'yes'}
  1. 问题重写器(Question Rewriter)

改成了更适合检索的问法。

question_rewriter.invoke({"question": question})

执行结果

'What is agent memory and how can it be effectively utilized in vector database retrieval?'
  1. Web 搜索工具

当问题和近期事件有关时,就会走 Tavily 搜索,而不是本地库。

from langchain_community.tools.tavily_search import TavilySearchResults
web_search_tool = TavilySearchResults(k=3)

执行结果日志

---ROUTE QUESTION---
What is the AlphaCodium paper about?
{'datasource': 'web_search'}
---ROUTE QUESTION TO WEB SEARCH---
---WEB SEARCH---
"Node 'web_search':"
'---'
---GENERATE---
---CHECK HALLUCINATIONS---
---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---
---GRADE GENERATION vs QUESTION---
---DECISION: GENERATION ADDRESSES QUESTION---
"Node 'generate':"
'---'
('The AlphaCodium paper introduces a new approach for code generation...')

10.工作流(LangGraph 具体实现)

我们用 LangGraph 把这些步骤连起来,形成一个有条件分支的工作流:

  • 开始 → 判断走 Web Search 还是 Vectorstore
  • 如果走 Vectorstore:检索 → 文档过滤 →
  • 如果靠谱 → 返回结果

  • 如果不靠谱 → 改写问题 → 再检索

  • 如果没文档:改写问题 → 再检索

  • 如果有文档:生成答案 → 检查是否靠谱

  • 如果走 Web Search:直接搜 → 生成答案 → 检查 → 返回结果

最终,系统能在不同类型的问题上灵活切换,而不是死板地“一问一搜”。

from typing import List
from typing_extensions import TypedDict
class GraphState(TypedDict):
    """
    Represents the state of our graph.
    Attributes:
        question: question
        generation: LLM generation
        documents: list of documents
    """
    question: str
    generation: str
    documents: List[str]
    
### Nodes
from langchain.schema import Document
def retrieve(state):
    """
    Retrieve documents
    Args:
        state (dict): The current graph state
    Returns:
        state (dict): New key added to state, documents, that contains retrieved documents
    """
    print("---RETRIEVE---")
    question = state["question"]
    # Retrieval
    documents = retriever.get_relevant_documents(question)
    return {"documents": documents, "question": question}
def generate(state):
    """
    Generate answer
    Args:
        state (dict): The current graph state
    Returns:
        state (dict): New key added to state, generation, that contains LLM generation
    """
    print("---GENERATE---")
    question = state["question"]
    documents = state["documents"]
    # RAG generation
    generation = rag_chain.invoke({"context": documents, "question": question})
    return {"documents": documents, "question": question, "generation": generation}
def grade_documents(state):
    """
    Determines whether the retrieved documents are relevant to the question.
    Args:
        state (dict): The current graph state
    Returns:
        state (dict): Updates documents key with only filtered relevant documents
    """
    print("---CHECK DOCUMENT RELEVANCE TO QUESTION---")
    question = state["question"]
    documents = state["documents"]
    # Score each doc
    filtered_docs = []
    for d in documents:
        score = retrieval_grader.invoke(
            {"question": question, "document": d.page_content}
        )
        grade = score["score"]
        if grade == "yes":
            print("---GRADE: DOCUMENT RELEVANT---")
            filtered_docs.append(d)
        else:
            print("---GRADE: DOCUMENT NOT RELEVANT---")
            continue
    return {"documents": filtered_docs, "question": question}
def transform_query(state):
    """
    Transform the query to produce a better question.
    Args:
        state (dict): The current graph state
    Returns:
        state (dict): Updates question key with a re-phrased question
    """
    print("---TRANSFORM QUERY---")
    question = state["question"]
    documents = state["documents"]
    # Re-write question
    better_question = question_rewriter.invoke({"question": question})
    return {"documents": documents, "question": better_question}
def web_search(state):
    """
    Web search based on the re-phrased question.
    Args:
        state (dict): The current graph state
    Returns:
        state (dict): Updates documents key with appended web results
    """
    print("---WEB SEARCH---")
    question = state["question"]
    # Web search
    docs = web_search_tool.invoke({"query": question})
    web_results = "\n".join([d["content"] for d in docs])
    web_results = Document(page_content=web_results)
    return {"documents": web_results, "question": question}
### Edges ###
def route_question(state):
    """
    Route question to web search or RAG.
    Args:
        state (dict): The current graph state
    Returns:
        str: Next node to call
    """
    print("---ROUTE QUESTION---")
    question = state["question"]
    print(question)
    source = question_router.invoke({"question": question})
    print(source)
    print(source["datasource"])
    if source["datasource"] == "web_search":
        print("---ROUTE QUESTION TO WEB SEARCH---")
        return "web_search"
    elif source["datasource"] == "vectorstore":
        print("---ROUTE QUESTION TO RAG---")
        return "vectorstore"
def decide_to_generate(state):
    """
    Determines whether to generate an answer, or re-generate a question.
    Args:
        state (dict): The current graph state
    Returns:
        str: Binary decision for next node to call
    """
    print("---ASSESS GRADED DOCUMENTS---")
    state["question"]
    filtered_documents = state["documents"]
    if not filtered_documents:
        # All documents have been filtered check_relevance
        # We will re-generate a new query
        print(
            "---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---"
        )
        return "transform_query"
    else:
        # We have relevant documents, so generate answer
        print("---DECISION: GENERATE---")
        return "generate"
def grade_generation_v_documents_and_question(state):
    """
    Determines whether the generation is grounded in the document and answers question.
    Args:
        state (dict): The current graph state
    Returns:
        str: Decision for next node to call
    """
    print("---CHECK HALLUCINATIONS---")
    question = state["question"]
    documents = state["documents"]
    generation = state["generation"]
    score = hallucination_grader.invoke(
        {"documents": documents, "generation": generation}
    )
    grade = score["score"]
    # Check hallucination
    if grade == "yes":
        print("---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---")
        # Check question-answering
        print("---GRADE GENERATION vs QUESTION---")
        score = answer_grader.invoke({"question": question, "generation": generation})
        grade = score["score"]
        if grade == "yes":
            print("---DECISION: GENERATION ADDRESSES QUESTION---")
            return "useful"
        else:
            print("---DECISION: GENERATION DOES NOT ADDRESS QUESTION---")
            return "not useful"
    else:
        pprint("---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---")
from langgraph.graph import END, StateGraph, START
workflow = StateGraph(GraphState)
# Define the nodes
workflow.add_node("web_search", web_search)  # web search
workflow.add_node("retrieve", retrieve)  # retrieve
workflow.add_node("grade_documents", grade_documents)  # grade documents
workflow.add_node("generate", generate)  # generate
workflow.add_node("transform_query", transform_query)  # transform_query
# Build graph
workflow.add_conditional_edges(
    START,
    route_question,
    {
        "web_search": "web_search",
        "vectorstore": "retrieve",
    },
)
workflow.add_edge("web_search", "generate")
workflow.add_edge("retrieve", "grade_documents")
workflow.add_conditional_edges(
    "grade_documents",
    decide_to_generate,
    {
        "transform_query": "transform_query",
        "generate": "generate",
    },
)
workflow.add_edge("transform_query", "retrieve")
workflow.add_conditional_edges(
    "generate",
    grade_generation_v_documents_and_question,
    {
        "not supported": "generate",
        "useful": END,
        "not useful": "transform_query",
    },
)
# Compile
app = workflow.compile()
inputs = {"question": "What is the AlphaCodium paper about?"}
for output in app.stream(inputs):
    for key, value in output.items():
        pprint(f"Node '{key}':")
    pprint("\n---\n")
pprint(value["generation"])

执行结果

---ROUTE QUESTION---
What is the AlphaCodium paper about?
{'datasource': 'web_search'}
---ROUTE QUESTION TO WEB SEARCH---
---WEB SEARCH---
"Node 'web_search':"
'---'
---GENERATE---
---CHECK HALLUCINATIONS---
---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---
---GRADE GENERATION vs QUESTION---
---DECISION: GENERATION ADDRESSES QUESTION---
"Node 'generate':"
'---'
('The AlphaCodium paper introduces a new approach for code generation...')

我们写的这套 自适应 RAG 系统展示了几个关键点:

灵活路由:不同问题走不同管道(Web / Vectorstore)。

自我纠错:检索结果不相关时,自动改写问题再试。

质量把控:通过“幻觉检测 + 答案有用性判断”,尽量避免胡编乱造。

本地化:Embedding 和 LLM 都可以跑在本地(隐私友好,节省成本)。

未来可以扩展的方向包括:增加“多步推理”路线(先子问题分解,再检索)。更细的路由分类(比如结构化查询 vs 自然语言查询)。融合图数据库或知识图谱,增强事实性。


如何系统学习掌握AI大模型?

AI大模型作为人工智能领域的重要技术突破,正成为推动各行各业创新和转型的关键力量。抓住AI大模型的风口,掌握AI大模型的知识和技能将变得越来越重要。

学习AI大模型是一个系统的过程,需要从基础开始,逐步深入到更高级的技术。

这里给大家精心整理了一份全面的AI大模型学习资源,包括:AI大模型全套学习路线图(从入门到实战)、精品AI大模型学习书籍手册、视频教程、实战学习、面试题等,资料免费分享

1. 成长路线图&学习规划

要学习一门新的技术,作为新手一定要先学习成长路线图方向不对,努力白费

这里,我们为新手和想要进一步提升的专业人士准备了一份详细的学习成长路线图和规划。可以说是最科学最系统的学习成长路线。

在这里插入图片描述

2. 大模型经典PDF书籍

书籍和学习文档资料是学习大模型过程中必不可少的,我们精选了一系列深入探讨大模型技术的书籍和学习文档,它们由领域内的顶尖专家撰写,内容全面、深入、详尽,为你学习大模型提供坚实的理论基础(书籍含电子版PDF)

在这里插入图片描述

3. 大模型视频教程

对于很多自学或者没有基础的同学来说,书籍这些纯文字类的学习教材会觉得比较晦涩难以理解,因此,我们提供了丰富的大模型视频教程,以动态、形象的方式展示技术概念,帮助你更快、更轻松地掌握核心知识

在这里插入图片描述

4. 大模型行业报告

行业分析主要包括对不同行业的现状、趋势、问题、机会等进行系统地调研和评估,以了解哪些行业更适合引入大模型的技术和应用,以及在哪些方面可以发挥大模型的优势。

在这里插入图片描述

5. 大模型项目实战

学以致用 ,当你的理论知识积累到一定程度,就需要通过项目实战,在实际操作中检验和巩固你所学到的知识,同时为你找工作和职业发展打下坚实的基础。

在这里插入图片描述

6. 大模型面试题

面试不仅是技术的较量,更需要充分的准备。

在你已经掌握了大模型技术之后,就需要开始准备面试,我们将提供精心整理的大模型面试题库,涵盖当前面试中可能遇到的各种技术问题,让你在面试中游刃有余。

在这里插入图片描述

全套的AI大模型学习资源已经整理打包,有需要的小伙伴可以微信扫描下方CSDN官方认证二维码,免费领取【保证100%免费

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐