在人工智能快速发展的今天,检索增强生成(RAG)技术已成为连接大语言模型与专业知识的重要桥梁。然而,传统的Naive RAG在处理复杂结构化数据和多维度查询时逐渐显露短板。本文将介绍一个基于LangGraph构建的GraphRAG多智能体系统,它以膳食规划为应用场景,展示了下一代增强生成技术的下一代发展方向。尽管聚焦于食物领域,但其架构具有极强的通用性,可广泛应用于需要复杂关系推理的众多行业。
在这里插入图片描述

从Naive RAG到Graph RAG:技术演进的必然

在膳食规划这类场景中,Naive RAG的局限性尤为明显。传统RAG系统从非结构化文本中检索信息,无法法表示实体间的明确关系,这使得难以处理"推荐我推荐适合素食者的早餐食谱,热量低于1000卡路里"这类需要理解多重约束关系的查询。它的推理过程局限于单一层面,无法完成需要遍历多个数据节点的多步推理,更重要的是,由于依赖于文本相似度检索,其结果缺乏可解释性,用户无法知晓答案的生成逻辑。

Graph RAG的出现正是为了突破这些限制。基于图结构的框架具有三大核心优势:首先,它能明确建模实体关系,将食谱、食材、 dietary需求等元素作为节点,用"包含""适合"等关系连接,形成结构化知识网络;其次,它支持多跳推理,能像人类思考一样逐步推导,比如先找到适合素食者的食谱,再筛选出早餐时段的选项,最后过滤出低热量的结果;最后,基于图的推理路径完全可追溯,每个结论都能通过节点间的连接关系解释来源,大幅提升了系统的可信度。

项目概览:智能膳食助手的核心能力

这个GraphRAG系统专注于解决三类膳食相关的复杂问题:根据饮食限制发现合适的食谱、为特定食谱生成精准的购物清单、在超市中定位商品的具体位置。通过结合语义搜索(处理模糊匹配)和精确的Cypher查询(处理结构化检索),系统能在Neo4j知识图谱上执行多步骤推理,为用户提供贴合语境的回答。

系统的工作流程可分为四个关键阶段:

查询分析与路由是流程的起点。用户的请求首先被分类,系统会判断是需要生成研究计划、询问更多信息,还是直接回复(针对无关话题)。例如,当用户说"推荐甜的食谱"时,系统会识别出缺少饮食限制、用餐时间等必要信息,从而要求补充细节。

研究计划生成阶段,系统会根据查询的复杂程度构建逐步方案。对于"素食早餐,热量低于1000卡路里"这样的请求,计划可能包括:先通过语义搜索找到素食相关的饮食节点,再定位早餐时段的节点,最后通过查询检索同时满足这两个条件且热量达标的食谱。

研究图谱执行是核心环节,系统会调用专门的子图处理计划中的每个步骤。它能生成Cypher查询语句,在Neo4j中检索相关节点和关系,同时结合语义搜索处理模糊解模糊查询,确保结果的准确性和全面性。

最后是答案生成阶段,系统将检索到的图谱数据整合,通过大语言模型生成自然语言回答,清晰呈现结果和推理过程。

构建知识图谱:从文本到结构化关系

知识图谱是整个系统的基础,我们需要将食谱、食材、 dietary信息等元素转化为图中的节点和关系。借助大语言模型,这个过程可以高效完成。

首先选择合适的大语言模型,这里使用GPT-4o,它在实体识别和关系提取方面表现优异。通过LangChain的LLMGraphTransformer,我们可以定义需要提取的节点类型(如Recipe、Foodproduct)和关系类型(如CONTAINS),让模型按照指定格式解析文本。

例如,对于一段关于"素食巧克力蛋糕食谱"的文本,模型能识别出"Vegan Chocolate Cake Recipe"作为Recipe节点,"无麸质面粉混合物"等作为Foodproduct节点,并建立"包含"关系。代码实现如下:

from langchain_experimental.graph_transformers import LLMGraphTransformer
from langchain_openai import ChatOpenAI
from langchain_core.documents import Document

# 初始化模型和转换器
llm = ChatOpenAI(temperature=0, model_name="gpt-4o")
llm_transformer_filtered = LLMGraphTransformer(
    llm=llm,
    allowed_nodes=["Recipe", "Foodproduct"],
    allowed_relationships=["CONTAINS"],
)

# 处理文本并提取图谱数据
text = "素食巧克力蛋糕食谱包含无麸质面粉混合物、可可粉等食材..."
documents = [Document(page_content=text)]
graph_documents_filtered = await llm_transformer_filtered.aconvert_to_graph_documents(documents)

提取后的图谱数据可以通过Neo4jGraph的add_graph_documents方法存储到数据库中,形成初步的知识图谱。之后,我们可以在Neo4j控制台通过Cypher语句查询验证,比如用MATCH p=(r:Recipe)-[:CONTAINS]->(fp:Foodproduct) RETURN p LIMIT 25查看食谱与食材的关系。

为了支持语义搜索,还需要为节点添加嵌入向量。以食谱节点为例,我们使用OpenAI的嵌入模型生成食谱名称的向量表示,并存储到Neo4j中,同时创建向量索引:

