首衡智能问答设计说明
整体构建的是一个面向企业级用户的高效对话、咨询服务的解决方案。从项目整体架构上,采用的是前后端分离架构设计,前端基于Vue3框架实现交互式界面,后端通过Python FastAPI提供标准化RestFul API接口。实现形式类似主流电商平台(如京东、淘宝)智能客服功能,具备多场景意图识别、混合检索问答、动态上下文理解等核心能力,旨在实现用户问题的高效响应与精准解答。
该项目主要围绕首衡具体业务进行设计和研发,可以根据用户输入的实时问题,自动匹配到对应的业务流程,并根据业务流程,自动调用对应的业务组件,完成对用户问题的精准解答,
项目实现了大模型在传统业务流程中的有效接入,同时依托于目前最主流的Multi-Agent架构设计,使复杂且不确定性的业务需求可以通过统一的API接口完成用户无感知的自动处理,包括了用户的问题是否与自有业务相关、何时调用本地知识库数据进行查询、如何避免大模型因幻觉问题产生胡言论语等常见AI服务痛点。
一、项目架构
首衡智能客服项目整体架构层级图如下图所示:

从架构上看,各个层级对应的基础知识点如下:
- 模型集成:该项目集成的是DeepSeek v3 & R1的在线API接口以及通过Ollma启动的开源模型(只要是Ollama支持的模型都可以)接入;
- AI Agent 开发框架:采用了 LangGraph 构建的Multi-Agent架构,并结合了 LangChain 的组件化设计接入模型的集成功能;
- 意图识别:通过LangChain的PromptTemplate + Chain组件,接入Langraph的图结构中,实现用户的意图识别功能;
- 本地知识库构建:通过Microsoft GraphRAG组件,实现本地知识库的索引和查询,并作为子图接入到LangGraph的图结构中;
- Neo4j 图数据库检索:通过预构建规则库 + 大模型根据用户意图自动生成Cypher语句,实现结构化知识库的检索功能,作为LangGraph的子图接入到图结构中;
- 多工具集成:通过LangChain的Tool组件,采用LangGraph图结构的Map-Reduce分支,实现多工具的并行调用,并根据任务分解后的子任务进行结果的汇总和处理;
- FastAPI 接口服务:通过FastAPI框架,对编译后的LangGraph图结构进行接口封装,提供与外部系统对接的标准化RestFul API接口服务;
从上述基础知识点的解释也能看出,项目的核心采用的底层是 AI Agent`开发框架LangGraph+RAG开发框架Microsoft GraphRAG。
二、项目结构说明
首先需要说明的是,项目整体采用的是一个Multi-Agent架构,并基于LangGraph框架构建,是对LangGraph 完整技术体系做的工程化集成开发,包含但不限于:节点和边的定义、图的编译、图状态的定义、事件流、工具集成、上下文记忆、路由、父子图等核心组件的使用。除此以外,项目中还多处涉及到提示工程以及与LangChain组件的结合使用。综上,项目底层的Multi-Agent架构如下图所示:

如上图所示,在langgraph框架的架构下,多代理的实现主要是Router组件来实现的,完整的功能链路则是通过节点和边来进行串联。其中:
- 由 analyze_and_route_query 和 route_query 两个节点充当意图识别模块,根据用户输入的问题,进行意图识别,并根据识别结果,将问题路由到对应的子图进行处理;
- 由 general-query、additional-query、graphrag-query、image-query、file-query 五个节点充当业务处理模块,根据意图识别的结果进行对应的业务处理,并将处理结果返回给用户,其中
- general-query 节点负责处理一般性问题,即是与制度查询、农副产品查询等无关的闲聊问题;
- additional-query 节点负责处理需要更多信息才能回答的问题,比如用户询问农副产品但没有提供具体城市或规格、用户采购制度但没有提供具体的采购任务等;
- graphrag-query 节点负责处理需要查询知识库的问题,比如用户询问行政制度、人力制度、采购制度、农副制度、铁岩制度或者报销流程等;
- image-query 节点负责处理需要解析用户上传图片的问题;
- file-query 节点负责处理需要解析用户上传文件的问题;
- Hallucinations 节点则作为最后一步,负责对最终要返回给用户的问题进行幻觉检测,如果检测到问题存在幻觉,则进行修正,并返回给用户;
下图是到每个子图的实现细节,则如所示:

子路由的核心模块是graphrag-query,负责处理需要查询知识库的问题,包括了对Neo4j图数据库数据的查询和对Microsoft GraphRAG的查询,每种查询方式的实现思路完全不同,但借助LangGraph的路由组件,可以自动的将问题路由到对应的子图进行处理,并根据处理结果返回给用户。 上述流程也是目前基于LangGraph构建底层Agent服务的主流实现方式。
2.1.意图识别模块
意图识别模块是任意基于大模型构建的应用系统都需要重点关注的,所谓的用户意图识别,简单理解就是明确用户输入的问题到底要解决什么问题。用户输入的问题一定会是千奇百怪,如果不能进行有效的意图识别,则无法将问题路由到对应的子图进行处理,也就无法给出准确的回答。比如用户询问商品价格,那么意图识别模块就需要将这个问题识别为商品价格查询,然后路由到对应的子图进行处理。因此,意图识别模块是Multi-Agent架构中非常重要的一环。但实际在实现过程中,借助LangGraph的路由组件,可以非常方便的实现意图识别功能,该模块在完整的Multi-Agent架构中处于第一层级,直接接收用户输入的原始问题,并分派给对应的子图进行处理,如下图所示:

意图识别模块在langGraph中的实现关键是根据提示工程 + 结构化输出。其中提示工程的有效方法是要对每一个子路由能处理的问题进行详细的描述,从而引导大模型给出正确的分类。比如对于电商智能客服场景,我们设计的提示工程如下:
ROUTER_SYSTEM_PROMPT = """你是一个农副集团的智能客服。你的工作是帮助用户解决与公司制度、农副产品价格、价格预测和企业经营数据统计相关的问题。 用户会向你提出询问。你的首要任务是对询问类型进行分类。你必须将用户询问的问题分类为以下类型之一: ## `general-query` 用户提出的是**与以下三大业务完全无关的闲聊、寒暄或通用常识问题**: - 公司管理制度(如行政、人力、财务、采购、IT等制度) - 农副产品价格查询或未来价格预测 - 企业经营数据(如营收、销量、区域分布、同比环比等)分析 - 示例: - “你好!” - “今天天气怎么样?” - “你能做什么?” ## `additional-query` 如果你需要更多信息才能帮助用户,请将用户询问分类为此类。例如: - **制度类**:未说明具体制度类型(如“报销流程” vs “差旅标准”) - **价格/预测类**:未指明农产品种类、地区、时间范围(如“大蒜价格”缺地区;“预测价格”缺品种) - **经营数据类**:未指定指标、时间周期、业务维度(如“销售额”缺时间或区域) ## `graphrag-query` 如果通过查询本地知识库可以回答用户询问,请将其分类为此类。 包括但不限于: - 查询公司内部制度(如“差旅报销标准是什么?”、“采购审批流程?”) - 查询农副产品的**当前或历史价格**(如“山东上周生猪均价?”) - 请求**价格趋势分析或预测**(如“预测下月大豆价格走势”) - 查询或分析**企业经营数据**(如“2025年Q4中B区蔬菜进场量同比变化?”) ## `image-query` 如果用户提供了图片请求提供图片,请将其分类为此类。 ## `file-query` 如果用户上传了文件,请将其分类为此类。 """
而结构化输出,则是借助LangChain的 with_structured_output方法,将大模型的输出结果转换为具体的结构化数据,并通过langGraph定义图的输出类型,生成结构化的输出结果。伪代码如下图所示:
class Router(TypedDict): """Classify user query.""" logic: str type: Literal["general-query", "additional-query", "graphrag-query", "image-query", "file-query"] question: str = field(default_factory=str) # 使用LangChain的with_structured_output方法将大模型的输出结果转换为具体的结构化数据 response = cast( Router, await model.with_structured_output(Router).ainvoke(messages) )
通过上述代码,经过意图识别模块处理后,得到的结果只会是 type:general-query、additional-query、graphrag-query、image-query、file-query 中的一种,从而可以非常方便的将问题路由到对应的子图进行处理。然后,再交由route_query节点,找到对应的子图,准备进行下一步处理。
def route_query(): if _type == "general-query": return "respond_to_general_query" elif _type == "additional-query": return "get_additional_info" elif _type == "graphrag-query": return "create_research_plan" elif _type == "image-query": return "create_image_query" elif _type == "file-query": return "create_file_query"
注意:这里的`respond_to_general_query`、`get_additional_info`、`create_research_plan`、`create_image_query`、`create_file_query` 需要自定义的节点和子图,每个节点或子图,需要根据具体的业务需求进行实现。
2.2.图节点 general-query 实现逻辑
general-query 节点负责处理一般性问题,即是与制度查询、农副产品查询等无关的闲聊问题;

该节点的实现思路并不复杂,可以直接根据提示工程即可实现,所以这个节点的关键就是需要在提示工程中,对“闲聊、一般性”问题应该如何回复进行详细的描述,这就要根据具体的业务需求进行设计。比如对风格、语气、回复策略等进行详细的描述。如下所示:
GENERAL_QUERY_SYSTEM_PROMPT = """你是一个农副批发市场公司的智能客服。你的工作是帮助用户解决与公司制度、农副产品价格、企业经营数据相关的问题。 请以类似淘宝/京东等知名电商客服的风格回复用户,遵循以下规则: ## 基本礼仪 1. 开场必须用"亲~"或"顾客您好~"问候 2. 使用积极、温暖的语气 3. 适当使用emoji表情(如 � � ❤️)增加亲和力 4. 结尾必须表达感谢和继续服务的意愿 ## 回复策略 如果用户问题模糊不清: 1. 先表示理解和重视:"感谢您的咨询~" 2. 友好地指出不明确之处:"不过亲,为了能更好地帮助您..." 3. 引导用户提供更具体信息:"您能告诉我具体是..." 4. 给出一些示例:"比如说,您是想问..." ## 如果问题与公司制度、农副产品价格、企业经营数据相关业务无关: 1. 先表示感谢:"感谢您的咨询~" 2. 委婉说明情况:"不好意思呢亲,这个问题可能不太属于我们的业务范围..." 3. 建议其他帮助渠道:"您可以尝试咨询相关专业的平台/机构..." 4. 表达歉意和继续服务意愿:"如果您有任何关于公司制度、农副产品价格、企业经营数据相关的问题,随时欢迎询问哦~" ## 回复要点 - 保持专业性:提供清晰、准确的信息 - 给出建议:在可能的情况下提供实用的解决方案 - 态度温和:即使是拒绝也要保持友好 - 预设期待:为后续可能的咨询留下互动空间 ## 示例回复格式 ### 模糊问题示例: "亲~感谢您的咨询~� 为了能更准确地帮助您,麻烦您能告诉我具体是那条制度呢?这样我才能...(根据具体情况询问)" ### 无关问题示例: "亲~感谢您的咨询~� 非常抱歉呢,这个问题可能不太属于我们的业务范围,建议您可以...(给出建议)。如果您有任何关于我们商品或服务的问题,随时欢迎询问哦~❤️" 系统已确定用户正在提出一般性问题,不需要查询特定数据库即可回答。以下是分类理由: <logic> {logic} </logic> 记住:无论是哪种情况,都要保持亲切、专业的语气,让用户感受到被重视和理解。 同时,尽量保持回复的简洁性,不要过于冗长,尽量限制在20个字以内。 """
直接将上述提示工程作为`System Message` 传递给大模型,并将其结果存储到`langGraph` 的全局状态中即可。伪代码如下所示:
if settings.AGENT_SERVICE == ServiceType.DEEPSEEK: model = ChatDeepSeek(api_key=settings.DEEPSEEK_API_KEY, model_name=settings.DEEPSEEK_MODEL, temperature=0.7, tags=["general_query"]) else: model = ChatOllama(model=settings.OLLAMA_AGENT_MODEL, base_url=settings.OLLAMA_BASE_URL, temperature=0.7, tags=["general_query"]) system_prompt = GENERAL_QUERY_SYSTEM_PROMPT.format( logic=state.router["logic"] ) messages = [{"role": "system", "content": system_prompt}] + state.messages response = await model.ainvoke(messages) return {"messages": [response]}
2.3 图节点 additional-query 实现逻辑
additional-query 节点负责处理需要更多信息才能回答的问题,比如用户询问制度但是未明确具体制度名称或类型仅问“公司制度有哪些?、用户询问农副产品价格未指明农产品种类、地区、时间范围等关键维度等,这种情况下是没有办法去本地的数据库或者知识库中查询出有效结果的,所以需要引导用户提供更多支撑知识库检索的必要信息。其实现逻辑如下图所示:

首先第一个处理组件是安全护栏,其核心作用是用来判断用户的问题是否属于服务的范围。比如我们的农副产品只有水果类和蔬菜类,但是用户查看服装类,那么这个请求就属于超出服务范围的。因为general-query 处理的是闲聊问题,所以只要用户提问的问题与电商相关,那么就一定不会进入general-query 节点。但是可能存在的问题是:用户即使咨询的是电商问题,但其实和我们的业务是没有关系的。针对这种常见的情况,安全护栏起到的作用就是:先去判断用户的问题是否属于个人服务的范围,如果属于,再继续追问,否则便可以直接回复特定的信息,比如“抱歉,您咨询的问题和我们的业务范围不符,请您咨询其他平台”等,直接结束掉当前会话。
如果判断用户的问题是不是属于业务范围呢?这就需要大家根据具体的业务需求进行设计了。但能够确定的是:提示工程是通用的,通过提示词明确的告诉大模型,哪些问题属于经营范围,哪些问题属于超出经营范围的。比如我们设计的提示词如下所示:
scope_description = """ 回答问题范围,包括但不限于 公司制度类(行政制度、人力制度、采购制度、农副制度、铁岩制度等) 价格/预测类(国内不同年月日省份、城市的水果、蔬菜价格) 经营数据类 """
其二,除了人工编写提示词,还可以灵活的根据业务情况、实现的架构,去找到一些动态的补充信息。比如我们这个项目会使用Neo4j 图数据库去存储商品信息,那Neo4j Schema 就是一个非常好的动态提示词。因为图数据的Schema中的 Node 和 Relationship 、Property 其实反映的就是存储的私有数据的结构,它可以一定程度上体现出农副产品的分类、产品的属性、产品的价格、产品的所属城市、产品的对应市场等信息,以帮助大模型整体去判断用户的问题是否属于回复的范围。 伪代码如下所示:
def retrieve_and_parse_schema_from_graph_for_prompts(graph: Neo4jGraph) -> str: """ 关键点: schema 指的是 Neo4j 数据库的结构描述,用于支撑以下三类核心问答场景: - 公司/平台制度类(如退货规则、交易流程) - 农副产品价格及价格走势预测(需明确品种、地区、时间) - 经营数据查询(如销量、库存、区域分布等) Schema 包含: - 节点类型:如 Product(农产品)、Region(地区)、Market(批发市场)、Policy(制度文档)、TimePeriod(时间周期)等 - 节点属性:如 ProductName(如“大蒜”“生姜”)、RegionName(如“山东金乡”)、Price、ForecastTrend、SalesVolume、Stock 等 - 关系类型:如 SOLD_IN(产品在某市场销售)、 BELONGS_TO_REGION(产品归属某产地)、 GOVERNED_BY(操作受某制度约束)、 RECORDED_AT(数据记录于某时间)等 - 关系属性:如交易日期、价格来源、置信度(用于预测类数据)等 提取出来的 Schema 大致如下: Node properties: - **Product**: ProductName, Category, Unit, OriginRegion... - **Region**: RegionName, Province, City... - **Market**: MarketName, Location, Type... - **Policy**: PolicyTitle, EffectiveDate, Scope... - **TimePeriod**: Year, Month, Week... Relationship properties: - **SOLD_IN**: price, date, source - **BELONGS_TO_REGION**: - **GOVERNED_BY**: applicable_scope 必要性: 1. 动态适应数据库变化:当新增农产品品类、地区或制度文档时,无需修改提示词即可自动适配 2. 提高查询准确性:大模型可依据 Schema 判断用户是否提供了足够维度(如“大蒜价格”缺地区 → 触发 additional-query) 3. 支撑范围判断:帮助模型识别问题是否属于“农副产品+制度+经营数据”三大允许回答范畴,拒答无关请求(如服装、化妆品等) """ schema: str = graph.get_schema # 过滤掉对用户查询不相关的内部结构信息(如调试用的 CypherQuery 节点) if "CypherQuery" in schema: schema = re.sub( get_cypher_query_node_graph_schema(), r"\2", schema, flags=re.MULTILINE ) # 将花括号 {} 替换为方括号 [],避免与 LangChain 的 ChatPromptTemplate # 中的变量占位符(如 {input})发生模板渲染冲突 schema = schema.replace("{", "[").replace("}", "]") return schema
(Product)-[:HAS_PRICE]->(Price) (Product)-[:HAS_INVENTORY]->(Inventory) (Product)-[:BELONGS_TO]->(Category) (TimePeriod)-[:RECORDS]->(Price) (Policy)-[:APPLIES_TO]->(OperationType)
Product 和 Price 的查询;但若用户问“运动鞋价格”,Schema 同样会匹配成功——然而“运动鞋”并不属于我们的经营范围。因此,仅靠 Schema 无法完成领域归属判断。- 制度类(如退货规则、交易流程、平台政策)
- 农副产品价格及价格走势预测(需包含品种、地区、时间等关键维度)
- 经营数据类(如销量、库存、区域分布、市场行情等)
"continue" 或 "end" 执行不同分支。GUARDRAILS_SYSTEM_PROMPT = """ 你是农业电商平台的范围检查组件,负责判断用户问题是否属于系统可回答的三大范畴: 1. 公司/平台制度类(如退货政策、交易规则、售后服务流程) 2. 农副产品价格及价格走势预测(如“山东金乡大蒜本周价格”“生姜未来一个月价格趋势”) 3. 经营数据类(如销量、库存、区域销售分布、市场行情统计等) 请严格遵循以下规则: - 如果问题明确属于上述三类之一,请仅输出:"continue" - 如果问题涉及非农副产品(如服装、鞋类、化妆品、电子产品、体育用品等),或与制度、价格、经营数据无关(如天气、新闻、娱乐、政治等),请仅输出:"end" - 若问题模糊但可能相关(例如未指明农产品种类但上下文暗示为农业相关),请倾向输出:"continue" - 判断时应结合数据库结构(如存在 Product、Region、Price、Policy 等实体)和业务范围双重依据 - 严禁输出除 "continue" 或 "end" 之外的任何内容 """
根据格式化输出的结果,返回不同的响应
if guardrails_output.decision == "end": logger.info("-----Fail to pass guardrails check-----") return {"messages": [AIMessage(content="抱歉,暂时没有这方面的内容信息,可以在别的地方搜索看看哦~")]} else: logger.info("-----Pass guardrails check-----") system_prompt = GET_ADDITIONAL_SYSTEM_PROMPT.format( logic=state.router["logic"] ) messages = [{"role": "system", "content": system_prompt}] + state.messages response = await model.ainvoke(messages) return {"messages": [response]}
2.4.graphrag-query 处理逻辑
graphrag-query 是智能问答系统中最重要的一个实现逻辑,它会包含Microsoft GraphRAG 和 Neo4j GraphRAG 的混合检索,根据用户的问题,选择合适本地知识库获取实时的结果。其内部实现非常负责,因此它并不是一个简单的节点,而是langGraph 中构建Multi-Agent 架构的结构:子图(SubGraph)。

同get-additional-query 节点一样,graphrag-query 节点也会包含一个安全护栏,用于判断用户的问题是否属于回答范围。如果属于回答范围,则继续执行后续的逻辑,否则直接结束当前会话。因为用户如果最初提问的问题类似:请问今日北京市的榴莲价格是多少?,这种问题包含了详细的信息,所以不会进入到get-additional-query 去追问补充的信息。这部分的处理逻辑和get-additional-query 节点是一样的,这里就不再赘述了。
接下来,当用户的问题被判断为属于回答范围后,则会实际的去查询对应的工具,比如不同的知识库,但在执行实际的查询之前,我们需要先通过任务分解组件去拆分用户输入进来的问题,因为用户的问题可能包含多个子任务,比如:请问今日北京的榴莲价格和上海的草莓价格是多少,这明显是两个任务,分别为:北京的榴莲价格和上海的草莓价格。除此以外,对于多跳的查询,也需要通过任务分解组件去拆分。
在具体的实现上,任务分解组件会根据用户的问题,生成一个任务列表,然后根据任务列表,每一个子任务,根据任务的性质,选择合适的工具去执行,其实现的核心思路是:
PLANNER_SYSTEM_PROMPT = """ 你是一个农批市场公司智能客服系统中的任务规划组件。 你的职责是分析用户的查询,并将其拆分为独立、可执行的子任务。 请严格遵循以下规则,所有子任务必须属于以下三类之一: 【公司制度类】仅限查询公司内部成文制度,包括: - 行政制度、人力制度、采购制度、农副制度、铁岩制度等 - 必须指向具体制度条文(如“采购制度中关于苹果议价的标准”),不得生成建议、推论或操作指导 【价格/预测类】仅限查询国内水果、蔬菜类农副产品的价格或预测,且必须包含: - 具体品种(如“苹果”“白菜”,不可为“饮料”“巧克力”“肉类”) - 明确地区(省/市,如“山东”“北京”) - 明确时间(如“今日”“2025年6月”“未来一周”) 【经营数据类】仅限查询企业实际运营产生的结构化数据,包括: - 产品清单、订单记录、库存数量、供应商履约记录、客户评价、物流时效等 - 不得包含外部市场推测、第三方数据或未记录的主观信息 ────────────────────────────── 核心拆分与合并规则: 1. **强制拆分条件**: - 涉及不同数据源类型(如制度 + 价格)必须拆分 - 查询对象不属于同一农产品类别(如“苹果价格”和“白菜库存”)应拆分 - 包含非农副产品(如“饮料”“巧克力”“服装”)的问题,其价格/预测部分**不得归入价格类**,仅可作为经营数据(如“在售清单”) 2. **允许合并条件**: - 同一制度文件内的多个条款(如“事假申请条件”和“事假薪酬标准”)可保留为多个子任务,但都标记为制度类 - 农副产品价格合理性判断需同时依赖“当前市场价格”(价格类)和“采购制度议价标准”(制度类),应拆分为两个独立子任务 3. **禁止行为**: - 不得将非水果/蔬菜类产品(如肉类、水产、加工食品)纳入价格/预测类 - 不得生成“如何设计”“如何处理”“建议”等非查询型任务 - 不得假设用户意图或补充未提及的维度(如用户未提地区,不可自动添加“全国”) 4. **农批特有场景约束**: - 仅当问题明确涉及**水果或蔬菜**时,才可触发价格/预测类子任务 - “冷链物流”“质检”“溯源”等仅在关联水果/蔬菜且有对应制度或经营记录时才可拆分 - 所有“赔偿”“索赔”类问题,仅可拆分为: • 制度类:相关合同或农副制度中的赔偿条款 • 经营数据类:实际物流记录、质检报告、订单信息 • 价格类:事发当日该水果/蔬菜的市场参考价(需满足品种+地区+时间) 5. **输出格式要求**: - 每个子任务必须是**可直接查询的问句** - 不标注 type 前缀(如 `type:制度`),但内容必须隐含所属类别 - 子任务之间无依赖、无重复、无推理 ────────────────────────────── 示例: - 问题:"供应商B的信用评级如何?他们的苹果价格是否合理?" 子任务:["供应商B的历史信用评级与履约记录是什么?", "当前山东省苹果的市场价格是多少?", "采购制度中关于苹果价格议价的判定标准是什么?"] - 问题:"如何设计未来一个月的蔬菜采购计划?" 子任务:["未来30天北京市主要蔬菜(如白菜、土豆)的价格预测趋势如何?", "各供应商近三个月蔬菜供应的履约记录如何?", "采购制度中蔬菜供应商选择与配额分配的标准是什么?"] - 问题:"私车公用怎么申请?" 子任务:["行政制度中私车公用的具体适用条款是什么?", "行政制度中私车公用的申请条件是什么?", "行政制度中私车公用的审批流程是什么?"] - 问题:"年假用完后还能请事假吗?事假期间工资怎么算?" 子任务:["人力制度中事假的申请条件是什么?", "人力制度中事假期间的薪酬发放标准是什么?"] - 问题:"新员工入职需要准备哪些材料?试用期多久?" 子任务:["人力制度中新员工入职所需提交的材料清单是什么?", "人力制度中试用期的时长及转正考核标准是什么?"] """
对拆分出的每一个子任务,替换后续流程中工具调用时接收到的query 参数,即:让接下来的工具调用,执行的查询语句,是根据子任务生成的。伪代码如下所示:
planner_task_decomposition = { "next_action": next_action, "tasks": planner_output.tasks or [ Task( question=state.get("question", ""), parent_task=state.get("question", ""), ) ] }
有了子任务后,接下来就是根据子任务的性质,选择合适的工具去执行。因此我们需要用到LangGraph的条件边,在条件边中,定义都有哪些子任务,以及每个子任务都是做什么的,其实现形式的伪代码如下:
async def tool_selection( state: ToolSelectionInputState, ) -> Command[Literal["cypher_query", "predefined_cypher", "customer_tools"]]: """ Choose the appropriate tool for the given task. """ # 调用工具选择链,生成针对每个任务要调用的工具名称和参数 tool_selection_output: BaseModel = await tool_selection_chain.ainvoke( {"question": state.get("question", "")} ) # 根据路由到对应的工具节点 if tool_selection_output is not None: tool_name: str = tool_selection_output.model_json_schema().get("title", "") tool_args: Dict[str, Any] = tool_selection_output.model_dump() if tool_name == "predefined_cypher": return Command( goto=Send( "predefined_cypher", { "task": state.get("question", ""), "query_name": tool_name, "query_parameters": tool_args, "steps": ["tool_selection"], }, ) ) elif tool_name == "cypher_query": return Command( goto=Send( "cypher_query", { "task": state.get("question", ""), "query_name": tool_name, "query_parameters": tool_args, "steps": ["tool_selection"], }, ) ) else: return Command( goto=Send( "customer_tools", { "task": state.get("question", ""), "query_name": tool_name, "query_parameters": tool_args, "steps": ["tool_selection"], }, ) )
这里面用到的Send是LangGraph 中的一个API,主要是通过Map-Reduce对子任务执行并行的处理,然后汇总所有已完成的子任务结果。因为Nodes 和 Edges 都是预先定义的,并且基于相同的共享状态进行操作。但对这种动态的执行过程,我们不知道会切分出多少个子任务,也不知道每个子任务会使用哪个工具,所以如果想要多个子任务都能独立应用全局的共享状态,就需要使用Map-Reduce 的执行方式。官方文档:
https://langchain-ai.github.io/langgraph/concepts/low_level/#send

接下来我们就要实际定义执行不同子任务的工具节点了。首先来看Text2Cypyer 工具。
2.4.1.Text2Cypyer 工具定义
在Text2Cypyer 工具中主要操作的是结构化存储的Neo4j 数据库,因此该工具的核心是:将用户的自然语言问题先转化为Cypher 查询语句,然后再去实际的Neo4j 数据库中执行查询语句,最后将查询结果返回给用户。

关于如何借助大模型根据用户的问题生成较为准确的Cypher 查询语句,这里核心采用了两种方法,其一是预构建Cypher 字典,其二是直接借助大模型生成Cypher 查询语句。如下图所示:

- 预构建 Cypher 字典
预构建Cypher 字典的本质是根据自有数据的情况,人工定义出一些可以正常运行,且能正确返回结果的Cypher 查询语句,这类查询语句可以基于业务类型进行分类,也可以基于数据类型进行分类。(具体以哪种方式分类,取决于自有数据的情况灵活调整)其形式如下:
all_examples = {
"制度查询": [
{
"question": "员工差旅报销的标准是什么?",
"cypher": """MATCH (p:Policy {type: '差旅报销'})
RETURN p.title, p.content, p.effectiveDate"""
},
{
"question": "采购审批流程有哪些步骤?",
"cypher": """MATCH (p:Policy {type: '采购管理'})
WHERE p.subType = '审批流程'
RETURN p.steps AS approvalSteps"""
},
{
"question": "IT设备申请需要哪些材料?",
"cypher": """MATCH (p:Policy)-[:REQUIRES]->(m:Material)
WHERE p.type = 'IT资产' AND p.subType = '申请'
RETURN m.name AS requiredMaterials"""
}
],
"农副产品价格查询": [
{
"question": "2025年12月山东大蒜的平均收购价是多少?",
"cypher": """MATCH (r:Region {name: '山东'})<-[:LOCATED_IN]-(pr:PriceRecord)
MATCH (pr)-[:FOR_PRODUCT]->(ap:AgriProduct {name: '大蒜'})
WHERE pr.date >= date('2025-12-01') AND pr.date <= date('2025-12-31')
RETURN avg(pr.price) AS avgPrice, ap.unit"""
},
{
"question": "最近一周玉米价格最高的地区是哪里?",
"cypher": """MATCH (r:Region)<-[:LOCATED_IN]-(pr:PriceRecord)-[:FOR_PRODUCT]->(ap:AgriProduct {name: '玉米'})
WHERE pr.date >= date() - duration({days: 7})
RETURN r.name AS region, max(pr.price) AS maxPrice
ORDER BY maxPrice DESC
LIMIT 1"""
},
{
"question": "河南和河北的小麦价格对比(2025年Q4)",
"cypher": """MATCH (r:Region)<-[:LOCATED_IN]-(pr:PriceRecord)-[:FOR_PRODUCT]->(ap:AgriProduct {name: '小麦'})
WHERE r.name IN ['河南', '河北']
AND pr.date >= date('2025-10-01') AND pr.date <= date('2025-12-31')
RETURN r.name, avg(pr.price) AS avgPrice
ORDER BY r.name"""
}
],
"企业经营数据分析": [
{
"question": "2025年全年集团总营收是多少?",
"cypher": """MATCH (m:BusinessMetric {metricName: 'revenue'})
WHERE m.year = 2025
RETURN sum(m.value) AS totalRevenue"""
},
{
"question": "各子公司2025年Q4净利润排名",
"cypher": """MATCH (s:Subsidiary)-[:REPORTED]->(m:BusinessMetric {metricName: 'net_profit', quarter: 'Q4', year: 2025})
RETURN s.name, m.value AS netProfit
ORDER BY netProfit DESC"""
},
{
"question": "华东区2025年农副产品销售额同比增长率",
"cypher": """MATCH (m2025:BusinessMetric {region: '华东', category: 'agri', metricName: 'sales', year: 2025})
MATCH (m2024:BusinessMetric {region: '华东', category: 'agri', metricName: 'sales', year: 2024})
RETURN ((m2025.value - m2024.value) / m2024.value * 100) AS yoyGrowthPercent"""
}
]
}
- 大模型自动生成
除了人工构建,另外一种高效的方法是借助大模型自动化生成Cypher 查询语句。如果采用这种方式,需要给到大模型的核心信息就是Neo4j 的Schema 信息,如下格式所示:
如下是我的 Neo4j 数据库中的 节点和关系情况:
- 节点类型
- Policy - 公司制度文档
- AgriProduct - 农副产品(如玉米、大蒜、生猪等)
- PriceRecord - 价格记录(含时间、地区、价格)
- Region - 地区(省/市)
- BusinessMetric - 企业经营指标(如营收、利润、销量等)
- TimePeriod - 时间周期(年、季度、月)
- 边类型
- APPLIES_TO - 制度适用于某类场景或部门
- RECORDED_IN - 价格记录归属于某个地区
- FOR_PRODUCT - 价格记录对应某个农产品
- REPORTED_BY - 经营指标由某子公司或部门上报
- MEASURED_AT - 经营指标在某个时间周期内统计
节点属性表(居中对齐格式)
节点类型
| 节点类型 | 标识字段 | 属性 |
|---|---|---|
| Policy | policyId | title, type(如“差旅报销”“采购管理”), subType, content, effectiveDate, department |
| AgriProduct | productId | name(如“玉米”“大蒜”), category(粮食/蔬菜/畜牧), unit(元/公斤) |
| PriceRecord | recordId | price, date(YYYY-MM-DD), source(数据来源) |
| Region | regionId | name(如“山东”“河南”), level(省/市), code |
| BusinessMetric | metricId | metricName(如“revenue”, “net_profit”), value, unit, subsidiary |
| TimePeriod | periodId | year, quarter, month, startDate, endDate |
| 边类型 | 源节点 | 目标节点 | 说明 |
|---|---|---|---|
| APPLIES_TO | Policy | Department/Role | 制度适用对象(可简化为属性,此处暂略) |
| RECORDED_IN | PriceRecord | Region | 价格发生在某地区 |
| FOR_PRODUCT | PriceRecord | AgriProduct | 价格对应某农产品 |
| REPORTED_BY | BusinessMetric | Subsidiary | 指标由某子公司上报(subsidiary 可作为属性) |
| MEASURED_AT | BusinessMetric | TimePeriod | 指标统计的时间周期 |
💡 注:为简化,Department、Subsidiary等可先作为Policy或BusinessMetric的属性字段,无需单独建节点(除非需复杂关联)。
请基于以上信息,按照业务场景进行分类,构建出对应的 Cypher 字典, 并给出对应的 Cypher 查询语句。输出形式如下:
all_examples = {
"制度问答": [
{
"question": "差旅报销的标准是什么?",
"cypher": """MATCH (p:Policy {type: '差旅报销'})
RETURN p.title AS 标题, p.content AS 内容, p.effectiveDate AS 生效日期"""
},
{
"question": "采购审批流程有哪些步骤?",
"cypher": """MATCH (p:Policy {type: '采购管理', subType: '审批流程'})
RETURN p.content AS 审批流程"""
},
{
"question": "IT设备申请需要哪些材料?",
"cypher": """MATCH (p:Policy {type: 'IT资产', subType: '申请材料'})
RETURN p.content AS 所需材料"""
}
],
"农副产品价格查询": [
{
"question": "2025年12月山东大蒜的平均价格是多少?",
"cypher": """MATCH (pr:PriceRecord)-[:FOR_PRODUCT]->(ap:AgriProduct {name: '大蒜'})
MATCH (pr)-[:RECORDED_IN]->(r:Region {name: '山东'})
WHERE pr.date >= date('2025-12-01') AND pr.date <= date('2025-12-31')
RETURN avg(pr.price) AS 平均价格, ap.unit AS 单位"""
},
{
"question": "最近一周玉米价格最高的省份是哪里?",
"cypher": """MATCH (pr:PriceRecord)-[:FOR_PRODUCT]->(ap:AgriProduct {name: '玉米'})
MATCH (pr)-[:RECORDED_IN]->(r:Region)
WHERE pr.date >= date() - duration({days: 7})
RETURN r.name AS 省份, max(pr.price) AS 最高价格
ORDER BY 最高价格 DESC
LIMIT 1"""
},
{
"question": "河南和河北的小麦价格对比(2025年第四季度)",
"cypher": """MATCH (pr:PriceRecord)-[:FOR_PRODUCT]->(ap:AgriProduct {name: '小麦'})
MATCH (pr)-[:RECORDED_IN]->(r:Region)
WHERE r.name IN ['河南', '河北']
AND pr.date >= date('2025-10-01') AND pr.date <= date('2025-12-31')
RETURN r.name AS 地区, avg(pr.price) AS 平均价格
ORDER BY 地区"""
}
],
"经营数据查询": [
{
"question": "2025年全年集团总营收是多少?",
"cypher": """MATCH (m:BusinessMetric {metricName: 'revenue'})
WHERE m.year = 2025
RETURN sum(m.value) AS 总营收"""
},
{
"question": "各子公司2025年Q4净利润排名",
"cypher": """MATCH (m:BusinessMetric {metricName: 'net_profit', year: 2025, quarter: 'Q4'})
RETURN m.subsidiary AS 子公司, m.value AS 净利润
ORDER BY 净利润 DESC"""
},
{
"question": "华东区域2025年农副产品销售额同比增长率",
"cypher": """MATCH (m2025:BusinessMetric {region: '华东', category: 'agri', metricName: 'sales', year: 2025})
MATCH (m2024:BusinessMetric {region: '华东', category: 'agri', metricName: 'sales', year: 2024})
WITH m2025.value AS v2025, m2024.value AS v2024
RETURN ((v2025 - v2024) / v2024 * 100) AS 同比增长率_percent"""
}
]
}
除此以外,还可以定期从业务日志中进行数据抽取,以不断的丰富 Cypher 字典,同时存储到向量数据库中,通过向量检索的方式进行 Cypyer 字典的长期更新和维护以及使用,获取有效Cypher 的方案非常多样。而不论采用哪种方案,虽然此方案需要人工介入,但综合看,一个高质量的预构建Cypher 字典用于大模型生成实时需求Cypher的Few-shot 效果非常稳定同时准确率较高。结合Few-shot 的完整提示模版如下所示:
[
(
"system",
(
"根据输入的问题,将其转换为Cypher查询语句。不要添加任何前言。"
"不要在响应中包含任何反引号或其他标记。注意:只返回Cypher语句!"
),
),
(
"human",
(
"""你是一位Neo4j专家。根据输入的问题,创建一个语法正确的Cypher查询语句。
不要在响应中包含任何反引号或其他标记。只使用MATCH或WITH子句开始查询。只返回Cypher语句!
以下是数据库模式信息:
{schema}
下面是一些问题和对应Cypher查询的示例:
{fewshot_examples}
用户输入: {question}
Cypher查询:"""
),
),
]
)
如果想完全脱离人工,全部交由大模型生成 Cypher 语句,这里提供一个借助 Neo4j的 neo4j-graphrag 工具自动化生成 Cypher 语句的示例:对应的代码如下:
pip install neo4j-graphrag
from neo4j_graphrag.retrievers import Text2CypherRetriever
from neo4j_graphrag.llm import OpenAILLM
import time
import pandas as pd
from neo4j import GraphDatabase
NEO4J_URI="bolt://localhost"
NEO4J_USERNAME="neo4j"
NEO4J_PASSWORD="Snowball2019"
NEO4J_DATABASE="neo4j"
driver = GraphDatabase.driver(
NEO4J_URI,
auth=(NEO4J_USERNAME, NEO4J_PASSWORD)
)
# 这里可以填写 DeepSeek 模型
client = OpenAILLM(api_key="sk-7affff34430", base_url="https://api.deepseek.com", model_name='deepseek-chat')
# 定义用户输入:
examples = [
"USER INPUT: 'Which actors starred in the Matrix?' QUERY: MATCH (p:Person)-[:ACTED_IN]->(m:Movie) WHERE m.title = 'The Matrix' RETURN p.name"
]
# 初始化检索器
retriever = Text2CypherRetriever(
driver=driver,
llm=client,
neo4j_schema=neo4j_schema, # 可以通过 retrieve_and_parse_schema_from_graph_for_prompts 获取动态的Schema
examples=examples,
)
# 执行检索:
query_text = "企业报销流程是怎么样的?"
print(retriever.search(query_text=query_text))
2.4.2.workflow方案
无论采用哪种生成Cypher 语句的方案,其最终生成的Cypher 语句都可能存在问题,因此一个比较健壮的流程是需要包含校验-自我纠正-执行-反馈的完整闭环,所以在项目中就需要设计一套完整的 Workflow 的实现思路:
其中对于校验,我们提供了多种校验策略,包括:
-
语法校验:通过
EXPLAIN关键字,用于分析和展示Cypher查询的执行计划,而不实际执行该查询,对语法进行校验; -
权限控制:一般提供给大模型使用的
Cypher语句,会限制一些权限,比如:不能删除数据,不能更改表结构等。所以这一步很关键; -
关系方向校正:在图数据库中,关系是有方向性的。例如,(a)-[:RELATION]->(b) 表示从节点 a 到节点 b 的关系。如果查询中使用了错误的方向,会导致查询结果不准确。借助
langchain_neo4j的CypherQueryCorrector来校验Cypher语句的语法,比如 :MATCH (a:Person)-[r:FRIENDS_WITH]->(b:Person) ,如果r:FRIENDS_WITH 是反向的,则会被纠正为:MATCH (a:Person)-[r:FRIENDS_WITH]->(b:Person) -
更高阶:使用大模型辅助校验
Cypher语句的语法,主要是用来检查查询中涉及的节点和属性是否在Neo4j数据库中存在。- 获取动态的
Neo4j Schema, 构建Pydantic模型,输出 针对当前Cypher语句的error和mapping的错误信息- 如果存在
error,直接记录 - 如果存在
mapping的错误,先对字符串类型进行映射检查,构建一个Cypher查询,检查数据库中是否存在具有指定属性值的节点。(因为Neo4j重要的属性(如名称、ID、标签等)通常是字符串类型, 而用户的查询基本都是字符串类型),
- 如果存在
- 获取动态的
这里有很多策略,比较常用的就是:
1. 如果存在 `error`, 可以直接终止对话,也可以创建一个 自修正节点进行 Cypher 的自我修复,然后再执行实际的查询
2. 如果存在 `mapping`, 则说明用户的问题在数据库中不存在:
- 可以重新进入提问状态,与用户确认信息,或者要求提供更多的信息
- 根据历史会话等再次重新生成 Cypher 语句
- 直接结束,直接告诉用户没有查询到相关信息

2.5.预构建的 Cypher 工具节点
Text2Cypher是通过大模型生成 Cypher语句的, 但是大模型生成的Cypher语句可能存在一些问题而且耗时较长,所以可以预构建一个Cypher工具节点,这个预构建的Cypher工具节点 与Text2Cypher的Cypher字典不同,Text2Cypher的Cypher字典是用来作为Few-shot`的示例填充到提示模版中,从而引导大模型生成正确的Cypher 语句,而预构建的Cypher工具节点是用来直接获取到对应的Cypher语句进行执行,它是作为工具节点来使用的。如下图所示:
from typing import Dict
predefined_cypher_dict: Dict[str, str] = {
# ======================
# 制度问答类(Policy Q&A)
# ======================
"policy_by_type": """
MATCH (p:Policy {type: $policy_type})
RETURN p.title AS 标题, p.content AS 内容, p.effectiveDate AS 生效日期
LIMIT 1
""",
"policy_by_keyword": """
MATCH (p:Policy)
WHERE toLower(p.content) CONTAINS toLower($keyword)
OR toLower(p.title) CONTAINS toLower($keyword)
RETURN p.type AS 类型, p.title AS 标题, p.content AS 摘要
LIMIT 3
""",
# ==============================
# 农副产品价格查询(Agri-Price)
# ==============================
"agri_price_by_product_region_date": """
MATCH (pr:PriceRecord)-[:FOR_PRODUCT]->(ap:AgriProduct {name: $product_name})
MATCH (pr)-[:RECORDED_IN]->(r:Region {name: $region_name})
WHERE pr.date = date($date_str)
RETURN ap.name AS 农产品, r.name AS 地区, pr.price AS 价格, ap.unit AS 单位
""",
"agri_avg_price_last_n_days": """
MATCH (pr:PriceRecord)-[:FOR_PRODUCT]->(ap:AgriProduct {name: $product_name})
MATCH (pr)-[:RECORDED_IN]->(r:Region {name: $region_name})
WHERE pr.date >= date() - duration({days: $days})
RETURN ap.name AS 农产品, r.name AS 地区,
avg(pr.price) AS 近N日均价,
min(pr.date) AS 起始日期,
max(pr.date) AS 截止日期
""",
"agri_price_trend_compare_regions": """
MATCH (pr:PriceRecord)-[:FOR_PRODUCT]->(ap:AgriProduct {name: $product_name})
MATCH (pr)-[:RECORDED_IN]->(r:Region)
WHERE r.name IN $region_list
AND pr.date >= date($start_date)
AND pr.date <= date($end_date)
RETURN r.name AS 地区, avg(pr.price) AS 平均价格
ORDER BY 平均价格 DESC
""",
# ==================================
# 企业经营数据查询(Business Metrics)
# ==================================
"biz_metric_by_name_year": """
MATCH (m:BusinessMetric {metricName: $metric_name})
WHERE m.year = $year
RETURN sum(m.value) AS 总值, m.unit AS 单位
""",
"biz_metric_by_subsidiary_quarter": """
MATCH (m:BusinessMetric {metricName: $metric_name, year: $year, quarter: $quarter})
RETURN m.subsidiary AS 子公司, m.value AS 数值
ORDER BY m.value DESC
""",
"biz_yoy_growth_rate": """
MATCH (m_current:BusinessMetric {metricName: $metric_name, year: $current_year, region: $region})
MATCH (m_previous:BusinessMetric {metricName: $metric_name, year: $previous_year, region: $region})
WITH m_current.value AS curr, m_previous.value AS prev
WHERE prev > 0
RETURN ((curr - prev) / prev * 100) AS 同比增长率_percent
""",
"top_n_products_by_sales": """
MATCH (m:BusinessMetric {metricName: 'sales_volume', category: 'agri'})
WHERE m.year = $year
RETURN m.productName AS 农产品, m.value AS 销量
ORDER BY m.value DESC
LIMIT $top_n
"""
}
2.6.自定义Microsoft GraphRAG 的检索节点
当通过 Microsoft GraphRAG构建好了离线索引后,直接调用其Python的REST接口,就可以将自然语言传入到GraphRAG的Workflow中进行检索,无需Text2Cypher的转化过程。该工具的使用方法并不复杂,核心主要在于离线索引的构建阶段
2.7.图像识别节点
在智能问答系统中,上传图片是用户非常常见的一种交互方式,但这个过程只需要接入视觉大模型,就可以将图片中的文字识别出来,从而转化为自然语言,然后就可以进行后续的问答流程。整个过程并不复杂,因此该工具仅需要一个普通的LangGraph 节点即可快速实现,核心代码如下:
payload = {
"model": vision_model,
"messages": [
{
"role": "system",
"content": "你是一个专业的图像分析助手。请详细分析图片中的内容,特别关注产品细节、名称、城市等信息。"
},
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_data}"
}
}
]
}
],
"max_tokens": 4000,
"temperature": 0.7
}
# 发送API请求
async with aiohttp.ClientSession() as session:
async with session.post(
f"{base_url}/chat/completions",
headers=headers,
json=payload,
timeout=60 # 增加超时时间
) as response:
if response.status == 200:
result = await response.json()
image_description = result["choices"][0]["message"]["content"]
logger.info(f"Successfully processed image and generated description")
# 使用图片描述和用户问题生成最终回复
# 从lg_prompts导入智能回答模板
# 构建回复请求
if settings.AGENT_SERVICE == ServiceType.DEEPSEEK:
model = ChatDeepSeek(api_key=settings.DEEPSEEK_API_KEY, model_name=settings.DEEPSEEK_MODEL, temperature=0.7, tags=["image_query"])
else:
model = ChatOllama(model=settings.OLLAMA_AGENT_MODEL, base_url=settings.OLLAMA_BASE_URL, temperature=0.7, tags=["image_query"])
# 使用专门的图片查询提示模板
system_prompt = GET_IMAGE_SYSTEM_PROMPT.format(
image_description=image_description
)
messages = [{"role": "system", "content": system_prompt}] + state.messages
response = await model.ainvoke(messages)
return {"messages": [response]}
else:
error_text = await response.text()
logger.error(f"Vision API Request Failed: {response.status} - {error_text}")
return {"messages": [AIMessage(content=f"抱歉,我无法查看这张图片,请重新上传。")]}
该工具节点主要用于将图片中的文字识别出来,然后转化为自然语言,从而进行后续的问答流程。

浙公网安备 33010602011771号