目录

项目简介

一、构建知识图谱:

二、 Cypher工具的开发

1、链接数据库:

2、Langchain 与 Neo4j 结合测试:

三、tool

1. email_tool

2. login_tool

 3. neo4j_tool

四、service

1. login_service

2. rag_neo4j_service

3. chat_service

五、agent

1. login_agent

2. chat_agent

六、view

1. login_view

2. chat_neo4j_view

七、前端

1. login:

2. chat:

八、项目展示


项目简介

 苯人这次的项目如标题所示,是将 Langchain+Neo4j+Agent 结合的案例,关于电商销售的。

  随着电商行业的不断发展,平台积累了庞大的用户、商品与交易数据。如何从这些复杂数据中快速挖掘有价值的信息,满足用户个性化的消费需求,已经成为一个重要课题。传统的数据库查询方式操作复杂,用户往往需要专业知识才能获取所需信息,缺乏灵活性与智能化。本项目基于 LangChain 框架与 Neo4j 知识图谱,结合大语言模型的自然语言处理能力,构建了一个智能电商销售系统。系统不仅能够支持用户的自动注册与登录(包含智能体自动生成账号、验证码邮件发送与后台数据库写入),还实现了基于知识图谱的数据问答,用户只需提出自然语言问题,即可获得关于电商销售的个性化回答。例如,用户可以直接询问“用户1的消费偏好品牌”,系统会自动转换为图数据库查询并返回结果。通过这一项目,能够探索知识图谱与大模型在电商场景中的结合应用,提升数据交互的智能化与便捷性。

项目架构主要分为三层:数据层,模型层,应用层。

数据层:用 Neo4j 构建电商知识图谱,存储用户、订单、商品等;

模型层:基于 LangChain 框架,接入大语言模型(LLaMA 等),通过 Agent 调用 Neo4j 的查询工具,实现基于知识图谱的 RAG,支持个性化推荐和历史问题查询;

应用层:用户通过前端交互,像注册、登录等流程也交由 Agent 驱动,Agent 会自动选择和调用合适的工具去完成多步骤任务。

整体来说, Agent 起到大脑的作用,Neo4j 是知识库,Langchain 负责把它们结合起来,应用层把这些能力对外提供服务。

一句话总结流程:用户从前端发送请求 -> view(视图层) -> agent(智能体层) -> service(服务层) -> tool(工具层) -> 数据库 -> 一路返回 LLM 生成的解释性答案给用户

这里的每一个层其实就是文件夹,但其实写代码的顺序不是按照上面的流程来的,下面分步说明:

一、构建知识图谱:

在创建数据库的时候要运行这段代码:

:use system;
CREATE DATABASE ecommerce   

ecommerce 就是数据库名,然后在这个数据库里创建:

然后生成的数据长这样,以 User 和 PLACED 关系为例:

我是让AI生成了100个用户,15个品类,30个品牌,500个商品,1500个订单,适用于我们这个案例,接下来就是写查询语句看节点关系这些是否正确创建了,比如测几个常用查询:

用户买得最多的商品前三:

MATCH (u:User {userId:"U1"})-[:PLACED]->(:Order)-[c:CONTAINS]->(p:Product)
RETURN p.name AS Product, sum(c.quantity) AS TotalQuantity
ORDER BY TotalQuantity DESC
LIMIT 3;

运行结果:

用户最近一次下单的内容:

MATCH (u:User {userId:"U1"})-[:PLACED]->(o:Order)-[c:CONTAINS]->(p:Product)
WITH u, o, p ORDER BY o.createTime DESC
WITH u, collect(DISTINCT {order:o.orderId, product:p.name})[0] AS LastOrder
RETURN LastOrder;

用户的品牌偏好:

MATCH (u:User {userId:"U1"})-[:PLACED]->(:Order)-[c:CONTAINS]->(p:Product)-[:PRODUCED_BY]->(b:Brand)
RETURN b.name AS Brand, sum(c.quantity) AS TotalBought
ORDER BY TotalBought DESC;

销量最好的商品前十:

MATCH (p:Product)<-[c:CONTAINS]-(:Order)
OPTIONAL MATCH (p)-[:BELONGS_TO]->(cat:Category)
OPTIONAL MATCH (p)-[:PRODUCED_BY]->(b:Brand)
RETURN p.name AS Product,
       cat.name AS Category,
       b.name AS Brand,
       sum(c.quantity) AS TotalSold
ORDER BY TotalSold DESC
LIMIT 10;

品牌对比销量:

MATCH (b:Brand)<-[:PRODUCED_BY]-(p:Product)<-[c:CONTAINS]-(:Order)
WHERE b.name IN ["苹果","华为"]
OPTIONAL MATCH (p)-[:BELONGS_TO]->(cat:Category)
RETURN b.name AS Brand, p.name AS Product, cat.name AS Category,
       sum(c.quantity) AS TotalSold
ORDER BY Brand, TotalSold DESC;

以上都有结果就说明数据库基本是没问题了,下一步就是 Cypher工具的开发,包括编写一个可靠的函数,接收自然语言问题,利用LLM(或经过微调的模型)生成Cypher查询,也就是让模型自己写上面那种查询语句,代码如下:

二、 Cypher工具的开发

因为要测试通过了才可以正式写入后端,所以先测试:

1、链接数据库:

# 导入操作使用到的包
from langchain_neo4j import Neo4jGraph

url ="bolt://localhost:7687"
username="neo4j"
password="12345678"
data_base = "ecommerce"
graph = Neo4jGraph(url=url, username=username, password=password, database=data_base)
print("链接成功!")

接下来就是将 Langchain 与 知识图谱结合起来:

2、Langchain 与 Neo4j 结合测试:

import os
from dotenv import load_dotenv
from langchain.chains import GraphCypherQAChain
from langchain_community.graphs import Neo4jGraph
from langchain_community.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from my_chat.my_chat_model import ChatModel

import warnings
from langchain_core._api import LangChainDeprecationWarning
warnings.filterwarnings("ignore", category=LangChainDeprecationWarning)

