Langchain+Neo4j+Agent 的结合案例-电商销售
目录
项目简介
一、构建知识图谱:
二、 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 Neo4jGraphurl ="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 ChatModelimport 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] = LoginInputdef _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节点的变量不能都是相同的c5. 每次返回商品信息时,同时显示品类和品牌,避免只显示“商品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 ChatPromptTemplateclass 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 ChatModelimport 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 LoginServicedef 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 ChatServicedef 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 randomfrom 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 rselse:data = {"code": 500,"msg": "发送失败"}return HttpResponse(json.dumps(data))#验证码登录
def code_login(request):if request.method == 'GET':code = request.GET.get("code")#获取cookiecookie_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"> </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"> </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"> </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"> </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"> </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"> </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"> </el-col></el-form-item></el-form></el-tab-pane></el-tabs></el-col><el-col :span="8"> </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 是字符串,可以安全使用 includesif (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-buttonv-for="(question, index) in quickQuestions":key="index"round@click="sendQuickQuestion(question)">{{ question }}</el-button></div></div><!-- 消息列表 --><divv-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"><imgv-if="message.role === 'user'"src="../../assets/images/user.jpeg"alt="User"><imgv-elsesrc="../../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-buttonv-if="message.role === 'assistant'"type="text"icon="el-icon-copy-document"size="mini"@click="copyToClipboard(message.content)">复制</el-button><el-buttontype="text"icon="el-icon-thumb"size="mini"@click="rateMessage(index, 'like')":class="{active: message.rating === 'like'}">{{ message.likes || 0 }}</el-button><el-buttontype="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-inputtype="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-buttontype="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加载状态为truethis.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加载状态为falseself.isLoading = false;}})},sendQuickQuestion(question) {this.inputMessage = questionthis.sendMessage()},resetConversation() {this.$confirm('确定要开始新的对话吗?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {this.messages = []})},scrollToBottom() {const container = this.$refs.messageContainercontainer.scrollTop = container.scrollHeight},formatMessage(content) {// 使用marked解析markdown并净化HTMLreturn 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 = nullmessage.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 = nullmessage.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 页面,开始聊天问答:
八、项目展示
其实还应该有登录演示,但是这里就不用展示了,就是平常的密码登录或者邮箱验证码登录,下面直接演示聊天问答:
可以看到,普通的聊天和问答,以及基于知识库的问题大模型都可以做出回答,说明基本是没问题了。但其实后面我们还写了一个智能体是关于注册系统的,用户第一次登录系统的话,数据库里没有他的用户名和密码嘛,注册的时候就会调用智能体然后自动将数据插入到后台的数据库,这样第二次登录系统的时候就可以直接登录了。
以上就是我们整个项目的展示,后期加的一些功能虽然没有没有写上来,但是目前的也够用了,有问题可以指出 (๑•̀ㅂ•́)و✧