import openai
from neo4j import GraphDatabase

# 生成嵌入并存储
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
recipe_id = "Vegan Chocolate Cake Recipe"
recipe_embedding = openai.embeddings.create(
    model="text-embedding-3-small", 
    input=recipe_id
).data[0].embedding

with driver.session() as session:
    session.run(
        "MATCH (r:Recipe {id: $recipe_id}) SET r.embedding = $embedding",
        recipe_id=recipe_id,
        embedding=recipe_embedding
    )
    # 创建向量索引
    session.run(
        "CREATE VECTOR INDEX recipe_index IF NOT EXISTS FOR (r:Recipe) ON (r.embedding) OPTIONS {indexConfig: {`vector.dimensions`: 1536, `vector.similarity_function`: 'cosine'}}"
    )

有了嵌入向量后,当用户查询"素食巧克力蛋糕食谱"时,系统能将查询转换为向量,通过索引找到最相似的食谱节点,即使名称不完全匹配也能准确检索。

设计工作流程:LangGraph状态管理与节点协作

LangGraph是构建这个多智能体系统的核心框架,其核心思想是通过状态(state)在节点(node)间传递信息,实现复杂的工作流。整个系统包含两个关键图谱:处理具体检索任务的研究子图,和管理整体流程的主图。

状态定义:数据流转的基础

状态是系统的"记忆",定义了在节点间传递的信息结构。我们首先定义Router类,用于存储查询分类结果,它包含分类逻辑和类型("more-info"需要更多信息、"valid"有效查询、"general"一般问题):

from typing import Literal
from pydantic import BaseModel

class Router(BaseModel):
    logic: str
    type: Literal["more-info", "valid", "general"]

InputState类用于存储用户与智能体的对话历史,AgentState则继承InputState,增加了路由信息、研究步骤列表和累积的知识:

from dataclasses import dataclass, field
from typing import Annotated
from langchain_core.messages import AnyMessage
from langgraph.graph import add_messages

@dataclass(kw_only=True)
class InputState:
    messages: Annotated[list[AnyMessage], add_messages]

@dataclass(kw_only=True)
class AgentState(InputState):
    router: Router = field(default_factory=lambda: Router(type="general", logic=""))
    steps: list[dict] = field(default_factory=list)
    knowledge: list[dict] = field(default_factory=list)

这些状态类确保了数据在整个工作流中的有序传递和更新,每个节点处理后的数据会被整合到状态中,传递给下一个节点。

主图工作流:从查询到回答的全程管控

主图负责统筹整个流程,包含六个关键节点,通过 edges 和 conditional edges 连接,形成完整的工作流。

分析与路由查询(analyze_and_route_query) 是流程的入口,它接收用户消息,调用大语言模型对查询进行分类,生成Router对象更新状态。例如,对于"慕尼黑的天气如何?“这样的查询,会被分类为"general”,因为它与食谱、购物清单或超市位置无关。

async def analyze_and_route_query(state: AgentState, config: RunnableConfig) -> dict[str, Router]:
    model = init_chat_model(name="analyze_and_route_query", **app_config["inference_model_params"])
    messages = [{"role": "system", "content": ROUTER_SYSTEM_PROMPT}] + state.messages
    response = await model.with_structured_output(Router).ainvoke(messages)
    return {"router": response}

路由决策(route_query) 根据分类结果决定下一步:如果是"valid"则进入创建研究计划节点;"more-info"则询问用户补充信息;"general"则直接回复无法提供帮助。

当需要更多信息时,ask_for_more_info 节点会生成友好的追问,例如用户只说"推荐甜的食谱"时,系统会询问饮食限制、用餐时间等必要信息。而 respond_to_general_query 节点则用于回复无关话题,礼貌地引导用户提出相关请求。

对于有效的查询,流程进入 create_research_plan 节点。这个节点会生成、精简和审查研究步骤,确保计划的可行性。例如,针对"素食早餐,热量低于1000卡路里"的请求,生成的计划可能包括语义搜索素食和早餐相关节点,再通过查询筛选符合热量要求的食谱。

async def create_research_plan(state: AgentState, config: RunnableConfig) -> dict:
    # 生成初始计划
    model = init_chat_model(name="create_research_plan",** app_config["inference_model_params"])
    system_prompt = RESEARCH_PLAN_SYSTEM_PROMPT.format(
        schema=neo4j_graph.get_structured_schema, 
        context="\n".join([item["content"] for item in state.knowledge])
    )
    messages = [{"role": "system", "content": system_prompt}] + state.messages
    plan = await model.with_structured_output(Plan).ainvoke(messages)
    
    # 精简和审查计划
    reduced_plan = await reduce_research_plan(plan)
    reviewed_plan = await review_research_plan(reduced_plan)
    
    return {"steps": reviewed_plan["steps"], "knowledge": []}

conduct_research 节点负责执行研究计划的步骤,它调用研究子图处理每个步骤,收集知识并更新剩余步骤。通过 check_finished 函数判断是否还有步骤未完成,形成循环,直到所有步骤执行完毕,最终进入 respond 节点生成最终回答。