#langchain结合neo4j完成问答案例
def test1():
    load_dotenv()
    #获取在线模型
    chat = ChatModel()
    llm = chat.get_online_model()
    prompt = ChatPromptTemplate.from_messages([
        ("system", """你是一个超级专业且很很会看注意事项的Neo4j Cypher查询生成器,现在有一个包含用户购买记录以及品类品牌信息的知识图谱,
            根据用户的问题生成合适的Cypher查询。生成查询时请严格遵循以下规则:
        
            数据库模式:
            - User节点属性: name, userId (例如: "U1", "U2")
            - Product节点属性: productId (例如: "P1"), price, name
            - Category节点属性: name, categoryId (例如: "C1")
            - Brand节点属性: brandId (例如: "B1"), name
            - Order节点属性: totalAmount, createTime, orderId (例如: "O1")
        
            关系及方向:
            - (u:User)-[:PLACED]->(o:Order)
            - (o:Order)-[:CONTAINS]->(p:Product)
            - (p:Product)-[:BELONGS_TO]->(c:Category)
            - (p:Product)-[:PRODUCED_BY]->(b:Brand)
        
            注意事项:
            1. 所有 ID 属性都是字符串形式,带前缀 (U, P, C, B, O)。
            2. [:CONTAINS] 关系上有属性 quantity ,用 c.quantity 引用,引用前先用 [c:CONTAINS] 给关系绑定变量,不能直接引用未定义的变量比如c.quantity 中的 c
               - 不要在 MATCH 中直接引用未定义变量
               - c.quantity 表示用户在订单中的购买数量,不是库存量!!!
               - 当统计品牌/品类销量或消费时,先用 [c:CONTAINS] 给关系绑定变量才能引用 c.quantity
               - 当用户问“买得最多/销量最高/最常买”时,必须使用 sum(c.quantity)。
            3. 关系方向必须严格遵守上面定义,绝不能写反。
            4. 每次返回商品信息时,同时显示品类和品牌,避免只显示“商品1”“商品2”。
            5. 当返回商品的品类或品牌时,先用 OPTIONAL MATCH 绑定节点,再用节点属性 .name 返回。
            """),
        ("human", "{query}"),
    ])

    #链接数据库
    graph = Neo4jGraph(
        url=os.getenv("NEO4J_URI"),
        username=os.getenv("NEO4J_USERNAME"),
        password=os.getenv("NEO4J_PASSWORD"),
        database="ecommerce" #要换成自己的数据库名
    )
    #创建一个链
    chain = GraphCypherQAChain.from_llm(
        llm=llm,
        graph=graph,
        allow_dangerous_requests=True,
        cypher_prompt=prompt,
        top_k=10,
        verbose=True, #日志用于调试使用 可以省略
    )
    rs = chain.invoke({"query": "用户2的品牌偏好"})
    print(rs)

if __name__ == '__main__':
    test1()

具体流程就是获取在线大模型,生成提示模版后创建链,然后对问题进行响应,运行结果如下:

最需要注意的就是提示词模版这一块了,自己写了才知道为什么会有专门的提示词工程师这个职位,这个提示词写得有一点不对或者不全面就会报错,而且就算写了有时候也会报同样的错,所以要不断调试才行。调试结果都没问题后就可以写工具了:

三、tool

这次一共有三个工具,分别是 email_tool、login_tool、neo4j_tool,下面分别按先后顺序介绍它们:

1. email_tool

 从发送验证码邮件的工具开始,也就是用户登录系统的第一种方式:邮箱验证码登录,具体代码如下:

import requests  # 用于发送HTTP请求到心知天气API
from dotenv import load_dotenv  # 用于加载环境变量文件(.env)
import os  # 用于访问操作系统环境变量
from langchain.tools import BaseTool  # LangChain的工具基类
from pydantic import BaseModel, Field, ConfigDict  # 数据验证和设置管理
from typing import Optional, Type, Any  # 类型注解
import os
from email.mime.text import MIMEText
import smtplib
from dotenv import load_dotenv

# 1先定义数据模型
class Email(BaseModel):
    #描述要准确
    to_email: str = Field(..., description="收件人的邮箱")
    subject: str = Field(..., description="邮箱的标题")
    content: str = Field(..., description="邮箱的内容")

#2 定义配置管理类 表示模型字段的配置 智能体工具类
class EmailTool(BaseTool):
    #arbitrary_types_allowed=True:允许数据类型采用Python的任意数据类型
    model_config = ConfigDict(arbitrary_types_allowed=True)

    #定义一个初始化方法
    def __init__(self, **kwargs: Any):
        super().__init__(
            name="get_email_tool",
            description = "用于发送邮件信息的,输入的参数应该是收件人的邮箱、邮件标题和邮件内容",
            **kwargs
        )
    #定义智能体的工具
    args_schema: Type[BaseTool] = Email

    # 定义执行方法
    def _run(self, to_email: str, subject: str, content: str) -> str:
        load_dotenv()
        # 创建邮件对象 content是邮件内容
        msg = MIMEText(content)
        # 收件人邮箱
        msg['To'] = to_email
        ##发件人邮箱
        msg['From'] = os.getenv("email_user")
        # 邮件主题
        msg['Subject'] = subject
        # 捕获异常
        try:
            # 创建SMTP对象
            smtp = smtplib.SMTP_SSL(host=os.getenv("email_host"), port=465)
            # 登录邮箱
            smtp.login(os.getenv("email_user"), os.getenv("email_password"))
            # 发送邮件 参数分别是发件人,收件人,邮件内容类型转换
            smtp.sendmail(os.getenv("email_user"), to_email, msg.as_string())
            # print("邮件发送成功!")
            #一定要加返回值
            return "邮件发送成功!"
        except Exception as e:
            print(e)
            print("邮件发送失败")

首先先描述输入参数,第一个Email类就是告诉智能体发送邮件时需要哪些参数,每个参数什么意思;第二个EmailTool 就是继承了BaseTool 的工具类;第三个 _run() 就是核心方法了,这里面就要写具体发送邮件的流程,可以先单独写一个测试文件看是否能成功运行,然后再粘过来,注意这里的环境变量要配好。下一步就是登录系统的工具,代码流程都跟这个雷同:

2. login_tool

