Langchain+Neo4j+Agent 的结合案例-电商销售
苯人这次的项目如标题所示,是将 Langchain+Neo4j+Agent 结合的案例,关于电商销售的。随着电商行业的不断发展,平台积累了庞大的用户、商品与交易数据。如何从这些复杂数据中快速挖掘有价值的信息,满足用户个性化的消费需求,已经成为一个重要课题。传统的数据库查询方式操作复杂,用户往往需要专业知识才能获取所需信息,缺乏灵活性与智能化。本项目基于,结合大语言模型的自然语言处理能力,构建了一个
目录
项目简介
苯人这次的项目如标题所示,是将 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"> </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 是字符串,可以安全使用 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 页面,开始聊天问答:
八、项目展示
其实还应该有登录演示,但是这里就不用展示了,就是平常的密码登录或者邮箱验证码登录,下面直接演示聊天问答:
可以看到,普通的聊天和问答,以及基于知识库的问题大模型都可以做出回答,说明基本是没问题了。但其实后面我们还写了一个智能体是关于注册系统的,用户第一次登录系统的话,数据库里没有他的用户名和密码嘛,注册的时候就会调用智能体然后自动将数据插入到后台的数据库,这样第二次登录系统的时候就可以直接登录了。
以上就是我们整个项目的展示,后期加的一些功能虽然没有没有写上来,但是目前的也够用了,有问题可以指出 (๑•̀ㅂ•́)و✧
更多推荐
所有评论(0)