研究子图:精准检索的实现核心

研究子图是执行具体检索任务的关键,它包含语义搜索和查询生成执行两条路径,根据步骤类型选择相应的处理方式。

语义搜索(semantic_search) 用于处理模糊匹配,例如根据"素食"查找对应的饮食节点。它首先调用大语言模型确定搜索参数(节点类型、属性、查询文本),然后使用OpenAI嵌入生成查询向量,在Neo4j的向量索引中查找最相似的节点:

def execute_semantic_search(node_label: str, attribute_name: str, query: str):
    index_name = f"{node_label.lower()}_{attribute_name}_index"
    query_embedding = openai.embeddings.create(
        model=app_config["embedding_model"], 
        input=query
    ).data[0].embedding
    
    response = neo4j_graph.query(f"""
        CALL db.index.vector.queryNodes('{index_name}', 1, {query_embedding})
        YIELD node, score
        RETURN node.name as name
        ORDER BY score DESC
    """)
    return response

生成查询(generate_queries) 用于处理结构化检索,它根据研究步骤生成Cypher查询,并通过两种方式校正:基于LLM的模式感知校正和基于解析器的结构校正,确保查询语句的正确性。例如,生成查询查找同时符合素食和早餐条件的食谱,并计算总热量:

async def generate_queries(state: ResearcherState, config: RunnableConfig) -> dict[str, list[str]]:
    model = init_chat_model(name="generate_queries", **app_config["inference_model_params"])
    system_prompt = GENERATE_QUERIES_SYSTEM_PROMPT.format(
        schema=neo4j_graph.get_schema, 
        context="\n".join([item["content"] for item in state.knowledge])
    )
    messages = [{"role": "system", "content": system_prompt}] + [{"role": "human", "content": state.step["question"]}]
    response = await model.with_structured_output({"queries": list[str]}).ainvoke(messages)
    
    # 校正查询
    response["queries"] = [await correct_query_by_llm(q) for q in response["queries"]]
    response["queries"] = [correct_query_by_parser(q) for q in response["queries"]]
    return {"queries": response["queries"]}

execute_query 节点负责执行校正后的Cypher查询,从Neo4j中获取数据,并将结果格式化为知识条目,供后续步骤使用。

研究子图通过这些节点的协作,实现了从模糊到精确、从语义到结构化的全方位检索能力,为复杂查询提供了有力支持。

实战案例:从查询到结果的完整流程

让我们通过"给我’pasta alla carbonara’食谱的购物清单"这个具体案例,看看系统如何工作。

首先,用户的查询被发送到主图的analyze_and_route_query节点,经过分析,被分类为"valid"(有效的购物清单查询),路由到create_research_plan节点。

创建研究计划阶段,系统生成两步计划:第一步通过语义搜索在Recipe节点中查找与"pasta alla carbonara"相似的食谱名称;第二步根据找到的食谱,查询其包含的食材对应的商店产品,形成购物清单。

计划进入conduct_research节点,首先执行第一步语义搜索。系统确定搜索参数为节点标签"Recipe"、属性"name"、查询文本"pasta alla carbonara",生成查询向量后在向量索引中检索,找到最相似的食谱节点名称是"Classic Carbonara"。

接着执行第二步,generate_queries节点生成Cypher查询:

MATCH (r:Recipe {name: 'Classic Carbonara'})-[:CONTAINS]->(fp:FoodProduct)<-[:IS_INSTANCE_OF]-(sp:StoreProduct)
RETURN sp.name, sp.brand, sp.price, sp.quantity, sp.quantity_unit
LIMIT 50

查询执行后,得到该食谱所需的商店产品信息,包括意大利面、培根、帕玛森芝士等,每种产品的品牌、价格和数量都清晰呈现。

所有步骤完成后,系统进入respond节点,将检索到的信息整理成自然语言回答,提供详细的购物清单,包括食材名称、推荐品牌、价格和购买数量,方便用户直接使用。

这个案例展示了系统如何将模糊的用户查询转化为精确的检索步骤,通过语义搜索和结构化查询的结合,高效获取所需信息,最终生成实用的结果。

挑战与展望:Graph RAG的未来发展

尽管Graph RAG在处理复杂查询方面表现出色,但实施过程中仍面临一些挑战。延迟问题是其中之一,多智能体交互和多步推理增加了系统的复杂性,导致响应时间延长,如何在速度和准确性之间找到平衡是关键。

评估与可观察性也是一大难点。随着系统复杂度提升,传统的评估指标难以全面衡量其性能,需要建立更完善的评估体系,同时增强系统的可观察性,以便追踪推理过程、发现问题并优化。

不过,Graph RAG的优势显而易见,它标志着人工智能在知识处理领域的重大进步。通过将大语言模型的理解能力与图结构的关系建模相结合,Graph RAG为处理复杂知识提供了新的范式。在医疗、金融、教育等需要深度知识推理的领域,Graph RAG有望发挥重要作用,推动人工智能技术向更智能、更可靠的方向发展。

Logo

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

更多推荐