这个工具就是用于校验用户的第二种登录方式:用户名加密码登录,实现流程是用户输入用户名和密码后,工具会去数据库表里查询是否存在,背后靠的是 Langchain 的SOL查询链,也就是用大模型生成 SQL 语句然后执行数据库查询,代码如下:

from langchain.tools import BaseTool
from pydantic import BaseModel, Field, ConfigDict
from typing import Any, Type, Optional
from dotenv import load_dotenv
import os
from model.my_chat_model import ChatModel
from langchain.chains import create_sql_query_chain
from langchain_community.utilities import SQLDatabase

#定义输入参数的数据模型类
class LoginInput(BaseModel):
    name: str = Field(..., description="用户名")
    password: str = Field(..., description="密码")


#定义工具类
class LoginTool(BaseTool):
    #定义模型是否允许输入参数
    model_config = ConfigDict(arbitrary_types_allowed=Type)
    #初始化
    def __init__(self, **kwargs):
        super().__init__(
            name = "get_login_tool",
            description = "主要用于完成系统的登录功能,必须输入用户名和密码,且都与数据库里的匹配才行",
            **kwargs
        )
    #定义工具参数
    args_schema : Type[BaseModel] = LoginInput

    def _run(self, name: str, password: str):
        # 获取大模型
        chat = ChatModel()
        llm = chat.get_online_model()
        # 创建数据库链接
        db = SQLDatabase.from_uri(
            "mysql+pymysql://root:root@localhost:3306/0908",
            include_tables=["user_info"],
        )
        # 创建 SQL查询链
        chain = create_sql_query_chain(llm, db)
        # 提问(让大模型生成SQL)
        question = f"请根据用户名是{name}和密码是{password}来查询信息"
        sql = chain.invoke({"question": question})
        print(sql)

        # 格式化SQL语句
        if "```sql" in sql:
            sql = sql.split("```sql")[1].split("```")[0]
        # print(sql)
        
        #执行查询并返回结果
        rs = db.run(sql)
        print(rs)
        return rs

同样,_run() 方法可以先用测试文件测试:

查询成功后就直接粘过去就行,到这里其实就可以写智能体了,因为已经有了验证码+登录的完整流程,但是因为是按文件夹顺序来的,所以还是先把tool写完吧,最后一个是 neo4j_tool:

 3. neo4j_tool

这个工具是知识图谱的专用工具,实现流程就是 用户提问-> 工具生成Cypher查询 -> 执行Neo4j数据库 -> 返回查询结果,代码如下:

from langchain.tools import BaseTool
from pydantic import BaseModel, Field, ConfigDict
from typing import Any, Type, Optional
from dotenv import load_dotenv
import os
from model.my_chat_model import ChatModel
from langchain_neo4j import GraphCypherQAChain, Neo4jGraph
from langchain_core.prompts import ChatPromptTemplate

#定义输入参数的数据模型类
class Neo4jInput(BaseModel):
    question: str = Field(..., description="问题")

#定义工具类
class Neo4jTool(BaseTool):
    #定义模型是否允许输入参数
    model_config = ConfigDict(arbitrary_types_allowed=Type)
    #初始化
    def __init__(self, **kwargs):
        super().__init__(
            name = "get_neo4j_tool",
            description = "用于查询电商销售知识图谱的数据,主要关于用户/商品/品牌/品类,必须输入问题",
            **kwargs
        )
    #定义工具参数
    args_schema : Type[BaseModel] = Neo4jInput
    #定义工具方法
    def _run(self, question: str):
        #加载环境变量
        load_dotenv()
        #获取模型
        chat = ChatModel()
        llm = chat.get_online_model()
        # 连接图形数据库
        graph = Neo4jGraph(
            url=os.getenv("NEO4J_URI"),
            username=os.getenv("NEO4J_USERNAME"),
            password=os.getenv("NEO4J_PASSWORD"),
            database="ecommerce" #换成自己的数据库名
        )
        #创建提示模型
        # 这里的system描述很重要
        prompt = ChatPromptTemplate.from_messages([
            ("system", """你是一个非常专业的Neo4j Cypher查询生成器,现在有一个包含用户的订单历史记录数据的知识图谱,
                    根据用户的问题生成合适的Cypher查询。生成查询时请严格遵循以下规则:

                    数据库模式:
                    - User节点属性: name, userId (例如: "U1", "U2")
                    - Product节点属性: productId (例如: "P1"), price, name
                    - Category节点属性: name, categoryId (例如: "C1")
                    - Brand节点属性: brandId (例如: "B1"), name
                    - Order节点属性: totalAmount, createTime, orderId (例如: "O1")

                    关系及方向:
                    - (u:User)-[:PLACED]->(o:Order)
                    - (o:Order)-[:CONTAINS]->(p:Product)
                    - (p:Product)-[:BELONGS_TO]->(c:Category)
                    - (p:Product)-[:PRODUCED_BY]->(b:Brand)

                    注意事项:
                    1. 所有 ID 属性都是字符串形式,带前缀 (U, P, C, B, O)。
                    2. [:CONTAINS] 关系上有属性 quantity ,用 c.quantity 引用,引用前先用 [c:CONTAINS] 给关系绑定变量,不能直接引用未定义的变量比如c.quantity 中的 c
                       - 不要在 MATCH 中直接引用未定义变量
                       - c.quantity 表示用户在订单中的购买数量,不是库存量!!!
                       - 当统计品牌/品类销量或消费时,先用 [c:CONTAINS] 给关系绑定变量才能引用 c.quantity
                       - 当用户问“买得最多/销量最高/最常买”时,必须使用 sum(c.quantity)
                    3. 关系方向必须严格遵守上面定义,绝不能写反。
                    4. [:CONTAINS] 关系 和 Category节点的变量不能都是相同的c
                    5. 每次返回商品信息时,同时显示品类和品牌,避免只显示“商品1”“商品2”。
                    6. 当返回商品的品类或品牌时,先用 OPTIONAL MATCH 绑定节点,再用节点属性 .name 返回。
                    7. OPTIONAL MATCH 语句必须放在 RETURN 之前,不能嵌套在 RETURN 中
                    """),
            ("human", "{query}"),
        ])
        # 创建链
        chain = GraphCypherQAChain.from_llm(
            llm=llm,
            graph=graph,
            allow_dangerous_requests=True,
            cypher_prompt=prompt,
            top_k=10,
            verbose=True,  # 日志用于调试使用 可以省略
        )
        #提问
        rs = chain.invoke({"query": question})
        #返回答案
        return rs["result"]

前面都差不多,后面的 _run() 方法就是一开始 Langchain 与 Neo4j 结合测试的代码,直接粘过来就是。

现在 tool 层写完了,每一个 tool 就像是一把专用的螺丝刀,用于完成特定的小任务,例如发送邮件登录、验证用户登录、查询知识图谱,它们轻量灵活,而接下来的service 层则是将这些基础工具进行整合和封装,形成一整套可复用的业务处理流程,就像一个已经配备好螺丝刀、电钻、手电筒的工具箱,方便后面的 agent 直接调用,下面开始:

四、service

1. login_service

封装所有的登录业务细节,不管是用户密码登录还是邮箱发送验证码登录,代码如下:

# service/login_service.py
from tool.login_tool import LoginTool
from tool.email_tool import EmailTool
from model.my_chat_model import ChatModel
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate

class LoginService:
    def __init__(self):
        self.chat = ChatModel()
        self.llm = self.chat.get_online_model()
        self.tools = [LoginTool(), EmailTool()]

        self.prompt = ChatPromptTemplate.from_messages([
            ("system", """
               你是一个智能的查询助手,你可以使用以下工具:
                    1. get_login_tool: 完成系统登录,用户名和密码必须正确
                    2. get_email_tool: 发送邮件信息
               根据用户需求智能选择工具:
                    - 用户名密码登录 → 执行 get_login_tool
                    - 发送验证码邮件 → 执行 get_email_tool
            """),
            ("human", "{input}"),
            ("placeholder", "{agent_scratchpad}")  # 工具调用
        ])

    def process_request(self, question: str):
        agent = create_tool_calling_agent(self.llm, self.tools, self.prompt)
        agent_executor = AgentExecutor(
            agent=agent,
            tools=self.tools,
            verbose=True
        )
        rs = agent_executor.invoke({"input": question})
        return rs["output"]

解释一下,这个代码文件名叫 login_service,封装了一个服务类叫 LoginService,它对外提供的方法是 process_request,调用这个接口就可以直接登录系统

2. rag_neo4j_service

用于封装 Langchain 与 Neo4j 的连接与对话链创建过程,后面的的 chat_agent 只需调用这个 service,就能直接获得一个可用的知识图谱问答链,而不必关心底层的配置与初始化细节,具体代码如下:

import os
from dotenv import load_dotenv
from langchain.chains import GraphCypherQAChain
from langchain_community.graphs import Neo4jGraph
from langchain_community.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from model.my_chat_model import ChatModel

import warnings
from langchain_core._api import LangChainDeprecationWarning
warnings.filterwarnings("ignore", category=LangChainDeprecationWarning)

class Ragneo4jservice:
    #数据初始化
    def __init__(self):
        #加载环境变量
        load_dotenv()
        #初始化模型
        self.chat=ChatModel()
        self.llm = self.chat.get_online_model()
        #初始化图形数据库
        self.graph = Neo4jGraph(
            url=os.getenv("NEO4J_URI"),
            username=os.getenv("NEO4J_USERNAME"),
            password=os.getenv("NEO4J_PASSWORD"),
            database="ecommerce" #要换成自己的数据库名
        )

    #创建一个对话链
    def create_chain(self):
        # 这里的system描述很重要
        prompt = ChatPromptTemplate.from_messages([
            ("system", """你是一个专业的Neo4j Cypher查询生成器,现在有一个关于用户的历史购买记录的知识图谱,
                    根据用户的问题生成合适的Cypher查询。生成查询时请严格遵循以下规则:

                    数据库模式:
                    - User节点属性: name, userId (例如: "U1", "U2")
                    - Product节点属性: productId (例如: "P1"), price, name
                    - Category节点属性: name, categoryId (例如: "C1")
                    - Brand节点属性: brandId (例如: "B1"), name
                    - Order节点属性: totalAmount, createTime, orderId (例如: "O1")

                    关系及方向:
                    - (u:User)-[:PLACED]->(o:Order)
                    - (o:Order)-[:CONTAINS]->(p:Product)
                    - (p:Product)-[:BELONGS_TO]->(c:Category)
                    - (p:Product)-[:PRODUCED_BY]->(b:Brand)

                    注意事项:
                    1. 所有 ID 属性都是字符串形式,带前缀 (U, P, C, B, O)。
                    2. [:CONTAINS] 关系上有属性 quantity表示购买数量,用 c.quantity 引用,
                       - 当统计品牌/品类销量或消费时,先用 [c:CONTAINS] 给关系绑定变量。
                       - 当用户问“买得最多/销量最高/最常买”时,必须使用 sum(c.quantity)。
                       - 不要在 MATCH 中直接引用未定义变量
                    3. 关系方向必须严格遵守上面定义,绝不能写反。
                    4. 每次返回商品信息时,同时显示品类和品牌,避免只显示“商品1”“商品2”。
                    5. 当返回商品的品类或品牌时,先用 OPTIONAL MATCH 绑定节点,再用节点属性 .name 返回。
                    """),
            ("human", "{query}"),
        ])
        #创建LangChain 的 Neo4j 问答链
        chain = GraphCypherQAChain.from_llm(
            llm=self.llm,
            graph=self.graph,
            allow_dangerous_requests=True,
            cypher_prompt=prompt,
            top_k=10,
            verbose=True,  # 日志用于调试使用 可以省略
        )
        #返回链对象
        return chain

调用 Ragneo4jservice 类的 create_chain 接口就能得到一个完整的 Langchain问答链,也就是只需要:

service = Ragneo4jservice()
chain = service.create_chain()
answer = chain.invoke({"query": "用户U1最喜欢的品牌是什么?"})

3. chat_service

 这里主要是实现大模型既能基于知识图谱做专业回答,又能进行普通的聊天问答,代码如下:

# service/chat_service.py
import os
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
from model.my_chat_model import ChatModel
from tool.neo4j_tool import Neo4jTool
from tool.document_tool import DocumentTool  # 可选


class ChatService:
    def __init__(self):
        # 初始化大模型
        self.chat = ChatModel()
        self.llm = self.chat.get_online_model()

        # 初始化工具(Neo4jTool 内部会用 Ragneo4jservice)
        self.tools = [Neo4jTool(), DocumentTool()]

        # 提示模版
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", """
            你是一个非常智能的助手,可以回答普通问题,可以陪用户聊天,也可以调用以下工具:
              1. get_neo4j_tool: 查询知识图谱中的消费记录、商品、品牌、品类等相关问题。
              2. get_document_tool: 查询文档内容。

            使用规则:
            - 如果问题是图数据库相关的,必须调用 get_neo4j_tool。
            - 如果是文档类问题,调用 get_document_tool。
            - 如果是普通聊天或代码问题,不调用工具,直接回答。
            """),
            ("human", "{input}"),
            ("placeholder", "{agent_scratchpad}")
        ])

        # 构建智能体
        self.agent = create_tool_calling_agent(self.llm, self.tools, self.prompt)
        self.agent_executor = AgentExecutor(
            agent=self.agent,
            tools=self.tools,
            verbose=True
        )

    """智能体问答接口"""
    def answer_question(self, question: str):
        rs = self.agent_executor.invoke({"input": question})
        return rs["output"]

 虽说不一定必须要有 service 层,但是它会让agent写起来更干净更简单,如果直接让 agent 去写所有东西,比如大模型初始化、Neo4j 连接、prompt 构建,那 agent 的代码会变得很臃肿,而且逻辑耦合度会非常高,一旦 Neo4j 地址、数据库模式或 prompt 规则变化,那要去每个 agent 里改,很麻烦,所以为了代码结构清晰、维护方便、方便扩展,service 层几乎是必须的设计,尤其是项目复杂度提升的时候。

那么接下来就可以开始写 agent了:

五、agent

 agent 其实相当于经理,决定什么时候用工具,什么时候直接回答

这里我们设计了两种 agent ,一个是发送邮件,一个是用户密码登录,由于我们前面封装了service 类,所以这里的代码就很简短,直接跟着运行结果一起看:

1. login_agent

# agent/login_agent.py
from service.login_service import LoginService

def create_login(question):
    service = LoginService()
    return service.process_request(question)

if __name__ == '__main__':
    que = "请登录系统,用户名:老板, 密码:123456"
    print(create_login(que))

运行结果:

2. chat_agent

# agent/chat_agent.py
from service.chat_service import ChatService

def create_agent(question):
    service = ChatService()
    return service.answer_question(question)

if __name__ == '__main__':
    print(create_agent("用户1购买得最多的商品是什么"))   # 知识图谱问答
    # print(create_agent("请写一下Python的hello world程序"))  # 普通聊天

运行结果:

 这表明大模型可以基于不同的问题选择是否调用工具,如果未来的提示模版需要改动只需要改 service 里的代码就好,agent 只专注于调用业务服务,维护更简单。

到此,我们已经把 agent 层service 层 区分开了,agent 现在只负责调用,不再堆满大模型、工具、prompt 的细节,而 service 封装了所有业务逻辑(模型初始化、工具加载、提示词配置、执行调用),接下来就是 view层了,用来接收用户的输入和展示 agent 的输出,也就是构建前后端交互的接口,这里我们用 Django 来写。

六、view

view层的作用流程大概是这样:

接收用户的HTTP请求(前端传来的参数)-> 调用agent(通过service) -> 把结果包装成HTTP响应返回前端

所以说,view 就像一个前台,将用户的需求告诉经理(agent),经理选择是否需要调用专业人员(是否使用工具),最后把经理处理的结果用用户能听懂的话(json格式数据)解释给用户,具体代码如下:

1. login_view

import random

from django.shortcuts import HttpResponse
from agent.login_agent import create_login
import json

#账号登录接口
def login(request):
    if request.method == 'GET':
        name = request.GET.get("name")
        password = request.GET.get("password")
        #创建一个登录的智能体
        question = f"请登录系统,用户名:{name}, 密码是:{password}"
        answer = create_login(question)
        if "成功" in answer:
            data = {
                "code": 200,
                "msg": "登录成功"
            }
            return HttpResponse(json.dumps(data))
        else:
            data = {
                "code": 500,
                "msg": "登录失败"
            }
            return HttpResponse(json.dumps(data))

#发送邮箱验证码接口
def send_email(request):
    if request.method == 'GET':
        email = request.GET.get("email")
        #随机生成验证码
        code = str(random.randint(1000, 10000))
        question = f"给用户的邮箱{email}发送验证码邮件{code},邮件标题是知识图谱后台系统验证码登录"
        answer = create_login(question)
        if "成功" in answer:
            data = {
                "code": 200,
                "msg": "发送成功"
            }
            rs = HttpResponse(json.dumps(data))
            #设置cookie max_age表示生效时间,单位为秒
            rs.set_cookie("code", code, max_age=300)
            return rs
        else:
            data = {
                "code": 500,
                "msg": "发送失败"
            }
            return HttpResponse(json.dumps(data))

#验证码登录
def code_login(request):
    if request.method == 'GET':
        code = request.GET.get("code")
        #获取cookie
        cookie_code = request.COOKIES.get("code")
        if not cookie_code: #验证码不存在或失效
            data = {
                "code": 500,
                "msg": "验证码失效,请重新发送"
            }

            return HttpResponse(json.dumps(data))
        elif code == cookie_code:
            data = {
                "code": 200,
                "msg": "登录成功!"
            }
            return HttpResponse(json.dumps(data))

 这里面定义的三个函数就分别是三个接口,login 用于取出 name 和 password,丢给 agent 去跑后返回“登录成功”或“登录失败”;send_email 用于取出 email并生成随机验证码,然后调用 agent 让它去发邮件,同时把验证码存在 cookie,方便后续校验;code_login 用于将前端传来的验证码与cookie 里的验证码对比,同样返回“登录成功”或“登录失败”。

所以总结一句,view 层就是负责 “请求 → 调 agent → 返回响应” 的那一层,是真正的前后端交互接口,它自己不写复杂逻辑,逻辑都已经在 service/agent/tool 里拆好了。

2. chat_neo4j_view

 是同样的逻辑:

import json
from django.shortcuts import HttpResponse #响应库
from agent.chat_agent_simple import create_agent

#聊天
def chat(request):
    if request.method == "GET":
        #获取用户发送的问题
        question = request.GET.get("question")
        print(f"question={question}")
        try:
            #创建智能体
            answer = create_agent(question)

            # 设置响应数据格式
            data = {
                "code": 200,
                "msg": "success",
                "data": answer
            }
            return HttpResponse(json.dumps(data))

        except Exception as e:
            print(e)
            data = {
                "code": 200,
                "msg": "服务器错误",
                "data": "我不知道答案捏"
            }
            return HttpResponse(json.dumps(data))

那现在我们有了 chat、login、sendEmail、codeLogin 这些接口,接下来就可以开始前端了

七、前端

前端的话,我们主要做了几个页面,有登录 login、聊天问答 chat 、索引 index(不是重点),当然因为前端本身对我来说就不是重点,所以这里直接贴主要代码了:

1. login:

<template>
  <div class="login-one">

    <el-row>
      <el-col :span="8">&nbsp;</el-col>
      <el-col :span="8">
        <br><br><br><br><br><br><br><br><br>
        <!--        <h1 align="center" style=" font-family: Helvetica;">零售智控管理平台</h1>-->
        <h1 align="center"
            style="font-family: 'Roboto', sans-serif;
           font-weight: 700;
           font-size: 42px;
           color: #34495e;
           text-shadow: none;">
          零售智控管理平台
        </h1>

        <el-tabs type="border-card">
          <!--用户注册-->
          <el-tab-pane label="用户注册">
            <el-form label-width="80px" style="background-color: white;border-radius: 3%;padding-top: 5px">
              <el-form-item label="用户名">
                <el-col :span="20">
                  <el-input v-model="registerName"></el-input>
                </el-col>
              </el-form-item>

              <el-form-item label="密码">
                <el-col :span="20">
                  <el-input v-model="registerPassword" show-password></el-input>
                </el-col>
              </el-form-item>

              <el-form-item>
                <el-col :span="6">&nbsp;</el-col>
                <el-col :span="10">
                  <el-button type="primary" icon="el-icon-edit" size="mini" @click="register">注册</el-button>
                </el-col>
                <el-col :span="8">&nbsp;</el-col>
              </el-form-item>
            </el-form>
          </el-tab-pane>

          <!-- 用户登录 -->
          <el-tab-pane label="用户登录">
            <el-form label-width="80px" style="background-color: white;border-radius: 3%;padding-top: 5px">

              <el-form-item label="用户名">
                <el-col :span="20">
                  <el-input v-model="name"></el-input>
                </el-col>
              </el-form-item>

              <el-form-item label="密码">
                <el-col :span="20">
                  <el-input v-model="password" show-password></el-input>
                </el-col>
              </el-form-item>

              <el-form-item>
                <el-col :span="6">&nbsp;</el-col>
                <el-col :span="10">
                  <el-button type="success" icon="el-icon-s-custom" size="mini" @click="login">登录</el-button>
                </el-col>
                <el-col :span="8">&nbsp;</el-col>
              </el-form-item>
            </el-form>
          </el-tab-pane>

          <!-- 邮箱登录 -->
          <el-tab-pane label="邮箱登录">
            <el-form label-width="80px" style="background-color: white;border-radius: 3%;padding-top: 5px">


              <el-form-item label="邮箱">
                <el-col :span="20">
                  <el-input v-model="email"></el-input>
                </el-col>
              </el-form-item>

              <el-form-item label="验证码">
                <el-col :span="20">
                  <el-input v-model="code" show-password></el-input>
                </el-col>
              </el-form-item>

              <el-form-item>
                <el-col :span="6">&nbsp;</el-col>
                <el-col :span="10">
                  <el-button type="success" icon="el-icon-s-custom" size="mini" @click="send_code">发送验证码
                  </el-button>
                  <el-button type="success" icon="el-icon-s-custom" size="mini" @click="code_login">登录</el-button>
                </el-col>
                <el-col :span="8">&nbsp;</el-col>
              </el-form-item>
            </el-form>

          </el-tab-pane>


        </el-tabs>

      </el-col>
      <el-col :span="8">&nbsp;</el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  name: "Login",
  data() {
    return {
      name: "",
      password: "",
      email: "",
      code: "",
      registerName: "",
      registerPassword: ""

    }
  },
  methods: {
    login() {
      const self = this;
      this.$http.get("/api/login/",
        {params: {name: self.name, password: self.password}})
        .then(function (rs) {
          let code = 0;
          let msg = "";

          if (rs.data && typeof rs.data === "object" && "code" in rs.data && "msg" in rs.data) {
            // 正确登录返回对象
            code = rs.data.code;
            msg = rs.data.msg;
          } else if (typeof rs.data === "string") {
            // 错误登录返回字符串
            msg = rs.data;
            // 根据字符串关键字判断状态码
            if (msg.includes("成功")) {
              code = 200;
            } else {
              code = 500;
            }
          } else {
            msg = JSON.stringify(rs.data);
            code = 500;
          }

          if (code === 200) {
            self.$message.success(msg);
            self.$router.push("/index");
          } else {
            self.$message.error(msg);
          }
        })
        .catch(function (err) {
          console.log(err);  // 打印错误信息
          self.$message.error("请求失败:" + err);
        });
    },
    send_code() {
      const self = this;
      this.$http.get("/api/sendEmail/", {params: {"email": self.email}})
        .then(function (rs) {
          if (rs.data.code === 200) {
            self.$message(rs.data.msg);
            // //跳转到聊天页面
            // self.$router.push("/index");
          } else {
            self.$message(rs.data.msg);
          }
        })
    },
    code_login() {
      const self = this;
      this.$http.get("/api/codeLogin/", {params: {"code": self.code}})
        .then(function (rs) {
          if (rs.data.code === 200) {
            self.$message(rs.data.msg);
            //跳转到聊天页面
            self.$router.push("/index");
          } else {
            self.$message(rs.data.msg);
          }
        })

    },
    register() {
      const self = this;
      this.$http.get("/api/register/", {
        params: {
          name: self.registerName,
          password: self.registerPassword
        }
      })
        .then(function (rs) {
          // rs.data 是对象,不是字符串!
          let msg = rs.data.msg || "操作完成";  // 提取 msg 字段

          // 确保 msg 是字符串
          if (typeof msg !== 'string') {
            msg = String(msg);
          }

          // 现在 msg 是字符串,可以安全使用 includes
          if (msg.includes("成功")) {
            self.$message.success(msg);
            self.$router.push("/login");
          } else if (msg.includes("失败") || rs.data.code === 500) {
            self.$message.error(msg);
          } else {
            self.$message.info(msg);
          }
        })
        .catch(function (err) {
          self.$message.error("请求失败:" + err);
        });
    }


  }

}
</script>

<style scoped>
@import url('../../assets/css/login.css');


</style>

2. chat:

<template>
  <div class="chat-container">
    <!-- 顶部导航栏 -->
    <el-header class="chat-header">
      <div class="header-content">
        <div class="logo">

          <span>DeepSeek Chat</span>
        </div>
        <div class="header-actions">
          <el-button type="success" icon="el-icon-refresh" @click="resetConversation">新对话</el-button>

        </div>
      </div>
    </el-header>

    <!-- 主聊天区域 -->
    <el-main class="chat-main">
      <div class="message-container" ref="messageContainer">
        <!-- 欢迎消息 -->
        <div class="welcome-message" v-if="messages.length === 0">
          <h2>欢迎使用 DeepSeek Chat</h2>
          <p>我是您的AI助手,可以回答各种问题、帮助创作和提供建议</p>
          <div class="quick-questions">
            <el-button
              v-for="(question, index) in quickQuestions"
              :key="index"
              round
              @click="sendQuickQuestion(question)"
            >
              {{ question }}
            </el-button>
          </div>
        </div>

        <!-- 消息列表 -->
        <div
          v-for="(message, index) in messages"
          :key="index"
          class="message-item"
          :class="{'user-message': message.role === 'user', 'ai-message': message.role === 'assistant'}"
        >
          <div class="message-avatar">
            <img
              v-if="message.role === 'user'"
              src="../../assets/images/user.jpeg"
              alt="User"
            >
            <img
              v-else
              src="../../assets/images/bot.jpeg"
              alt="AI"
            >
          </div>
          <div class="message-content">
            <div class="message-text" v-html="formatMessage(message.content)"></div>
            <div class="message-actions">
              <el-button
                v-if="message.role === 'assistant'"
                type="text"
                icon="el-icon-copy-document"
                size="mini"
                @click="copyToClipboard(message.content)"
              >
                复制
              </el-button>
              <el-button
                type="text"
                icon="el-icon-thumb"
                size="mini"
                @click="rateMessage(index, 'like')"
                :class="{active: message.rating === 'like'}"
              >
                {{ message.likes || 0 }}
              </el-button>
              <el-button
                type="text"
                icon="el-icon-thumb"
                size="mini"
                @click="rateMessage(index, 'dislike')"
                :class="{active: message.rating === 'dislike'}"
              >
                {{ message.dislikes || 0 }}
              </el-button>
              <span class="message-time">{{ formatTime(message.timestamp) }}</span>
            </div>
          </div>
        </div>

        <!-- 加载指示器 -->
        <div class="loading-indicator" v-if="isLoading">

          <span>AI正在思考...</span>
        </div>
      </div>

    </el-main>

    <!-- 输入区域 -->
    <el-footer class="chat-footer">
      <div class="input-container">
        <el-input
          type="textarea"
          :rows="2"
          :autosize="{ minRows: 2, maxRows: 6 }"
          placeholder="输入您的问题..."
          v-model="inputMessage"
          @keyup.enter.native="sendMessage"
          :disabled="isLoading"
          ref="inputArea"
        >
        </el-input>
        <div class="input-actions">
          <el-button
            type="primary"
            :loading="isLoading"
            @click="sendMessage"
            :disabled="!inputMessage.trim()"
          >
            发送
          </el-button>
        </div>
      </div>
      <div class="footer-notice">
        <span>DeepSeek Chat 可能会产生不准确的信息,请谨慎验证</span>
      </div>
    </el-footer>

  </div>
</template>

<script>

import { marked } from 'marked'
import DOMPurify from 'dompurify'

export default {
  name: 'ChatPage',
  data() {
    return {
      messages: [],//聊天信息
      inputMessage: '',//输入框内容
      isLoading: false,//Ai是否正在加载和发送按钮状态
      showSettings: false, //设置面板,没有使用
      quickQuestions: [ //快捷问题
        "如何学习Vue.js?",
        "用户88购买得最多的商品前三",
        "销量前十的品牌",
        "解释一下量子计算的基本概念"
      ],
      responseLength: 3,
      enableWebSearch: false
    }
  },
  methods: {

    sendMessage() {//发送信息
      if (!this.inputMessage.trim() || this.isLoading) return //判断是否有值

      //构建用户问题的对象
      const userMessage = {
        role: 'user',
        content: this.inputMessage,
        timestamp: new Date()
      }
      //添加用户聊天信息
      this.messages.push(userMessage)
      //清空输入框
      this.inputMessage = ''
      //设置发送按钮是禁止和 AI加载状态为true
      this.isLoading = true

      // 滚动到底部
      this.$nextTick(() => {
        this.scrollToBottom()
      })
      const self = this;
      // this.$http.post("/api/user-info/chat",{"question":userMessage.content})
      this.$http.get("/api/chat/",{params:{"question":userMessage.content}})
        .then(function (rs){
          if(rs.data.code === 200){
            //构建AI回复的消息对象
            const aiMessage = {
              role: 'assistant',//表示AI回复
              content: rs.data.data,// 内容
              timestamp: new Date()
            }
            //添加AI回复到聊天记录里
            self.messages.push(aiMessage);
            //设置发送按钮是允许和 AI加载状态为false
            self.isLoading = false;

          }
        })

    },
    sendQuickQuestion(question) {
      this.inputMessage = question
      this.sendMessage()
    },
    resetConversation() {
      this.$confirm('确定要开始新的对话吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {

        this.messages = []
      })
    },
    scrollToBottom() {
      const container = this.$refs.messageContainer
      container.scrollTop = container.scrollHeight
    },
    formatMessage(content) {
      // 使用marked解析markdown并净化HTML
      return DOMPurify.sanitize(marked.parse(content || ''))
    },
    formatTime(timestamp) {
      return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
    },
    copyToClipboard(text) {
      navigator.clipboard.writeText(text).then(() => {
        this.$message.success('已复制到剪贴板')
      })
    },
    rateMessage(index, type) {
      const message = this.messages[index]

      if (type === 'like') {
        if (message.rating === 'like') {
          message.rating = null
          message.likes = (message.likes || 1) - 1
        } else {
          if (message.rating === 'dislike') {
            message.dislikes = (message.dislikes || 1) - 1
          }
          message.rating = 'like'
          message.likes = (message.likes || 0) + 1
        }
      } else {
        if (message.rating === 'dislike') {
          message.rating = null
          message.dislikes = (message.dislikes || 1) - 1
        } else {
          if (message.rating === 'like') {
            message.likes = (message.likes || 1) - 1
          }
          message.rating = 'dislike'
          message.dislikes = (message.dislikes || 0) + 1
        }
      }
    },
    focusInput() {
      this.$refs.inputArea.focus()
    }
  },
  mounted() {
    this.focusInput()
  }
}
</script>

<style scoped>
.chat-container {
  height: 100vh;
  display: flex;
  flex-direction: column;
  background-color: #f5f7fa;
}

.chat-header {
  background-color: #ffffff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  padding: 0 20px;
}

.header-content {
  height: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  max-width: 1200px;
  margin: 0 auto;
  width: 100%;
}

.logo {
  display: flex;
  align-items: center;
  font-size: 18px;
  font-weight: bold;
}

.logo img {
  margin-right: 10px;
  border-radius: 50%;
}

.header-actions .el-button {
  margin-left: 10px;
}

.chat-main {
  flex: 1;
  padding: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.message-container {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  max-width: 900px;
  margin: 0 auto;
  width: 100%;
}

.welcome-message {
  text-align: center;
  padding: 40px 0;
  color: #606266;
}

.welcome-message h2 {
  font-size: 24px;
  margin-bottom: 16px;
}

.welcome-message p {
  font-size: 16px;
  margin-bottom: 24px;
}

.quick-questions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 20px;
}

.quick-questions .el-button {
  margin: 0 5px 5px 0;
}

.message-item {
  display: flex;
  margin-bottom: 20px;
}

.message-avatar {
  margin-right: 15px;
}

.message-avatar img {
  width: 40px;
  height: 40px;
  border-radius: 50%;
}

.message-content {
  flex: 1;
  max-width: calc(100% - 55px);
}

.message-text {
  padding: 12px 16px;
  border-radius: 8px;
  line-height: 1.6;
  word-wrap: break-word;
}

.user-message .message-text {
  background-color: #e6f7ff;
  border: 1px solid #91d5ff;
}

.ai-message .message-text {
  background-color: #ffffff;
  border: 1px solid #ebeef5;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}

.message-actions {
  margin-top: 8px;
  display: flex;
  align-items: center;
  font-size: 12px;
  color: #909399;
}

.message-actions .el-button {
  padding: 0;
  margin-right: 10px;
  color: #909399;
}

.message-actions .el-button.active {
  color: #409eff;
}

.message-time {
  margin-left: auto;
}

.loading-indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 15px;
  color: #909399;
}

.loading-indicator .el-icon {
  margin-right: 8px;
  animation: rotating 2s linear infinite;
}

@keyframes rotating {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.chat-footer {
  padding: 0;
  background-color: #ffffff;
  box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.1);
}

.input-container {
  max-width: 700px;
  margin: 0 auto;
  padding: 15px;
  width: 100%;
}

.input-tools {
  margin-bottom: 5px;
}

.input-tools .el-button {
  padding: 0;
  margin-right: 10px;
}

.input-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 10px;
}

.footer-notice {
  text-align: center;
  padding: 10px;
  font-size: 12px;
  color: #909399;
  border-top: 1px solid #ebeef5;
}

/* Markdown内容样式 */
.message-text :deep(pre) {
  background-color: #f6f8fa;
  padding: 12px;
  border-radius: 6px;
  overflow-x: auto;
}

.message-text :deep(code) {
  background-color: #f6f8fa;
  padding: 2px 4px;
  border-radius: 3px;
  font-family: monospace;
}

.message-text :deep(blockquote) {
  border-left: 3px solid #dfe2e5;
  color: #6a737d;
  padding-left: 12px;
  margin-left: 0;
}

.message-text :deep(ul),
.message-text :deep(ol) {
  padding-left: 20px;
}

.message-text :deep(table) {
  border-collapse: collapse;
  width: 100%;
  margin: 12px 0;
}

.message-text :deep(th),
.message-text :deep(td) {
  border: 1px solid #dfe2e5;
  padding: 6px 13px;
}

.message-text :deep(th) {
  background-color: #f6f8fa;
}
</style>

整个 chat 文件夹长这样:

  

将前端与后端一起启动后,直接进入 chat 页面,开始聊天问答:

八、项目展示

 其实还应该有登录演示,但是这里就不用展示了,就是平常的密码登录或者邮箱验证码登录,下面直接演示聊天问答:

可以看到,普通的聊天和问答,以及基于知识库的问题大模型都可以做出回答,说明基本是没问题了。但其实后面我们还写了一个智能体是关于注册系统的,用户第一次登录系统的话,数据库里没有他的用户名和密码嘛,注册的时候就会调用智能体然后自动将数据插入到后台的数据库,这样第二次登录系统的时候就可以直接登录了。

以上就是我们整个项目的展示,后期加的一些功能虽然没有没有写上来,但是目前的也够用了,有问题可以指出 (๑•̀ㅂ•́)و✧

Logo

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

更多推荐